eb99bbd45c57e7599be746264ab8b48b62bf4033
[idea/community.git] / platform / util / src / com / intellij / openapi / util / text / StringUtil.java
1 /*
2  * Copyright 2000-2013 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.openapi.util.text;
17
18 import com.intellij.CommonBundle;
19 import com.intellij.openapi.diagnostic.Logger;
20 import com.intellij.openapi.progress.ProcessCanceledException;
21 import com.intellij.openapi.util.Pair;
22 import com.intellij.openapi.util.TextRange;
23 import com.intellij.util.*;
24 import com.intellij.util.containers.ContainerUtil;
25 import com.intellij.util.text.CharArrayCharSequence;
26 import com.intellij.util.text.CharArrayUtil;
27 import com.intellij.util.text.StringFactory;
28 import org.jetbrains.annotations.Contract;
29 import org.jetbrains.annotations.NonNls;
30 import org.jetbrains.annotations.NotNull;
31 import org.jetbrains.annotations.Nullable;
32
33 import java.beans.Introspector;
34 import java.io.IOException;
35 import java.util.*;
36 import java.util.regex.Matcher;
37 import java.util.regex.Pattern;
38
39 //TeamCity inherits StringUtil: do not add private constructors!!!
40 @SuppressWarnings({"UtilityClassWithoutPrivateConstructor", "MethodOverridesStaticMethodOfSuperclass"})
41 public class StringUtil extends StringUtilRt {
42   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.util.text.StringUtil");
43
44   @NonNls private static final String VOWELS = "aeiouy";
45   @NonNls private static final Pattern EOL_SPLIT_PATTERN = Pattern.compile(" *(\r|\n|\r\n)+ *");
46   @NonNls private static final Pattern EOL_SPLIT_PATTERN_WITH_EMPTY = Pattern.compile(" *(\r|\n|\r\n) *");
47
48   public static final NotNullFunction<String, String> QUOTER = new NotNullFunction<String, String>() {
49     @Override
50     @NotNull
51     public String fun(String s) {
52       return "\"" + s + "\"";
53     }
54   };
55
56   public static final NotNullFunction<String, String> SINGLE_QUOTER = new NotNullFunction<String, String>() {
57     @Override
58     @NotNull
59     public String fun(String s) {
60       return "'" + s + "'";
61     }
62   };
63
64   @NotNull
65   public static List<String> getWordsInStringLongestFirst(@NotNull String find) {
66     List<String> words = getWordsIn(find);
67     // hope long words are rare
68     Collections.sort(words, new Comparator<String>() {
69       @Override
70       public int compare(@NotNull final String o1, @NotNull final String o2) {
71         return o2.length() - o1.length();
72       }
73     });
74     return words;
75   }
76
77   @NotNull
78   public static String escapePattern(@NotNull final String text) {
79     return replace(replace(text, "'", "''"), "{", "'{'");
80   }
81
82   @NotNull
83   public static <T> Function<T, String> createToStringFunction(@NotNull Class<T> cls) {
84     return new Function<T, String>() {
85       @Override
86       public String fun(@NotNull T o) {
87         return o.toString();
88       }
89     };
90   }
91
92   @NotNull
93   public static Function<String, String> TRIMMER = new Function<String, String>() {
94     @Nullable
95     @Override
96     public String fun(@Nullable String s) {
97       return s == null ? null : s.trim();
98     }
99   };
100
101   @NotNull
102   public static String replace(@NonNls @NotNull String text, @NonNls @NotNull String oldS, @NonNls @Nullable String newS) {
103     return replace(text, oldS, newS, false);
104   }
105
106   @NotNull
107   public static String replaceIgnoreCase(@NotNull String text, @NotNull String oldS, @Nullable String newS) {
108     return replace(text, oldS, newS, true);
109   }
110
111   public static void replaceChar(@NotNull char[] buffer, char oldChar, char newChar, int start, int end) {
112     for (int i = start; i < end; i++) {
113       char c = buffer[i];
114       if (c == oldChar) {
115         buffer[i] = newChar;
116       }
117     }
118   }
119
120   @NotNull
121   public static String replaceChar(@NotNull String buffer, char oldChar, char newChar) {
122     StringBuilder newBuffer = null;
123     for (int i = 0; i < buffer.length(); i++) {
124       char c = buffer.charAt(i);
125       if (c == oldChar) {
126         if (newBuffer == null) {
127           newBuffer = new StringBuilder(buffer.length());
128           newBuffer.append(buffer, 0, i);
129         }
130
131         newBuffer.append(newChar);
132       }
133       else if (newBuffer != null) {
134         newBuffer.append(c);
135       }
136     }
137     return newBuffer == null ? buffer : newBuffer.toString();
138   }
139
140   public static String replace(@NotNull final String text, @NotNull final String oldS, @Nullable final String newS, boolean ignoreCase) {
141     if (text.length() < oldS.length()) return text;
142
143     final String text1 = ignoreCase ? text.toLowerCase() : text;
144     final String oldS1 = ignoreCase ? oldS.toLowerCase() : oldS;
145     StringBuilder newText = null;
146     int i = 0;
147
148     while (i < text1.length()) {
149       int i1 = text1.indexOf(oldS1, i);
150       if (i1 < 0) {
151         if (i == 0) return text;
152         newText.append(text, i, text.length());
153         break;
154       }
155       else {
156         if (newS == null) return null;
157         if (newText == null) newText = new StringBuilder(text.length() - i);
158         newText.append(text, i, i1);
159         newText.append(newS);
160         i = i1 + oldS.length();
161       }
162     }
163     return newText != null ? newText.toString() : "";
164   }
165
166   /**
167    * Implementation copied from {@link String#indexOf(String, int)} except character comparisons made case insensitive
168    */
169   public static int indexOfIgnoreCase(@NotNull String where, @NotNull String what, int fromIndex) {
170     int targetCount = what.length();
171     int sourceCount = where.length();
172
173     if (fromIndex >= sourceCount) {
174       return targetCount == 0 ? sourceCount : -1;
175     }
176
177     if (fromIndex < 0) {
178       fromIndex = 0;
179     }
180
181     if (targetCount == 0) {
182       return fromIndex;
183     }
184
185     char first = what.charAt(0);
186     int max = sourceCount - targetCount;
187
188     for (int i = fromIndex; i <= max; i++) {
189       /* Look for first character. */
190       if (!charsEqualIgnoreCase(where.charAt(i), first)) {
191         while (++i <= max && !charsEqualIgnoreCase(where.charAt(i), first)) ;
192       }
193
194       /* Found first character, now look at the rest of v2 */
195       if (i <= max) {
196         int j = i + 1;
197         int end = j + targetCount - 1;
198         for (int k = 1; j < end && charsEqualIgnoreCase(where.charAt(j), what.charAt(k)); j++, k++) ;
199
200         if (j == end) {
201           /* Found whole string. */
202           return i;
203         }
204       }
205     }
206
207     return -1;
208   }
209
210   public static int indexOfIgnoreCase(@NotNull String where, char what, int fromIndex) {
211     int sourceCount = where.length();
212
213     if (fromIndex >= sourceCount) {
214       return -1;
215     }
216
217     if (fromIndex < 0) {
218       fromIndex = 0;
219     }
220
221     for (int i = fromIndex; i < sourceCount; i++) {
222       if (charsEqualIgnoreCase(where.charAt(i), what)) {
223         return i;
224       }
225     }
226
227     return -1;
228   }
229
230   public static boolean containsIgnoreCase(@NotNull String where, @NotNull String what) {
231     return indexOfIgnoreCase(where, what, 0) >= 0;
232   }
233
234   public static boolean endsWithIgnoreCase(@NonNls @NotNull String str, @NonNls @NotNull String suffix) {
235     final int stringLength = str.length();
236     final int suffixLength = suffix.length();
237     return stringLength >= suffixLength && str.regionMatches(true, stringLength - suffixLength, suffix, 0, suffixLength);
238   }
239
240   public static boolean startsWithIgnoreCase(@NonNls @NotNull String str, @NonNls @NotNull String prefix) {
241     return StringUtilRt.startsWithIgnoreCase(str, prefix);
242   }
243
244   public static String stripHtml(@NotNull String html, boolean convertBreaks) {
245     if (convertBreaks) {
246       html = html.replaceAll("<br/?>", "\n\n");
247     }
248
249     return html.replaceAll("<(.|\n)*?>", "");
250   }
251
252   public static String toLowerCase(@Nullable final String str) {
253     //noinspection ConstantConditions
254     return str == null ? null : str.toLowerCase();
255   }
256
257   @NotNull
258   public static String getPackageName(@NotNull String fqName) {
259     return getPackageName(fqName, '.');
260   }
261
262   @NotNull
263   public static String getPackageName(@NotNull String fqName, char separator) {
264     int lastPointIdx = fqName.lastIndexOf(separator);
265     if (lastPointIdx >= 0) {
266       return fqName.substring(0, lastPointIdx);
267     }
268     return "";
269   }
270
271   public static int getLineBreakCount(@NotNull CharSequence text) {
272     int count = 0;
273     for (int i = 0; i < text.length(); i++) {
274       char c = text.charAt(i);
275       if (c == '\n') {
276         count++;
277       }
278       else if (c == '\r') {
279         if (i + 1 < text.length() && text.charAt(i + 1) == '\n') {
280           //noinspection AssignmentToForLoopParameter
281           i++;
282           count++;
283         }
284         else {
285           count++;
286         }
287       }
288     }
289     return count;
290   }
291
292   public static boolean containsLineBreak(@NotNull CharSequence text) {
293     for (int i = 0; i < text.length(); i++) {
294       char c = text.charAt(i);
295       if (isLineBreak(c)) return true;
296     }
297     return false;
298   }
299
300   public static boolean isLineBreak(char c) {
301     return c == '\n' || c == '\r';
302   }
303
304   @NotNull
305   public static String escapeLineBreak(@NotNull String text) {
306     StringBuilder buffer = new StringBuilder(text.length());
307     for (int i = 0; i < text.length(); i++) {
308       char c = text.charAt(i);
309       switch (c) {
310         case '\n': buffer.append("\\n"); break;
311         case '\r': buffer.append("\\r"); break;
312         default: buffer.append(c);
313       }
314     }
315     return buffer.toString();
316   }
317
318   public static boolean endsWithLineBreak(@NotNull CharSequence text) {
319     int len = text.length();
320     return len > 0 && isLineBreak(text.charAt(len - 1));
321   }
322
323   public static int lineColToOffset(@NotNull CharSequence text, int line, int col) {
324     int curLine = 0;
325     int offset = 0;
326     while (line != curLine) {
327       if (offset == text.length()) return -1;
328       char c = text.charAt(offset);
329       if (c == '\n') {
330         curLine++;
331       }
332       else if (c == '\r') {
333         curLine++;
334         if (offset < text.length() - 1 && text.charAt(offset + 1) == '\n') {
335           offset++;
336         }
337       }
338       offset++;
339     }
340     return offset + col;
341   }
342
343   public static int offsetToLineNumber(@NotNull CharSequence text, int offset) {
344     int curLine = 0;
345     int curOffset = 0;
346     while (curOffset < offset) {
347       if (curOffset == text.length()) return -1;
348       char c = text.charAt(curOffset);
349       if (c == '\n') {
350         curLine++;
351       }
352       else if (c == '\r') {
353         curLine++;
354         if (curOffset < text.length() - 1 && text.charAt(curOffset + 1) == '\n') {
355           curOffset++;
356         }
357       }
358       curOffset++;
359     }
360     return curLine;
361   }
362
363   /**
364    * Classic dynamic programming algorithm for string differences.
365    */
366   public static int difference(@NotNull String s1, @NotNull String s2) {
367     int[][] a = new int[s1.length()][s2.length()];
368
369     for (int i = 0; i < s1.length(); i++) {
370       a[i][0] = i;
371     }
372
373     for (int j = 0; j < s2.length(); j++) {
374       a[0][j] = j;
375     }
376
377     for (int i = 1; i < s1.length(); i++) {
378       for (int j = 1; j < s2.length(); j++) {
379
380         a[i][j] = Math.min(Math.min(a[i - 1][j - 1] + (s1.charAt(i) == s2.charAt(j) ? 0 : 1), a[i - 1][j] + 1), a[i][j - 1] + 1);
381       }
382     }
383
384     return a[s1.length() - 1][s2.length() - 1];
385   }
386
387   @NotNull
388   public static String wordsToBeginFromUpperCase(@NotNull String s) {
389     return toTitleCase(s, ourPrepositions);
390   }
391
392   @NotNull
393   public static String toTitleCase(@NotNull String s) {
394     return toTitleCase(s, ArrayUtil.EMPTY_STRING_ARRAY);
395   }
396
397   @NotNull
398   private static String toTitleCase(@NotNull String s, @NotNull String[] prepositions) {
399     StringBuilder buffer = null;
400     for (int i = 0; i < s.length(); i++) {
401       char prevChar = i == 0 ? ' ' : s.charAt(i - 1);
402       char currChar = s.charAt(i);
403       if (!Character.isLetterOrDigit(prevChar) && prevChar != '\'') {
404         if (Character.isLetterOrDigit(currChar)) {
405           if (!Character.isUpperCase(currChar)) {
406             int j = i;
407             for (; j < s.length(); j++) {
408               if (!Character.isLetterOrDigit(s.charAt(j))) {
409                 break;
410               }
411             }
412             if (!isPreposition(s, i, j - 1, prepositions)) {
413               if (buffer == null) {
414                 buffer = new StringBuilder(s);
415               }
416               buffer.setCharAt(i, toUpperCase(currChar));
417             }
418           }
419         }
420       }
421     }
422     if (buffer == null) {
423       return s;
424     }
425     else {
426       return buffer.toString();
427     }
428   }
429
430   @NonNls private static final String[] ourPrepositions = {"at", "the", "and", "not", "if", "a", "or", "to", "in", "on", "into", "by"};
431
432
433   public static boolean isPreposition(@NotNull String s, int firstChar, int lastChar) {
434     return isPreposition(s, firstChar, lastChar, ourPrepositions);
435   }
436
437   public static boolean isPreposition(@NotNull String s, int firstChar, int lastChar, @NotNull String[] prepositions) {
438     for (String preposition : prepositions) {
439       boolean found = false;
440       if (lastChar - firstChar + 1 == preposition.length()) {
441         found = true;
442         for (int j = 0; j < preposition.length(); j++) {
443           if (!(toLowerCase(s.charAt(firstChar + j)) == preposition.charAt(j))) {
444             found = false;
445           }
446         }
447       }
448       if (found) {
449         return true;
450       }
451     }
452     return false;
453   }
454
455   @NotNull
456   public static NotNullFunction<String, String> escaper(final boolean escapeSlash, @Nullable final String additionalChars) {
457     return new NotNullFunction<String, String>() {
458       @NotNull
459       @Override
460       public String fun(@NotNull String dom) {
461         final StringBuilder builder = new StringBuilder(dom.length());
462         escapeStringCharacters(dom.length(), dom, additionalChars, escapeSlash, builder);
463         return builder.toString();
464       }
465     };
466   }
467
468
469   public static void escapeStringCharacters(int length, @NotNull String str, @NotNull @NonNls StringBuilder buffer) {
470     escapeStringCharacters(length, str, "\"", buffer);
471   }
472
473   @NotNull
474   public static StringBuilder escapeStringCharacters(int length,
475                                                      @NotNull String str,
476                                                      @Nullable String additionalChars,
477                                                      @NotNull @NonNls StringBuilder buffer) {
478     return escapeStringCharacters(length, str, additionalChars, true, buffer);
479   }
480
481   @NotNull
482   public static StringBuilder escapeStringCharacters(int length,
483                                                      @NotNull String str,
484                                                      @Nullable String additionalChars,
485                                                      boolean escapeSlash,
486                                                      @NotNull @NonNls StringBuilder buffer) {
487     char prev = 0;
488     for (int idx = 0; idx < length; idx++) {
489       char ch = str.charAt(idx);
490       switch (ch) {
491         case '\b':
492           buffer.append("\\b");
493           break;
494
495         case '\t':
496           buffer.append("\\t");
497           break;
498
499         case '\n':
500           buffer.append("\\n");
501           break;
502
503         case '\f':
504           buffer.append("\\f");
505           break;
506
507         case '\r':
508           buffer.append("\\r");
509           break;
510
511         default:
512           if (escapeSlash && ch == '\\') {
513             buffer.append("\\\\");
514           }
515           else if (additionalChars != null && additionalChars.indexOf(ch) > -1 && (escapeSlash || prev != '\\')) {
516             buffer.append("\\").append(ch);
517           }
518           else if (Character.isISOControl(ch)) {
519             String hexCode = StringUtilRt.toUpperCase(Integer.toHexString(ch));
520             buffer.append("\\u");
521             int paddingCount = 4 - hexCode.length();
522             while (paddingCount-- > 0) {
523               buffer.append(0);
524             }
525             buffer.append(hexCode);
526           }
527           else {
528             buffer.append(ch);
529           }
530       }
531       prev = ch;
532     }
533     return buffer;
534   }
535
536   @NotNull
537   public static String escapeStringCharacters(@NotNull String s) {
538     StringBuilder buffer = new StringBuilder(s.length());
539     escapeStringCharacters(s.length(), s, buffer);
540     return buffer.toString();
541   }
542
543
544   @NotNull
545   public static String unescapeStringCharacters(@NotNull String s) {
546     StringBuilder buffer = new StringBuilder(s.length());
547     unescapeStringCharacters(s.length(), s, buffer);
548     return buffer.toString();
549   }
550
551   @NotNull
552   public static String unquoteString(@NotNull String s) {
553     char c;
554     if (s.length() <= 1 || (c = s.charAt(0)) != '"' && c != '\'' || s.charAt(s.length() - 1) != c) {
555       return s;
556     }
557     return s.substring(1, s.length() - 1);
558   }
559
560   /**
561    * This is just an optimized version of Matcher.quoteReplacement
562    */
563   @NotNull
564   public static String quoteReplacement(@NotNull String s) {
565     boolean needReplacements = false;
566
567     for (int i = 0; i < s.length(); i++) {
568       char c = s.charAt(i);
569       if (c == '\\' || c == '$') {
570         needReplacements = true;
571         break;
572       }
573     }
574
575     if (!needReplacements) return s;
576
577     StringBuilder sb = new StringBuilder(s.length() * 6 / 5);
578     for (int i = 0; i < s.length(); i++) {
579       char c = s.charAt(i);
580       if (c == '\\') {
581         sb.append('\\');
582         sb.append('\\');
583       }
584       else if (c == '$') {
585         sb.append('\\');
586         sb.append('$');
587       }
588       else {
589         sb.append(c);
590       }
591     }
592     return sb.toString();
593   }
594
595   private static void unescapeStringCharacters(int length, @NotNull String s, @NotNull StringBuilder buffer) {
596     boolean escaped = false;
597     for (int idx = 0; idx < length; idx++) {
598       char ch = s.charAt(idx);
599       if (!escaped) {
600         if (ch == '\\') {
601           escaped = true;
602         }
603         else {
604           buffer.append(ch);
605         }
606       }
607       else {
608         switch (ch) {
609           case 'n':
610             buffer.append('\n');
611             break;
612
613           case 'r':
614             buffer.append('\r');
615             break;
616
617           case 'b':
618             buffer.append('\b');
619             break;
620
621           case 't':
622             buffer.append('\t');
623             break;
624
625           case 'f':
626             buffer.append('\f');
627             break;
628
629           case '\'':
630             buffer.append('\'');
631             break;
632
633           case '\"':
634             buffer.append('\"');
635             break;
636
637           case '\\':
638             buffer.append('\\');
639             break;
640
641           case 'u':
642             if (idx + 4 < length) {
643               try {
644                 int code = Integer.parseInt(s.substring(idx + 1, idx + 5), 16);
645                 idx += 4;
646                 buffer.append((char)code);
647               }
648               catch (NumberFormatException e) {
649                 buffer.append("\\u");
650               }
651             }
652             else {
653               buffer.append("\\u");
654             }
655             break;
656
657           default:
658             buffer.append(ch);
659             break;
660         }
661         escaped = false;
662       }
663     }
664
665     if (escaped) buffer.append('\\');
666   }
667
668   @SuppressWarnings({"HardCodedStringLiteral"})
669   @NotNull
670   public static String pluralize(@NotNull String suggestion) {
671     if (suggestion.endsWith("Child") || suggestion.endsWith("child")) {
672       return suggestion + "ren";
673     }
674
675     if (suggestion.equals("this")) {
676       return "these";
677     }
678     if (suggestion.equals("This")) {
679       return "These";
680     }
681
682     if (endsWithIgnoreCase(suggestion, "s") || endsWithIgnoreCase(suggestion, "x") || endsWithIgnoreCase(suggestion, "ch")) {
683       return suggestion + "es";
684     }
685
686     int len = suggestion.length();
687     if (endsWithIgnoreCase(suggestion, "y") && len > 1 && !isVowel(toLowerCase(suggestion.charAt(len - 2)))) {
688       return suggestion.substring(0, len - 1) + "ies";
689     }
690
691     return suggestion + "s";
692   }
693
694   @NotNull
695   public static String capitalizeWords(@NotNull String text,
696                                        boolean allWords) {
697     return capitalizeWords(text, " \t\n\r\f", allWords, false);
698   }
699
700   @NotNull
701   public static String capitalizeWords(@NotNull String text,
702                                        @NotNull String tokenizerDelim,
703                                        boolean allWords,
704                                        boolean leaveOriginalDelims) {
705     final StringTokenizer tokenizer = new StringTokenizer(text, tokenizerDelim, leaveOriginalDelims);
706     final StringBuilder out = new StringBuilder(text.length());
707     boolean toCapitalize = true;
708     while (tokenizer.hasMoreTokens()) {
709       final String word = tokenizer.nextToken();
710       if (!leaveOriginalDelims && out.length() > 0) {
711         out.append(' ');
712       }
713       out.append(toCapitalize ? capitalize(word) : word);
714       if (!allWords) {
715         toCapitalize = false;
716       }
717     }
718     return out.toString();
719   }
720
721   public static String decapitalize(String s) {
722     return Introspector.decapitalize(s);
723   }
724
725   public static boolean isVowel(char c) {
726     return VOWELS.indexOf(c) >= 0;
727   }
728
729   @NotNull
730   public static String capitalize(@NotNull String s) {
731     if (s.isEmpty()) return s;
732     if (s.length() == 1) return StringUtilRt.toUpperCase(s);
733
734     // Optimization
735     if (Character.isUpperCase(s.charAt(0))) return s;
736     return toUpperCase(s.charAt(0)) + s.substring(1);
737   }
738
739   @Contract("null -> false")
740   public static boolean isCapitalized(@Nullable String s) {
741     return s != null && !s.isEmpty() && Character.isUpperCase(s.charAt(0));
742   }
743
744   @NotNull
745   public static String capitalizeWithJavaBeanConvention(@NotNull String s) {
746     if (s.length() > 1 && Character.isUpperCase(s.charAt(1))) {
747       return s;
748     }
749     return capitalize(s);
750   }
751
752   public static int stringHashCode(@NotNull CharSequence chars) {
753     if (chars instanceof String) return chars.hashCode();
754     if (chars instanceof CharSequenceWithStringHash) return chars.hashCode();
755     if (chars instanceof CharArrayCharSequence) return chars.hashCode();
756
757     return stringHashCode(chars, 0, chars.length());
758   }
759
760   public static int stringHashCode(@NotNull CharSequence chars, int from, int to) {
761     int h = 0;
762     for (int off = from; off < to; off++) {
763       h = 31 * h + chars.charAt(off);
764     }
765     return h;
766   }
767
768   public static int stringHashCode(char[] chars, int from, int to) {
769     int h = 0;
770     for (int off = from; off < to; off++) {
771       h = 31 * h + chars[off];
772     }
773     return h;
774   }
775
776   public static int stringHashCodeInsensitive(@NotNull char[] chars, int from, int to) {
777     int h = 0;
778     for (int off = from; off < to; off++) {
779       h = 31 * h + toLowerCase(chars[off]);
780     }
781     return h;
782   }
783
784   public static int stringHashCodeInsensitive(@NotNull CharSequence chars, int from, int to) {
785     int h = 0;
786     for (int off = from; off < to; off++) {
787       h = 31 * h + toLowerCase(chars.charAt(off));
788     }
789     return h;
790   }
791
792   public static int stringHashCodeInsensitive(@NotNull CharSequence chars) {
793     return stringHashCodeInsensitive(chars, 0, chars.length());
794   }
795
796   /**
797    * Equivalent to string.startsWith(prefixes[0] + prefixes[1] + ...) but avoids creating an object for concatenation.
798    */
799   public static boolean startsWithConcatenation(@NotNull String string, @NotNull String... prefixes) {
800     int offset = 0;
801     for (String prefix : prefixes) {
802       int prefixLen = prefix.length();
803       if (!string.regionMatches(offset, prefix, 0, prefixLen)) {
804         return false;
805       }
806       offset += prefixLen;
807     }
808     return true;
809   }
810
811   /** @deprecated use {@link #startsWithConcatenation(String, String...)} (to remove in IDEA 14). */
812   @SuppressWarnings("UnusedDeclaration")
813   public static boolean startsWithConcatenationOf(@NotNull String string, @NotNull String firstPrefix, @NotNull String secondPrefix) {
814     return startsWithConcatenation(string, firstPrefix, secondPrefix);
815   }
816
817   /** @deprecated use {@link #startsWithConcatenation(String, String...)} (to remove in IDEA 14). */
818   @SuppressWarnings("UnusedDeclaration")
819   public static boolean startsWithConcatenationOf(@NotNull String string,
820                                                   @NotNull String firstPrefix,
821                                                   @NotNull String secondPrefix,
822                                                   @NotNull String thirdPrefix) {
823     return startsWithConcatenation(string, firstPrefix, secondPrefix, thirdPrefix);
824   }
825
826   @NotNull
827   public static String trimEnd(@NotNull String s, @NonNls @NotNull String suffix) {
828     if (s.endsWith(suffix)) {
829       return s.substring(0, s.length() - suffix.length());
830     }
831     return s;
832   }
833
834   @NotNull
835   public static String trimLog(@NotNull final String text, final int limit) {
836     if (limit > 5 && text.length() > limit) {
837       return text.substring(0, limit - 5) + " ...\n";
838     }
839     return text;
840   }
841
842   @NotNull
843   public static String trimLeading(@NotNull String string) {
844     int index = 0;
845     while (index < string.length() && Character.isWhitespace(string.charAt(index))) index++;
846     return string.substring(index);
847   }
848
849   @NotNull
850   public static String trimTrailing(@NotNull String string) {
851     int index = string.length() - 1;
852     while (index >= 0 && Character.isWhitespace(string.charAt(index))) index--;
853     return string.substring(0, index + 1);
854   }
855
856   public static boolean startsWithChar(@Nullable CharSequence s, char prefix) {
857     return s != null && s.length() != 0 && s.charAt(0) == prefix;
858   }
859
860   public static boolean endsWithChar(@Nullable CharSequence s, char suffix) {
861     return StringUtilRt.endsWithChar(s, suffix);
862   }
863
864   @NotNull
865   public static String trimStart(@NotNull String s, @NonNls @NotNull String prefix) {
866     if (s.startsWith(prefix)) {
867       return s.substring(prefix.length());
868     }
869     return s;
870   }
871
872   @NotNull
873   public static String pluralize(@NotNull String base, int n) {
874     if (n == 1) return base;
875     return pluralize(base);
876   }
877
878   public static void repeatSymbol(@NotNull Appendable buffer, char symbol, int times) {
879     assert times >= 0 : times;
880     try {
881       for (int i = 0; i < times; i++) {
882         buffer.append(symbol);
883       }
884     }
885     catch (IOException e) {
886       LOG.error(e);
887     }
888   }
889
890   @Contract("null -> false")
891   public static boolean isNotEmpty(@Nullable String s) {
892     return s != null && !s.isEmpty();
893   }
894
895   @Contract("null -> true")
896   public static boolean isEmpty(@Nullable String s) {
897     return s == null || s.isEmpty();
898   }
899
900   @Contract("null -> true")
901   public static boolean isEmpty(@Nullable CharSequence cs) {
902     return cs == null || cs.length() == 0;
903   }
904
905   public static int length(@Nullable CharSequence cs) {
906     return cs == null ? 0 : cs.length();
907   }
908
909   @NotNull
910   public static String notNullize(@Nullable final String s) {
911     return notNullize(s, "");
912   }
913
914   @NotNull
915   public static String notNullize(@Nullable final String s, @NotNull String defaultValue) {
916     return s == null ? defaultValue : s;
917   }
918
919   @Nullable
920   public static String nullize(@Nullable final String s) {
921     return nullize(s, false);
922   }
923
924   @Nullable
925   public static String nullize(@Nullable final String s, boolean nullizeSpaces) {
926     if (nullizeSpaces) {
927       if (isEmptyOrSpaces(s)) return null;
928     }
929     else {
930       if (isEmpty(s)) return null;
931     }
932     return s;
933   }
934
935   @Contract("null -> true")
936   public static boolean isEmptyOrSpaces(@Nullable final String s) {
937     if(s == null || s.isEmpty()) {
938       return true;
939     }
940     for(int i = 0; i < s.length(); i++) {
941       if(s.charAt(i) > ' ') {
942         return false;
943       }
944     }
945     return true;
946   }
947
948   /**
949    * Allows to answer if given symbol is white space, tabulation or line feed.
950    *
951    * @param c symbol to check
952    * @return <code>true</code> if given symbol is white space, tabulation or line feed; <code>false</code> otherwise
953    */
954   public static boolean isWhiteSpace(char c) {
955     return c == '\n' || c == '\t' || c == ' ';
956   }
957
958   @NotNull
959   public static String getThrowableText(@NotNull Throwable aThrowable) {
960     return ExceptionUtil.getThrowableText(aThrowable);
961   }
962
963   @NotNull
964   public static String getThrowableText(@NotNull Throwable aThrowable, @NonNls @NotNull final String stackFrameSkipPattern) {
965     return ExceptionUtil.getThrowableText(aThrowable, stackFrameSkipPattern);
966   }
967
968   @Nullable
969   public static String getMessage(@NotNull Throwable e) {
970     return ExceptionUtil.getMessage(e);
971   }
972
973   @NotNull
974   public static String repeatSymbol(final char aChar, final int count) {
975     char[] buffer = new char[count];
976     Arrays.fill(buffer, aChar);
977     return StringFactory.createShared(buffer);
978   }
979
980   @NotNull
981   public static String repeat(@NotNull String s, int count) {
982     assert count >=0 : count;
983     StringBuilder sb = new StringBuilder(s.length() * count);
984     for (int i = 0; i < count; i++) {
985       sb.append(s);
986     }
987     return sb.toString();
988   }
989
990   @NotNull
991   public static List<String> splitHonorQuotes(@NotNull String s, char separator) {
992     final List<String> result = new ArrayList<String>();
993     final StringBuilder builder = new StringBuilder(s.length());
994     boolean inQuotes = false;
995     for (int i = 0; i < s.length(); i++) {
996       final char c = s.charAt(i);
997       if (c == separator && !inQuotes) {
998         if (builder.length() > 0) {
999           result.add(builder.toString());
1000           builder.setLength(0);
1001         }
1002         continue;
1003       }
1004
1005       if ((c == '"' || c == '\'') && !(i > 0 && s.charAt(i - 1) == '\\')) {
1006         inQuotes = !inQuotes;
1007       }
1008       builder.append(c);
1009     }
1010
1011     if (builder.length() > 0) {
1012       result.add(builder.toString());
1013     }
1014     return result;
1015   }
1016
1017
1018   @NotNull
1019   public static List<String> split(@NotNull String s, @NotNull String separator) {
1020     return split(s, separator, true);
1021   }
1022
1023   @NotNull
1024   public static List<String> split(@NotNull String s, @NotNull String separator,
1025                                    boolean excludeSeparator) {
1026     return split(s, separator, excludeSeparator, true);
1027   }
1028
1029   @NotNull
1030   public static List<String> split(@NotNull String s, @NotNull String separator,
1031                                    boolean excludeSeparator, boolean excludeEmptyStrings) {
1032     if (separator.isEmpty()) {
1033       return Collections.singletonList(s);
1034     }
1035     List<String> result = new ArrayList<String>();
1036     int pos = 0;
1037     while (true) {
1038       int index = s.indexOf(separator, pos);
1039       if (index == -1) break;
1040       final int nextPos = index + separator.length();
1041       String token = s.substring(pos, excludeSeparator ? index : nextPos);
1042       if (!token.isEmpty() || !excludeEmptyStrings) {
1043         result.add(token);
1044       }
1045       pos = nextPos;
1046     }
1047     if (pos < s.length() || !excludeEmptyStrings && pos == s.length()) {
1048       result.add(s.substring(pos, s.length()));
1049     }
1050     return result;
1051   }
1052
1053   @NotNull
1054   public static Iterable<String> tokenize(@NotNull String s, @NotNull String separators) {
1055     final com.intellij.util.text.StringTokenizer tokenizer = new com.intellij.util.text.StringTokenizer(s, separators);
1056     return new Iterable<String>() {
1057       @NotNull
1058       @Override
1059       public Iterator<String> iterator() {
1060         return new Iterator<String>() {
1061           @Override
1062           public boolean hasNext() {
1063             return tokenizer.hasMoreTokens();
1064           }
1065
1066           @Override
1067           public String next() {
1068             return tokenizer.nextToken();
1069           }
1070
1071           @Override
1072           public void remove() {
1073             throw new UnsupportedOperationException();
1074           }
1075         };
1076       }
1077     };
1078   }
1079
1080   @NotNull
1081   public static Iterable<String> tokenize(@NotNull final StringTokenizer tokenizer) {
1082     return new Iterable<String>() {
1083       @NotNull
1084       @Override
1085       public Iterator<String> iterator() {
1086         return new Iterator<String>() {
1087           @Override
1088           public boolean hasNext() {
1089             return tokenizer.hasMoreTokens();
1090           }
1091
1092           @Override
1093           public String next() {
1094             return tokenizer.nextToken();
1095           }
1096
1097           @Override
1098           public void remove() {
1099             throw new UnsupportedOperationException();
1100           }
1101         };
1102       }
1103     };
1104   }
1105
1106   @NotNull
1107   public static List<String> getWordsIn(@NotNull String text) {
1108     List<String> result = new SmartList<String>();
1109     int start = -1;
1110     for (int i = 0; i < text.length(); i++) {
1111       char c = text.charAt(i);
1112       boolean isIdentifierPart = Character.isJavaIdentifierPart(c);
1113       if (isIdentifierPart && start == -1) {
1114         start = i;
1115       }
1116       if (isIdentifierPart && i == text.length() - 1 && start != -1) {
1117         result.add(text.substring(start, i + 1));
1118       }
1119       else if (!isIdentifierPart && start != -1) {
1120         result.add(text.substring(start, i));
1121         start = -1;
1122       }
1123     }
1124     return result;
1125   }
1126
1127   @NotNull
1128   public static List<TextRange> getWordIndicesIn(@NotNull String text) {
1129     List<TextRange> result = new SmartList<TextRange>();
1130     int start = -1;
1131     for (int i = 0; i < text.length(); i++) {
1132       char c = text.charAt(i);
1133       boolean isIdentifierPart = Character.isJavaIdentifierPart(c);
1134       if (isIdentifierPart && start == -1) {
1135         start = i;
1136       }
1137       if (isIdentifierPart && i == text.length() - 1 && start != -1) {
1138         result.add(new TextRange(start, i + 1));
1139       }
1140       else if (!isIdentifierPart && start != -1) {
1141         result.add(new TextRange(start, i));
1142         start = -1;
1143       }
1144     }
1145     return result;
1146   }
1147
1148   @NotNull
1149   public static String join(@NotNull final String[] strings, @NotNull final String separator) {
1150     return join(strings, 0, strings.length, separator);
1151   }
1152
1153   @NotNull
1154   public static String join(@NotNull final String[] strings, int startIndex, int endIndex, @NotNull final String separator) {
1155     final StringBuilder result = new StringBuilder();
1156     for (int i = startIndex; i < endIndex; i++) {
1157       if (i > startIndex) result.append(separator);
1158       result.append(strings[i]);
1159     }
1160     return result.toString();
1161   }
1162
1163   @NotNull
1164   public static String[] zip(@NotNull String[] strings1, @NotNull String[] strings2, String separator) {
1165     if (strings1.length != strings2.length) throw new IllegalArgumentException();
1166
1167     String[] result = ArrayUtil.newStringArray(strings1.length);
1168     for (int i = 0; i < result.length; i++) {
1169       result[i] = strings1[i] + separator + strings2[i];
1170     }
1171
1172     return result;
1173   }
1174
1175   @NotNull
1176   public static String[] surround(@NotNull String[] strings1, String prefix, String suffix) {
1177     String[] result = ArrayUtil.newStringArray(strings1.length);
1178     for (int i = 0; i < result.length; i++) {
1179       result[i] = prefix + strings1[i] + suffix;
1180     }
1181
1182     return result;
1183   }
1184
1185   @NotNull
1186   public static <T> String join(@NotNull T[] items, @NotNull Function<T, String> f, @NotNull @NonNls String separator) {
1187     return join(Arrays.asList(items), f, separator);
1188   }
1189
1190   @NotNull
1191   public static <T> String join(@NotNull Collection<? extends T> items, @NotNull Function<? super T, String> f, @NotNull @NonNls String separator) {
1192     if (items.isEmpty()) return "";
1193     return join((Iterable<? extends T>)items, f, separator);
1194   }
1195
1196   public static String join(@NotNull Iterable<?> items, @NotNull @NonNls String separator) {
1197     StringBuilder result = new StringBuilder();
1198     for (Object item : items) {
1199       result.append(item).append(separator);
1200     }
1201     if (result.length() > 0) {
1202       result.setLength(result.length() - separator.length());
1203     }
1204     return result.toString();
1205   }
1206
1207   @NotNull
1208   public static <T> String join(@NotNull Iterable<? extends T> items, @NotNull Function<? super T, String> f, @NotNull @NonNls String separator) {
1209     final StringBuilder result = new StringBuilder();
1210     for (T item : items) {
1211       String string = f.fun(item);
1212       if (string != null && !string.isEmpty()) {
1213         if (result.length() != 0) result.append(separator);
1214         result.append(string);
1215       }
1216     }
1217     return result.toString();
1218   }
1219
1220   @NotNull
1221   public static String join(@NotNull Collection<? extends String> strings, @NotNull String separator) {
1222     StringBuilder result = new StringBuilder();
1223     join(strings, separator, result);
1224     return result.toString();
1225   }
1226
1227   public static void join(@NotNull Collection<? extends String> strings, @NotNull String separator, @NotNull StringBuilder result) {
1228     boolean isFirst = true;
1229     for (String string : strings) {
1230       if (string != null && !string.isEmpty()) {
1231         if (isFirst) {
1232           isFirst = false;
1233         }
1234         else {
1235           result.append(separator);
1236         }
1237         result.append(string);
1238       }
1239     }
1240   }
1241
1242   @NotNull
1243   public static String join(@NotNull final int[] strings, @NotNull final String separator) {
1244     final StringBuilder result = new StringBuilder();
1245     for (int i = 0; i < strings.length; i++) {
1246       if (i > 0) result.append(separator);
1247       result.append(strings[i]);
1248     }
1249     return result.toString();
1250   }
1251
1252   @NotNull
1253   public static String join(@Nullable final String... strings) {
1254     if (strings == null || strings.length == 0) return "";
1255
1256     final StringBuilder builder = new StringBuilder();
1257     for (final String string : strings) {
1258       builder.append(string);
1259     }
1260     return builder.toString();
1261   }
1262
1263   @NotNull
1264   public static String stripQuotesAroundValue(@NotNull String text) {
1265     if (startsWithChar(text, '\"') || startsWithChar(text, '\'')) text = text.substring(1);
1266     if (endsWithChar(text, '\"') || endsWithChar(text, '\'')) text = text.substring(0, text.length() - 1);
1267     return text;
1268   }
1269
1270   public static boolean isQuotedString(@NotNull String text) {
1271     return startsWithChar(text, '\"') && endsWithChar(text, '\"')
1272            || startsWithChar(text, '\'') && endsWithChar(text, '\'');
1273   }
1274
1275   /**
1276    * Formats the specified file size as a string.
1277    *
1278    * @param fileSize the size to format.
1279    * @return the size formatted as a string.
1280    * @since 5.0.1
1281    */
1282   @NotNull
1283   public static String formatFileSize(final long fileSize) {
1284     if (fileSize < 0x400) {
1285       return CommonBundle.message("format.file.size.bytes", fileSize);
1286     }
1287     if (fileSize < 0x100000) {
1288       long kbytes = fileSize * 100 / 1024;
1289       final String kbs = kbytes / 100 + "." + formatMinor(kbytes % 100);
1290       return CommonBundle.message("format.file.size.kbytes", kbs);
1291     }
1292     long mbytes = fileSize * 100 / 1024 / 1024;
1293     final String size = mbytes / 100 + "." + formatMinor(mbytes % 100);
1294     return CommonBundle.message("format.file.size.mbytes", size);
1295   }
1296
1297   @NotNull
1298   public static String formatDuration(long duration) {
1299     final long minutes = duration / 60000;
1300     final long seconds = ((duration + 500L) % 60000) / 1000;
1301     if (minutes > 0L) {
1302       return minutes + " min " + seconds + " sec";
1303     }
1304     return seconds + " sec";
1305   }
1306
1307   @NotNull
1308   private static String formatMinor(long number) {
1309     if (number > 0L && number <= 9L) {
1310       return "0" + number;
1311     }
1312     return String.valueOf(number);
1313   }
1314
1315   /**
1316    * Returns unpluralized variant using English based heuristics like properties -> property, names -> name, children -> child.
1317    * Returns <code>null</code> if failed to match appropriate heuristic.
1318    *
1319    * @param name english word in plural form
1320    * @return name in singular form or <code>null</code> if failed to find one.
1321    */
1322   @SuppressWarnings({"HardCodedStringLiteral"})
1323   @Nullable
1324   public static String unpluralize(@NotNull final String name) {
1325     if (name.endsWith("sses") || name.endsWith("shes") || name.endsWith("ches") || name.endsWith("xes")) { //?
1326       return name.substring(0, name.length() - 2);
1327     }
1328
1329     if (name.endsWith("ses")) {
1330       return name.substring(0, name.length() - 1);
1331     }
1332
1333     if (name.endsWith("ies")) {
1334       if (name.endsWith("cookies") || name.endsWith("Cookies")) {
1335         return name.substring(0, name.length() - "ookies".length()) + "ookie";
1336       }
1337
1338       return name.substring(0, name.length() - 3) + "y";
1339     }
1340
1341     if (name.endsWith("leaves") || name.endsWith("Leaves")) {
1342       return name.substring(0, name.length() - "eaves".length()) + "eaf";
1343     }
1344
1345     String result = stripEnding(name, "s");
1346     if (result != null) {
1347       return result;
1348     }
1349
1350     if (name.endsWith("children")) {
1351       return name.substring(0, name.length() - "children".length()) + "child";
1352     }
1353
1354     if (name.endsWith("Children") && name.length() > "Children".length()) {
1355       return name.substring(0, name.length() - "Children".length()) + "Child";
1356     }
1357
1358
1359     return null;
1360   }
1361
1362   @Nullable
1363   private static String stripEnding(@NotNull String name, @NotNull String ending) {
1364     if (name.endsWith(ending)) {
1365       if (name.equals(ending)) return name; // do not return empty string
1366       return name.substring(0, name.length() - 1);
1367     }
1368     return null;
1369   }
1370
1371   public static boolean containsAlphaCharacters(@NotNull String value) {
1372     for (int i = 0; i < value.length(); i++) {
1373       if (Character.isLetter(value.charAt(i))) return true;
1374     }
1375     return false;
1376   }
1377
1378   public static boolean containsAnyChar(@NotNull final String value, @NotNull final String chars) {
1379     if (chars.length() > value.length()) {
1380       return containsAnyChar(value, chars, 0, value.length());
1381     }
1382     else {
1383       return containsAnyChar(chars, value, 0, chars.length());
1384     }
1385   }
1386
1387   public static boolean containsAnyChar(@NotNull final String value,
1388                                         @NotNull final String chars,
1389                                         final int start, final int end) {
1390     for (int i = start; i < end; i++) {
1391       if (chars.indexOf(value.charAt(i)) >= 0) {
1392         return true;
1393       }
1394     }
1395
1396     return false;
1397   }
1398
1399   public static boolean containsChar(@NotNull final String value, final char ch) {
1400     return value.indexOf(ch) >= 0;
1401   }
1402
1403   /**
1404    * @deprecated use #capitalize(String)
1405    */
1406   @Nullable
1407   public static String firstLetterToUpperCase(@Nullable final String displayString) {
1408     if (displayString == null || displayString.isEmpty()) return displayString;
1409     char firstChar = displayString.charAt(0);
1410     char uppedFirstChar = toUpperCase(firstChar);
1411
1412     if (uppedFirstChar == firstChar) return displayString;
1413
1414     char[] buffer = displayString.toCharArray();
1415     buffer[0] = uppedFirstChar;
1416     return StringFactory.createShared(buffer);
1417   }
1418
1419   /**
1420    * Strip out all characters not accepted by given filter
1421    *
1422    * @param s      e.g. "/n    my string "
1423    * @param filter e.g. {@link CharFilter#NOT_WHITESPACE_FILTER}
1424    * @return stripped string e.g. "mystring"
1425    */
1426   @NotNull
1427   public static String strip(@NotNull final String s, @NotNull final CharFilter filter) {
1428     final StringBuilder result = new StringBuilder(s.length());
1429     for (int i = 0; i < s.length(); i++) {
1430       char ch = s.charAt(i);
1431       if (filter.accept(ch)) {
1432         result.append(ch);
1433       }
1434     }
1435     return result.toString();
1436   }
1437
1438   @NotNull
1439   public static List<String> findMatches(@NotNull String s, @NotNull Pattern pattern) {
1440     return findMatches(s, pattern, 1);
1441   }
1442
1443   @NotNull
1444   public static List<String> findMatches(@NotNull String s, @NotNull Pattern pattern, int groupIndex) {
1445     List<String> result = new SmartList<String>();
1446     Matcher m = pattern.matcher(s);
1447     while (m.find()) {
1448       String group = m.group(groupIndex);
1449       if (group != null) {
1450         result.add(group);
1451       }
1452     }
1453     return result;
1454   }
1455
1456   /**
1457    * Find position of the first character accepted by given filter.
1458    *
1459    * @param s      the string to search
1460    * @param filter search filter
1461    * @return position of the first character accepted or -1 if not found
1462    */
1463   public static int findFirst(@NotNull final CharSequence s, @NotNull CharFilter filter) {
1464     for (int i = 0; i < s.length(); i++) {
1465       char ch = s.charAt(i);
1466       if (filter.accept(ch)) {
1467         return i;
1468       }
1469     }
1470     return -1;
1471   }
1472
1473   @NotNull
1474   public static String replaceSubstring(@NotNull String string, @NotNull TextRange range, @NotNull String replacement) {
1475     return range.replace(string, replacement);
1476   }
1477
1478   public static boolean startsWithWhitespace(@NotNull String text) {
1479     return !text.isEmpty() && Character.isWhitespace(text.charAt(0));
1480   }
1481
1482   public static boolean isChar(CharSequence seq, int index, char c) {
1483     return index >= 0 && index < seq.length() && seq.charAt(index) == c;
1484   }
1485
1486   public static boolean startsWith(@NotNull CharSequence text, @NotNull CharSequence prefix) {
1487     int l1 = text.length();
1488     int l2 = prefix.length();
1489     if (l1 < l2) return false;
1490
1491     for (int i = 0; i < l2; i++) {
1492       if (text.charAt(i) != prefix.charAt(i)) return false;
1493     }
1494
1495     return true;
1496   }
1497
1498   public static boolean startsWith(@NotNull CharSequence text, int startIndex, @NotNull CharSequence prefix) {
1499     int l1 = text.length() - startIndex;
1500     int l2 = prefix.length();
1501     if (l1 < l2) return false;
1502
1503     for (int i = 0; i < l2; i++) {
1504       if (text.charAt(i + startIndex) != prefix.charAt(i)) return false;
1505     }
1506
1507     return true;
1508   }
1509
1510   public static boolean endsWith(@NotNull CharSequence text, @NotNull CharSequence suffix) {
1511     int l1 = text.length();
1512     int l2 = suffix.length();
1513     if (l1 < l2) return false;
1514
1515     for (int i = l1 - 1; i >= l1 - l2; i--) {
1516       if (text.charAt(i) != suffix.charAt(i + l2 - l1)) return false;
1517     }
1518
1519     return true;
1520   }
1521
1522   @NotNull
1523   public static String commonPrefix(@NotNull String s1, @NotNull String s2) {
1524     return s1.substring(0, commonPrefixLength(s1, s2));
1525   }
1526
1527   public static int commonPrefixLength(@NotNull CharSequence s1, @NotNull CharSequence s2) {
1528     int i;
1529     int minLength = Math.min(s1.length(), s2.length());
1530     for (i = 0; i < minLength; i++) {
1531       if (s1.charAt(i) != s2.charAt(i)) {
1532         break;
1533       }
1534     }
1535     return i;
1536   }
1537
1538   @NotNull
1539   public static String commonSuffix(@NotNull String s1, @NotNull String s2) {
1540     return s1.substring(s1.length() - commonSuffixLength(s1, s2));
1541   }
1542
1543   public static int commonSuffixLength(@NotNull CharSequence s1, @NotNull CharSequence s2) {
1544     int s1Length = s1.length();
1545     int s2Length = s2.length();
1546     if (s1Length == 0 || s2Length == 0) return 0;
1547     int i;
1548     for (i = 0; i < s1Length && i < s2Length; i++) {
1549       if (s1.charAt(s1Length - i - 1) != s2.charAt(s2Length - i - 1)) {
1550         break;
1551       }
1552     }
1553     return i;
1554   }
1555
1556   /**
1557    * Allows to answer if target symbol is contained at given char sequence at <code>[start; end)</code> interval.
1558    *
1559    * @param s     target char sequence to check
1560    * @param start start offset to use within the given char sequence (inclusive)
1561    * @param end   end offset to use within the given char sequence (exclusive)
1562    * @param c     target symbol to check
1563    * @return <code>true</code> if given symbol is contained at the target range of the given char sequence;
1564    *         <code>false</code> otherwise
1565    */
1566   public static boolean contains(@NotNull CharSequence s, int start, int end, char c) {
1567     return indexOf(s, c, start, end) >= 0;
1568   }
1569
1570   public static boolean containsWhitespaces(@Nullable CharSequence s) {
1571     if (s == null) return false;
1572
1573     for (int i = 0; i < s.length(); i++) {
1574       if (Character.isWhitespace(s.charAt(i))) return true;
1575     }
1576     return false;
1577   }
1578
1579   public static int indexOf(@NotNull CharSequence s, char c) {
1580     return indexOf(s, c, 0, s.length());
1581   }
1582
1583   public static int indexOf(@NotNull CharSequence s, char c, int start) {
1584     return indexOf(s, c, start, s.length());
1585   }
1586
1587   public static int indexOf(@NotNull CharSequence s, char c, int start, int end) {
1588     for (int i = start; i < end; i++) {
1589       if (s.charAt(i) == c) return i;
1590     }
1591     return -1;
1592   }
1593
1594   public static int indexOf(@NotNull CharSequence s, char c, int start, int end, boolean caseSensitive) {
1595     for (int i = start; i < end; i++) {
1596       if (charsMatch(s.charAt(i), c, !caseSensitive)) return i;
1597     }
1598     return -1;
1599   }
1600
1601   public static int indexOf(@NotNull char[] s, char c, int start, int end, boolean caseSensitive) {
1602     for (int i = start; i < end; i++) {
1603       if (charsMatch(s[i], c, !caseSensitive)) return i;
1604     }
1605     return -1;
1606   }
1607
1608   public static int indexOfSubstringEnd(@NotNull String text, @NotNull String subString) {
1609     int i = text.indexOf(subString);
1610     if (i == -1) return -1;
1611     return i + subString.length();
1612   }
1613
1614   public static int indexOfAny(@NotNull final String s, @NotNull final String chars) {
1615     return indexOfAny(s, chars, 0, s.length());
1616   }
1617
1618   public static int indexOfAny(@NotNull final String s, @NotNull final String chars, final int start, final int end) {
1619     for (int i = start; i < end; i++) {
1620       if (containsChar(chars, s.charAt(i))) return i;
1621     }
1622     return -1;
1623   }
1624
1625   @Nullable
1626   public static String substringAfter(@NotNull String text, @NotNull String subString) {
1627     int i = text.indexOf(subString);
1628     if (i == -1) return null;
1629     return text.substring(i + subString.length());
1630   }
1631
1632   /**
1633    * Allows to retrieve index of last occurrence of the given symbols at <code>[start; end)</code> sub-sequence of the given text.
1634    *
1635    * @param s     target text
1636    * @param c     target symbol which last occurrence we want to check
1637    * @param start start offset of the target text (inclusive)
1638    * @param end   end offset of the target text (exclusive)
1639    * @return index of the last occurrence of the given symbol at the target sub-sequence of the given text if any;
1640    *         <code>-1</code> otherwise
1641    */
1642   public static int lastIndexOf(@NotNull CharSequence s, char c, int start, int end) {
1643     for (int i = end - 1; i >= start; i--) {
1644       if (s.charAt(i) == c) return i;
1645     }
1646     return -1;
1647   }
1648
1649   @NotNull
1650   public static String first(@NotNull String text, final int maxLength, final boolean appendEllipsis) {
1651     return text.length() > maxLength ? text.substring(0, maxLength) + (appendEllipsis ? "..." : "") : text;
1652   }
1653
1654   @NotNull
1655   public static CharSequence first(@NotNull CharSequence text, final int length, final boolean appendEllipsis) {
1656     return text.length() > length ? text.subSequence(0, length) + (appendEllipsis ? "..." : "") : text;
1657   }
1658
1659   @NotNull
1660   public static CharSequence last(@NotNull CharSequence text, final int length, boolean prependEllipsis) {
1661     return text.length() > length ? (prependEllipsis ? "..." : "") + text.subSequence(text.length() - length, text.length()) : text;
1662   }
1663
1664   @NotNull
1665   private static String escapeChar(@NotNull final String str, final char character) {
1666     final StringBuilder buf = new StringBuilder(str);
1667     escapeChar(buf, character);
1668     return buf.toString();
1669   }
1670
1671   private static void escapeChar(@NotNull final StringBuilder buf, final char character) {
1672     int idx = 0;
1673     while ((idx = indexOf(buf, character, idx)) >= 0) {
1674       buf.insert(idx, "\\");
1675       idx += 2;
1676     }
1677   }
1678
1679   @NotNull
1680   public static String escapeQuotes(@NotNull final String str) {
1681     return escapeChar(str, '"');
1682   }
1683
1684   public static void escapeQuotes(@NotNull final StringBuilder buf) {
1685     escapeChar(buf, '"');
1686   }
1687
1688   @NotNull
1689   public static String escapeSlashes(@NotNull final String str) {
1690     return escapeChar(str, '/');
1691   }
1692
1693   @NotNull
1694   public static String escapeBackSlashes(@NotNull final String str) {
1695     return escapeChar(str, '\\');
1696   }
1697
1698   public static void escapeSlashes(@NotNull final StringBuilder buf) {
1699     escapeChar(buf, '/');
1700   }
1701
1702   @NotNull
1703   public static String unescapeSlashes(@NotNull final String str) {
1704     final StringBuilder buf = new StringBuilder(str.length());
1705     unescapeSlashes(buf, str);
1706     return buf.toString();
1707   }
1708
1709   private static void unescapeSlashes(@NotNull StringBuilder buf, @NotNull String str) {
1710     final int length = str.length();
1711     final int last = length - 1;
1712     for (int i = 0; i < length; i++) {
1713       char ch = str.charAt(i);
1714       if (ch == '\\' && i != last) {
1715         i++;
1716         ch = str.charAt(i);
1717         if (ch != '/') buf.append('\\');
1718       }
1719
1720       buf.append(ch);
1721     }
1722   }
1723
1724   public static void quote(@NotNull final StringBuilder builder) {
1725     quote(builder, '\"');
1726   }
1727
1728   public static void quote(@NotNull final StringBuilder builder, final char quotingChar) {
1729     builder.insert(0, quotingChar);
1730     builder.append(quotingChar);
1731   }
1732
1733   @NotNull
1734   public static String wrapWithDoubleQuote(@NotNull String str) {
1735     return '\"' + str + "\"";
1736   }
1737
1738   @NonNls private static final String[] REPLACES_REFS = {"&lt;", "&gt;", "&amp;", "&#39;", "&quot;"};
1739   @NonNls private static final String[] REPLACES_DISP = {"<", ">", "&", "'", "\""};
1740
1741   public static String unescapeXml(@Nullable final String text) {
1742     if (text == null) return null;
1743     return replace(text, REPLACES_REFS, REPLACES_DISP);
1744   }
1745
1746   public static String escapeXml(@Nullable final String text) {
1747     if (text == null) return null;
1748     return replace(text, REPLACES_DISP, REPLACES_REFS);
1749   }
1750
1751   @NotNull
1752   public static String escapeToRegexp(@NotNull String text) {
1753     final StringBuilder result = new StringBuilder(text.length());
1754     return escapeToRegexp(text, result).toString();
1755   }
1756
1757   @NotNull
1758   public static StringBuilder escapeToRegexp(@NotNull CharSequence text, @NotNull StringBuilder builder) {
1759     for (int i = 0; i < text.length(); i++) {
1760       final char c = text.charAt(i);
1761       if (c == ' ' || Character.isLetter(c) || Character.isDigit(c) || c == '_') {
1762         builder.append(c);
1763       }
1764       else if (c == '\n') {
1765         builder.append("\\n");
1766       }
1767       else {
1768         builder.append('\\').append(c);
1769       }
1770     }
1771
1772     return builder;
1773   }
1774
1775   public static boolean isNotEscapedBackslash(@NotNull char[] chars, int startOffset, int backslashOffset) {
1776     if (chars[backslashOffset] != '\\') {
1777       return false;
1778     }
1779     boolean escaped = false;
1780     for (int i = startOffset; i < backslashOffset; i++) {
1781       if (chars[i] == '\\') {
1782         escaped = !escaped;
1783       }
1784       else {
1785         escaped = false;
1786       }
1787     }
1788     return !escaped;
1789   }
1790
1791   public static boolean isNotEscapedBackslash(@NotNull CharSequence text, int startOffset, int backslashOffset) {
1792     if (text.charAt(backslashOffset) != '\\') {
1793       return false;
1794     }
1795     boolean escaped = false;
1796     for (int i = startOffset; i < backslashOffset; i++) {
1797       if (text.charAt(i) == '\\') {
1798         escaped = !escaped;
1799       }
1800       else {
1801         escaped = false;
1802       }
1803     }
1804     return !escaped;
1805   }
1806
1807   @NotNull
1808   public static String replace(@NotNull String text, @NotNull String[] from, @NotNull String[] to) {
1809     final StringBuilder result = new StringBuilder(text.length());
1810     replace:
1811     for (int i = 0; i < text.length(); i++) {
1812       for (int j = 0; j < from.length; j += 1) {
1813         String toReplace = from[j];
1814         String replaceWith = to[j];
1815
1816         final int len = toReplace.length();
1817         if (text.regionMatches(i, toReplace, 0, len)) {
1818           result.append(replaceWith);
1819           i += len - 1;
1820           continue replace;
1821         }
1822       }
1823       result.append(text.charAt(i));
1824     }
1825     return result.toString();
1826   }
1827
1828   @NotNull
1829   public static String[] filterEmptyStrings(@NotNull String[] strings) {
1830     int emptyCount = 0;
1831     for (String string : strings) {
1832       if (string == null || string.isEmpty()) emptyCount++;
1833     }
1834     if (emptyCount == 0) return strings;
1835
1836     String[] result = ArrayUtil.newStringArray(strings.length - emptyCount);
1837     int count = 0;
1838     for (String string : strings) {
1839       if (string == null || string.isEmpty()) continue;
1840       result[count++] = string;
1841     }
1842
1843     return result;
1844   }
1845
1846   public static int countNewLines(@NotNull CharSequence text) {
1847     return countChars(text, '\n');
1848   }
1849
1850   public static int countChars(@NotNull CharSequence text, char c) {
1851     int count = 0;
1852
1853     for (int i = 0; i < text.length(); ++i) {
1854       final char ch = text.charAt(i);
1855       if (ch == c) {
1856         ++count;
1857       }
1858     }
1859     return count;
1860   }
1861
1862   @NotNull
1863   public static String capitalsOnly(@NotNull String s) {
1864     StringBuilder b = new StringBuilder();
1865     for (int i = 0; i < s.length(); i++) {
1866       if (Character.isUpperCase(s.charAt(i))) {
1867         b.append(s.charAt(i));
1868       }
1869     }
1870
1871     return b.toString();
1872   }
1873
1874   /**
1875    * @param args Strings to join.
1876    * @return {@code null} if any of given Strings is {@code null}.
1877    */
1878   @Nullable
1879   public static String joinOrNull(@NotNull String... args) {
1880     StringBuilder r = new StringBuilder();
1881     for (String arg : args) {
1882       if (arg == null) return null;
1883       r.append(arg);
1884     }
1885     return r.toString();
1886   }
1887
1888   @Nullable
1889   public static String getPropertyName(@NonNls @NotNull String methodName) {
1890     if (methodName.startsWith("get")) {
1891       return Introspector.decapitalize(methodName.substring(3));
1892     }
1893     else if (methodName.startsWith("is")) {
1894       return Introspector.decapitalize(methodName.substring(2));
1895     }
1896     else if (methodName.startsWith("set")) {
1897       return Introspector.decapitalize(methodName.substring(3));
1898     }
1899     else {
1900       return null;
1901     }
1902   }
1903
1904   public static boolean isJavaIdentifierStart(char c) {
1905     return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || Character.isJavaIdentifierStart(c);
1906   }
1907
1908   public static boolean isJavaIdentifierPart(char c) {
1909     return c >= '0' && c <= '9' || isJavaIdentifierStart(c);
1910   }
1911
1912   public static boolean isJavaIdentifier(@NotNull String text) {
1913     int len = text.length();
1914     if (len == 0) return false;
1915
1916     if (!isJavaIdentifierStart(text.charAt(0))) return false;
1917
1918     for (int i = 1; i < len; i++) {
1919       if (!isJavaIdentifierPart(text.charAt(i))) return false;
1920     }
1921
1922     return true;
1923   }
1924
1925   /**
1926    * Escape property name or key in property file. Unicode characters are escaped as well.
1927    *
1928    * @param input an input to escape
1929    * @param isKey if true, the rules for key escaping are applied. The leading space is escaped in that case.
1930    * @return an escaped string
1931    */
1932   @NotNull
1933   public static String escapeProperty(@NotNull String input, final boolean isKey) {
1934     final StringBuilder escaped = new StringBuilder(input.length());
1935     for (int i = 0; i < input.length(); i++) {
1936       final char ch = input.charAt(i);
1937       switch (ch) {
1938         case ' ':
1939           if (isKey && i == 0) {
1940             // only the leading space has to be escaped
1941             escaped.append('\\');
1942           }
1943           escaped.append(' ');
1944           break;
1945         case '\t':
1946           escaped.append("\\t");
1947           break;
1948         case '\r':
1949           escaped.append("\\r");
1950           break;
1951         case '\n':
1952           escaped.append("\\n");
1953           break;
1954         case '\f':
1955           escaped.append("\\f");
1956           break;
1957         case '\\':
1958         case '#':
1959         case '!':
1960         case ':':
1961         case '=':
1962           escaped.append('\\');
1963           escaped.append(ch);
1964           break;
1965         default:
1966           if (20 < ch && ch < 0x7F) {
1967             escaped.append(ch);
1968           }
1969           else {
1970             escaped.append("\\u");
1971             escaped.append(Character.forDigit((ch >> 12) & 0xF, 16));
1972             escaped.append(Character.forDigit((ch >> 8) & 0xF, 16));
1973             escaped.append(Character.forDigit((ch >> 4) & 0xF, 16));
1974             escaped.append(Character.forDigit((ch) & 0xF, 16));
1975           }
1976           break;
1977       }
1978     }
1979     return escaped.toString();
1980   }
1981
1982   public static String getQualifiedName(@Nullable String packageName, String className) {
1983     if (packageName == null || packageName.isEmpty()) {
1984       return className;
1985     }
1986     return packageName + '.' + className;
1987   }
1988
1989   public static int compareVersionNumbers(@Nullable String v1, @Nullable String v2) {
1990     if (v1 == null && v2 == null) {
1991       return 0;
1992     }
1993     if (v1 == null) {
1994       return -1;
1995     }
1996     if (v2 == null) {
1997       return 1;
1998     }
1999
2000     String[] part1 = v1.split("[\\.\\_\\-]");
2001     String[] part2 = v2.split("[\\.\\_\\-]");
2002
2003     int idx = 0;
2004     for (; idx < part1.length && idx < part2.length; idx++) {
2005       String p1 = part1[idx];
2006       String p2 = part2[idx];
2007
2008       int cmp;
2009       if (p1.matches("\\d+") && p2.matches("\\d+")) {
2010         cmp = new Integer(p1).compareTo(new Integer(p2));
2011       }
2012       else {
2013         cmp = part1[idx].compareTo(part2[idx]);
2014       }
2015       if (cmp != 0) return cmp;
2016     }
2017
2018     if (part1.length == part2.length) {
2019       return 0;
2020     }
2021     else if (part1.length > idx) {
2022       return 1;
2023     }
2024     else {
2025       return -1;
2026     }
2027   }
2028
2029   @SuppressWarnings("UnusedDeclaration")
2030   /** @deprecated use {@linkplain #getOccurrenceCount(String, char)} (to remove in IDEA 13) */
2031   public static int getOccurenceCount(@NotNull String text, final char c) {
2032     return getOccurrenceCount(text, c);
2033   }
2034
2035   public static int getOccurrenceCount(@NotNull String text, final char c) {
2036     int res = 0;
2037     int i = 0;
2038     while (i < text.length()) {
2039       i = text.indexOf(c, i);
2040       if (i >= 0) {
2041         res++;
2042         i++;
2043       }
2044       else {
2045         break;
2046       }
2047     }
2048     return res;
2049   }
2050
2051   public static int getOccurrenceCount(@NotNull String text, @NotNull String s) {
2052     int res = 0;
2053     int i = 0;
2054     while (i < text.length()) {
2055       i = text.indexOf(s, i);
2056       if (i >= 0) {
2057         res++;
2058         i++;
2059       }
2060       else {
2061         break;
2062       }
2063     }
2064     return res;
2065   }
2066
2067   @NotNull
2068   public static String fixVariableNameDerivedFromPropertyName(@NotNull String name) {
2069     if (isEmptyOrSpaces(name)) return name;
2070     char c = name.charAt(0);
2071     if (isVowel(c)) {
2072       return "an" + Character.toUpperCase(c) + name.substring(1);
2073     }
2074     return "a" + Character.toUpperCase(c) + name.substring(1);
2075   }
2076
2077   @NotNull
2078   public static String sanitizeJavaIdentifier(@NotNull String name) {
2079     final StringBuilder result = new StringBuilder(name.length());
2080
2081     for (int i = 0; i < name.length(); i++) {
2082       final char ch = name.charAt(i);
2083       if (Character.isLetterOrDigit(ch)) {
2084         if (result.length() == 0 && !Character.isJavaIdentifierStart(ch)) {
2085           result.append("_");
2086         }
2087         result.append(ch);
2088       }
2089     }
2090
2091     return result.toString();
2092   }
2093
2094   public static void assertValidSeparators(@NotNull CharSequence s) {
2095     char[] chars = CharArrayUtil.fromSequenceWithoutCopying(s);
2096     int slashRIndex = -1;
2097
2098     if (chars != null) {
2099       for (int i = 0, len = s.length(); i < len; ++i) {
2100         if (chars[i] == '\r') {
2101           slashRIndex = i;
2102           break;
2103         }
2104       }
2105     }
2106     else {
2107       for (int i = 0, len = s.length(); i < len; i++) {
2108         if (s.charAt(i) == '\r') {
2109           slashRIndex = i;
2110           break;
2111         }
2112       }
2113     }
2114
2115     if (slashRIndex != -1) {
2116       String context =
2117         String.valueOf(last(s.subSequence(0, slashRIndex), 10, true)) + first(s.subSequence(slashRIndex, s.length()), 10, true);
2118       context = escapeStringCharacters(context);
2119       LOG.error("Wrong line separators: '" + context + "' at offset " + slashRIndex);
2120     }
2121   }
2122
2123   @NotNull
2124   public static String tail(@NotNull String s, final int idx) {
2125     return idx >= s.length() ? "" : s.substring(idx, s.length());
2126   }
2127
2128   /**
2129    * Splits string by lines.
2130    *
2131    * @param string String to split
2132    * @return array of strings
2133    */
2134   @NotNull
2135   public static String[] splitByLines(@NotNull String string) {
2136     return splitByLines(string, true);
2137   }
2138
2139   /**
2140    * Splits string by lines. If several line separators are in a row corresponding empty lines
2141    * are also added to result if {@code excludeEmptyStrings} is {@code false}.
2142    *
2143    * @param string String to split
2144    * @return array of strings
2145    */
2146   @NotNull
2147   public static String[] splitByLines(@NotNull String string, boolean excludeEmptyStrings) {
2148     return (excludeEmptyStrings ? EOL_SPLIT_PATTERN : EOL_SPLIT_PATTERN_WITH_EMPTY).split(string);
2149   }
2150
2151   @NotNull
2152   public static List<Pair<String, Integer>> getWordsWithOffset(@NotNull String s) {
2153     List<Pair<String, Integer>> res = ContainerUtil.newArrayList();
2154     s += " ";
2155     StringBuilder name = new StringBuilder();
2156     int startInd = -1;
2157     for (int i = 0; i < s.length(); i++) {
2158       if (Character.isWhitespace(s.charAt(i))) {
2159         if (name.length() > 0) {
2160           res.add(Pair.create(name.toString(), startInd));
2161           name.setLength(0);
2162           startInd = -1;
2163         }
2164       }
2165       else {
2166         if (startInd == -1) {
2167           startInd = i;
2168         }
2169         name.append(s.charAt(i));
2170       }
2171     }
2172     return res;
2173   }
2174
2175   /**
2176    * Implementation of "Sorting for Humans: Natural Sort Order":
2177    * http://www.codinghorror.com/blog/2007/12/sorting-for-humans-natural-sort-order.html
2178    */
2179   public static int naturalCompare(@NotNull String string1, @NotNull String string2) {
2180     return naturalCompare(string1, string2, false);
2181   }
2182
2183   private static int naturalCompare(@NotNull String string1, @NotNull String string2, boolean caseSensitive) {
2184     final int string1Length = string1.length();
2185     final int string2Length = string2.length();
2186     for (int i = 0, j = 0; i < string1Length && j < string2Length; i++, j++) {
2187       char ch1 = string1.charAt(i);
2188       char ch2 = string2.charAt(j);
2189       if ((isDigit(ch1) || ch1 == ' ') && (isDigit(ch2) || ch2 == ' ')) {
2190         int startNum1 = i;
2191         while (ch1 == ' ' || ch1 == '0') { // skip leading spaces and zeros
2192           startNum1++;
2193           if (startNum1 >= string1Length) break;
2194           ch1 = string1.charAt(startNum1);
2195         }
2196         int startNum2 = j;
2197         while (ch2 == ' ' || ch2 == '0') {
2198           startNum2++;
2199           if (startNum2 >= string2Length) break;
2200           ch2 = string2.charAt(startNum2);
2201         }
2202         i = startNum1;
2203         j = startNum2;
2204         while (i < string1Length && isDigit(string1.charAt(i))) i++;
2205         while (j < string2Length && isDigit(string2.charAt(j))) j++;
2206         String digits1 = string1.substring(startNum1, i);
2207         String digits2 = string2.substring(startNum2, j);
2208         if (digits1.length() != digits2.length()) {
2209           return digits1.length() - digits2.length();
2210         }
2211         int numberDiff = digits1.compareTo(digits2);
2212         if (numberDiff != 0) {
2213           return numberDiff;
2214         }
2215         i--;
2216         j--;
2217         final int lengthDiff = (i - startNum1) - (j - startNum2);
2218         if (lengthDiff != 0) {
2219           return lengthDiff;
2220         }
2221         for (; startNum1 < i; startNum1++, startNum2++) {
2222           final int diff = string1.charAt(startNum1) - string2.charAt(startNum2);
2223           if (diff != 0) {
2224             return diff;
2225           }
2226         }
2227       }
2228       else {
2229         if (caseSensitive) {
2230           return ch1 - ch2;
2231         }
2232         else {
2233           // similar logic to charsMatch() below
2234           if (ch1 != ch2) {
2235             final int diff1 = StringUtilRt.toUpperCase(ch1) - StringUtilRt.toUpperCase(ch2);
2236             if (diff1 != 0) {
2237               final int diff2 = StringUtilRt.toLowerCase(ch1) - StringUtilRt.toLowerCase(ch2);
2238               if (diff2 != 0) {
2239                 return diff2;
2240               }
2241             }
2242           }
2243         }
2244       }
2245     }
2246     if (!caseSensitive && string1Length == string2Length) {
2247       // do case sensitive compare if case insensitive strings are equal
2248       return naturalCompare(string1, string2, true);
2249     }
2250     return string1Length - string2Length;
2251   }
2252
2253   private static boolean isDigit(char c) {
2254     return c >= '0' && c <= '9';
2255   }
2256
2257   public static int compare(@Nullable String s1, @Nullable String s2, boolean ignoreCase) {
2258     //noinspection StringEquality
2259     if (s1 == s2) return 0;
2260     if (s1 == null) return -1;
2261     if (s2 == null) return 1;
2262     return ignoreCase ? s1.compareToIgnoreCase(s2) : s1.compareTo(s2);
2263   }
2264
2265   public static int comparePairs(@Nullable String s1, @Nullable String t1, @Nullable String s2, @Nullable String t2, boolean ignoreCase) {
2266     final int compare = compare(s1, s2, ignoreCase);
2267     return compare != 0 ? compare : compare(t1, t2, ignoreCase);
2268   }
2269
2270   public static int hashCode(@NotNull CharSequence s) {
2271     return stringHashCode(s);
2272   }
2273
2274   public static boolean equals(@Nullable CharSequence s1, @Nullable CharSequence s2) {
2275     if (s1 == null ^ s2 == null) {
2276       return false;
2277     }
2278
2279     if (s1 == null) {
2280       return true;
2281     }
2282
2283     if (s1.length() != s2.length()) {
2284       return false;
2285     }
2286     for (int i = 0; i < s1.length(); i++) {
2287       if (s1.charAt(i) != s2.charAt(i)) {
2288         return false;
2289       }
2290     }
2291     return true;
2292   }
2293
2294   public static boolean equalsIgnoreCase(@Nullable CharSequence s1, @Nullable CharSequence s2) {
2295     if (s1 == null ^ s2 == null) {
2296       return false;
2297     }
2298
2299     if (s1 == null) {
2300       return true;
2301     }
2302
2303     if (s1.length() != s2.length()) {
2304       return false;
2305     }
2306     for (int i = 0; i < s1.length(); i++) {
2307       if (!charsMatch(s1.charAt(i),s2.charAt(i), true)) {
2308         return false;
2309       }
2310     }
2311     return true;
2312   }
2313
2314   public static int compare(char c1, char c2, boolean ignoreCase) {
2315     // duplicating String.equalsIgnoreCase logic
2316     int d = c1 - c2;
2317     if (d == 0 || !ignoreCase) {
2318       return d;
2319     }
2320     // If characters don't match but case may be ignored,
2321     // try converting both characters to uppercase.
2322     // If the results match, then the comparison scan should
2323     // continue.
2324     char u1 = StringUtilRt.toUpperCase(c1);
2325     char u2 = StringUtilRt.toUpperCase(c2);
2326     d = u1 - u2;
2327     if (d != 0) {
2328       // Unfortunately, conversion to uppercase does not work properly
2329       // for the Georgian alphabet, which has strange rules about case
2330       // conversion.  So we need to make one last check before
2331       // exiting.
2332       d = StringUtilRt.toLowerCase(u1) - StringUtilRt.toLowerCase(u2);
2333     }
2334     return d;
2335   }
2336
2337   public static boolean charsMatch(char c1, char c2, boolean ignoreCase) {
2338     return compare(c1,c2,ignoreCase) == 0;
2339   }
2340
2341   @NotNull
2342   public static String formatLinks(@NotNull String message) {
2343     Pattern linkPattern = Pattern.compile("http://[a-zA-Z0-9\\./\\-\\+]+");
2344     StringBuffer result = new StringBuffer();
2345     Matcher m = linkPattern.matcher(message);
2346     while (m.find()) {
2347       m.appendReplacement(result, "<a href=\"" + m.group() + "\">" + m.group() + "</a>");
2348     }
2349     m.appendTail(result);
2350     return result.toString();
2351   }
2352
2353   public static boolean isHexDigit(char c) {
2354     return '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F';
2355   }
2356
2357   public static boolean isOctalDigit(char c) {
2358     return '0' <= c && c <= '7';
2359   }
2360
2361   @NotNull
2362   public static String shortenTextWithEllipsis(@NotNull final String text, final int maxLength, final int suffixLength) {
2363     return shortenTextWithEllipsis(text, maxLength, suffixLength, false);
2364   }
2365
2366   @NotNull
2367   public static String shortenTextWithEllipsis(@NotNull final String text, final int maxLength, final int suffixLength, boolean useEllipsisSymbol) {
2368     final int prefix_length = maxLength - suffixLength - 3;
2369     assert prefix_length > 0;
2370     String result;
2371     final int textLength = text.length();
2372     if (textLength > maxLength) {
2373       result = text.substring(0, prefix_length) + (useEllipsisSymbol ? "\u2026" : "...") + text.substring(textLength - suffixLength);
2374     }
2375     else {
2376       result = text;
2377     }
2378     return result;
2379   }
2380
2381   @NotNull
2382   public static String shortenPathWithEllipsis(@NotNull final String path, final int maxLength, boolean useEllipsisSymbol) {
2383     return shortenTextWithEllipsis(path, maxLength, (int)(maxLength * 0.7), useEllipsisSymbol);
2384   }
2385
2386   @NotNull
2387   public static String shortenPathWithEllipsis(@NotNull final String path, final int maxLength) {
2388     return shortenPathWithEllipsis(path, maxLength, false);
2389   }
2390
2391   public static boolean charsEqual(char a, char b, boolean ignoreCase) {
2392     return ignoreCase ? charsEqualIgnoreCase(a, b) : a == b;
2393   }
2394   public static boolean charsEqualIgnoreCase(char a, char b) {
2395     return StringUtilRt.charsEqualIgnoreCase(a, b);
2396   }
2397
2398   public static char toUpperCase(char a) {
2399     return StringUtilRt.toUpperCase(a);
2400   }
2401
2402   @NotNull
2403   public static String toUpperCase(@NotNull String a) {
2404     return StringUtilRt.toUpperCase(a);
2405   }
2406
2407   public static char toLowerCase(final char a) {
2408     return StringUtilRt.toLowerCase(a);
2409   }
2410
2411   @NotNull
2412   public static String convertLineSeparators(@NotNull String text) {
2413     return StringUtilRt.convertLineSeparators(text);
2414   }
2415
2416   @NotNull
2417   public static String convertLineSeparators(@NotNull String text, boolean keepCarriageReturn) {
2418     return StringUtilRt.convertLineSeparators(text, keepCarriageReturn);
2419   }
2420
2421   @NotNull
2422   public static String convertLineSeparators(@NotNull String text, @NotNull String newSeparator) {
2423     return StringUtilRt.convertLineSeparators(text, newSeparator);
2424   }
2425
2426   @NotNull
2427   public static String convertLineSeparators(@NotNull String text, @NotNull String newSeparator, @Nullable int[] offsetsToKeep) {
2428     return StringUtilRt.convertLineSeparators(text, newSeparator, offsetsToKeep);
2429   }
2430
2431   @NotNull
2432   public static String convertLineSeparators(@NotNull String text,
2433                                              @NotNull String newSeparator,
2434                                              @Nullable int[] offsetsToKeep,
2435                                              boolean keepCarriageReturn) {
2436     return StringUtilRt.convertLineSeparators(text, newSeparator, offsetsToKeep, keepCarriageReturn);
2437   }
2438
2439   public static int parseInt(final String string, final int defaultValue) {
2440     return StringUtilRt.parseInt(string, defaultValue);
2441   }
2442
2443   public static double parseDouble(final String string, final double defaultValue) {
2444     return StringUtilRt.parseDouble(string, defaultValue);
2445   }
2446
2447   public static boolean parseBoolean(String string, final boolean defaultValue) {
2448     return StringUtilRt.parseBoolean(string, defaultValue);
2449   }
2450
2451   @NotNull
2452   public static String getShortName(@NotNull Class aClass) {
2453     return StringUtilRt.getShortName(aClass);
2454   }
2455
2456   @NotNull
2457   public static String getShortName(@NotNull String fqName) {
2458     return StringUtilRt.getShortName(fqName);
2459   }
2460
2461   @NotNull
2462   public static String getShortName(@NotNull String fqName, char separator) {
2463     return StringUtilRt.getShortName(fqName, separator);
2464   }
2465
2466   @NotNull
2467   public static CharSequence newBombedCharSequence(@NotNull CharSequence sequence, long delay) {
2468     final long myTime = System.currentTimeMillis() + delay;
2469     return new BombedCharSequence(sequence) {
2470       @Override
2471       protected void checkCanceled() {
2472         long l = System.currentTimeMillis();
2473         if (l >= myTime) {
2474           throw new ProcessCanceledException();
2475         }
2476       }
2477     };
2478   }
2479
2480   public static boolean trimEnd(@NotNull StringBuilder buffer, @NotNull CharSequence end) {
2481     if (endsWith(buffer, end)) {
2482       buffer.delete(buffer.length() - end.length(), buffer.length());
2483       return true;
2484     }
2485     return false;
2486   }
2487
2488   private static boolean trimStart(@NotNull StringBuilder buffer, @NotNull CharSequence start) {
2489     if (startsWith(buffer, start)) {
2490       buffer.delete(0, start.length());
2491       return true;
2492     }
2493     return false;
2494   }
2495
2496   /**
2497    * Say smallPart = "op" and bigPart="open". Method returns true for "Ope" and false for "ops"
2498    */
2499   public static boolean isBetween(@NotNull String string, @NotNull String smallPart, @NotNull String bigPart) {
2500     final String s = string.toLowerCase();
2501     return s.startsWith(smallPart.toLowerCase()) && bigPart.toLowerCase().startsWith(s);
2502   }
2503
2504   /**
2505    * Expirable CharSequence. Very useful to control external library execution time,
2506    * i.e. when java.util.regex.Pattern match goes out of control.
2507    */
2508   public abstract static class BombedCharSequence implements CharSequence {
2509     private CharSequence delegate;
2510     private int i = 0;
2511
2512     public BombedCharSequence(@NotNull CharSequence sequence) {
2513       delegate = sequence;
2514     }
2515
2516     @Override
2517     public int length() {
2518       check();
2519       return delegate.length();
2520     }
2521
2522     @Override
2523     public char charAt(int i) {
2524       check();
2525       return delegate.charAt(i);
2526     }
2527
2528     protected void check() {
2529       if ((++i & 1023) == 0) {
2530         checkCanceled();
2531       }
2532     }
2533
2534     @NotNull
2535     @Override
2536     public String toString() {
2537       check();
2538       return delegate.toString();
2539     }
2540
2541     protected abstract void checkCanceled();
2542
2543     @Override
2544     public CharSequence subSequence(int i, int i1) {
2545       check();
2546       return delegate.subSequence(i, i1);
2547     }
2548   }
2549 }