@@ -439,7 +439,7 @@ public Prepared prepare(long minUtcMillis, long maxUtcMillis) {
439
439
440
440
@ Override
441
441
public Prepared prepareForUnknown () {
442
- LocalTimeOffset offset = LocalTimeOffset .lookupFixedOffset (timeZone );
442
+ LocalTimeOffset offset = LocalTimeOffset .fixedOffset (timeZone );
443
443
if (offset != null ) {
444
444
if (unitRoundsToMidnight ) {
445
445
return new FixedToMidnightRounding (offset );
@@ -555,7 +555,7 @@ public long inGap(long localMillis, Gap gap) {
555
555
@ Override
556
556
public long beforeGap (long localMillis , Gap gap ) {
557
557
return gap .previous ().localToUtc (localMillis , this );
558
- };
558
+ }
559
559
560
560
@ Override
561
561
public long inOverlap (long localMillis , Overlap overlap ) {
@@ -565,7 +565,7 @@ public long inOverlap(long localMillis, Overlap overlap) {
565
565
@ Override
566
566
public long beforeOverlap (long localMillis , Overlap overlap ) {
567
567
return overlap .previous ().localToUtc (localMillis , this );
568
- };
568
+ }
569
569
}
570
570
571
571
private class NotToMidnightRounding extends AbstractNotToMidnightRounding implements LocalTimeOffset .Strategy {
@@ -739,21 +739,15 @@ public final long nextRoundingValue(long utcMillis) {
739
739
740
740
static class TimeIntervalRounding extends Rounding {
741
741
static final byte ID = 2 ;
742
- /** Since, there is no offset of -1 ms, it is safe to use -1 for non-fixed timezones */
743
- private static final long TZ_OFFSET_NON_FIXED = -1 ;
744
742
745
743
private final long interval ;
746
744
private final ZoneId timeZone ;
747
- /** For fixed offset timezones, this is the offset in milliseconds, otherwise TZ_OFFSET_NON_FIXED */
748
- private final long fixedOffsetMillis ;
749
745
750
746
TimeIntervalRounding (long interval , ZoneId timeZone ) {
751
747
if (interval < 1 )
752
748
throw new IllegalArgumentException ("Zero or negative time interval not supported" );
753
749
this .interval = interval ;
754
750
this .timeZone = timeZone ;
755
- this .fixedOffsetMillis = timeZone .getRules ().isFixedOffset () ?
756
- timeZone .getRules ().getOffset (Instant .EPOCH ).getTotalSeconds () * 1000 : TZ_OFFSET_NON_FIXED ;
757
751
}
758
752
759
753
TimeIntervalRounding (StreamInput in ) throws IOException {
@@ -773,88 +767,32 @@ public byte id() {
773
767
774
768
@ Override
775
769
public Prepared prepare (long minUtcMillis , long maxUtcMillis ) {
776
- return prepareForUnknown ();
770
+ long minLookup = minUtcMillis - interval ;
771
+ long maxLookup = maxUtcMillis ;
772
+
773
+ LocalTimeOffset .Lookup lookup = LocalTimeOffset .lookup (timeZone , minLookup , maxLookup );
774
+ if (lookup == null ) {
775
+ return prepareJavaTime ();
776
+ }
777
+ LocalTimeOffset fixedOffset = lookup .fixedInRange (minLookup , maxLookup );
778
+ if (fixedOffset != null ) {
779
+ return new FixedRounding (fixedOffset );
780
+ }
781
+ return new VariableRounding (lookup );
777
782
}
778
783
779
784
@ Override
780
785
public Prepared prepareForUnknown () {
786
+ LocalTimeOffset offset = LocalTimeOffset .fixedOffset (timeZone );
787
+ if (offset != null ) {
788
+ return new FixedRounding (offset );
789
+ }
781
790
return prepareJavaTime ();
782
791
}
783
792
784
793
@ Override
785
794
Prepared prepareJavaTime () {
786
- return new Prepared () {
787
- @ Override
788
- public long round (long utcMillis ) {
789
- if (fixedOffsetMillis != TZ_OFFSET_NON_FIXED ) {
790
- // This works as long as the tz offset doesn't change. It is worth getting this case out of the way first,
791
- // as the calculations for fixing things near to offset changes are a little expensive and unnecessary
792
- // in the common case of working with fixed offset timezones (such as UTC).
793
- long localMillis = utcMillis + fixedOffsetMillis ;
794
- return (roundKey (localMillis , interval ) * interval ) - fixedOffsetMillis ;
795
- }
796
- final Instant utcInstant = Instant .ofEpochMilli (utcMillis );
797
- final LocalDateTime rawLocalDateTime = LocalDateTime .ofInstant (utcInstant , timeZone );
798
-
799
- // a millisecond value with the same local time, in UTC, as `utcMillis` has in `timeZone`
800
- final long localMillis = utcMillis + timeZone .getRules ().getOffset (utcInstant ).getTotalSeconds () * 1000 ;
801
- assert localMillis == rawLocalDateTime .toInstant (ZoneOffset .UTC ).toEpochMilli ();
802
-
803
- final long roundedMillis = roundKey (localMillis , interval ) * interval ;
804
- final LocalDateTime roundedLocalDateTime = LocalDateTime .ofInstant (Instant .ofEpochMilli (roundedMillis ), ZoneOffset .UTC );
805
-
806
- // Now work out what roundedLocalDateTime actually means
807
- final List <ZoneOffset > currentOffsets = timeZone .getRules ().getValidOffsets (roundedLocalDateTime );
808
- if (currentOffsets .isEmpty () == false ) {
809
- // There is at least one instant with the desired local time. In general the desired result is
810
- // the latest rounded time that's no later than the input time, but this could involve rounding across
811
- // a timezone transition, which may yield the wrong result
812
- final ZoneOffsetTransition previousTransition = timeZone .getRules ().previousTransition (utcInstant .plusMillis (1 ));
813
- for (int offsetIndex = currentOffsets .size () - 1 ; 0 <= offsetIndex ; offsetIndex --) {
814
- final OffsetDateTime offsetTime = roundedLocalDateTime .atOffset (currentOffsets .get (offsetIndex ));
815
- final Instant offsetInstant = offsetTime .toInstant ();
816
- if (previousTransition != null && offsetInstant .isBefore (previousTransition .getInstant ())) {
817
- /*
818
- * Rounding down across the transition can yield the
819
- * wrong result. It's best to return to the transition
820
- * time and round that down.
821
- */
822
- return round (previousTransition .getInstant ().toEpochMilli () - 1 );
823
- }
824
-
825
- if (utcInstant .isBefore (offsetTime .toInstant ()) == false ) {
826
- return offsetInstant .toEpochMilli ();
827
- }
828
- }
829
-
830
- final OffsetDateTime offsetTime = roundedLocalDateTime .atOffset (currentOffsets .get (0 ));
831
- final Instant offsetInstant = offsetTime .toInstant ();
832
- assert false : this + " failed to round " + utcMillis + " down: " + offsetInstant + " is the earliest possible" ;
833
- return offsetInstant .toEpochMilli (); // TODO or throw something?
834
- } else {
835
- // The desired time isn't valid because within a gap, so just return the gap time.
836
- ZoneOffsetTransition zoneOffsetTransition = timeZone .getRules ().getTransition (roundedLocalDateTime );
837
- return zoneOffsetTransition .getInstant ().toEpochMilli ();
838
- }
839
- }
840
-
841
- @ Override
842
- public long nextRoundingValue (long time ) {
843
- int offsetSeconds = timeZone .getRules ().getOffset (Instant .ofEpochMilli (time )).getTotalSeconds ();
844
- long millis = time + interval + offsetSeconds * 1000 ;
845
- return ZonedDateTime .ofInstant (Instant .ofEpochMilli (millis ), ZoneOffset .UTC )
846
- .withZoneSameLocal (timeZone )
847
- .toInstant ().toEpochMilli ();
848
- }
849
-
850
- private long roundKey (long value , long interval ) {
851
- if (value < 0 ) {
852
- return (value - interval + 1 ) / interval ;
853
- } else {
854
- return value / interval ;
855
- }
856
- }
857
- };
795
+ return new JavaTimeRounding ();
858
796
}
859
797
860
798
@ Override
@@ -888,6 +826,160 @@ public boolean equals(Object obj) {
888
826
public String toString () {
889
827
return "Rounding[" + interval + " in " + timeZone + "]" ;
890
828
}
829
+
830
+ private long roundKey (long value , long interval ) {
831
+ if (value < 0 ) {
832
+ return (value - interval + 1 ) / interval ;
833
+ } else {
834
+ return value / interval ;
835
+ }
836
+ }
837
+
838
+ /**
839
+ * Rounds to down inside of a time zone with an "effectively fixed"
840
+ * time zone. A time zone can be "effectively fixed" if:
841
+ * <ul>
842
+ * <li>It is UTC</li>
843
+ * <li>It is a fixed offset from UTC at all times (UTC-5, America/Phoenix)</li>
844
+ * <li>It is fixed over the entire range of dates that will be rounded</li>
845
+ * </ul>
846
+ */
847
+ private class FixedRounding implements Prepared {
848
+ private final LocalTimeOffset offset ;
849
+
850
+ FixedRounding (LocalTimeOffset offset ) {
851
+ this .offset = offset ;
852
+ }
853
+
854
+ @ Override
855
+ public long round (long utcMillis ) {
856
+ return offset .localToUtcInThisOffset (roundKey (offset .utcToLocalTime (utcMillis ), interval ) * interval );
857
+ }
858
+
859
+ @ Override
860
+ public long nextRoundingValue (long utcMillis ) {
861
+ // TODO this is used in date range's collect so we should optimize it too
862
+ return new JavaTimeRounding ().nextRoundingValue (utcMillis );
863
+ }
864
+ }
865
+
866
+ /**
867
+ * Rounds down inside of any time zone, even if it is not
868
+ * "effectively fixed". See {@link FixedRounding} for a description of
869
+ * "effectively fixed".
870
+ */
871
+ private class VariableRounding implements Prepared , LocalTimeOffset .Strategy {
872
+ private final LocalTimeOffset .Lookup lookup ;
873
+
874
+ VariableRounding (LocalTimeOffset .Lookup lookup ) {
875
+ this .lookup = lookup ;
876
+ }
877
+
878
+ @ Override
879
+ public long round (long utcMillis ) {
880
+ LocalTimeOffset offset = lookup .lookup (utcMillis );
881
+ return offset .localToUtc (roundKey (offset .utcToLocalTime (utcMillis ), interval ) * interval , this );
882
+ }
883
+
884
+ @ Override
885
+ public long nextRoundingValue (long utcMillis ) {
886
+ // TODO this is used in date range's collect so we should optimize it too
887
+ return new JavaTimeRounding ().nextRoundingValue (utcMillis );
888
+ }
889
+
890
+ @ Override
891
+ public long inGap (long localMillis , Gap gap ) {
892
+ return gap .startUtcMillis ();
893
+ }
894
+
895
+ @ Override
896
+ public long beforeGap (long localMillis , Gap gap ) {
897
+ return gap .previous ().localToUtc (localMillis , this );
898
+ }
899
+
900
+ @ Override
901
+ public long inOverlap (long localMillis , Overlap overlap ) {
902
+ // Convert the overlap at this offset because that'll produce the largest result.
903
+ return overlap .localToUtcInThisOffset (localMillis );
904
+ }
905
+
906
+ @ Override
907
+ public long beforeOverlap (long localMillis , Overlap overlap ) {
908
+ return overlap .previous ().localToUtc (roundKey (overlap .firstNonOverlappingLocalTime () - 1 , interval ) * interval , this );
909
+ }
910
+ }
911
+
912
+ /**
913
+ * Rounds down inside of any time zone using {@link LocalDateTime}
914
+ * directly. It'll be slower than {@link VariableRounding} and much
915
+ * slower than {@link FixedRounding}. We use it when we don' have an
916
+ * "effectively fixed" time zone and we can't get a
917
+ * {@link LocalTimeOffset.Lookup}. We might not be able to get one
918
+ * because:
919
+ * <ul>
920
+ * <li>We don't know how to look up the minimum and maximum dates we
921
+ * are going to round.</li>
922
+ * <li>We expect to round over thousands and thousands of years worth
923
+ * of dates with the same {@link Prepared} instance.</li>
924
+ * </ul>
925
+ */
926
+ private class JavaTimeRounding implements Prepared {
927
+ @ Override
928
+ public long round (long utcMillis ) {
929
+ final Instant utcInstant = Instant .ofEpochMilli (utcMillis );
930
+ final LocalDateTime rawLocalDateTime = LocalDateTime .ofInstant (utcInstant , timeZone );
931
+
932
+ // a millisecond value with the same local time, in UTC, as `utcMillis` has in `timeZone`
933
+ final long localMillis = utcMillis + timeZone .getRules ().getOffset (utcInstant ).getTotalSeconds () * 1000 ;
934
+ assert localMillis == rawLocalDateTime .toInstant (ZoneOffset .UTC ).toEpochMilli ();
935
+
936
+ final long roundedMillis = roundKey (localMillis , interval ) * interval ;
937
+ final LocalDateTime roundedLocalDateTime = LocalDateTime .ofInstant (Instant .ofEpochMilli (roundedMillis ), ZoneOffset .UTC );
938
+
939
+ // Now work out what roundedLocalDateTime actually means
940
+ final List <ZoneOffset > currentOffsets = timeZone .getRules ().getValidOffsets (roundedLocalDateTime );
941
+ if (currentOffsets .isEmpty () == false ) {
942
+ // There is at least one instant with the desired local time. In general the desired result is
943
+ // the latest rounded time that's no later than the input time, but this could involve rounding across
944
+ // a timezone transition, which may yield the wrong result
945
+ final ZoneOffsetTransition previousTransition = timeZone .getRules ().previousTransition (utcInstant .plusMillis (1 ));
946
+ for (int offsetIndex = currentOffsets .size () - 1 ; 0 <= offsetIndex ; offsetIndex --) {
947
+ final OffsetDateTime offsetTime = roundedLocalDateTime .atOffset (currentOffsets .get (offsetIndex ));
948
+ final Instant offsetInstant = offsetTime .toInstant ();
949
+ if (previousTransition != null && offsetInstant .isBefore (previousTransition .getInstant ())) {
950
+ /*
951
+ * Rounding down across the transition can yield the
952
+ * wrong result. It's best to return to the transition
953
+ * time and round that down.
954
+ */
955
+ return round (previousTransition .getInstant ().toEpochMilli () - 1 );
956
+ }
957
+
958
+ if (utcInstant .isBefore (offsetTime .toInstant ()) == false ) {
959
+ return offsetInstant .toEpochMilli ();
960
+ }
961
+ }
962
+
963
+ final OffsetDateTime offsetTime = roundedLocalDateTime .atOffset (currentOffsets .get (0 ));
964
+ final Instant offsetInstant = offsetTime .toInstant ();
965
+ assert false : this + " failed to round " + utcMillis + " down: " + offsetInstant + " is the earliest possible" ;
966
+ return offsetInstant .toEpochMilli (); // TODO or throw something?
967
+ } else {
968
+ // The desired time isn't valid because within a gap, so just return the start of the gap
969
+ ZoneOffsetTransition zoneOffsetTransition = timeZone .getRules ().getTransition (roundedLocalDateTime );
970
+ return zoneOffsetTransition .getInstant ().toEpochMilli ();
971
+ }
972
+ }
973
+
974
+ @ Override
975
+ public long nextRoundingValue (long time ) {
976
+ int offsetSeconds = timeZone .getRules ().getOffset (Instant .ofEpochMilli (time )).getTotalSeconds ();
977
+ long millis = time + interval + offsetSeconds * 1000 ;
978
+ return ZonedDateTime .ofInstant (Instant .ofEpochMilli (millis ), ZoneOffset .UTC )
979
+ .withZoneSameLocal (timeZone )
980
+ .toInstant ().toEpochMilli ();
981
+ }
982
+ }
891
983
}
892
984
893
985
static class OffsetRounding extends Rounding {
0 commit comments