001package arcommon;
002
003import java.text.DecimalFormat;
004import java.util.ArrayList;
005import java.util.Arrays;
006import java.util.Comparator;
007import java.util.Iterator;
008import java.util.LinkedHashMap;
009import java.util.List;
010import java.util.regex.Matcher;
011import java.util.regex.Pattern;
012import java.util.regex.PatternSyntaxException;
013
014import arcommon.exception.ExceptionUtil;
015
016public final class StringUtil {
017    public static final int STRING_SPLIT_INCLUDING_TRAILING_EMPTY_STRINGS = -1;
018    
019    public static String onlyFirstLinesOf(String string, int maximumNumberOfLines) {
020        int numberOfLines = StringUtil.numberOfCharsInString(string, '\n');
021        if (numberOfLines > maximumNumberOfLines) {
022            String[] split = string.split("\n");
023            string = "";
024            for (int i = 0; i < maximumNumberOfLines; i++) {
025                string += split[i] + "\n";
026            }
027            string += "... and " + (numberOfLines - maximumNumberOfLines) + " more";
028        }
029        return string;
030    }
031    
032    public static String indentLines(String string, String indent) {
033        String[] split = string.split("\n");
034        string = "";
035        for (int i = 0; i < split.length; i++) {
036            string += indent + split[i] + "\n";
037        }
038        return string;
039    }
040    
041    public static int indexOfOccurrence(String baseString, String search, int occurrenceNumber) {
042        if (occurrenceNumber <= 0) {
043            throw new IllegalArgumentException("occurrenceNumber <= 0");
044        }
045        int fromIndex = 0;
046        int index;
047        do {
048            index = baseString.indexOf(search, fromIndex);
049            if (index == -1) {
050                return -1;
051            }
052            fromIndex = index + 1;
053            occurrenceNumber--;
054        } while (occurrenceNumber > 0);
055        return index;
056    }
057
058    public static String replace(String base, String from, String to) {
059        return replace(base, from, to, true);
060    }
061
062    public static String replace(String base, String from, String to, boolean all) {
063        int index = 0;
064        StringBuilder result = new StringBuilder(base);
065        do {
066            index = result.indexOf(from, index);
067            if (index != -1) {
068                String temp1 = result.substring(0, index);
069                String temp2 = result.substring(index + from.length());
070                result = new StringBuilder(temp1).append(to).append(temp2);
071                index += to.length();
072            }
073        } while (index != -1 && all);
074        return result.toString();
075    }
076
077    private static final char[] FROM = {'\"'  ,'\\'   ,'\n'  ,'\r'  ,','   };
078    private static final String[] TO = {"\\\"", "\\\\", "\\n", "\\r", "\\,"};
079
080    public static void appendedSeparator(StringBuilder stringBuilder) {
081        if (stringBuilder.length() > 0 && stringBuilder.charAt(stringBuilder.length() - 1) != '\n') {
082            stringBuilder.append(',');
083        }
084    }
085
086    public static void appendedField(StringBuilder stringBuilder, String string) {
087        appendedSeparator(stringBuilder);
088        stringBuilder.append("\"");
089        if (string != null) {
090            wrapString(stringBuilder, string);
091        }
092        stringBuilder.append("\"");
093    }
094
095    public static void wrapString(StringBuilder stringBuilderOutput, String string) {
096        StringBuilder stringBuilder = new StringBuilder();
097        int offset = 0;
098        while (offset < string.length()) {
099            int lowestPosition = -1;
100            int foundIndex = -1;
101            for (int i = 0; i < FROM.length; i++) {
102                int position = string.indexOf(FROM[i], offset);
103                if (position != -1 && (lowestPosition > position || lowestPosition == -1)) {
104                    lowestPosition = position;
105                    foundIndex = i;
106                }
107            }
108            if (lowestPosition == -1) {
109                stringBuilder.append(string.substring(offset));
110                break;
111            }
112            stringBuilder.append(string.substring(offset, lowestPosition));
113            stringBuilder.append(TO[foundIndex]);
114            offset = lowestPosition + 1;
115        }
116        stringBuilderOutput.append(stringBuilder);
117    }
118
119    public static String unwrapString(String string) {
120        int offset = 0;
121        StringBuilder stringBuilder = new StringBuilder();
122        while (offset <= string.length()) {
123            int lowestPosition = -1;
124            int foundIndex = -1;
125            for (int i = 0; i < FROM.length; i++) {
126                int position = string.indexOf(TO[i], offset);
127                if (position != -1 && (lowestPosition > position || lowestPosition == -1)) {
128                    lowestPosition = position;
129                    foundIndex = i;
130                }
131            }
132            if (lowestPosition == -1) {
133                stringBuilder.append(string.substring(offset));
134                break;
135            }
136            stringBuilder.append(string.substring(offset, lowestPosition));
137            stringBuilder.append(FROM[foundIndex]);
138            offset = lowestPosition + 2;
139        }
140
141        return stringBuilder.toString();
142    }
143    
144    public static String javaConstantEncodeString(String s) {
145        return javaConstantEncodeString(s, false);
146    }
147    
148    public static String javaConstantEncodeString(String s, boolean forceAscii) {
149        StringBuilder stringBuilder = new StringBuilder();
150        javaConstantEncodeString(stringBuilder, s, forceAscii);
151        return stringBuilder.toString();
152    }
153    
154    public static void javaConstantEncodeString(StringBuilder stringBuilder, String string) {
155        javaConstantEncodeString(stringBuilder, string, false);
156    }
157    
158    public static void javaConstantEncodeString(StringBuilder stringBuilder, String string, boolean forceAscii) {
159        for (int i = 0; i < string.length(); i++) {
160            char c = string.charAt(i);
161            switch (c) {
162                // List is from https://codegym.cc/groups/posts/escaping-characters-java
163                case '\t' :
164                    stringBuilder.append("\\t");
165                break;
166                case '\b' :
167                    stringBuilder.append("\\b");
168                break;
169                case '\n' :
170                    stringBuilder.append("\\n");
171                break;
172                case '\r' :
173                    stringBuilder.append("\\r");
174                break;
175                case '\f' :
176                    stringBuilder.append("\\f");
177                break;
178                case '\'' :
179                    stringBuilder.append("\\\'");
180                break;
181                case '\"' :
182                    stringBuilder.append("\\\"");
183                break;
184                case '\\' :
185                    stringBuilder.append("\\\\");
186                break;
187                default :
188                    if (c < 32 || (forceAscii && c >= 128)) {
189                        stringBuilder.append("\\u");
190                        // Code copied from https://stackoverflow.com/questions/6230190/convert-international-string-to-u-codes-in-java
191                        // and modified a lot
192                        stringBuilder.append(String.format("%04X", (int)c));
193                    } else {
194                        stringBuilder.append(c);
195                    }
196                break;
197            }
198        }
199    }
200
201    public static String javaConstantDecodeString(String s) {
202        StringBuilder stringBuilder = new StringBuilder();
203        javaConstantDecodeString(stringBuilder, s);
204        return stringBuilder.toString();
205    }
206    
207    public static void javaConstantDecodeString(StringBuilder stringBuilder, String string) {
208        if (!string.contains("\\")) {
209            stringBuilder.append(string);
210            return ;
211        }
212        
213        for (int i = 0; i < string.length(); i++) {
214            char c = string.charAt(i);
215            if (c == '\\') {
216                c = string.charAt(++i);
217                switch (c) {
218                    case 't' :
219                        stringBuilder.append("\t");
220                    break;
221                    case 'b' :
222                        stringBuilder.append("\b");
223                    break;
224                    case 'n' :
225                        stringBuilder.append("\n");
226                    break;
227                    case 'r' :
228                        stringBuilder.append("\r");
229                    break;
230                    case 'f' :
231                        stringBuilder.append("\f");
232                    break;
233                    case '\\' :
234                        stringBuilder.append("\\");
235                    break;
236                    case '\"' :
237                        stringBuilder.append("\"");
238                    break;
239                    case '\'' :
240                        stringBuilder.append("'");
241                    break;
242                    case 'u' :
243                        String u = string.substring(i + 1, i + 5);
244                        i += 4;
245                        try {
246                            int uAsHex = Integer.parseInt(u, 16);
247                            stringBuilder.append((char)uAsHex);
248                        } catch (NumberFormatException e) {
249                            throw new RuntimeException(e);
250                        }
251                    break;
252                    default :
253                        int from = Math.max(0, i - 50);
254                        int to = Math.min(string.length(), i + 50);
255                        throw new RuntimeException("Invalid escape character: " + c + " at index: " + i + " in string.substring(): \"" + string.substring(from, to) + "\"");
256                }
257            } else {
258                stringBuilder.append(c);
259            }
260        }
261    }
262
263    public static void appendedField(StringBuilder stringBuilder, int number) {
264        appendedSeparator(stringBuilder);
265        stringBuilder.append(number);
266    }
267
268    public static void appendedField(StringBuilder stringBuilder, long number) {
269        appendedSeparator(stringBuilder);
270        stringBuilder.append(number);
271    }
272
273    public static void appendedField(StringBuilder stringBuilder, boolean b) {
274        appendedSeparator(stringBuilder);
275        stringBuilder.append(b);
276    }
277
278    public static class Fields {
279        private List<String> fields;
280        private int position;
281        private Fields(List<String> pfields) {
282            fields = pfields;
283        }
284
285        public String readString() {
286            return readString(position++);
287        }
288
289        private String readString(int index) {
290            return fields.get(index);
291        }
292
293        public int readInt() {
294            return stringToInt(readString());
295        }
296
297        public int readIntNoRemove(int index) {
298            return stringToInt(readString(index));
299        }
300
301        public boolean readBoolean() {
302            return readString().equals("true");
303        }
304    }
305
306    public static Fields splitStringAtCommaAsFields(String line) {
307        return splitStringAtCommaAsFields(line, true);
308    }
309
310    public static Fields splitStringAtCommaAsFields(String line, boolean safe) {
311        String[] fieldsArray = splitStringAtComma(line, safe);
312        List<String> fields = Arrays.asList(fieldsArray);
313        return new Fields(fields);
314    }
315
316    private static final int OUTSIDE = 0;
317    private static final int IN_FIELD = 1;
318    private static final int IN_STRING = 2;
319
320    public static String[] splitStringAtComma(String line, boolean safe) {
321        ArrayList<String> fields = new ArrayList<String>(40);
322        int mode = OUTSIDE;
323        int fieldStartPosition = 0;
324        boolean backslash = false;
325        StringBuilder stringBuilder = new StringBuilder();
326        for (int i = 0; i < line.length(); i++) {
327            char c = line.charAt(i);
328            switch (mode) {
329                case OUTSIDE :
330                    if (c == '\"') {
331                        mode = IN_STRING;
332                        fieldStartPosition = i + 1;
333                    } else {
334                        fieldStartPosition = i;
335                        mode = IN_FIELD;
336                    }
337                    break;
338                case IN_FIELD :
339                    if (c == ',') {
340                        StringBuilder s = new StringBuilder(line.substring(fieldStartPosition, i));
341                        fields.add(s.toString());
342                        mode = OUTSIDE;
343                    }
344                    break;
345                case IN_STRING :
346                    if (backslash) {
347                        backslash = false;
348                        switch (c) {
349                            case '\"' :
350                                stringBuilder.append('\"');
351                            break;
352                            case '\\' :
353                                stringBuilder.append('\\');
354                            break;
355                            case 'n' :
356                                stringBuilder.append('\n');
357                            break;
358                            case 'r' :
359                                stringBuilder.append('\r');
360                            break;
361                            case ',' :
362                                stringBuilder.append(',');
363                            break;
364                        }
365                    } else {
366                        if (c == '\\' && safe) {
367                            backslash = true;
368                        } else if (c == '\"') {
369                            fields.add(stringBuilder.toString());
370                            stringBuilder = new StringBuilder();
371                            i++;
372                            mode = OUTSIDE;
373                        } else {
374                            stringBuilder.append(c);
375                        }
376                    }
377                    break;
378                default :
379                    throw new RuntimeException("Internal error");
380//              break;
381            }
382        }
383        if (mode == IN_STRING) {
384            throw new RuntimeException("Internal error");
385        }
386        if (mode == IN_FIELD) {
387            fields.add(line.substring(fieldStartPosition, line.length()));
388        }
389        return fields.toArray(new String[fields.size()]);
390    }
391
392    public static boolean isStringAnInt(String text) {
393        boolean result = false;
394        try {
395            Integer.parseInt(text);
396            result = true;
397        } catch (Exception e) {
398            result = false;
399        }
400        return result;
401    }
402
403    public static boolean isStringALong(String text) {
404        boolean result = false;
405        try {
406            Long.parseLong(text);
407            result = true;
408        } catch (Exception e) {
409            result = false;
410        }
411        return result;
412    }
413
414    public static boolean isStringADouble(String text) {
415        boolean result = false;
416        try {
417            Double.parseDouble(text);
418            result = true;
419        } catch (Exception e) {
420            result = false;
421        }
422        return result;
423    }
424
425    public static byte stringToByte(String text) {
426        return stringToByte(text, true);
427    }
428
429    public static byte stringToByte(String text, boolean throwOnFail) {
430        byte result = 0;
431        try {
432            result = Byte.parseByte(text);
433        } catch (Exception e) {
434            if (throwOnFail) {
435                throw new RuntimeException("Text: \"" + text + "\"", e);
436            }
437            ExceptionUtil.printException(e);
438        }
439        return result;
440    }
441
442    public static int stringToInt(String text) {
443        return stringToInt(text, true);
444    }
445
446    public static int stringToInt(String text, boolean throwOnFail) {
447        int result = 0;
448        try {
449            result = Integer.parseInt(text);
450        } catch (Exception e) {
451            if (throwOnFail) {
452                throw new RuntimeException("Text: \"" + text + "\"", e);
453            }
454            ExceptionUtil.printException(e);
455        }
456        return result;
457    }
458
459    public static long stringToLong(String text) {
460        return stringToLong(text, true);
461    }
462
463    public static long stringToLong(String text, boolean throwOnFail) {
464        long result = 0;
465        try {
466            result = Long.parseLong(text);
467        } catch (Exception e) {
468            if (throwOnFail) {
469                throw new RuntimeException(text, e);
470            }
471            ExceptionUtil.printException(e);
472        }
473        return result;
474    }
475
476    public static float stringToFloat(String text) {
477        return stringToFloat(text, true);
478    }
479
480    public static float stringToFloat(String text, boolean throwOnFail) {
481        float result = 0.0F;
482        try {
483            text = text.replaceAll(",", ".");
484            result = Float.parseFloat(text);
485        } catch (Exception e) {
486            if (throwOnFail) {
487                throw new RuntimeException(text, e);
488            }
489            ExceptionUtil.printException(e);
490        }
491        return result;
492    }
493
494    public static double stringToDouble(String text) {
495        return stringToDouble(text, true);
496    }
497
498    /**
499     * Return the double value in the text parameter.
500     * <br>
501     * NOTE: This method can't handle strings containing thousands separator(s)
502     */
503    public static double stringToDouble(String text, boolean throwOnFail) {
504        double result = 0.0;
505        try {
506            text = text.replaceAll(",", ".");
507            result = Double.parseDouble(text);
508        } catch (Exception e) {
509            if (throwOnFail) {
510                throw new RuntimeException(text, e);
511            }
512            ExceptionUtil.printException(e);
513        }
514        return result;
515    }
516
517    public static String longToStringWithFixLengthWith0(long number, int length) {
518        return longToStringWithFixLength(number, length, '0');
519    }
520
521    public static String longToStringWithFixLengthWithSpace(long number, int length) {
522        return longToStringWithFixLength(number, length, ' ');
523    }
524
525    public static String longToStringWithFixLength(long number, int length, char fillChar) {
526        String result = "" + (number < 0 ? -number : number);
527        if (number < 0) {
528            result = "-" + result;
529        }
530        result = stringWithFixLength(result, length, fillChar);
531        return result;
532    }
533
534    private static DecimalFormat decimalFormat = new DecimalFormat();
535
536    public static String intToStringWithThousands(int number) {
537        return decimalFormat.format(number);
538    }
539
540    public static String intToStringWithThousands(long number) {
541        return decimalFormat.format(number);
542    }
543
544    private static String[] unit = new String[] {"", "K", "M", "G", "T"};
545
546    public static String longToStringWithUnit(long number) {
547        boolean negative = number < 0;
548        if (negative) {
549            number = -number;
550        }
551        int log10 = MathUtil.log10(number);
552        if (log10 > 0) {
553            log10--;
554        }
555        log10 = log10 / 3 * 3;
556        double result = number / Math.pow(10, log10);
557        if (result < 10) {
558            result = Math.round(result * 100) / 100.0D;
559        } else {
560            if (result < 100) {
561                result = Math.round(result * 10) / 10.0D;
562            } else {
563                result = Math.round(result);
564            }
565        }
566        int index = log10 / 3;
567        if (index >= unit.length) {
568            return (negative ? "-" : "") + decimalFormat.format(result) + "?";
569        }
570        return (negative ? "-" : "") + decimalFormat.format(result) + unit[index];
571    }
572
573    public static int numberOfCharsInString(String baseString, char searchChar) {
574        int result = 0;
575        int fromIndex = 0;
576        do {
577            int index = baseString.indexOf(searchChar, fromIndex);
578            if (index == -1) {
579                break;
580            }
581            fromIndex = index + 1;
582            result++;
583        } while (true);
584        return result;
585    }
586
587    public static String stringWithNumberOfChar(int number, char c) {
588        // String.repeat() is @since 11 so it can't be used here.
589        StringBuilder result = new StringBuilder();
590        for (int i = 0; i < number; i++) {
591            result.append(c);
592        }
593        return result.toString();
594    }
595
596    public static String stringWithFixLength(String string, int length, char fillChar) {
597        return stringWithFixLength(string, length, fillChar, STRING_WITH_FIX_LENGTH_ALIGN.ALIGN_RIGHT);
598    }
599
600    public static enum STRING_WITH_FIX_LENGTH_ALIGN {ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT};
601
602    public static String stringWithFixLength(String string, int length, char fillChar, STRING_WITH_FIX_LENGTH_ALIGN align) {
603        if (length <= 0) {
604            throw new IllegalArgumentException("length <= 0 " + length);
605        }
606        String result = string;
607        if (result.length() < length) {
608            switch (align) {
609                case ALIGN_LEFT :
610                    result = result + stringWithNumberOfChar(length - result.length(), fillChar);
611                break;
612                case ALIGN_CENTER :
613                    result = stringWithNumberOfChar((length - result.length() + 1) / 2, fillChar) + result + stringWithNumberOfChar((length - result.length()) / 2, fillChar);
614                break;
615                case ALIGN_RIGHT :
616                    result = stringWithNumberOfChar(length - result.length(), fillChar) + result;
617                break;
618                default :
619                    throw new RuntimeException("Internal error: Unknown align " + align);
620//              break;
621            }
622        }
623        return result;
624    }
625
626    private static final long MAXIMUM_LONG_LENGTH = MathUtil.log10(Long.MAX_VALUE);
627    private static Comparator<String> numericStringCompare;
628
629    public static final Comparator<String> getNumericStringCompare() {
630        if (numericStringCompare == null) {
631            numericStringCompare = new Comparator<String>() {
632                public int compare(String string1, String string2) {
633                    int position1 = 0;
634                    int position2 = 0;
635                    int tempPosition1 = 0;
636                    int tempPosition2 = 0;
637                    int length1 = string1.length();
638                    int length2 = string2.length();
639                    while (position1 < length1 && position2 < length2) {
640                        char c1 = string1.charAt(position1);
641                        char c2 = string2.charAt(position2);
642                        if ((Character.isDigit(c1) || c1 == ' ') && (Character.isDigit(c2) || c1 == ' ')) {
643                            tempPosition1 = position1;
644                            position1 = readNumberPosition(string1, position1, length1);
645                            tempPosition2 = position2;
646                            position2 = readNumberPosition(string2, position2, length2);
647                            long number1 = readNumberValue(string1, tempPosition1, position1, length1);
648                            long number2 = readNumberValue(string2, tempPosition2, position2, length2);
649                            if (number1 == Long.MIN_VALUE || number2 == Long.MIN_VALUE) {
650                                position1 = tempPosition1;
651                                position2 = tempPosition2;
652                            } else {
653                                if (number1 != number2) {
654                                    return MathUtil.signum(number1 - number2);
655                                }
656                                if (position1 >= length1 || position2 >= length2) {
657                                    break;
658                                }
659                                c1 = string1.charAt(position1);
660                                c2 = string2.charAt(position2);
661                            }
662                        }
663                        if (c1 != c2) {
664                            c1 = Character.toUpperCase(c1);
665                            c2 = Character.toUpperCase(c2);
666                            if (c1 != c2) {
667                                c1 = Character.toLowerCase(c1);
668                                c2 = Character.toLowerCase(c2);
669                                if (c1 != c2) {
670                                    return c1 - c2;
671                                }
672                            }
673                        }
674                        position1++;
675                        position2++;
676                    }
677                    return length1 - length2;
678                }
679            };
680        }
681        return numericStringCompare;
682    }
683    
684    private static int readNumberPosition(String string, int position, int length) {
685        char c;
686        c = string.charAt(position);
687        while (c == ' ') {
688            position++;
689            if (position >= length) {
690                break;
691            }
692            c = string.charAt(position);
693        }
694        while (Character.isDigit(c)) {
695            position++;
696            if (position >= length) {
697                break;
698            }
699            c = string.charAt(position);
700        }
701        return position;
702    }
703
704    private static long readNumberValue(String string, int fromPosition, int toPosition, int length) {
705        String tempString;
706        if (toPosition - fromPosition >= length) {
707            tempString = string.substring(fromPosition);
708        } else {
709            tempString = string.substring(fromPosition, toPosition);
710        }
711        tempString = tempString.trim();
712        if (tempString.length() == 0) {
713            return Long.MIN_VALUE;
714        }
715        if (tempString.length() >= MAXIMUM_LONG_LENGTH) {
716            return Long.MAX_VALUE;
717        }
718        long number = Long.parseLong(tempString);
719        return number;
720    }
721
722    public static final String DEFAULT_WILDCARD_SEPARATOR = ";";
723    public static final String DEFAULT_WILDCARD_NOT_PREFIX = "!";
724    public static final String DEFAULT_WILDCARD_REG_EXP_PREFIX = "~";
725    
726    public static String multiWildcardError(String wildcard) {
727        return multiWildcardError(wildcard, DEFAULT_WILDCARD_SEPARATOR, DEFAULT_WILDCARD_NOT_PREFIX, DEFAULT_WILDCARD_REG_EXP_PREFIX);
728    }
729        
730    public static String multiWildcardError(String wildcard, String separator, String notPrefix, String regExpPrefix) {
731        try {
732            doesStringMatchMultiWildcard("Dummy", wildcard, separator, notPrefix, regExpPrefix, false); // Hack
733            return null;
734        } catch (PatternSyntaxException e) {
735            return e.getMessage();
736        }
737    }
738
739    public static boolean doesStringMatchMultiWildcard(String string, String wildcard, boolean caseSensitive) {
740        return doesStringMatchMultiWildcard(string, wildcard, DEFAULT_WILDCARD_SEPARATOR, DEFAULT_WILDCARD_NOT_PREFIX, DEFAULT_WILDCARD_REG_EXP_PREFIX, caseSensitive);
741    }
742
743    public static boolean doesStringMatchMultiWildcard(String string, String wildcard, String separator, String notPrefix, String regExpPrefix, boolean caseSensitive) {
744        if (wildcard == null || wildcard.length() == 0) {
745            return true;
746        }
747        String[] wildcardSplit = wildcard.split(separator);
748        for (int i = 0; i < wildcardSplit.length; i++) {
749            boolean not = false;
750            if (wildcardSplit[i].startsWith(notPrefix)) {
751                wildcardSplit[i] = wildcardSplit[i].substring(notPrefix.length());
752                not = true;
753            }
754            if (wildcardSplit[i].startsWith(regExpPrefix)) {
755                String regExp = wildcardSplit[i].substring(regExpPrefix.length());
756                boolean match = doesStringMatchRegExp(string, regExp);
757                if (match != not) {
758                    return true;
759                }
760            } else {
761                if (doesStringMatchWildcard(string, wildcardSplit[i], caseSensitive) != not) {
762                    return true;
763                }
764            }
765        }
766        return false;
767    }
768    
769    public static final int DEFAULT_REG_EXP_FLAGS = Pattern.DOTALL | Pattern.MULTILINE;
770    
771    /**
772     * Returns the index of the first occurrence of regExp in string. Returns -1 if not found.
773     * <br>
774     * "Contains"<br>
775     * For "Equals" see {@link #doesStringMatchRegExp(String, String)}
776     */
777    public static int indexOfFirstRegExpMatch(String string, String regExp) {
778        return indexOfFirstRegExpMatch(string, regExp, DEFAULT_REG_EXP_FLAGS);
779    }
780    
781    public static int indexOfFirstRegExpMatch(String string, String regExp, int flags) {
782        Matcher matcher = getMatcherForRegExp(string, regExp, flags);
783        if (matcher.find()) {
784            return matcher.start();
785        }
786        return -1;
787    }
788    
789    public static String[] getMatchesForRegExp(String string, String regExp) {
790        return getMatchesForRegExp(string, regExp, DEFAULT_REG_EXP_FLAGS);
791    }
792
793    public static String[] getMatchesForRegExp(String string, String regExp, int flags) {
794        Matcher matcher = getMatcherForRegExp(string, regExp, flags);
795
796        ArrayList<String> matches = new ArrayList<String>();
797        while (matcher.find()) {
798            String match = matcher.group(0);
799            matches.add(match);
800        }
801        
802        return matches.toArray(new String[matches.size()]);
803    }
804    
805    /**
806     * Returns the result from calling {@link #getFirstGroupForRegExp(String, String, int)} with {@code DEFAULT_REG_EXP_FLAGS}.
807     * <br>
808     * See {@link #getFirstGroupForRegExp(String, String, int)} for details.
809     */
810    public static String getFirstGroupForRegExp(String string, String regExp) {
811        return getFirstGroupForRegExp(string, regExp, DEFAULT_REG_EXP_FLAGS);
812    }
813    
814    /**
815     * Returns the first group of the first match of the regular expression {@code regExp} in {@code string} using {@code flags}.
816     * <br>
817     * If no match or group is found null is returned.<br>
818     * NOTE: If more than one match or group is found an exception is thrown.
819     */
820    public static String getFirstGroupForRegExp(String string, String regExp, int flags) {
821        String[] groupsForRegExp = getGroupsForRegExpAsFlatArray(string, regExp, DEFAULT_REG_EXP_FLAGS);
822        if (groupsForRegExp.length > 1) {
823            throw new RuntimeException("groupsForRegExp.length > 1 (" + groupsForRegExp.length + ") for string \"" + string + "\", regExp: \"" + regExp + "\" and flags: " + flags);
824        }
825        if (groupsForRegExp.length == 0) {
826            return null;
827        }
828        return groupsForRegExp[0];
829    }
830    
831    public static String[] getGroupsForRegExpAsFlatArrayWithValidation(int expectedLength, String string, String regExp) {
832        return getGroupsForRegExpAsFlatArrayWithValidation(expectedLength, string, regExp, DEFAULT_REG_EXP_FLAGS);
833    }
834    
835    public static String[] getGroupsForRegExpAsFlatArrayWithValidation(int expectedLength, String string, String regExp, int flags) {
836        String[] groupsForRegExpAsFlatArray = getGroupsForRegExpAsFlatArray(string, regExp, flags);
837        if (groupsForRegExpAsFlatArray.length != expectedLength) {
838            throw new RuntimeException("groupsForRegExpAsFlatArray.length != " + expectedLength + " (" + groupsForRegExpAsFlatArray.length + " != " + expectedLength + ") for string: \"" + string + "\", regExp: \"" + regExp + "\" and flags: " + flags);
839        }
840        return groupsForRegExpAsFlatArray;
841    }
842
843    public static String[] getGroupsForRegExpAsFlatArray(String string, String regExp) {
844        return getGroupsForRegExpAsFlatArray(string, regExp, DEFAULT_REG_EXP_FLAGS);
845    }
846    
847    public static String[] getGroupsForRegExpAsFlatArray(String string, String regExp, int flags) {
848        Matcher matcher = getMatcherForRegExp(string, regExp, flags);
849
850        ArrayList<String> result = new ArrayList<String>();
851        while (matcher.find()) {
852            int groupCount = matcher.groupCount();
853            for (int i = 0; i < groupCount; i++) {
854                String group = matcher.group(i + 1);
855                result.add(group);
856            }
857        }
858        
859        return result.toArray(new String[result.size()]);
860    }
861    
862    public static String[][] getGroupsForRegExpAs2DimensionsArray(String string, String regExp) {
863        return getGroupsForRegExpAs2DimensionsArray(string, regExp, DEFAULT_REG_EXP_FLAGS);
864    }
865    
866    public static String[][] getGroupsForRegExpAs2DimensionsArray(String string, String regExp, int flags) {
867        Matcher matcher = getMatcherForRegExp(string, regExp, flags);
868
869        ArrayList<String[]> result = new ArrayList<String[]>();
870        while (matcher.find()) {
871            int groupCount = matcher.groupCount();
872            String[] array = new String[groupCount];
873            result.add(array);
874            for (int i = 0; i < groupCount; i++) {
875                String group = matcher.group(i + 1);
876                array[i] = group;
877            }
878        }
879        
880        return result.toArray(new String[result.size()][]);
881    }
882    
883    public static String getFirstNamedGroupForRegExp(String string, String regExp, String namedGroup) {
884        return getFirstNamedGroupForRegExp(string, regExp, namedGroup, DEFAULT_REG_EXP_FLAGS);
885    }
886    
887    public static String getFirstNamedGroupForRegExp(String string, String regExp, String namedGroup, int flags) {
888        Matcher matcher = StringUtil.getMatcherForRegExp(string, regExp);
889        if (matcher.matches()) {
890            return matcher.group(namedGroup);
891        }
892        return null;
893    }
894    
895    /**
896     * Returns true if the entire string matches regular expression in regExp. Otherwise it returns false.
897     * <br>
898     * "Equals"<br>
899     * For "Contains" see {@link #indexOfFirstRegExpMatch(String, String)}
900     */
901    public static boolean doesStringMatchRegExp(String string, String regExp) {
902        return doesStringMatchRegExp(string, regExp, DEFAULT_REG_EXP_FLAGS);
903    }
904
905    public static boolean doesStringMatchRegExp(String string, String regExp, int flags) {
906        Matcher matcher = getMatcherForRegExp(string, regExp);
907        return matcher.matches();
908    }
909
910    private static class RegExpFlag {
911        private String regExp;
912        private int flags;
913        
914        public RegExpFlag(String pregExp, int pflags) {
915            regExp = pregExp;
916            flags = pflags;
917        }
918        
919        @Override
920        public boolean equals(Object obj) {
921            if (!(obj instanceof RegExpFlag)) {
922                throw new RuntimeException("!(obj instanceof RegExpFlag) " + obj.getClass() + " " + obj);
923            }
924            RegExpFlag regExpFlag = (RegExpFlag)obj;
925            return regExpFlag.regExp.equals(regExp) && regExpFlag.flags == flags;
926        }
927        
928        @Override
929        public int hashCode() {
930            return regExp.hashCode();
931        }
932        
933        @Override
934        public String toString() {
935            return regExp + ", " + flags;
936        }
937    }
938   
939    public static boolean usePatternCache = true;
940    public static int patternCacheSize = 500;
941    private static LinkedHashMap<RegExpFlag, Pattern> regExpCache = new LinkedHashMap<RegExpFlag, Pattern>();
942    
943    public static Matcher getMatcherForRegExp(String string, String regExp) {
944        // See https://www.javamex.com/tutorials/regular_expressions/multiline.shtml
945        return getMatcherForRegExp(string, regExp, DEFAULT_REG_EXP_FLAGS);
946    }
947    
948    public static Matcher getMatcherForRegExp(String string, String regExp, int flags) {
949        Pattern pattern;
950        if (usePatternCache) {
951            synchronized (regExpCache) {
952                RegExpFlag regExpFlag = new RegExpFlag(regExp, flags);
953                pattern = regExpCache.get(regExpFlag);
954                if (pattern == null) {
955                    pattern = Pattern.compile(regExp, flags);
956                    regExpCache.put(regExpFlag, pattern);
957                    if (regExpCache.size() > patternCacheSize) {
958                        Iterator<RegExpFlag> iterator = regExpCache.keySet().iterator();
959                        iterator.next();
960                        iterator.remove();
961                    }
962                }
963            }
964        } else {
965            pattern = Pattern.compile(regExp, flags);
966        }
967        
968        Matcher matcher = pattern.matcher(string);
969        return matcher;
970    }
971    
972    public static void clearPatternCache() {
973        synchronized (regExpCache) {
974            regExpCache.clear();
975        }
976    }
977
978    public static boolean doesStringMatchWildcard(String string, String wildcard, boolean caseSensitive) {
979        if (wildcard.equals("") || wildcard.equals("*")) {
980            return true;
981        }
982        
983        if (!caseSensitive) {
984            string = string.toLowerCase();
985            wildcard = wildcard.toLowerCase();
986        }
987        
988        if (wildcard.indexOf('*') == -1) {
989            return string.equals(wildcard);
990        }
991        
992        while (true) {
993            boolean startsWithStar = false;
994            while (wildcard.length() > 0 && wildcard.charAt(0) == '*') {
995                startsWithStar = true;
996                wildcard = wildcard.substring(1);
997            }
998            int position = wildcard.indexOf('*');
999            String searchString;
1000            if (position == -1) {
1001                searchString = wildcard;
1002                wildcard = "";
1003            } else {
1004                searchString = wildcard.substring(0, position);
1005                wildcard = wildcard.substring(position);
1006            }
1007            if (wildcard.length() > 0) {
1008                if (startsWithStar) {
1009                    position = string.indexOf(searchString);
1010                    if (position != -1) {
1011                        string = string.substring(position + searchString.length());
1012                    } else {
1013                        return false;
1014                    }
1015                } else {
1016                    if (string.startsWith(searchString)) {
1017                        string = string.substring(searchString.length());
1018                    } else {
1019                        return false;
1020                    }
1021                }
1022            } else {
1023                if (startsWithStar) {
1024                    return string.endsWith(searchString);
1025                }
1026                return string.equals(searchString);
1027            }
1028        }
1029    }
1030
1031    public static String byteToBitString(byte number) {
1032        String result = "";
1033        long numberTemp;
1034        if (number < 0) {
1035            numberTemp = 0xFF + number + 1;
1036        } else {
1037            numberTemp = number;
1038        }
1039        long factor = 1;
1040        do {
1041            result = (numberTemp % (factor * 2)) / factor + result;
1042            factor *= 2;
1043        } while (factor < 256);
1044        return result;
1045    }
1046
1047    public static String intToBitString(int number) {
1048        return intToBitString(number, -1);
1049    }
1050    
1051    public static String intToBitString(int number, int length) {
1052        String result = "";
1053        long factor = 1;
1054        long numberTemp;
1055        if (number < 0) {
1056            numberTemp = 0x7fffffff + number + 1;
1057        } else {
1058            numberTemp = number;
1059        }
1060        if (length > 0) {
1061            for (int i = 0; i < length; i++) {
1062                result = (numberTemp % (factor * 2)) / factor + result;
1063                factor *= 2;
1064            }
1065        } else {
1066            do {
1067                result = (numberTemp % (factor * 2)) / factor + result;
1068                factor *= 2;
1069            } while (numberTemp > factor);
1070        }
1071        return result;
1072    }
1073
1074    public static int bitStringToInt(String bits) {
1075        int result = 0;
1076        for (int i = 0; i < bits.length(); i++) {
1077            if (bits.charAt(i) != '0' && bits.charAt(i) != '1') {
1078                throw new RuntimeException("Wrong char " + bits.charAt(i) + " i " + bits);
1079            }
1080            result = result * 2 + (bits.charAt(i) == '1' ? 1 : 0);
1081        }
1082        return result;
1083    }
1084    
1085    public static final char[] HEX = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
1086    
1087    /**
1088     * Return the number parameter as hex.
1089     * <br>
1090     * @see java.lang.Integer#toString(int, int) see java.lang.Integer#toString(int, 16)
1091     * @see java.lang.Integer#toHexString(int) see java.lang.Integer#toHexString(int) (dynamic length, lower case)
1092     * @see java.lang.Integer#parseInt(String, int) see java.lang.Integer#parseInt("00ff00", 16)
1093     * @see java.lang.String#format(String, Object...) see java.lang.String#format("%04X", Integer) (Always 4 in length, upper case. "%04x" will make it lower case.)
1094     */
1095    public static String toHex(int number) {
1096        return toHex(number, 0);
1097    }
1098    
1099    public static String toHex(int number, int minLength) {
1100        if (number < 0) {
1101            throw new RuntimeException("number < 0");
1102        }
1103        if (minLength < 0) {
1104            throw new RuntimeException("minLength < 0");
1105        }
1106        String result = "";
1107        while (number > 0) {
1108            result = HEX[number % 16] + result;
1109            number /= 16;
1110        }
1111        if (result.length() == 0) {
1112            result = "0";
1113        }
1114        
1115        if (minLength > 0) {
1116            result = StringUtil.stringWithFixLength(result, minLength, '0');
1117        }
1118        
1119        return result;
1120    }
1121
1122    public static char lastChar(String string) {
1123        return string.charAt(string.length() - 1);
1124    }
1125
1126    public static String firstCharUpperCase(String text) {
1127        if (text == null || text.length() == 0) {
1128            return text;
1129        }
1130        return text.substring(0, 1).toUpperCase() + text.substring(1);
1131    }
1132
1133    public static String toWordCase(String text) {
1134        int index = 0;
1135        String result = text;
1136        do {
1137            if (result.length() < index + 1) {
1138                return result;
1139            }
1140            String before = result.substring(0, index);
1141            String wordCaseChar = result.substring(index, index + 1).toUpperCase();
1142            String after = result.substring(index + 1);
1143            result = before + wordCaseChar + after;
1144            index = result.indexOf(' ', index + 1);
1145            if (index == -1) {
1146                break;
1147            }
1148            index++;
1149        } while (true);
1150        return result;
1151    }
1152
1153    public static String toSentenceCase(String text) {
1154        int index = 0;
1155        String result = text;
1156        do {
1157            String before = result.substring(0, index);
1158            String wordCaseChar = result.substring(index, index + 1).toUpperCase();
1159            String after = result.substring(index + 1);
1160            result = before + wordCaseChar + after;
1161            index = result.indexOf('.', index + 1);
1162            if (index == -1) {
1163                break;
1164            }
1165            index++;
1166            if (index + 1 >= result.length()) {
1167                return result;
1168            }
1169            
1170            while (result.charAt(index) == ' ') {
1171                index++;
1172                if (index + 1 >= result.length()) {
1173                    return result;
1174                }
1175            }
1176        } while (true);
1177        return result;
1178    }
1179
1180    public static String toCamelCase(String string) {
1181        String result = "";
1182        int lastIndex = 0;
1183        do {
1184            int index1 = string.indexOf('_', lastIndex);
1185            int index2 = string.indexOf(' ', lastIndex);
1186            if (index1 == -1 && index2 == -1) {
1187                result += string.substring(lastIndex).toLowerCase();
1188                break;
1189            }
1190            if (index1 == -1) {
1191                index1 = Integer.MAX_VALUE;
1192            }
1193            if (index2 == -1) {
1194                index2 = Integer.MAX_VALUE;
1195            }
1196            int index = Math.min(index1, index2);
1197            result += string.substring(lastIndex, index).toLowerCase();
1198            if (index + 1 >= string.length()) {
1199                return result;
1200            }
1201            result += Character.toUpperCase(string.charAt(index + 1));
1202            lastIndex = index + 2;
1203        } while (true);
1204        return result;
1205    }
1206
1207    public static String toUnderscoreCase(String string) {
1208        boolean lastWasLowerCaseChar = false;
1209        String result = "";
1210        for (int i = 0; i < string.length(); i++) {
1211            char c = string.charAt(i);
1212            if ((Character.isUpperCase(c) && lastWasLowerCaseChar) || c == ' ') {
1213                result += "_";
1214                if (c != ' ') {
1215                    result += Character.toLowerCase(c);
1216                    lastWasLowerCaseChar = true;
1217                } else {
1218                    lastWasLowerCaseChar = false;
1219                }
1220            } else {
1221                lastWasLowerCaseChar = Character.isLowerCase(c);
1222                result += c;
1223            }
1224        }
1225        return result;
1226    }
1227    
1228    /**
1229     * Returns if s1 and s2 are equal in any combination of null.
1230     */
1231    public static boolean stringEquals(String s1, String s2) {
1232        if (s1 == null || s2 == null) {
1233            return s1 == s2;
1234        }
1235        return s1.equals(s2);
1236    }
1237}