LEFT | RIGHT |
1 // © 2017 and later: Unicode, Inc. and others. | 1 // © 2017 and later: Unicode, Inc. and others. |
2 // License & terms of use: http://www.unicode.org/copyright.html#License | 2 // License & terms of use: http://www.unicode.org/copyright.html#License |
3 package com.ibm.icu.impl.number.parse; | 3 package com.ibm.icu.impl.number.parse; |
4 | 4 |
| 5 import java.text.ParsePosition; |
5 import java.util.ArrayList; | 6 import java.util.ArrayList; |
| 7 import java.util.Collection; |
6 import java.util.Comparator; | 8 import java.util.Comparator; |
7 import java.util.List; | 9 import java.util.List; |
8 | 10 |
9 import com.ibm.icu.impl.number.AffixPatternProvider; | 11 import com.ibm.icu.impl.number.AffixPatternProvider; |
10 import com.ibm.icu.impl.number.AffixUtils; | 12 import com.ibm.icu.impl.number.AffixUtils; |
11 import com.ibm.icu.impl.number.MutablePatternModifier; | 13 import com.ibm.icu.impl.number.CustomSymbolCurrency; |
| 14 import com.ibm.icu.impl.number.DecimalFormatProperties; |
| 15 import com.ibm.icu.impl.number.Parse.ParseMode; |
12 import com.ibm.icu.impl.number.PatternStringParser; | 16 import com.ibm.icu.impl.number.PatternStringParser; |
13 import com.ibm.icu.number.NumberFormatter.SignDisplay; | 17 import com.ibm.icu.impl.number.PatternStringParser.ParsedPatternInfo; |
14 import com.ibm.icu.number.NumberFormatter.UnitWidth; | 18 import com.ibm.icu.impl.number.PropertiesAffixPatternProvider; |
| 19 import com.ibm.icu.impl.number.RoundingUtils; |
| 20 import com.ibm.icu.number.Grouper; |
15 import com.ibm.icu.text.DecimalFormatSymbols; | 21 import com.ibm.icu.text.DecimalFormatSymbols; |
| 22 import com.ibm.icu.text.UnicodeSet; |
16 import com.ibm.icu.util.Currency; | 23 import com.ibm.icu.util.Currency; |
| 24 import com.ibm.icu.util.CurrencyAmount; |
17 import com.ibm.icu.util.ULocale; | 25 import com.ibm.icu.util.ULocale; |
18 | 26 |
19 /** | 27 /** |
20 * Primary number parsing implementation class. | 28 * Primary number parsing implementation class. |
21 * | 29 * |
22 * @author sffc | 30 * @author sffc |
23 * | 31 * |
24 */ | 32 */ |
25 public class NumberParserImpl { | 33 public class NumberParserImpl { |
26 public static NumberParserImpl createParserFromPattern(String pattern) { | 34 @Deprecated |
27 NumberParserImpl parser = new NumberParserImpl(); | 35 public static NumberParserImpl createParserFromPattern(String pattern, boole
an strictGrouping) { |
28 ULocale locale = ULocale.ENGLISH; | 36 // Temporary frontend for testing. |
| 37 |
| 38 int parseFlags = ParsingUtils.PARSE_FLAG_IGNORE_CASE |
| 39 | ParsingUtils.PARSE_FLAG_INCLUDE_UNPAIRED_AFFIXES; |
| 40 if (strictGrouping) { |
| 41 parseFlags |= ParsingUtils.PARSE_FLAG_STRICT_GROUPING_SIZE; |
| 42 } |
| 43 |
| 44 NumberParserImpl parser = new NumberParserImpl(parseFlags, true); |
| 45 ULocale locale = new ULocale("en_IN"); |
29 DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale); | 46 DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale); |
30 | 47 IgnorablesMatcher ignorables = IgnorablesMatcher.DEFAULT; |
31 MutablePatternModifier mod = new MutablePatternModifier(false); | 48 |
32 AffixPatternProvider provider = PatternStringParser.parseToPatternInfo(p
attern); | 49 ParsedPatternInfo patternInfo = PatternStringParser.parseToPatternInfo(p
attern); |
33 mod.setPatternInfo(provider); | 50 AffixMatcher.generateFromAffixPatternProvider(patternInfo, parser, ignor
ables, parseFlags); |
34 mod.setPatternAttributes(SignDisplay.AUTO, false); | 51 |
35 mod.setSymbols(symbols, | 52 Grouper grouper = Grouper.defaults().withLocaleData(patternInfo); |
36 Currency.getInstance("USD"), | 53 |
37 UnitWidth.FULL_NAME, | 54 parser.addMatcher(ignorables); |
38 null); | 55 parser.addMatcher(DecimalMatcher.getInstance(symbols, grouper, parseFlag
s)); |
39 int flags = 0; | 56 parser.addMatcher(MinusSignMatcher.getInstance(symbols)); |
40 if (provider.containsSymbolType(AffixUtils.TYPE_PERCENT)) { | 57 parser.addMatcher(ScientificMatcher.getInstance(symbols, grouper, parseF
lags)); |
41 flags |= ParsedNumber.FLAG_PERCENT; | 58 parser.addMatcher(CurrencyTrieMatcher.getInstance(locale)); |
42 } | 59 parser.addMatcher(new RequireNumberMatcher()); |
43 if (provider.containsSymbolType(AffixUtils.TYPE_PERMILLE)) { | 60 |
44 flags |= ParsedNumber.FLAG_PERMILLE; | |
45 } | |
46 AffixMatcher.generateFromPatternModifier(mod, flags, parser); | |
47 | |
48 parser.addMatcher(DecimalMatcher.getInstance(symbols)); | |
49 parser.addMatcher(WhitespaceMatcher.getInstance()); | |
50 parser.addMatcher(new MinusSignMatcher()); | |
51 parser.addMatcher(new ScientificMatcher(symbols)); | |
52 parser.addMatcher(new CurrencyMatcher(locale)); | |
53 | |
54 parser.setComparator(new Comparator<ParsedNumber>() { | |
55 @Override | |
56 public int compare(ParsedNumber o1, ParsedNumber o2) { | |
57 return o1.charsConsumed - o2.charsConsumed; | |
58 } | |
59 }); | |
60 parser.freeze(); | 61 parser.freeze(); |
61 return parser; | 62 return parser; |
62 } | 63 } |
63 | 64 |
| 65 public static Number parseStatic( |
| 66 String input, |
| 67 ParsePosition ppos, |
| 68 DecimalFormatProperties properties, |
| 69 DecimalFormatSymbols symbols) { |
| 70 NumberParserImpl parser = createParserFromProperties(properties, symbols
, false, false); |
| 71 ParsedNumber result = new ParsedNumber(); |
| 72 parser.parse(input, true, result); |
| 73 if (result.success()) { |
| 74 ppos.setIndex(result.charsConsumed); |
| 75 return result.getNumber(); |
| 76 } else { |
| 77 ppos.setErrorIndex(result.charsConsumed); |
| 78 return null; |
| 79 } |
| 80 } |
| 81 |
| 82 public static CurrencyAmount parseStaticCurrency( |
| 83 String input, |
| 84 ParsePosition ppos, |
| 85 DecimalFormatProperties properties, |
| 86 DecimalFormatSymbols symbols) { |
| 87 NumberParserImpl parser = createParserFromProperties(properties, symbols
, true, false); |
| 88 ParsedNumber result = new ParsedNumber(); |
| 89 parser.parse(input, true, result); |
| 90 if (result.success()) { |
| 91 ppos.setIndex(result.charsConsumed); |
| 92 // TODO: Clean this up |
| 93 Currency currency; |
| 94 if (result.currencyCode != null) { |
| 95 currency = Currency.getInstance(result.currencyCode); |
| 96 } else { |
| 97 assert 0 != (result.flags & ParsedNumber.FLAG_HAS_DEFAULT_CURREN
CY); |
| 98 currency = CustomSymbolCurrency |
| 99 .resolve(properties.getCurrency(), symbols.getULocale(),
symbols); |
| 100 } |
| 101 return new CurrencyAmount(result.getNumber(), currency); |
| 102 } else { |
| 103 ppos.setErrorIndex(result.charsConsumed); |
| 104 return null; |
| 105 } |
| 106 } |
| 107 |
| 108 public static NumberParserImpl createDefaultParserForLocale(ULocale loc, boo
lean optimize) { |
| 109 DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(loc); |
| 110 DecimalFormatProperties properties = PatternStringParser.parseToProperti
es("0"); |
| 111 return createParserFromProperties(properties, symbols, false, optimize); |
| 112 } |
| 113 |
| 114 public static NumberParserImpl createParserFromProperties( |
| 115 DecimalFormatProperties properties, |
| 116 DecimalFormatSymbols symbols, |
| 117 boolean parseCurrency, |
| 118 boolean optimize) { |
| 119 |
| 120 ULocale locale = symbols.getULocale(); |
| 121 AffixPatternProvider patternInfo = new PropertiesAffixPatternProvider(pr
operties); |
| 122 Currency currency = CustomSymbolCurrency.resolve(properties.getCurrency(
), locale, symbols); |
| 123 boolean isStrict = properties.getParseMode() == ParseMode.STRICT; |
| 124 boolean decimalSeparatorRequired = properties.getDecimalPatternMatchRequ
ired() |
| 125 ? (properties.getDecimalSeparatorAlwaysShown() |
| 126 || properties.getMaximumFractionDigits() != 0) |
| 127 : false; |
| 128 Grouper grouper = Grouper.defaults().withProperties(properties); |
| 129 int parseFlags = 0; |
| 130 if (!properties.getParseCaseSensitive()) { |
| 131 parseFlags |= ParsingUtils.PARSE_FLAG_IGNORE_CASE; |
| 132 } |
| 133 if (properties.getParseIntegerOnly()) { |
| 134 parseFlags |= ParsingUtils.PARSE_FLAG_INTEGER_ONLY; |
| 135 } |
| 136 if (isStrict) { |
| 137 parseFlags |= ParsingUtils.PARSE_FLAG_STRICT_GROUPING_SIZE; |
| 138 } else { |
| 139 parseFlags |= ParsingUtils.PARSE_FLAG_INCLUDE_UNPAIRED_AFFIXES; |
| 140 } |
| 141 if (grouper.getPrimary() == -1) { |
| 142 parseFlags |= ParsingUtils.PARSE_FLAG_GROUPING_DISABLED; |
| 143 } |
| 144 if (parseCurrency || patternInfo.hasCurrencySign()) { |
| 145 parseFlags |= ParsingUtils.PARSE_FLAG_MONETARY_SEPARATORS; |
| 146 } |
| 147 IgnorablesMatcher ignorables = isStrict ? IgnorablesMatcher.STRICT : Ign
orablesMatcher.DEFAULT; |
| 148 |
| 149 NumberParserImpl parser = new NumberParserImpl(parseFlags, optimize); |
| 150 |
| 151 ////////////////////// |
| 152 /// AFFIX MATCHERS /// |
| 153 ////////////////////// |
| 154 |
| 155 // Set up a pattern modifier with mostly defaults to generate AffixMatch
ers. |
| 156 AffixMatcher.generateFromAffixPatternProvider(patternInfo, parser, ignor
ables, parseFlags); |
| 157 |
| 158 //////////////////////// |
| 159 /// CURRENCY MATCHER /// |
| 160 //////////////////////// |
| 161 |
| 162 if (parseCurrency || patternInfo.hasCurrencySign()) { |
| 163 parser.addMatcher(CurrencyTrieMatcher.getInstance(locale)); |
| 164 parser.addMatcher(CurrencyMatcher.getInstance(currency, locale, pars
eFlags)); |
| 165 } |
| 166 |
| 167 /////////////////////////////// |
| 168 /// OTHER STANDARD MATCHERS /// |
| 169 /////////////////////////////// |
| 170 |
| 171 if (!isStrict |
| 172 || patternInfo.containsSymbolType(AffixUtils.TYPE_PLUS_SIGN) |
| 173 || properties.getSignAlwaysShown()) { |
| 174 parser.addMatcher(PlusSignMatcher.getInstance(symbols)); |
| 175 } |
| 176 parser.addMatcher(MinusSignMatcher.getInstance(symbols)); |
| 177 parser.addMatcher(NanMatcher.getInstance(symbols, parseFlags)); |
| 178 parser.addMatcher(PercentMatcher.getInstance(symbols)); |
| 179 parser.addMatcher(PermilleMatcher.getInstance(symbols)); |
| 180 parser.addMatcher(InfinityMatcher.getInstance(symbols)); |
| 181 String padString = properties.getPadString(); |
| 182 if (padString != null && !ignorables.getSet().contains(padString)) { |
| 183 parser.addMatcher(new PaddingMatcher(padString)); |
| 184 } |
| 185 parser.addMatcher(ignorables); |
| 186 parser.addMatcher(DecimalMatcher.getInstance(symbols, grouper, parseFlag
s)); |
| 187 if (!properties.getParseNoExponent()) { |
| 188 parser.addMatcher(ScientificMatcher.getInstance(symbols, grouper, pa
rseFlags)); |
| 189 } |
| 190 |
| 191 ////////////////// |
| 192 /// VALIDATORS /// |
| 193 ////////////////// |
| 194 |
| 195 parser.addMatcher(new RequireNumberMatcher()); |
| 196 if (isStrict) { |
| 197 parser.addMatcher(new RequireAffixMatcher()); |
| 198 } |
| 199 if (isStrict && properties.getMinimumExponentDigits() > 0) { |
| 200 parser.addMatcher(new RequireExponentMatcher()); |
| 201 } |
| 202 if (parseCurrency) { |
| 203 parser.addMatcher(new RequireCurrencyMatcher()); |
| 204 } |
| 205 if (decimalSeparatorRequired) { |
| 206 parser.addMatcher(new RequireDecimalSeparatorMatcher()); |
| 207 } |
| 208 if (properties.getMultiplier() != null) { |
| 209 // We need to use a math context in order to prevent non-terminating
decimal expansions. |
| 210 // This is only used when dividing by the multiplier. |
| 211 parser.addMatcher(new MultiplierHandler(properties.getMultiplier(), |
| 212 RoundingUtils.getMathContextOr34Digits(properties))); |
| 213 } |
| 214 |
| 215 parser.freeze(); |
| 216 return parser; |
| 217 } |
| 218 |
| 219 private final int parseFlags; |
64 private final List<NumberParseMatcher> matchers; | 220 private final List<NumberParseMatcher> matchers; |
| 221 private final List<UnicodeSet> leadCodePointses; |
65 private Comparator<ParsedNumber> comparator; | 222 private Comparator<ParsedNumber> comparator; |
66 private boolean frozen; | 223 private boolean frozen; |
67 | 224 |
68 public NumberParserImpl() { | 225 /** |
| 226 * Creates a new, empty parser. |
| 227 * |
| 228 * @param ignoreCase |
| 229 * If true, perform case-folding. This parameter needs to go into
the constructor because |
| 230 * its value is used during the construction of the matcher chain
. |
| 231 * @param optimize |
| 232 * If true, compute "lead chars" UnicodeSets for the matchers. Th
is reduces parsing |
| 233 * runtime but increases construction runtime. If the parser is g
oing to be used only once |
| 234 * or twice, set this to false; if it is going to be used hundred
s of times, set it to |
| 235 * true. |
| 236 */ |
| 237 public NumberParserImpl(int parseFlags, boolean optimize) { |
69 matchers = new ArrayList<NumberParseMatcher>(); | 238 matchers = new ArrayList<NumberParseMatcher>(); |
| 239 if (optimize) { |
| 240 leadCodePointses = new ArrayList<UnicodeSet>(); |
| 241 } else { |
| 242 leadCodePointses = null; |
| 243 } |
| 244 comparator = ParsedNumber.COMPARATOR; // default value |
| 245 this.parseFlags = parseFlags; |
70 frozen = false; | 246 frozen = false; |
71 } | 247 } |
72 | 248 |
73 public void addMatcher(NumberParseMatcher matcher) { | 249 public void addMatcher(NumberParseMatcher matcher) { |
74 matchers.add(matcher); | 250 assert !frozen; |
| 251 this.matchers.add(matcher); |
| 252 if (leadCodePointses != null) { |
| 253 UnicodeSet leadCodePoints = matcher.getLeadCodePoints(); |
| 254 assert leadCodePoints.isFrozen(); |
| 255 this.leadCodePointses.add(leadCodePoints); |
| 256 } |
| 257 } |
| 258 |
| 259 public void addMatchers(Collection<? extends NumberParseMatcher> matchers) { |
| 260 assert !frozen; |
| 261 this.matchers.addAll(matchers); |
| 262 if (leadCodePointses != null) { |
| 263 for (NumberParseMatcher matcher : matchers) { |
| 264 UnicodeSet leadCodePoints = matcher.getLeadCodePoints(); |
| 265 assert leadCodePoints.isFrozen(); |
| 266 this.leadCodePointses.add(leadCodePoints); |
| 267 } |
| 268 } |
75 } | 269 } |
76 | 270 |
77 public void setComparator(Comparator<ParsedNumber> comparator) { | 271 public void setComparator(Comparator<ParsedNumber> comparator) { |
| 272 assert !frozen; |
78 this.comparator = comparator; | 273 this.comparator = comparator; |
79 } | 274 } |
80 | 275 |
81 public void freeze() { | 276 public void freeze() { |
82 frozen = true; | 277 frozen = true; |
83 } | 278 } |
84 | 279 |
85 public void parse(String input, boolean greedy, ParsedNumber result) { | 280 public void parse(String input, boolean greedy, ParsedNumber result) { |
| 281 parse(input, 0, greedy, result); |
| 282 } |
| 283 |
| 284 /** |
| 285 * Primary entrypoint to parsing code path. |
| 286 * |
| 287 * @param input |
| 288 * The string to parse. This is a String, not CharSequence, to en
force assumptions about |
| 289 * immutability (CharSequences are not guaranteed to be immutable
). |
| 290 * @param start |
| 291 * The index into the string at which to start parsing. |
| 292 * @param greedy |
| 293 * Whether to use the faster but potentially less accurate greedy
code path. |
| 294 * @param result |
| 295 * Output variable to store results. |
| 296 */ |
| 297 public void parse(String input, int start, boolean greedy, ParsedNumber resu
lt) { |
86 assert frozen; | 298 assert frozen; |
87 StringSegment segment = new StringSegment(input); | 299 StringSegment segment = new StringSegment(ParsingUtils.maybeFold(input,
parseFlags)); |
| 300 segment.adjustOffset(start); |
88 if (greedy) { | 301 if (greedy) { |
89 parseGreedyRecursive(segment, result); | 302 parseGreedyRecursive(segment, result); |
90 } else { | 303 } else { |
91 parseLongestRecursive(segment, result); | 304 parseLongestRecursive(segment, result); |
92 } | 305 } |
93 for (NumberParseMatcher matcher : matchers) { | 306 for (NumberParseMatcher matcher : matchers) { |
94 matcher.postProcess(result); | 307 matcher.postProcess(result); |
95 } | 308 } |
96 } | 309 } |
97 | 310 |
98 private void parseGreedyRecursive(StringSegment segment, ParsedNumber result
) { | 311 private void parseGreedyRecursive(StringSegment segment, ParsedNumber result
) { |
99 // Base Case | 312 // Base Case |
100 if (segment.length() == 0) { | 313 if (segment.length() == 0) { |
101 return; | 314 return; |
102 } | 315 } |
103 | 316 |
104 int initialOffset = segment.getOffset(); | 317 int initialOffset = segment.getOffset(); |
| 318 int leadCp = segment.getCodePoint(); |
105 for (int i = 0; i < matchers.size(); i++) { | 319 for (int i = 0; i < matchers.size(); i++) { |
| 320 if (leadCodePointses != null && !leadCodePointses.get(i).contains(le
adCp)) { |
| 321 continue; |
| 322 } |
106 NumberParseMatcher matcher = matchers.get(i); | 323 NumberParseMatcher matcher = matchers.get(i); |
107 matcher.match(segment, result); | 324 matcher.match(segment, result); |
108 if (segment.getOffset() != initialOffset) { | 325 if (segment.getOffset() != initialOffset) { |
109 // In a greedy parse, recurse on only the first match. | 326 // In a greedy parse, recurse on only the first match. |
110 parseGreedyRecursive(segment, result); | 327 parseGreedyRecursive(segment, result); |
111 // The following line resets the offset so that the StringSegmen
t says the same across the function | 328 // The following line resets the offset so that the StringSegmen
t says the same across |
| 329 // the function |
112 // call boundary. Since we recurse only once, this line is not s
trictly necessary. | 330 // call boundary. Since we recurse only once, this line is not s
trictly necessary. |
113 segment.setOffset(initialOffset); | 331 segment.setOffset(initialOffset); |
114 return; | 332 return; |
115 } | 333 } |
116 } | 334 } |
117 | 335 |
118 // NOTE: If we get here, the greedy parse completed without consuming th
e entire string. | 336 // NOTE: If we get here, the greedy parse completed without consuming th
e entire string. |
119 } | 337 } |
120 | 338 |
121 private void parseLongestRecursive(StringSegment segment, ParsedNumber resul
t) { | 339 private void parseLongestRecursive(StringSegment segment, ParsedNumber resul
t) { |
122 // Base Case | 340 // Base Case |
123 if (segment.length() == 0) { | 341 if (segment.length() == 0) { |
124 return; | 342 return; |
125 } | 343 } |
126 | 344 |
127 // TODO: Give a nice way for the matcher to reset the ParsedNumber? | 345 // TODO: Give a nice way for the matcher to reset the ParsedNumber? |
128 ParsedNumber initial = new ParsedNumber(); | 346 ParsedNumber initial = new ParsedNumber(); |
129 initial.copyFrom(result); | 347 initial.copyFrom(result); |
130 ParsedNumber candidate = new ParsedNumber(); | 348 ParsedNumber candidate = new ParsedNumber(); |
131 | 349 |
132 int initialOffset = segment.getOffset(); | 350 int initialOffset = segment.getOffset(); |
133 for (int i = 0; i < matchers.size(); i++) { | 351 for (int i = 0; i < matchers.size(); i++) { |
134 NumberParseMatcher matcher = matchers.get(i); | 352 NumberParseMatcher matcher = matchers.get(i); |
135 // In a non-greedy parse, we attempt all possible matches and pick t
he best. | 353 // In a non-greedy parse, we attempt all possible matches and pick t
he best. |
136 for (int charsToConsume = 1; charsToConsume <= segment.length(); cha
rsToConsume++) { | 354 for (int charsToConsume = 0; charsToConsume < segment.length();) { |
| 355 charsToConsume += Character.charCount(Character.codePointAt(segm
ent, charsToConsume)); |
| 356 |
| 357 // Run the matcher on a segment of the current length. |
137 candidate.copyFrom(initial); | 358 candidate.copyFrom(initial); |
138 | |
139 // Run the matcher on a segment of the current length. | |
140 segment.setLength(charsToConsume); | 359 segment.setLength(charsToConsume); |
141 boolean maybeMore = matcher.match(segment, candidate); | 360 boolean maybeMore = matcher.match(segment, candidate); |
142 segment.resetLength(); | 361 segment.resetLength(); |
143 | 362 |
144 // If the entire segment was consumed, recurse. | 363 // If the entire segment was consumed, recurse. |
145 if (segment.getOffset() - initialOffset == charsToConsume) { | 364 if (segment.getOffset() - initialOffset == charsToConsume) { |
146 parseLongestRecursive(segment, candidate); | 365 parseLongestRecursive(segment, candidate); |
147 if (comparator.compare(candidate, result) > 0) { | 366 if (comparator.compare(candidate, result) > 0) { |
148 result.copyFrom(candidate); | 367 result.copyFrom(candidate); |
149 } | 368 } |
150 } | 369 } |
151 | 370 |
152 // Since the segment can be re-used, reset the offset. | 371 // Since the segment can be re-used, reset the offset. |
153 // This does not have an effect if the matcher did not consume a
ny chars. | 372 // This does not have an effect if the matcher did not consume a
ny chars. |
154 segment.setOffset(initialOffset); | 373 segment.setOffset(initialOffset); |
155 | 374 |
156 // Unless the matcher wants to see the next char, continue to th
e next matcher. | 375 // Unless the matcher wants to see the next char, continue to th
e next matcher. |
157 if (!maybeMore) { | 376 if (!maybeMore) { |
158 break; | 377 break; |
159 } | 378 } |
160 } | 379 } |
161 } | 380 } |
162 } | 381 } |
163 | 382 |
164 @Override | 383 @Override |
165 public String toString() { | 384 public String toString() { |
166 return "<NumberParserImpl matchers=" + matchers.toString() + ">"; | 385 return "<NumberParserImpl matchers=" + matchers.toString() + ">"; |
167 } | 386 } |
168 } | 387 } |
LEFT | RIGHT |