• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos
3  *
4  * All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions are met:
8  *
9  *  * Redistributions of source code must retain the above copyright notice,
10  *    this list of conditions and the following disclaimer.
11  *
12  *  * Redistributions in binary form must reproduce the above copyright notice,
13  *    this list of conditions and the following disclaimer in the documentation
14  *    and/or other materials provided with the distribution.
15  *
16  *  * Neither the name of JSR-310 nor the names of its contributors
17  *    may be used to endorse or promote products derived from this software
18  *    without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31  */
32 package org.threeten.bp.format;
33 
34 import java.util.ArrayList;
35 import java.util.HashMap;
36 import java.util.List;
37 import java.util.Locale;
38 import java.util.Map;
39 
40 import org.threeten.bp.Period;
41 import org.threeten.bp.ZoneId;
42 import org.threeten.bp.chrono.Chronology;
43 import org.threeten.bp.chrono.IsoChronology;
44 import org.threeten.bp.format.DateTimeFormatterBuilder.ReducedPrinterParser;
45 import org.threeten.bp.jdk8.DefaultInterfaceTemporalAccessor;
46 import org.threeten.bp.jdk8.Jdk8Methods;
47 import org.threeten.bp.temporal.TemporalField;
48 import org.threeten.bp.temporal.TemporalQueries;
49 import org.threeten.bp.temporal.TemporalQuery;
50 import org.threeten.bp.temporal.UnsupportedTemporalTypeException;
51 
52 /**
53  * Context object used during date and time parsing.
54  * <p>
55  * This class represents the current state of the parse.
56  * It has the ability to store and retrieve the parsed values and manage optional segments.
57  * It also provides key information to the parsing methods.
58  * <p>
59  * Once parsing is complete, the {@link #toBuilder()} is typically used
60  * to obtain a builder that can combine the separate parsed fields into meaningful values.
61  *
62  * <h3>Specification for implementors</h3>
63  * This class is a mutable context intended for use from a single thread.
64  * Usage of the class is thread-safe within standard parsing as a new instance of this class
65  * is automatically created for each parse and parsing is single-threaded
66  */
67 final class DateTimeParseContext {
68 
69     /**
70      * The locale, not null.
71      */
72     private Locale locale;
73     /**
74      * The symbols, not null.
75      */
76     private DecimalStyle symbols;
77     /**
78      * The override chronology.
79      */
80     private Chronology overrideChronology;
81     /**
82      * The override zone.
83      */
84     private ZoneId overrideZone;
85     /**
86      * Whether to parse using case sensitively.
87      */
88     private boolean caseSensitive = true;
89     /**
90      * Whether to parse using strict rules.
91      */
92     private boolean strict = true;
93     /**
94      * The list of parsed data.
95      */
96     private final ArrayList<Parsed> parsed = new ArrayList<Parsed>();
97 
98     /**
99      * Creates a new instance of the context.
100      *
101      * @param formatter  the formatter controlling the parse, not null
102      */
DateTimeParseContext(DateTimeFormatter formatter)103     DateTimeParseContext(DateTimeFormatter formatter) {
104         super();
105         this.locale = formatter.getLocale();
106         this.symbols = formatter.getDecimalStyle();
107         this.overrideChronology = formatter.getChronology();
108         this.overrideZone = formatter.getZone();
109         parsed.add(new Parsed());
110     }
111 
112     // for testing
DateTimeParseContext(Locale locale, DecimalStyle symbols, Chronology chronology)113     DateTimeParseContext(Locale locale, DecimalStyle symbols, Chronology chronology) {
114         super();
115         this.locale = locale;
116         this.symbols = symbols;
117         this.overrideChronology = chronology;
118         this.overrideZone = null;
119         parsed.add(new Parsed());
120     }
121 
DateTimeParseContext(DateTimeParseContext other)122     DateTimeParseContext(DateTimeParseContext other) {
123         super();
124         this.locale = other.locale;
125         this.symbols = other.symbols;
126         this.overrideChronology = other.overrideChronology;
127         this.overrideZone = other.overrideZone;
128         this.caseSensitive = other.caseSensitive;
129         this.strict = other.strict;
130         parsed.add(new Parsed());
131     }
132 
133     /**
134      * Creates a copy of this context.
135      */
copy()136     DateTimeParseContext copy() {
137         return new DateTimeParseContext(this);
138     }
139 
140     //-----------------------------------------------------------------------
141     /**
142      * Gets the locale.
143      * <p>
144      * This locale is used to control localization in the parse except
145      * where localization is controlled by the symbols.
146      *
147      * @return the locale, not null
148      */
getLocale()149     Locale getLocale() {
150         return locale;
151     }
152 
153     /**
154      * Gets the formatting symbols.
155      * <p>
156      * The symbols control the localization of numeric parsing.
157      *
158      * @return the formatting symbols, not null
159      */
getSymbols()160     DecimalStyle getSymbols() {
161         return symbols;
162     }
163 
164     /**
165      * Gets the effective chronology during parsing.
166      *
167      * @return the effective parsing chronology, not null
168      */
getEffectiveChronology()169     Chronology getEffectiveChronology() {
170         Chronology chrono = currentParsed().chrono;
171         if (chrono == null) {
172             chrono = overrideChronology;
173             if (chrono == null) {
174                 chrono = IsoChronology.INSTANCE;
175             }
176         }
177         return chrono;
178     }
179 
180     //-----------------------------------------------------------------------
181     /**
182      * Checks if parsing is case sensitive.
183      *
184      * @return true if parsing is case sensitive, false if case insensitive
185      */
isCaseSensitive()186     boolean isCaseSensitive() {
187         return caseSensitive;
188     }
189 
190     /**
191      * Sets whether the parsing is case sensitive or not.
192      *
193      * @param caseSensitive  changes the parsing to be case sensitive or not from now on
194      */
setCaseSensitive(boolean caseSensitive)195     void setCaseSensitive(boolean caseSensitive) {
196         this.caseSensitive = caseSensitive;
197     }
198 
199     /**
200      * Helper to compare two {@code CharSequence} instances.
201      * This uses {@link #isCaseSensitive()}.
202      *
203      * @param cs1  the first character sequence, not null
204      * @param offset1  the offset into the first sequence, valid
205      * @param cs2  the second character sequence, not null
206      * @param offset2  the offset into the second sequence, valid
207      * @param length  the length to check, valid
208      * @return true if equal
209      */
subSequenceEquals(CharSequence cs1, int offset1, CharSequence cs2, int offset2, int length)210     boolean subSequenceEquals(CharSequence cs1, int offset1, CharSequence cs2, int offset2, int length) {
211         if (offset1 + length > cs1.length() || offset2 + length > cs2.length()) {
212             return false;
213         }
214         if (isCaseSensitive()) {
215             for (int i = 0; i < length; i++) {
216                 char ch1 = cs1.charAt(offset1 + i);
217                 char ch2 = cs2.charAt(offset2 + i);
218                 if (ch1 != ch2) {
219                     return false;
220                 }
221             }
222         } else {
223             for (int i = 0; i < length; i++) {
224                 char ch1 = cs1.charAt(offset1 + i);
225                 char ch2 = cs2.charAt(offset2 + i);
226                 if (ch1 != ch2 && Character.toUpperCase(ch1) != Character.toUpperCase(ch2) &&
227                         Character.toLowerCase(ch1) != Character.toLowerCase(ch2)) {
228                     return false;
229                 }
230             }
231         }
232         return true;
233     }
234 
235     /**
236      * Helper to compare two {@code char}.
237      * This uses {@link #isCaseSensitive()}.
238      *
239      * @param ch1  the first character
240      * @param ch2  the second character
241      * @return true if equal
242      */
charEquals(char ch1, char ch2)243     boolean charEquals(char ch1, char ch2) {
244         if (isCaseSensitive()) {
245             return ch1 == ch2;
246         }
247         return charEqualsIgnoreCase(ch1, ch2);
248     }
249 
250     /**
251      * Compares two characters ignoring case.
252      *
253      * @param c1  the first
254      * @param c2  the second
255      * @return true if equal
256      */
charEqualsIgnoreCase(char c1, char c2)257     static boolean charEqualsIgnoreCase(char c1, char c2) {
258         return c1 == c2 ||
259                 Character.toUpperCase(c1) == Character.toUpperCase(c2) ||
260                 Character.toLowerCase(c1) == Character.toLowerCase(c2);
261     }
262 
263     //-----------------------------------------------------------------------
264     /**
265      * Checks if parsing is strict.
266      * <p>
267      * Strict parsing requires exact matching of the text and sign styles.
268      *
269      * @return true if parsing is strict, false if lenient
270      */
isStrict()271     boolean isStrict() {
272         return strict;
273     }
274 
275     /**
276      * Sets whether parsing is strict or lenient.
277      *
278      * @param strict  changes the parsing to be strict or lenient from now on
279      */
setStrict(boolean strict)280     void setStrict(boolean strict) {
281         this.strict = strict;
282     }
283 
284     //-----------------------------------------------------------------------
285     /**
286      * Starts the parsing of an optional segment of the input.
287      */
startOptional()288     void startOptional() {
289         parsed.add(currentParsed().copy());
290     }
291 
292     /**
293      * Ends the parsing of an optional segment of the input.
294      *
295      * @param successful  whether the optional segment was successfully parsed
296      */
endOptional(boolean successful)297     void endOptional(boolean successful) {
298         if (successful) {
299             parsed.remove(parsed.size() - 2);
300         } else {
301             parsed.remove(parsed.size() - 1);
302         }
303     }
304 
305     //-----------------------------------------------------------------------
306     /**
307      * Gets the currently active temporal objects.
308      *
309      * @return the current temporal objects, not null
310      */
currentParsed()311     private Parsed currentParsed() {
312         return parsed.get(parsed.size() - 1);
313     }
314 
315     //-----------------------------------------------------------------------
316     /**
317      * Gets the first value that was parsed for the specified field.
318      * <p>
319      * This searches the results of the parse, returning the first value found
320      * for the specified field. No attempt is made to derive a value.
321      * The field may have an out of range value.
322      * For example, the day-of-month might be set to 50, or the hour to 1000.
323      *
324      * @param field  the field to query from the map, null returns null
325      * @return the value mapped to the specified field, null if field was not parsed
326      */
getParsed(TemporalField field)327     Long getParsed(TemporalField field) {
328         return currentParsed().fieldValues.get(field);
329     }
330 
331     /**
332      * Stores the parsed field.
333      * <p>
334      * This stores a field-value pair that has been parsed.
335      * The value stored may be out of range for the field - no checks are performed.
336      *
337      * @param field  the field to set in the field-value map, not null
338      * @param value  the value to set in the field-value map
339      * @param errorPos  the position of the field being parsed
340      * @param successPos  the position after the field being parsed
341      * @return the new position
342      */
setParsedField(TemporalField field, long value, int errorPos, int successPos)343     int setParsedField(TemporalField field, long value, int errorPos, int successPos) {
344         Jdk8Methods.requireNonNull(field, "field");
345         Long old = currentParsed().fieldValues.put(field, value);
346         return (old != null && old.longValue() != value) ? ~errorPos : successPos;
347     }
348 
349     /**
350      * Stores the parsed chronology.
351      * <p>
352      * This stores the chronology that has been parsed.
353      * No validation is performed other than ensuring it is not null.
354      *
355      * @param chrono  the parsed chronology, not null
356      */
setParsed(Chronology chrono)357     void setParsed(Chronology chrono) {
358         Jdk8Methods.requireNonNull(chrono, "chrono");
359         Parsed currentParsed = currentParsed();
360         currentParsed.chrono = chrono;
361         if (currentParsed.callbacks != null) {
362             List<Object[]> callbacks = new ArrayList<Object[]>(currentParsed.callbacks);
363             currentParsed.callbacks.clear();
364             for (Object[] objects : callbacks) {
365                 ReducedPrinterParser pp = (ReducedPrinterParser) objects[0];
366                 pp.setValue(this, (Long) objects[1], (Integer) objects[2], (Integer) objects[3]);
367             }
368         }
369     }
370 
addChronologyChangedParser(ReducedPrinterParser reducedPrinterParser, long value, int errorPos, int successPos)371     void addChronologyChangedParser(ReducedPrinterParser reducedPrinterParser, long value, int errorPos, int successPos) {
372         Parsed currentParsed = currentParsed();
373         if (currentParsed.callbacks == null) {
374             currentParsed.callbacks = new ArrayList<Object[]>(2);
375         }
376         currentParsed.callbacks.add(new Object[] {reducedPrinterParser, value, errorPos, successPos});
377     }
378 
379     /**
380      * Stores the parsed zone.
381      * <p>
382      * This stores the zone that has been parsed.
383      * No validation is performed other than ensuring it is not null.
384      *
385      * @param zone  the parsed zone, not null
386      */
setParsed(ZoneId zone)387     void setParsed(ZoneId zone) {
388         Jdk8Methods.requireNonNull(zone, "zone");
389         currentParsed().zone = zone;
390     }
391 
392     /**
393      * Stores the leap second.
394      */
setParsedLeapSecond()395     void setParsedLeapSecond() {
396         currentParsed().leapSecond = true;
397     }
398 
399     //-----------------------------------------------------------------------
400     /**
401      * Returns a {@code TemporalAccessor} that can be used to interpret
402      * the results of the parse.
403      *
404      * @return an accessor with the results of the parse, not null
405      */
toParsed()406     Parsed toParsed() {
407         return currentParsed();
408     }
409 
410     //-----------------------------------------------------------------------
411     /**
412      * Returns a string version of the context for debugging.
413      *
414      * @return a string representation of the context data, not null
415      */
416     @Override
toString()417     public String toString() {
418         return currentParsed().toString();
419     }
420 
421     //-----------------------------------------------------------------------
422     /**
423      * Temporary store of parsed data.
424      */
425     final class Parsed extends DefaultInterfaceTemporalAccessor {
426         Chronology chrono = null;
427         ZoneId zone = null;
428         final Map<TemporalField, Long> fieldValues = new HashMap<TemporalField, Long>();
429         boolean leapSecond;
430         Period excessDays = Period.ZERO;
431         List<Object[]> callbacks;
432 
Parsed()433         private Parsed() {
434         }
copy()435         protected Parsed copy() {
436             Parsed cloned = new Parsed();
437             cloned.chrono = this.chrono;
438             cloned.zone = this.zone;
439             cloned.fieldValues.putAll(this.fieldValues);
440             cloned.leapSecond = this.leapSecond;
441             return cloned;
442         }
443         @Override
toString()444         public String toString() {
445             return fieldValues.toString() + "," + chrono + "," + zone;
446         }
447         @Override
isSupported(TemporalField field)448         public boolean isSupported(TemporalField field) {
449             return fieldValues.containsKey(field);
450         }
451         @Override
get(TemporalField field)452         public int get(TemporalField field) {
453             if (fieldValues.containsKey(field) == false) {
454                 throw new UnsupportedTemporalTypeException("Unsupported field: " + field);
455             }
456             long value = fieldValues.get(field);
457             return Jdk8Methods.safeToInt(value);
458         }
459         @Override
getLong(TemporalField field)460         public long getLong(TemporalField field) {
461             if (fieldValues.containsKey(field) == false) {
462                 throw new UnsupportedTemporalTypeException("Unsupported field: " + field);
463             }
464             return fieldValues.get(field);
465         }
466         @SuppressWarnings("unchecked")
467         @Override
query(TemporalQuery<R> query)468         public <R> R query(TemporalQuery<R> query) {
469             if (query == TemporalQueries.chronology()) {
470                 return (R) chrono;
471             }
472             if (query == TemporalQueries.zoneId() || query == TemporalQueries.zone()) {
473                 return (R) zone;
474             }
475             return super.query(query);
476         }
477 
478         /**
479          * Returns a {@code DateTimeBuilder} that can be used to interpret
480          * the results of the parse.
481          * <p>
482          * This method is typically used once parsing is complete to obtain the parsed data.
483          * Parsing will typically result in separate fields, such as year, month and day.
484          * The returned builder can be used to combine the parsed data into meaningful
485          * objects such as {@code LocalDate}, potentially applying complex processing
486          * to handle invalid parsed data.
487          *
488          * @return a new builder with the results of the parse, not null
489          */
toBuilder()490         DateTimeBuilder toBuilder() {
491             DateTimeBuilder builder = new DateTimeBuilder();
492             builder.fieldValues.putAll(fieldValues);
493             builder.chrono = getEffectiveChronology();
494             if (zone != null) {
495                 builder.zone = zone;
496             } else {
497                 builder.zone = overrideZone;
498             }
499             builder.leapSecond = leapSecond;
500             builder.excessDays = excessDays;
501             return builder;
502         }
503     }
504 
505     //-------------------------------------------------------------------------
506     // for testing
507     /**
508      * Sets the locale.
509      * <p>
510      * This locale is used to control localization in the print output except
511      * where localization is controlled by the symbols.
512      *
513      * @param locale  the locale, not null
514      */
setLocale(Locale locale)515     void setLocale(Locale locale) {
516         Jdk8Methods.requireNonNull(locale, "locale");
517         this.locale = locale;
518     }
519 
520 }
521