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.ChronoZonedDateTimeImpl; 13 14 import hunt.time.temporal.ChronoUnit; 15 16 import hunt.Exceptions; 17 import hunt.Integer; 18 import hunt.Long; 19 import hunt.stream.ObjectInput; 20 import hunt.stream.ObjectOutput; 21 import hunt.stream.Common; 22 import hunt.util.Comparator; 23 import hunt.time.Instant; 24 import hunt.time.LocalDateTime; 25 import hunt.time.ZoneId; 26 import hunt.time.ZoneOffset; 27 import hunt.time.temporal.ChronoField; 28 import hunt.time.temporal.ChronoUnit; 29 import hunt.time.temporal.Temporal; 30 import hunt.time.temporal.TemporalField; 31 import hunt.time.temporal.TemporalUnit; 32 import hunt.time.zone.ZoneOffsetTransition; 33 import hunt.time.zone.ZoneRules; 34 import hunt.collection.List; 35 import hunt.time.chrono.ChronoLocalDate; 36 import hunt.time.chrono.ChronoZonedDateTime; 37 import hunt.time.chrono.ChronoLocalDateTimeImpl; 38 import hunt.time.chrono.Chronology; 39 import hunt.time.chrono.ChronoLocalDateTime; 40 import hunt.time.temporal.TemporalAdjuster; 41 import hunt.time.chrono.Ser; 42 import hunt.time.temporal.ValueRange; 43 import hunt.time.temporal.TemporalAmount; 44 import std.conv; 45 import hunt.time.Exceptions; 46 import hunt.time.Exceptions; 47 // import hunt.time.format.DateTimeFormatter; 48 import hunt.time.LocalTime; 49 /** 50 * A date-time with a time-zone _in the calendar neutral API. 51 * !(p) 52 * {@code ZoneChronoDateTime} is an immutable representation of a date-time with a time-zone. 53 * This class stores all date and time fields, to a precision of nanoseconds, 54 * as well as a time-zone and zone offset. 55 * !(p) 56 * The purpose of storing the time-zone is to distinguish the ambiguous case where 57 * the local time-line overlaps, typically as a result of the end of daylight time. 58 * Information about the local-time can be obtained using methods on the time-zone. 59 * 60 * @implSpec 61 * This class is immutable and thread-safe. 62 * 63 * @serial Document the delegation of this class _in the serialized-form specification. 64 * @param !(D) the concrete type for the date of this date-time 65 * @since 1.8 66 */ 67 final class ChronoZonedDateTimeImpl(D = ChronoLocalDate) if(is(D : ChronoLocalDate)) 68 : ChronoZonedDateTime!(D) { //, Serializable 69 70 71 /** 72 * The local date-time. 73 */ 74 private /*transient*/ ChronoLocalDateTimeImpl!(D) dateTime; 75 /** 76 * The zone offset. 77 */ 78 private /*transient*/ ZoneOffset offset; 79 /** 80 * The zone ID. 81 */ 82 private /*transient*/ ZoneId zone; 83 84 //----------------------------------------------------------------------- 85 /** 86 * Obtains an instance from a local date-time using the preferred offset if possible. 87 * 88 * @param localDateTime the local date-time, not null 89 * @param zone the zone identifier, not null 90 * @param preferredOffset the zone offset, null if no preference 91 * @return the zoned date-time, not null 92 */ 93 static ChronoZonedDateTime!(R) ofBest(R)( 94 ChronoLocalDateTimeImpl!(R) localDateTime, ZoneId zone, ZoneOffset preferredOffset) /* if(is(R : ChronoLocalDate)) */ { 95 assert(localDateTime, "localDateTime"); 96 assert(zone, "zone"); 97 if (cast(ZoneOffset)(zone) !is null) { 98 return new ChronoZonedDateTimeImpl!()(localDateTime, cast(ZoneOffset) zone, zone); 99 } 100 ZoneRules rules = zone.getRules(); 101 LocalDateTime isoLDT = LocalDateTime.from(localDateTime); 102 List!(ZoneOffset) validOffsets = rules.getValidOffsets(isoLDT); 103 ZoneOffset offset; 104 if (validOffsets.size() == 1) { 105 offset = validOffsets.get(0); 106 } else if (validOffsets.size() == 0) { 107 ZoneOffsetTransition trans = rules.getTransition(isoLDT); 108 localDateTime = localDateTime.plusSeconds(trans.getDuration().getSeconds()); 109 offset = trans.getOffsetAfter(); 110 } else { 111 if (preferredOffset !is null && validOffsets.contains(preferredOffset)) { 112 offset = preferredOffset; 113 } else { 114 offset = validOffsets.get(0); 115 } 116 } 117 assert(offset, "offset"); // protect against bad ZoneRules 118 return new ChronoZonedDateTimeImpl!()(localDateTime, offset, zone); 119 } 120 121 /** 122 * Obtains an instance from an instant using the specified time-zone. 123 * 124 * @param chrono the chronology, not null 125 * @param instant the instant, not null 126 * @param zone the zone identifier, not null 127 * @return the zoned date-time, not null 128 */ 129 static ChronoZonedDateTimeImpl!(ChronoLocalDate) ofInstant(Chronology chrono, Instant instant, ZoneId zone) { 130 ZoneRules rules = zone.getRules(); 131 ZoneOffset offset = rules.getOffset(instant); 132 assert(offset, "offset"); // protect against bad ZoneRules 133 LocalDateTime ldt = LocalDateTime.ofEpochSecond(instant.getEpochSecond(), instant.getNano(), offset); 134 ChronoLocalDateTimeImpl!(ChronoLocalDate) cldt = cast(ChronoLocalDateTimeImpl!(ChronoLocalDate))chrono.localDateTime(ldt); 135 return new ChronoZonedDateTimeImpl!(ChronoLocalDate)(cldt, offset, zone); 136 } 137 138 /** 139 * Obtains an instance from an {@code Instant}. 140 * 141 * @param instant the instant to create the date-time from, not null 142 * @param zone the time-zone to use, validated not null 143 * @return the zoned date-time, validated not null 144 */ 145 /*@SuppressWarnings("unchecked")*/ 146 private ChronoZonedDateTimeImpl!(D) create(Instant instant, ZoneId zone) { 147 return cast(ChronoZonedDateTimeImpl!(D))ofInstant(getChronology(), instant, zone); 148 } 149 150 /** 151 * Casts the {@code Temporal} to {@code ChronoZonedDateTimeImpl} ensuring it bas the specified chronology. 152 * 153 * @param chrono the chronology to check for, not null 154 * @param temporal a date-time to cast, not null 155 * @return the date-time checked and cast to {@code ChronoZonedDateTimeImpl}, not null 156 * @throws ClassCastException if the date-time cannot be cast to ChronoZonedDateTimeImpl 157 * or the chronology is not equal this Chronology 158 */ 159 static ChronoZonedDateTimeImpl!(R) ensureValid(R)(Chronology chrono, Temporal temporal) { 160 /*@SuppressWarnings("unchecked")*/ 161 ChronoZonedDateTimeImpl!(R) other = cast(ChronoZonedDateTimeImpl!(R))temporal; 162 if ((chrono == other.getChronology()) == false) { 163 throw new ClassCastException("Chronology mismatch, required: " ~ chrono.getId() 164 ~ ", actual: " ~ other.getChronology().getId()); 165 } 166 return other; 167 } 168 169 //----------------------------------------------------------------------- 170 /** 171 * Constructor. 172 * 173 * @param dateTime the date-time, not null 174 * @param offset the zone offset, not null 175 * @param zone the zone ID, not null 176 */ 177 private this(ChronoLocalDateTimeImpl!(D) dateTime, ZoneOffset offset, ZoneId zone) { 178 this.dateTime = dateTime; 179 this.offset = offset; 180 this.zone = zone; 181 } 182 183 //----------------------------------------------------------------------- 184 override 185 public ZoneOffset getOffset() { 186 return offset; 187 } 188 189 override 190 public ChronoZonedDateTime!(D) withEarlierOffsetAtOverlap() { 191 ZoneOffsetTransition trans = getZone().getRules().getTransition(LocalDateTime.from(this)); 192 if (trans !is null && trans.isOverlap()) { 193 ZoneOffset earlierOffset = trans.getOffsetBefore(); 194 if ((earlierOffset == offset) == false) { 195 return new ChronoZonedDateTimeImpl!()(dateTime, earlierOffset, zone); 196 } 197 } 198 return this; 199 } 200 201 override 202 public ChronoZonedDateTime!(D) withLaterOffsetAtOverlap() { 203 ZoneOffsetTransition trans = getZone().getRules().getTransition(LocalDateTime.from(this)); 204 if (trans !is null) { 205 ZoneOffset offset = trans.getOffsetAfter(); 206 if ((offset == getOffset()) == false) { 207 return new ChronoZonedDateTimeImpl!(D)(dateTime, offset, zone); 208 } 209 } 210 return this; 211 } 212 213 //----------------------------------------------------------------------- 214 override 215 public ChronoLocalDateTime!(D) toLocalDateTime() { 216 return dateTime; 217 } 218 219 override 220 public ZoneId getZone() { 221 return zone; 222 } 223 224 override 225 public ChronoZonedDateTime!(D) withZoneSameLocal(ZoneId zone) { 226 return ofBest(dateTime, zone, offset); 227 } 228 229 override 230 public ChronoZonedDateTime!(D) withZoneSameInstant(ZoneId zone) { 231 assert(zone, "zone"); 232 return this.zone == (zone) ? this : create(dateTime.toInstant(offset), zone); 233 } 234 235 //----------------------------------------------------------------------- 236 override 237 public bool isSupported(TemporalField field) { 238 return cast(ChronoField)(field) !is null || (field !is null && field.isSupportedBy(this)); 239 } 240 241 //----------------------------------------------------------------------- 242 override 243 public ChronoZonedDateTime!(D) _with(TemporalField field, long newValue) { 244 if (cast(ChronoField)(field) !is null) { 245 ChronoField f = cast(ChronoField) field; 246 { 247 if( f == ChronoField.INSTANT_SECONDS) return plus(newValue - toEpochSecond(), ChronoUnit.SECONDS); 248 if( f == ChronoField.OFFSET_SECONDS) { 249 ZoneOffset offset = ZoneOffset.ofTotalSeconds(f.checkValidIntValue(newValue)); 250 return create(dateTime.toInstant(offset), zone); 251 } 252 } 253 return ofBest(dateTime._with(field, newValue), zone, offset); 254 } 255 return ChronoZonedDateTimeImpl!D.ensureValid!D(getChronology(), field.adjustInto(this, newValue)); 256 } 257 258 //----------------------------------------------------------------------- 259 override 260 public ChronoZonedDateTime!(D) plus(long amountToAdd, TemporalUnit unit) { 261 if (cast(ChronoUnit)(unit) !is null) { 262 return super_with(dateTime.plus(amountToAdd, unit)); 263 } 264 return ChronoZonedDateTimeImpl!D.ensureValid!D(getChronology(), unit.addTo(this, amountToAdd)); /// TODO: Generics replacement Risk! 265 } 266 ChronoZonedDateTime!(D) super_with(TemporalAdjuster adjuster) { 267 return ChronoZonedDateTimeImpl!D.ensureValid!D(getChronology(), adjuster.adjustInto(this)); 268 } 269 270 //----------------------------------------------------------------------- 271 override 272 public long until(Temporal endExclusive, TemporalUnit unit) { 273 assert(endExclusive, "endExclusive"); 274 /*@SuppressWarnings("unchecked")*/ 275 ChronoZonedDateTime!(D) end = cast(ChronoZonedDateTime!(D)) getChronology().zonedDateTime(endExclusive); 276 if (cast(ChronoUnit)(unit) !is null) { 277 end = end.withZoneSameInstant(offset); 278 return dateTime.until(end.toLocalDateTime(), unit); 279 } 280 assert(unit, "unit"); 281 return unit.between(this, end); 282 } 283 284 //----------------------------------------------------------------------- 285 /** 286 * Writes the ChronoZonedDateTime using a 287 * <a href="{@docRoot}/serialized-form.html#hunt.time.chrono.Ser">dedicated serialized form</a>. 288 * @serialData 289 * !(pre) 290 * _out.writeByte(3); // identifies a ChronoZonedDateTime 291 * _out.writeObject(toLocalDateTime()); 292 * _out.writeObject(getOffset()); 293 * _out.writeObject(getZone()); 294 * </pre> 295 * 296 * @return the instance of {@code Ser}, not null 297 */ 298 private Object writeReplace() { 299 return new Ser(Ser.CHRONO_ZONE_DATE_TIME_TYPE, this); 300 } 301 302 /** 303 * Defend against malicious streams. 304 * 305 * @param s the stream to read 306 * @throws InvalidObjectException always 307 */ 308 ///@gxc 309 // private void readObject(ObjectInputStream s) /*throws InvalidObjectException*/ { 310 // throw new InvalidObjectException("Deserialization via serialization delegate"); 311 // } 312 313 void writeExternal(ObjectOutput _out) /*throws IOException*/ { 314 _out.writeObject(dateTime); 315 _out.writeObject(offset); 316 _out.writeObject(zone); 317 } 318 319 static ChronoZonedDateTime!(ChronoLocalDate) readExternal(ObjectInput _in) /*throws IOException, ClassNotFoundException */{ 320 ChronoLocalDateTime!(ChronoLocalDate) dateTime = cast(ChronoLocalDateTime!(ChronoLocalDate)) _in.readObject(); 321 ZoneOffset offset = cast(ZoneOffset) _in.readObject(); 322 ZoneId zone = cast(ZoneId) _in.readObject(); 323 return dateTime.atZone(offset).withZoneSameLocal(zone); 324 // TODO: ZDT uses ofLenient() 325 } 326 327 //------------------------------------------------------------------------- 328 override 329 public bool opEquals(Object obj) { 330 if (this is obj) { 331 return true; 332 } 333 if (cast(ChronoZonedDateTime!D)(obj) !is null) { 334 return compareTo(cast(ChronoZonedDateTime!(D)) obj) == 0; 335 } 336 return false; 337 } 338 339 override 340 public size_t toHash() @trusted nothrow { 341 try{ 342 return toLocalDateTime().toHash() ^ getOffset().toHash() ^ Integer.rotateLeft(cast(int)(getZone().toHash()), 3); 343 }catch(Exception e){ 344 return int.init; 345 } 346 } 347 348 override 349 public string toString() { 350 string str = toLocalDateTime().toString() ~ getOffset().toString(); 351 if (getOffset() != getZone()) { 352 str ~= '[' ~ getZone().toString() ~ ']'; 353 } 354 return str; 355 } 356 357 override 358 ValueRange range(TemporalField field) { 359 if (cast(ChronoField)(field) !is null) { 360 if (field == ChronoField.INSTANT_SECONDS || field == ChronoField.OFFSET_SECONDS) { 361 return field.range(); 362 } 363 return toLocalDateTime().range(field); 364 } 365 return field.rangeRefinedBy(this); 366 } 367 368 override 369 int get(TemporalField field) { 370 if (cast(ChronoField)(field) !is null) { 371 auto f = cast(ChronoField) field; 372 { 373 if( f == ChronoField.INSTANT_SECONDS) 374 throw new UnsupportedTemporalTypeException("Invalid field 'InstantSeconds' for get() method, use getLong() instead"); 375 if( f == ChronoField.OFFSET_SECONDS) 376 return getOffset().getTotalSeconds(); 377 } 378 return toLocalDateTime().get(field); 379 } 380 return /* Temporal. */super_get(field); 381 } 382 383 int super_get(TemporalField field) { 384 ValueRange range = range(field); 385 if (range.isIntValue() == false) { 386 throw new UnsupportedTemporalTypeException("Invalid field " ~ field.toString ~ " for get() method, use getLong() instead"); 387 } 388 long value = getLong(field); 389 if (range.isValidValue(value) == false) { 390 throw new DateTimeException("Invalid value for " ~ field.toString ~ " (valid values " ~ range.toString ~ "): " ~ value.to!string); 391 } 392 return cast(int) value; 393 } 394 395 override 396 long getLong(TemporalField field) { 397 if (cast(ChronoField)(field) !is null) { 398 auto f = cast(ChronoField) field; 399 { 400 if ( f == ChronoField.INSTANT_SECONDS) return toEpochSecond(); 401 if ( f == ChronoField.OFFSET_SECONDS)return getOffset().getTotalSeconds(); 402 } 403 return toLocalDateTime().getLong(field); 404 } 405 return field.getFrom(this); 406 } 407 override 408 bool isSupported(TemporalUnit unit) { 409 if (cast(ChronoUnit)(unit) !is null) { 410 return unit != ChronoUnit.FOREVER; 411 } 412 return unit !is null && unit.isSupportedBy(this); 413 } 414 415 override 416 ChronoZonedDateTime!(D) _with(TemporalAdjuster adjuster) { 417 return ChronoZonedDateTimeImpl!D.ensureValid!D(getChronology(), /* Temporal. */adjuster.adjustInto(this)); 418 } 419 420 override 421 ChronoZonedDateTime!(D) plus(TemporalAmount amount) { 422 return ChronoZonedDateTimeImpl!D.ensureValid!D(getChronology(), /* Temporal. */amount.addTo(this)); 423 } 424 override 425 ChronoZonedDateTime!(D) minus(TemporalAmount amount) { 426 return ChronoZonedDateTimeImpl!D.ensureValid!D(getChronology(), /* Temporal. */amount.subtractFrom(this)); 427 } 428 429 override 430 ChronoZonedDateTime!(D) minus(long amountToSubtract, TemporalUnit unit) { 431 return ChronoZonedDateTimeImpl!D.ensureValid!D(getChronology(), /* Temporal. */(amountToSubtract == Long.MIN_VALUE ? plus(Long.MAX_VALUE, unit).plus(1, unit) : plus(-amountToSubtract, unit))); 432 } 433 434 override 435 int compareTo(ChronoZonedDateTime!(ChronoLocalDate) other) { 436 int cmp = compare(toEpochSecond(), other.toEpochSecond()); 437 if (cmp == 0) { 438 cmp = toLocalTime().getNano() - other.toLocalTime().getNano(); 439 if (cmp == 0) { 440 cmp = toLocalDateTime().compareTo(other.toLocalDateTime()); 441 if (cmp == 0) { 442 cmp = getZone().getId().compare(other.getZone().getId()); 443 if (cmp == 0) { 444 cmp = getChronology().compareTo(other.getChronology()); 445 } 446 } 447 } 448 } 449 return cmp; 450 } 451 452 override 453 int opCmp(ChronoZonedDateTime!(ChronoLocalDate) other) { 454 int cmp = compare(toEpochSecond(), other.toEpochSecond()); 455 if (cmp == 0) { 456 cmp = toLocalTime().getNano() - other.toLocalTime().getNano(); 457 if (cmp == 0) { 458 cmp = toLocalDateTime().compareTo(other.toLocalDateTime()); 459 if (cmp == 0) { 460 cmp = getZone().getId().compare(other.getZone().getId()); 461 if (cmp == 0) { 462 cmp = getChronology().compareTo(other.getChronology()); 463 } 464 } 465 } 466 } 467 return cmp; 468 } 469 470 override 471 bool isBefore(ChronoZonedDateTime!(ChronoLocalDate) other) { 472 long thisEpochSec = toEpochSecond(); 473 long otherEpochSec = other.toEpochSecond(); 474 return thisEpochSec < otherEpochSec || 475 (thisEpochSec == otherEpochSec && toLocalTime().getNano() < other.toLocalTime().getNano()); 476 } 477 478 override 479 bool isAfter(ChronoZonedDateTime!(ChronoLocalDate) other) { 480 long thisEpochSec = toEpochSecond(); 481 long otherEpochSec = other.toEpochSecond(); 482 return thisEpochSec > otherEpochSec || 483 (thisEpochSec == otherEpochSec && toLocalTime().getNano() > other.toLocalTime().getNano()); 484 } 485 486 override 487 bool isEqual(ChronoZonedDateTime!(ChronoLocalDate) other) { 488 return toEpochSecond() == other.toEpochSecond() && 489 toLocalTime().getNano() == other.toLocalTime().getNano(); 490 } 491 492 override 493 long toEpochSecond() { 494 long epochDay = toLocalDate().toEpochDay(); 495 long secs = epochDay * 86400 + toLocalTime().toSecondOfDay(); 496 secs -= getOffset().getTotalSeconds(); 497 return secs; 498 } 499 500 override 501 Instant toInstant() { 502 return Instant.ofEpochSecond(toEpochSecond(), toLocalTime().getNano()); 503 } 504 505 // override 506 // string format(DateTimeFormatter formatter) { 507 // assert(formatter, "formatter"); 508 // return formatter.format(this); 509 // } 510 511 override 512 LocalTime toLocalTime() { 513 return toLocalDateTime().toLocalTime(); 514 } 515 516 override 517 D toLocalDate() { 518 return toLocalDateTime().toLocalDate(); 519 } 520 521 override 522 Chronology getChronology() { 523 return toLocalDate().getChronology(); 524 } 525 }