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.ChronoLocalDateImpl; 13 14 import hunt.time.temporal.ChronoField; 15 import hunt.Exceptions; 16 import hunt.stream.Common; 17 import hunt.time.Exceptions; 18 import hunt.time.temporal.ChronoUnit; 19 import hunt.time.temporal.Temporal; 20 import hunt.time.temporal.TemporalAdjuster; 21 import hunt.time.temporal.TemporalAmount; 22 import hunt.time.temporal.TemporalField; 23 import hunt.time.temporal.TemporalUnit; 24 import hunt.time.Exceptions; 25 import hunt.time.temporal.ValueRange; 26 import hunt.time.chrono.ChronoLocalDate; 27 import hunt.time.chrono.Chronology; 28 import hunt.Long; 29 import hunt.math.Helper; 30 import std.conv; 31 import hunt.util.StringBuilder; 32 /** 33 * A date expressed _in terms of a standard year-month-day calendar system. 34 * !(p) 35 * This class is used by applications seeking to handle dates _in non-ISO calendar systems. 36 * For example, the Japanese, Minguo, Thai Buddhist and others. 37 * !(p) 38 * {@code ChronoLocalDate} is built on the generic concepts of year, month and day. 39 * The calendar system, represented by a {@link hunt.time.chrono.Chronology}, expresses the relationship between 40 * the fields and this class allows the resulting date to be manipulated. 41 * !(p) 42 * Note that not all calendar systems are suitable for use with this class. 43 * For example, the Mayan calendar uses a system that bears no relation to years, months and days. 44 * !(p) 45 * The API design encourages the use of {@code LocalDate} for the majority of the application. 46 * This includes code to read and write from a persistent data store, such as a database, 47 * and to send dates and times across a network. The {@code ChronoLocalDate} instance is then used 48 * at the user interface level to deal with localized input/output. 49 * 50 * !(P)Example: </p> 51 * !(pre) 52 * System._out.printf("Example()%n"); 53 * // Enumerate the list of available calendars and print today for each 54 * Set<Chronology> chronos = Chronology.getAvailableChronologies(); 55 * foreach(Chronology chrono ; chronos) { 56 * ChronoLocalDate date = chrono.dateNow(); 57 * System._out.printf(" %20s: %s%n", chrono.getID(), date.toString()); 58 * } 59 * 60 * // Print the Hijrah date and calendar 61 * ChronoLocalDate date = Chronology.of("Hijrah").dateNow(); 62 * int day = date.get(ChronoField.DAY_OF_MONTH); 63 * int dow = date.get(ChronoField.DAY_OF_WEEK); 64 * int month = date.get(ChronoField.MONTH_OF_YEAR); 65 * int year = date.get(ChronoField.YEAR); 66 * System._out.printf(" Today is %s %s %d-%s-%d%n", date.getChronology().getID(), 67 * dow, day, month, year); 68 * 69 * // Print today's date and the last day of the year 70 * ChronoLocalDate now1 = Chronology.of("Hijrah").dateNow(); 71 * ChronoLocalDate first = now1._with(ChronoField.DAY_OF_MONTH, 1) 72 * ._with(ChronoField.MONTH_OF_YEAR, 1); 73 * ChronoLocalDate last = first.plus(1, ChronoUnit.YEARS) 74 * .minus(1, ChronoUnit.DAYS); 75 * System._out.printf(" Today is %s: start: %s; end: %s%n", last.getChronology().getID(), 76 * first, last); 77 * </pre> 78 * 79 * !(h3)Adding Calendars</h3> 80 * !(p) The set of calendars is extensible by defining a subclass of {@link ChronoLocalDate} 81 * to represent a date instance and an implementation of {@code Chronology} 82 * to be the factory for the ChronoLocalDate subclass. 83 * </p> 84 * !(p) To permit the discovery of the additional calendar types the implementation of 85 * {@code Chronology} must be registered as a Service implementing the {@code Chronology} interface 86 * _in the {@code META-INF/Services} file as per the specification of {@link java.util.ServiceLoader}. 87 * The subclass must function according to the {@code Chronology} class description and must provide its 88 * {@link hunt.time.chrono.Chronology#getId() chronlogy ID} and {@link Chronology#getCalendarType() calendar type}. </p> 89 * 90 * @implSpec 91 * This abstract class must be implemented with care to ensure other classes operate correctly. 92 * All implementations that can be instantiated must be final, immutable and thread-safe. 93 * Subclasses should be Serializable wherever possible. 94 * 95 * @param !(D) the ChronoLocalDate of this date-time 96 * @since 1.8 97 */ 98 abstract class ChronoLocalDateImpl(D = ChronoLocalDate) if(is(D : ChronoLocalDate)) 99 : ChronoLocalDate, Temporal, TemporalAdjuster { //, Serializable 100 101 102 /** 103 * Casts the {@code Temporal} to {@code ChronoLocalDate} ensuring it bas the specified chronology. 104 * 105 * @param chrono the chronology to check for, not null 106 * @param temporal a date-time to cast, not null 107 * @return the date-time checked and cast to {@code ChronoLocalDate}, not null 108 * @throws ClassCastException if the date-time cannot be cast to ChronoLocalDate 109 * or the chronology is not equal this Chronology 110 */ 111 static D ensureValid(D)(Chronology chrono, Temporal temporal) { 112 /*@SuppressWarnings("unchecked")*/ 113 D other = cast(D)temporal; 114 if ((chrono == other.getChronology()) == false) { 115 throw new ClassCastException("Chronology mismatch, expected: " ~ chrono.getId() ~ ", actual: " ~ other.getChronology().getId()); 116 } 117 return other; 118 } 119 120 //----------------------------------------------------------------------- 121 /** 122 * Creates an instance. 123 */ 124 this() { 125 } 126 127 override 128 /*@SuppressWarnings("unchecked")*/ 129 public D _with(TemporalAdjuster adjuster) { 130 return cast(D) /* ChronoLocalDate. super.*/super_with(adjuster); 131 } 132 ChronoLocalDate super_with(TemporalAdjuster adjuster) { 133 return ChronoLocalDateImpl!D.ensureValid!D(getChronology(), adjuster.adjustInto(this)); 134 } 135 136 override 137 /*@SuppressWarnings("unchecked")*/ 138 public D _with(TemporalField field, long value) { 139 return cast(D) /* ChronoLocalDate. super.*/super_with(field, value); 140 } 141 ChronoLocalDate super_with(TemporalField field, long newValue) { 142 if (cast(ChronoField)(field) !is null) { 143 throw new UnsupportedTemporalTypeException("Unsupported field: " ~ typeid(field).name); 144 } 145 return ChronoLocalDateImpl!D.ensureValid!D(getChronology(), field.adjustInto(this, newValue)); 146 } 147 148 //----------------------------------------------------------------------- 149 override 150 /*@SuppressWarnings("unchecked")*/ 151 public D plus(TemporalAmount amount) { 152 return cast(D) /* ChronoLocalDate.super. */super_plus(amount); 153 } 154 ChronoLocalDate super_plus(TemporalAmount amount) { 155 return ChronoLocalDateImpl!D.ensureValid!D(getChronology(),amount.addTo(this)); 156 } 157 //----------------------------------------------------------------------- 158 override 159 /*@SuppressWarnings("unchecked")*/ 160 public D plus(long amountToAdd, TemporalUnit unit) { 161 if (cast(ChronoUnit)(unit) !is null) { 162 ChronoUnit f = cast(ChronoUnit) unit; 163 { 164 if( f == ChronoUnit.DAYS) return plusDays(amountToAdd); 165 if( f == ChronoUnit.WEEKS) return plusDays(MathHelper.multiplyExact(amountToAdd, 7)); 166 if( f == ChronoUnit.MONTHS) return plusMonths(amountToAdd); 167 if( f == ChronoUnit.YEARS) return plusYears(amountToAdd); 168 if( f == ChronoUnit.DECADES) return plusYears(MathHelper.multiplyExact(amountToAdd, 10)); 169 if( f == ChronoUnit.CENTURIES) return plusYears(MathHelper.multiplyExact(amountToAdd, 100)); 170 if( f == ChronoUnit.MILLENNIA) return plusYears(MathHelper.multiplyExact(amountToAdd, 1000)); 171 if( f == ChronoUnit.ERAS) return _with(ChronoField.ERA, MathHelper.addExact(getLong(ChronoField.ERA), amountToAdd)); 172 } 173 throw new UnsupportedTemporalTypeException("Unsupported unit: " ~ f.toString); 174 } 175 return cast(D) /* ChronoLocalDate. super.*/super_plus(amountToAdd, unit); 176 } 177 ChronoLocalDate super_plus(long amountToAdd, TemporalUnit unit) { 178 if (cast(ChronoUnit)(unit) !is null) { 179 throw new UnsupportedTemporalTypeException("Unsupported unit: " ~ typeid(unit).name); 180 } 181 return ChronoLocalDateImpl!D.ensureValid!D(getChronology(), unit.addTo(this, amountToAdd)); 182 } 183 184 override 185 /*@SuppressWarnings("unchecked")*/ 186 public D minus(TemporalAmount amount) { 187 return cast(D) /* ChronoLocalDate. */super_minus(amount); 188 } 189 190 ChronoLocalDate super_minus(TemporalAmount amount) { 191 return ChronoLocalDateImpl!D.ensureValid!D(getChronology(), amount.subtractFrom(this)); 192 } 193 194 override 195 /*@SuppressWarnings("unchecked")*/ 196 public D minus(long amountToSubtract, TemporalUnit unit) { 197 return cast(D) /* ChronoLocalDate. */super_minus(amountToSubtract, unit); 198 } 199 ChronoLocalDate super_minus(long amountToSubtract, TemporalUnit unit) { 200 return ChronoLocalDateImpl!D.ensureValid!D(getChronology(), (amountToSubtract == Long.MIN_VALUE ? plus(Long.MAX_VALUE, unit).plus(1, unit) : plus(-amountToSubtract, unit))); 201 } 202 203 //----------------------------------------------------------------------- 204 /** 205 * Returns a copy of this date with the specified number of years added. 206 * !(p) 207 * This adds the specified period _in years to the date. 208 * In some cases, adding years can cause the resulting date to become invalid. 209 * If this occurs, then other fields, typically the day-of-month, will be adjusted to ensure 210 * that the result is valid. Typically this will select the last valid day of the month. 211 * !(p) 212 * This instance is immutable and unaffected by this method call. 213 * 214 * @param yearsToAdd the years to add, may be negative 215 * @return a date based on this one with the years added, not null 216 * @throws DateTimeException if the result exceeds the supported date range 217 */ 218 abstract D plusYears(long yearsToAdd); 219 220 /** 221 * Returns a copy of this date with the specified number of months added. 222 * !(p) 223 * This adds the specified period _in months to the date. 224 * In some cases, adding months can cause the resulting date to become invalid. 225 * If this occurs, then other fields, typically the day-of-month, will be adjusted to ensure 226 * that the result is valid. Typically this will select the last valid day of the month. 227 * !(p) 228 * This instance is immutable and unaffected by this method call. 229 * 230 * @param monthsToAdd the months to add, may be negative 231 * @return a date based on this one with the months added, not null 232 * @throws DateTimeException if the result exceeds the supported date range 233 */ 234 abstract D plusMonths(long monthsToAdd); 235 236 /** 237 * Returns a copy of this date with the specified number of weeks added. 238 * !(p) 239 * This adds the specified period _in weeks to the date. 240 * In some cases, adding weeks can cause the resulting date to become invalid. 241 * If this occurs, then other fields will be adjusted to ensure that the result is valid. 242 * !(p) 243 * The default implementation uses {@link #plusDays(long)} using a 7 day week. 244 * !(p) 245 * This instance is immutable and unaffected by this method call. 246 * 247 * @param weeksToAdd the weeks to add, may be negative 248 * @return a date based on this one with the weeks added, not null 249 * @throws DateTimeException if the result exceeds the supported date range 250 */ 251 D plusWeeks(long weeksToAdd) { 252 return plusDays(MathHelper.multiplyExact(weeksToAdd, 7)); 253 } 254 255 /** 256 * Returns a copy of this date with the specified number of days added. 257 * !(p) 258 * This adds the specified period _in days to the date. 259 * !(p) 260 * This instance is immutable and unaffected by this method call. 261 * 262 * @param daysToAdd the days to add, may be negative 263 * @return a date based on this one with the days added, not null 264 * @throws DateTimeException if the result exceeds the supported date range 265 */ 266 abstract D plusDays(long daysToAdd); 267 268 //----------------------------------------------------------------------- 269 /** 270 * Returns a copy of this date with the specified number of years subtracted. 271 * !(p) 272 * This subtracts the specified period _in years to the date. 273 * In some cases, subtracting years can cause the resulting date to become invalid. 274 * If this occurs, then other fields, typically the day-of-month, will be adjusted to ensure 275 * that the result is valid. Typically this will select the last valid day of the month. 276 * !(p) 277 * The default implementation uses {@link #plusYears(long)}. 278 * !(p) 279 * This instance is immutable and unaffected by this method call. 280 * 281 * @param yearsToSubtract the years to subtract, may be negative 282 * @return a date based on this one with the years subtracted, not null 283 * @throws DateTimeException if the result exceeds the supported date range 284 */ 285 /*@SuppressWarnings("unchecked")*/ 286 D minusYears(long yearsToSubtract) { 287 return (yearsToSubtract == Long.MIN_VALUE ? (cast(ChronoLocalDateImpl!(D))plusYears(Long.MAX_VALUE)).plusYears(1) : plusYears(-yearsToSubtract)); 288 } 289 290 /** 291 * Returns a copy of this date with the specified number of months subtracted. 292 * !(p) 293 * This subtracts the specified period _in months to the date. 294 * In some cases, subtracting months can cause the resulting date to become invalid. 295 * If this occurs, then other fields, typically the day-of-month, will be adjusted to ensure 296 * that the result is valid. Typically this will select the last valid day of the month. 297 * !(p) 298 * The default implementation uses {@link #plusMonths(long)}. 299 * !(p) 300 * This instance is immutable and unaffected by this method call. 301 * 302 * @param monthsToSubtract the months to subtract, may be negative 303 * @return a date based on this one with the months subtracted, not null 304 * @throws DateTimeException if the result exceeds the supported date range 305 */ 306 /*@SuppressWarnings("unchecked")*/ 307 D minusMonths(long monthsToSubtract) { 308 return (monthsToSubtract == Long.MIN_VALUE ? (cast(ChronoLocalDateImpl!(D))plusMonths(Long.MAX_VALUE)).plusMonths(1) : plusMonths(-monthsToSubtract)); 309 } 310 311 /** 312 * Returns a copy of this date with the specified number of weeks subtracted. 313 * !(p) 314 * This subtracts the specified period _in weeks to the date. 315 * In some cases, subtracting weeks can cause the resulting date to become invalid. 316 * If this occurs, then other fields will be adjusted to ensure that the result is valid. 317 * !(p) 318 * The default implementation uses {@link #plusWeeks(long)}. 319 * !(p) 320 * This instance is immutable and unaffected by this method call. 321 * 322 * @param weeksToSubtract the weeks to subtract, may be negative 323 * @return a date based on this one with the weeks subtracted, not null 324 * @throws DateTimeException if the result exceeds the supported date range 325 */ 326 /*@SuppressWarnings("unchecked")*/ 327 D minusWeeks(long weeksToSubtract) { 328 return (weeksToSubtract == Long.MIN_VALUE ? (cast(ChronoLocalDateImpl!(D))plusWeeks(Long.MAX_VALUE)).plusWeeks(1) : plusWeeks(-weeksToSubtract)); 329 } 330 331 /** 332 * Returns a copy of this date with the specified number of days subtracted. 333 * !(p) 334 * This subtracts the specified period _in days to the date. 335 * !(p) 336 * The default implementation uses {@link #plusDays(long)}. 337 * !(p) 338 * This instance is immutable and unaffected by this method call. 339 * 340 * @param daysToSubtract the days to subtract, may be negative 341 * @return a date based on this one with the days subtracted, not null 342 * @throws DateTimeException if the result exceeds the supported date range 343 */ 344 /*@SuppressWarnings("unchecked")*/ 345 D minusDays(long daysToSubtract) { 346 return (daysToSubtract == Long.MIN_VALUE ? (cast(ChronoLocalDateImpl!(D))plusDays(Long.MAX_VALUE)).plusDays(1) : plusDays(-daysToSubtract)); 347 } 348 349 //----------------------------------------------------------------------- 350 override 351 public long until(Temporal endExclusive, TemporalUnit unit) { 352 assert(endExclusive, "endExclusive"); 353 ChronoLocalDate end = getChronology().date(endExclusive); 354 if (cast(ChronoUnit)(unit) !is null) { 355 auto f = cast(ChronoUnit) unit; 356 { 357 if( f == ChronoUnit.DAYS) return daysUntil(end); 358 if( f == ChronoUnit.WEEKS) return daysUntil(end) / 7; 359 if( f == ChronoUnit.MONTHS) return monthsUntil(end); 360 if( f == ChronoUnit.YEARS) return monthsUntil(end) / 12; 361 if( f == ChronoUnit.DECADES) return monthsUntil(end) / 120; 362 if( f == ChronoUnit.CENTURIES) return monthsUntil(end) / 1200; 363 if( f == ChronoUnit.MILLENNIA) return monthsUntil(end) / 12000; 364 if( f == ChronoUnit.ERAS) return end.getLong(ChronoField.ERA) - getLong(ChronoField.ERA); 365 } 366 throw new UnsupportedTemporalTypeException("Unsupported unit: " ~ f.toString); 367 } 368 assert(unit, "unit"); 369 return unit.between(this, end); 370 } 371 372 private long daysUntil(ChronoLocalDate end) { 373 return end.toEpochDay() - toEpochDay(); // no overflow 374 } 375 376 private long monthsUntil(ChronoLocalDate end) { 377 ValueRange range = getChronology().range(ChronoField.MONTH_OF_YEAR); 378 if (range.getMaximum() != 12) { 379 throw new IllegalStateException("ChronoLocalDateImpl only supports Chronologies with 12 months per year"); 380 } 381 long packed1 = getLong(ChronoField.PROLEPTIC_MONTH) * 32L + get(ChronoField.DAY_OF_MONTH); // no overflow 382 long packed2 = end.getLong(ChronoField.PROLEPTIC_MONTH) * 32L + end.get(ChronoField.DAY_OF_MONTH); // no overflow 383 return (packed2 - packed1) / 32; 384 } 385 386 override 387 public bool opEquals(Object obj) { 388 if (this is obj) { 389 return true; 390 } 391 if (cast(ChronoLocalDate)(obj) !is null) { 392 return compareTo(cast(ChronoLocalDate) obj) == 0; 393 } 394 return false; 395 } 396 397 override 398 public size_t toHash() @trusted nothrow { 399 try{ 400 long epDay = toEpochDay(); 401 return getChronology().toHash() ^ (cast(int) (epDay ^ (epDay >>> 32))); 402 }catch(Exception e){} 403 return int.init; 404 } 405 406 override 407 public string toString() { 408 // getLong() reduces chances of exceptions _in toString() 409 long yoe = getLong(ChronoField.YEAR_OF_ERA); 410 long moy = getLong(ChronoField.MONTH_OF_YEAR); 411 long dom = getLong(ChronoField.DAY_OF_MONTH); 412 StringBuilder buf = new StringBuilder(30); 413 buf.append(getChronology().toString()) 414 .append(" ") 415 .append(typeid(getEra()).name) ///@gxc 416 .append(" ") 417 .append(yoe) 418 .append(moy < 10 ? "-0" : "-").append(moy) 419 .append(dom < 10 ? "-0" : "-").append(dom); 420 return buf.toString(); 421 } 422 423 }