rename startIndex => index
[idea/community.git] / platform / util / src / com / intellij / openapi / util / text / StringUtil.java
1 /*
2  * Copyright 2000-2016 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.openapi.diagnostic.Logger;
19 import com.intellij.openapi.progress.ProcessCanceledException;
20 import com.intellij.openapi.util.Pair;
21 import com.intellij.openapi.util.TextRange;
22 import com.intellij.util.*;
23 import com.intellij.util.containers.ContainerUtil;
24 import com.intellij.util.text.CharArrayUtil;
25 import com.intellij.util.text.CharSequenceSubSequence;
26 import com.intellij.util.text.StringFactory;
27 import org.jetbrains.annotations.Contract;
28 import org.jetbrains.annotations.NonNls;
29 import org.jetbrains.annotations.NotNull;
30 import org.jetbrains.annotations.Nullable;
31
32 import javax.swing.text.MutableAttributeSet;
33 import javax.swing.text.html.HTML;
34 import javax.swing.text.html.HTMLEditorKit;
35 import javax.swing.text.html.parser.ParserDelegator;
36 import java.beans.Introspector;
37 import java.io.IOException;
38 import java.io.Reader;
39 import java.io.StringReader;
40 import java.util.*;
41 import java.util.regex.Matcher;
42 import java.util.regex.Pattern;
43
44 import static java.lang.Math.max;
45 import static java.lang.Math.min;
46
47 //TeamCity inherits StringUtil: do not add private constructors!!!
48 @SuppressWarnings({"UtilityClassWithoutPrivateConstructor", "MethodOverridesStaticMethodOfSuperclass"})
49 public class StringUtil extends StringUtilRt {
50   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.util.text.StringUtil");
51
52   @NonNls private static final String VOWELS = "aeiouy";
53   @NonNls private static final Pattern EOL_SPLIT_KEEP_SEPARATORS = Pattern.compile("(?<=(\r\n|\n))|(?<=\r)(?=[^\n])");
54   @NonNls private static final Pattern EOL_SPLIT_PATTERN = Pattern.compile(" *(\r|\n|\r\n)+ *");
55   @NonNls private static final Pattern EOL_SPLIT_PATTERN_WITH_EMPTY = Pattern.compile(" *(\r|\n|\r\n) *");
56   @NonNls private static final Pattern EOL_SPLIT_DONT_TRIM_PATTERN = Pattern.compile("(\r|\n|\r\n)+");
57
58   private static class MyHtml2Text extends HTMLEditorKit.ParserCallback {
59     @NotNull private final StringBuilder myBuffer = new StringBuilder();
60
61     public void parse(Reader in) throws IOException {
62       myBuffer.setLength(0);
63       new ParserDelegator().parse(in, this, Boolean.TRUE);
64     }
65
66     @Override
67     public void handleText(char[] text, int pos) {
68       myBuffer.append(text);
69     }
70
71     @Override
72     public void handleStartTag(HTML.Tag tag, MutableAttributeSet set, int i) {
73       handleTag(tag);
74     }
75
76     @Override
77     public void handleSimpleTag(HTML.Tag tag, MutableAttributeSet set, int i) {
78       handleTag(tag);
79     }
80
81     private void handleTag(HTML.Tag tag) {
82       if (tag.breaksFlow() && myBuffer.length() > 0) {
83         myBuffer.append(SystemProperties.getLineSeparator());
84       }
85     }
86
87     public String getText() {
88       return myBuffer.toString();
89     }
90   }
91
92   private static final MyHtml2Text html2TextParser = new MyHtml2Text();
93
94   public static final NotNullFunction<String, String> QUOTER = new NotNullFunction<String, String>() {
95     @Override
96     @NotNull
97     public String fun(String s) {
98       return "\"" + s + "\"";
99     }
100   };
101
102   public static final NotNullFunction<String, String> SINGLE_QUOTER = new NotNullFunction<String, String>() {
103     @Override
104     @NotNull
105     public String fun(String s) {
106       return "'" + s + "'";
107     }
108   };
109
110   @NotNull
111   @Contract(pure = true)
112   public static List<String> getWordsInStringLongestFirst(@NotNull String find) {
113     List<String> words = getWordsIn(find);
114     // hope long words are rare
115     Collections.sort(words, new Comparator<String>() {
116       @Override
117       public int compare(@NotNull final String o1, @NotNull final String o2) {
118         return o2.length() - o1.length();
119       }
120     });
121     return words;
122   }
123
124   @NotNull
125   @Contract(pure = true)
126   public static String escapePattern(@NotNull final String text) {
127     return replace(replace(text, "'", "''"), "{", "'{'");
128   }
129
130   @NotNull
131   @Contract(pure = true)
132   public static <T> Function<T, String> createToStringFunction(@NotNull Class<T> cls) {
133     return new Function<T, String>() {
134       @Override
135       public String fun(@NotNull T o) {
136         return o.toString();
137       }
138     };
139   }
140
141   @NotNull
142   public static Function<String, String> TRIMMER = new Function<String, String>() {
143     @Nullable
144     @Override
145     public String fun(@Nullable String s) {
146       return trim(s);
147     }
148   };
149
150   @NotNull
151   @Contract(pure = true)
152   public static String replace(@NonNls @NotNull String text, @NonNls @NotNull String oldS, @NonNls @NotNull String newS) {
153     return replace(text, oldS, newS, false);
154   }
155
156   @NotNull
157   @Contract(pure = true)
158   public static String replaceIgnoreCase(@NonNls @NotNull String text, @NonNls @NotNull String oldS, @NonNls @NotNull String newS) {
159     return replace(text, oldS, newS, true);
160   }
161
162   public static void replaceChar(@NotNull char[] buffer, char oldChar, char newChar, int start, int end) {
163     for (int i = start; i < end; i++) {
164       char c = buffer[i];
165       if (c == oldChar) {
166         buffer[i] = newChar;
167       }
168     }
169   }
170
171   @NotNull
172   @Contract(pure = true)
173   public static String replaceChar(@NotNull String buffer, char oldChar, char newChar) {
174     StringBuilder newBuffer = null;
175     for (int i = 0; i < buffer.length(); i++) {
176       char c = buffer.charAt(i);
177       if (c == oldChar) {
178         if (newBuffer == null) {
179           newBuffer = new StringBuilder(buffer.length());
180           newBuffer.append(buffer, 0, i);
181         }
182
183         newBuffer.append(newChar);
184       }
185       else if (newBuffer != null) {
186         newBuffer.append(c);
187       }
188     }
189     return newBuffer == null ? buffer : newBuffer.toString();
190   }
191
192   @Contract(pure = true)
193   public static String replace(@NonNls @NotNull final String text, @NonNls @NotNull final String oldS, @NonNls @NotNull final String newS, final boolean ignoreCase) {
194     if (text.length() < oldS.length()) return text;
195
196     StringBuilder newText = null;
197     int i = 0;
198
199     while (i < text.length()) {
200       final int index = ignoreCase? indexOfIgnoreCase(text, oldS, i) : text.indexOf(oldS, i);
201       if (index < 0) {
202         if (i == 0) {
203           return text;
204         }
205
206         newText.append(text, i, text.length());
207         break;
208       }
209       else {
210         if (newText == null) {
211           if (text.length() == oldS.length()) {
212             return newS;
213           }
214           newText = new StringBuilder(text.length() - i);
215         }
216
217         newText.append(text, i, index);
218         newText.append(newS);
219         i = index + oldS.length();
220       }
221     }
222     return newText != null ? newText.toString() : "";
223   }
224
225   /**
226    * Implementation copied from {@link String#indexOf(String, int)} except character comparisons made case insensitive
227    */
228   @Contract(pure = true)
229   public static int indexOfIgnoreCase(@NotNull String where, @NotNull String what, int fromIndex) {
230     int targetCount = what.length();
231     int sourceCount = where.length();
232
233     if (fromIndex >= sourceCount) {
234       return targetCount == 0 ? sourceCount : -1;
235     }
236
237     if (fromIndex < 0) {
238       fromIndex = 0;
239     }
240
241     if (targetCount == 0) {
242       return fromIndex;
243     }
244
245     char first = what.charAt(0);
246     int max = sourceCount - targetCount;
247
248     for (int i = fromIndex; i <= max; i++) {
249       /* Look for first character. */
250       if (!charsEqualIgnoreCase(where.charAt(i), first)) {
251         while (++i <= max && !charsEqualIgnoreCase(where.charAt(i), first)) ;
252       }
253
254       /* Found first character, now look at the rest of v2 */
255       if (i <= max) {
256         int j = i + 1;
257         int end = j + targetCount - 1;
258         for (int k = 1; j < end && charsEqualIgnoreCase(where.charAt(j), what.charAt(k)); j++, k++) ;
259
260         if (j == end) {
261           /* Found whole string. */
262           return i;
263         }
264       }
265     }
266
267     return -1;
268   }
269
270   @Contract(pure = true)
271   public static int indexOfIgnoreCase(@NotNull String where, char what, int fromIndex) {
272     int sourceCount = where.length();
273     for (int i = max(fromIndex, 0); i < sourceCount; i++) {
274       if (charsEqualIgnoreCase(where.charAt(i), what)) {
275         return i;
276       }
277     }
278
279     return -1;
280   }
281
282   @Contract(pure = true)
283   public static boolean containsIgnoreCase(@NotNull String where, @NotNull String what) {
284     return indexOfIgnoreCase(where, what, 0) >= 0;
285   }
286
287   @Contract(pure = true)
288   public static boolean endsWithIgnoreCase(@NonNls @NotNull String str, @NonNls @NotNull String suffix) {
289     return StringUtilRt.endsWithIgnoreCase(str, suffix);
290   }
291
292   @Contract(pure = true)
293   public static boolean startsWithIgnoreCase(@NonNls @NotNull String str, @NonNls @NotNull String prefix) {
294     return StringUtilRt.startsWithIgnoreCase(str, prefix);
295   }
296
297   @Contract(pure = true)
298   @NotNull
299   public static String stripHtml(@NotNull String html, boolean convertBreaks) {
300     if (convertBreaks) {
301       html = html.replaceAll("<br/?>", "\n\n");
302     }
303
304     return html.replaceAll("<(.|\n)*?>", "");
305   }
306
307   @Contract(value = "null -> null; !null -> !null", pure = true)
308   public static String toLowerCase(@Nullable final String str) {
309     //noinspection ConstantConditions
310     return str == null ? null : str.toLowerCase();
311   }
312
313   @NotNull
314   @Contract(pure = true)
315   public static String getPackageName(@NotNull String fqName) {
316     return getPackageName(fqName, '.');
317   }
318
319   /**
320    * Given a fqName returns the package name for the type or the containing type.
321    * <p/>
322    * <ul>
323    * <li><code>java.lang.String</code> -> <code>java.lang</code></li>
324    * <li><code>java.util.Map.Entry</code> -> <code>java.util.Map</code></li>
325    * </ul>
326    *
327    * @param fqName    a fully qualified type name. Not supposed to contain any type arguments
328    * @param separator the separator to use. Typically '.'
329    * @return the package name of the type or the declarator of the type. The empty string if the given fqName is unqualified
330    */
331   @NotNull
332   @Contract(pure = true)
333   public static String getPackageName(@NotNull String fqName, char separator) {
334     int lastPointIdx = fqName.lastIndexOf(separator);
335     if (lastPointIdx >= 0) {
336       return fqName.substring(0, lastPointIdx);
337     }
338     return "";
339   }
340
341   @Contract(pure = true)
342   public static int getLineBreakCount(@NotNull CharSequence text) {
343     int count = 0;
344     for (int i = 0; i < text.length(); i++) {
345       char c = text.charAt(i);
346       if (c == '\n') {
347         count++;
348       }
349       else if (c == '\r') {
350         if (i + 1 < text.length() && text.charAt(i + 1) == '\n') {
351           //noinspection AssignmentToForLoopParameter
352           i++;
353           count++;
354         }
355         else {
356           count++;
357         }
358       }
359     }
360     return count;
361   }
362
363   @Contract(pure = true)
364   public static boolean containsLineBreak(@NotNull CharSequence text) {
365     for (int i = 0; i < text.length(); i++) {
366       char c = text.charAt(i);
367       if (isLineBreak(c)) return true;
368     }
369     return false;
370   }
371
372   @Contract(pure = true)
373   public static boolean isLineBreak(char c) {
374     return c == '\n' || c == '\r';
375   }
376
377   @NotNull
378   @Contract(pure = true)
379   public static String escapeLineBreak(@NotNull String text) {
380     StringBuilder buffer = new StringBuilder(text.length());
381     for (int i = 0; i < text.length(); i++) {
382       char c = text.charAt(i);
383       switch (c) {
384         case '\n':
385           buffer.append("\\n");
386           break;
387         case '\r':
388           buffer.append("\\r");
389           break;
390         default:
391           buffer.append(c);
392       }
393     }
394     return buffer.toString();
395   }
396
397   @Contract(pure = true)
398   public static boolean endsWithLineBreak(@NotNull CharSequence text) {
399     int len = text.length();
400     return len > 0 && isLineBreak(text.charAt(len - 1));
401   }
402
403   @Contract(pure = true)
404   public static int lineColToOffset(@NotNull CharSequence text, int line, int col) {
405     int curLine = 0;
406     int offset = 0;
407     while (line != curLine) {
408       if (offset == text.length()) return -1;
409       char c = text.charAt(offset);
410       if (c == '\n') {
411         curLine++;
412       }
413       else if (c == '\r') {
414         curLine++;
415         if (offset < text.length() - 1 && text.charAt(offset + 1) == '\n') {
416           offset++;
417         }
418       }
419       offset++;
420     }
421     return offset + col;
422   }
423
424   @Contract(pure = true)
425   public static int offsetToLineNumber(@NotNull CharSequence text, int offset) {
426     int curLine = 0;
427     int curOffset = 0;
428     while (curOffset < offset) {
429       if (curOffset == text.length()) return -1;
430       char c = text.charAt(curOffset);
431       if (c == '\n') {
432         curLine++;
433       }
434       else if (c == '\r') {
435         curLine++;
436         if (curOffset < text.length() - 1 && text.charAt(curOffset + 1) == '\n') {
437           curOffset++;
438         }
439       }
440       curOffset++;
441     }
442     return curLine;
443   }
444
445   /**
446    * Classic dynamic programming algorithm for string differences.
447    */
448   @Contract(pure = true)
449   public static int difference(@NotNull String s1, @NotNull String s2) {
450     int[][] a = new int[s1.length()][s2.length()];
451
452     for (int i = 0; i < s1.length(); i++) {
453       a[i][0] = i;
454     }
455
456     for (int j = 0; j < s2.length(); j++) {
457       a[0][j] = j;
458     }
459
460     for (int i = 1; i < s1.length(); i++) {
461       for (int j = 1; j < s2.length(); j++) {
462
463         a[i][j] = min(min(a[i - 1][j - 1] + (s1.charAt(i) == s2.charAt(j) ? 0 : 1), a[i - 1][j] + 1), a[i][j - 1] + 1);
464       }
465     }
466
467     return a[s1.length() - 1][s2.length() - 1];
468   }
469
470   @NotNull
471   @Contract(pure = true)
472   public static String wordsToBeginFromUpperCase(@NotNull String s) {
473     return fixCapitalization(s, ourPrepositions, true);
474   }
475
476   @NotNull
477   @Contract(pure = true)
478   public static String wordsToBeginFromLowerCase(@NotNull String s) {
479     return fixCapitalization(s, ourPrepositions, false);
480   }
481
482   @NotNull
483   @Contract(pure = true)
484   public static String toTitleCase(@NotNull String s) {
485     return fixCapitalization(s, ArrayUtil.EMPTY_STRING_ARRAY, true);
486   }
487
488   @NotNull
489   private static String fixCapitalization(@NotNull String s, @NotNull String[] prepositions, boolean title) {
490     StringBuilder buffer = null;
491     for (int i = 0; i < s.length(); i++) {
492       char prevChar = i == 0 ? ' ' : s.charAt(i - 1);
493       char currChar = s.charAt(i);
494       if (!Character.isLetterOrDigit(prevChar) && prevChar != '\'') {
495         if (Character.isLetterOrDigit(currChar)) {
496           if (title || Character.isUpperCase(currChar)) {
497             int j = i;
498             for (; j < s.length(); j++) {
499               if (!Character.isLetterOrDigit(s.charAt(j))) {
500                 break;
501               }
502             }
503             if (!title && j > i + 1 && !Character.isLowerCase(s.charAt(i + 1))) {
504               // filter out abbreviations like I18n, SQL and CSS
505               continue;
506             }
507             if (!isPreposition(s, i, j - 1, prepositions)) {
508               if (buffer == null) {
509                 buffer = new StringBuilder(s);
510               }
511               buffer.setCharAt(i, title ? toUpperCase(currChar) : toLowerCase(currChar));
512             }
513           }
514         }
515       }
516     }
517     return buffer == null ? s : buffer.toString();
518   }
519
520   @NonNls private static final String[] ourPrepositions = {
521     "a", "an", "and", "as", "at", "but", "by", "down", "for", "from", "if", "in", "into", "not", "of", "on", "onto", "or", "out", "over",
522     "per", "nor", "the", "to", "up", "upon", "via", "with"
523   };
524
525   @Contract(pure = true)
526   public static boolean isPreposition(@NotNull String s, int firstChar, int lastChar) {
527     return isPreposition(s, firstChar, lastChar, ourPrepositions);
528   }
529
530   @Contract(pure = true)
531   public static boolean isPreposition(@NotNull String s, int firstChar, int lastChar, @NotNull String[] prepositions) {
532     for (String preposition : prepositions) {
533       boolean found = false;
534       if (lastChar - firstChar + 1 == preposition.length()) {
535         found = true;
536         for (int j = 0; j < preposition.length(); j++) {
537           if (toLowerCase(s.charAt(firstChar + j)) != preposition.charAt(j)) {
538             found = false;
539           }
540         }
541       }
542       if (found) {
543         return true;
544       }
545     }
546     return false;
547   }
548
549   @NotNull
550   @Contract(pure = true)
551   public static NotNullFunction<String, String> escaper(final boolean escapeSlash, @Nullable final String additionalChars) {
552     return new NotNullFunction<String, String>() {
553       @NotNull
554       @Override
555       public String fun(@NotNull String dom) {
556         final StringBuilder builder = new StringBuilder(dom.length());
557         escapeStringCharacters(dom.length(), dom, additionalChars, escapeSlash, builder);
558         return builder.toString();
559       }
560     };
561   }
562
563
564   public static void escapeStringCharacters(int length, @NotNull String str, @NotNull @NonNls StringBuilder buffer) {
565     escapeStringCharacters(length, str, "\"", buffer);
566   }
567
568   @NotNull
569   public static StringBuilder escapeStringCharacters(int length,
570                                                      @NotNull String str,
571                                                      @Nullable String additionalChars,
572                                                      @NotNull @NonNls StringBuilder buffer) {
573     return escapeStringCharacters(length, str, additionalChars, true, buffer);
574   }
575
576   @NotNull
577   public static StringBuilder escapeStringCharacters(int length,
578                                                      @NotNull String str,
579                                                      @Nullable String additionalChars,
580                                                      boolean escapeSlash,
581                                                      @NotNull @NonNls StringBuilder buffer) {
582     return escapeStringCharacters(length, str, additionalChars, escapeSlash, true, buffer);
583   }
584
585   @NotNull
586   public static StringBuilder escapeStringCharacters(int length,
587                                                      @NotNull String str,
588                                                      @Nullable String additionalChars,
589                                                      boolean escapeSlash,
590                                                      boolean escapeUnicode,
591                                                      @NotNull @NonNls StringBuilder buffer) {
592     char prev = 0;
593     for (int idx = 0; idx < length; idx++) {
594       char ch = str.charAt(idx);
595       switch (ch) {
596         case '\b':
597           buffer.append("\\b");
598           break;
599
600         case '\t':
601           buffer.append("\\t");
602           break;
603
604         case '\n':
605           buffer.append("\\n");
606           break;
607
608         case '\f':
609           buffer.append("\\f");
610           break;
611
612         case '\r':
613           buffer.append("\\r");
614           break;
615
616         default:
617           if (escapeSlash && ch == '\\') {
618             buffer.append("\\\\");
619           }
620           else if (additionalChars != null && additionalChars.indexOf(ch) > -1 && (escapeSlash || prev != '\\')) {
621             buffer.append("\\").append(ch);
622           }
623           else if (escapeUnicode && !isPrintableUnicode(ch)) {
624             CharSequence hexCode = StringUtilRt.toUpperCase(Integer.toHexString(ch));
625             buffer.append("\\u");
626             int paddingCount = 4 - hexCode.length();
627             while (paddingCount-- > 0) {
628               buffer.append(0);
629             }
630             buffer.append(hexCode);
631           }
632           else {
633             buffer.append(ch);
634           }
635       }
636       prev = ch;
637     }
638     return buffer;
639   }
640
641   @Contract(pure = true)
642   public static boolean isPrintableUnicode(char c) {
643     int t = Character.getType(c);
644     return t != Character.UNASSIGNED && t != Character.LINE_SEPARATOR && t != Character.PARAGRAPH_SEPARATOR &&
645            t != Character.CONTROL && t != Character.FORMAT && t != Character.PRIVATE_USE && t != Character.SURROGATE;
646   }
647
648   @NotNull
649   @Contract(pure = true)
650   public static String escapeStringCharacters(@NotNull String s) {
651     StringBuilder buffer = new StringBuilder(s.length());
652     escapeStringCharacters(s.length(), s, "\"", buffer);
653     return buffer.toString();
654   }
655
656   @NotNull
657   @Contract(pure = true)
658   public static String escapeCharCharacters(@NotNull String s) {
659     StringBuilder buffer = new StringBuilder(s.length());
660     escapeStringCharacters(s.length(), s, "\'", buffer);
661     return buffer.toString();
662   }
663
664   @NotNull
665   @Contract(pure = true)
666   public static String unescapeStringCharacters(@NotNull String s) {
667     StringBuilder buffer = new StringBuilder(s.length());
668     unescapeStringCharacters(s.length(), s, buffer);
669     return buffer.toString();
670   }
671
672   private static boolean isQuoteAt(@NotNull String s, int ind) {
673     char ch = s.charAt(ind);
674     return ch == '\'' || ch == '\"';
675   }
676
677   @Contract(pure = true)
678   public static boolean isQuotedString(@NotNull String s) {
679     return s.length() > 1 && isQuoteAt(s, 0) && s.charAt(0) == s.charAt(s.length() - 1);
680   }
681
682   @NotNull
683   @Contract(pure = true)
684   public static String unquoteString(@NotNull String s) {
685     if (isQuotedString(s)) {
686       return s.substring(1, s.length() - 1);
687     }
688     return s;
689   }
690
691   @NotNull
692   @Contract(pure = true)
693   public static String unquoteString(@NotNull String s, char quotationChar) {
694     if (s.length() > 1 && quotationChar == s.charAt(0) && quotationChar == s.charAt(s.length() - 1)) {
695       return s.substring(1, s.length() - 1);
696     }
697     return s;
698   }
699
700   /**
701    * This is just an optimized version of Matcher.quoteReplacement
702    */
703   @NotNull
704   @Contract(pure = true)
705   public static String quoteReplacement(@NotNull String s) {
706     boolean needReplacements = false;
707
708     for (int i = 0; i < s.length(); i++) {
709       char c = s.charAt(i);
710       if (c == '\\' || c == '$') {
711         needReplacements = true;
712         break;
713       }
714     }
715
716     if (!needReplacements) return s;
717
718     StringBuilder sb = new StringBuilder(s.length() * 6 / 5);
719     for (int i = 0; i < s.length(); i++) {
720       char c = s.charAt(i);
721       if (c == '\\') {
722         sb.append('\\');
723         sb.append('\\');
724       }
725       else if (c == '$') {
726         sb.append('\\');
727         sb.append('$');
728       }
729       else {
730         sb.append(c);
731       }
732     }
733     return sb.toString();
734   }
735
736   private static void unescapeStringCharacters(int length, @NotNull String s, @NotNull StringBuilder buffer) {
737     boolean escaped = false;
738     for (int idx = 0; idx < length; idx++) {
739       char ch = s.charAt(idx);
740       if (!escaped) {
741         if (ch == '\\') {
742           escaped = true;
743         }
744         else {
745           buffer.append(ch);
746         }
747       }
748       else {
749         int octalEscapeMaxLength = 2;
750         switch (ch) {
751           case 'n':
752             buffer.append('\n');
753             break;
754
755           case 'r':
756             buffer.append('\r');
757             break;
758
759           case 'b':
760             buffer.append('\b');
761             break;
762
763           case 't':
764             buffer.append('\t');
765             break;
766
767           case 'f':
768             buffer.append('\f');
769             break;
770
771           case '\'':
772             buffer.append('\'');
773             break;
774
775           case '\"':
776             buffer.append('\"');
777             break;
778
779           case '\\':
780             buffer.append('\\');
781             break;
782
783           case 'u':
784             if (idx + 4 < length) {
785               try {
786                 int code = Integer.parseInt(s.substring(idx + 1, idx + 5), 16);
787                 //noinspection AssignmentToForLoopParameter
788                 idx += 4;
789                 buffer.append((char)code);
790               }
791               catch (NumberFormatException e) {
792                 buffer.append("\\u");
793               }
794             }
795             else {
796               buffer.append("\\u");
797             }
798             break;
799
800           case '0':
801           case '1':
802           case '2':
803           case '3':
804             octalEscapeMaxLength = 3;
805           case '4':
806           case '5':
807           case '6':
808           case '7':
809             int escapeEnd = idx + 1;
810             while (escapeEnd < length && escapeEnd < idx + octalEscapeMaxLength && isOctalDigit(s.charAt(escapeEnd))) escapeEnd++;
811             try {
812               buffer.append((char)Integer.parseInt(s.substring(idx, escapeEnd), 8));
813             }
814             catch (NumberFormatException e) {
815               throw new RuntimeException("Couldn't parse " + s.substring(idx, escapeEnd), e); // shouldn't happen
816             }
817             //noinspection AssignmentToForLoopParameter
818             idx = escapeEnd - 1;
819             break;
820
821           default:
822             buffer.append(ch);
823             break;
824         }
825         escaped = false;
826       }
827     }
828
829     if (escaped) buffer.append('\\');
830   }
831
832   @SuppressWarnings("HardCodedStringLiteral")
833   @NotNull
834   @Contract(pure = true)
835   public static String pluralize(@NotNull String suggestion) {
836     if (suggestion.endsWith("Child") || suggestion.endsWith("child")) {
837       return suggestion + "ren";
838     }
839
840     if (suggestion.equals("this")) {
841       return "these";
842     }
843     if (suggestion.equals("This")) {
844       return "These";
845     }
846     if (suggestion.equals("fix") || suggestion.equals("Fix")) {
847       return suggestion + "es";
848     }
849
850     if (endsWithIgnoreCase(suggestion, "es")) {
851       return suggestion;
852     }
853
854     int len = suggestion.length();
855     if (endsWithIgnoreCase(suggestion, "ex") || endsWithIgnoreCase(suggestion, "ix")) {
856       return suggestion.substring(0, len - 2) + "ices";
857     }
858     if (endsWithIgnoreCase(suggestion, "um")) {
859       return suggestion.substring(0, len - 2) + "a";
860     }
861     if (endsWithIgnoreCase(suggestion, "an")) {
862       return suggestion.substring(0, len - 2) + "en";
863     }
864
865     if (endsWithIgnoreCase(suggestion, "s") || endsWithIgnoreCase(suggestion, "x") ||
866         endsWithIgnoreCase(suggestion, "ch") || endsWithIgnoreCase(suggestion, "sh")) {
867       return suggestion + "es";
868     }
869
870     if (endsWithIgnoreCase(suggestion, "y") && len > 1 && !isVowel(toLowerCase(suggestion.charAt(len - 2)))) {
871       return suggestion.substring(0, len - 1) + "ies";
872     }
873
874     return suggestion + "s";
875   }
876
877   @NotNull
878   @Contract(pure = true)
879   public static String capitalizeWords(@NotNull String text,
880                                        boolean allWords) {
881     return capitalizeWords(text, " \t\n\r\f", allWords, false);
882   }
883
884   @NotNull
885   @Contract(pure = true)
886   public static String capitalizeWords(@NotNull String text,
887                                        @NotNull String tokenizerDelim,
888                                        boolean allWords,
889                                        boolean leaveOriginalDelims) {
890     final StringTokenizer tokenizer = new StringTokenizer(text, tokenizerDelim, leaveOriginalDelims);
891     final StringBuilder out = new StringBuilder(text.length());
892     boolean toCapitalize = true;
893     while (tokenizer.hasMoreTokens()) {
894       final String word = tokenizer.nextToken();
895       if (!leaveOriginalDelims && out.length() > 0) {
896         out.append(' ');
897       }
898       out.append(toCapitalize ? capitalize(word) : word);
899       if (!allWords) {
900         toCapitalize = false;
901       }
902     }
903     return out.toString();
904   }
905
906   @Contract(pure = true)
907   public static String decapitalize(String s) {
908     return Introspector.decapitalize(s);
909   }
910
911   @Contract(pure = true)
912   public static boolean isVowel(char c) {
913     return VOWELS.indexOf(c) >= 0;
914   }
915
916   @NotNull
917   @Contract(pure = true)
918   public static String capitalize(@NotNull String s) {
919     if (s.isEmpty()) return s;
920     if (s.length() == 1) return StringUtilRt.toUpperCase(s).toString();
921
922     // Optimization
923     if (Character.isUpperCase(s.charAt(0))) return s;
924     return toUpperCase(s.charAt(0)) + s.substring(1);
925   }
926
927   @Contract(value = "null -> false", pure = true)
928   public static boolean isCapitalized(@Nullable String s) {
929     return s != null && !s.isEmpty() && Character.isUpperCase(s.charAt(0));
930   }
931
932   @NotNull
933   @Contract(pure = true)
934   public static String capitalizeWithJavaBeanConvention(@NotNull String s) {
935     if (s.length() > 1 && Character.isUpperCase(s.charAt(1))) {
936       return s;
937     }
938     return capitalize(s);
939   }
940
941   @Contract(pure = true)
942   public static int stringHashCode(@NotNull CharSequence chars) {
943     if (chars instanceof String || chars instanceof CharSequenceWithStringHash) {
944       // we know for sure these classes have conformant (and maybe faster) hashCode()
945       return chars.hashCode();
946     }
947
948     return stringHashCode(chars, 0, chars.length());
949   }
950
951   @Contract(pure = true)
952   public static int stringHashCode(@NotNull CharSequence chars, int from, int to) {
953     int h = 0;
954     for (int off = from; off < to; off++) {
955       h = 31 * h + chars.charAt(off);
956     }
957     return h;
958   }
959
960   @Contract(pure = true)
961   public static int stringHashCode(char[] chars, int from, int to) {
962     int h = 0;
963     for (int off = from; off < to; off++) {
964       h = 31 * h + chars[off];
965     }
966     return h;
967   }
968
969   @Contract(pure = true)
970   public static int stringHashCodeInsensitive(@NotNull char[] chars, int from, int to) {
971     int h = 0;
972     for (int off = from; off < to; off++) {
973       h = 31 * h + toLowerCase(chars[off]);
974     }
975     return h;
976   }
977
978   @Contract(pure = true)
979   public static int stringHashCodeInsensitive(@NotNull CharSequence chars, int from, int to) {
980     int h = 0;
981     for (int off = from; off < to; off++) {
982       h = 31 * h + toLowerCase(chars.charAt(off));
983     }
984     return h;
985   }
986
987   @Contract(pure = true)
988   public static int stringHashCodeInsensitive(@NotNull CharSequence chars) {
989     return stringHashCodeInsensitive(chars, 0, chars.length());
990   }
991
992   @Contract(pure = true)
993   public static int stringHashCodeIgnoreWhitespaces(char[] chars, int from, int to) {
994     int h = 0;
995     for (int off = from; off < to; off++) {
996       char c = chars[off];
997       if (!isWhiteSpace(c)) {
998         h = 31 * h + c;
999       }
1000     }
1001     return h;
1002   }
1003
1004   @Contract(pure = true)
1005   public static int stringHashCodeIgnoreWhitespaces(@NotNull CharSequence chars, int from, int to) {
1006     int h = 0;
1007     for (int off = from; off < to; off++) {
1008       char c = chars.charAt(off);
1009       if (!isWhiteSpace(c)) {
1010         h = 31 * h + c;
1011       }
1012     }
1013     return h;
1014   }
1015
1016   @Contract(pure = true)
1017   public static int stringHashCodeIgnoreWhitespaces(@NotNull CharSequence chars) {
1018     return stringHashCodeIgnoreWhitespaces(chars, 0, chars.length());
1019   }
1020
1021   /**
1022    * Equivalent to string.startsWith(prefixes[0] + prefixes[1] + ...) but avoids creating an object for concatenation.
1023    */
1024   @Contract(pure = true)
1025   public static boolean startsWithConcatenation(@NotNull String string, @NotNull String... prefixes) {
1026     int offset = 0;
1027     for (String prefix : prefixes) {
1028       int prefixLen = prefix.length();
1029       if (!string.regionMatches(offset, prefix, 0, prefixLen)) {
1030         return false;
1031       }
1032       offset += prefixLen;
1033     }
1034     return true;
1035   }
1036
1037   @Contract(value = "null -> null; !null -> !null", pure = true)
1038   public static String trim(@Nullable String s) {
1039     return s == null ? null : s.trim();
1040   }
1041
1042   @NotNull
1043   @Contract(pure = true)
1044   public static String trimEnd(@NotNull String s, @NonNls @NotNull String suffix) {
1045     return trimEnd(s, suffix, false);
1046   }
1047
1048   @NotNull
1049   @Contract(pure = true)
1050   public static String trimEnd(@NotNull String s, @NonNls @NotNull String suffix, boolean ignoreCase) {
1051     boolean endsWith = ignoreCase ? endsWithIgnoreCase(s, suffix) : s.endsWith(suffix);
1052     if (endsWith) {
1053       return s.substring(0, s.length() - suffix.length());
1054     }
1055     return s;
1056   }
1057
1058   @NotNull
1059   @Contract(pure = true)
1060   public static String trimEnd(@NotNull String s, char suffix) {
1061     if (endsWithChar(s, suffix)) {
1062       return s.substring(0, s.length() - 1);
1063     }
1064     return s;
1065   }
1066
1067   @NotNull
1068   @Contract(pure = true)
1069   public static String trimLog(@NotNull final String text, final int limit) {
1070     if (limit > 5 && text.length() > limit) {
1071       return text.substring(0, limit - 5) + " ...\n";
1072     }
1073     return text;
1074   }
1075
1076   @NotNull
1077   @Contract(pure = true)
1078   public static String trimLeading(@NotNull String string) {
1079     return trimLeading((CharSequence)string).toString();
1080   }
1081   @NotNull
1082   @Contract(pure = true)
1083   public static CharSequence trimLeading(@NotNull CharSequence string) {
1084     int index = 0;
1085     while (index < string.length() && Character.isWhitespace(string.charAt(index))) index++;
1086     return string.subSequence(index, string.length());
1087   }
1088
1089   @NotNull
1090   @Contract(pure = true)
1091   public static String trimLeading(@NotNull String string, char symbol) {
1092     int index = 0;
1093     while (index < string.length() && string.charAt(index) == symbol) index++;
1094     return string.substring(index);
1095   }
1096
1097   @NotNull
1098   @Contract(pure = true)
1099   public static String trimTrailing(@NotNull String string) {
1100     return trimTrailing((CharSequence)string).toString();
1101   }
1102
1103   @NotNull
1104   @Contract(pure = true)
1105   public static CharSequence trimTrailing(@NotNull CharSequence string) {
1106     int index = string.length() - 1;
1107     while (index >= 0 && Character.isWhitespace(string.charAt(index))) index--;
1108     return string.subSequence(0, index + 1);
1109   }
1110
1111   @NotNull
1112   @Contract(pure = true)
1113   public static String trimTrailing(@NotNull String string, char symbol) {
1114     int index = string.length() - 1;
1115     while (index >= 0 && string.charAt(index) == symbol) index--;
1116     return string.substring(0, index + 1);
1117   }
1118
1119   @Contract(pure = true)
1120   public static boolean startsWithChar(@Nullable CharSequence s, char prefix) {
1121     return s != null && s.length() != 0 && s.charAt(0) == prefix;
1122   }
1123
1124   @Contract(pure = true)
1125   public static boolean endsWithChar(@Nullable CharSequence s, char suffix) {
1126     return StringUtilRt.endsWithChar(s, suffix);
1127   }
1128
1129   @NotNull
1130   @Contract(pure = true)
1131   public static String trimStart(@NotNull String s, @NonNls @NotNull String prefix) {
1132     if (s.startsWith(prefix)) {
1133       return s.substring(prefix.length());
1134     }
1135     return s;
1136   }
1137
1138   @NotNull
1139   @Contract(pure = true)
1140   public static String trimExtension(@NotNull String name) {
1141     int index = name.lastIndexOf('.');
1142     return index < 0 ? name : name.substring(0, index);
1143   }
1144
1145   @NotNull
1146   @Contract(pure = true)
1147   public static String trimExtensions(@NotNull String name) {
1148     int index = name.indexOf('.');
1149     return index < 0 ? name : name.substring(0, index);
1150   }
1151
1152   @NotNull
1153   @Contract(pure = true)
1154   public static String pluralize(@NotNull String base, int n) {
1155     if (n == 1) return base;
1156     return pluralize(base);
1157   }
1158
1159   public static void repeatSymbol(@NotNull Appendable buffer, char symbol, int times) {
1160     assert times >= 0 : times;
1161     try {
1162       for (int i = 0; i < times; i++) {
1163         buffer.append(symbol);
1164       }
1165     }
1166     catch (IOException e) {
1167       LOG.error(e);
1168     }
1169   }
1170
1171   @Contract(pure = true)
1172   public static String defaultIfEmpty(@Nullable String value, String defaultValue) {
1173     return isEmpty(value) ? defaultValue : value;
1174   }
1175
1176   @Contract(value = "null -> false", pure = true)
1177   public static boolean isNotEmpty(@Nullable String s) {
1178     return s != null && !s.isEmpty();
1179   }
1180
1181   @Contract(value = "null -> true", pure=true)
1182   public static boolean isEmpty(@Nullable String s) {
1183     return s == null || s.isEmpty();
1184   }
1185
1186   @Contract(value = "null -> true",pure = true)
1187   public static boolean isEmpty(@Nullable CharSequence cs) {
1188     return cs == null || cs.length() == 0;
1189   }
1190
1191   @Contract(pure = true)
1192   public static int length(@Nullable CharSequence cs) {
1193     return cs == null ? 0 : cs.length();
1194   }
1195
1196   @NotNull
1197   @Contract(pure = true)
1198   public static String notNullize(@Nullable final String s) {
1199     return notNullize(s, "");
1200   }
1201
1202   @NotNull
1203   @Contract(pure = true)
1204   public static String notNullize(@Nullable final String s, @NotNull String defaultValue) {
1205     return s == null ? defaultValue : s;
1206   }
1207
1208   @Nullable
1209   @Contract(pure = true)
1210   public static String nullize(@Nullable final String s) {
1211     return nullize(s, false);
1212   }
1213
1214   @Nullable
1215   @Contract(pure = true)
1216   public static String nullize(@Nullable final String s, boolean nullizeSpaces) {
1217     if (nullizeSpaces) {
1218       if (isEmptyOrSpaces(s)) return null;
1219     }
1220     else {
1221       if (isEmpty(s)) return null;
1222     }
1223     return s;
1224   }
1225
1226   @Contract(value = "null -> true",pure = true)
1227   // we need to keep this method to preserve backward compatibility
1228   public static boolean isEmptyOrSpaces(@Nullable String s) {
1229     return isEmptyOrSpaces((CharSequence)s);
1230   }
1231
1232   @Contract(value = "null -> true", pure = true)
1233   public static boolean isEmptyOrSpaces(@Nullable CharSequence s) {
1234     if (isEmpty(s)) {
1235       return true;
1236     }
1237     for (int i = 0; i < s.length(); i++) {
1238       if (s.charAt(i) > ' ') {
1239         return false;
1240       }
1241     }
1242     return true;
1243   }
1244
1245   /**
1246    * Allows to answer if given symbol is white space, tabulation or line feed.
1247    *
1248    * @param c symbol to check
1249    * @return <code>true</code> if given symbol is white space, tabulation or line feed; <code>false</code> otherwise
1250    */
1251   @Contract(pure = true)
1252   public static boolean isWhiteSpace(char c) {
1253     return c == '\n' || c == '\t' || c == ' ';
1254   }
1255
1256   @NotNull
1257   @Contract(pure = true)
1258   public static String getThrowableText(@NotNull Throwable aThrowable) {
1259     return ExceptionUtil.getThrowableText(aThrowable);
1260   }
1261
1262   @NotNull
1263   @Contract(pure = true)
1264   public static String getThrowableText(@NotNull Throwable aThrowable, @NonNls @NotNull final String stackFrameSkipPattern) {
1265     return ExceptionUtil.getThrowableText(aThrowable, stackFrameSkipPattern);
1266   }
1267
1268   @Nullable
1269   @Contract(pure = true)
1270   public static String getMessage(@NotNull Throwable e) {
1271     return ExceptionUtil.getMessage(e);
1272   }
1273
1274   @NotNull
1275   @Contract(pure = true)
1276   public static String repeatSymbol(final char aChar, final int count) {
1277     char[] buffer = new char[count];
1278     Arrays.fill(buffer, aChar);
1279     return StringFactory.createShared(buffer);
1280   }
1281
1282   @NotNull
1283   @Contract(pure = true)
1284   public static String repeat(@NotNull String s, int count) {
1285     assert count >= 0 : count;
1286     StringBuilder sb = new StringBuilder(s.length() * count);
1287     for (int i = 0; i < count; i++) {
1288       sb.append(s);
1289     }
1290     return sb.toString();
1291   }
1292
1293   @NotNull
1294   @Contract(pure = true)
1295   public static List<String> splitHonorQuotes(@NotNull String s, char separator) {
1296     final List<String> result = new ArrayList<String>();
1297     final StringBuilder builder = new StringBuilder(s.length());
1298     boolean inQuotes = false;
1299     for (int i = 0; i < s.length(); i++) {
1300       final char c = s.charAt(i);
1301       if (c == separator && !inQuotes) {
1302         if (builder.length() > 0) {
1303           result.add(builder.toString());
1304           builder.setLength(0);
1305         }
1306         continue;
1307       }
1308
1309       if ((c == '"' || c == '\'') && !(i > 0 && s.charAt(i - 1) == '\\')) {
1310         inQuotes = !inQuotes;
1311       }
1312       builder.append(c);
1313     }
1314
1315     if (builder.length() > 0) {
1316       result.add(builder.toString());
1317     }
1318     return result;
1319   }
1320
1321
1322   @NotNull
1323   @Contract(pure = true)
1324   public static List<String> split(@NotNull String s, @NotNull String separator) {
1325     return split(s, separator, true);
1326   }
1327   @NotNull
1328   @Contract(pure = true)
1329   public static List<CharSequence> split(@NotNull CharSequence s, @NotNull CharSequence separator) {
1330     return split(s, separator, true, true);
1331   }
1332
1333   @NotNull
1334   @Contract(pure = true)
1335   public static List<String> split(@NotNull String s, @NotNull String separator,
1336                                    boolean excludeSeparator) {
1337     return split(s, separator, excludeSeparator, true);
1338   }
1339
1340   @NotNull
1341   @Contract(pure = true)
1342   public static List<String> split(@NotNull String s, @NotNull String separator,
1343                                    boolean excludeSeparator, boolean excludeEmptyStrings) {
1344     return (List)split((CharSequence)s,separator,excludeSeparator,excludeEmptyStrings);
1345   }
1346   @NotNull
1347   @Contract(pure = true)
1348   public static List<CharSequence> split(@NotNull CharSequence s, @NotNull CharSequence separator,
1349                                    boolean excludeSeparator, boolean excludeEmptyStrings) {
1350     if (separator.length() == 0) {
1351       return Collections.singletonList(s);
1352     }
1353     List<CharSequence> result = new ArrayList<CharSequence>();
1354     int pos = 0;
1355     while (true) {
1356       int index = indexOf(s,separator, pos);
1357       if (index == -1) break;
1358       final int nextPos = index + separator.length();
1359       CharSequence token = s.subSequence(pos, excludeSeparator ? index : nextPos);
1360       if (token.length() != 0 || !excludeEmptyStrings) {
1361         result.add(token);
1362       }
1363       pos = nextPos;
1364     }
1365     if (pos < s.length() || !excludeEmptyStrings && pos == s.length()) {
1366       result.add(s.subSequence(pos, s.length()));
1367     }
1368     return result;
1369   }
1370
1371   @NotNull
1372   @Contract(pure = true)
1373   public static Iterable<String> tokenize(@NotNull String s, @NotNull String separators) {
1374     final com.intellij.util.text.StringTokenizer tokenizer = new com.intellij.util.text.StringTokenizer(s, separators);
1375     return new Iterable<String>() {
1376       @NotNull
1377       @Override
1378       public Iterator<String> iterator() {
1379         return new Iterator<String>() {
1380           @Override
1381           public boolean hasNext() {
1382             return tokenizer.hasMoreTokens();
1383           }
1384
1385           @Override
1386           public String next() {
1387             return tokenizer.nextToken();
1388           }
1389
1390           @Override
1391           public void remove() {
1392             throw new UnsupportedOperationException();
1393           }
1394         };
1395       }
1396     };
1397   }
1398
1399   @NotNull
1400   @Contract(pure = true)
1401   public static Iterable<String> tokenize(@NotNull final StringTokenizer tokenizer) {
1402     return new Iterable<String>() {
1403       @NotNull
1404       @Override
1405       public Iterator<String> iterator() {
1406         return new Iterator<String>() {
1407           @Override
1408           public boolean hasNext() {
1409             return tokenizer.hasMoreTokens();
1410           }
1411
1412           @Override
1413           public String next() {
1414             return tokenizer.nextToken();
1415           }
1416
1417           @Override
1418           public void remove() {
1419             throw new UnsupportedOperationException();
1420           }
1421         };
1422       }
1423     };
1424   }
1425
1426   /**
1427    * @return list containing all words in {@code text}, or {@link ContainerUtil#emptyList()} if there are none.
1428    * The <b>word</b> here means the maximum sub-string consisting entirely of characters which are <code>Character.isJavaIdentifierPart(c)</code>.
1429    */
1430   @NotNull
1431   @Contract(pure = true)
1432   public static List<String> getWordsIn(@NotNull String text) {
1433     List<String> result = null;
1434     int start = -1;
1435     for (int i = 0; i < text.length(); i++) {
1436       char c = text.charAt(i);
1437       boolean isIdentifierPart = Character.isJavaIdentifierPart(c);
1438       if (isIdentifierPart && start == -1) {
1439         start = i;
1440       }
1441       if (isIdentifierPart && i == text.length() - 1 && start != -1) {
1442         if (result == null) {
1443           result = new SmartList<String>();
1444         }
1445         result.add(text.substring(start, i + 1));
1446       }
1447       else if (!isIdentifierPart && start != -1) {
1448         if (result == null) {
1449           result = new SmartList<String>();
1450         }
1451         result.add(text.substring(start, i));
1452         start = -1;
1453       }
1454     }
1455     if (result == null) {
1456       return ContainerUtil.emptyList();
1457     }
1458     return result;
1459   }
1460
1461   @NotNull
1462   @Contract(pure = true)
1463   public static List<TextRange> getWordIndicesIn(@NotNull String text) {
1464     List<TextRange> result = new SmartList<TextRange>();
1465     int start = -1;
1466     for (int i = 0; i < text.length(); i++) {
1467       char c = text.charAt(i);
1468       boolean isIdentifierPart = Character.isJavaIdentifierPart(c);
1469       if (isIdentifierPart && start == -1) {
1470         start = i;
1471       }
1472       if (isIdentifierPart && i == text.length() - 1 && start != -1) {
1473         result.add(new TextRange(start, i + 1));
1474       }
1475       else if (!isIdentifierPart && start != -1) {
1476         result.add(new TextRange(start, i));
1477         start = -1;
1478       }
1479     }
1480     return result;
1481   }
1482
1483   @NotNull
1484   @Contract(pure = true)
1485   public static String join(@NotNull final String[] strings, @NotNull final String separator) {
1486     return join(strings, 0, strings.length, separator);
1487   }
1488
1489   @NotNull
1490   @Contract(pure = true)
1491   public static String join(@NotNull final String[] strings, int startIndex, int endIndex, @NotNull final String separator) {
1492     final StringBuilder result = new StringBuilder();
1493     for (int i = startIndex; i < endIndex; i++) {
1494       if (i > startIndex) result.append(separator);
1495       result.append(strings[i]);
1496     }
1497     return result.toString();
1498   }
1499
1500   @NotNull
1501   @Contract(pure = true)
1502   public static String[] zip(@NotNull String[] strings1, @NotNull String[] strings2, String separator) {
1503     if (strings1.length != strings2.length) throw new IllegalArgumentException();
1504
1505     String[] result = ArrayUtil.newStringArray(strings1.length);
1506     for (int i = 0; i < result.length; i++) {
1507       result[i] = strings1[i] + separator + strings2[i];
1508     }
1509
1510     return result;
1511   }
1512
1513   @NotNull
1514   @Contract(pure = true)
1515   public static String[] surround(@NotNull String[] strings1, String prefix, String suffix) {
1516     String[] result = ArrayUtil.newStringArray(strings1.length);
1517     for (int i = 0; i < result.length; i++) {
1518       result[i] = prefix + strings1[i] + suffix;
1519     }
1520
1521     return result;
1522   }
1523
1524   @NotNull
1525   @Contract(pure = true)
1526   public static <T> String join(@NotNull T[] items, @NotNull Function<T, String> f, @NotNull @NonNls String separator) {
1527     return join(Arrays.asList(items), f, separator);
1528   }
1529
1530   @NotNull
1531   @Contract(pure = true)
1532   public static <T> String join(@NotNull Collection<? extends T> items,
1533                                 @NotNull Function<? super T, String> f,
1534                                 @NotNull @NonNls String separator) {
1535     if (items.isEmpty()) return "";
1536     return join((Iterable<? extends T>)items, f, separator);
1537   }
1538
1539   @Contract(pure = true)
1540   public static String join(@NotNull Iterable<?> items, @NotNull @NonNls String separator) {
1541     StringBuilder result = new StringBuilder();
1542     for (Object item : items) {
1543       result.append(item).append(separator);
1544     }
1545     if (result.length() > 0) {
1546       result.setLength(result.length() - separator.length());
1547     }
1548     return result.toString();
1549   }
1550
1551   @NotNull
1552   @Contract(pure = true)
1553   public static <T> String join(@NotNull Iterable<? extends T> items,
1554                                 @NotNull Function<? super T, String> f,
1555                                 @NotNull @NonNls String separator) {
1556     final StringBuilder result = new StringBuilder();
1557     for (T item : items) {
1558       String string = f.fun(item);
1559       if (string != null && !string.isEmpty()) {
1560         if (result.length() != 0) result.append(separator);
1561         result.append(string);
1562       }
1563     }
1564     return result.toString();
1565   }
1566
1567   @NotNull
1568   @Contract(pure = true)
1569   public static String join(@NotNull Collection<String> strings, @NotNull String separator) {
1570     if (strings.size() <= 1) {
1571       return notNullize(ContainerUtil.getFirstItem(strings));
1572     }
1573     StringBuilder result = new StringBuilder();
1574     join(strings, separator, result);
1575     return result.toString();
1576   }
1577
1578   public static void join(@NotNull Collection<String> strings, @NotNull String separator, @NotNull StringBuilder result) {
1579     boolean isFirst = true;
1580     for (String string : strings) {
1581       if (string != null) {
1582         if (isFirst) {
1583           isFirst = false;
1584         }
1585         else {
1586           result.append(separator);
1587         }
1588         result.append(string);
1589       }
1590     }
1591   }
1592
1593   @NotNull
1594   @Contract(pure = true)
1595   public static String join(@NotNull final int[] strings, @NotNull final String separator) {
1596     final StringBuilder result = new StringBuilder();
1597     for (int i = 0; i < strings.length; i++) {
1598       if (i > 0) result.append(separator);
1599       result.append(strings[i]);
1600     }
1601     return result.toString();
1602   }
1603
1604   @NotNull
1605   @Contract(pure = true)
1606   public static String join(@Nullable final String... strings) {
1607     if (strings == null || strings.length == 0) return "";
1608
1609     final StringBuilder builder = new StringBuilder();
1610     for (final String string : strings) {
1611       builder.append(string);
1612     }
1613     return builder.toString();
1614   }
1615
1616   /**
1617    * Consider using {@link StringUtil#unquoteString(String)} instead.
1618    * Note: this method has an odd behavior:
1619    *   Quotes are removed even if leading and trailing quotes are different or
1620    *                           if there is only one quote (leading or trailing).
1621    */
1622   @NotNull
1623   @Contract(pure = true)
1624   public static String stripQuotesAroundValue(@NotNull String text) {
1625     final int len = text.length();
1626     if (len > 0) {
1627       final int from = isQuoteAt(text, 0) ? 1 : 0;
1628       final int to = len > 1 && isQuoteAt(text, len - 1) ? len - 1 : len;
1629       if (from > 0 || to < len) {
1630         return text.substring(from, to);
1631       }
1632     }
1633     return text;
1634   }
1635
1636   /**
1637    * Formats the specified file size as a string.
1638    *
1639    * @param fileSize the size to format.
1640    * @return the size formatted as a string.
1641    * @since 5.0.1
1642    */
1643   @NotNull
1644   @Contract(pure = true)
1645   public static String formatFileSize(long fileSize) {
1646     return formatValue(fileSize, null,
1647                        new String[]{"B", "K", "M", "G", "T", "P", "E"},
1648                        new long[]{1000, 1000, 1000, 1000, 1000, 1000});
1649   }
1650
1651   @NotNull
1652   @Contract(pure = true)
1653   public static String formatDuration(long duration) {
1654     return formatValue(duration, " ",
1655                        new String[]{"ms", "s", "m", "h", "d", "w", "mo", "yr", "c", "ml", "ep"},
1656                        new long[]{1000, 60, 60, 24, 7, 4, 12, 100, 10, 10000});
1657   }
1658
1659   @NotNull
1660   private static String formatValue(long value, String partSeparator, String[] units, long[] multipliers) {
1661     StringBuilder sb = new StringBuilder();
1662     long count = value;
1663     long remainder = 0;
1664     int i = 0;
1665     for (; i < units.length; i++) {
1666       long multiplier = i < multipliers.length ? multipliers[i] : -1;
1667       if (multiplier == -1 || count < multiplier) break;
1668       remainder = count % multiplier;
1669       count /= multiplier;
1670       if (partSeparator != null && (remainder != 0 || sb.length() > 0)) {
1671         sb.insert(0, units[i]).insert(0, remainder).insert(0, partSeparator);
1672       }
1673     }
1674     if (partSeparator != null || remainder == 0) {
1675       sb.insert(0, units[i]).insert(0, count);
1676     }
1677     else if (remainder > 0) {
1678       sb.append(String.format(Locale.US, "%.2f", count + (double)remainder / multipliers[i - 1])).append(units[i]);
1679     }
1680     return sb.toString();
1681   }
1682
1683   /**
1684    * Returns unpluralized variant using English based heuristics like properties -> property, names -> name, children -> child.
1685    * Returns <code>null</code> if failed to match appropriate heuristic.
1686    *
1687    * @param name english word in plural form
1688    * @return name in singular form or <code>null</code> if failed to find one.
1689    */
1690   @SuppressWarnings("HardCodedStringLiteral")
1691   @Nullable
1692   @Contract(pure = true)
1693   public static String unpluralize(@NotNull final String name) {
1694     if (name.endsWith("sses") || name.endsWith("shes") || name.endsWith("ches") || name.endsWith("xes")) { //?
1695       return name.substring(0, name.length() - 2);
1696     }
1697
1698     if (name.endsWith("ses")) {
1699       return name.substring(0, name.length() - 1);
1700     }
1701
1702     if (name.endsWith("ies")) {
1703       if (name.endsWith("cookies") || name.endsWith("Cookies")) {
1704         return name.substring(0, name.length() - "ookies".length()) + "ookie";
1705       }
1706
1707       return name.substring(0, name.length() - 3) + "y";
1708     }
1709
1710     if (name.endsWith("leaves") || name.endsWith("Leaves")) {
1711       return name.substring(0, name.length() - "eaves".length()) + "eaf";
1712     }
1713
1714     String result = stripEnding(name, "s");
1715     if (result != null) {
1716       return result;
1717     }
1718
1719     if (name.endsWith("children")) {
1720       return name.substring(0, name.length() - "children".length()) + "child";
1721     }
1722
1723     if (name.endsWith("Children") && name.length() > "Children".length()) {
1724       return name.substring(0, name.length() - "Children".length()) + "Child";
1725     }
1726
1727
1728     return null;
1729   }
1730
1731   @Nullable
1732   @Contract(pure = true)
1733   private static String stripEnding(@NotNull String name, @NotNull String ending) {
1734     if (name.endsWith(ending)) {
1735       if (name.equals(ending)) return name; // do not return empty string
1736       return name.substring(0, name.length() - 1);
1737     }
1738     return null;
1739   }
1740
1741   @Contract(pure = true)
1742   public static boolean containsAlphaCharacters(@NotNull String value) {
1743     for (int i = 0; i < value.length(); i++) {
1744       if (Character.isLetter(value.charAt(i))) return true;
1745     }
1746     return false;
1747   }
1748
1749   @Contract(pure = true)
1750   public static boolean containsAnyChar(@NotNull final String value, @NotNull final String chars) {
1751     if (chars.length() > value.length()) {
1752       return containsAnyChar(value, chars, 0, value.length());
1753     }
1754     else {
1755       return containsAnyChar(chars, value, 0, chars.length());
1756     }
1757   }
1758
1759   @Contract(pure = true)
1760   public static boolean containsAnyChar(@NotNull final String value,
1761                                         @NotNull final String chars,
1762                                         final int start, final int end) {
1763     for (int i = start; i < end; i++) {
1764       if (chars.indexOf(value.charAt(i)) >= 0) {
1765         return true;
1766       }
1767     }
1768
1769     return false;
1770   }
1771
1772   @Contract(pure = true)
1773   public static boolean containsChar(@NotNull final String value, final char ch) {
1774     return value.indexOf(ch) >= 0;
1775   }
1776
1777   /**
1778    * @deprecated use #capitalize(String)
1779    */
1780   @Contract(value = "null -> null; !null -> !null", pure = true)
1781   public static String firstLetterToUpperCase(@Nullable final String displayString) {
1782     if (displayString == null || displayString.isEmpty()) return displayString;
1783     char firstChar = displayString.charAt(0);
1784     char uppedFirstChar = toUpperCase(firstChar);
1785
1786     if (uppedFirstChar == firstChar) return displayString;
1787
1788     char[] buffer = displayString.toCharArray();
1789     buffer[0] = uppedFirstChar;
1790     return StringFactory.createShared(buffer);
1791   }
1792
1793   /**
1794    * Strip out all characters not accepted by given filter
1795    *
1796    * @param s      e.g. "/n    my string "
1797    * @param filter e.g. {@link CharFilter#NOT_WHITESPACE_FILTER}
1798    * @return stripped string e.g. "mystring"
1799    */
1800   @NotNull
1801   @Contract(pure = true)
1802   public static String strip(@NotNull final String s, @NotNull final CharFilter filter) {
1803     final StringBuilder result = new StringBuilder(s.length());
1804     for (int i = 0; i < s.length(); i++) {
1805       char ch = s.charAt(i);
1806       if (filter.accept(ch)) {
1807         result.append(ch);
1808       }
1809     }
1810     return result.toString();
1811   }
1812
1813   @NotNull
1814   @Contract(pure = true)
1815   public static List<String> findMatches(@NotNull String s, @NotNull Pattern pattern) {
1816     return findMatches(s, pattern, 1);
1817   }
1818
1819   @NotNull
1820   @Contract(pure = true)
1821   public static List<String> findMatches(@NotNull String s, @NotNull Pattern pattern, int groupIndex) {
1822     List<String> result = new SmartList<String>();
1823     Matcher m = pattern.matcher(s);
1824     while (m.find()) {
1825       String group = m.group(groupIndex);
1826       if (group != null) {
1827         result.add(group);
1828       }
1829     }
1830     return result;
1831   }
1832
1833   /**
1834    * Find position of the first character accepted by given filter.
1835    *
1836    * @param s      the string to search
1837    * @param filter search filter
1838    * @return position of the first character accepted or -1 if not found
1839    */
1840   @Contract(pure = true)
1841   public static int findFirst(@NotNull final CharSequence s, @NotNull CharFilter filter) {
1842     for (int i = 0; i < s.length(); i++) {
1843       char ch = s.charAt(i);
1844       if (filter.accept(ch)) {
1845         return i;
1846       }
1847     }
1848     return -1;
1849   }
1850
1851   @NotNull
1852   @Contract(pure = true)
1853   public static String replaceSubstring(@NotNull String string, @NotNull TextRange range, @NotNull String replacement) {
1854     return range.replace(string, replacement);
1855   }
1856
1857   @Contract(pure = true)
1858   public static boolean startsWithWhitespace(@NotNull String text) {
1859     return !text.isEmpty() && Character.isWhitespace(text.charAt(0));
1860   }
1861
1862   @Contract(pure = true)
1863   public static boolean isChar(CharSequence seq, int index, char c) {
1864     return index >= 0 && index < seq.length() && seq.charAt(index) == c;
1865   }
1866
1867   @Contract(pure = true)
1868   public static boolean startsWith(@NotNull CharSequence text, @NotNull CharSequence prefix) {
1869     int l1 = text.length();
1870     int l2 = prefix.length();
1871     if (l1 < l2) return false;
1872
1873     for (int i = 0; i < l2; i++) {
1874       if (text.charAt(i) != prefix.charAt(i)) return false;
1875     }
1876
1877     return true;
1878   }
1879
1880   @Contract(pure = true)
1881   public static boolean startsWith(@NotNull CharSequence text, int startIndex, @NotNull CharSequence prefix) {
1882     int l1 = text.length() - startIndex;
1883     int l2 = prefix.length();
1884     if (l1 < l2) return false;
1885
1886     for (int i = 0; i < l2; i++) {
1887       if (text.charAt(i + startIndex) != prefix.charAt(i)) return false;
1888     }
1889
1890     return true;
1891   }
1892
1893   @Contract(pure = true)
1894   public static boolean endsWith(@NotNull CharSequence text, @NotNull CharSequence suffix) {
1895     int l1 = text.length();
1896     int l2 = suffix.length();
1897     if (l1 < l2) return false;
1898
1899     for (int i = l1 - 1; i >= l1 - l2; i--) {
1900       if (text.charAt(i) != suffix.charAt(i + l2 - l1)) return false;
1901     }
1902
1903     return true;
1904   }
1905
1906   @NotNull
1907   @Contract(pure = true)
1908   public static String commonPrefix(@NotNull String s1, @NotNull String s2) {
1909     return s1.substring(0, commonPrefixLength(s1, s2));
1910   }
1911
1912   @Contract(pure = true)
1913   public static int commonPrefixLength(@NotNull CharSequence s1, @NotNull CharSequence s2) {
1914     int i;
1915     int minLength = min(s1.length(), s2.length());
1916     for (i = 0; i < minLength; i++) {
1917       if (s1.charAt(i) != s2.charAt(i)) {
1918         break;
1919       }
1920     }
1921     return i;
1922   }
1923
1924   @NotNull
1925   @Contract(pure = true)
1926   public static String commonSuffix(@NotNull String s1, @NotNull String s2) {
1927     return s1.substring(s1.length() - commonSuffixLength(s1, s2));
1928   }
1929
1930   @Contract(pure = true)
1931   public static int commonSuffixLength(@NotNull CharSequence s1, @NotNull CharSequence s2) {
1932     int s1Length = s1.length();
1933     int s2Length = s2.length();
1934     if (s1Length == 0 || s2Length == 0) return 0;
1935     int i;
1936     for (i = 0; i < s1Length && i < s2Length; i++) {
1937       if (s1.charAt(s1Length - i - 1) != s2.charAt(s2Length - i - 1)) {
1938         break;
1939       }
1940     }
1941     return i;
1942   }
1943
1944   /**
1945    * Allows to answer if target symbol is contained at given char sequence at <code>[start; end)</code> interval.
1946    *
1947    * @param s     target char sequence to check
1948    * @param start start offset to use within the given char sequence (inclusive)
1949    * @param end   end offset to use within the given char sequence (exclusive)
1950    * @param c     target symbol to check
1951    * @return <code>true</code> if given symbol is contained at the target range of the given char sequence;
1952    * <code>false</code> otherwise
1953    */
1954   @Contract(pure = true)
1955   public static boolean contains(@NotNull CharSequence s, int start, int end, char c) {
1956     return indexOf(s, c, start, end) >= 0;
1957   }
1958
1959   @Contract(pure = true)
1960   public static boolean containsWhitespaces(@Nullable CharSequence s) {
1961     if (s == null) return false;
1962
1963     for (int i = 0; i < s.length(); i++) {
1964       if (Character.isWhitespace(s.charAt(i))) return true;
1965     }
1966     return false;
1967   }
1968
1969   @Contract(pure = true)
1970   public static int indexOf(@NotNull CharSequence s, char c) {
1971     return indexOf(s, c, 0, s.length());
1972   }
1973
1974   @Contract(pure = true)
1975   public static int indexOf(@NotNull CharSequence s, char c, int start) {
1976     return indexOf(s, c, start, s.length());
1977   }
1978
1979   @Contract(pure = true)
1980   public static int indexOf(@NotNull CharSequence s, char c, int start, int end) {
1981     end = min(end, s.length());
1982     for (int i = max(start, 0); i < end; i++) {
1983       if (s.charAt(i) == c) return i;
1984     }
1985     return -1;
1986   }
1987
1988   @Contract(pure = true)
1989   public static boolean contains(@NotNull CharSequence sequence, @NotNull CharSequence infix) {
1990     return indexOf(sequence, infix) >= 0;
1991   }
1992
1993   @Contract(pure = true)
1994   public static int indexOf(@NotNull CharSequence sequence, @NotNull CharSequence infix) {
1995     return indexOf(sequence, infix, 0);
1996   }
1997
1998   @Contract(pure = true)
1999   public static int indexOf(@NotNull CharSequence sequence, @NotNull CharSequence infix, int start) {
2000     for (int i = start; i <= sequence.length() - infix.length(); i++) {
2001       if (startsWith(sequence, i, infix)) {
2002         return i;
2003       }
2004     }
2005     return -1;
2006   }
2007
2008   @Contract(pure = true)
2009   public static int indexOf(@NotNull CharSequence s, char c, int start, int end, boolean caseSensitive) {
2010     end = min(end, s.length());
2011     for (int i = max(start, 0); i < end; i++) {
2012       if (charsMatch(s.charAt(i), c, !caseSensitive)) return i;
2013     }
2014     return -1;
2015   }
2016
2017   @Contract(pure = true)
2018   public static int indexOf(@NotNull char[] s, char c, int start, int end, boolean caseSensitive) {
2019     end = min(end, s.length);
2020     for (int i = max(start, 0); i < end; i++) {
2021       if (charsMatch(s[i], c, !caseSensitive)) return i;
2022     }
2023     return -1;
2024   }
2025
2026   @Contract(pure = true)
2027   public static int indexOfSubstringEnd(@NotNull String text, @NotNull String subString) {
2028     int i = text.indexOf(subString);
2029     if (i == -1) return -1;
2030     return i + subString.length();
2031   }
2032
2033   @Contract(pure = true)
2034   public static int indexOfAny(@NotNull final String s, @NotNull final String chars) {
2035     return indexOfAny(s, chars, 0, s.length());
2036   }
2037
2038   @Contract(pure = true)
2039   public static int indexOfAny(@NotNull final CharSequence s, @NotNull final String chars) {
2040     return indexOfAny(s, chars, 0, s.length());
2041   }
2042
2043   @Contract(pure = true)
2044   public static int indexOfAny(@NotNull final String s, @NotNull final String chars, final int start, final int end) {
2045     return indexOfAny((CharSequence)s, chars, start, end);
2046   }
2047
2048   @Contract(pure = true)
2049   public static int indexOfAny(@NotNull final CharSequence s, @NotNull final String chars, final int start, int end) {
2050     end = min(end, s.length());
2051     for (int i = max(start, 0); i < end; i++) {
2052       if (containsChar(chars, s.charAt(i))) return i;
2053     }
2054     return -1;
2055   }
2056
2057   @Contract(pure = true)
2058   public static int lastIndexOfAny(@NotNull CharSequence s, @NotNull final String chars) {
2059     for (int i = s.length() - 1; i >= 0; i--) {
2060       if (containsChar(chars, s.charAt(i))) return i;
2061     }
2062     return -1;
2063   }
2064
2065   @Nullable
2066   @Contract(pure = true)
2067   public static String substringBefore(@NotNull String text, @NotNull String subString) {
2068     int i = text.indexOf(subString);
2069     if (i == -1) return null;
2070     return text.substring(0, i);
2071   }
2072
2073   @Nullable
2074   @Contract(pure = true)
2075   public static String substringAfter(@NotNull String text, @NotNull String subString) {
2076     int i = text.indexOf(subString);
2077     if (i == -1) return null;
2078     return text.substring(i + subString.length());
2079   }
2080
2081   /**
2082    * Allows to retrieve index of last occurrence of the given symbols at <code>[start; end)</code> sub-sequence of the given text.
2083    *
2084    * @param s     target text
2085    * @param c     target symbol which last occurrence we want to check
2086    * @param start start offset of the target text (inclusive)
2087    * @param end   end offset of the target text (exclusive)
2088    * @return index of the last occurrence of the given symbol at the target sub-sequence of the given text if any;
2089    * <code>-1</code> otherwise
2090    */
2091   @Contract(pure = true)
2092   public static int lastIndexOf(@NotNull CharSequence s, char c, int start, int end) {
2093     return StringUtilRt.lastIndexOf(s, c, start, end);
2094   }
2095
2096   @NotNull
2097   @Contract(pure = true)
2098   public static String first(@NotNull String text, final int maxLength, final boolean appendEllipsis) {
2099     return text.length() > maxLength ? text.substring(0, maxLength) + (appendEllipsis ? "..." : "") : text;
2100   }
2101
2102   @NotNull
2103   @Contract(pure = true)
2104   public static CharSequence first(@NotNull CharSequence text, final int length, final boolean appendEllipsis) {
2105     return text.length() > length ? text.subSequence(0, length) + (appendEllipsis ? "..." : "") : text;
2106   }
2107
2108   @NotNull
2109   @Contract(pure = true)
2110   public static CharSequence last(@NotNull CharSequence text, final int length, boolean prependEllipsis) {
2111     return text.length() > length ? (prependEllipsis ? "..." : "") + text.subSequence(text.length() - length, text.length()) : text;
2112   }
2113
2114   @NotNull
2115   @Contract(pure = true)
2116   public static String firstLast(@NotNull String text, int length) {
2117     return text.length() > length
2118            ? text.subSequence(0, length / 2) + "\u2026" + text.subSequence(text.length() - length / 2 - 1, text.length())
2119            : text;
2120   }
2121
2122   @NotNull
2123   @Contract(pure = true)
2124   public static String escapeChar(@NotNull final String str, final char character) {
2125     return escapeChars(str, character);
2126   }
2127
2128   @NotNull
2129   @Contract(pure = true)
2130   public static String escapeChars(@NotNull final String str, final char... character) {
2131     final StringBuilder buf = new StringBuilder(str);
2132     for (char c : character) {
2133       escapeChar(buf, c);
2134     }
2135     return buf.toString();
2136   }
2137
2138   public static void escapeChar(@NotNull final StringBuilder buf, final char character) {
2139     int idx = 0;
2140     while ((idx = indexOf(buf, character, idx)) >= 0) {
2141       buf.insert(idx, "\\");
2142       idx += 2;
2143     }
2144   }
2145
2146   @NotNull
2147   @Contract(pure = true)
2148   public static String escapeQuotes(@NotNull final String str) {
2149     return escapeChar(str, '"');
2150   }
2151
2152   public static void escapeQuotes(@NotNull final StringBuilder buf) {
2153     escapeChar(buf, '"');
2154   }
2155
2156   @NotNull
2157   @Contract(pure = true)
2158   public static String escapeSlashes(@NotNull final String str) {
2159     return escapeChar(str, '/');
2160   }
2161
2162   @NotNull
2163   @Contract(pure = true)
2164   public static String escapeBackSlashes(@NotNull final String str) {
2165     return escapeChar(str, '\\');
2166   }
2167
2168   public static void escapeSlashes(@NotNull final StringBuilder buf) {
2169     escapeChar(buf, '/');
2170   }
2171
2172   @NotNull
2173   @Contract(pure = true)
2174   public static String unescapeSlashes(@NotNull final String str) {
2175     final StringBuilder buf = new StringBuilder(str.length());
2176     unescapeChar(buf, str, '/');
2177     return buf.toString();
2178   }
2179
2180   @NotNull
2181   @Contract(pure = true)
2182   public static String unescapeBackSlashes(@NotNull final String str) {
2183     final StringBuilder buf = new StringBuilder(str.length());
2184     unescapeChar(buf, str, '\\');
2185     return buf.toString();
2186   }
2187
2188   @NotNull
2189   @Contract(pure = true)
2190   public static String unescapeChar(@NotNull final String str, char unescapeChar) {
2191     final StringBuilder buf = new StringBuilder(str.length());
2192     unescapeChar(buf, str, unescapeChar);
2193     return buf.toString();
2194   }
2195
2196   private static void unescapeChar(@NotNull StringBuilder buf, @NotNull String str, char unescapeChar) {
2197     final int length = str.length();
2198     final int last = length - 1;
2199     for (int i = 0; i < length; i++) {
2200       char ch = str.charAt(i);
2201       if (ch == '\\' && i != last) {
2202         i++;
2203         ch = str.charAt(i);
2204         if (ch != unescapeChar) buf.append('\\');
2205       }
2206
2207       buf.append(ch);
2208     }
2209   }
2210
2211   public static void quote(@NotNull final StringBuilder builder) {
2212     quote(builder, '\"');
2213   }
2214
2215   public static void quote(@NotNull final StringBuilder builder, final char quotingChar) {
2216     builder.insert(0, quotingChar);
2217     builder.append(quotingChar);
2218   }
2219
2220   @NotNull
2221   @Contract(pure = true)
2222   public static String wrapWithDoubleQuote(@NotNull String str) {
2223     return '\"' + str + "\"";
2224   }
2225
2226   @NonNls private static final String[] REPLACES_REFS = {"&lt;", "&gt;", "&amp;", "&#39;", "&quot;"};
2227   @NonNls private static final String[] REPLACES_DISP = {"<", ">", "&", "'", "\""};
2228
2229   @Contract(value = "null -> null; !null -> !null",pure = true)
2230   public static String unescapeXml(@Nullable final String text) {
2231     if (text == null) return null;
2232     return replace(text, REPLACES_REFS, REPLACES_DISP);
2233   }
2234
2235   @Contract(value = "null -> null; !null -> !null",pure = true)
2236   public static String escapeXml(@Nullable final String text) {
2237     if (text == null) return null;
2238     return replace(text, REPLACES_DISP, REPLACES_REFS);
2239   }
2240
2241   public static String removeHtmlTags (@Nullable String htmlString) {
2242     if (isEmpty(htmlString)) return htmlString;
2243     try {
2244       html2TextParser.parse(new StringReader(htmlString));
2245     }
2246     catch (IOException e) {
2247         LOG.error(e);
2248     }
2249     return html2TextParser.getText();
2250   }
2251
2252   @NonNls private static final String[] MN_QUOTED = {"&&", "__"};
2253   @NonNls private static final String[] MN_CHARS = {"&", "_"};
2254
2255   @Contract(value = "null -> null; !null -> !null", pure = true)
2256   public static String escapeMnemonics(@Nullable String text) {
2257     if (text == null) return null;
2258     return replace(text, MN_CHARS, MN_QUOTED);
2259   }
2260
2261   @NotNull
2262   @Contract(pure = true)
2263   public static String htmlEmphasize(@NotNull String text) {
2264     return "<b><code>" + escapeXml(text) + "</code></b>";
2265   }
2266
2267
2268   @NotNull
2269   @Contract(pure = true)
2270   public static String escapeToRegexp(@NotNull String text) {
2271     final StringBuilder result = new StringBuilder(text.length());
2272     return escapeToRegexp(text, result).toString();
2273   }
2274
2275   @NotNull
2276   public static StringBuilder escapeToRegexp(@NotNull CharSequence text, @NotNull StringBuilder builder) {
2277     for (int i = 0; i < text.length(); i++) {
2278       final char c = text.charAt(i);
2279       if (c == ' ' || Character.isLetter(c) || Character.isDigit(c) || c == '_') {
2280         builder.append(c);
2281       }
2282       else if (c == '\n') {
2283         builder.append("\\n");
2284       }
2285       else {
2286         builder.append('\\').append(c);
2287       }
2288     }
2289
2290     return builder;
2291   }
2292
2293   @Contract(pure = true)
2294   public static boolean isEscapedBackslash(@NotNull char[] chars, int startOffset, int backslashOffset) {
2295     if (chars[backslashOffset] != '\\') {
2296       return true;
2297     }
2298     boolean escaped = false;
2299     for (int i = startOffset; i < backslashOffset; i++) {
2300       if (chars[i] == '\\') {
2301         escaped = !escaped;
2302       }
2303       else {
2304         escaped = false;
2305       }
2306     }
2307     return escaped;
2308   }
2309
2310   @Contract(pure = true)
2311   public static boolean isEscapedBackslash(@NotNull CharSequence text, int startOffset, int backslashOffset) {
2312     if (text.charAt(backslashOffset) != '\\') {
2313       return true;
2314     }
2315     boolean escaped = false;
2316     for (int i = startOffset; i < backslashOffset; i++) {
2317       if (text.charAt(i) == '\\') {
2318         escaped = !escaped;
2319       }
2320       else {
2321         escaped = false;
2322       }
2323     }
2324     return escaped;
2325   }
2326
2327   @NotNull
2328   @Contract(pure = true)
2329   public static String replace(@NotNull String text, @NotNull String[] from, @NotNull String[] to) {
2330     return replace(text, Arrays.asList(from), Arrays.asList(to));
2331   }
2332
2333   @NotNull
2334   @Contract(pure = true)
2335   public static String replace(@NotNull String text, @NotNull List<String> from, @NotNull List<String> to) {
2336     assert from.size() == to.size();
2337     final StringBuilder result = new StringBuilder(text.length());
2338     replace:
2339     for (int i = 0; i < text.length(); i++) {
2340       for (int j = 0; j < from.size(); j += 1) {
2341         String toReplace = from.get(j);
2342         String replaceWith = to.get(j);
2343
2344         final int len = toReplace.length();
2345         if (text.regionMatches(i, toReplace, 0, len)) {
2346           result.append(replaceWith);
2347           i += len - 1;
2348           continue replace;
2349         }
2350       }
2351       result.append(text.charAt(i));
2352     }
2353     return result.toString();
2354   }
2355
2356   @NotNull
2357   @Contract(pure = true)
2358   public static String[] filterEmptyStrings(@NotNull String[] strings) {
2359     int emptyCount = 0;
2360     for (String string : strings) {
2361       if (string == null || string.isEmpty()) emptyCount++;
2362     }
2363     if (emptyCount == 0) return strings;
2364
2365     String[] result = ArrayUtil.newStringArray(strings.length - emptyCount);
2366     int count = 0;
2367     for (String string : strings) {
2368       if (string == null || string.isEmpty()) continue;
2369       result[count++] = string;
2370     }
2371
2372     return result;
2373   }
2374
2375   @Contract(pure = true)
2376   public static int countNewLines(@NotNull CharSequence text) {
2377     return countChars(text, '\n');
2378   }
2379
2380   @Contract(pure = true)
2381   public static int countChars(@NotNull CharSequence text, char c) {
2382     return countChars(text, c, 0, false);
2383   }
2384
2385   @Contract(pure = true)
2386   public static int countChars(@NotNull CharSequence text, char c, int offset, boolean stopAtOtherChar) {
2387     return countChars(text, c, offset, text.length(), stopAtOtherChar);
2388   }
2389
2390   @Contract(pure = true)
2391   public static int countChars(@NotNull CharSequence text, char c, int start, int end, boolean stopAtOtherChar) {
2392     int count = 0;
2393     for (int i = start, len = min(text.length(), end); i < len; ++i) {
2394       if (text.charAt(i) == c) {
2395         count++;
2396       }
2397       else if (stopAtOtherChar) {
2398         break;
2399       }
2400     }
2401     return count;
2402   }
2403
2404   @NotNull
2405   @Contract(pure = true)
2406   public static String capitalsOnly(@NotNull String s) {
2407     StringBuilder b = new StringBuilder();
2408     for (int i = 0; i < s.length(); i++) {
2409       if (Character.isUpperCase(s.charAt(i))) {
2410         b.append(s.charAt(i));
2411       }
2412     }
2413
2414     return b.toString();
2415   }
2416
2417   /**
2418    * @param args Strings to join.
2419    * @return {@code null} if any of given Strings is {@code null}.
2420    */
2421   @Nullable
2422   @Contract(pure = true)
2423   public static String joinOrNull(@NotNull String... args) {
2424     StringBuilder r = new StringBuilder();
2425     for (String arg : args) {
2426       if (arg == null) return null;
2427       r.append(arg);
2428     }
2429     return r.toString();
2430   }
2431
2432   @Nullable
2433   @Contract(pure = true)
2434   public static String getPropertyName(@NonNls @NotNull String methodName) {
2435     if (methodName.startsWith("get")) {
2436       return Introspector.decapitalize(methodName.substring(3));
2437     }
2438     if (methodName.startsWith("is")) {
2439       return Introspector.decapitalize(methodName.substring(2));
2440     }
2441     if (methodName.startsWith("set")) {
2442       return Introspector.decapitalize(methodName.substring(3));
2443     }
2444     return null;
2445   }
2446
2447   @Contract(pure = true)
2448   public static boolean isJavaIdentifierStart(char c) {
2449     return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || Character.isJavaIdentifierStart(c);
2450   }
2451
2452   @Contract(pure = true)
2453   public static boolean isJavaIdentifierPart(char c) {
2454     return c >= '0' && c <= '9' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || Character.isJavaIdentifierPart(c);
2455   }
2456
2457   @Contract(pure = true)
2458   public static boolean isJavaIdentifier(@NotNull String text) {
2459     int len = text.length();
2460     if (len == 0) return false;
2461
2462     if (!isJavaIdentifierStart(text.charAt(0))) return false;
2463
2464     for (int i = 1; i < len; i++) {
2465       if (!isJavaIdentifierPart(text.charAt(i))) return false;
2466     }
2467
2468     return true;
2469   }
2470
2471   /**
2472    * Escape property name or key in property file. Unicode characters are escaped as well.
2473    *
2474    * @param input an input to escape
2475    * @param isKey if true, the rules for key escaping are applied. The leading space is escaped in that case.
2476    * @return an escaped string
2477    */
2478   @NotNull
2479   @Contract(pure = true)
2480   public static String escapeProperty(@NotNull String input, final boolean isKey) {
2481     final StringBuilder escaped = new StringBuilder(input.length());
2482     for (int i = 0; i < input.length(); i++) {
2483       final char ch = input.charAt(i);
2484       switch (ch) {
2485         case ' ':
2486           if (isKey && i == 0) {
2487             // only the leading space has to be escaped
2488             escaped.append('\\');
2489           }
2490           escaped.append(' ');
2491           break;
2492         case '\t':
2493           escaped.append("\\t");
2494           break;
2495         case '\r':
2496           escaped.append("\\r");
2497           break;
2498         case '\n':
2499           escaped.append("\\n");
2500           break;
2501         case '\f':
2502           escaped.append("\\f");
2503           break;
2504         case '\\':
2505         case '#':
2506         case '!':
2507         case ':':
2508         case '=':
2509           escaped.append('\\');
2510           escaped.append(ch);
2511           break;
2512         default:
2513           if (20 < ch && ch < 0x7F) {
2514             escaped.append(ch);
2515           }
2516           else {
2517             escaped.append("\\u");
2518             escaped.append(Character.forDigit((ch >> 12) & 0xF, 16));
2519             escaped.append(Character.forDigit((ch >> 8) & 0xF, 16));
2520             escaped.append(Character.forDigit((ch >> 4) & 0xF, 16));
2521             escaped.append(Character.forDigit((ch) & 0xF, 16));
2522           }
2523           break;
2524       }
2525     }
2526     return escaped.toString();
2527   }
2528
2529   @Contract(pure = true)
2530   public static String getQualifiedName(@Nullable String packageName, String className) {
2531     if (packageName == null || packageName.isEmpty()) {
2532       return className;
2533     }
2534     return packageName + '.' + className;
2535   }
2536
2537   @Contract(pure = true)
2538   public static int compareVersionNumbers(@Nullable String v1, @Nullable String v2) {
2539     // todo duplicates com.intellij.util.text.VersionComparatorUtil.compare
2540     // todo please refactor next time you make changes here
2541     if (v1 == null && v2 == null) {
2542       return 0;
2543     }
2544     if (v1 == null) {
2545       return -1;
2546     }
2547     if (v2 == null) {
2548       return 1;
2549     }
2550
2551     String[] part1 = v1.split("[\\.\\_\\-]");
2552     String[] part2 = v2.split("[\\.\\_\\-]");
2553
2554     int idx = 0;
2555     for (; idx < part1.length && idx < part2.length; idx++) {
2556       String p1 = part1[idx];
2557       String p2 = part2[idx];
2558
2559       int cmp;
2560       if (p1.matches("\\d+") && p2.matches("\\d+")) {
2561         cmp = new Integer(p1).compareTo(new Integer(p2));
2562       }
2563       else {
2564         cmp = part1[idx].compareTo(part2[idx]);
2565       }
2566       if (cmp != 0) return cmp;
2567     }
2568
2569     if (part1.length == part2.length) {
2570       return 0;
2571     }
2572     else {
2573       boolean left = part1.length > idx;
2574       String[] parts = left ? part1 : part2;
2575
2576       for (; idx < parts.length; idx++) {
2577         String p = parts[idx];
2578         int cmp;
2579         if (p.matches("\\d+")) {
2580           cmp = new Integer(p).compareTo(0);
2581         }
2582         else {
2583           cmp = 1;
2584         }
2585         if (cmp != 0) return left ? cmp : -cmp;
2586       }
2587       return 0;
2588     }
2589   }
2590
2591   @Contract(pure = true)
2592   public static int getOccurrenceCount(@NotNull String text, final char c) {
2593     int res = 0;
2594     int i = 0;
2595     while (i < text.length()) {
2596       i = text.indexOf(c, i);
2597       if (i >= 0) {
2598         res++;
2599         i++;
2600       }
2601       else {
2602         break;
2603       }
2604     }
2605     return res;
2606   }
2607
2608   @Contract(pure = true)
2609   public static int getOccurrenceCount(@NotNull String text, @NotNull String s) {
2610     int res = 0;
2611     int i = 0;
2612     while (i < text.length()) {
2613       i = text.indexOf(s, i);
2614       if (i >= 0) {
2615         res++;
2616         i++;
2617       }
2618       else {
2619         break;
2620       }
2621     }
2622     return res;
2623   }
2624
2625   @NotNull
2626   @Contract(pure = true)
2627   public static String fixVariableNameDerivedFromPropertyName(@NotNull String name) {
2628     if (isEmptyOrSpaces(name)) return name;
2629     char c = name.charAt(0);
2630     if (isVowel(c)) {
2631       return "an" + Character.toUpperCase(c) + name.substring(1);
2632     }
2633     return "a" + Character.toUpperCase(c) + name.substring(1);
2634   }
2635
2636   @NotNull
2637   @Contract(pure = true)
2638   public static String sanitizeJavaIdentifier(@NotNull String name) {
2639     final StringBuilder result = new StringBuilder(name.length());
2640
2641     for (int i = 0; i < name.length(); i++) {
2642       final char ch = name.charAt(i);
2643       if (Character.isJavaIdentifierPart(ch)) {
2644         if (result.length() == 0 && !Character.isJavaIdentifierStart(ch)) {
2645           result.append("_");
2646         }
2647         result.append(ch);
2648       }
2649     }
2650
2651     return result.toString();
2652   }
2653
2654   public static void assertValidSeparators(@NotNull CharSequence s) {
2655     char[] chars = CharArrayUtil.fromSequenceWithoutCopying(s);
2656     int slashRIndex = -1;
2657
2658     if (chars != null) {
2659       for (int i = 0, len = s.length(); i < len; ++i) {
2660         if (chars[i] == '\r') {
2661           slashRIndex = i;
2662           break;
2663         }
2664       }
2665     }
2666     else {
2667       for (int i = 0, len = s.length(); i < len; i++) {
2668         if (s.charAt(i) == '\r') {
2669           slashRIndex = i;
2670           break;
2671         }
2672       }
2673     }
2674
2675     if (slashRIndex != -1) {
2676       String context =
2677         String.valueOf(last(s.subSequence(0, slashRIndex), 10, true)) + first(s.subSequence(slashRIndex, s.length()), 10, true);
2678       context = escapeStringCharacters(context);
2679       LOG.error("Wrong line separators: '" + context + "' at offset " + slashRIndex);
2680     }
2681   }
2682
2683   @NotNull
2684   @Contract(pure = true)
2685   public static String tail(@NotNull String s, final int idx) {
2686     return idx >= s.length() ? "" : s.substring(idx, s.length());
2687   }
2688
2689   /**
2690    * Splits string by lines.
2691    *
2692    * @param string String to split
2693    * @return array of strings
2694    */
2695   @NotNull
2696   @Contract(pure = true)
2697   public static String[] splitByLines(@NotNull String string) {
2698     return splitByLines(string, true);
2699   }
2700
2701   /**
2702    * Splits string by lines. If several line separators are in a row corresponding empty lines
2703    * are also added to result if {@code excludeEmptyStrings} is {@code false}.
2704    *
2705    * @param string String to split
2706    * @return array of strings
2707    */
2708   @NotNull
2709   @Contract(pure = true)
2710   public static String[] splitByLines(@NotNull String string, boolean excludeEmptyStrings) {
2711     return (excludeEmptyStrings ? EOL_SPLIT_PATTERN : EOL_SPLIT_PATTERN_WITH_EMPTY).split(string);
2712   }
2713
2714   @NotNull
2715   @Contract(pure = true)
2716   public static String[] splitByLinesDontTrim(@NotNull String string) {
2717     return EOL_SPLIT_DONT_TRIM_PATTERN.split(string);
2718   }
2719
2720   /**
2721    * Splits string by lines, keeping all line separators at the line ends and in the empty lines.
2722    * <br> E.g. splitting text
2723    * <blockquote>
2724    *   foo\r\n<br>
2725    *   \n<br>
2726    *   bar\n<br>
2727    *   \r\n<br>
2728    *   baz\r<br>
2729    *   \r<br>
2730    * </blockquote>
2731    * will return the following array: foo\r\n, \n, bar\n, \r\n, baz\r, \r
2732    *
2733    */
2734   @NotNull
2735   @Contract(pure = true)
2736   public static String[] splitByLinesKeepSeparators(@NotNull String string) {
2737     return EOL_SPLIT_KEEP_SEPARATORS.split(string);
2738   }
2739
2740   @NotNull
2741   @Contract(pure = true)
2742   public static List<Pair<String, Integer>> getWordsWithOffset(@NotNull String s) {
2743     List<Pair<String, Integer>> res = ContainerUtil.newArrayList();
2744     s += " ";
2745     StringBuilder name = new StringBuilder();
2746     int startInd = -1;
2747     for (int i = 0; i < s.length(); i++) {
2748       if (Character.isWhitespace(s.charAt(i))) {
2749         if (name.length() > 0) {
2750           res.add(Pair.create(name.toString(), startInd));
2751           name.setLength(0);
2752           startInd = -1;
2753         }
2754       }
2755       else {
2756         if (startInd == -1) {
2757           startInd = i;
2758         }
2759         name.append(s.charAt(i));
2760       }
2761     }
2762     return res;
2763   }
2764
2765   /**
2766    * Implementation of <a href="http://www.codinghorror.com/blog/2007/12/sorting-for-humans-natural-sort-order.html"/>
2767    * "Sorting for Humans: Natural Sort Order"</a>
2768    */
2769   @Contract(pure = true)
2770   public static int naturalCompare(@Nullable String string1, @Nullable String string2) {
2771     return naturalCompare(string1, string2, false);
2772   }
2773
2774   @Contract(pure = true)
2775   private static int naturalCompare(@Nullable String string1, @Nullable String string2, boolean caseSensitive) {
2776     //noinspection StringEquality
2777     if (string1 == string2) {
2778       return 0;
2779     }
2780     if (string1 == null) {
2781       return -1;
2782     }
2783     if (string2 == null) {
2784       return 1;
2785     }
2786
2787     final int string1Length = string1.length();
2788     final int string2Length = string2.length();
2789     int i = 0;
2790     int j = 0;
2791     for (; i < string1Length && j < string2Length; i++, j++) {
2792       char ch1 = string1.charAt(i);
2793       char ch2 = string2.charAt(j);
2794       if ((isDecimalDigit(ch1) || ch1 == ' ') && (isDecimalDigit(ch2) || ch2 == ' ')) {
2795         int startNum1 = i;
2796         while (ch1 == ' ' || ch1 == '0') { // skip leading spaces and zeros
2797           startNum1++;
2798           if (startNum1 >= string1Length) break;
2799           ch1 = string1.charAt(startNum1);
2800         }
2801         int startNum2 = j;
2802         while (ch2 == ' ' || ch2 == '0') { // skip leading spaces and zeros
2803           startNum2++;
2804           if (startNum2 >= string2Length) break;
2805           ch2 = string2.charAt(startNum2);
2806         }
2807         i = startNum1;
2808         j = startNum2;
2809         // find end index of number
2810         while (i < string1Length && isDecimalDigit(string1.charAt(i))) i++;
2811         while (j < string2Length && isDecimalDigit(string2.charAt(j))) j++;
2812         final int lengthDiff = (i - startNum1) - (j - startNum2);
2813         if (lengthDiff != 0) {
2814           // numbers with more digits are always greater than shorter numbers
2815           return lengthDiff;
2816         }
2817         for (; startNum1 < i; startNum1++, startNum2++) {
2818           // compare numbers with equal digit count
2819           final int diff = string1.charAt(startNum1) - string2.charAt(startNum2);
2820           if (diff != 0) {
2821             return diff;
2822           }
2823         }
2824         i--;
2825         j--;
2826       }
2827       else {
2828         if (caseSensitive) {
2829           return ch1 - ch2;
2830         }
2831         else {
2832           // similar logic to charsMatch() below
2833           if (ch1 != ch2) {
2834             final int diff1 = StringUtilRt.toUpperCase(ch1) - StringUtilRt.toUpperCase(ch2);
2835             if (diff1 != 0) {
2836               final int diff2 = StringUtilRt.toLowerCase(ch1) - StringUtilRt.toLowerCase(ch2);
2837               if (diff2 != 0) {
2838                 return diff2;
2839               }
2840             }
2841           }
2842         }
2843       }
2844     }
2845     // After the loop the end of one of the strings might not have been reached, if the other
2846     // string ends with a number and the strings are equal until the end of that number. When
2847     // there are more characters in the string, then it is greater.
2848     if (i < string1Length) {
2849       return 1;
2850     }
2851     if (j < string2Length) {
2852       return -1;
2853     }
2854     if (!caseSensitive && string1Length == string2Length) {
2855       // do case sensitive compare if case insensitive strings are equal
2856       return naturalCompare(string1, string2, true);
2857     }
2858     return string1Length - string2Length;
2859   }
2860
2861   @Contract(pure = true)
2862   public static boolean isDecimalDigit(char c) {
2863     return c >= '0' && c <= '9';
2864   }
2865
2866   @Contract(pure = true)
2867   public static int compare(@Nullable String s1, @Nullable String s2, boolean ignoreCase) {
2868     //noinspection StringEquality
2869     if (s1 == s2) return 0;
2870     if (s1 == null) return -1;
2871     if (s2 == null) return 1;
2872     return ignoreCase ? s1.compareToIgnoreCase(s2) : s1.compareTo(s2);
2873   }
2874
2875   @Contract(pure = true)
2876   public static int comparePairs(@Nullable String s1, @Nullable String t1, @Nullable String s2, @Nullable String t2, boolean ignoreCase) {
2877     final int compare = compare(s1, s2, ignoreCase);
2878     return compare != 0 ? compare : compare(t1, t2, ignoreCase);
2879   }
2880
2881   @Contract(pure = true)
2882   public static int hashCode(@NotNull CharSequence s) {
2883     return stringHashCode(s);
2884   }
2885
2886   @Contract(pure = true)
2887   public static boolean equals(@Nullable CharSequence s1, @Nullable CharSequence s2) {
2888     if (s1 == null ^ s2 == null) {
2889       return false;
2890     }
2891
2892     if (s1 == null) {
2893       return true;
2894     }
2895
2896     if (s1.length() != s2.length()) {
2897       return false;
2898     }
2899     for (int i = 0; i < s1.length(); i++) {
2900       if (s1.charAt(i) != s2.charAt(i)) {
2901         return false;
2902       }
2903     }
2904     return true;
2905   }
2906
2907   @Contract(pure = true)
2908   public static boolean equalsIgnoreCase(@Nullable CharSequence s1, @Nullable CharSequence s2) {
2909     if (s1 == null ^ s2 == null) {
2910       return false;
2911     }
2912
2913     if (s1 == null) {
2914       return true;
2915     }
2916
2917     if (s1.length() != s2.length()) {
2918       return false;
2919     }
2920     for (int i = 0; i < s1.length(); i++) {
2921       if (!charsMatch(s1.charAt(i), s2.charAt(i), true)) {
2922         return false;
2923       }
2924     }
2925     return true;
2926   }
2927
2928   @Contract(pure = true)
2929   public static boolean equalsIgnoreWhitespaces(@Nullable CharSequence s1, @Nullable CharSequence s2) {
2930     if (s1 == null ^ s2 == null) {
2931       return false;
2932     }
2933
2934     if (s1 == null) {
2935       return true;
2936     }
2937
2938     int len1 = s1.length();
2939     int len2 = s2.length();
2940
2941     int index1 = 0;
2942     int index2 = 0;
2943     while (index1 < len1 && index2 < len2) {
2944       if (s1.charAt(index1) == s2.charAt(index2)) {
2945         index1++;
2946         index2++;
2947         continue;
2948       }
2949
2950       boolean skipped = false;
2951       while (index1 != len1 && isWhiteSpace(s1.charAt(index1))) {
2952         skipped = true;
2953         index1++;
2954       }
2955       while (index2 != len2 && isWhiteSpace(s2.charAt(index2))) {
2956         skipped = true;
2957         index2++;
2958       }
2959
2960       if (!skipped) return false;
2961     }
2962
2963     for (; index1 != len1; index1++) {
2964       if (!isWhiteSpace(s1.charAt(index1))) return false;
2965     }
2966     for (; index2 != len2; index2++) {
2967       if (!isWhiteSpace(s2.charAt(index2))) return false;
2968     }
2969
2970     return true;
2971   }
2972
2973   @Contract(pure = true)
2974   public static boolean equalsTrimWhitespaces(@NotNull CharSequence s1, @NotNull CharSequence s2) {
2975     int start1 = 0;
2976     int end1 = s1.length();
2977     int start2 = 0;
2978     int end2 = s2.length();
2979
2980     while (start1 < end1) {
2981       char c = s1.charAt(start1);
2982       if (!isWhiteSpace(c)) break;
2983       start1++;
2984     }
2985
2986     while (start1 < end1) {
2987       char c = s1.charAt(end1 - 1);
2988       if (!isWhiteSpace(c)) break;
2989       end1--;
2990     }
2991
2992     while (start2 < end2) {
2993       char c = s2.charAt(start2);
2994       if (!isWhiteSpace(c)) break;
2995       start2++;
2996     }