1 /*
2 * Copyright (c) 2014, 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.HashMap;
37 import java.util.HashSet;
38 import java.util.List;
39 import java.util.Map;
40 import java.util.Map.Entry;
41 import java.util.NavigableMap;
42 import java.util.Objects;
43 import java.util.Set;
44 import java.util.TreeMap;
45 import java.util.TreeSet;
46 import java.util.concurrent.ConcurrentHashMap;
47 import java.time.*;
48 import java.time.Year;
49 import java.time.chrono.IsoChronology;
50 import java.time.temporal.TemporalAdjusters;
51 import java.time.zone.ZoneOffsetTransition;
52 import java.time.zone.ZoneOffsetTransitionRule;
53 import java.time.zone.ZoneOffsetTransitionRule.TimeDefinition;
54 import java.time.zone.ZoneRulesException;
55
56 /**
57 * Compile and build time-zone rules from IANA timezone data
58 *
59 * @author Xueming Shen
60 * @author Stephen Colebourne
61 * @author Michael Nascimento Santos
62 *
63 * @since 1.9
64 */
65
66 class TzdbZoneRulesProvider {
67
68 /**
69 * Creates an instance.
70 *
71 * @throws ZoneRulesException if unable to load
72 */
73 public TzdbZoneRulesProvider(List<Path> files) {
74 try {
75 load(files);
76 } catch (Exception ex) {
77 throw new ZoneRulesException("Unable to load TZDB time-zone rules", ex);
78 }
79 }
80
81 public Set<String> getZoneIds() {
82 return new TreeSet(regionIds);
83 }
84
85 public Map<String, String> getAliasMap() {
86 return links;
87 }
88
89 public ZoneRules getZoneRules(String zoneId) {
90 Object obj = zones.get(zoneId);
91 if (obj == null) {
92 String zoneId0 = zoneId;
93 if (links.containsKey(zoneId)) {
94 zoneId = links.get(zoneId);
95 obj = zones.get(zoneId);
96 }
97 if (obj == null) {
98 // Timezone link can be located in 'backward' file and it
99 // can refer to another link, so we need to check for
100 // link one more time, before throwing an exception
101 String zoneIdBack = zoneId;
102 if (links.containsKey(zoneId)) {
103 zoneId = links.get(zoneId);
104 obj = zones.get(zoneId);
105 }
106 if (obj == null) {
107 throw new ZoneRulesException("Unknown time-zone ID: " + zoneIdBack);
108 }
109 }
110 }
111 if (obj instanceof ZoneRules) {
112 return (ZoneRules)obj;
113 }
114 try {
115 ZoneRules zrules = buildRules(zoneId, (List<ZoneLine>)obj);
116 zones.put(zoneId, zrules);
117 return zrules;
118 } catch (Exception ex) {
119 throw new ZoneRulesException(
120 "Invalid binary time-zone data: TZDB:" + zoneId, ex);
121 }
122 }
123
124 //////////////////////////////////////////////////////////////////////
125
126 /**
127 * All the regions that are available.
128 */
129 private List<String> regionIds = new ArrayList<>(600);
130
131 /**
132 * Zone region to rules mapping
133 */
134 private final Map<String, Object> zones = new ConcurrentHashMap<>();
135
136 /**
137 * compatibility list
138 */
139 private static HashSet<String> excludedZones;
140 static {
141 // (1) exclude EST, HST and MST. They are supported
142 // via the short-id mapping
143 // (2) remove UTC and GMT
144 // (3) remove ROC, which is not supported in j.u.tz
145 excludedZones = new HashSet<>(10);
146 excludedZones.add("EST");
147 excludedZones.add("HST");
148 excludedZones.add("MST");
149 excludedZones.add("GMT+0");
150 excludedZones.add("GMT-0");
151 excludedZones.add("ROC");
152 }
153
154 private Map<String, String> links = new HashMap<>(150);
155 private Map<String, List<RuleLine>> rules = new HashMap<>(500);
156
157 private void load(List<Path> files) throws IOException {
158
159 for (Path file : files) {
160 List<ZoneLine> openZone = null;
161 try {
162 for (String line : Files.readAllLines(file, StandardCharsets.ISO_8859_1)) {
163 if (line.length() == 0 || line.charAt(0) == '#') {
164 continue;
165 }
166 //StringIterator itr = new StringIterator(line);
167 String[] tokens = split(line);
168 if (openZone != null && // continuing zone line
169 Character.isWhitespace(line.charAt(0)) &&
170 tokens.length > 0) {
171 ZoneLine zLine = new ZoneLine();
172 openZone.add(zLine);
173 if (zLine.parse(tokens, 0)) {
174 openZone = null;
175 }
176 continue;
177 }
178 if (line.startsWith("Zone")) { // parse Zone line
179 String name = tokens[1];
180 if (excludedZones.contains(name)){
181 continue;
182 }
183 if (zones.containsKey(name)) {
184 throw new IllegalArgumentException(
185 "Duplicated zone name in file: " + name +
186 ", line: [" + line + "]");
187 }
188 openZone = new ArrayList<>(10);
189 zones.put(name, openZone);
190 regionIds.add(name);
191 ZoneLine zLine = new ZoneLine();
192 openZone.add(zLine);
193 if (zLine.parse(tokens, 2)) {
194 openZone = null;
195 }
196 } else if (line.startsWith("Rule")) { // parse Rule line
197 String name = tokens[1];
198 if (!rules.containsKey(name)) {
199 rules.put(name, new ArrayList<RuleLine>(10));
200 }
201 rules.get(name).add(new RuleLine().parse(tokens));
202 } else if (line.startsWith("Link")) { // parse link line
203 if (tokens.length >= 3) {
204 String realId = tokens[1];
205 String aliasId = tokens[2];
206 if (excludedZones.contains(aliasId)){
207 continue;
208 }
209 links.put(aliasId, realId);
210 regionIds.add(aliasId);
211 } else {
212 throw new IllegalArgumentException(
213 "Invalid Link line in file" +
214 file + ", line: [" + line + "]");
215 }
216 } else {
217 // skip unknown line
218 }
219 }
220
221 } catch (Exception ex) {
222 throw new RuntimeException("Failed while processing file [" + file +
223 "]", ex);
224 }
225 }
226 }
227
228 private String[] split(String str) {
229 int off = 0;
230 int end = str.length();
231 ArrayList<String> list = new ArrayList<>(10);
232 while (off < end) {
233 char c = str.charAt(off);
234 if (c == '\t' || c == ' ') {
235 off++;
236 continue;
237 }
238 if (c == '#') { // comment
239 break;
240 }
241 int start = off;
242 while (off < end) {
243 c = str.charAt(off);
244 if (c == ' ' || c == '\t') {
245 break;
246 }
247 off++;
248 }
249 if (start != off) {
250 list.add(str.substring(start, off));
251 }
252 }
253 return list.toArray(new String[list.size()]);
254 }
255
256 /**
257 * Class representing a month-day-time in the TZDB file.
258 */
259 private static abstract class MonthDayTime {
260 /** The month of the cutover. */
261 Month month = Month.JANUARY;
262
263 /** The day-of-month of the cutover. */
264 int dayOfMonth = 1;
265
266 /** Whether to adjust forwards. */
267 boolean adjustForwards = true;
268
269 /** The day-of-week of the cutover. */
270 DayOfWeek dayOfWeek;
271
272 /** The time of the cutover, in second of day */
273 int secsOfDay = 0;
274
275 /** Whether this is midnight end of day. */
276 boolean endOfDay;
277 /** The time of the cutover. */
278
279 TimeDefinition timeDefinition = TimeDefinition.WALL;
280
281 void adjustToForwards(int year) {
282 if (adjustForwards == false && dayOfMonth > 0) {
283 // weekDay<=monthDay case, don't have it in tzdb data for now
284 LocalDate adjustedDate = LocalDate.of(year, month, dayOfMonth).minusDays(6);
285 dayOfMonth = adjustedDate.getDayOfMonth();
286 month = adjustedDate.getMonth();
287 adjustForwards = true;
288 }
289 }
290
291 LocalDateTime toDateTime(int year) {
292 LocalDate date;
293 if (dayOfMonth < 0) {
294 int monthLen = month.length(IsoChronology.INSTANCE.isLeapYear(year));
295 date = LocalDate.of(year, month, monthLen + 1 + dayOfMonth);
296 if (dayOfWeek != null) {
297 date = date.with(TemporalAdjusters.previousOrSame(dayOfWeek));
298 }
299 } else {
300 date = LocalDate.of(year, month, dayOfMonth);
301 if (dayOfWeek != null) {
302 date = date.with(TemporalAdjusters.nextOrSame(dayOfWeek));
303 }
304 }
305 if (endOfDay) {
306 date = date.plusDays(1);
307 }
308 return LocalDateTime.of(date, LocalTime.ofSecondOfDay(secsOfDay));
309 }
310
311 /**
312 * Parses the MonthDaytime segment of a tzdb line.
313 */
314 private void parse(String[] tokens, int off) {
315 month = parseMonth(tokens[off++]);
316 if (off < tokens.length) {
317 String dayRule = tokens[off++];
318 if (dayRule.startsWith("last")) {
319 dayOfMonth = -1;
320 dayOfWeek = parseDayOfWeek(dayRule.substring(4));
321 adjustForwards = false;
322 } else {
323 int index = dayRule.indexOf(">=");
324 if (index > 0) {
325 dayOfWeek = parseDayOfWeek(dayRule.substring(0, index));
326 dayRule = dayRule.substring(index + 2);
327 } else {
328 index = dayRule.indexOf("<=");
329 if (index > 0) {
330 dayOfWeek = parseDayOfWeek(dayRule.substring(0, index));
331 adjustForwards = false;
332 dayRule = dayRule.substring(index + 2);
333 }
334 }
335 dayOfMonth = Integer.parseInt(dayRule);
336 if (dayOfMonth < -28 || dayOfMonth > 31 || dayOfMonth == 0) {
337 throw new IllegalArgumentException(
338 "Day of month indicator must be between -28 and 31 inclusive excluding zero");
339 }
340 }
341 if (off < tokens.length) {
342 String timeStr = tokens[off++];
343 secsOfDay = parseSecs(timeStr);
344 if (secsOfDay == 86400) {
345 // time must be midnight when end of day flag is true
346 endOfDay = true;
347 secsOfDay = 0;
348 }
349 timeDefinition = parseTimeDefinition(timeStr.charAt(timeStr.length() - 1));
350 }
351 }
352 }
353
354 int parseYear(String year, int defaultYear) {
355 switch (year.toLowerCase()) {
356 case "min": return 1900;
357 case "max": return Year.MAX_VALUE;
358 case "only": return defaultYear;
359 }
360 return Integer.parseInt(year);
361 }
362
363 Month parseMonth(String mon) {
364 switch (mon) {
365 case "Jan": return Month.JANUARY;
366 case "Feb": return Month.FEBRUARY;
367 case "Mar": return Month.MARCH;
368 case "Apr": return Month.APRIL;
369 case "May": return Month.MAY;
370 case "Jun": return Month.JUNE;
371 case "Jul": return Month.JULY;
372 case "Aug": return Month.AUGUST;
373 case "Sep": return Month.SEPTEMBER;
374 case "Oct": return Month.OCTOBER;
375 case "Nov": return Month.NOVEMBER;
376 case "Dec": return Month.DECEMBER;
377 }
378 throw new IllegalArgumentException("Unknown month: " + mon);
379 }
380
381 DayOfWeek parseDayOfWeek(String dow) {
382 switch (dow) {
383 case "Mon": return DayOfWeek.MONDAY;
384 case "Tue": return DayOfWeek.TUESDAY;
385 case "Wed": return DayOfWeek.WEDNESDAY;
386 case "Thu": return DayOfWeek.THURSDAY;
387 case "Fri": return DayOfWeek.FRIDAY;
388 case "Sat": return DayOfWeek.SATURDAY;
389 case "Sun": return DayOfWeek.SUNDAY;
390 }
391 throw new IllegalArgumentException("Unknown day-of-week: " + dow);
392 }
393
394 String parseOptional(String str) {
395 return str.equals("-") ? null : str;
396 }
397
398 static final boolean isDigit(char c) {
399 return c >= '0' && c <= '9';
400 }
401
402 private int parseSecs(String time) {
403 if (time.equals("-")) {
404 return 0;
405 }
406 // faster hack
407 int secs = 0;
408 int sign = 1;
409 int off = 0;
410 int len = time.length();
411 if (off < len && time.charAt(off) == '-') {
412 sign = -1;
413 off++;
414 }
415 char c0, c1;
416 if (off < len && isDigit(c0 = time.charAt(off++))) {
417 int hour = c0 - '0';
418 if (off < len && isDigit(c1 = time.charAt(off))) {
419 hour = hour * 10 + c1 - '0';
420 off++;
421 }
422 secs = hour * 60 * 60;
423 if (off < len && time.charAt(off++) == ':') {
424 if (off + 1 < len &&
425 isDigit(c0 = time.charAt(off++)) &&
426 isDigit(c1 = time.charAt(off++))) {
427 // minutes
428 secs += ((c0 - '0') * 10 + c1 - '0') * 60;
429 if (off < len && time.charAt(off++) == ':') {
430 if (off + 1 < len &&
431 isDigit(c0 = time.charAt(off++)) &&
432 isDigit(c1 = time.charAt(off++))) {
433 // seconds
434 secs += ((c0 - '0') * 10 + c1 - '0');
435 }
436 }
437 }
438
439 }
440 return secs * sign;
441 }
442 throw new IllegalArgumentException("[" + time + "]");
443 }
444
445 int parseOffset(String str) {
446 int secs = parseSecs(str);
447 if (Math.abs(secs) > 18 * 60 * 60) {
448 throw new IllegalArgumentException(
449 "Zone offset not in valid range: -18:00 to +18:00");
450 }
451 return secs;
452 }
453
454 int parsePeriod(String str) {
455 return parseSecs(str);
456 }
457
458 TimeDefinition parseTimeDefinition(char c) {
459 switch (c) {
460 case 's':
461 case 'S':
462 // standard time
463 return TimeDefinition.STANDARD;
464 case 'u':
465 case 'U':
466 case 'g':
467 case 'G':
468 case 'z':
469 case 'Z':
470 // UTC
471 return TimeDefinition.UTC;
472 case 'w':
473 case 'W':
474 default:
475 // wall time
476 return TimeDefinition.WALL;
477 }
478 }
479 }
480
481 /**
482 * Class representing a rule line in the TZDB file.
483 */
484 private static class RuleLine extends MonthDayTime {
485 /** The start year. */
486 int startYear;
487
488 /** The end year. */
489 int endYear;
490
491 /** The amount of savings, in seconds. */
492 int savingsAmount;
493
494 /** The text name of the zone. */
495 String text;
496
497 /**
498 * Converts this to a transition rule.
499 *
500 * @param standardOffset the active standard offset, not null
501 * @param savingsBeforeSecs the active savings before the transition in seconds
502 * @return the transition, not null
503 */
504 ZoneOffsetTransitionRule toTransitionRule(ZoneOffset stdOffset, int savingsBefore) {
505 // rule shared by different zones, so don't change it
506 Month month = this.month;
507 int dayOfMonth = this.dayOfMonth;
508 DayOfWeek dayOfWeek = this.dayOfWeek;
509 boolean endOfDay = this.endOfDay;
510
511 // optimize stored format
512 if (dayOfMonth < 0) {
513 if (month != Month.FEBRUARY) { // not Month.FEBRUARY
514 dayOfMonth = month.maxLength() - 6;
515 }
516 }
517 if (endOfDay && dayOfMonth > 0 &&
518 (dayOfMonth == 28 && month == Month.FEBRUARY) == false) {
519 LocalDate date = LocalDate.of(2004, month, dayOfMonth).plusDays(1); // leap-year
520 month = date.getMonth();
521 dayOfMonth = date.getDayOfMonth();
522 if (dayOfWeek != null) {
523 dayOfWeek = dayOfWeek.plus(1);
524 }
525 endOfDay = false;
526 }
527 // build rule
528 return ZoneOffsetTransitionRule.of(
529 //month, dayOfMonth, dayOfWeek, time, endOfDay, timeDefinition,
530 month, dayOfMonth, dayOfWeek,
531 LocalTime.ofSecondOfDay(secsOfDay), endOfDay, timeDefinition,
532 stdOffset,
533 ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + savingsBefore),
534 ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + savingsAmount));
535 }
536
537 RuleLine parse(String[] tokens) {
538 startYear = parseYear(tokens[2], 0);
539 endYear = parseYear(tokens[3], startYear);
540 if (startYear > endYear) {
541 throw new IllegalArgumentException(
542 "Invalid <Rule> line/Year order invalid:" + startYear + " > " + endYear);
543 }
544 //parseOptional(s.next()); // type is unused
545 super.parse(tokens, 5); // monthdaytime parsing
546 savingsAmount = parsePeriod(tokens[8]);
547 //rule.text = parseOptional(s.next());
548 return this;
549 }
550 }
551
552 /**
553 * Class representing a linked set of zone lines in the TZDB file.
554 */
555 private static class ZoneLine extends MonthDayTime {
556 /** The standard offset. */
557 int stdOffsetSecs;
558
559 /** The fixed savings amount. */
560 int fixedSavingsSecs = 0;
561
562 /** The savings rule. */
563 String savingsRule;
564
565 /** The text name of the zone. */
566 String text;
567
568 /** The cutover year */
569 int year = Year.MAX_VALUE;
570
571 /** The cutover date time */
572 LocalDateTime ldt;
573
574 /** The cutover date/time in epoch seconds/UTC */
575 long ldtSecs = Long.MIN_VALUE;
576
577 LocalDateTime toDateTime() {
578 if (ldt == null) {
579 ldt = toDateTime(year);
580 }
581 return ldt;
582 }
583
584 /**
585 * Creates the date-time epoch second in the wall offset for the local
586 * date-time at the end of the window.
587 *
588 * @param savingsSecs the amount of savings in use in seconds
589 * @return the created date-time epoch second in the wall offset, not null
590 */
591 long toDateTimeEpochSecond(int savingsSecs) {
592 if (ldtSecs == Long.MIN_VALUE) {
593 ldtSecs = toDateTime().toEpochSecond(ZoneOffset.UTC);
594 }
595 switch(timeDefinition) {
596 case UTC: return ldtSecs;
597 case STANDARD: return ldtSecs - stdOffsetSecs;
598 default: return ldtSecs - (stdOffsetSecs + savingsSecs); // WALL
599 }
600 }
601
602 boolean parse(String[] tokens, int off) {
603 stdOffsetSecs = parseOffset(tokens[off++]);
604 savingsRule = parseOptional(tokens[off++]);
605 if (savingsRule != null && savingsRule.length() > 0 &&
606 (savingsRule.charAt(0) == '-' || isDigit(savingsRule.charAt(0)))) {
607 try {
608 fixedSavingsSecs = parsePeriod(savingsRule);
609 savingsRule = null;
610 } catch (Exception ex) {
611 fixedSavingsSecs = 0;
612 }
613 }
614 text = tokens[off++];
615 if (off < tokens.length) {
616 year = Integer.parseInt(tokens[off++]);
617 if (off < tokens.length) {
618 super.parse(tokens, off); // MonthDayTime
619 }
620 return false;
621 } else {
622 return true;
623 }
624 }
625 }
626
627 /**
628 * Class representing a rule line in the TZDB file for a particular year.
629 */
630 private static class TransRule implements Comparable<TransRule>
631 {
632 private int year;
633 private RuleLine rule;
634
635 /** The trans date/time */
636 private LocalDateTime ldt;
637
638 /** The trans date/time in epoch seconds (assume UTC) */
639 long ldtSecs;
640
641 TransRule(int year, RuleLine rule) {
642 this.year = year;
643 this.rule = rule;
644 this.ldt = rule.toDateTime(year);
645 this.ldtSecs = ldt.toEpochSecond(ZoneOffset.UTC);
646 }
647
648 ZoneOffsetTransition toTransition(ZoneOffset standardOffset, int savingsBeforeSecs) {
649 // copy of code in ZoneOffsetTransitionRule to avoid infinite loop
650 ZoneOffset wallOffset = ZoneOffset.ofTotalSeconds(
651 standardOffset.getTotalSeconds() + savingsBeforeSecs);
652 ZoneOffset offsetAfter = ZoneOffset.ofTotalSeconds(
653 standardOffset.getTotalSeconds() + rule.savingsAmount);
654 LocalDateTime dt = rule.timeDefinition
655 .createDateTime(ldt, standardOffset, wallOffset);
656 return ZoneOffsetTransition.of(dt, wallOffset, offsetAfter);
657 }
658
659 long toEpochSecond(ZoneOffset stdOffset, int savingsBeforeSecs) {
660 switch(rule.timeDefinition) {
661 case UTC: return ldtSecs;
662 case STANDARD: return ldtSecs - stdOffset.getTotalSeconds();
663 default: return ldtSecs - (stdOffset.getTotalSeconds() + savingsBeforeSecs); // WALL
664 }
665 }
666
667 /**
668 * Tests if this a real transition with the active savings in seconds
669 *
670 * @param savingsBefore the active savings in seconds
671 * @return true, if savings changes
672 */
673 boolean isTransition(int savingsBefore) {
674 return rule.savingsAmount != savingsBefore;
675 }
676
677 public int compareTo(TransRule other) {
678 return (ldtSecs < other.ldtSecs)? -1 : ((ldtSecs == other.ldtSecs) ? 0 : 1);
679 }
680 }
681
682 private ZoneRules buildRules(String zoneId, List<ZoneLine> zones) {
683 if (zones.isEmpty()) {
684 throw new IllegalStateException("No available zone window");
685 }
686 final List<ZoneOffsetTransition> standardTransitionList = new ArrayList<>(4);
687 final List<ZoneOffsetTransition> transitionList = new ArrayList<>(256);
688 final List<ZoneOffsetTransitionRule> lastTransitionRuleList = new ArrayList<>(2);
689
690 final ZoneLine zone0 = zones.get(0);
691 // initialize the standard offset, wallOffset and savings for loop
692
693 //ZoneOffset stdOffset = zone0.standardOffset;
694 ZoneOffset stdOffset = ZoneOffset.ofTotalSeconds(zone0.stdOffsetSecs);
695
696 int savings = zone0.fixedSavingsSecs;
697 ZoneOffset wallOffset = ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + savings);
698
699 // start ldt of each zone window
700 LocalDateTime zoneStart = LocalDateTime.MIN;
701
702 // first stanard offset
703 ZoneOffset firstStdOffset = stdOffset;
704 // first wall offset
705 ZoneOffset firstWallOffset = wallOffset;
706
707 for (ZoneLine zone : zones) {
708 // check if standard offset changed, update it if yes
709 ZoneOffset stdOffsetPrev = stdOffset; // for effectiveSavings check
710 if (zone.stdOffsetSecs != stdOffset.getTotalSeconds()) {
711 ZoneOffset stdOffsetNew = ZoneOffset.ofTotalSeconds(zone.stdOffsetSecs);
712 standardTransitionList.add(
713 ZoneOffsetTransition.of(
714 LocalDateTime.ofEpochSecond(zoneStart.toEpochSecond(wallOffset),
715 0,
716 stdOffset),
717 stdOffset,
718 stdOffsetNew));
719 stdOffset = stdOffsetNew;
720 }
721
722 LocalDateTime zoneEnd;
723 if (zone.year == Year.MAX_VALUE) {
724 zoneEnd = LocalDateTime.MAX;
725 } else {
726 zoneEnd = zone.toDateTime();
727 }
728 if (zoneEnd.compareTo(zoneStart) < 0) {
729 throw new IllegalStateException("Windows must be in date-time order: " +
730 zoneEnd + " < " + zoneStart);
731 }
732 // calculate effective savings at the start of the window
733 List<TransRule> trules = null;
734 List<TransRule> lastRules = null;
735
736 int effectiveSavings = zone.fixedSavingsSecs;
737 if (zone.savingsRule != null) {
738 List<RuleLine> tzdbRules = rules.get(zone.savingsRule);
739 if (tzdbRules == null) {
740 throw new IllegalArgumentException("<Rule> not found: " +
741 zone.savingsRule);
742 }
743 trules = new ArrayList<>(256);
744 lastRules = new ArrayList<>(2);
745 int lastRulesStartYear = Year.MIN_VALUE;
746
747 // merge the rules to transitions
748 for (RuleLine rule : tzdbRules) {
749 if (rule.startYear > zoneEnd.getYear()) {
750 // rules will not be used for this zone entry
751 continue;
752 }
753 rule.adjustToForwards(2004); // irrelevant, treat as leap year
754
755 int startYear = rule.startYear;
756 int endYear = rule.endYear;
757 if (zoneEnd.equals(LocalDateTime.MAX)) {
758 if (endYear == Year.MAX_VALUE) {
759 endYear = startYear;
760 lastRules.add(new TransRule(endYear, rule));
761 }
762 lastRulesStartYear = Math.max(startYear, lastRulesStartYear);
763 } else {
764 if (endYear == Year.MAX_VALUE) {
765 //endYear = zoneEnd.getYear();
766 endYear = zone.year;
767 }
768 }
769 int year = startYear;
770 while (year <= endYear) {
771 trules.add(new TransRule(year, rule));
772 year++;
773 }
774 }
775
776 // last rules, fill the gap years between different last rules
777 if (zoneEnd.equals(LocalDateTime.MAX)) {
778 lastRulesStartYear = Math.max(lastRulesStartYear, zoneStart.getYear()) + 1;
779 for (TransRule rule : lastRules) {
780 if (rule.year <= lastRulesStartYear) {
781 int year = rule.year;
782 while (year <= lastRulesStartYear) {
783 trules.add(new TransRule(year, rule.rule));
784 year++;
785 }
786 rule.year = lastRulesStartYear;
787 rule.ldt = rule.rule.toDateTime(year);
788 rule.ldtSecs = rule.ldt.toEpochSecond(ZoneOffset.UTC);
789 }
790 }
791 Collections.sort(lastRules);
792 }
793 // sort the merged rules
794 Collections.sort(trules);
795
796 effectiveSavings = 0;
797 for (TransRule rule : trules) {
798 if (rule.toEpochSecond(stdOffsetPrev, savings) >
799 zoneStart.toEpochSecond(wallOffset)) {
800 // previous savings amount found, which could be the
801 // savings amount at the instant that the window starts
802 // (hence isAfter)
803 break;
804 }
805 effectiveSavings = rule.rule.savingsAmount;
806 }
807 }
808 // check if the start of the window represents a transition
809 ZoneOffset effectiveWallOffset =
810 ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + effectiveSavings);
811
812 if (!wallOffset.equals(effectiveWallOffset)) {
813 transitionList.add(ZoneOffsetTransition.of(zoneStart,
814 wallOffset,
815 effectiveWallOffset));
816 }
817 savings = effectiveSavings;
818 // apply rules within the window
819 if (trules != null) {
820 long zoneStartEpochSecs = zoneStart.toEpochSecond(wallOffset);
821 for (TransRule trule : trules) {
822 if (trule.isTransition(savings)) {
823 long epochSecs = trule.toEpochSecond(stdOffset, savings);
824 if (epochSecs < zoneStartEpochSecs ||
825 epochSecs >= zone.toDateTimeEpochSecond(savings)) {
826 continue;
827 }
828 transitionList.add(trule.toTransition(stdOffset, savings));
829 savings = trule.rule.savingsAmount;
830 }
831 }
832 }
833 if (lastRules != null) {
834 for (TransRule trule : lastRules) {
835 lastTransitionRuleList.add(trule.rule.toTransitionRule(stdOffset, savings));
836 savings = trule.rule.savingsAmount;
837 }
838 }
839
840 // finally we can calculate the true end of the window, passing it to the next window
841 wallOffset = ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + savings);
842 zoneStart = LocalDateTime.ofEpochSecond(zone.toDateTimeEpochSecond(savings),
843 0,
844 wallOffset);
845 }
846 return new ZoneRules(firstStdOffset,
847 firstWallOffset,
848 standardTransitionList,
849 transitionList,
850 lastTransitionRuleList);
851 }
852
853 }
--- EOF ---