001package jmri.util; 002 003import java.util.ArrayList; 004import java.util.List; 005import java.util.regex.Matcher; 006import java.util.regex.Pattern; 007import jmri.jmrit.symbolicprog.tabbedframe.PaneProgFrame; 008import org.slf4j.Logger; 009import org.slf4j.LoggerFactory; 010 011/** 012 * Common utility methods for working with CVs 013 * <p> 014 * We needed a place to refactor common CV-processing idioms in JMRI code, so 015 * this class was created. It's more of a library of procedures than a real 016 * class, as (so far) all of the operations have needed no state information. 017 * 018 * @author Bob Jacobsen Copyright (C) 2003 019 * @author Dave Heap Copyright (C) 2016 020 */ 021public class CvUtil { 022 023 /** 024 * 025 * 026 * @param cvString a string that may contain one <em><strong>and only 027 * one</strong></em> instance <em><strong>one</strong></em> 028 * of the following expandable forms; 029 * <br> 030 * (parentheses can be used to limit numeric boundaries and/or restrict the 031 * portions to be expanded): 032 * <ul> 033 * <li> A comma-separated list. Examples:<pre> 034 * "1,5,7" expands to [1, 5, 7] 035 * "16.3.25(1,2,5,7)" expands to [16.3.251, 16.3.252, 16.3.255, 16.3.257] 036 * </pre></li> 037 * <li> A hyphen-separated numeric range, in either direction. Examples: 038 * <pre> 039 * "16.3.25(1-7)" expands to [16.3.251, 16.3.252, 16.3.253, 16.3.254, 16.3.255, 16.3.256, 16.3.257] 040 * "16.3.2(53-48)" expands to [16.3.253, 16.3.252, 16.3.251, 16.3.250, 16.3.249, 16.3.248] 041 * "16.3(1-7).25" expands to [16.31.25, 16.32.25, 16.33.25, 16.34.25, 16.35.25, 16.36.25, 16.37.25] 042 * "98-103" expands to [98, 99, 100, 101, 102, 103] 043 * </pre></li> 044 * <li> A numeric starting value, followed by a colon and a count, in either 045 * direction. Examples: 046 * <pre> 047 * "25.3.250:4" expands to [25.3.250, 25.3.251, 25.3.252, 25.3.253] 048 * "25.3.250:-4" expands to [25.3.250, 25.3.249, 25.3.248, 25.3.247] 049 * </pre></li> 050 * </ul> 051 * 052 * @return A list of CVs produced by expanding the string 053 * <br><strong>or</strong><br> 054 * an empty list if nothing to expand. 055 */ 056 public static List<String> expandCvList(String cvString) { 057 List<String> ret = new ArrayList<>(); 058 Pattern pattern; 059 Matcher matcher; 060 String prefix = ""; 061 String theString = cvString; 062 String suffix = ""; 063 pattern = Pattern.compile("[(),\\-:]"); 064 matcher = pattern.matcher(theString); 065 if (matcher.find()) { 066 pattern = Pattern.compile("^([^(),\\-:]*?)\\(??([^()]*)\\)??([^(),\\-:]*?)$"); 067 matcher = pattern.matcher(theString); 068 if (matcher.find()) { 069 prefix = matcher.group(1); 070 theString = matcher.group(2); 071 suffix = matcher.group(3); 072 } 073 pattern = Pattern.compile("^([^(),\\-:]+?)(,[^(),\\-:]+?)+?$"); 074 matcher = pattern.matcher(theString); 075 if (matcher.find()) { 076 String[] theArray = theString.split(","); 077 for (int i = 0; i < theArray.length; i++) { 078 ret.add(prefix + theArray[i] + suffix); 079 } 080 return ret; 081 } 082 pattern = Pattern.compile("^([^(),\\-:]*?)(\\d+)-(\\d+)([^(),\\-:]*?)$"); 083 matcher = pattern.matcher(theString); 084 if (matcher.find()) { 085 String subPrefix = matcher.group(1); 086 int start = Integer.parseInt(matcher.group(2)); 087 int end = Integer.parseInt(matcher.group(3)); 088 int inc = 0; 089 String subSuffix = matcher.group(4); 090 if (start < end) { 091 inc = 1; 092 } else if (start > end) { 093 inc = -1; 094 } 095 int j = start; 096 do { 097 ret.add(prefix + subPrefix + j + subSuffix + suffix); 098 j = j + inc; 099 } while (j != (end + inc)); 100 return ret; 101 } 102 pattern = Pattern.compile("^([^(),\\-:]*?)(\\d+):(-?\\d+)([^(),\\-:]*?)$"); 103 matcher = pattern.matcher(theString); 104 if (matcher.find()) { 105 String subPrefix = matcher.group(1); 106 int start = Integer.parseInt(matcher.group(2)); 107 int count = Integer.parseInt(matcher.group(3)); 108 int inc = 0; 109 String subSuffix = matcher.group(4); 110 if (count > 0) { 111 inc = 1; 112 } else if (count < 0) { 113 inc = -1; 114 } 115 int j = start; 116 do { 117 ret.add(prefix + subPrefix + j + subSuffix + suffix); 118 j = j + inc; 119 } while (j != (start + count)); 120 return ret; 121 } 122 pattern = Pattern.compile("[(),\\-:]"); 123 matcher = pattern.matcher(theString); 124 if (!matcher.find()) { 125 ret.add(prefix + theString + suffix); 126 } else { 127 log.error("Invalid string '{}'", cvString); 128 } 129 } 130 return ret; 131 } 132 133 /** 134 * Optionally add CV numbers and bit numbers to tool tip text based on 135 * Roster Preferences setting. 136 * 137 * @param toolTip The tool tip text. It can be plain text or HTML 138 * format. 139 * @param cvDescription The CV description text. 140 * @param mask The bit mask, a (list of) string containing only the 141 * characters 'V' & 'X', with 'V' signifying a used 142 * bit. 143 * @return The original tool tip text plus (if the Roster Preferences allow) 144 * a parenthesized CV and bit mask description. 145 */ 146 public static String addCvDescription(String toolTip, String cvDescription, String mask) { 147 String descString = cvDescription; 148 String maskDescString = getMaskDescription(mask); 149 if (maskDescString.length() > 0 && !cvDescription.endsWith(".")) { 150 // skip overridden getCvDescription() that already includes maskDescription, eg SplitVariableValue 151 descString = descString + " " + maskDescString; 152 } 153 if (PaneProgFrame.getShowCvNumbers() && (descString != null)) { 154 if (toolTip == null || toolTip.length() < 1) { 155 toolTip = descString; 156 } else { 157 toolTip = StringUtil.concatTextHtmlAware(toolTip, " (" + descString + ")"); 158 } 159 } else if (toolTip == null) { 160 toolTip = ""; 161 162 } 163 return toolTip; 164 } 165 166 /** 167 * Generate bit numbers from a bit mask if applicable. 168 * 169 * @param mask A string containing only the characters 'V' & 'X', 170 * with 'V' signifying a used bit. 171 * @return A plain text description of the used bits. (For example, "bits 172 * 0-3,7" from the string "VXXXVVVV".) Empty String 173 * if not applicable 174 */ 175 public static String getMaskDescription(String mask) { 176 StringBuilder maskDescString = new StringBuilder(); 177 if ((mask != null) && (mask.contains("X"))) { 178 int lastBit = mask.length() - 1; 179 int lastV = -2; 180 if (mask.contains("V")) { 181 if (mask.indexOf('V') == mask.lastIndexOf('V')) { 182 maskDescString.append("bit ").append(lastBit - mask.indexOf('V')); 183 } else { 184 maskDescString.append("bits "); 185 for (int i = 0; i <= lastBit; i++) { 186 char descStringLastChar = maskDescString.charAt(maskDescString.length() - 1); 187 if (mask.charAt(lastBit - i) == 'V') { 188 if (descStringLastChar == ' ') { 189 maskDescString.append(i); 190 } else if (lastV == (i - 1)) { 191 if (descStringLastChar != '-') { 192 maskDescString.append("-"); 193 } 194 } else { 195 maskDescString.append(",").append(i); 196 } 197 lastV = i; 198 } 199 descStringLastChar = maskDescString.charAt(maskDescString.length() - 1); 200 if ((descStringLastChar == '-') && ((mask.charAt(lastBit - i) != 'V') || (i == lastBit))) { 201 maskDescString.append(lastV); 202 } 203 } 204 } 205 } else { 206 maskDescString.append("no bits"); 207 } 208 log.trace("{} Mask:{}", maskDescString, mask); 209 } 210 return maskDescString.toString(); 211 } 212 213 private final static Logger log = LoggerFactory.getLogger(CvUtil.class.getName()); 214 215}