|
|
@@ -2,7 +2,6 @@ package net.ranides.assira.time2;
|
|
|
|
|
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
|
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
|
|
-import lombok.RequiredArgsConstructor;
|
|
|
import net.ranides.assira.time2.json.ZonedTimeDeserializer;
|
|
|
import net.ranides.assira.time2.json.ZonedTimeSerializer;
|
|
|
import org.jetbrains.annotations.Contract;
|
|
|
@@ -10,7 +9,12 @@ import org.jetbrains.annotations.NotNull;
|
|
|
import org.jetbrains.annotations.Nullable;
|
|
|
|
|
|
import java.sql.Timestamp;
|
|
|
-import java.time.*;
|
|
|
+import java.time.Duration;
|
|
|
+import java.time.Instant;
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.time.LocalTime;
|
|
|
+import java.time.ZoneId;
|
|
|
+import java.time.ZonedDateTime;
|
|
|
import java.time.temporal.ChronoField;
|
|
|
import java.time.temporal.ChronoUnit;
|
|
|
import java.time.temporal.TemporalField;
|
|
|
@@ -18,23 +22,30 @@ import java.util.Objects;
|
|
|
|
|
|
@JsonDeserialize(using = ZonedTimeDeserializer.class)
|
|
|
@JsonSerialize(using = ZonedTimeSerializer.class)
|
|
|
-public abstract class ZonedTime implements Comparable<ZonedTime> {
|
|
|
-
|
|
|
- private static final boolean ALLOW_ZONE_CONVERSION = true;
|
|
|
+public class ZonedTime implements Comparable<ZonedTime> {
|
|
|
|
|
|
// @todo SQL timestamp test
|
|
|
|
|
|
+ private final ZoneId zone;
|
|
|
+
|
|
|
+ private final LocalTime time;
|
|
|
+
|
|
|
+ private ZonedTime(ZoneId zone, LocalTime time) {
|
|
|
+ this.zone = zone;
|
|
|
+ this.time = time;
|
|
|
+ }
|
|
|
+
|
|
|
@Contract("null -> null; !null -> !null")
|
|
|
public static @Nullable ZonedTime of(@Nullable ZonedDateTime input) {
|
|
|
if(Objects.isNull(input)) {
|
|
|
return null;
|
|
|
}
|
|
|
- return new Complete(input);
|
|
|
+ return new ZonedTime(input.getZone(), input.toLocalTime());
|
|
|
}
|
|
|
|
|
|
@ZoneExplicit
|
|
|
public static @NotNull ZonedTime of(@NotNull ZoneId zone, @NotNull LocalTime time) {
|
|
|
- return new Incomplete(zone, time);
|
|
|
+ return new ZonedTime(zone, time);
|
|
|
}
|
|
|
|
|
|
@Contract("null -> null; !null -> !null")
|
|
|
@@ -45,10 +56,11 @@ public abstract class ZonedTime implements Comparable<ZonedTime> {
|
|
|
return of(ZoneId.systemDefault(), input);
|
|
|
}
|
|
|
|
|
|
- @ZoneExplicit
|
|
|
@Deprecated
|
|
|
- public static ZonedTime of(@NotNull ZoneId zone, @NotNull Timestamp source) {
|
|
|
- return new Incomplete(zone, source.toLocalDateTime().atZone(zone).toLocalTime());
|
|
|
+ @ZoneExplicit
|
|
|
+ @Contract("_,null -> null; _,!null -> !null")
|
|
|
+ public static ZonedTime of(@NotNull ZoneId zone, @Nullable Timestamp source) {
|
|
|
+ return new ZonedTime(zone, source.toLocalDateTime().atZone(zone).toLocalTime());
|
|
|
}
|
|
|
|
|
|
@ZoneExplicit
|
|
|
@@ -58,12 +70,16 @@ public abstract class ZonedTime implements Comparable<ZonedTime> {
|
|
|
}
|
|
|
|
|
|
public static @NotNull ZonedTime now() {
|
|
|
- return new Complete(ZonedDateTime.now());
|
|
|
+ return of(ZonedDateTime.now());
|
|
|
}
|
|
|
|
|
|
- public abstract ZoneId getZone();
|
|
|
+ public ZoneId getZone() {
|
|
|
+ return zone;
|
|
|
+ }
|
|
|
|
|
|
- public abstract ZonedTime plus(long amount, ChronoUnit unit);
|
|
|
+ public ZonedTime plus(long amount, ChronoUnit unit) {
|
|
|
+ return new ZonedTime(zone, time.plus(amount, unit));
|
|
|
+ }
|
|
|
|
|
|
public final ZonedTime minus(long amount, ChronoUnit unit) {
|
|
|
return plus(-amount, unit);
|
|
|
@@ -101,25 +117,45 @@ public abstract class ZonedTime implements Comparable<ZonedTime> {
|
|
|
return minus(nanos, ChronoUnit.NANOS);
|
|
|
}
|
|
|
|
|
|
- public abstract int getHour();
|
|
|
+ public int getHour() {
|
|
|
+ return time.getHour();
|
|
|
+ }
|
|
|
|
|
|
- public abstract int getMinute();
|
|
|
+ public int getMinute() {
|
|
|
+ return time.getMinute();
|
|
|
+ }
|
|
|
|
|
|
- public abstract int getSecond();
|
|
|
+ public int getSecond() {
|
|
|
+ return time.getSecond();
|
|
|
+ }
|
|
|
|
|
|
- public abstract int getNano();
|
|
|
+ public int getNano() {
|
|
|
+ return time.getNano();
|
|
|
+ }
|
|
|
|
|
|
- public abstract long toNanoOfDay();
|
|
|
+ public long toNanoOfDay() {
|
|
|
+ return time.toNanoOfDay();
|
|
|
+ }
|
|
|
|
|
|
@ZoneHazard
|
|
|
- public abstract boolean isAfter(ZonedTime other);
|
|
|
+ public boolean isAfter(ZonedTime other) {
|
|
|
+ if(!isSameZone(other)) {
|
|
|
+ throw new UnsupportedOperationException();
|
|
|
+ }
|
|
|
+ return toNanoOfDay() > other.toNanoOfDay();
|
|
|
+ }
|
|
|
|
|
|
public final boolean isAfter(ZonedDateTime other) {
|
|
|
return toNanoOfDay() > other.withZoneSameInstant(getZone()).toLocalTime().toNanoOfDay();
|
|
|
}
|
|
|
|
|
|
@ZoneHazard
|
|
|
- public abstract boolean isBefore(ZonedTime other);
|
|
|
+ public boolean isBefore(ZonedTime other) {
|
|
|
+ if(!isSameZone(other)) {
|
|
|
+ throw new UnsupportedOperationException();
|
|
|
+ }
|
|
|
+ return toNanoOfDay() < other.toNanoOfDay();
|
|
|
+ }
|
|
|
|
|
|
public final boolean isBefore(ZonedDateTime other) {
|
|
|
return toNanoOfDay() < other.withZoneSameInstant(getZone()).toLocalTime().toNanoOfDay();
|
|
|
@@ -135,8 +171,9 @@ public abstract class ZonedTime implements Comparable<ZonedTime> {
|
|
|
return (obj instanceof ZonedTime) && isEqual((ZonedTime)obj);
|
|
|
}
|
|
|
|
|
|
- @ZoneHazard
|
|
|
- public abstract boolean isEqual(ZonedTime other);
|
|
|
+ public boolean isEqual(ZonedTime other) {
|
|
|
+ return isSameZone(other) && toNanoOfDay() == other.toNanoOfDay();
|
|
|
+ }
|
|
|
|
|
|
public final boolean isEqualTime(ZonedDateTime other) {
|
|
|
return toNanoOfDay() == other.withZoneSameInstant(getZone()).toLocalTime().toNanoOfDay();
|
|
|
@@ -156,50 +193,39 @@ public abstract class ZonedTime implements Comparable<ZonedTime> {
|
|
|
return getZone().equals(other);
|
|
|
}
|
|
|
|
|
|
- public abstract boolean isZoneConvertible(@NotNull ZoneId zone);
|
|
|
-
|
|
|
- @ZoneExplicit
|
|
|
- @ZoneHazard
|
|
|
- public abstract ZonedTime withZone(ZoneId zone);
|
|
|
-
|
|
|
- @ZoneExplicit
|
|
|
- @ZoneHazard
|
|
|
- public final ZonedTime withZoneUtc() {
|
|
|
- return withZone(DateTimeUtils.ZONE_UTC);
|
|
|
- }
|
|
|
-
|
|
|
- @ZoneExplicit
|
|
|
- @ZoneHazard
|
|
|
- public final ZonedTime withZonePolish() {
|
|
|
- return withZone(DateTimeUtils.ZONE_PL);
|
|
|
+ public ZonedTime with(TemporalField field, long amount) {
|
|
|
+ return new ZonedTime(zone, time.with(field, amount));
|
|
|
}
|
|
|
|
|
|
- public abstract ZonedTime with(TemporalField field, long amount);
|
|
|
-
|
|
|
- public ZonedTime withHour(int value) {
|
|
|
- return with(ChronoField.HOUR_OF_DAY, value);
|
|
|
+ public ZonedTime withHour(int amount) {
|
|
|
+ return with(ChronoField.HOUR_OF_DAY, amount);
|
|
|
}
|
|
|
|
|
|
- public ZonedTime withMinute(int value) {
|
|
|
- return with(ChronoField.MINUTE_OF_HOUR, value);
|
|
|
+ public ZonedTime withMinute(int amount) {
|
|
|
+ return with(ChronoField.MINUTE_OF_HOUR, amount);
|
|
|
}
|
|
|
|
|
|
- public ZonedTime withSecond(int value) {
|
|
|
- return with(ChronoField.SECOND_OF_MINUTE, value);
|
|
|
+ public ZonedTime withSecond(int amount) {
|
|
|
+ return with(ChronoField.SECOND_OF_MINUTE, amount);
|
|
|
}
|
|
|
|
|
|
- public ZonedTime withNano(int value) {
|
|
|
- return with(ChronoField.NANO_OF_SECOND, value);
|
|
|
+ public ZonedTime withNano(int amount) {
|
|
|
+ return with(ChronoField.NANO_OF_SECOND, amount);
|
|
|
}
|
|
|
|
|
|
@ZoneHazard
|
|
|
- public final @NotNull ZonedDateTime withDate(@NotNull ZonedDate time) {
|
|
|
- return Objects.requireNonNull(time).withTime(this);
|
|
|
+ public final @NotNull ZonedDateTime withDate(@NotNull ZonedDate date) {
|
|
|
+ if(!isSameZone(date.getZone())) {
|
|
|
+ throw new UnsupportedOperationException();
|
|
|
+ }
|
|
|
+ return ZonedDateTime.of(LocalDate.ofEpochDay(date.toEpochDay()), time, getZone());
|
|
|
}
|
|
|
|
|
|
public ZonedDateTime withDate(ZonedDateTime datetime) {
|
|
|
- //noinspection timezone-hazard
|
|
|
- return withDate(ZonedDate.of(datetime));
|
|
|
+ if(!isSameZone(datetime)) {
|
|
|
+ throw new UnsupportedOperationException();
|
|
|
+ }
|
|
|
+ return ZonedDateTime.of(datetime.toLocalDate(), time, getZone());
|
|
|
}
|
|
|
|
|
|
public final Duration until(ZonedDateTime end) {
|
|
|
@@ -217,7 +243,11 @@ public abstract class ZonedTime implements Comparable<ZonedTime> {
|
|
|
}
|
|
|
|
|
|
@ZoneHazard
|
|
|
- public abstract long until(ZonedTime end, ChronoUnit unit);
|
|
|
+ public long until(ZonedTime end, ChronoUnit unit) {
|
|
|
+ // @todo require zone
|
|
|
+ long nanos = end.toNanoOfDay() - toNanoOfDay();
|
|
|
+ return nanos / unit.getDuration().toNanos();
|
|
|
+ }
|
|
|
|
|
|
@ZoneHazard
|
|
|
@Override
|
|
|
@@ -233,178 +263,7 @@ public abstract class ZonedTime implements Comparable<ZonedTime> {
|
|
|
|
|
|
@Override
|
|
|
public String toString() {
|
|
|
- return DateTimeFormatters.formatTime(withDate(ZonedDateTime.now())) + " [" + getZone() + "]";
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * This wrapper allows all operations even if timezones do not match.
|
|
|
- * Because we have full datetime, we can convert between timezones safely.
|
|
|
- */
|
|
|
- @RequiredArgsConstructor
|
|
|
- private static class Complete extends ZonedTime {
|
|
|
-
|
|
|
- private final ZonedDateTime value;
|
|
|
-
|
|
|
- @Override
|
|
|
- public ZoneId getZone() {
|
|
|
- return value.getZone();
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public boolean isAfter(ZonedTime other) {
|
|
|
- return withZone(other.getZone()).toNanoOfDay() > other.toNanoOfDay();
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public boolean isBefore(ZonedTime other) {
|
|
|
- return withZone(other.getZone()).toNanoOfDay() < other.toNanoOfDay();
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public boolean isEqual(ZonedTime other) {
|
|
|
- return withZone(other.getZone()).toNanoOfDay() == other.toNanoOfDay();
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public boolean isZoneConvertible(ZoneId zone) {
|
|
|
- return isSameZone(zone) || ALLOW_ZONE_CONVERSION;
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public ZonedTime withZone(ZoneId zone) {
|
|
|
- if(!isZoneConvertible(zone)) {
|
|
|
- throw new UnsupportedOperationException();
|
|
|
- }
|
|
|
- return new Complete(value.withZoneSameInstant(zone));
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public ZonedTime with(TemporalField field, long amount) {
|
|
|
- return new Complete(value.with(field, amount));
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public ZonedTime plus(long amount, ChronoUnit unit) {
|
|
|
- return new Complete(value.plus(amount, unit));
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public long until(ZonedTime end, ChronoUnit unit) {
|
|
|
- long nanos = end.toNanoOfDay() - withZone(end.getZone()).toNanoOfDay();
|
|
|
- return nanos / unit.getDuration().toNanos();
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public int getHour() {
|
|
|
- return value.getHour();
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public int getMinute() {
|
|
|
- return value.getMinute();
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public int getSecond() {
|
|
|
- return value.getSecond();
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public int getNano() {
|
|
|
- return value.getNano();
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public long toNanoOfDay() {
|
|
|
- return value.toLocalTime().toNanoOfDay();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * This wrapper throws exception when timezones do not match.
|
|
|
- * It is impossible to convert between timezones without full datetime.
|
|
|
- */
|
|
|
- @RequiredArgsConstructor
|
|
|
- private static class Incomplete extends ZonedTime {
|
|
|
-
|
|
|
- private final ZoneId zone;
|
|
|
-
|
|
|
- private final LocalTime value;
|
|
|
-
|
|
|
- @Override
|
|
|
- public ZoneId getZone() {
|
|
|
- return zone;
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public boolean isAfter(ZonedTime other) {
|
|
|
- return toNanoOfDay() > other.withZone(getZone()).toNanoOfDay();
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public boolean isBefore(ZonedTime other) {
|
|
|
- return toNanoOfDay() < other.withZone(getZone()).toNanoOfDay();
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public boolean isEqual(ZonedTime other) {
|
|
|
- return toNanoOfDay() == other.withZone(getZone()).toNanoOfDay();
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public boolean isZoneConvertible(ZoneId zone) {
|
|
|
- return getZone().equals(zone);
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public ZonedTime withZone(ZoneId zone) {
|
|
|
- if(!isSameZone(zone)) {
|
|
|
- throw new UnsupportedOperationException("This time " + this + " can't be converted to " + zone);
|
|
|
- } else {
|
|
|
- return this;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public ZonedTime with(TemporalField field, long amount) {
|
|
|
- return new Incomplete(zone, value.with(field, amount));
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public ZonedTime plus(long amount, ChronoUnit unit) {
|
|
|
- return new Incomplete(zone, value.plus(amount, unit));
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public long until(ZonedTime end, ChronoUnit unit) {
|
|
|
- long nanos = end.withZone(getZone()).toNanoOfDay() - toNanoOfDay();
|
|
|
- return nanos / unit.getDuration().toNanos();
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public int getHour() {
|
|
|
- return value.getHour();
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public int getMinute() {
|
|
|
- return value.getMinute();
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public int getSecond() {
|
|
|
- return value.getSecond();
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public int getNano() {
|
|
|
- return value.getNano();
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public long toNanoOfDay() {
|
|
|
- return value.toNanoOfDay();
|
|
|
- }
|
|
|
+ return DateTimeFormatters1.formatTimeShort(withDate(ZonedDateTime.now())) + " [" + getZone() + "]";
|
|
|
}
|
|
|
|
|
|
}
|