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.chrono.AbstractChronology; 13 14 import hunt.time.temporal.ChronoField; 15 import hunt.time.temporal.ChronoUnit; 16 import hunt.time.temporal.TemporalAdjusters; 17 18 import hunt.stream.DataInput; 19 import hunt.stream.DataOutput; 20 21 //import hunt.io.ObjectInputStream; 22 // import hunt.io.ObjectStreamException; 23 import hunt.stream.Common; 24 import hunt.time.Exceptions; 25 import hunt.time.DayOfWeek; 26 import hunt.time.format.ResolverStyle; 27 import hunt.time.temporal.ChronoField; 28 import hunt.time.temporal.TemporalAdjusters; 29 import hunt.time.temporal.TemporalField; 30 import hunt.time.temporal.ValueRange; 31 import hunt.time.util; 32 33 import hunt.time.chrono.IsoChronology; 34 import hunt.time.chrono.Era; 35 import hunt.time.chrono.Ser; 36 import hunt.time.util.Common; 37 import hunt.time.util.ServiceLoader; 38 import hunt.time.chrono.Chronology; 39 import hunt.time.chrono.ChronoLocalDate; 40 41 import hunt.collection.HashSet; 42 import hunt.collection.List; 43 import hunt.collection.Map; 44 import hunt.collection.Set; 45 import hunt.collection.HashMap; 46 import hunt.Exceptions; 47 import hunt.Long; 48 import hunt.logging; 49 import hunt.math.Helper; 50 import hunt.util.Comparator; 51 import hunt.util.Locale; 52 53 import std.conv; 54 import std.concurrency : initOnce; 55 56 public abstract class AbstractChronology : Chronology { 57 58 /** 59 * Map of available calendars by ID. 60 */ 61 private static HashMap!(string, Chronology) CHRONOS_BY_ID() { 62 __gshared HashMap!(string, Chronology) _d; 63 return initOnce!(_d)(new HashMap!(string, Chronology)()); // = new ConcurrentHashMap!(string, Chronology)(); 64 } 65 66 /** 67 * Map of available calendars by calendar type. 68 */ 69 static HashMap!(string, Chronology) CHRONOS_BY_TYPE() { 70 __gshared HashMap!(string, Chronology) _d; 71 return initOnce!(_d)(new HashMap!(string, Chronology)()); // = new ConcurrentHashMap!(string, Chronology)(); 72 } 73 74 /** 75 * Register a Chronology by its ID and type for lookup by {@link #of(string)}. 76 * Chronologies must not be registered until they are completely constructed. 77 * Specifically, not _in the constructor of Chronology. 78 * 79 * @param chrono the chronology to register; not null 80 * @return the already registered Chronology if any, may be null 81 */ 82 static Chronology registerChrono(Chronology chrono) { 83 return registerChrono(chrono, chrono.getId()); 84 } 85 86 /** 87 * Register a Chronology by ID and type for lookup by {@link #of(string)}. 88 * Chronos must not be registered until they are completely constructed. 89 * Specifically, not _in the constructor of Chronology. 90 * 91 * @param chrono the chronology to register; not null 92 * @param id the ID to register the chronology; not null 93 * @return the already registered Chronology if any, may be null 94 */ 95 static Chronology registerChrono(Chronology chrono, string id) { 96 Chronology prev = CHRONOS_BY_ID.putIfAbsent(id, chrono); 97 if (prev is null) { 98 string type = chrono.getCalendarType(); 99 if (type !is null) { 100 CHRONOS_BY_TYPE.putIfAbsent(type, chrono); 101 } 102 } 103 return prev; 104 } 105 106 /** 107 * Initialization of the maps from id and type to Chronology. 108 * The ServiceLoader is used to find and register any implementations 109 * of {@link hunt.time.chrono.AbstractChronology} found _in the bootclass loader. 110 * The built-_in chronologies are registered explicitly. 111 * Calendars configured via the Thread's context classloader are local 112 * to that thread and are ignored. 113 * <p> 114 * The initialization is done only once using the registration 115 * of the IsoChronology as the test and the final step. 116 * Multiple threads may perform the initialization concurrently. 117 * Only the first registration of each Chronology is retained by the 118 * ConcurrentHashMap. 119 * @return true if the cache was initialized 120 */ 121 private static bool initCache() { 122 if (CHRONOS_BY_ID.get("ISO") is null) { 123 // Initialization is incomplete 124 125 // Register built-_in Chronologies 126 ///@gxc 127 // registerChrono(HijrahChronology.INSTANCE); 128 // registerChrono(JapaneseChronology.INSTANCE); 129 // registerChrono(MinguoChronology.INSTANCE); 130 // registerChrono(ThaiBuddhistChronology.INSTANCE); 131 132 // Register Chronologies from the ServiceLoader 133 // @SuppressWarnings("rawtypes") 134 ServiceLoader!(AbstractChronology) loader; 135 foreach( obj ; loader.objs) { 136 AbstractChronology chrono = obj.ctor(); 137 string id = chrono.getId(); 138 if (id == ("ISO") || registerChrono(chrono) !is null) { 139 // Log the attempt to replace an existing Chronology 140 // PlatformLogger logger = PlatformLogger.getLogger("hunt.time.chrono"); 141 version(HUNT_DEBUG) trace("Ignoring duplicate Chronology, from ServiceLoader configuration " ~ id); 142 } 143 } 144 145 // finally, register IsoChronology to mark initialization is complete 146 registerChrono(IsoChronology.INSTANCE); 147 return true; 148 } 149 return false; 150 } 151 152 //----------------------------------------------------------------------- 153 /** 154 * Obtains an instance of {@code Chronology} from a locale. 155 * !(p) 156 * See {@link Chronology#ofLocale(Locale)}. 157 * 158 * @param locale the locale to use to obtain the calendar system, not null 159 * @return the calendar system associated with the locale, not null 160 * @throws hunt.time.Exceptions if the locale-specified calendar cannot be found 161 */ 162 static Chronology ofLocale(Locale locale) { 163 assert(locale, "locale"); 164 string type = locale.getUnicodeLocaleType("ca"); 165 if (type is null || "iso" == (type) || "iso8601" == (type)) { 166 return IsoChronology.INSTANCE; 167 } 168 // Not pre-defined; lookup by the type 169 do { 170 Chronology chrono = CHRONOS_BY_TYPE.get(type); 171 if (chrono !is null) { 172 return chrono; 173 } 174 // If not found, do the initialization (once) and repeat the lookup 175 } while (initCache()); 176 177 // Look for a Chronology using ServiceLoader of the Thread's ContextClassLoader 178 // Application provided Chronologies must not be cached 179 // @SuppressWarnings("rawtypes") 180 ServiceLoader!(AbstractChronology) loader; 181 foreach( obj ; loader.objs) { 182 Chronology chrono = obj.ctor(); 183 if (type == (chrono.getCalendarType())) { 184 return chrono; 185 } 186 } 187 throw new DateTimeException("Unknown calendar system: " ~ type); 188 } 189 190 //----------------------------------------------------------------------- 191 /** 192 * Obtains an instance of {@code Chronology} from a chronology ID or 193 * calendar system type. 194 * !(p) 195 * See {@link Chronology#of(string)}. 196 * 197 * @param id the chronology ID or calendar system type, not null 198 * @return the chronology with the identifier requested, not null 199 * @throws hunt.time.Exceptions if the chronology cannot be found 200 */ 201 static Chronology of(string id) { 202 assert(id, "id"); 203 do { 204 Chronology chrono = of0(id); 205 if (chrono !is null) { 206 return chrono; 207 } 208 // If not found, do the initialization (once) and repeat the lookup 209 } while (initCache()); 210 211 // Look for a Chronology using ServiceLoader of the Thread's ContextClassLoader 212 // Application provided Chronologies must not be cached 213 // @SuppressWarnings("rawtypes") 214 ServiceLoader!(AbstractChronology) loader; 215 foreach( obj ; loader.objs) { 216 Chronology chrono = cast(Chronology)(obj.ctor()); 217 if (id == (chrono.getId()) || id == (chrono.getCalendarType())) { 218 return chrono; 219 } 220 } 221 throw new DateTimeException("Unknown chronology: " ~ id); 222 } 223 224 /** 225 * Obtains an instance of {@code Chronology} from a chronology ID or 226 * calendar system type. 227 * 228 * @param id the chronology ID or calendar system type, not null 229 * @return the chronology with the identifier requested, or {@code null} if not found 230 */ 231 private static Chronology of0(string id) { 232 Chronology chrono = CHRONOS_BY_ID.get(id); 233 if (chrono is null) { 234 chrono = CHRONOS_BY_TYPE.get(id); 235 } 236 return chrono; 237 } 238 239 /** 240 * Returns the available chronologies. 241 * !(p) 242 * Each returned {@code Chronology} is available for use _in the system. 243 * The set of chronologies includes the system chronologies and 244 * any chronologies provided by the application via ServiceLoader 245 * configuration. 246 * 247 * @return the independent, modifiable set of the available chronology IDs, not null 248 */ 249 static Set!(Chronology) getAvailableChronologies() { 250 initCache(); // force initialization 251 HashSet!(Chronology) chronos = new HashSet!(Chronology)(); 252 foreach( value ;CHRONOS_BY_ID.values()) 253 { 254 chronos.add(value); 255 } 256 257 /// Add _in Chronologies from the ServiceLoader configuration 258 // @SuppressWarnings("rawtypes") 259 ServiceLoader!(AbstractChronology) loader; 260 foreach( obj ; loader.objs) { 261 Chronology chrono = obj.ctor(); 262 chronos.add(chrono); 263 } 264 return chronos; 265 } 266 267 //----------------------------------------------------------------------- 268 /** 269 * Creates an instance. 270 */ 271 protected this() { 272 } 273 274 //----------------------------------------------------------------------- 275 /** 276 * Resolves parsed {@code ChronoField} values into a date during parsing. 277 * <p> 278 * Most {@code TemporalField} implementations are resolved using the 279 * resolve method on the field. By contrast, the {@code ChronoField} class 280 * defines fields that only have meaning relative to the chronology. 281 * As such, {@code ChronoField} date fields are resolved here _in the 282 * context of a specific chronology. 283 * <p> 284 * {@code ChronoField} instances are resolved by this method, which may 285 * be overridden _in subclasses. 286 * <ul> 287 * <li>{@code EPOCH_DAY} - If present, this is converted to a date and 288 * all other date fields are then cross-checked against the date. 289 * <li>{@code PROLEPTIC_MONTH} - If present, then it is split into the 290 * {@code YEAR} and {@code MONTH_OF_YEAR}. If the mode is strict or smart 291 * then the field is validated. 292 * <li>{@code YEAR_OF_ERA} and {@code ERA} - If both are present, then they 293 * are combined to form a {@code YEAR}. In lenient mode, the {@code YEAR_OF_ERA} 294 * range is not validated, _in smart and strict mode it is. The {@code ERA} is 295 * validated for range _in all three modes. If only the {@code YEAR_OF_ERA} is 296 * present, and the mode is smart or lenient, then the last available era 297 * is assumed. In strict mode, no era is assumed and the {@code YEAR_OF_ERA} is 298 * left untouched. If only the {@code ERA} is present, then it is left untouched. 299 * <li>{@code YEAR}, {@code MONTH_OF_YEAR} and {@code DAY_OF_MONTH} - 300 * If all three are present, then they are combined to form a date. 301 * In all three modes, the {@code YEAR} is validated. 302 * If the mode is smart or strict, then the month and day are validated. 303 * If the mode is lenient, then the date is combined _in a manner equivalent to 304 * creating a date on the first day of the first month _in the requested year, 305 * then adding the difference _in months, then the difference _in days. 306 * If the mode is smart, and the day-of-month is greater than the maximum for 307 * the year-month, then the day-of-month is adjusted to the last day-of-month. 308 * If the mode is strict, then the three fields must form a valid date. 309 * <li>{@code YEAR} and {@code DAY_OF_YEAR} - 310 * If both are present, then they are combined to form a date. 311 * In all three modes, the {@code YEAR} is validated. 312 * If the mode is lenient, then the date is combined _in a manner equivalent to 313 * creating a date on the first day of the requested year, then adding 314 * the difference _in days. 315 * If the mode is smart or strict, then the two fields must form a valid date. 316 * <li>{@code YEAR}, {@code MONTH_OF_YEAR}, {@code ALIGNED_WEEK_OF_MONTH} and 317 * {@code ALIGNED_DAY_OF_WEEK_IN_MONTH} - 318 * If all four are present, then they are combined to form a date. 319 * In all three modes, the {@code YEAR} is validated. 320 * If the mode is lenient, then the date is combined _in a manner equivalent to 321 * creating a date on the first day of the first month _in the requested year, then adding 322 * the difference _in months, then the difference _in weeks, then _in days. 323 * If the mode is smart or strict, then the all four fields are validated to 324 * their outer ranges. The date is then combined _in a manner equivalent to 325 * creating a date on the first day of the requested year and month, then adding 326 * the amount _in weeks and days to reach their values. If the mode is strict, 327 * the date is additionally validated to check that the day and week adjustment 328 * did not change the month. 329 * <li>{@code YEAR}, {@code MONTH_OF_YEAR}, {@code ALIGNED_WEEK_OF_MONTH} and 330 * {@code DAY_OF_WEEK} - If all four are present, then they are combined to 331 * form a date. The approach is the same as described above for 332 * years, months and weeks _in {@code ALIGNED_DAY_OF_WEEK_IN_MONTH}. 333 * The day-of-week is adjusted as the next or same matching day-of-week once 334 * the years, months and weeks have been handled. 335 * <li>{@code YEAR}, {@code ALIGNED_WEEK_OF_YEAR} and {@code ALIGNED_DAY_OF_WEEK_IN_YEAR} - 336 * If all three are present, then they are combined to form a date. 337 * In all three modes, the {@code YEAR} is validated. 338 * If the mode is lenient, then the date is combined _in a manner equivalent to 339 * creating a date on the first day of the requested year, then adding 340 * the difference _in weeks, then _in days. 341 * If the mode is smart or strict, then the all three fields are validated to 342 * their outer ranges. The date is then combined _in a manner equivalent to 343 * creating a date on the first day of the requested year, then adding 344 * the amount _in weeks and days to reach their values. If the mode is strict, 345 * the date is additionally validated to check that the day and week adjustment 346 * did not change the year. 347 * <li>{@code YEAR}, {@code ALIGNED_WEEK_OF_YEAR} and {@code DAY_OF_WEEK} - 348 * If all three are present, then they are combined to form a date. 349 * The approach is the same as described above for years and weeks _in 350 * {@code ALIGNED_DAY_OF_WEEK_IN_YEAR}. The day-of-week is adjusted as the 351 * next or same matching day-of-week once the years and weeks have been handled. 352 * </ul> 353 * <p> 354 * The default implementation is suitable for most calendar systems. 355 * If {@link hunt.time.temporal.ChronoField#YEAR_OF_ERA} is found without an {@link hunt.time.temporal.ChronoField#ERA} 356 * then the last era _in {@link #eras()} is used. 357 * The implementation assumes a 7 day week, that the first day-of-month 358 * has the value 1, that first day-of-year has the value 1, and that the 359 * first of the month and year always exists. 360 * 361 * @param fieldValues the map of fields to values, which can be updated, not null 362 * @param resolverStyle the requested type of resolve, not null 363 * @return the resolved date, null if insufficient information to create a date 364 * @throws hunt.time.Exceptions if the date cannot be resolved, typically 365 * because of a conflict _in the input data 366 */ 367 override 368 public ChronoLocalDate resolveDate(Map!(TemporalField, Long) fieldValues, ResolverStyle resolverStyle) { 369 // check epoch-day before inventing era 370 if (fieldValues.containsKey(ChronoField.EPOCH_DAY)) { 371 return dateEpochDay(fieldValues.remove(ChronoField.EPOCH_DAY).longValue()); 372 } 373 374 // fix proleptic month before inventing era 375 resolveProlepticMonth(fieldValues, resolverStyle); 376 377 // invent era if necessary to resolve year-of-era 378 ChronoLocalDate resolved = resolveYearOfEra(fieldValues, resolverStyle); 379 if (resolved !is null) { 380 return resolved; 381 } 382 383 // build date 384 if (fieldValues.containsKey(ChronoField.YEAR)) { 385 if (fieldValues.containsKey(ChronoField.MONTH_OF_YEAR)) { 386 if (fieldValues.containsKey(ChronoField.DAY_OF_MONTH)) { 387 return resolveYMD(fieldValues, resolverStyle); 388 } 389 if (fieldValues.containsKey(ChronoField.ALIGNED_WEEK_OF_MONTH)) { 390 if (fieldValues.containsKey(ChronoField.ALIGNED_DAY_OF_WEEK_IN_MONTH)) { 391 return resolveYMAA(fieldValues, resolverStyle); 392 } 393 if (fieldValues.containsKey(ChronoField.DAY_OF_WEEK)) { 394 return resolveYMAD(fieldValues, resolverStyle); 395 } 396 } 397 } 398 if (fieldValues.containsKey(ChronoField.DAY_OF_YEAR)) { 399 return resolveYD(fieldValues, resolverStyle); 400 } 401 if (fieldValues.containsKey(ChronoField.ALIGNED_WEEK_OF_YEAR)) { 402 if (fieldValues.containsKey(ChronoField.ALIGNED_DAY_OF_WEEK_IN_YEAR)) { 403 return resolveYAA(fieldValues, resolverStyle); 404 } 405 if (fieldValues.containsKey(ChronoField.DAY_OF_WEEK)) { 406 return resolveYAD(fieldValues, resolverStyle); 407 } 408 } 409 } 410 return null; 411 } 412 413 void resolveProlepticMonth(Map!(TemporalField, Long) fieldValues, ResolverStyle resolverStyle) { 414 Long pMonth = fieldValues.remove(ChronoField.PROLEPTIC_MONTH); 415 if (pMonth !is null) { 416 if (resolverStyle != ResolverStyle.LENIENT) { 417 ChronoField.PROLEPTIC_MONTH.checkValidValue(pMonth.longValue()); 418 } 419 // first day-of-month is likely to be safest for setting proleptic-month 420 // cannot add to year zero, as not all chronologies have a year zero 421 ChronoLocalDate chronoDate = dateNow() 422 ._with(ChronoField.DAY_OF_MONTH, 1)._with(ChronoField.PROLEPTIC_MONTH, pMonth.longValue()); 423 addFieldValue(fieldValues, ChronoField.MONTH_OF_YEAR, chronoDate.get(ChronoField.MONTH_OF_YEAR)); 424 addFieldValue(fieldValues, ChronoField.YEAR, chronoDate.get(ChronoField.YEAR)); 425 } 426 } 427 428 ChronoLocalDate resolveYearOfEra(Map!(TemporalField, Long) fieldValues, ResolverStyle resolverStyle) { 429 Long yoeLong = fieldValues.remove(ChronoField.YEAR_OF_ERA); 430 if (yoeLong !is null) { 431 Long eraLong = fieldValues.remove(ChronoField.ERA); 432 int yoe; 433 if (resolverStyle != ResolverStyle.LENIENT) { 434 yoe = range(ChronoField.YEAR_OF_ERA).checkValidIntValue(yoeLong.longValue(), ChronoField.YEAR_OF_ERA); 435 } else { 436 yoe = MathHelper.toIntExact(yoeLong.longValue()); 437 } 438 if (eraLong !is null) { 439 Era eraObj = eraOf(range(ChronoField.ERA).checkValidIntValue(eraLong.longValue(), ChronoField.ERA)); 440 addFieldValue(fieldValues, ChronoField.YEAR, prolepticYear(eraObj, yoe)); 441 } else { 442 if (fieldValues.containsKey(ChronoField.YEAR)) { 443 int year = range(ChronoField.YEAR).checkValidIntValue(fieldValues.get(ChronoField.YEAR).longValue(), ChronoField.YEAR); 444 ChronoLocalDate chronoDate = dateYearDay(year, 1); 445 addFieldValue(fieldValues, ChronoField.YEAR, prolepticYear(chronoDate.getEra(), yoe)); 446 } else if (resolverStyle == ResolverStyle.STRICT) { 447 // do not invent era if strict 448 // reinstate the field removed earlier, no cross-check issues 449 fieldValues.put(ChronoField.YEAR_OF_ERA, yoeLong); 450 } else { 451 List!(Era) eras = eras(); 452 if (eras.isEmpty()) { 453 addFieldValue(fieldValues, ChronoField.YEAR, yoe); 454 } else { 455 Era eraObj = eras.get(eras.size() - 1); 456 addFieldValue(fieldValues, ChronoField.YEAR, prolepticYear(eraObj, yoe)); 457 } 458 } 459 } 460 } else if (fieldValues.containsKey(ChronoField.ERA)) { 461 range(ChronoField.ERA).checkValidValue(fieldValues.get(ChronoField.ERA).longValue(), ChronoField.ERA); // always validated 462 } 463 return null; 464 } 465 466 ChronoLocalDate resolveYMD(Map!(TemporalField, Long) fieldValues, ResolverStyle resolverStyle) { 467 int y = range(ChronoField.YEAR).checkValidIntValue(fieldValues.remove(ChronoField.YEAR).longValue(), ChronoField.YEAR); 468 if (resolverStyle == ResolverStyle.LENIENT) { 469 long months = MathHelper.subtractExact(fieldValues.remove(ChronoField.MONTH_OF_YEAR).longValue(), 1); 470 long days = MathHelper.subtractExact(fieldValues.remove(ChronoField.DAY_OF_MONTH).longValue(), 1); 471 return date(y, 1, 1).plus(months, ChronoUnit.MONTHS).plus(days, ChronoUnit.DAYS); 472 } 473 int moy = range(ChronoField.MONTH_OF_YEAR).checkValidIntValue(fieldValues.remove(ChronoField.MONTH_OF_YEAR).longValue(), ChronoField.MONTH_OF_YEAR); 474 ValueRange domRange = range(ChronoField.DAY_OF_MONTH); 475 int dom = domRange.checkValidIntValue(fieldValues.remove(ChronoField.DAY_OF_MONTH).longValue(), ChronoField.DAY_OF_MONTH); 476 if (resolverStyle == ResolverStyle.SMART) { // previous valid 477 try { 478 return date(y, moy, dom); 479 } catch (DateTimeException ex) { 480 return date(y, moy, 1)._with(TemporalAdjusters.lastDayOfMonth()); 481 } 482 } 483 return date(y, moy, dom); 484 } 485 486 ChronoLocalDate resolveYD(Map!(TemporalField, Long) fieldValues, ResolverStyle resolverStyle) { 487 int y = range(ChronoField.YEAR).checkValidIntValue(fieldValues.remove(ChronoField.YEAR).longValue(), ChronoField.YEAR); 488 if (resolverStyle == ResolverStyle.LENIENT) { 489 long days = MathHelper.subtractExact(fieldValues.remove(ChronoField.DAY_OF_YEAR).longValue(), 1); 490 return dateYearDay(y, 1).plus(days, ChronoUnit.DAYS); 491 } 492 int doy = range(ChronoField.DAY_OF_YEAR).checkValidIntValue(fieldValues.remove(ChronoField.DAY_OF_YEAR).longValue(), ChronoField.DAY_OF_YEAR); 493 return dateYearDay(y, doy); // smart is same as strict 494 } 495 496 ChronoLocalDate resolveYMAA(Map!(TemporalField, Long) fieldValues, ResolverStyle resolverStyle) { 497 int y = range(ChronoField.YEAR).checkValidIntValue(fieldValues.remove(ChronoField.YEAR).longValue(), ChronoField.YEAR); 498 if (resolverStyle == ResolverStyle.LENIENT) { 499 long months = MathHelper.subtractExact(fieldValues.remove(ChronoField.MONTH_OF_YEAR).longValue(), 1); 500 long weeks = MathHelper.subtractExact(fieldValues.remove(ChronoField.ALIGNED_WEEK_OF_MONTH).longValue(), 1); 501 long days = MathHelper.subtractExact(fieldValues.remove(ChronoField.ALIGNED_DAY_OF_WEEK_IN_MONTH).longValue(), 1); 502 return date(y, 1, 1).plus(months, ChronoUnit.MONTHS).plus(weeks, ChronoUnit.WEEKS).plus(days, ChronoUnit.DAYS); 503 } 504 int moy = range(ChronoField.MONTH_OF_YEAR).checkValidIntValue(fieldValues.remove(ChronoField.MONTH_OF_YEAR).longValue(), ChronoField.MONTH_OF_YEAR); 505 int aw = range(ChronoField.ALIGNED_WEEK_OF_MONTH).checkValidIntValue(fieldValues.remove(ChronoField.ALIGNED_WEEK_OF_MONTH).longValue(), ChronoField.ALIGNED_WEEK_OF_MONTH); 506 int ad = range(ChronoField.ALIGNED_DAY_OF_WEEK_IN_MONTH).checkValidIntValue(fieldValues.remove(ChronoField.ALIGNED_DAY_OF_WEEK_IN_MONTH).longValue(), ChronoField.ALIGNED_DAY_OF_WEEK_IN_MONTH); 507 ChronoLocalDate date = date(y, moy, 1).plus((aw - 1) * 7 + (ad - 1), ChronoUnit.DAYS); 508 if (resolverStyle == ResolverStyle.STRICT && date.get(ChronoField.MONTH_OF_YEAR) != moy) { 509 throw new DateTimeException("Strict mode rejected resolved date as it is _in a different month"); 510 } 511 return date; 512 } 513 514 ChronoLocalDate resolveYMAD(Map!(TemporalField, Long) fieldValues, ResolverStyle resolverStyle) { 515 int y = range(ChronoField.YEAR).checkValidIntValue(fieldValues.remove(ChronoField.YEAR).longValue(), ChronoField.YEAR); 516 if (resolverStyle == ResolverStyle.LENIENT) { 517 long months = MathHelper.subtractExact(fieldValues.remove(ChronoField.MONTH_OF_YEAR).longValue(), 1); 518 long weeks = MathHelper.subtractExact(fieldValues.remove(ChronoField.ALIGNED_WEEK_OF_MONTH).longValue(), 1); 519 long dow = MathHelper.subtractExact(fieldValues.remove(ChronoField.DAY_OF_WEEK).longValue(), 1); 520 return resolveAligned(date(y, 1, 1), months, weeks, dow); 521 } 522 int moy = range(ChronoField.MONTH_OF_YEAR).checkValidIntValue(fieldValues.remove(ChronoField.MONTH_OF_YEAR).longValue(), ChronoField.MONTH_OF_YEAR); 523 int aw = range(ChronoField.ALIGNED_WEEK_OF_MONTH).checkValidIntValue(fieldValues.remove(ChronoField.ALIGNED_WEEK_OF_MONTH).longValue(), ChronoField.ALIGNED_WEEK_OF_MONTH); 524 int dow = range(ChronoField.DAY_OF_WEEK).checkValidIntValue(fieldValues.remove(ChronoField.DAY_OF_WEEK).longValue(), ChronoField.DAY_OF_WEEK); 525 ChronoLocalDate date = date(y, moy, 1).plus((aw - 1) * 7, ChronoUnit.DAYS)._with(TemporalAdjusters.nextOrSame(DayOfWeek.of(dow))); 526 if (resolverStyle == ResolverStyle.STRICT && date.get(ChronoField.MONTH_OF_YEAR) != moy) { 527 throw new DateTimeException("Strict mode rejected resolved date as it is _in a different month"); 528 } 529 return date; 530 } 531 532 ChronoLocalDate resolveYAA(Map!(TemporalField, Long) fieldValues, ResolverStyle resolverStyle) { 533 int y = range(ChronoField.YEAR).checkValidIntValue(fieldValues.remove(ChronoField.YEAR).longValue(), ChronoField.YEAR); 534 if (resolverStyle == ResolverStyle.LENIENT) { 535 long weeks = MathHelper.subtractExact(fieldValues.remove(ChronoField.ALIGNED_WEEK_OF_YEAR).longValue(), 1); 536 long days = MathHelper.subtractExact(fieldValues.remove(ChronoField.ALIGNED_DAY_OF_WEEK_IN_YEAR).longValue(), 1); 537 return dateYearDay(y, 1).plus(weeks, ChronoUnit.WEEKS).plus(days, ChronoUnit.DAYS); 538 } 539 int aw = range(ChronoField.ALIGNED_WEEK_OF_YEAR).checkValidIntValue(fieldValues.remove(ChronoField.ALIGNED_WEEK_OF_YEAR).longValue(), ChronoField.ALIGNED_WEEK_OF_YEAR); 540 int ad = range(ChronoField.ALIGNED_DAY_OF_WEEK_IN_YEAR).checkValidIntValue(fieldValues.remove(ChronoField.ALIGNED_DAY_OF_WEEK_IN_YEAR).longValue(), ChronoField.ALIGNED_DAY_OF_WEEK_IN_YEAR); 541 ChronoLocalDate date = dateYearDay(y, 1).plus((aw - 1) * 7 + (ad - 1), ChronoUnit.DAYS); 542 if (resolverStyle == ResolverStyle.STRICT && date.get(ChronoField.YEAR) != y) { 543 throw new DateTimeException("Strict mode rejected resolved date as it is _in a different year"); 544 } 545 return date; 546 } 547 548 ChronoLocalDate resolveYAD(Map!(TemporalField, Long) fieldValues, ResolverStyle resolverStyle) { 549 int y = range(ChronoField.YEAR).checkValidIntValue(fieldValues.remove(ChronoField.YEAR).longValue(), ChronoField.YEAR); 550 if (resolverStyle == ResolverStyle.LENIENT) { 551 long weeks = MathHelper.subtractExact(fieldValues.remove(ChronoField.ALIGNED_WEEK_OF_YEAR).longValue(), 1); 552 long dow = MathHelper.subtractExact(fieldValues.remove(ChronoField.DAY_OF_WEEK).longValue(), 1); 553 return resolveAligned(dateYearDay(y, 1), 0, weeks, dow); 554 } 555 int aw = range(ChronoField.ALIGNED_WEEK_OF_YEAR).checkValidIntValue(fieldValues.remove(ChronoField.ALIGNED_WEEK_OF_YEAR).longValue(), ChronoField.ALIGNED_WEEK_OF_YEAR); 556 int dow = range(ChronoField.DAY_OF_WEEK).checkValidIntValue(fieldValues.remove(ChronoField.DAY_OF_WEEK).longValue(), ChronoField.DAY_OF_WEEK); 557 ChronoLocalDate date = dateYearDay(y, 1).plus((aw - 1) * 7, ChronoUnit.DAYS)._with(TemporalAdjusters.nextOrSame(DayOfWeek.of(dow))); 558 if (resolverStyle == ResolverStyle.STRICT && date.get(ChronoField.YEAR) != y) { 559 throw new DateTimeException("Strict mode rejected resolved date as it is _in a different year"); 560 } 561 return date; 562 } 563 564 ChronoLocalDate resolveAligned(ChronoLocalDate base, long months, long weeks, long dow) { 565 ChronoLocalDate date = base.plus(months, ChronoUnit.MONTHS).plus(weeks, ChronoUnit.WEEKS); 566 if (dow > 7) { 567 date = date.plus((dow - 1) / 7, ChronoUnit.WEEKS); 568 dow = ((dow - 1) % 7) + 1; 569 } else if (dow < 1) { 570 date = date.plus(MathHelper.subtractExact(dow, 7) / 7, ChronoUnit.WEEKS); 571 dow = ((dow + 6) % 7) + 1; 572 } 573 return date._with(TemporalAdjusters.nextOrSame(DayOfWeek.of(cast(int) dow))); 574 } 575 576 /** 577 * Adds a field-value pair to the map, checking for conflicts. 578 * !(p) 579 * If the field is not already present, then the field-value pair is added to the map. 580 * If the field is already present and it has the same value as that specified, no action occurs. 581 * If the field is already present and it has a different value to that specified, then 582 * an exception is thrown. 583 * 584 * @param field the field to add, not null 585 * @param value the value to add, not null 586 * @throws hunt.time.Exceptions if the field is already present with a different value 587 */ 588 void addFieldValue(Map!(TemporalField, Long) fieldValues, ChronoField field, long value) { 589 Long old = fieldValues.get(field); // check first for better error message 590 if (old !is null && old.longValue() != value) { 591 throw new DateTimeException("Conflict found: " ~ typeid(field).name ~ " " ~ old.to!string ~ " differs from " ~ typeid(field).name ~ " " ~ value.to!string); 592 } 593 fieldValues.put(field, new Long(value)); 594 } 595 596 //----------------------------------------------------------------------- 597 /** 598 * Compares this chronology to another chronology. 599 * !(p) 600 * The comparison order first by the chronology ID string, then by any 601 * additional information specific to the subclass. 602 * It is "consistent with equals", as defined by {@link Comparable}. 603 * 604 * @implSpec 605 * This implementation compares the chronology ID. 606 * Subclasses must compare any additional state that they store. 607 * 608 * @param other the other chronology to compare to, not null 609 * @return the comparator value, negative if less, positive if greater 610 */ 611 override 612 public int compareTo(Chronology other) { 613 return getId().compare(other.getId()); 614 } 615 616 /** 617 * Checks if this chronology is equal to another chronology. 618 * <p> 619 * The comparison is based on the entire state of the object. 620 * 621 * @implSpec 622 * This implementation checks the type and calls 623 * {@link #compareTo(hunt.time.chrono.Chronology)}. 624 * 625 * @param obj the object to check, null returns false 626 * @return true if this is equal to the other chronology 627 */ 628 override 629 public bool opEquals(Object obj) { 630 if (this is obj) { 631 return true; 632 } 633 if (cast(AbstractChronology)(obj) !is null) { 634 return compareTo(cast(AbstractChronology) obj) == 0; 635 } 636 return false; 637 } 638 639 640 /** 641 * A hash code for this chronology. 642 * <p> 643 * The hash code should be based on the entire state of the object. 644 * 645 * @implSpec 646 * This implementation is based on the chronology ID and class. 647 * Subclasses should add any additional state that they store. 648 * 649 * @return a suitable hash code 650 */ 651 override 652 public size_t toHash() @trusted nothrow { 653 try 654 { 655 return hashOf(typeid(this).name) ^ hashOf(getId()); 656 } 657 catch(Exception e){} 658 return int.init; 659 } 660 661 //----------------------------------------------------------------------- 662 /** 663 * Outputs this chronology as a {@code string}, using the chronology ID. 664 * 665 * @return a string representation of this chronology, not null 666 */ 667 override 668 public string toString() { 669 return getId(); 670 } 671 672 //----------------------------------------------------------------------- 673 /** 674 * Writes the Chronology using a 675 * <a href="{@docRoot}/serialized-form.html#hunt.time.chrono.Ser">dedicated serialized form</a>. 676 * <pre> 677 * _out.writeByte(1); // identifies this as a Chronology 678 * _out.writeUTF(getId()); 679 * </pre> 680 * 681 * @return the instance of {@code Ser}, not null 682 */ 683 Object writeReplace() { 684 return new Ser(Ser.CHRONO_TYPE, this); 685 } 686 687 /** 688 * Defend against malicious streams. 689 * 690 * @param s the stream to read 691 * @throws java.io.InvalidObjectException always 692 */ 693 ///@gxc 694 // private void readObject(ObjectInputStream s) /*throws ObjectStreamException*/ { 695 // throw new InvalidObjectException("Deserialization via serialization delegate"); 696 // } 697 698 void writeExternal(DataOutput _out) /*throws IOException*/ { 699 _out.writeUTF(getId()); 700 } 701 702 static Chronology readExternal(DataInput _in) /*throws IOException*/ { 703 string id = _in.readUTF(); 704 return Chronology.of(id); 705 } 706 707 }