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      * "&lt;NumberStringBuilder [-123.45] [-iii.ff]&gt;"
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