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 }