001package Torello.Java;
002
003import java.util.function.*;
004import java.util.stream.*;
005
006import Torello.Java.Function.*;
007
008/**
009 * An efficient way to replace multiple substring's, or single-characters, inside of a single
010 * Java String, <I>in place, without rebuilding the returned {@code String} more than once.</I>
011 * 
012 * <EMBED CLASS=external-html DATA-FILE-ID=STR_REPLACE>
013 */
014@Torello.JavaDoc.StaticFunctional
015public class StrReplace
016{
017    private StrReplace() { }
018
019    /**
020     * Convenience Method.
021     * <BR />Case-Sensitive
022     * <BR />Invokes: {@link #r(boolean, String, String[], String[])}
023     */
024    public static String r(String s, String[] matchStrs, String[] replaceStrs)
025    { return r(false, s, matchStrs, replaceStrs); }
026
027    /**
028     * This shall replace each instance of the elements of parameter {@code 'matchStrs'} in input
029     * {@code String 's'} with the elements of <I><B>parallel array</I></B> {@code 'replaceStrs'}
030     *
031     * @param ignoreCase When this parameter is set to {@code TRUE}, then the comparisons that
032     * determine whether a match has occurred shall ignore the case of the characters involved.
033     * 
034     * @param s This may be any Java {@code String}.
035     * 
036     * @param matchStrs This is a {@code String[] array} that should hold some sub-strings of input
037     * parameter {@code 's'}.  This method shall search {@code 's'} - <I>left to right</I> - for
038     * any instances of the list of {@code 'matchStrs'} and replace those sub-strings with 
039     * whatever {@code String} is in the <I>same array-index location (parallel-array)</I> from
040     * input parameter {@code 'replaceStrs'}
041     * 
042     * <BR /><BR /><B STYLE="color: red;">MULTIPLE-MATCH SCENARIOS:</B> If there are substring's
043     * within parameter {@code 'matchStrs'} such that the loop-iterations of this method could
044     * select multiple, different {@code String's} as a substring match with the input parameter
045     * {@code 's'}, then the loops will <I><B>always replace the first match found with input
046     * {@code String[] array} parameter {@code 'matchStrs'}</I></B>.
047     * 
048     * <DIV CLASS="EXAMPLE">{@code
049     * String[] matches         = { "Bell", "Belle", "Belleview" };
050     * String[] replacements    = { "Ring", "Flower", "Microsoft Corporate HQ" };
051     * String   theString       = "Microsoft Corporate Apartments are in Belleview, Washington";
052     *
053     * System.out.println(StrReplace.r(false, theString, matches, replacements));
054     *
055     * // Would print to terminal:
056     * // Microsoft Corporate Apartments are in Ringeview, Washington
057     * 
058     * // This is because the point when the "Replace Loop" cursor reaches character 'B' in
059     * // 'Bellview', the first match it finds with parameter 'matches' is the String "Bell"
060     * // ... And because the 'replacements' parameter maps the word "Bell" to "Ring"
061     * }</DIV>
062     * 
063     * @param replaceStrs This is also an {@code String[] array} that should hold sub-strings.
064     * Every time a copy of any {@code 'matchStr'} is found within {@code 's'}, the index of the
065     * sub-string match from {@code 'matchStrs'} shall be used to lookup the parallel
066     * {@code 'replaceStr'}, and used to over-write or replace that sub-string inside {@code 's'}.
067     * 
068     * <BR /><BR /><B STYLE="color: red;">PARALLEL ARRAY:</B> This array should be considered
069     * parallel to input {@code String[] array 'matchStrs'}.  It provides a replacement mapping.
070     * It is required to be the exact same length as array {@code 'matchStrs'}, or an exception
071     * shall throw.
072     * 
073     * @return This shall return a new-{@code String} where the replacements that were requested
074     * have been substituted.
075     * 
076     * @throws NullPointerException If any of the {@code String's} inside the {@code String[]
077     * arrays} contain null pointers.
078     * 
079     * @throws ParallelArrayException If the length of array {@code matchStrs} does not equal
080     * the length of array {@code replaceStrs}, then this exception shall throw.   This is because
081     * these arrays are intended to be parallel arrays, where the references in the second array
082     * are supposed to be used to replace sub-strings (in {@code 's'}) from the first array.
083     */
084    public static String r
085        (boolean ignoreCase, String s, String[] matchStrs, String[] replaceStrs)
086    {
087        // Make sure these arrays are parallel, and if not throw ParallelArrayException
088        // If there are any 'null' values in these arrays, throw NullPointerException
089
090        ParallelArrayException.check
091            (matchStrs, "matchStrs", true, replaceStrs, "replaceStrs", true);
092
093        // Java Stream's shall keep records of where and which the matches occurred
094        IntStream.Builder   whereB  = IntStream.builder();
095        IntStream.Builder   whichB  = IntStream.builder();
096        int                 delta   = 0;
097
098        // This part of the code finds the locations of all the matches in the input string.
099        // It does not build the new String, but rather, finds indexes first.  This way a
100        // char[] array can be built, and then populated with the updated sub-strings.
101
102        TOP:
103        for (int i=0; i < s.length(); i++)
104
105            for (int j=0; j < matchStrs.length; j++)
106
107                if (s.regionMatches(ignoreCase, i, matchStrs[j], 0, matchStrs[j].length()))
108                {
109                    // Save the "original String index" of WHERE the match occurred
110                    whereB.accept(i);
111
112                    // Save the "match index" of WHICH match has occurred
113                    whichB.accept(j);
114
115                    // Keep a record of the 'delta' - which is the change in size of the 
116                    // output/returned String
117                    delta = delta - matchStrs[j].length() + replaceStrs[j].length();
118                    
119                    // Make sure to advance the index-pointer, skip the most recent match
120                    i += matchStrs[j].length() - 1;
121                    
122                    continue TOP;
123                }
124
125        // List of indices into the input-String for WHERE matches occurred.
126        int[] whereArr = whereB.build().toArray();
127
128        // List of indices into the match-array for WHICH matches occurred.
129        int[] whichArr = whichB.build().toArray();
130
131        // The new "Char Array" which will be built into a String.  The "change in size" was
132        // computed earlier
133        char[] cArr = new char[s.length() + delta];
134
135
136        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
137        // If there were no matches, return the original string
138        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
139
140        if (whereArr.length == 0) return s;
141
142
143        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
144        // "Pre-Loop Priming Update" or "Priming Read"
145        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
146
147        // This just copies the first non-matching sub-string portion to the cArr[]
148        s.getChars(0, whereArr[0], cArr, 0);
149
150        // These are the loop-control variables
151        int oldStrPos   = whereArr[0];
152        int newStrPos   = whereArr[0];
153        int i           = 0;
154
155        while (i < whichArr.length)
156        {
157            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
158            // Copy the next match from the "Replacement Strings Array"
159            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
160
161            String  replaceStr  = replaceStrs[whichArr[i]];
162
163            replaceStr.getChars(0, replaceStr.length(), cArr, newStrPos);
164
165            // Advance the pointers
166            newStrPos += replaceStr.length();
167            oldStrPos += matchStrs[whichArr[i]].length();
168            i++;
169
170            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
171            // Copy the next non-matching sub-string section from the "Old Input String"
172            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
173
174            int end = (i < whichArr.length) ? whereArr[i] : s.length();
175            s.getChars(oldStrPos, end, cArr, newStrPos);
176
177            // Advance the pointers
178            newStrPos += (end - oldStrPos);
179            oldStrPos = end;
180        }
181
182        // Convert the character array into a String
183        return new String(cArr);
184    }
185
186    /**
187     * Convenience Method.
188     * <BR />Case-Sensitive
189     * <BR />Invokes: {@link #r(boolean, String, String[], IntTFunction)}
190     */
191    public static String r(
192        String s, String[] matchStrs, IntTFunction<String, String> replaceFunction)
193    { return r(false, s, matchStrs, replaceFunction); }
194
195    /**
196     * This shall replace each instance of the elements of parameter {@code 'matchStrs'} in input
197     * {@code String 's'} with the {@code String}-value returned by the {@code 'replaceFunction'}
198     * lambda-method / {@code functional-interface}.
199     * 
200     * @param ignoreCase When this parameter is set to {@code TRUE}, then the comparisons that
201     * determine whether a match has occurred shall ignore the case of the characters involved.
202     * 
203     * @param s This may be any Java {@code String}.
204     * 
205     * @param matchStrs This is an {@code String[] array} that should hold some sub-strings of input
206     * parameter {@code 's'}.  This method shall search {@code 's'} - <I>left to right</I> - for
207     * any instances of the list of {@code 'matchStrs'} and replace those sub-strings with 
208     * whatever {@code String} is returned by the {@code 'replaceFunction'}.
209     * 
210     * <BR /><BR /><B STYLE="color: red;">MULTIPLE-MATCH SCENARIOS:</B> If there are substring's
211     * within parameter {@code 'matchStrs'} such that the loop-iterations of this method could
212     * select multiple, different {@code String's} as a substring match with the input parameter
213     * {@code 's'}, then the loops will <I><B>always replace the first match found with input
214     * {@code String[] array} parameter {@code 'matchStrs'}</I></B>.
215     * 
216     * <DIV CLASS="EXAMPLE">{@code
217     * String[] matches         = { "Bell", "Belle", "Belleview" };
218     * String   theString       = "Microsoft Corporate Apartments are in Belleview, Washington";
219     *
220     * System.out.println
221     *     (StrReplace.r(false, theString, matches, (int i, String s) -> s.toUpperCase()));
222     *
223     * // Would print to terminal:
224     * // Microsoft Corporate Apartments are in BELLeview, Washington
225     * 
226     * // This is because the point when the "Replace Loop" cursor reaches character 'B' in
227     * // 'Bellview', the first match it finds with parameter 'matches' is the String "Bell"
228     * // ... And because the 'replaceFunction' parameter merely asks the match-String be
229     * //     converted to upper-case.
230     * }</DIV>
231     * 
232     * @param replaceFunction This shall receive as input a Java {@code String} that has matched
233     * one of the {@code String's} that are within {@code 'matchStrs'}, along with the
234     * {@code String}-index into the {@code String} where that match occured.  It must reply with
235     * a replacement {@code String} (to replace that sub-string within the input {@code String}
236     * parameter {@code 's'})  
237     * 
238     * <EMBED CLASS='external-html' DATA-IN=String DATA-OUT=String
239     *      DATA-FILE-ID=STR_REPLACE_LOOK_AR>
240     * 
241     * @return This shall return a new-{@code String} where the replacements that were requested
242     * have been substituted.
243     * 
244     * @throws NullPointerException If any of the {@code String's} inside the {@code String[]
245     * arrays} contain null pointers.
246     */
247    public static String r(
248        boolean ignoreCase, String s, String[] matchStrs,
249        IntTFunction<String, String> replaceFunction
250    )
251    {
252        // Loop simply checks for null pointers.
253        for (int i=0; i < matchStrs.length; i++)
254
255            if (matchStrs[i] == null) throw new NullPointerException(
256                "The " + i + StringParse.ordinalIndicator(i) + " of array parameter " +
257                "'matchStrs' is null."
258            );
259
260        // Use a StringBuilder to build the return String.  It is precisely what it was
261        // it was designed for.
262
263        StringBuilder sb = new StringBuilder();
264
265        int last = 0;
266
267        // Main Loop: Builds a replacement StringBuilder.  Looks for matches between the input
268        // String 's', and the matchStrs in array String[] matchStrs.
269
270        TOP:
271        for (int i=0; i < s.length(); i++)
272
273            for (int j=0; j < matchStrs.length; j++)
274
275                if (s.regionMatches(ignoreCase, i, matchStrs[j], 0, matchStrs[j].length()))
276                {
277                    // A match was found, so begin
278                    String nonMatchStr  = s.substring(last, i);
279                    String oldStr       = s.substring(i, i + matchStrs[j].length());
280                    String newStr       = replaceFunction.apply(i, oldStr);
281
282                    // Append them to the StringBuilder.
283                    if (nonMatchStr.length() > 0) sb.append(nonMatchStr);
284                    sb.append(newStr);
285
286                    // Update the pointers
287                    last    = i + oldStr.length();
288                    i       = last - 1; // use -1 because of the loop-incrementer at top
289
290                    continue TOP;
291                }
292
293        if (last == 0) return s; // There were no matches, return original String.
294
295        // This happens when there are remaining characters after the last match that occurred.
296        // Same as the HTML Parser - trailing READ/PARSE.
297
298        if (last < s.length()) sb.append(s.substring(last));
299
300        return sb.toString();
301    }
302
303    /**
304     * Convenience Method.
305     * <BR />Case-Sensitive
306     * <BR />Invokes: {@link #r(String, boolean, String[], ToCharIntTFunc)}
307     */
308    public static String r(
309        String s, ToCharIntTFunc<String> replaceFunction, String[] matchStrs
310    )
311    { return r(s, false, matchStrs, replaceFunction); }
312
313    /**
314     * This shall replace each instance of the elements of parameter {@code 'matchStrs'} in input
315     * {@code String 's'} with the {@code char}-value returned by the {@code 'replaceFunction'}
316     * lambda-method / {@code functional-interface}.
317     * 
318     * <DIV CLASS="EXAMPLE">{@code
319     * String[] matches    = { "&Pi;", "&Rho;", "&Sigma;", "&Tau;", "&Upsilon;", "&Phi;" };
320     * String   theString  = "Greek: &Pi;, &Rho;, &Sigma;, &Tau;, &Upsilon;, &Phi;";
321     * 
322     * System.out.println
323     *     (StrReplace.r(theString, false, matches, (int i, String s) -> Escape.escHTMLToChar(s)));
324     *
325     * // Would print to terminal:
326     * // Greek: Π, Ρ, Σ, Τ, Υ, Φ 
327     * }</DIV>
328     * 
329     * @param s This may be any Java {@code String}.
330     * 
331     * @param ignoreCase When this parameter is set to {@code TRUE}, then the comparisons that
332     * determine whether a match has occurred shall ignore the case of the characters involved.
333     * 
334     * @param matchStrs This is an {@code String[] array} that should hold some sub-strings of input
335     * parameter {@code 's'}.  This method shall search {@code 's'} - <I>left to right</I> - for
336     * any instances of the list of {@code 'matchStrs'} and replace those sub-strings with 
337     * whatever {@code char} is returned by the {@code 'replaceFunction'} for that given 
338     * match-{@code String};
339     * 
340     * <BR /><BR /><B STYLE="color: red;">MULTIPLE-MATCH SCENARIOS:</B> If there are multiple
341     * copies (either ignoring case, or not ignoring case), of an identical {@code String} put into
342     * {@code String[]} array parameter {@code 'matchStrs'}, this method will not generate an
343     * exception (or anything like that) in such scenarios.
344     * 
345     * <BR /><BR />It is important to note that when invoking the {@code replaceFunction's} method
346     * {@code apply(String)}, the {@code String} that is provided to {@code apply} will be the
347     * <I>exact substring</I> found in the original-{@code String}.
348     * 
349     * @param replaceFunction This shall receive as input a Java {@code String} that has matched
350     * one of the {@code String's} that are within {@code 'matchStrs'}, along with the
351     * {@code String}-index into the {@code String} where that match occured.  It must reply with
352     * a replacement {@code 'char'} (which will replace that matched sub-string found within 
353     * {@code 's'}).
354     * 
355     * <EMBED CLASS='external-html' DATA-IN=String DATA-OUT=char
356     *      DATA-FILE-ID=STR_REPLACE_LOOK_AR>
357     * 
358     * @return This shall return a new-{@code String} where the replacements that were requested
359     * have been substituted.
360     * 
361     * @throws NullPointerException If any of the {@code String's} inside the {@code String[]
362     * arrays} contain null pointers.
363     */
364    public static String r(
365        String s, boolean ignoreCase, String[] matchStrs,
366        ToCharIntTFunc<String> replaceFunction
367    )
368    {
369        // Loop simply checks for null pointers.
370        for (int i=0; i < matchStrs.length; i++)
371
372            if (matchStrs[i] == null) throw new NullPointerException(
373                "The " + i + StringParse.ordinalIndicator(i) + " of array parameter " +
374                "'matchStrs' is null."
375            );
376
377        // NOTE: This builder used to save the indices where matches are found.
378        //
379        // The IntStream that we are building will be "interleaved" in the sense that each integer
380        // in an *EVEN* location in the output array shall represent the starting-index of a 
381        // sub-string match, and the *ODD* integer (the one that immediately follows it) represents
382        // the ending-index of a sub-string match.
383
384        IntStream.Builder b = IntStream.builder();
385
386        // This is used to keep track of the size-change of the output string
387        int delta = 0;
388
389        // Main Loop: Builds a replacement StringBuilder.  Looks for matches between the input
390        // String 's', and the matchStrs in array String[] matchStrs.
391
392        TOP:
393        for (int i=0; i < s.length(); i++)
394
395            for (int j=0; j < matchStrs.length; j++)
396
397                if (s.regionMatches(ignoreCase, i, matchStrs[j], 0, matchStrs[j].length()))
398                {
399                    // SEE NOTE ABOVE: When a match is found, the starting-index of the 
400                    // substring match is appended to the IntStream, then IMMEDIATELY AFTERWARDS
401                    // the ending-index of the substring match is appended.
402
403                    int len = matchStrs[j].length();
404                    b.accept(i);
405                    b.accept(i + len);
406
407                    // The change in size of the output String is precisely the length of the
408                    // match minus 1.  A substring is being replaced by a character.
409
410                    delta += (len - 1);
411
412                    // A Match was found, so skip to the next location.  Move past the match
413                    // that was just identified.
414                    i += (len -1);
415
416                    continue TOP;
417                }
418        
419        // Keeps track of all the locations in the original string where matches occurred.
420        int[] whereArr = b.build().toArray();
421
422        // If there were no matches, return the original String
423        if (whereArr.length == 0) return s;
424
425        // The output string will be stored here.
426        char[] cArr = new char[s.length() - delta];
427
428        // These are the array indices of both arrays, and the original string
429        int i           = 0;    // Match-String Location Pointer Array
430        int oldStrPos   = 0;    // Pointer to Input-String index
431        int newStrPos   = 0;    // Pointer to Output Char-Array index
432
433        // Iterate and replace the substrings.
434        while (i < whereArr.length)
435        {
436            if (oldStrPos == whereArr[i])
437            {
438                // Ask the "Replace Function" for the replacement character for the matched
439                // substring
440                //
441                // NOTE: Each *EVEN* location in the whereArr contains a start-index, and the
442                //       *ODD* location immediately following contains an end-index.  These
443                //       start-end pair identify the substring matches that were found.
444                //
445                // NOW:  Grab that substring, and pass it to the parameter-provided (user
446                //       provided)  "replaceFunction" which informs this method what character
447                //       to use when replacing a substring
448
449                cArr[newStrPos++] = replaceFunction.apply
450                    (whereArr[i], s.substring(whereArr[i], whereArr[i+1]));
451
452                // Advance the "pointers" (advance the array indices)
453                //
454                // The pointer to the "source array" (a.k.a. the "original array") should be
455                // advanced to the location of the end of the match we just found.  That end
456                // was pointed at by the start-end *PAIR* whereArr[i] ... whereArr[i+1].
457                //
458                // NOTE: the substring match is "exclusive of" the actual character
459                //       located at whereArr[i+1].  Specifically, in the original string, there
460                //       was a sub-string match to replace with a single character that began
461                //       at original-string index/location whereArr[i] and continuing to 
462                //       index/location (whereArr[i+1]-1)
463                //
464                //       Java's java.lang.String.subtring(star, end) is *ALWAYS* exclusive of
465                //       the character located at 'end'
466
467                oldStrPos = whereArr[i+1];
468
469                // Skip the "pair" (starting-index and ending-index).   Note that at the end
470                // of the loop, variable 'i' shall *ALWAYS* be an even-number.
471                i += 2;
472            }
473
474            else cArr[newStrPos++] = s.charAt(oldStrPos++);
475        }
476
477        // Just like in HTMLNode Parse, the trailing ("tail") of characters after the final
478        // match in the String need to be appended to the output char[] array.  These are
479        // "missed" or "skipped" by the above replacement loop.  (Similar to a "trailing read")
480
481        while (newStrPos < cArr.length) cArr[newStrPos++] = s.charAt(oldStrPos++);
482
483        // AGAIN: All of the replacements where done on a "char[] array".  Convert that array
484        //        to an actual String, and return it to the user.
485
486        return new String(cArr);
487    }
488
489    /**
490     * Convenience Method.
491     * <BR />Case-Sensitive
492     * <BR />Invokes: {@link #r(boolean, String, String[], char[])}
493     */
494    public static String r(
495        String s, String[] matchStrs, char[] replaceChars
496    )
497    { return r(false, s, matchStrs, replaceChars); }
498
499    /**
500     * This shall replace each instance of the elements of parameter {@code 'matchStrs'} in input
501     * {@code String 's'} with the provided characters in <I><B>parallel array</I></B>
502     * {@code 'replaceChars'}.
503     *
504     * <DIV CLASS="EXAMPLE">{@code
505     * String[]  matches       = { "&Pi;", "&Rho;", "&Sigma;", "&Tau;", "&Upsilon;", "&Phi;" };
506     * char[]    replacements  = { 'Π', 'Ρ', 'Σ', 'Τ', 'Υ', 'Φ'  };
507     * String    theString     = "Greek Letters: &Pi;, &Rho;, &Sigma;, &Tau;, &Upsilon;, &Phi;";
508     * 
509     * System.out.println(StrReplace.r(false, theString, matches, replacements);
510     *
511     * // Would print to terminal the following String:
512     * // Greek Letters: Π, Ρ, Σ, Τ, Υ, Φ 
513     * }</DIV>
514     * 
515     * @param s This may be any Java {@code String}.
516     * 
517     * @param ignoreCase When this parameter is set to {@code TRUE}, then the comparisons that
518     * determine whether a match has occurred shall ignore the case of the characters involved.
519     * 
520     * @param matchStrs This is a {@code String[] array} that should hold some sub-strings of input
521     * parameter {@code 's'}.  This method shall search {@code 's'} - <I>left to right</I> - for
522     * any instances of the list of {@code 'matchStrs'} and replace those sub-strings with 
523     * whatever {@code char} is in the <I>same array-index location (parallel-array)</I> from
524     * input parameter {@code 'replaceChars'}.
525     * 
526     * <BR /><BR /><B STYLE="color: red;">MULTIPLE-MATCH SCENARIOS:</B> If there are substring's
527     * within parameter {@code 'matchStrs'} such that the loop-iterations of this method could
528     * select multiple, different {@code String's} as a substring match with the input parameter
529     * {@code 's'}, then the loops will <I><B>always replace the first match found with input
530     * {@code String[] array} parameter {@code 'matchStrs'}</I></B>.
531     * 
532     * @param replaceChars This is also a {@code char[] array}.  Every time a copy of any of the
533     * {@code 'matchStrs'} is found within {@code 's'}, the index of the {@code String} match from
534     * {@code 'matchStrs'} shall be used to lookup the parallel {@code 'replaceChar'}, and used
535     * to over-write or replace that character inside {@code 's'}.
536     * 
537     * <BR /><BR /><B STYLE="color: red;">PARALLEL ARRAY:</B> This array should be considered
538     * parallel to input {@code char[] array 'matchStrs'}.  It provides a replacement mapping.
539     * It is required to be the exact same length as array {@code 'matchChars'}, or an exception
540     * shall throw.
541     * 
542     * @return This shall return a new-{@code String} where the replacements that were requested
543     * have been substituted.
544     * 
545     * @throws NullPointerException If any of the {@code String's} inside the {@code String[]
546     * matchStrs} are null pointers.
547     * 
548     * @throws ParallelArrayException If the arrays {@code matchStrs} and {@code replaceChars}
549     * are not identical lengths.  These arrays must be parallel
550     */
551    public static String r
552        (boolean ignoreCase, String s, String[] matchStrs, char[] replaceChars)
553    {
554        // Check that these arrays are parallel, and if not, throw ParallelArrayException
555        // If 'matchStr' has a null, throw NullPointerException
556
557        ParallelArrayException.check
558            (matchStrs, "matchStrs", true, replaceChars, "replaceChars");
559
560        // The first stream is used to save the indices where matches are found.
561        // The second stream is used to save WHICH MATCH has occurred, in order to retrieve the
562        // replacement character.
563        //
564        // NOTE: This builder used to save the indices where matches are found.
565        //
566        // The IntStream that we are building will be "interleaved" in the sense that each integer
567        // in an *EVEN* location in the output array shall represent the starting-index of a 
568        // sub-string match, and the *ODD* integer (the one that immediately follows it) represents
569        // the ending-index of a sub-string match.
570
571        IntStream.Builder whereB = IntStream.builder();
572
573        // This is saving which match occurred
574        IntStream.Builder whichB = IntStream.builder();
575
576        // This is used to keep track of the size-change of the output string
577        int delta = 0;
578
579        // Main Loop: Builds a replacement StringBuilder.  Looks for matches between the input
580        // String 's', and the matchStrs in array String[] matchStrs.
581
582        TOP:
583        for (int i=0; i < s.length(); i++)
584
585            for (int j=0; j < matchStrs.length; j++)
586
587                if (s.regionMatches(ignoreCase, i, matchStrs[j], 0, matchStrs[j].length()))
588                {
589                    // SEE NOTE ABOVE: When a match is found, the starting-index of the 
590                    // substring match is appended to the IntStream, then IMMEDIATELY AFTERWARDS
591                    // the ending-index of the substring match is appended.
592
593                    int len = matchStrs[j].length();
594
595                    whereB.accept(i);
596                    whereB.accept(i + len);
597
598                    // The second IntStream shall store WHICH MATCH has occurred.  The match that
599                    // occurred is identified by an array-index into the "replaceChars" char[] array
600                    // that was provided by the user to this method through the parameter list at
601                    // the top of this.
602
603                    whichB.accept(j);
604
605                    // The change in size of the output String is precisely the length of the
606                    // match minus 1.  A substring is being replaced by a character.
607
608                    delta += (len - 1);
609
610                    // A Match was found, so skip to the next location.  Move past the match
611                    // that was just identified.
612                    i += (len -1);
613                    continue TOP;
614                }
615        
616        // Keeps track of all the locations in the original string where matches occurred.
617        int[] whereArr = whereB.build().toArray();
618        int[] whichArr = whichB.build().toArray();
619
620        // If there were no matches, return the original String
621        if (whichArr.length == 0) return s;
622
623        // The output string will be stored here.
624        char[] cArr = new char[s.length() - delta];
625
626        // These are the array indices of both arrays, and the original string
627        int i           = 0;    // Index into Match-String Location Pointer Array 
628        int oldStrPos   = 0;    // Pointer / index into Input-String
629        int newStrPos   = 0;    // Pointer / index into Output Char-Array
630        int rArrPos     = 0;    // Pointer / index into Replace Characters Char-Array
631
632        // Iterate and replace the substrings.
633        while (i < whereArr.length)
634        {
635            if (oldStrPos == whereArr[i])
636            {
637                // Retrieve the replacement character from the 'replaceChars' array.
638                // The *CORRECT INDEX* from the replacement-char array is the next location
639                // in the whichArr... And *THIS INDEX* is called 'rArrPos'
640                // NOTE: Good Variable Names became uncompromisingly difficult here. 
641
642                cArr[newStrPos++] = replaceChars[whichArr[rArrPos++]];
643
644                // Advance the "pointers" (advance the array indices)
645                //
646                // The pointer to the "source array" (a.k.a. the "original array") should be
647                // advanced to the location of the end of the match we just found.  That end
648                // was pointed at by the start-end *PAIR* whereArr[i] ... whereArr[i+1].
649                //
650                // NOTE: the substring match is "exclusive of" the actual character
651                //       located at whereArr[i+1].  Specifically, in the original string, there
652                //       was a sub-string match to replace with a single character that began
653                //       at original-string index/location whereArr[i] and continuing to 
654                //       index/location (whereArr[i+1]-1)
655                //
656                //       Java's java.lang.String.subtring(star, end) is *ALWAYs* exclusive of
657                //       the character located at 'end'
658        
659                oldStrPos = whereArr[i+1];
660
661                // Skip the "pair" (starting-index and ending-index).   Note that at the end
662                // of the loop, variable 'i' shall *ALWAYS* be an even-number.
663
664                i += 2;
665            }
666
667            else cArr[newStrPos++] = s.charAt(oldStrPos++);
668        }
669
670        // Just like in HTMLNode Parse, the trailing ("tail") of characters after the final
671        // match in the String need to be append to the output char[] array.  These are "missed"
672        // or "skipped" by the above replacement loop.  (Similar to a "trailing read")
673        //
674        // OLD CODE - REPLACED WITH LINE BELOW
675        // while (newStrPos < cArr.length) cArr[newStrPos++] = s.charAt(oldStrPos++);
676
677        s.getChars(oldStrPos, s.length(), cArr, newStrPos);
678
679        // Convert the character array into a String
680        return new String(cArr);
681    }
682
683    /**
684     * Convenience Method.
685     * <BR />Case-Sensitive
686     * <BR />Invokes: {@link #r(boolean, String, char[], String[])}
687     */
688    public static String r(String s, char[] matchChars, String[] replaceStrs)
689    { return r(false, s, matchChars, replaceStrs); }
690
691    /**
692     * This shall replace each instance of the characters of parameter {@code 'matchStrs'} in input
693     * {@code String 's'} with the {@code String's} of <I><B>parallel array</I></B>
694     * {@code 'replaceStrs'}.
695     *
696     * @param ignoreCase When this parameter is set to {@code TRUE}, then the comparisons that
697     * determine whether a match has occurred shall ignore the case of the characters involved.
698     * 
699     * @param s This may be any Java {@code String}.
700     * 
701     * @param matchChars This is a {@code char[] array} that should hold some set of characters
702     * which are expected to be contained within the input parameter {@code 's'}.  This method
703     * shall search {@code 's'} - <I>left to right</I> - for any instances of the list of
704     * {@code 'matchChars'} and replace those characters with whatever {@code String} is in the
705     * <I>same array-index location (parallel-array)</I> from input parameter
706     * {@code 'replaceStrs'}
707     * 
708     * <BR /><BR /><B STYLE="color: red;">MULTIPLE-MATCH SCENARIOS:</B> If there are multiple
709     * copies of an the <I>exact same character</I> in input parameter {@code 'matchChars'},
710     * this should be considered an error-case.  The code in this method does not actually go into
711     * that level of error checking, and as such, if parameter {@code 'matchChars'} attempts to
712     * map the same {@code char} to more than one replacement-{@code String}, the loop will
713     * simply use the first-mapping found in {@code 'replaceStrs'} that is found.  No exceptions
714     * will throw when presented with this type of input.
715     *
716     * <BR /><BR /><B>ALSO:</B> If an upper-case and lower-case version of the <I>exact same
717     * character</I> is provided in {@code char[] array} parameter {@code 'matchChars'}, and the
718     * {@code boolean flag} parameter {@code 'ignoreCase'} were set to {@code TRUE}, whichever of
719     * the two characters (upper-case or lower-case) that <I>occurs first in array parameter
720     * {@code 'matchChars'}</I> would be used to provide a replacement-{@code String} from array
721     * parameter {@code 'replaceStrs'}
722     * 
723     * @param replaceStrs This is a {@code String[] array} that should hold sub-strings.
724     * Every time a copy of any of the {@code 'matchChars'} is found within {@code 's'}, the
725     * index of the character match from {@code 'matchChars'} shall be used to lookup the parallel
726     * {@code 'replaceStr'}, and used to over-write or replace that character inside {@code 's'}.
727     * 
728     * <BR /><BR /><B STYLE="color: red;">PARALLEL ARRAY:</B> This array should be considered
729     * parallel to input {@code char[] array 'matchChars'}.  It provides a replacement mapping.
730     * It is required to be the exact same length as array {@code 'matchChars'}, or an exception
731     * shall throw.
732     * 
733     * @return This shall return a new-{@code String} where the replacements that were requested
734     * have been substituted.
735     * 
736     * @throws NullPointerException If any of the {@code String's} inside {@code String[]
737     * replaceStrs} are null.
738     * 
739     * @throws ParallelArrayException If the length of array {@code matchChars} does not equal
740     * the length of array {@code replaceStrs}, then this exception shall throw.   This is because
741     * these arrays are intended to be parallel arrays, where the references in the second array
742     * are supposed to be used to replace sub-strings (in {@code 's'}) from the first array.
743     */
744    public static String r
745        (boolean ignoreCase, String s, char[] matchChars, String[] replaceStrs)
746    {
747        // Make sure these arrays are Parallel, and throw ParallelArrayException if not
748        // If 'replaceStrs' contains null-values, throw NullPointerException
749
750        ParallelArrayException.check
751            (replaceStrs, "replaceStrs", true, matchChars, "matchChars");
752
753        // If the case of the characters is being ignored, it is easier to just set them all
754        // to lower-case right now.
755
756        if (ignoreCase)
757        {
758            matchChars = matchChars.clone();
759            for (int i=0; i < matchChars.length; i++)
760                matchChars[i] = Character.toLowerCase(matchChars[i]);
761        }
762    
763        // Java Stream's shall keep records of *WHERE* and *WHICH* the matches occurs
764        IntStream.Builder   whereB  = IntStream.builder();
765        IntStream.Builder   whichB  = IntStream.builder();
766        int                 delta   = 0; // string length change
767
768        // This part of the code finds the locations of all the matches in the input string.
769        // It does not build the new String, but rather, finds indexes first.  This way a
770        // char[] array can be built, and then populated with the updated sub-strings.
771
772        TOP:
773        for (int i=0; i < s.length(); i++)
774        {
775            char c = ignoreCase ? Character.toLowerCase(s.charAt(i)) : s.charAt(i);
776
777            for (int j=0; j < matchChars.length; j++)
778
779                if (c == matchChars[j])
780                {
781                    // Save the "original String index" of WHERE the match occurred
782                    whereB.accept(i);
783
784                    // Save the "match index" of WHICH char-match has occurred
785                    whichB.accept(j);
786
787                    // Keep a record of the 'delta' - and this is the size of the string to insert.
788                    delta += replaceStrs[j].length() - 1;
789                    
790                    continue TOP;
791                }
792        }
793
794        // List of indices into the input-String for WHERE matches occurred.
795        int[] whereArr = whereB.build().toArray();
796
797        // List of indices into the match-array for WHICH matches occurred.
798        int[] whichArr = whichB.build().toArray();
799
800        // IMPORTANT: If no matches in input char[] array 'matchChars' were found or identified
801        //            inside the input-string, return the original String immediately, with no
802        //            changes!
803
804        if (whereArr.length == 0) return s;
805
806        // The new "Char Array" which will be built into a String.  The "change in size" 
807        // was computed earlier
808
809        char[]  cArr        = new char[s.length() + delta];
810
811        // These are some loop-control variables
812        int oldStrPos   = 0;
813        int newStrPos   = 0;
814        int matchNum    = 0;
815
816
817        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
818        // "Pre-Loop Priming Update" or "PRIMING READ" - populates char array with replace-strings
819        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
820
821        // copies the first non-matching sub-string portion to the cArr[]
822        if (whereArr[0] > 0)
823        {
824            s.getChars(0, whereArr[0], cArr, 0);
825
826            // Advance the pointers
827            newStrPos = oldStrPos = whereArr[0]; 
828        }
829
830
831        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
832        // Iterate through each of the matches
833        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
834
835        while (matchNum < whichArr.length)
836        {
837            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
838            // Copy the next MATCHING char-substitute String from the "Replacement Strings Array"
839            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
840
841            String replaceStr = replaceStrs[whichArr[matchNum]];
842
843            /*
844                FROM: Java's JDK Documentation for java.lang.String
845
846                public void getChars(   int srcBegin,
847                                        int srcEnd,
848                                        char[] dst,
849                                        int dstBegin    )
850                            
851                Copies characters from this string into the destination character array.
852
853                Parameters:
854                    srcBegin    - index of the first character in the string to copy.
855                    srcEnd      - index after the last character in the string to copy.
856                    dst         - the destination array.
857                    dstBegin    - the start offset in the destination array.
858
859                // OLD CODE: 
860                int len = replaceStr.length();
861                for (int i=0; i < len; i++) cArr[newStrPos++] = replaceStr.charAt(i);
862
863            */
864            replaceStr.getChars(0, replaceStr.length(), cArr, newStrPos);
865
866            // In the new (output) string (currently a char[]), we have added "len" characters
867            newStrPos += replaceStr.length();
868
869            // Since we are replacing a *SINGLE* character with a new (replacement) String,
870            // the pointer to the source / old string is advanced by *ONLY* one.
871
872            oldStrPos++;
873
874            // This "index" is pointing to an array that is holding the match information.
875            // Essentially, here, we are just moving on to the next match.  Therefore
876            // increment by only 1.
877
878            matchNum++;
879
880
881            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
882            // Copy the next NON-MATCHING PORTION sub-string from the "Old Input String"
883            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
884
885            // Have We copied the entire string yet?
886            if (oldStrPos < s.length())
887            {
888                int endCopyPos = (matchNum < whichArr.length) ? whereArr[matchNum] : s.length();
889
890                s.getChars(oldStrPos, endCopyPos, cArr, newStrPos);
891
892                // We have just copied (end - oldStrPos) characters to the new array, from the
893                // old array.  Advance the newStrPos by this many characters.
894                newStrPos += (endCopyPos - oldStrPos);
895
896                // Advance the oldStr by the same number of character.  The line of code below
897                // will be identical to this line of code: oldStrpos += (endCopyPos - oldStrPos);
898                // Same as oldStrPos += (endCopyPos - oldStrPos) (Obviously!)
899                oldStrPos = endCopyPos; 
900            }
901        }
902
903        // Convert the char[] array (cArr) into a String, and return it.
904        return new String(cArr);
905    }
906
907    /**
908     * Convenience Method.
909     * <BR />Case-Sensitive
910     * <BR />Invokes: {@link #r(boolean, String, char[], IntCharFunction)}
911     */
912    public static String r
913        (String s, char[] matchChars, IntCharFunction<String> replaceFunction)
914    { return r(false, s, matchChars, replaceFunction); }
915
916    /**
917     * This shall replace each instance of the characters of parameter {@code 'matchStrs'} in input
918     * {@code String 's'} with the {@code String}-value returned by the {@code 'replaceFunction'}
919     * lambda-method / {@code functional-interface}.
920     *
921     * <DIV CLASS="EXAMPLE">{@code
922     * // THIS EXAMPLE SHOWS HOW THIS METHOD CAN BE USED WITH REGULAR-EXPRESSION PROCESSING.
923     *
924     * // These are (some / most) of the characters that would need to be 'escaped' to use
925     * // them for the actual characters they represent inside of a Regular-Expression Pattern.
926     * final char[] CHARS_TO_ESCAPE = { '*', '.', '[', ']', '(', ')', '+', '|', '?', ':' };
927     *
928     * // This method invocation uses a lambda-expression that simply "prepends" a forward
929     * // slash to whatever character is being replaced with a String.  This will "escape" any
930     * // punctuation in the text that needs to "bypass" the Regular-Expression Engine - meaning
931     * // that these symbols, when found inside the text, should not be interpreted as commands
932     * // to RegEx, but rather as plain old brackets, parenthesis, periods, etc...
933     * text = StrReplace.r(text, CHARS_TO_ESCAPE, (int i, char c) -> "\\" + c);
934     * }</DIV>
935     * 
936     * @param ignoreCase When this parameter is set to {@code TRUE}, then the comparisons that
937     * determine whether a match has occurred shall ignore the case of the characters involved.
938     * 
939     * @param s This may be any Java {@code String}.
940     * 
941     * @param matchChars This is a {@code char[] array} that should hold some set of characters
942     * which are expected to be contained within the input parameter {@code 's'}.  This method
943     * shall search {@code 's'} - <I>left to right</I> - for any instances of the list of
944     * {@code 'matchChars'} and replace those characters with the results from input 
945     * {@code functional-interface} parameter {@code 'replaceFunction'}.
946     * 
947     * @param replaceFunction This shall receive any Java {@code 'char'} along with the index into
948     * {@code String 's'} where that {@code 'char'} is located.  This function must reply with a
949     * replace-{@code String}.  This shall be used to replace any instances of that character
950     * found inside the input {@code String}.
951     * 
952     * <EMBED CLASS='external-html' DATA-IN=char DATA-OUT=String
953     *      DATA-FILE-ID=STR_REPLACE_LOOK_AR>
954     * 
955     * @return This shall return a new-{@code String} where the replacements that were requested
956     * have been substituted.
957     */
958    public static String r(
959            boolean ignoreCase, String s, char[] matchChars,
960            IntCharFunction<String> replaceFunction
961        )
962    {
963        // Use a StringBuilder.  It is easier since the 'Replace Function' is going to be
964        // *GENERATING* a new String each and every time there is a match.  This is essentially
965        // what class StringBuilder was deigned for.
966
967        StringBuilder sb = new StringBuilder();
968
969        // If the case of the characters is being ignored, it is easier to just set them all
970        // to lower-case right now.
971
972        if (ignoreCase)
973        {
974            matchChars = matchChars.clone();
975            for (int i=0; i < matchChars.length; i++)
976                matchChars[i] = Character.toLowerCase(matchChars[i]);
977        }
978
979        // IMPORTANT: This entire method is "The Easy Way"  Here, we are just reusing Java's
980        //            StringBuilder class to build the String, piece-by-piece.  It is
981        //            unknown whether this is less efficient than working with a char[] array
982
983        TOP:
984        for (int i=0; i < s.length(); i++)
985        {
986            char c = ignoreCase ? Character.toLowerCase(s.charAt(i)) : s.charAt(i);
987
988            for (int j=0; j < matchChars.length; j++)
989
990                if (c == matchChars[j])
991                { 
992                    sb.append(replaceFunction.apply(i, c));
993                    continue TOP;
994                }
995
996            sb.append(s.charAt(i));
997        }
998
999        return sb.toString();
1000    }
1001
1002    /**
1003     * Convenience Method.
1004     * <BR />Case-Sensitive
1005     * <BR />Invokes: {@link #r(boolean, String, char[], char[])}
1006     */
1007    public static String r(String s, char[] matchChars, char[] replaceChars)
1008    { return r(false, s, matchChars, replaceChars); }
1009
1010    /**
1011     * This shall replace any instance of any of the characters in array-parameter 
1012     * {@code 'matchChars'} with the character's provided in array-parameter
1013     * {@code 'replaceChars'}.
1014     * 
1015     * <DIV CLASS="EXAMPLE">{@code
1016     * // In this example, some of the Higher-Order UNICODE Punctuation Characters are replaced
1017     * // With simple ASCII-Versions of similar punctuation symbols.  Occasionally, foreign
1018     * // language news-sources will utilize these "Alternate Punctuation Symbols" in Asian
1019     * // Language Texts.  Translating these documents necessitates converting these to simple
1020     * // ASCII versions of the punctuation, for readability purposes.  (Since translated text
1021     * // in English wouldn't need to use these symbols).
1022     *
1023     * char[] unicodeChars = { '〔', '〕', '〈', '〉', '《', '》', '「', '」', '〖', '〗', '【', '】' };
1024     * char[] replacements = { '[',  ']',  '<', '>',  '\"', '\"', '[',  ']',  '{',  '}',  '<',  '>' };
1025     * String theString    = "会议强调,制定出台《中国共产党中央委员会工作条例》";
1026     * 
1027     * // Use this method to replace all instance of the mentioned UNICODE characters with 
1028     * // standard punctuation.  Note, after replacing the punctuation, translation would occur
1029     * // in the next step...
1030     * System.out.println(StrReplace.r(theString, unicodeChars, replacements));
1031     * 
1032     * // Prints:
1033     * // 会议强调,制定出台"中国共产党中央委员会工作条例"
1034     * // Which translates to:
1035     * // The meeting emphasized the formulation and promulgation of the "Regulations on the Work
1036     * // of the Central Committee of the Communist Party of China"
1037     * }</DIV>
1038     * 
1039     * @param ignoreCase When this parameter is set to {@code TRUE}, then the comparisons that
1040     * determine whether a match has occurred shall ignore the case of the characters involved.
1041     * 
1042     * @param s This may be any valid Java {@code String}.  It is expected to contain at least
1043     * some of the characters that are listed in parameter {@code 'matchChars'}.
1044     * 
1045     * @param matchChars This is a {@code char[] array} that should hold some set of characters
1046     * which are expected to be contained within the input parameter {@code 's'}.  This method
1047     * shall search {@code 's'} - <I>left to right</I> - for any instances of the list of
1048     * {@code 'matchChars'} and replace those characters with whatever {@code char} is in the
1049     * <I>same array-index location (parallel-array)</I> from input parameter
1050     * {@code 'replaceChars'}
1051     * 
1052     * <BR /><BR /><B STYLE="color: red;">MULTIPLE-MATCH SCENARIOS:</B> If there are multiple
1053     * copies of an the <I>exact same character</I> in input parameter {@code 'matchChars'},
1054     * this should be considered an error-case.  The code in this method does not actually go into
1055     * that level of error checking, and as such, if parameter {@code 'matchChars'} attempts to
1056     * map the same {@code char} to more than one replacement-{@code char}, the loop will
1057     * simply use the first-mapping found in {@code 'replaceStrs'} that is found.  No exceptions
1058     * will throw when presented with this type of input.
1059     *
1060     * @param replaceChars This is also a {@code char[] array}.  Every time a copy of any of the
1061     * {@code 'matchChars'} is found within {@code 's'}, the index of the character match from
1062     * {@code 'matchChars'} shall be used to lookup the parallel {@code 'replaceChar'}, and used
1063     * to over-write or replace that character inside {@code 's'}.
1064     * 
1065     * <BR /><BR /><B STYLE="color: red;">PARALLEL ARRAY:</B> This array should be considered
1066     * parallel to input {@code char[] array 'matchChars'}.  It provides a replacement mapping.
1067     * It is required to be the exact same length as array {@code 'matchChars'}, or an exception
1068     * shall throw.
1069     * 
1070     * @return This shall return a copy of the input {@code String}, with all characters that
1071     * matched the characters in {@code 'matchChars'}, <I>replaced by the characters in 
1072     * {@code 'replaceChars'}</I>.
1073     * 
1074     * @throws ParallelArrayException If the length of the {@code 'matchChars' array} is not
1075     * equal to the length of the {@code 'replaceChars'} array.
1076     */
1077    public static String r(boolean ignoreCase, String s, char[] matchChars, char[] replaceChars)
1078    {
1079        // Make sure these arrays are of equal length
1080        ParallelArrayException.check(matchChars, "matchChars", replaceChars, "replaceChars");
1081
1082        // The methods in this class all perform the replacements by first creating an
1083        // appropriately-sized output char[] array.  The last step of each of the methods is to
1084        // invoke the String constructor: new String(char[]) where a character array is converted
1085        // into a String.
1086
1087        char[] cArr = s.toCharArray();
1088
1089        if (ignoreCase)
1090        {
1091            char[] matchCharsLC = new char[matchChars.length];
1092
1093            for (int i=0; i < matchChars.length; i++)
1094                matchCharsLC[i] = Character.toLowerCase(matchChars[i]);
1095
1096            matchChars = matchCharsLC;
1097        }
1098
1099        if (ignoreCase)
1100
1101            TOP1:
1102            for (int i=0; i < cArr.length; i++)
1103            {
1104                char c = Character.toLowerCase(cArr[i]);
1105
1106                for (int j=0; j < matchChars.length; j++)
1107
1108                    if (c == matchChars[j])
1109                    {
1110                        cArr[i] = replaceChars[j];  // If a match was found, just replace it
1111                        continue TOP1;              // This method, really is THAT EASY.
1112                    }
1113            }
1114
1115        else
1116
1117            TOP2:
1118            for (int i=0; i < cArr.length; i++)
1119            {
1120                char c = cArr[i];
1121
1122                for (int j=0; j < matchChars.length; j++)
1123
1124                    if (c == matchChars[j])
1125                    {
1126                        cArr[i] = replaceChars[j];  // If a match was found, just replace it
1127                        continue TOP2;              // This method, really is THAT EASY.
1128                    }
1129            }
1130
1131        // Convert the character array into a String
1132        return new String(cArr);
1133    }
1134
1135    /**
1136     * Convenience Method.
1137     * <BR />Case-Sensitive
1138     * <BR />Invokes: {@link #r(boolean, String, char[], char)}
1139     */
1140    public static String r(String s, char[] matchChars, char prependChar)
1141    { return r(false, s, matchChars, prependChar); }
1142
1143    /**
1144     * This shall "prepend" a specified / chosen character before each instance of a list
1145     * of characters in an input-{@code String}.  {@code LAY-SPEAK:} If, for example, the
1146     * {@code 'prependChar'} provided were the back-slash character {@code '\'}, then this
1147     * method would insert a back-slash before each and every one of the {@code 'matchChars'}
1148     * that it found inside {@code 's'}.
1149     * 
1150     * <BR /><BR />This method is used to escape certain characters for things like regular
1151     * expressions and javascript.  Note the examples below.  These two methods are provided in
1152     * {@link StrSource}.  These methods are {@link StrSource#escStrForRegEx(String)},
1153     * {@link StrSource#escStrForJavaScript(String)}.
1154     * 
1155     * <DIV CLASS="EXAMPLE">{@code 
1156     * private static final char[] JS_ESCAPE_CHARS_ARR = { '\\', '/', '\n', '\"' };
1157     * 
1158     * // When using Java to build Java-Script "Strings", escape these characters
1159     * public static String escStrForJavaScript(String str)
1160     * { return StrReplace.r(str, JS_ESCAPE_CHARS_ARR, '\\'); }
1161     * 
1162     * // This is a list of "control characters" for regular-expressions.  These characters
1163     * // need to be escaped if they are expected to be taken literally, rather than as a control
1164     * // character in regex.
1165     * 
1166     * private static final char[] REGEX_ESCAPE_CHARS_ARR =
1167     * { '\\', '/', '(', ')', '[', ']', '{', '}', '$', '^', '+', '*', '?', '-', '.' };
1168     * 
1169     * public static String escStrForRegEx(String str)
1170     * { return StrReplace.r(str, REGEX_ESCAPE_CHARS_ARR, '\\'); }
1171     * }</DIV>
1172     * 
1173     * @param ignoreCase When this parameter is set to {@code TRUE}, then the comparisons that
1174     * determine whether a match has occurred shall ignore the case of the characters involved.
1175     *
1176     * @param s This may be any valid Java {@code String}.  It is expected to contain at least
1177     * some of the characters that are listed in parameter {@code 'matchChars'}.
1178     * 
1179     * @param matchChars This is a {@code char[] array} that should hold some set of characters
1180     * which are expected to be contained within the input parameter {@code 's'}.  This method
1181     * shall search {@code 's'} - <I>left to right</I> - for any instances of the list of
1182     * {@code 'matchChars'} and insert the character {@code 'prependChar'} directly before each
1183     * match-character identified in {@code String}-parameter {@code 's'}.
1184     * 
1185     * @param prependChar This character will be inserted directly before each instance of 
1186     * {@code matcChars}-characters that are found within input {@code String}-parameter
1187     * {@code 's'}
1188     * 
1189     * @return This shall return a new {@code String} with the {@code 'prependChar'} before each
1190     * instance of one of the {@code 'matchChars'} identified in the original {@code String 's'}.
1191     */
1192    public static String r(boolean ignoreCase, String s, char[] matchChars, char prependChar)
1193    {
1194        // Need a temporary 'count' variable
1195        int count = 0;
1196
1197        // Improve the loop counter efficiency, use a 'len' instead of s.length()
1198        int len = s.length();
1199
1200        if (ignoreCase)
1201        {
1202            char[] matchCharsLC = new char[matchChars.length];
1203
1204            for (int i=0; i < matchChars.length; i++)
1205                matchCharsLC[i] = Character.toLowerCase(matchChars[i]);
1206
1207            matchChars = matchCharsLC;
1208        }
1209
1210        // Use a Java Stream to save the locations of the matches
1211        IntStream.Builder whereB = IntStream.builder();
1212
1213        // Count how many escape-characters are in the input-string
1214        if (ignoreCase)
1215
1216            TOP1:
1217            for (int i=0; i < len; i++)
1218            {
1219                char c = Character.toLowerCase(s.charAt(i));
1220
1221                // This checks if the character is a match with any of the match-characters in
1222                // the input array.
1223
1224                for (char matchChar : matchChars)
1225
1226                    if (matchChar == c)
1227                    {
1228                        whereB.accept(i);
1229                        continue TOP1;
1230                    }
1231            }
1232
1233        else
1234
1235            TOP2:
1236            for (int i=0; i < len; i++)
1237            {
1238                char c = s.charAt(i);
1239
1240                // This checks if the character is a match with any of the match-characters in
1241                // the input array.
1242
1243                for (char matchChar : matchChars)
1244
1245                    if (matchChar == c)
1246                    {
1247                        whereB.accept(i);
1248                        continue TOP2;
1249                    }
1250            }
1251
1252        // Build the java stream, and turn it into an index-pointer array (int array of
1253        // array indices)
1254
1255        int[] whereArr = whereB.build().toArray();
1256    
1257        // No matches, return original string.
1258        if (whereArr.length == 0) return s;
1259
1260        // The 'escaped string' will be longer than the original String by
1261        // 'whereArr.length' characters.  Use a character array to build this string
1262
1263        char[] cArr = new char[len + whereArr.length];
1264
1265        // There are now three different indices to keep up with - when doing this replace.
1266        // The input-string index.  The index of the "Match Locations Array" (whereArr), and the
1267        // index into the returned String - which, for now, is represented as a char[] array.
1268
1269        int oldStrPos   = 0;
1270        int newStrPos   = 0;
1271        int i           = 0;
1272
1273        while (oldStrPos < len)
1274        {
1275            // The next character in the input string
1276            char c = s.charAt(oldStrPos);
1277
1278            // Was this one of the matches - as computed in the earlier loop?
1279            if (oldStrPos == whereArr[i])
1280            {
1281                // if "YES", then insert the prependChar, AND the original-string-char
1282                cArr[newStrPos++] = prependChar;
1283                cArr[newStrPos++] = c;
1284
1285                // Here, increment the "Match Locations Array" index-location too.
1286                // (We have just "used up" one of the match-locations.
1287                i++;
1288            }
1289            else
1290                // if "NO", then just insert the original-string-char
1291                cArr[newStrPos++] = c;
1292
1293            // Only at the end should we increment the input-string index-location.
1294            oldStrPos++;
1295        }
1296
1297        // Convert the char array that was built into a String, and return it.
1298        return String.valueOf(cArr);
1299    }
1300
1301    /**
1302     * Convenience Method.
1303     * <BR />Invokes: {@link #r(String, boolean, char[], ToCharIntCharFunc)}
1304     */
1305    public static String r(char[] matchChars, ToCharIntCharFunc replaceFunction, String s)
1306    { return r(s, false, matchChars, replaceFunction); }
1307
1308    /**
1309     * This method shall receive a list of {@code 'char'}, and then search the input
1310     * {@code String} parameter {@code 's'} for any instances of the characters listed in
1311     * {@code 'matchChars'} - and replace them.  The replacement characters must be provided
1312     * by the Functional-Interface Parameter {@code 'replaceFunction'}.
1313     * 
1314     * <BR /><BR />The character-equality comparisons may be done in a case-insensitive manner,
1315     * if requested (using the {@code 'ignoreCase'} parameter).
1316     * 
1317     * @param s This may be any valid Java {@code String}.  It is expected to contain at least
1318     * some of the characters that are listed in parameter {@code 'matchChars'}.
1319     * 
1320     * @param ignoreCase If this parameter receives {@code TRUE}, then the equality comparisons
1321     * between the input {@code String 's'}, and {@code 'matchChars'} will be done on a case
1322     * insensitive basis.
1323     * 
1324     * @param matchChars This is a {@code char[] array} that should hold some set of characters
1325     * which are expected to be contained insiide the input parameter {@code 's'}.  This method
1326     * shall search {@code 's'} - <I>left to right</I> - for any instances of the list of
1327     * {@code 'matchChars'} and replace those characters with ones returned by
1328     * {@code 'replaceFunction.apply(i, c);'}.  Note that, here, {@code 'i'} is the
1329     * {@code String}-index where the {@code 'matchChar'} was found, and {@code 'c'} is the 
1330     * character that was matched.
1331     * 
1332     * @param replaceFunction This shall receive any Java {@code 'char'} along with the index into
1333     * {@code String 's'} where that {@code 'char'} is located.  This function must reply with a
1334     * replace-{@code char}.  This shall be used to replace instances of that character
1335     * found inside the input {@code String}. 
1336     * 
1337     * <EMBED CLASS='external-html' DATA-IN=char DATA-OUT=char
1338     *      DATA-FILE-ID=STR_REPLACE_LOOK_AR>
1339     * 
1340     * @return A new {@code String} where any and all characters that were listed in 
1341     * {@code 'matchChars'} have been replaced by the return-values of {@code 'replaceFunction'}.
1342     */
1343    public static String r(
1344        String s, boolean ignoreCase, char[] matchChars,
1345        ToCharIntCharFunc replaceFunction
1346    )
1347    {
1348        char[] cArr = s.toCharArray();
1349
1350        if (ignoreCase)
1351        {
1352            // Make sure the 'var-args' are not changed.  We must resort to copying
1353            // the input 'matchChars' array.
1354
1355            char[] tempArr = matchChars;
1356            matchChars = new char[tempArr.length];
1357
1358            // Set them to lower-case, as they are copied.
1359            for (int i=0; i < matchChars.length; i++)
1360                matchChars[i] = Character.toLowerCase(tempArr[i]);
1361
1362            // Do the replacement on a case-insensitive basis
1363            for (int i=0; i < cArr.length; i++)
1364            {
1365                char c = Character.toLowerCase(cArr[i]);
1366
1367                for (int j=0; j < matchChars.length; j++)
1368
1369                    if (c == matchChars[j])
1370                    {
1371                        cArr[i] = replaceFunction.apply(i, cArr[i]);
1372                        break;
1373                    }
1374            }
1375        }
1376        else
1377
1378            // Do the replacement, case-sensitive.
1379            for (int i=0; i < cArr.length; i++)
1380                for (int j=0; j < matchChars.length; j++)
1381                    if (cArr[i] == matchChars[j])
1382                    {
1383                        cArr[i] = replaceFunction.apply(i, cArr[i]);
1384                        break;
1385                    }
1386
1387        return new String(cArr);
1388    }
1389}