• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      * "&lt;FormattedStringBuilder [-123.45] [-iii.ff]&gt;"
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