• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // © 2019 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.text.AttributedCharacterIterator;
6 import java.text.AttributedString;
7 import java.text.FieldPosition;
8 import java.text.Format.Field;
9 
10 import com.ibm.icu.text.ConstrainedFieldPosition;
11 import com.ibm.icu.text.ListFormatter;
12 import com.ibm.icu.text.NumberFormat;
13 import com.ibm.icu.text.UFormat;
14 import com.ibm.icu.text.UnicodeSet;
15 
16 /**
17  * Implementation of FormattedValue based on FormattedStringBuilder.
18  *
19  * The implementation currently revolves around numbers and number fields.
20  * However, it can be generalized in the future when there is a need.
21  *
22  * In C++, this implements FormattedValue. In Java, it is a stateless
23  * collection of static functions to avoid having to use nested objects.
24  *
25  * @author sffc (Shane Carr)
26  */
27 public class FormattedValueStringBuilderImpl {
28 
29     /**
30      * Placeholder field used for calculating spans.
31      * Does not currently support nested fields beyond one level.
32      */
33     public static class SpanFieldPlaceholder implements FormattedStringBuilder.FieldWrapper {
34         public UFormat.SpanField spanField;
35         public Field normalField;
36         public Object value;
37         public int start;
38         public int length;
39 
unwrap()40         public Field unwrap() {
41             return normalField;
42         }
43     }
44 
45     /**
46      * Finds the index at which a span field begins.
47      *
48      * @param value The value of the span field to search for.
49      * @return The index, or -1 if not found.
50      */
findSpan(FormattedStringBuilder self, Object value)51     public static int findSpan(FormattedStringBuilder self, Object value) {
52         for (int i = self.zero; i < self.zero + self.length; i++) {
53             if (!(self.fields[i] instanceof SpanFieldPlaceholder)) {
54                 continue;
55             }
56             if (((SpanFieldPlaceholder) self.fields[i]).value.equals(value)) {
57                 return i - self.zero;
58             }
59         }
60         return -1;
61     }
62 
63     /**
64      * Upgrade a range of a string to a span field.
65      *
66      * Similar to appendSpanInfo in ICU4C.
67      */
applySpanRange( FormattedStringBuilder self, UFormat.SpanField spanField, Object value, int start, int end)68     public static void applySpanRange(
69             FormattedStringBuilder self,
70             UFormat.SpanField spanField,
71             Object value,
72             int start,
73             int end) {
74         for (int i = start + self.zero; i < end + self.zero; i++) {
75             Object oldField = self.fields[i];
76             SpanFieldPlaceholder newField = new SpanFieldPlaceholder();
77             newField.spanField = spanField;
78             newField.normalField = (java.text.Format.Field) oldField;
79             newField.value = value;
80             newField.start = start;
81             newField.length = end - start;
82             self.fields[i] = newField;
83         }
84     }
85 
nextFieldPosition(FormattedStringBuilder self, FieldPosition fp)86     public static boolean nextFieldPosition(FormattedStringBuilder self, FieldPosition fp) {
87         java.text.Format.Field rawField = fp.getFieldAttribute();
88 
89         if (rawField == null) {
90             // Backwards compatibility: read from fp.getField()
91             if (fp.getField() == NumberFormat.INTEGER_FIELD) {
92                 rawField = NumberFormat.Field.INTEGER;
93             } else if (fp.getField() == NumberFormat.FRACTION_FIELD) {
94                 rawField = NumberFormat.Field.FRACTION;
95             } else {
96                 // No field is set
97                 return false;
98             }
99         }
100 
101         if (!(rawField instanceof NumberFormat.Field)) {
102             throw new IllegalArgumentException(
103                     "You must pass an instance of com.ibm.icu.text.NumberFormat.Field as your FieldPosition attribute.  You passed: "
104                             + rawField.getClass().toString());
105         }
106 
107         ConstrainedFieldPosition cfpos = new ConstrainedFieldPosition();
108         cfpos.constrainField(rawField);
109         cfpos.setState(rawField, null, fp.getBeginIndex(), fp.getEndIndex());
110         if (nextPosition(self, cfpos, null)) {
111             fp.setBeginIndex(cfpos.getStart());
112             fp.setEndIndex(cfpos.getLimit());
113             return true;
114         }
115 
116         // Special case: fraction should start after integer if fraction is not present
117         if (rawField == NumberFormat.Field.FRACTION && fp.getEndIndex() == 0) {
118             boolean inside = false;
119             int i = self.zero;
120             for (; i < self.zero + self.length; i++) {
121                 if (isIntOrGroup(self.fields[i]) || self.fields[i] == NumberFormat.Field.DECIMAL_SEPARATOR) {
122                     inside = true;
123                 } else if (inside) {
124                     break;
125                 }
126             }
127             fp.setBeginIndex(i - self.zero);
128             fp.setEndIndex(i - self.zero);
129         }
130 
131         return false;
132     }
133 
toCharacterIterator(FormattedStringBuilder self, Field numericField)134     public static AttributedCharacterIterator toCharacterIterator(FormattedStringBuilder self, Field numericField) {
135         ConstrainedFieldPosition cfpos = new ConstrainedFieldPosition();
136         AttributedString as = new AttributedString(self.toString());
137         while (nextPosition(self, cfpos, numericField)) {
138             // Backwards compatibility: field value = field
139             Object value = cfpos.getFieldValue();
140             if (value == null) {
141                 value = cfpos.getField();
142             }
143             as.addAttribute(cfpos.getField(), value, cfpos.getStart(), cfpos.getLimit());
144         }
145         return as.getIterator();
146     }
147 
148     static class NullField extends Field {
149         private static final long serialVersionUID = 1L;
150         static final NullField END = new NullField("end");
NullField(String name)151         private NullField(String name) {
152             super(name);
153         }
154     }
155 
156     /**
157      * Implementation of nextPosition consistent with the contract of FormattedValue.
158      *
159      * @param cfpos
160      *            The argument passed to the public API.
161      * @param numericField
162      *            Optional. If non-null, apply this field to the entire numeric portion of the string.
163      * @return See FormattedValue#nextPosition.
164      */
nextPosition(FormattedStringBuilder self, ConstrainedFieldPosition cfpos, Field numericField)165     public static boolean nextPosition(FormattedStringBuilder self, ConstrainedFieldPosition cfpos, Field numericField) {
166         int fieldStart = -1;
167         Object currField = null;
168         boolean prevIsSpan = false;
169         if (cfpos.getLimit() > 0) {
170             prevIsSpan = cfpos.getField() instanceof UFormat.SpanField
171                 && cfpos.getStart() < cfpos.getLimit();
172         }
173         boolean prevIsNumeric = false;
174         if (numericField != null) {
175             prevIsNumeric = cfpos.getField() == numericField;
176         }
177         boolean prevIsInteger = cfpos.getField() == NumberFormat.Field.INTEGER;
178 
179         for (int i = self.zero + cfpos.getLimit(); i <= self.zero + self.length; i++) {
180             Object _field = (i < self.zero + self.length) ? self.fields[i] : NullField.END;
181             // Case 1: currently scanning a field.
182             if (currField != null) {
183                 if (currField != _field) {
184                     int end = i - self.zero;
185                     // Grouping separators can be whitespace; don't throw them out!
186                     if (isTrimmable(currField)) {
187                         end = trimBack(self, end);
188                     }
189                     if (end <= fieldStart) {
190                         // Entire field position is ignorable; skip.
191                         fieldStart = -1;
192                         currField = null;
193                         i--;  // look at this index again
194                         continue;
195                     }
196                     int start = fieldStart;
197                     if (isTrimmable(currField)) {
198                         start = trimFront(self, start);
199                     }
200                     cfpos.setState((Field) currField, null, start, end);
201                     return true;
202                 }
203                 continue;
204             }
205             // Special case: emit normalField if we are pointing at the end of spanField.
206             if (i > self.zero && prevIsSpan) {
207                 assert self.fields[i-1] instanceof SpanFieldPlaceholder;
208                 SpanFieldPlaceholder ph = (SpanFieldPlaceholder) self.fields[i-1];
209                 if (ph.normalField == ListFormatter.Field.ELEMENT) {
210                     // Special handling for ULISTFMT_ELEMENT_FIELD
211                     if (cfpos.matchesField(ListFormatter.Field.ELEMENT, null)) {
212                         fieldStart = i - self.zero - ph.length;
213                         int end = fieldStart + ph.length;
214                         cfpos.setState(ListFormatter.Field.ELEMENT, null, fieldStart, end);
215                         return true;
216                     }
217                 } else {
218                     // Re-wind, since there may be multiple fields in the span.
219                     i -= ph.length;
220                     assert i >= self.zero;
221                     _field = ((SpanFieldPlaceholder) self.fields[i]).normalField;
222                 }
223             }
224             // Special case: coalesce the INTEGER if we are pointing at the end of the INTEGER.
225             if (cfpos.matchesField(NumberFormat.Field.INTEGER, null)
226                     && i > self.zero
227                     && !prevIsInteger
228                     && !prevIsNumeric
229                     && isIntOrGroup(self.fields[i - 1])
230                     && !isIntOrGroup(_field)) {
231                 int j = i - 1;
232                 for (; j >= self.zero && isIntOrGroup(self.fields[j]); j--) {}
233                 cfpos.setState(NumberFormat.Field.INTEGER, null, j - self.zero + 1, i - self.zero);
234                 return true;
235             }
236             // Special case: coalesce NUMERIC if we are pointing at the end of the NUMERIC.
237             if (numericField != null
238                     && cfpos.matchesField(numericField, null)
239                     && i > self.zero
240                     && !prevIsNumeric
241                     && isNumericField(self.fields[i - 1])
242                     && !isNumericField(_field)) {
243                 // Re-wind to the beginning of the field and then emit it
244                 int j = i - 1;
245                 for (; j >= self.zero && isNumericField(self.fields[j]); j--) {}
246                 cfpos.setState(numericField, null, j - self.zero + 1, i - self.zero);
247                 return true;
248             }
249             // Check for span field
250             SpanFieldPlaceholder ph = null;
251             if (_field instanceof SpanFieldPlaceholder) {
252                 ph = (SpanFieldPlaceholder) _field;
253                 _field = ph.normalField;
254             }
255             if (ph != null && (ph.start == -1 || ph.start == i - self.zero)) {
256                 if (cfpos.matchesField(ph.spanField, ph.value)) {
257                     fieldStart = i - self.zero;
258                     int end = fieldStart + ph.length;
259                     cfpos.setState(ph.spanField, ph.value, fieldStart, end);
260                     return true;
261                 } else if (ph.normalField == ListFormatter.Field.ELEMENT) {
262                     // Special handling for ListFormatter.Field.ELEMENT
263                     if (cfpos.matchesField(ListFormatter.Field.ELEMENT, null)) {
264                         fieldStart = i - self.zero;
265                         int end = fieldStart + ph.length;
266                         cfpos.setState(ListFormatter.Field.ELEMENT, null, fieldStart, end);
267                         return true;
268                     } else {
269                         // Failed to match; jump ahead
270                         i += ph.length - 1;
271                         // goto loopend
272                     }
273                 }
274             }
275             // Special case: skip over INTEGER; will be coalesced later.
276             else if (_field == NumberFormat.Field.INTEGER) {
277                 _field = null;
278             }
279             // No field starting at this position.
280             else if (_field == null || _field == NullField.END) {
281                 // goto loopend
282             }
283             // No SpanField
284             else if (cfpos.matchesField((Field) _field, null)) {
285                 fieldStart = i - self.zero;
286                 currField = _field;
287             }
288             // loopend:
289             prevIsSpan = false;
290             prevIsNumeric = false;
291             prevIsInteger = false;
292         }
293 
294         assert currField == null;
295         // Always set the position to the end so that we don't revisit previous sections
296         cfpos.setState(
297             cfpos.getField(),
298             cfpos.getFieldValue(),
299             self.length,
300             self.length);
301         return false;
302     }
303 
isIntOrGroup(Object field)304     private static boolean isIntOrGroup(Object field) {
305         field = FormattedStringBuilder.unwrapField(field);
306         return field == NumberFormat.Field.INTEGER || field == NumberFormat.Field.GROUPING_SEPARATOR;
307     }
308 
isNumericField(Object field)309     private static boolean isNumericField(Object field) {
310         field = FormattedStringBuilder.unwrapField(field);
311         return field == null || NumberFormat.Field.class.isAssignableFrom(field.getClass());
312     }
313 
isTrimmable(Object field)314     private static boolean isTrimmable(Object field) {
315         return field != NumberFormat.Field.GROUPING_SEPARATOR
316                 && !(field instanceof ListFormatter.Field);
317     }
318 
trimBack(FormattedStringBuilder self, int limit)319     private static int trimBack(FormattedStringBuilder self, int limit) {
320         return StaticUnicodeSets.get(StaticUnicodeSets.Key.DEFAULT_IGNORABLES)
321                 .spanBack(self, limit, UnicodeSet.SpanCondition.CONTAINED);
322     }
323 
trimFront(FormattedStringBuilder self, int start)324     private static int trimFront(FormattedStringBuilder self, int start) {
325         return StaticUnicodeSets.get(StaticUnicodeSets.Key.DEFAULT_IGNORABLES)
326                 .span(self, start, UnicodeSet.SpanCondition.CONTAINED);
327     }
328 }
329