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 = { "Π", "Ρ", "Σ", "Τ", "Υ", "Φ" }; 320 * String theString = "Greek: Π, Ρ, Σ, Τ, Υ, Φ"; 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 = { "Π", "Ρ", "Σ", "Τ", "Υ", "Φ" }; 506 * char[] replacements = { 'Π', 'Ρ', 'Σ', 'Τ', 'Υ', 'Φ' }; 507 * String theString = "Greek Letters: Π, Ρ, Σ, Τ, Υ, Φ"; 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}