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' &amp; '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' &amp; '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}