1 /*
2  * hunt-time: A time library for D programming language.
3  *
4  * Copyright (C) 2015-2018 HuntLabs
5  *
6  * Website: https://www.huntlabs.net/
7  *
8  * Licensed under the Apache-2.0 License.
9  *
10  */
11 
12 module hunt.time.format.DateTimeParseContext;
13 
14 import hunt.time.ZoneId;
15 import hunt.time.chrono.Chronology;
16 import hunt.time.chrono.IsoChronology;
17 import hunt.time.temporal.TemporalAccessor;
18 import hunt.time.temporal.TemporalField;
19 import hunt.time.format.DateTimeFormatter;
20 import hunt.time.format.Parsed;
21 import hunt.time.format.DecimalStyle;
22 import hunt.time.format.ResolverStyle;
23 
24 import hunt.collection.ArrayList;
25 import hunt.collection.Set;
26 import hunt.Functions;
27 import hunt.Long;
28 import hunt.text.Common;
29 import hunt.util.Locale;
30 
31 import std.ascii;
32 
33 /**
34  * Context object used during date and time parsing.
35  * !(p)
36  * This class represents the current state of the parse.
37  * It has the ability to store and retrieve the parsed values and manage optional segments.
38  * It also provides key information to the parsing methods.
39  * !(p)
40  * Once parsing is complete, the {@link #toUnresolved()} is used to obtain the unresolved
41  * result data. The {@link #toResolved()} is used to obtain the resolved result.
42  *
43  * @implSpec
44  * This class is a mutable context intended for use from a single thread.
45  * Usage of the class is thread-safe within standard parsing as a new instance of this class
46  * is automatically created for each parse and parsing is single-threaded
47  *
48  * @since 1.8
49  */
50 final class DateTimeParseContext {
51 
52     /**
53      * The formatter, not null.
54      */
55     private DateTimeFormatter formatter;
56     /**
57      * Whether to parse using case sensitively.
58      */
59     private bool caseSensitive = true;
60     /**
61      * Whether to parse using strict rules.
62      */
63     private bool strict = true;
64     /**
65      * The list of parsed data.
66      */
67     private  ArrayList!(Parsed) parsed;
68     /**
69      * List of Consumers!(Chronology) to be notified if the Chronology changes.
70      */
71     private ArrayList!(Consumer!(Chronology)) chronoListeners = null;
72 
73      this()
74     {
75         parsed = new ArrayList!(Parsed)();
76     }
77 
78     /**
79      * Creates a new instance of the context.
80      *
81      * @param formatter  the formatter controlling the parse, not null
82      */
83     this(DateTimeFormatter formatter) {
84         // super();
85         parsed = new ArrayList!(Parsed)();
86         this.formatter = formatter;
87         parsed.add(new Parsed());
88     }
89 
90     /**
91      * Creates a copy of this context.
92      * This retains the case sensitive and strict flags.
93      */
94     DateTimeParseContext copy() {
95         DateTimeParseContext newContext = new DateTimeParseContext(formatter);
96         newContext.caseSensitive = caseSensitive;
97         newContext.strict = strict;
98         return newContext;
99     }
100 
101     //-----------------------------------------------------------------------
102     /**
103      * Gets the locale.
104      * !(p)
105      * This locale is used to control localization _in the parse except
106      * where localization is controlled by the DecimalStyle.
107      *
108      * @return the locale, not null
109      */
110     Locale getLocale() {
111         return formatter.getLocale();
112     }
113 
114     /**
115      * Gets the DecimalStyle.
116      * !(p)
117      * The DecimalStyle controls the numeric parsing.
118      *
119      * @return the DecimalStyle, not null
120      */
121     DecimalStyle getDecimalStyle() {
122         return formatter.getDecimalStyle();
123     }
124 
125     /**
126      * Gets the effective chronology during parsing.
127      *
128      * @return the effective parsing chronology, not null
129      */
130     Chronology getEffectiveChronology() {
131         Chronology chrono = currentParsed().chrono;
132         if (chrono is null) {
133             chrono = formatter.getChronology();
134             if (chrono is null) {
135                 chrono = IsoChronology.INSTANCE;
136             }
137         }
138         return chrono;
139     }
140 
141     //-----------------------------------------------------------------------
142     /**
143      * Checks if parsing is case sensitive.
144      *
145      * @return true if parsing is case sensitive, false if case insensitive
146      */
147     bool isCaseSensitive() {
148         return caseSensitive;
149     }
150 
151     /**
152      * Sets whether the parsing is case sensitive or not.
153      *
154      * @param caseSensitive  changes the parsing to be case sensitive or not from now on
155      */
156     void setCaseSensitive(bool caseSensitive) {
157         this.caseSensitive = caseSensitive;
158     }
159 
160     //-----------------------------------------------------------------------
161     /**
162      * Helper to compare two {@code CharSequence} instances.
163      * This uses {@link #isCaseSensitive()}.
164      *
165      * @param cs1  the first character sequence, not null
166      * @param offset1  the offset into the first sequence, valid
167      * @param cs2  the second character sequence, not null
168      * @param offset2  the offset into the second sequence, valid
169      * @param length  the length to check, valid
170      * @return true if equal
171      */
172     bool subSequenceEquals(string cs1, int offset1, string cs2, int offset2, int length) {
173         if (offset1 + length > cs1.length || offset2 + length > cs2.length) {
174             return false;
175         }
176         if (isCaseSensitive()) {
177             for (int i = 0; i < length; i++) {
178                 char ch1 = cs1[offset1 + i];
179                 char ch2 = cs2[offset2 + i];
180                 if (ch1 != ch2) {
181                     return false;
182                 }
183             }
184         } else {
185             for (int i = 0; i < length; i++) {
186                 char ch1 = cs1[offset1 + i];
187                 char ch2 = cs2[offset2 + i];
188                 if (ch1 != ch2 && toUpper(ch1) != toUpper(ch2) &&
189                         toLower(ch1) != toLower(ch2)) {
190                     return false;
191                 }
192             }
193         }
194         return true;
195     }
196 
197     /**
198      * Helper to compare two {@code char}.
199      * This uses {@link #isCaseSensitive()}.
200      *
201      * @param ch1  the first character
202      * @param ch2  the second character
203      * @return true if equal
204      */
205     bool charEquals(char ch1, char ch2) {
206         if (isCaseSensitive()) {
207             return ch1 == ch2;
208         }
209         return charEqualsIgnoreCase(ch1, ch2);
210     }
211 
212     /**
213      * Compares two characters ignoring case.
214      *
215      * @param c1  the first
216      * @param c2  the second
217      * @return true if equal
218      */
219     static bool charEqualsIgnoreCase(char c1, char c2) {
220         return c1 == c2 ||
221                 toUpper(c1) == toUpper(c2) ||
222                 toLower(c1) == toLower(c2);
223     }
224 
225     //-----------------------------------------------------------------------
226     /**
227      * Checks if parsing is strict.
228      * !(p)
229      * Strict parsing requires exact matching of the text and sign styles.
230      *
231      * @return true if parsing is strict, false if lenient
232      */
233     bool isStrict() {
234         return strict;
235     }
236 
237     /**
238      * Sets whether parsing is strict or lenient.
239      *
240      * @param strict  changes the parsing to be strict or lenient from now on
241      */
242     void setStrict(bool strict) {
243         this.strict = strict;
244     }
245 
246     //-----------------------------------------------------------------------
247     /**
248      * Starts the parsing of an optional segment of the input.
249      */
250     void startOptional() {
251         parsed.add(currentParsed().copy());
252     }
253 
254     /**
255      * Ends the parsing of an optional segment of the input.
256      *
257      * @param successful  whether the optional segment was successfully parsed
258      */
259     void endOptional(bool successful) {
260         if (successful) {
261             parsed.removeAt(parsed.size() - 2);
262         } else {
263             parsed.removeAt(parsed.size() - 1);
264         }
265     }
266 
267     //-----------------------------------------------------------------------
268     /**
269      * Gets the currently active temporal objects.
270      *
271      * @return the current temporal objects, not null
272      */
273     private Parsed currentParsed() {
274         return parsed.get(parsed.size() - 1);
275     }
276 
277     /**
278      * Gets the unresolved result of the parse.
279      *
280      * @return the result of the parse, not null
281      */
282     Parsed toUnresolved() {
283         return currentParsed();
284     }
285 
286     /**
287      * Gets the resolved result of the parse.
288      *
289      * @return the result of the parse, not null
290      */
291     TemporalAccessor toResolved(ResolverStyle resolverStyle, Set!(TemporalField) resolverFields) {
292         Parsed parsed = currentParsed();
293         parsed.chrono = getEffectiveChronology();
294         parsed.zone = (parsed.zone !is null ? parsed.zone : formatter.getZone());
295         return parsed.resolve(resolverStyle, resolverFields);
296     }
297 
298 
299     //-----------------------------------------------------------------------
300     /**
301      * Gets the first value that was parsed for the specified field.
302      * !(p)
303      * This searches the results of the parse, returning the first value found
304      * for the specified field. No attempt is made to derive a value.
305      * The field may have an _out of range value.
306      * For example, the day-of-month might be set to 50, or the hour to 1000.
307      *
308      * @param field  the field to query from the map, null returns null
309      * @return the value mapped to the specified field, null if field was not parsed
310      */
311     Long getParsed(TemporalField field) {
312         return currentParsed().fieldValues.get(field);
313     }
314 
315     /**
316      * Stores the parsed field.
317      * !(p)
318      * This stores a field-value pair that has been parsed.
319      * The value stored may be _out of range for the field - no checks are performed.
320      *
321      * @param field  the field to set _in the field-value map, not null
322      * @param value  the value to set _in the field-value map
323      * @param errorPos  the position of the field being parsed
324      * @param successPos  the position after the field being parsed
325      * @return the new position
326      */
327     int setParsedField(TemporalField field, long value, int errorPos, int successPos) {
328         assert(field, "field");
329         Long old = currentParsed().fieldValues.put(field, new Long(value));
330         return (old !is null && old.longValue() != value) ? ~errorPos : successPos;
331     }
332 
333     /**
334      * Stores the parsed chronology.
335      * !(p)
336      * This stores the chronology that has been parsed.
337      * No validation is performed other than ensuring it is not null.
338      * !(p)
339      * The list of listeners is copied and cleared so that each
340      * listener is called only once.  A listener can add itself again
341      * if it needs to be notified of future changes.
342      *
343      * @param chrono  the parsed chronology, not null
344      */
345     void setParsed(Chronology chrono) {
346         assert(chrono, "chrono");
347         currentParsed().chrono = chrono;
348         if (chronoListeners !is null && !chronoListeners.isEmpty()) {
349             // @SuppressWarnings({"rawtypes", "unchecked"})
350             Consumer!(Chronology)[] listeners = new Consumer!(Chronology)[1];
351 
352             foreach(c ; chronoListeners)
353                 listeners ~= c;
354             // Consumer!(Chronology)[] listeners = chronoListeners.toArray(tmp);
355             chronoListeners.clear();
356             foreach(Consumer!(Chronology) l ; listeners) {
357                 l(chrono);
358             }
359         }
360     }
361 
362     /**
363      * Adds a Consumer!(Chronology) to the list of listeners to be notified
364      * if the Chronology changes.
365      * @param listener a Consumer!(Chronology) to be called when Chronology changes
366      */
367     void addChronoChangedListener(Consumer!(Chronology) listener) {
368         if (chronoListeners is null) {
369             chronoListeners = new ArrayList!(Consumer!(Chronology))();
370         }
371         chronoListeners.add(listener);
372     }
373 
374     /**
375      * Stores the parsed zone.
376      * !(p)
377      * This stores the zone that has been parsed.
378      * No validation is performed other than ensuring it is not null.
379      *
380      * @param zone  the parsed zone, not null
381      */
382     void setParsed(ZoneId zone) {
383         assert(zone, "zone");
384         currentParsed().zone = zone;
385     }
386 
387     /**
388      * Stores the parsed leap second.
389      */
390     void setParsedLeapSecond() {
391         currentParsed().leapSecond = true;
392     }
393 
394     //-----------------------------------------------------------------------
395     /**
396      * Returns a string version of the context for debugging.
397      *
398      * @return a string representation of the context data, not null
399      */
400     override
401     public string toString() {
402         return currentParsed().toString();
403     }
404 
405 }