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