1 /*
2 * Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26
27 package build.tools.tzdb;
28
29 import java.io.IOException;
30 import java.nio.charset.StandardCharsets;
31 import java.nio.file.Files;
32 import java.nio.file.Path;
33 import java.nio.file.Paths;
34 import java.util.ArrayList;
35 import java.util.Collections;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.Map.Entry;
39 import java.util.NavigableMap;
40 import java.util.Objects;
41 import java.util.Set;
42 import java.util.TreeMap;
43 import java.util.TreeSet;
44 import java.util.concurrent.ConcurrentSkipListMap;
45 import java.time.*;
46 import java.time.Year;
47 import java.time.chrono.IsoChronology;
48 import java.time.temporal.TemporalAdjusters;
49 import java.time.zone.ZoneOffsetTransition;
50 import java.time.zone.ZoneOffsetTransitionRule;
51 import java.time.zone.ZoneOffsetTransitionRule.TimeDefinition;
52 import java.time.zone.ZoneRulesException;
53
54 /**
55 * Compile and build time-zone rules from IANA timezone data
56 *
57 * @author Xueming Shen
58 * @author Stephen Colebourne
59 * @author Michael Nascimento Santos
60 *
61 * @since 9
62 */
63
64 class TzdbZoneRulesProvider {
65
66 /**
67 * Creates an instance.
68 *
69 * @throws ZoneRulesException if unable to load
70 */
71 public TzdbZoneRulesProvider(List<Path> files) {
255 * Class representing a month-day-time in the TZDB file.
256 */
257 private static abstract class MonthDayTime {
258 /** The month of the cutover. */
259 Month month = Month.JANUARY;
260
261 /** The day-of-month of the cutover. */
262 int dayOfMonth = 1;
263
264 /** Whether to adjust forwards. */
265 boolean adjustForwards = true;
266
267 /** The day-of-week of the cutover. */
268 DayOfWeek dayOfWeek;
269
270 /** The time of the cutover, in second of day */
271 int secsOfDay = 0;
272
273 /** Whether this is midnight end of day. */
274 boolean endOfDay;
275 /** The time of the cutover. */
276
277 TimeDefinition timeDefinition = TimeDefinition.WALL;
278
279 void adjustToForwards(int year) {
280 if (adjustForwards == false && dayOfMonth > 0) {
281 // weekDay<=monthDay case, don't have it in tzdb data for now
282 LocalDate adjustedDate = LocalDate.of(year, month, dayOfMonth).minusDays(6);
283 dayOfMonth = adjustedDate.getDayOfMonth();
284 month = adjustedDate.getMonth();
285 adjustForwards = true;
286 }
287 }
288
289 LocalDateTime toDateTime(int year) {
290 LocalDate date;
291 if (dayOfMonth < 0) {
292 int monthLen = month.length(IsoChronology.INSTANCE.isLeapYear(year));
293 date = LocalDate.of(year, month, monthLen + 1 + dayOfMonth);
294 if (dayOfWeek != null) {
295 date = date.with(TemporalAdjusters.previousOrSame(dayOfWeek));
296 }
326 index = dayRule.indexOf("<=");
327 if (index > 0) {
328 dayOfWeek = parseDayOfWeek(dayRule.substring(0, index));
329 adjustForwards = false;
330 dayRule = dayRule.substring(index + 2);
331 }
332 }
333 dayOfMonth = Integer.parseInt(dayRule);
334 if (dayOfMonth < -28 || dayOfMonth > 31 || dayOfMonth == 0) {
335 throw new IllegalArgumentException(
336 "Day of month indicator must be between -28 and 31 inclusive excluding zero");
337 }
338 }
339 if (off < tokens.length) {
340 String timeStr = tokens[off++];
341 secsOfDay = parseSecs(timeStr);
342 if (secsOfDay == 86400) {
343 // time must be midnight when end of day flag is true
344 endOfDay = true;
345 secsOfDay = 0;
346 }
347 timeDefinition = parseTimeDefinition(timeStr.charAt(timeStr.length() - 1));
348 }
349 }
350 }
351
352 int parseYear(String year, int defaultYear) {
353 switch (year.toLowerCase()) {
354 case "min": return 1900;
355 case "max": return Year.MAX_VALUE;
356 case "only": return defaultYear;
357 }
358 return Integer.parseInt(year);
359 }
360
361 Month parseMonth(String mon) {
362 switch (mon) {
363 case "Jan": return Month.JANUARY;
364 case "Feb": return Month.FEBRUARY;
365 case "Mar": return Month.MARCH;
480 * Class representing a rule line in the TZDB file.
481 */
482 private static class RuleLine extends MonthDayTime {
483 /** The start year. */
484 int startYear;
485
486 /** The end year. */
487 int endYear;
488
489 /** The amount of savings, in seconds. */
490 int savingsAmount;
491
492 /** The text name of the zone. */
493 String text;
494
495 /**
496 * Converts this to a transition rule.
497 *
498 * @param standardOffset the active standard offset, not null
499 * @param savingsBeforeSecs the active savings before the transition in seconds
500 * @return the transition, not null
501 */
502 ZoneOffsetTransitionRule toTransitionRule(ZoneOffset stdOffset, int savingsBefore) {
503 // rule shared by different zones, so don't change it
504 Month month = this.month;
505 int dayOfMonth = this.dayOfMonth;
506 DayOfWeek dayOfWeek = this.dayOfWeek;
507 boolean endOfDay = this.endOfDay;
508
509 // optimize stored format
510 if (dayOfMonth < 0) {
511 if (month != Month.FEBRUARY) { // not Month.FEBRUARY
512 dayOfMonth = month.maxLength() - 6;
513 }
514 }
515 if (endOfDay && dayOfMonth > 0 &&
516 (dayOfMonth == 28 && month == Month.FEBRUARY) == false) {
517 LocalDate date = LocalDate.of(2004, month, dayOfMonth).plusDays(1); // leap-year
518 month = date.getMonth();
519 dayOfMonth = date.getDayOfMonth();
520 if (dayOfWeek != null) {
521 dayOfWeek = dayOfWeek.plus(1);
522 }
523 endOfDay = false;
524 }
525 // build rule
526 return ZoneOffsetTransitionRule.of(
527 //month, dayOfMonth, dayOfWeek, time, endOfDay, timeDefinition,
528 month, dayOfMonth, dayOfWeek,
529 LocalTime.ofSecondOfDay(secsOfDay), endOfDay, timeDefinition,
530 stdOffset,
531 ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + savingsBefore),
532 ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + savingsAmount));
533 }
534
535 RuleLine parse(String[] tokens) {
536 startYear = parseYear(tokens[2], 0);
537 endYear = parseYear(tokens[3], startYear);
538 if (startYear > endYear) {
539 throw new IllegalArgumentException(
540 "Invalid <Rule> line/Year order invalid:" + startYear + " > " + endYear);
541 }
542 //parseOptional(s.next()); // type is unused
543 super.parse(tokens, 5); // monthdaytime parsing
544 savingsAmount = parsePeriod(tokens[8]);
545 //rule.text = parseOptional(s.next());
546 return this;
547 }
548 }
549
550 /**
551 * Class representing a linked set of zone lines in the TZDB file.
552 */
626 * Class representing a rule line in the TZDB file for a particular year.
627 */
628 private static class TransRule implements Comparable<TransRule>
629 {
630 private int year;
631 private RuleLine rule;
632
633 /** The trans date/time */
634 private LocalDateTime ldt;
635
636 /** The trans date/time in epoch seconds (assume UTC) */
637 long ldtSecs;
638
639 TransRule(int year, RuleLine rule) {
640 this.year = year;
641 this.rule = rule;
642 this.ldt = rule.toDateTime(year);
643 this.ldtSecs = ldt.toEpochSecond(ZoneOffset.UTC);
644 }
645
646 ZoneOffsetTransition toTransition(ZoneOffset standardOffset, int savingsBeforeSecs) {
647 // copy of code in ZoneOffsetTransitionRule to avoid infinite loop
648 ZoneOffset wallOffset = ZoneOffset.ofTotalSeconds(
649 standardOffset.getTotalSeconds() + savingsBeforeSecs);
650 ZoneOffset offsetAfter = ZoneOffset.ofTotalSeconds(
651 standardOffset.getTotalSeconds() + rule.savingsAmount);
652 LocalDateTime dt = rule.timeDefinition
653 .createDateTime(ldt, standardOffset, wallOffset);
654 return ZoneOffsetTransition.of(dt, wallOffset, offsetAfter);
655 }
656
657 long toEpochSecond(ZoneOffset stdOffset, int savingsBeforeSecs) {
658 switch(rule.timeDefinition) {
659 case UTC: return ldtSecs;
660 case STANDARD: return ldtSecs - stdOffset.getTotalSeconds();
661 default: return ldtSecs - (stdOffset.getTotalSeconds() + savingsBeforeSecs); // WALL
662 }
663 }
664
665 /**
666 * Tests if this a real transition with the active savings in seconds
667 *
668 * @param savingsBefore the active savings in seconds
669 * @return true, if savings changes
670 */
671 boolean isTransition(int savingsBefore) {
672 return rule.savingsAmount != savingsBefore;
673 }
674
675 public int compareTo(TransRule other) {
676 return (ldtSecs < other.ldtSecs)? -1 : ((ldtSecs == other.ldtSecs) ? 0 : 1);
677 }
678 }
679
680 private ZoneRules buildRules(String zoneId, List<ZoneLine> zones) {
681 if (zones.isEmpty()) {
682 throw new IllegalStateException("No available zone window");
683 }
684 final List<ZoneOffsetTransition> standardTransitionList = new ArrayList<>(4);
685 final List<ZoneOffsetTransition> transitionList = new ArrayList<>(256);
686 final List<ZoneOffsetTransitionRule> lastTransitionRuleList = new ArrayList<>(2);
687
688 final ZoneLine zone0 = zones.get(0);
689 // initialize the standard offset, wallOffset and savings for loop
690
691 //ZoneOffset stdOffset = zone0.standardOffset;
692 ZoneOffset stdOffset = ZoneOffset.ofTotalSeconds(zone0.stdOffsetSecs);
693
694 int savings = zone0.fixedSavingsSecs;
695 ZoneOffset wallOffset = ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + savings);
696
697 // start ldt of each zone window
698 LocalDateTime zoneStart = LocalDateTime.MIN;
699
700 // first stanard offset
701 ZoneOffset firstStdOffset = stdOffset;
702 // first wall offset
703 ZoneOffset firstWallOffset = wallOffset;
704
705 for (ZoneLine zone : zones) {
706 // check if standard offset changed, update it if yes
707 ZoneOffset stdOffsetPrev = stdOffset; // for effectiveSavings check
708 if (zone.stdOffsetSecs != stdOffset.getTotalSeconds()) {
709 ZoneOffset stdOffsetNew = ZoneOffset.ofTotalSeconds(zone.stdOffsetSecs);
710 standardTransitionList.add(
711 ZoneOffsetTransition.of(
712 LocalDateTime.ofEpochSecond(zoneStart.toEpochSecond(wallOffset),
713 0,
714 stdOffset),
715 stdOffset,
716 stdOffsetNew));
717 stdOffset = stdOffsetNew;
718 }
719
720 LocalDateTime zoneEnd;
721 if (zone.year == Year.MAX_VALUE) {
722 zoneEnd = LocalDateTime.MAX;
723 } else {
724 zoneEnd = zone.toDateTime();
725 }
774 // last rules, fill the gap years between different last rules
775 if (zoneEnd.equals(LocalDateTime.MAX)) {
776 lastRulesStartYear = Math.max(lastRulesStartYear, zoneStart.getYear()) + 1;
777 for (TransRule rule : lastRules) {
778 if (rule.year <= lastRulesStartYear) {
779 int year = rule.year;
780 while (year <= lastRulesStartYear) {
781 trules.add(new TransRule(year, rule.rule));
782 year++;
783 }
784 rule.year = lastRulesStartYear;
785 rule.ldt = rule.rule.toDateTime(year);
786 rule.ldtSecs = rule.ldt.toEpochSecond(ZoneOffset.UTC);
787 }
788 }
789 Collections.sort(lastRules);
790 }
791 // sort the merged rules
792 Collections.sort(trules);
793
794 effectiveSavings = 0;
795 for (TransRule rule : trules) {
796 if (rule.toEpochSecond(stdOffsetPrev, savings) >
797 zoneStart.toEpochSecond(wallOffset)) {
798 // previous savings amount found, which could be the
799 // savings amount at the instant that the window starts
800 // (hence isAfter)
801 break;
802 }
803 effectiveSavings = rule.rule.savingsAmount;
804 }
805 }
806 // check if the start of the window represents a transition
807 ZoneOffset effectiveWallOffset =
808 ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + effectiveSavings);
809
810 if (!wallOffset.equals(effectiveWallOffset)) {
811 transitionList.add(ZoneOffsetTransition.of(zoneStart,
812 wallOffset,
813 effectiveWallOffset));
814 }
815 savings = effectiveSavings;
816 // apply rules within the window
817 if (trules != null) {
818 long zoneStartEpochSecs = zoneStart.toEpochSecond(wallOffset);
819 for (TransRule trule : trules) {
820 if (trule.isTransition(savings)) {
821 long epochSecs = trule.toEpochSecond(stdOffset, savings);
822 if (epochSecs < zoneStartEpochSecs ||
823 epochSecs >= zone.toDateTimeEpochSecond(savings)) {
824 continue;
825 }
826 transitionList.add(trule.toTransition(stdOffset, savings));
827 savings = trule.rule.savingsAmount;
828 }
829 }
830 }
831 if (lastRules != null) {
832 for (TransRule trule : lastRules) {
833 lastTransitionRuleList.add(trule.rule.toTransitionRule(stdOffset, savings));
834 savings = trule.rule.savingsAmount;
835 }
836 }
837
838 // finally we can calculate the true end of the window, passing it to the next window
839 wallOffset = ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + savings);
840 zoneStart = LocalDateTime.ofEpochSecond(zone.toDateTimeEpochSecond(savings),
841 0,
842 wallOffset);
843 }
844 return new ZoneRules(firstStdOffset,
845 firstWallOffset,
846 standardTransitionList,
847 transitionList,
848 lastTransitionRuleList);
849 }
850
851 }
|
1 /*
2 * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26
27 package build.tools.tzdb;
28
29 import java.io.IOException;
30 import java.nio.charset.StandardCharsets;
31 import java.nio.file.Files;
32 import java.nio.file.Path;
33 import java.nio.file.Paths;
34 import java.util.*;
35 import java.util.Map.Entry;
36 import java.util.concurrent.ConcurrentSkipListMap;
37 import java.time.*;
38 import java.time.Year;
39 import java.time.chrono.IsoChronology;
40 import java.time.temporal.TemporalAdjusters;
41 import build.tools.tzdb.ZoneOffsetTransitionRule.TimeDefinition;
42 import java.time.zone.ZoneRulesException;
43
44 /**
45 * Compile and build time-zone rules from IANA timezone data
46 *
47 * @author Xueming Shen
48 * @author Stephen Colebourne
49 * @author Michael Nascimento Santos
50 *
51 * @since 9
52 */
53
54 class TzdbZoneRulesProvider {
55
56 /**
57 * Creates an instance.
58 *
59 * @throws ZoneRulesException if unable to load
60 */
61 public TzdbZoneRulesProvider(List<Path> files) {
245 * Class representing a month-day-time in the TZDB file.
246 */
247 private static abstract class MonthDayTime {
248 /** The month of the cutover. */
249 Month month = Month.JANUARY;
250
251 /** The day-of-month of the cutover. */
252 int dayOfMonth = 1;
253
254 /** Whether to adjust forwards. */
255 boolean adjustForwards = true;
256
257 /** The day-of-week of the cutover. */
258 DayOfWeek dayOfWeek;
259
260 /** The time of the cutover, in second of day */
261 int secsOfDay = 0;
262
263 /** Whether this is midnight end of day. */
264 boolean endOfDay;
265
266 /** The time definition of the cutover. */
267 TimeDefinition timeDefinition = TimeDefinition.WALL;
268
269 void adjustToForwards(int year) {
270 if (adjustForwards == false && dayOfMonth > 0) {
271 // weekDay<=monthDay case, don't have it in tzdb data for now
272 LocalDate adjustedDate = LocalDate.of(year, month, dayOfMonth).minusDays(6);
273 dayOfMonth = adjustedDate.getDayOfMonth();
274 month = adjustedDate.getMonth();
275 adjustForwards = true;
276 }
277 }
278
279 LocalDateTime toDateTime(int year) {
280 LocalDate date;
281 if (dayOfMonth < 0) {
282 int monthLen = month.length(IsoChronology.INSTANCE.isLeapYear(year));
283 date = LocalDate.of(year, month, monthLen + 1 + dayOfMonth);
284 if (dayOfWeek != null) {
285 date = date.with(TemporalAdjusters.previousOrSame(dayOfWeek));
286 }
316 index = dayRule.indexOf("<=");
317 if (index > 0) {
318 dayOfWeek = parseDayOfWeek(dayRule.substring(0, index));
319 adjustForwards = false;
320 dayRule = dayRule.substring(index + 2);
321 }
322 }
323 dayOfMonth = Integer.parseInt(dayRule);
324 if (dayOfMonth < -28 || dayOfMonth > 31 || dayOfMonth == 0) {
325 throw new IllegalArgumentException(
326 "Day of month indicator must be between -28 and 31 inclusive excluding zero");
327 }
328 }
329 if (off < tokens.length) {
330 String timeStr = tokens[off++];
331 secsOfDay = parseSecs(timeStr);
332 if (secsOfDay == 86400) {
333 // time must be midnight when end of day flag is true
334 endOfDay = true;
335 secsOfDay = 0;
336 } else if (secsOfDay < 0 || secsOfDay > 86400) {
337 // beyond 0:00-24:00 range. Adjust the cutover date.
338 int beyondDays = secsOfDay / 86400;
339 secsOfDay %= 86400;
340 if (secsOfDay < 0) {
341 secsOfDay = 86400 + secsOfDay;
342 beyondDays -= 1;
343 }
344 LocalDate date = LocalDate.of(2004, month, dayOfMonth).plusDays(beyondDays); // leap-year
345 month = date.getMonth();
346 dayOfMonth = date.getDayOfMonth();
347 if (dayOfWeek != null) {
348 dayOfWeek = dayOfWeek.plus(beyondDays);
349 }
350 }
351 timeDefinition = parseTimeDefinition(timeStr.charAt(timeStr.length() - 1));
352 }
353 }
354 }
355
356 int parseYear(String year, int defaultYear) {
357 switch (year.toLowerCase()) {
358 case "min": return 1900;
359 case "max": return Year.MAX_VALUE;
360 case "only": return defaultYear;
361 }
362 return Integer.parseInt(year);
363 }
364
365 Month parseMonth(String mon) {
366 switch (mon) {
367 case "Jan": return Month.JANUARY;
368 case "Feb": return Month.FEBRUARY;
369 case "Mar": return Month.MARCH;
484 * Class representing a rule line in the TZDB file.
485 */
486 private static class RuleLine extends MonthDayTime {
487 /** The start year. */
488 int startYear;
489
490 /** The end year. */
491 int endYear;
492
493 /** The amount of savings, in seconds. */
494 int savingsAmount;
495
496 /** The text name of the zone. */
497 String text;
498
499 /**
500 * Converts this to a transition rule.
501 *
502 * @param standardOffset the active standard offset, not null
503 * @param savingsBeforeSecs the active savings before the transition in seconds
504 * @param negativeSavings minimum savings in the rule, usually zero, but negative if negative DST is
505 * in effect.
506 * @return the transition, not null
507 */
508 ZoneOffsetTransitionRule toTransitionRule(ZoneOffset stdOffset, int savingsBefore, int negativeSavings) {
509 // rule shared by different zones, so don't change it
510 Month month = this.month;
511 int dayOfMonth = this.dayOfMonth;
512 DayOfWeek dayOfWeek = this.dayOfWeek;
513 boolean endOfDay = this.endOfDay;
514
515 // optimize stored format
516 if (dayOfMonth < 0) {
517 if (month != Month.FEBRUARY) { // not Month.FEBRUARY
518 dayOfMonth = month.maxLength() - 6;
519 }
520 }
521 if (endOfDay && dayOfMonth > 0 &&
522 (dayOfMonth == 28 && month == Month.FEBRUARY) == false) {
523 LocalDate date = LocalDate.of(2004, month, dayOfMonth).plusDays(1); // leap-year
524 month = date.getMonth();
525 dayOfMonth = date.getDayOfMonth();
526 if (dayOfWeek != null) {
527 dayOfWeek = dayOfWeek.plus(1);
528 }
529 endOfDay = false;
530 }
531
532 // build rule
533 return ZoneOffsetTransitionRule.of(
534 //month, dayOfMonth, dayOfWeek, time, endOfDay, timeDefinition,
535 month, dayOfMonth, dayOfWeek,
536 LocalTime.ofSecondOfDay(secsOfDay), endOfDay, timeDefinition,
537 stdOffset,
538 ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + savingsBefore),
539 ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + savingsAmount - negativeSavings));
540 }
541
542 RuleLine parse(String[] tokens) {
543 startYear = parseYear(tokens[2], 0);
544 endYear = parseYear(tokens[3], startYear);
545 if (startYear > endYear) {
546 throw new IllegalArgumentException(
547 "Invalid <Rule> line/Year order invalid:" + startYear + " > " + endYear);
548 }
549 //parseOptional(s.next()); // type is unused
550 super.parse(tokens, 5); // monthdaytime parsing
551 savingsAmount = parsePeriod(tokens[8]);
552 //rule.text = parseOptional(s.next());
553 return this;
554 }
555 }
556
557 /**
558 * Class representing a linked set of zone lines in the TZDB file.
559 */
633 * Class representing a rule line in the TZDB file for a particular year.
634 */
635 private static class TransRule implements Comparable<TransRule>
636 {
637 private int year;
638 private RuleLine rule;
639
640 /** The trans date/time */
641 private LocalDateTime ldt;
642
643 /** The trans date/time in epoch seconds (assume UTC) */
644 long ldtSecs;
645
646 TransRule(int year, RuleLine rule) {
647 this.year = year;
648 this.rule = rule;
649 this.ldt = rule.toDateTime(year);
650 this.ldtSecs = ldt.toEpochSecond(ZoneOffset.UTC);
651 }
652
653 ZoneOffsetTransition toTransition(ZoneOffset standardOffset, int savingsBeforeSecs, int negativeSavings) {
654 // copy of code in ZoneOffsetTransitionRule to avoid infinite loop
655 ZoneOffset wallOffset = ZoneOffset.ofTotalSeconds(
656 standardOffset.getTotalSeconds() + savingsBeforeSecs);
657 ZoneOffset offsetAfter = ZoneOffset.ofTotalSeconds(
658 standardOffset.getTotalSeconds() + rule.savingsAmount - negativeSavings);
659 LocalDateTime dt = rule.timeDefinition
660 .createDateTime(ldt, standardOffset, wallOffset);
661 return ZoneOffsetTransition.of(dt, wallOffset, offsetAfter);
662 }
663
664 long toEpochSecond(ZoneOffset stdOffset, int savingsBeforeSecs) {
665 switch(rule.timeDefinition) {
666 case UTC: return ldtSecs;
667 case STANDARD: return ldtSecs - stdOffset.getTotalSeconds();
668 default: return ldtSecs - (stdOffset.getTotalSeconds() + savingsBeforeSecs); // WALL
669 }
670 }
671
672 /**
673 * Tests if this a real transition with the active savings in seconds
674 *
675 * @param savingsBefore the active savings in seconds
676 * @param negativeSavings minimum savings in the rule, usually zero, but negative if negative DST is
677 * in effect.
678 * @return true, if savings changes
679 */
680 boolean isTransition(int savingsBefore, int negativeSavings) {
681 return rule.savingsAmount - negativeSavings != savingsBefore;
682 }
683
684 public int compareTo(TransRule other) {
685 return (ldtSecs < other.ldtSecs)? -1 : ((ldtSecs == other.ldtSecs) ? 0 : 1);
686 }
687 }
688
689 private ZoneRules buildRules(String zoneId, List<ZoneLine> zones) {
690 if (zones.isEmpty()) {
691 throw new IllegalStateException("No available zone window");
692 }
693 final List<ZoneOffsetTransition> standardTransitionList = new ArrayList<>(4);
694 final List<ZoneOffsetTransition> transitionList = new ArrayList<>(256);
695 final List<ZoneOffsetTransitionRule> lastTransitionRuleList = new ArrayList<>(2);
696
697 final ZoneLine zone0 = zones.get(0);
698 // initialize the standard offset, wallOffset and savings for loop
699
700 //ZoneOffset stdOffset = zone0.standardOffset;
701 ZoneOffset stdOffset = ZoneOffset.ofTotalSeconds(zone0.stdOffsetSecs);
702
703 int savings = zone0.fixedSavingsSecs;
704 ZoneOffset wallOffset = ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + savings);
705
706 // start ldt of each zone window
707 LocalDateTime zoneStart = LocalDateTime.MIN;
708
709 // first standard offset
710 ZoneOffset firstStdOffset = stdOffset;
711 // first wall offset
712 ZoneOffset firstWallOffset = wallOffset;
713
714 for (ZoneLine zone : zones) {
715 // Adjust stdOffset, if negative DST is observed. It should be either
716 // fixed amount, or expressed in the named Rules.
717 int negativeSavings = Math.min(zone.fixedSavingsSecs, findNegativeSavings(zoneStart, zone));
718 if (negativeSavings < 0) {
719 zone.stdOffsetSecs += negativeSavings;
720 if (zone.fixedSavingsSecs < 0) {
721 zone.fixedSavingsSecs = 0;
722 }
723 }
724
725 // check if standard offset changed, update it if yes
726 ZoneOffset stdOffsetPrev = stdOffset; // for effectiveSavings check
727 if (zone.stdOffsetSecs != stdOffset.getTotalSeconds()) {
728 ZoneOffset stdOffsetNew = ZoneOffset.ofTotalSeconds(zone.stdOffsetSecs);
729 standardTransitionList.add(
730 ZoneOffsetTransition.of(
731 LocalDateTime.ofEpochSecond(zoneStart.toEpochSecond(wallOffset),
732 0,
733 stdOffset),
734 stdOffset,
735 stdOffsetNew));
736 stdOffset = stdOffsetNew;
737 }
738
739 LocalDateTime zoneEnd;
740 if (zone.year == Year.MAX_VALUE) {
741 zoneEnd = LocalDateTime.MAX;
742 } else {
743 zoneEnd = zone.toDateTime();
744 }
793 // last rules, fill the gap years between different last rules
794 if (zoneEnd.equals(LocalDateTime.MAX)) {
795 lastRulesStartYear = Math.max(lastRulesStartYear, zoneStart.getYear()) + 1;
796 for (TransRule rule : lastRules) {
797 if (rule.year <= lastRulesStartYear) {
798 int year = rule.year;
799 while (year <= lastRulesStartYear) {
800 trules.add(new TransRule(year, rule.rule));
801 year++;
802 }
803 rule.year = lastRulesStartYear;
804 rule.ldt = rule.rule.toDateTime(year);
805 rule.ldtSecs = rule.ldt.toEpochSecond(ZoneOffset.UTC);
806 }
807 }
808 Collections.sort(lastRules);
809 }
810 // sort the merged rules
811 Collections.sort(trules);
812
813 effectiveSavings = -negativeSavings;
814 for (TransRule rule : trules) {
815 if (rule.toEpochSecond(stdOffsetPrev, savings) >
816 zoneStart.toEpochSecond(wallOffset)) {
817 // previous savings amount found, which could be the
818 // savings amount at the instant that the window starts
819 // (hence isAfter)
820 break;
821 }
822 effectiveSavings = rule.rule.savingsAmount - negativeSavings;
823 }
824 }
825 // check if the start of the window represents a transition
826 ZoneOffset effectiveWallOffset =
827 ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + effectiveSavings);
828
829 if (!wallOffset.equals(effectiveWallOffset)) {
830 transitionList.add(ZoneOffsetTransition.of(zoneStart,
831 wallOffset,
832 effectiveWallOffset));
833 }
834 savings = effectiveSavings;
835 // apply rules within the window
836 if (trules != null) {
837 long zoneStartEpochSecs = zoneStart.toEpochSecond(wallOffset);
838 for (TransRule trule : trules) {
839 if (trule.isTransition(savings, negativeSavings)) {
840 long epochSecs = trule.toEpochSecond(stdOffset, savings);
841 if (epochSecs < zoneStartEpochSecs ||
842 epochSecs >= zone.toDateTimeEpochSecond(savings)) {
843 continue;
844 }
845 transitionList.add(trule.toTransition(stdOffset, savings, negativeSavings));
846 savings = trule.rule.savingsAmount - negativeSavings;
847 }
848 }
849 }
850 if (lastRules != null) {
851 for (TransRule trule : lastRules) {
852 lastTransitionRuleList.add(trule.rule.toTransitionRule(stdOffset, savings, negativeSavings));
853 savings = trule.rule.savingsAmount - negativeSavings;
854 }
855 }
856
857 // finally we can calculate the true end of the window, passing it to the next window
858 wallOffset = ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + savings);
859 zoneStart = LocalDateTime.ofEpochSecond(zone.toDateTimeEpochSecond(savings),
860 0,
861 wallOffset);
862 }
863 return new ZoneRules(firstStdOffset,
864 firstWallOffset,
865 standardTransitionList,
866 transitionList,
867 lastTransitionRuleList);
868 }
869
870 /**
871 * Find the minimum negative savings in named Rules for a Zone. Savings are only
872 * looked at for the period of the subject Zone.
873 *
874 * @param zoneStart start LDT of the zone
875 * @param zl ZoneLine to look at
876 */
877 private int findNegativeSavings(LocalDateTime zoneStart, ZoneLine zl) {
878 int negativeSavings = 0;
879 LocalDateTime zoneEnd = zl.toDateTime();
880
881 if (zl.savingsRule != null) {
882 List<RuleLine> rlines = rules.get(zl.savingsRule);
883 if (rlines == null) {
884 throw new IllegalArgumentException("<Rule> not found: " +
885 zl.savingsRule);
886 }
887
888 negativeSavings = Math.min(0, rlines.stream()
889 .filter(l -> windowOverlap(l, zoneStart.getYear(), zoneEnd.getYear()))
890 .map(l -> l.savingsAmount)
891 .min(Comparator.naturalOrder())
892 .orElse(0));
893 }
894
895 return negativeSavings;
896 }
897
898 private boolean windowOverlap(RuleLine ruleLine, int zoneStartYear, int zoneEndYear) {
899 boolean overlap = zoneStartYear <= ruleLine.startYear && zoneEndYear >= ruleLine.startYear ||
900 zoneStartYear <= ruleLine.endYear && zoneEndYear >= ruleLine.endYear;
901
902 return overlap;
903 }
904 }
|