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