1 // © 2017 and later: Unicode, Inc. and others. 2 // License & terms of use: http://www.unicode.org/copyright.html 3 package com.ibm.icu.impl; 4 5 import java.util.Arrays; 6 import java.util.HashMap; 7 import java.util.Map; 8 9 // NumberFormat is imported only for the toDebugString() implementation. 10 import com.ibm.icu.text.NumberFormat; 11 12 /** 13 * A StringBuilder optimized for formatting. It implements the following key features beyond a 14 * normal JDK StringBuilder: 15 * 16 * <ol> 17 * <li>Efficient prepend as well as append. 18 * <li>Keeps tracks of Fields in an efficient manner. 19 * <li>String operations are fast-pathed to code point operations when possible. 20 * </ol> 21 * 22 * See also FormattedValueStringBuilderImpl. 23 * 24 * @author sffc (Shane Carr) 25 */ 26 public class FormattedStringBuilder implements CharSequence, Appendable { 27 28 public static interface FieldWrapper { unwrap()29 java.text.Format.Field unwrap(); 30 } 31 unwrapField(Object field)32 public static java.text.Format.Field unwrapField(Object field) { 33 if (field == null) { 34 return null; 35 } else if (field instanceof FieldWrapper) { 36 return ((FieldWrapper) field).unwrap(); 37 } else if (field instanceof java.text.Format.Field) { 38 return (java.text.Format.Field) field; 39 } else { 40 throw new AssertionError("Not a field: " + field); 41 } 42 } 43 44 /** A constant, empty FormattedStringBuilder. Do NOT call mutative operations on this. */ 45 public static final FormattedStringBuilder EMPTY = new FormattedStringBuilder(); 46 47 char[] chars; 48 Object[] fields; 49 int zero; 50 int length; 51 52 /** Number of characters from the end where .append() operations insert. */ 53 int appendOffset = 0; 54 55 /** Field applied when Appendable methods are used. */ 56 Object appendableField = null; 57 FormattedStringBuilder()58 public FormattedStringBuilder() { 59 this(40); 60 } 61 FormattedStringBuilder(int capacity)62 public FormattedStringBuilder(int capacity) { 63 chars = new char[capacity]; 64 fields = new Object[capacity]; 65 zero = capacity / 2; 66 length = 0; 67 } 68 FormattedStringBuilder(FormattedStringBuilder source)69 public FormattedStringBuilder(FormattedStringBuilder source) { 70 copyFrom(source); 71 } 72 copyFrom(FormattedStringBuilder source)73 public void copyFrom(FormattedStringBuilder source) { 74 chars = Arrays.copyOf(source.chars, source.chars.length); 75 fields = Arrays.copyOf(source.fields, source.fields.length); 76 zero = source.zero; 77 length = source.length; 78 } 79 80 @Override length()81 public int length() { 82 return length; 83 } 84 codePointCount()85 public int codePointCount() { 86 return Character.codePointCount(this, 0, length()); 87 } 88 89 @Override charAt(int index)90 public char charAt(int index) { 91 assert index >= 0; 92 assert index < length; 93 return chars[zero + index]; 94 } 95 96 public Object fieldAt(int index) { 97 assert index >= 0; 98 assert index < length; 99 return fields[zero + index]; 100 } 101 102 public int getFirstCodePoint() { 103 if (length == 0) { 104 return -1; 105 } 106 return Character.codePointAt(chars, zero, zero + length); 107 } 108 109 public int getLastCodePoint() { 110 if (length == 0) { 111 return -1; 112 } 113 return Character.codePointBefore(chars, zero + length, zero); 114 } 115 116 public int codePointAt(int index) { 117 return Character.codePointAt(chars, zero + index, zero + length); 118 } 119 120 public int codePointBefore(int index) { 121 return Character.codePointBefore(chars, zero + index, zero); 122 } 123 124 public FormattedStringBuilder clear() { 125 zero = getCapacity() / 2; 126 length = 0; 127 return this; 128 } 129 130 /** 131 * Sets the index at which append operations insert. Defaults to the end. 132 * 133 * @param index The index at which append operations should insert. 134 */ 135 public void setAppendIndex(int index) { 136 appendOffset = length - index; 137 } 138 139 public int appendChar16(char codeUnit, Object field) { 140 return insertChar16(length - appendOffset, codeUnit, field); 141 } 142 143 public int insertChar16(int index, char codeUnit, Object field) { 144 int count = 1; 145 int position = prepareForInsert(index, count); 146 chars[position] = codeUnit; 147 fields[position] = field; 148 return count; 149 } 150 151 /** 152 * Appends the specified codePoint to the end of the string. 153 * 154 * @return The number of chars added: 1 if the code point is in the BMP, or 2 otherwise. 155 */ 156 public int appendCodePoint(int codePoint, Object field) { 157 return insertCodePoint(length - appendOffset, codePoint, field); 158 } 159 160 /** 161 * Inserts the specified codePoint at the specified index in the string. 162 * 163 * @return The number of chars added: 1 if the code point is in the BMP, or 2 otherwise. 164 */ 165 public int insertCodePoint(int index, int codePoint, Object field) { 166 int count = Character.charCount(codePoint); 167 int position = prepareForInsert(index, count); 168 Character.toChars(codePoint, chars, position); 169 fields[position] = field; 170 if (count == 2) 171 fields[position + 1] = field; 172 return count; 173 } 174 175 /** 176 * Appends the specified CharSequence to the end of the string. 177 * 178 * @return The number of chars added, which is the length of CharSequence. 179 */ 180 public int append(CharSequence sequence, Object field) { 181 return insert(length - appendOffset, sequence, field); 182 } 183 184 /** 185 * Inserts the specified CharSequence at the specified index in the string. 186 * 187 * @return The number of chars added, which is the length of CharSequence. 188 */ 189 public int insert(int index, CharSequence sequence, Object field) { 190 if (sequence.length() == 0) { 191 // Nothing to insert. 192 return 0; 193 } else if (sequence.length() == 1) { 194 // Fast path: on a single-char string, using insertCodePoint below is 70% faster than the 195 // CharSequence method: 12.2 ns versus 41.9 ns for five operations on my Linux x86-64. 196 return insertCodePoint(index, sequence.charAt(0), field); 197 } else { 198 return insert(index, sequence, 0, sequence.length(), field); 199 } 200 } 201 202 /** 203 * Inserts the specified CharSequence at the specified index in the string, reading from the 204 * CharSequence from start (inclusive) to end (exclusive). 205 * 206 * @return The number of chars added, which is the length of CharSequence. 207 */ 208 public int insert(int index, CharSequence sequence, int start, int end, Object field) { 209 int count = end - start; 210 int position = prepareForInsert(index, count); 211 for (int i = 0; i < count; i++) { 212 chars[position + i] = sequence.charAt(start + i); 213 fields[position + i] = field; 214 } 215 return count; 216 } 217 218 /** 219 * Replaces the chars between startThis and endThis with the chars between startOther and endOther of 220 * the given CharSequence. Calling this method with startThis == endThis is equivalent to calling 221 * insert. 222 * 223 * @return The number of chars added, which may be negative if the removed segment is longer than the 224 * length of the CharSequence segment that was inserted. 225 */ 226 public int splice( 227 int startThis, 228 int endThis, 229 CharSequence sequence, 230 int startOther, 231 int endOther, 232 Object field) { 233 int thisLength = endThis - startThis; 234 int otherLength = endOther - startOther; 235 int count = otherLength - thisLength; 236 int position; 237 if (count > 0) { 238 // Overall, chars need to be added. 239 position = prepareForInsert(startThis, count); 240 } else { 241 // Overall, chars need to be removed or kept the same. 242 position = remove(startThis, -count); 243 } 244 for (int i = 0; i < otherLength; i++) { 245 chars[position + i] = sequence.charAt(startOther + i); 246 fields[position + i] = field; 247 } 248 return count; 249 } 250 251 /** 252 * Appends the chars in the specified char array to the end of the string, and associates them with 253 * the fields in the specified field array, which must have the same length as chars. 254 * 255 * @return The number of chars added, which is the length of the char array. 256 */ 257 public int append(char[] chars, Object[] fields) { 258 return insert(length - appendOffset, chars, fields); 259 } 260 261 /** 262 * Inserts the chars in the specified char array at the specified index in the string, and associates 263 * them with the fields in the specified field array, which must have the same length as chars. 264 * 265 * @return The number of chars added, which is the length of the char array. 266 */ 267 public int insert(int index, char[] chars, Object[] fields) { 268 assert fields == null || chars.length == fields.length; 269 int count = chars.length; 270 if (count == 0) 271 return 0; // nothing to insert 272 int position = prepareForInsert(index, count); 273 for (int i = 0; i < count; i++) { 274 this.chars[position + i] = chars[i]; 275 this.fields[position + i] = fields == null ? null : fields[i]; 276 } 277 return count; 278 } 279 280 /** 281 * Appends the contents of another {@link FormattedStringBuilder} to the end of this instance. 282 * 283 * @return The number of chars added, which is the length of the other {@link FormattedStringBuilder}. 284 */ 285 public int append(FormattedStringBuilder other) { 286 return insert(length - appendOffset, other); 287 } 288 289 /** 290 * Inserts the contents of another {@link FormattedStringBuilder} into this instance at the given index. 291 * 292 * @return The number of chars added, which is the length of the other {@link FormattedStringBuilder}. 293 */ 294 public int insert(int index, FormattedStringBuilder other) { 295 if (this == other) { 296 throw new IllegalArgumentException("Cannot call insert/append on myself"); 297 } 298 int count = other.length; 299 if (count == 0) { 300 // Nothing to insert. 301 return 0; 302 } 303 int position = prepareForInsert(index, count); 304 for (int i = 0; i < count; i++) { 305 this.chars[position + i] = other.charAt(i); 306 this.fields[position + i] = other.fieldAt(i); 307 } 308 return count; 309 } 310 311 /** 312 * Shifts around existing data if necessary to make room for new characters. 313 * 314 * @param index 315 * The location in the string where the operation is to take place. 316 * @param count 317 * The number of chars (UTF-16 code units) to be inserted at that location. 318 * @return The position in the char array to insert the chars. 319 */ 320 private int prepareForInsert(int index, int count) { 321 if (index == -1) { 322 index = length; 323 } 324 if (index == 0 && zero - count >= 0) { 325 // Append to start 326 zero -= count; 327 length += count; 328 return zero; 329 } else if (index == length && zero + length + count < getCapacity()) { 330 // Append to end 331 length += count; 332 return zero + length - count; 333 } else { 334 // Move chars around and/or allocate more space 335 return prepareForInsertHelper(index, count); 336 } 337 } 338 339 private int prepareForInsertHelper(int index, int count) { 340 // Java note: Keeping this code out of prepareForInsert() increases the speed of append 341 // operations. 342 int oldCapacity = getCapacity(); 343 int oldZero = zero; 344 char[] oldChars = chars; 345 Object[] oldFields = fields; 346 if (length + count > oldCapacity) { 347 int newCapacity = (length + count) * 2; 348 int newZero = newCapacity / 2 - (length + count) / 2; 349 350 char[] newChars = new char[newCapacity]; 351 Object[] newFields = new Object[newCapacity]; 352 353 // First copy the prefix and then the suffix, leaving room for the new chars that the 354 // caller wants to insert. 355 System.arraycopy(oldChars, oldZero, newChars, newZero, index); 356 System.arraycopy(oldChars, 357 oldZero + index, 358 newChars, 359 newZero + index + count, 360 length - index); 361 System.arraycopy(oldFields, oldZero, newFields, newZero, index); 362 System.arraycopy(oldFields, 363 oldZero + index, 364 newFields, 365 newZero + index + count, 366 length - index); 367 368 chars = newChars; 369 fields = newFields; 370 zero = newZero; 371 length += count; 372 } else { 373 int newZero = oldCapacity / 2 - (length + count) / 2; 374 375 // First copy the entire string to the location of the prefix, and then move the suffix 376 // to make room for the new chars that the caller wants to insert. 377 System.arraycopy(oldChars, oldZero, oldChars, newZero, length); 378 System.arraycopy(oldChars, 379 newZero + index, 380 oldChars, 381 newZero + index + count, 382 length - index); 383 System.arraycopy(oldFields, oldZero, oldFields, newZero, length); 384 System.arraycopy(oldFields, 385 newZero + index, 386 oldFields, 387 newZero + index + count, 388 length - index); 389 390 zero = newZero; 391 length += count; 392 } 393 return zero + index; 394 } 395 396 /** 397 * Removes the "count" chars starting at "index". Returns the position at which the chars were 398 * removed. 399 */ 400 private int remove(int index, int count) { 401 int position = index + zero; 402 System.arraycopy(chars, position + count, chars, position, length - index - count); 403 System.arraycopy(fields, position + count, fields, position, length - index - count); 404 length -= count; 405 return position; 406 } 407 408 private int getCapacity() { 409 return chars.length; 410 } 411 412 /** Note: this returns a FormattedStringBuilder. Do not return publicly. */ 413 @Override 414 @Deprecated 415 public CharSequence subSequence(int start, int end) { 416 assert start >= 0; 417 assert end <= length; 418 assert end >= start; 419 FormattedStringBuilder other = new FormattedStringBuilder(this); 420 other.zero = zero + start; 421 other.length = end - start; 422 return other; 423 } 424 425 /** Use this instead of subSequence if returning publicly. */ 426 public String subString(int start, int end) { 427 if (start < 0 || end > length || end < start) { 428 throw new IndexOutOfBoundsException(); 429 } 430 return new String(chars, start + zero, end - start); 431 } 432 433 /** 434 * Returns the string represented by the characters in this string builder. 435 * 436 * <p> 437 * For a string intended be used for debugging, use {@link #toDebugString}. 438 */ 439 @Override 440 public String toString() { 441 return new String(chars, zero, length); 442 } 443 444 private static final Map<Object, Character> fieldToDebugChar = new HashMap<>(); 445 446 static { 447 fieldToDebugChar.put(NumberFormat.Field.SIGN, '-'); 448 fieldToDebugChar.put(NumberFormat.Field.INTEGER, 'i'); 449 fieldToDebugChar.put(NumberFormat.Field.FRACTION, 'f'); 450 fieldToDebugChar.put(NumberFormat.Field.EXPONENT, 'e'); 451 fieldToDebugChar.put(NumberFormat.Field.EXPONENT_SIGN, '+'); 452 fieldToDebugChar.put(NumberFormat.Field.EXPONENT_SYMBOL, 'E'); 453 fieldToDebugChar.put(NumberFormat.Field.DECIMAL_SEPARATOR, '.'); 454 fieldToDebugChar.put(NumberFormat.Field.GROUPING_SEPARATOR, ','); 455 fieldToDebugChar.put(NumberFormat.Field.PERCENT, '%'); 456 fieldToDebugChar.put(NumberFormat.Field.PERMILLE, '‰'); 457 fieldToDebugChar.put(NumberFormat.Field.CURRENCY, '$'); 458 fieldToDebugChar.put(NumberFormat.Field.MEASURE_UNIT, 'u'); 459 fieldToDebugChar.put(NumberFormat.Field.COMPACT, 'C'); 460 } 461 462 /** 463 * Returns a string that includes field information, for debugging purposes. 464 * 465 * <p> 466 * For example, if the string is "-12.345", the debug string will be something like 467 * "<FormattedStringBuilder [-123.45] [-iii.ff]>" 468 * 469 * @return A string for debugging purposes. 470 */ 471 public String toDebugString() { 472 StringBuilder sb = new StringBuilder(); 473 sb.append("<FormattedStringBuilder ["); 474 sb.append(this.toString()); 475 sb.append("] ["); 476 for (int i = zero; i < zero + length; i++) { 477 if (fields[i] == null) { 478 sb.append('n'); 479 } else if (fieldToDebugChar.containsKey(fields[i])) { 480 sb.append(fieldToDebugChar.get(fields[i])); 481 } else { 482 sb.append('?'); 483 } 484 } 485 sb.append("]>"); 486 return sb.toString(); 487 } 488 489 /** @return A new array containing the contents of this string builder. */ 490 public char[] toCharArray() { 491 return Arrays.copyOfRange(chars, zero, zero + length); 492 } 493 494 /** @return A new array containing the field values of this string builder. */ 495 public Object[] toFieldArray() { 496 return Arrays.copyOfRange(fields, zero, zero + length); 497 } 498 499 /** 500 * Call this method before using any of the Appendable overrides. 501 * 502 * @param field The field used when inserting strings. 503 */ 504 public void setAppendableField(Object field) { 505 appendableField = field; 506 } 507 508 /** 509 * This method is provided for Java Appendable compatibility. In most cases, please use the append methods that take 510 * a Field parameter. If you do use this method, you must call {@link #setAppendableField} first. 511 */ 512 @Override 513 public Appendable append(CharSequence csq) { 514 assert appendableField != null; 515 insert(length - appendOffset, csq, appendableField); 516 return this; 517 } 518 519 /** 520 * This method is provided for Java Appendable compatibility. In most cases, please use the append methods that take 521 * a Field parameter. If you do use this method, you must call {@link #setAppendableField} first. 522 */ 523 @Override 524 public Appendable append(CharSequence csq, int start, int end) { 525 assert appendableField != null; 526 insert(length - appendOffset, csq, start, end, appendableField); 527 return this; 528 } 529 530 /** 531 * This method is provided for Java Appendable compatibility. In most cases, please use the append methods that take 532 * a Field parameter. If you do use this method, you must call {@link #setAppendableField} first. 533 */ 534 @Override 535 public Appendable append(char c) { 536 assert appendableField != null; 537 insertChar16(length - appendOffset, c, appendableField); 538 return this; 539 } 540 541 /** 542 * @return Whether the contents and field values of this string builder are equal to the given chars 543 * and fields. 544 * @see #toCharArray 545 * @see #toFieldArray 546 */ 547 public boolean contentEquals(char[] chars, Object[] fields) { 548 if (chars.length != length) 549 return false; 550 if (fields.length != length) 551 return false; 552 for (int i = 0; i < length; i++) { 553 if (this.chars[zero + i] != chars[i]) { 554 return false; 555 } 556 if (unwrapField(this.fields[zero + i]) != unwrapField(fields[i])) { 557 return false; 558 } 559 } 560 return true; 561 } 562 563 /** 564 * @param other 565 * The instance to compare. 566 * @return Whether the contents of this instance is currently equal to the given instance. 567 */ 568 public boolean contentEquals(FormattedStringBuilder other) { 569 if (length != other.length) 570 return false; 571 for (int i = 0; i < length; i++) { 572 if (charAt(i) != other.charAt(i)) { 573 return false; 574 } 575 if (unwrapField(fieldAt(i)) != unwrapField(other.fieldAt(i))) { 576 return false; 577 } 578 } 579 return true; 580 } 581 582 @Override 583 public int hashCode() { 584 throw new UnsupportedOperationException("Don't call #hashCode() or #equals() on a mutable."); 585 } 586 587 @Override 588 public boolean equals(Object other) { 589 throw new UnsupportedOperationException("Don't call #hashCode() or #equals() on a mutable."); 590 } 591 } 592