Stdlib: save tzdb as set of rules instead of set of transitions

Generate transitions at run time
This commit is contained in:
Alexey Andreev 2021-03-16 19:53:29 +03:00
parent 71f87d79a5
commit 3cfe4644cd
4 changed files with 303 additions and 52 deletions

View File

@ -193,9 +193,10 @@ public class DateTimeZoneBuilder {
int saveMillis = 0;
int ruleSetCount = iRuleSets.size();
GregorianCalendar calendar = new GregorianCalendar(getGMT());
for (int i = 0; i < ruleSetCount; i++) {
RuleSet rs = iRuleSets.get(i);
Transition next = rs.firstTransition(millis);
Transition next = rs.firstTransition(millis, calendar);
if (next == null) {
continue;
}
@ -206,7 +207,7 @@ public class DateTimeZoneBuilder {
// Copy it since we're going to destroy it.
rs = new RuleSet(rs);
while ((next = rs.nextTransition(millis, saveMillis)) != null) {
while ((next = rs.nextTransition(millis, saveMillis, calendar)) != null) {
if (addTransition(transitions, next)) {
if (tailZone != null) {
// Got the extra transition before DSTZone.
@ -305,20 +306,52 @@ public class DateTimeZoneBuilder {
}
public void write(StringBuilder sb) {
sb.append(iMode);
int flags = 0;
if (iAdvance) {
flags |= 1;
}
if (iDayOfWeek != 0) {
flags |= 2;
}
switch (iMode) {
case 'w':
flags |= 4;
break;
case 's':
flags |= 8;
break;
}
Base46.encodeUnsigned(sb, flags);
Base46.encodeUnsigned(sb, iMonthOfYear);
Base46.encodeUnsigned(sb, iDayOfMonth);
if (iDayOfWeek != 0) {
Base46.encode(sb, iDayOfWeek);
sb.append(iAdvance ? 'y' : 'n');
}
StorableDateTimeZone.writeUnsignedTime(sb, iMillisOfDay);
}
public static OfYear read(CharFlow flow) {
char mode = flow.characters[flow.pointer++];
int flags = Base46.decodeUnsigned(flow);
boolean advance = (flags & 1) != 0;
boolean hasDayOfWeek = (flags & 2) != 0;
int modeBits = (flags >>> 2) & 3;
char mode;
switch (modeBits) {
case 1:
mode = 'w';
break;
case 2:
mode = 's';
break;
default:
mode = 'u';
break;
}
int monthOfYear = Base46.decodeUnsigned(flow);
int dayOfMonth = Base46.decodeUnsigned(flow);
int dayOfWeek = Base46.decode(flow);
boolean advance = flow.characters[flow.pointer++] == 'y';
int dayOfWeek = hasDayOfWeek ? Base46.decode(flow) : 0;
int millisOfDay = (int) StorableDateTimeZone.readUnsignedTime(flow);
return new OfYear(mode, monthOfYear, dayOfMonth, dayOfWeek, advance, millisOfDay);
}
@ -358,7 +391,7 @@ public class DateTimeZoneBuilder {
/**
* @param standardOffset standard offset just before next recurrence
*/
public long next(long instant, int standardOffset, int saveMillis) {
public long next(long instant, int standardOffset, int saveMillis, GregorianCalendar calendar) {
int offset;
if (iMode == 'w') {
offset = standardOffset + saveMillis;
@ -371,7 +404,6 @@ public class DateTimeZoneBuilder {
// Convert from UTC to local time.
instant += offset;
GregorianCalendar calendar = new GregorianCalendar(getGMT());
calendar.setTimeInMillis(instant);
calendar.set(Calendar.MONTH, iMonthOfYear - 1);
calendar.set(Calendar.DATE, 1);
@ -505,7 +537,7 @@ public class DateTimeZoneBuilder {
*/
static final class Recurrence {
final OfYear iOfYear;
final int iSaveMillis;
int iSaveMillis;
Recurrence(OfYear ofYear, int saveMillis) {
iOfYear = ofYear;
@ -519,8 +551,8 @@ public class DateTimeZoneBuilder {
/**
* @param standardOffset standard offset just before next recurrence
*/
public long next(long instant, int standardOffset, int saveMillis) {
return iOfYear.next(instant, standardOffset, saveMillis);
public long next(long instant, int standardOffset, int saveMillis, GregorianCalendar calendar) {
return iOfYear.next(instant, standardOffset, saveMillis, calendar);
}
/**
@ -578,8 +610,7 @@ public class DateTimeZoneBuilder {
return iRecurrence.getSaveMillis();
}
public long next(final long instant, int standardOffset, int saveMillis) {
Calendar calendar = Calendar.getInstance(getGMT());
public long next(final long instant, int standardOffset, int saveMillis, GregorianCalendar calendar) {
final int wallOffset = standardOffset + saveMillis;
long testInstant = instant;
@ -591,6 +622,10 @@ public class DateTimeZoneBuilder {
year = calendar.get(Calendar.YEAR);
}
if (year > iToYear) {
return instant;
}
if (year < iFromYear) {
calendar.setTimeInMillis(0);
calendar.set(Calendar.YEAR, iFromYear);
@ -601,7 +636,7 @@ public class DateTimeZoneBuilder {
testInstant -= 1;
}
long next = iRecurrence.next(testInstant, standardOffset, saveMillis);
long next = iRecurrence.next(testInstant, standardOffset, saveMillis, calendar);
if (next > instant) {
calendar.setTimeInMillis(next + wallOffset);
@ -662,7 +697,8 @@ public class DateTimeZoneBuilder {
if (other == null) {
return true;
}
return iMillis > other.iMillis && iWallOffset != other.iWallOffset;
return iMillis > other.iMillis
&& (iWallOffset != other.iWallOffset || iStandardOffset != other.iStandardOffset);
}
}
@ -737,7 +773,7 @@ public class DateTimeZoneBuilder {
*
* @param firstMillis millis of first transition
*/
public Transition firstTransition(final long firstMillis) {
public Transition firstTransition(final long firstMillis, GregorianCalendar calendar) {
if (iInitialNameKey != null) {
// Initial zone info explicitly set, so don't search the rules.
return new Transition(firstMillis, iStandardOffset + iInitialSaveMillis, iStandardOffset);
@ -755,7 +791,7 @@ public class DateTimeZoneBuilder {
Transition first = null;
Transition next;
while ((next = nextTransition(millis, saveMillis)) != null) {
while ((next = nextTransition(millis, saveMillis, calendar)) != null) {
millis = next.getMillis();
if (millis == firstMillis) {
@ -768,7 +804,7 @@ public class DateTimeZoneBuilder {
// Find first rule without savings. This way a more
// accurate nameKey is found even though no rule
// extends to the RuleSet's lower limit.
for (Rule rule : copy) {
for (Rule rule : iRules) {
if (rule.getSaveMillis() == 0) {
first = new Transition(firstMillis, rule, iStandardOffset);
break;
@ -806,15 +842,20 @@ public class DateTimeZoneBuilder {
*
* @param saveMillis savings before next transition
*/
public Transition nextTransition(final long instant, final int saveMillis) {
public Transition nextTransition(long instant, int saveMillis, GregorianCalendar calendar) {
// Find next matching rule.
Rule nextRule = null;
long nextMillis = Long.MAX_VALUE;
int nextYear = Integer.MAX_VALUE;
Iterator<Rule> it = iRules.iterator();
while (it.hasNext()) {
Rule rule = it.next();
long next = rule.next(instant, iStandardOffset, saveMillis);
if (rule.iFromYear > nextYear) {
continue;
}
long next = rule.next(instant, iStandardOffset, saveMillis, calendar);
if (next <= instant) {
it.remove();
continue;
@ -825,6 +866,7 @@ public class DateTimeZoneBuilder {
// Found a better match.
nextRule = rule;
nextMillis = next;
nextYear = calendar.get(Calendar.YEAR);
}
}
@ -833,16 +875,14 @@ public class DateTimeZoneBuilder {
}
// Stop precalculating if year reaches some arbitrary limit.
Calendar c = Calendar.getInstance(getGMT());
c.setTimeInMillis(nextMillis);
if (c.get(Calendar.YEAR) >= YEAR_LIMIT) {
calendar.setTimeInMillis(nextMillis);
if (calendar.get(Calendar.YEAR) >= YEAR_LIMIT) {
return null;
}
// Check if upper limit reached or passed.
if (iUpperYear < Integer.MAX_VALUE) {
long upperMillis =
iUpperOfYear.setInstant(iUpperYear, iStandardOffset, saveMillis);
long upperMillis = iUpperOfYear.setInstant(iUpperYear, iStandardOffset, saveMillis);
if (nextMillis >= upperMillis) {
// At or after upper limit.
return null;
@ -890,6 +930,7 @@ public class DateTimeZoneBuilder {
final int iStandardOffset;
final Recurrence iStartRecurrence;
final Recurrence iEndRecurrence;
final GregorianCalendar calendar = new GregorianCalendar(getGMT());
DSTZone(String id, int standardOffset, Recurrence startRecurrence, Recurrence endRecurrence) {
super(id);
@ -923,7 +964,7 @@ public class DateTimeZoneBuilder {
long end;
try {
start = startRecurrence.next(instant, standardOffset, endRecurrence.getSaveMillis());
start = startRecurrence.next(instant, standardOffset, endRecurrence.getSaveMillis(), calendar);
if (instant > 0 && start < 0) {
// Overflowed.
start = instant;
@ -934,7 +975,7 @@ public class DateTimeZoneBuilder {
}
try {
end = endRecurrence.next(instant, standardOffset, startRecurrence.getSaveMillis());
end = endRecurrence.next(instant, standardOffset, startRecurrence.getSaveMillis(), calendar);
if (instant > 0 && end < 0) {
// Overflowed.
end = instant;
@ -994,14 +1035,14 @@ public class DateTimeZoneBuilder {
long end;
try {
start = startRecurrence.next(instant, standardOffset, endRecurrence.getSaveMillis());
start = startRecurrence.next(instant, standardOffset, endRecurrence.getSaveMillis(), calendar);
} catch (IllegalArgumentException | ArithmeticException e) {
// Overflowed.
start = instant;
}
try {
end = endRecurrence.next(instant, standardOffset, startRecurrence.getSaveMillis());
end = endRecurrence.next(instant, standardOffset, startRecurrence.getSaveMillis(), calendar);
} catch (IllegalArgumentException | ArithmeticException e) {
// Overflowed.
end = instant;
@ -1092,18 +1133,39 @@ public class DateTimeZoneBuilder {
++start;
}
if (start > 1) {
--start;
iTransitions[start] = iTransitions[0];
iWallOffsets[start] = iWallOffsets[0];
iStandardOffsets[start] = iStandardOffsets[0];
}
Base46.encodeUnsigned(sb, PRECALCULATED);
Base46.encodeUnsigned(sb, iTransitions.length - start);
int lengthEncoded = (iTransitions.length - start) << 1;
if (iTransitions[start] == Long.MIN_VALUE) {
lengthEncoded |= 1;
}
Base46.encodeUnsigned(sb, lengthEncoded);
long[] transitions = iTransitions.clone();
for (int i = 0; i < transitions.length; ++i) {
transitions[i] = (transitions[i] / 60_000) * 60_000;
}
if (iTransitions[start] == Long.MIN_VALUE) {
if (start + 1 < transitions.length) {
writeTime(sb, transitions[start + 1]);
for (int i = start + 2; i < transitions.length; ++i) {
writeTime(sb, transitions[i] - transitions[i - 1] - (365 * 3600 * 1000 / 2));
}
}
} else {
writeTime(sb, transitions[start]);
for (int i = start + 1; i < transitions.length; ++i) {
writeTime(sb, transitions[i] - transitions[i - 1] - (365 * 3600 * 1000 / 2));
}
}
writeTimeArray(sb, Arrays.copyOfRange(iWallOffsets, start, transitions.length));
writeTimeArray(sb, Arrays.copyOfRange(iStandardOffsets, start, transitions.length));
@ -1118,14 +1180,26 @@ public class DateTimeZoneBuilder {
public static StorableDateTimeZone readZone(String id, CharFlow flow) {
int length = Base46.decodeUnsigned(flow);
boolean firstLongIsMin = (length & 1) != 0;
length >>>= 1;
long[] transitions = new long[length];
int[] wallOffsets = new int[length];
int[] standardOffsets = new int[length];
if (firstLongIsMin) {
transitions[0] = Long.MIN_VALUE;
if (transitions.length > 1) {
transitions[1] = readTime(flow);
for (int i = 2; i < length; ++i) {
transitions[i] = transitions[i - 1] + readTime(flow) + 365 * 3600 * 1000 / 2;
}
}
} else {
transitions[0] = readTime(flow);
for (int i = 1; i < length; ++i) {
transitions[i] = transitions[i - 1] + readTime(flow) + 365 * 3600 * 1000 / 2;
}
}
readTimeArray(flow, wallOffsets);
readTimeArray(flow, standardOffsets);
@ -1277,4 +1351,175 @@ public class DateTimeZoneBuilder {
return false;
}
}
static final class RuleBasedZone extends StorableDateTimeZone {
private DateTimeZoneBuilder builder;
private DateTimeZone zone;
RuleBasedZone(String id, DateTimeZoneBuilder builder) {
super(id);
this.builder = builder;
}
private void initZone() {
if (zone == null) {
zone = builder.toDateTimeZone(getID(), true);
}
}
@Override
public int getOffset(long instant) {
initZone();
return zone.getOffset(instant);
}
@Override
public int getStandardOffset(long instant) {
initZone();
return zone.getStandardOffset(instant);
}
@Override
public boolean isFixed() {
initZone();
return zone.isFixed();
}
@Override
public long nextTransition(long instant) {
initZone();
return zone.nextTransition(instant);
}
@Override
public long previousTransition(long instant) {
initZone();
return zone.previousTransition(instant);
}
@Override
public void write(StringBuilder sb) {
Base46.encodeUnsigned(sb, RULE_BASED);
Base46.encodeUnsigned(sb, builder.iRuleSets.size());
int currentYear = Integer.MIN_VALUE;
int fromYear = currentYear;
for (RuleSet rs : builder.iRuleSets) {
writeTime(sb, rs.iInitialSaveMillis);
writeTime(sb, rs.iStandardOffset);
List<Rule> rules = filterRules(fromYear, rs.iUpperYear, rs.iRules);
Base46.encodeUnsigned(sb, (rules.size() << 1) | (rs.iInitialNameKey != null ? 1 : 0));
for (Rule rule : rules) {
if (currentYear == Integer.MIN_VALUE) {
Base46.encode(sb, rule.getFromYear());
} else {
Base46.encode(sb, rule.getFromYear() - currentYear);
}
currentYear = rule.getFromYear();
if (rule.getToYear() != Integer.MAX_VALUE) {
int yearDelta = rule.getToYear() - currentYear;
if (yearDelta < 0) {
yearDelta = (-yearDelta - 1) << 1 | 1;
} else {
yearDelta = yearDelta << 1;
}
Base46.encodeUnsigned(sb, yearDelta + 1);
currentYear = rule.getToYear();
} else {
Base46.encodeUnsigned(sb, 0);
}
rule.iRecurrence.write(sb);
}
if (rs.iUpperYear == Integer.MAX_VALUE) {
Base46.encodeUnsigned(sb, 0);
} else {
int encodedYear;
if (currentYear == Integer.MIN_VALUE) {
encodedYear = rs.iUpperYear;
} else {
encodedYear = rs.iUpperYear - currentYear;
}
encodedYear = encodedYear < 0 ? ((-encodedYear - 1) << 1) | 1 : encodedYear << 1;
Base46.encodeUnsigned(sb, encodedYear + 1);
rs.iUpperOfYear.write(sb);
currentYear = rs.iUpperYear;
}
fromYear = rs.iUpperYear;
}
}
public static StorableDateTimeZone readZone(String id, CharFlow flow) {
DateTimeZoneBuilder builder = new DateTimeZoneBuilder();
int ruleSetCount = Base46.decodeUnsigned(flow);
int currentYear = Integer.MIN_VALUE;
for (int i = 0; i < ruleSetCount; ++i) {
RuleSet rs = new RuleSet();
rs.iInitialSaveMillis = (int) readTime(flow);
rs.iStandardOffset = (int) readTime(flow);
int ruleCount = Base46.decodeUnsigned(flow);
if ((ruleCount & 1) != 0) {
rs.iInitialNameKey = "*";
}
ruleCount >>>= 1;
for (int j = 0; j < ruleCount; ++j) {
int fromYear = Base46.decode(flow);
if (currentYear != Integer.MIN_VALUE) {
fromYear += currentYear;
}
currentYear = fromYear;
int toYear = Base46.decodeUnsigned(flow);
if (toYear == 0) {
toYear = Integer.MAX_VALUE;
} else {
toYear--;
toYear = (toYear & 1) == 0 ? toYear >>> 1 : -(toYear >>> 1) - 1;
if (currentYear != Integer.MIN_VALUE) {
toYear += currentYear;
}
currentYear = toYear;
}
Recurrence recurrence = Recurrence.read(flow);
rs.iRules.add(new Rule(recurrence, fromYear, toYear));
}
int year = Base46.decodeUnsigned(flow);
if (year == 0) {
rs.iUpperYear = Integer.MAX_VALUE;
} else {
year--;
if ((year & 1) == 0) {
year >>>= 1;
} else {
year = -(year >>> 1) - 1;
}
if (currentYear != Integer.MIN_VALUE) {
year += currentYear;
}
rs.iUpperYear = year;
rs.iUpperOfYear = OfYear.read(flow);
currentYear = year;
}
builder.iRuleSets.add(rs);
}
return new RuleBasedZone(id, builder);
}
private List<Rule> filterRules(int fromYear, int toYear, List<Rule> rules) {
List<Rule> filtered = new ArrayList<>();
if (fromYear != Integer.MIN_VALUE) {
int bestYear = Integer.MIN_VALUE;
for (Rule rule : rules) {
if (rule.getToYear() < fromYear && rule.getToYear() > bestYear) {
bestYear = rule.getToYear();
}
}
fromYear = bestYear;
}
for (Rule rule : rules) {
if (rule.getToYear() >= fromYear && rule.getFromYear() <= toYear) {
filtered.add(rule);
}
}
return filtered;
}
}
}

View File

@ -19,12 +19,14 @@ import org.teavm.classlib.impl.Base46;
import org.teavm.classlib.impl.CharFlow;
import org.teavm.classlib.impl.tz.DateTimeZoneBuilder.DSTZone;
import org.teavm.classlib.impl.tz.DateTimeZoneBuilder.PrecalculatedZone;
import org.teavm.classlib.impl.tz.DateTimeZoneBuilder.RuleBasedZone;
public abstract class StorableDateTimeZone extends DateTimeZone {
public static final int PRECALCULATED = 0;
public static final int FIXED = 1;
public static final int DST = 3;
public static final int ALIAS = 4;
public static final int RULE_BASED = 5;
public StorableDateTimeZone(String id) {
super(id);
@ -34,9 +36,9 @@ public abstract class StorableDateTimeZone extends DateTimeZone {
public static void writeTime(StringBuilder sb, long time) {
if (time % 1800_000 == 0) {
Base46.encode(sb, (int) ((time / 1800_000) << 1));
Base46.encode(sb, (time / 1800_000) << 1);
} else {
Base46.encode(sb, (int) (((time / 60_000) << 1) | 1));
Base46.encode(sb, (time << 1) | 1);
}
}
@ -45,7 +47,7 @@ public abstract class StorableDateTimeZone extends DateTimeZone {
if ((value & 1) == 0) {
return (value >> 1) * 1800_000;
} else {
return (value >> 1) * 60_000;
return value >> 1;
}
}
@ -53,7 +55,7 @@ public abstract class StorableDateTimeZone extends DateTimeZone {
if (time % 1800_000 == 0) {
Base46.encodeUnsigned(sb, (int) ((time / 1800_000) << 1));
} else {
Base46.encodeUnsigned(sb, (int) (((time / 60_000) << 1) | 1));
Base46.encodeUnsigned(sb, (int) ((time << 1) | 1));
}
}
@ -62,7 +64,7 @@ public abstract class StorableDateTimeZone extends DateTimeZone {
if ((value & 1) == 0) {
return (value >>> 1) * 1800_000;
} else {
return (value >>> 1) * 60_000;
return value >>> 1;
}
}
@ -124,6 +126,8 @@ public abstract class StorableDateTimeZone extends DateTimeZone {
return DSTZone.readZone(id, flow);
case FIXED:
return FixedDateTimeZone.readZone(id, flow);
case RULE_BASED:
return RuleBasedZone.readZone(id, flow);
default:
throw new IllegalArgumentException("Unknown zone type: " + type);
}

View File

@ -60,7 +60,7 @@ public class TimeZoneCache {
String data = aliasLine.substring(index + 1);
CharFlow flow = new CharFlow(data.toCharArray());
Base46.decode(flow);
result.put(id, result.get(data.substring(flow.pointer)));
result.put(id, new AliasDateTimeZone(id, result.get(data.substring(flow.pointer))));
}
return result;
}

View File

@ -128,6 +128,7 @@ public class ZoneInfoCompiler {
long end = ISOChronology.getInstanceUTC().year().set(0, 2050);
int offset = tz.getOffset(millis);
int stdOffset = tz.getStandardOffset(millis);
List<Long> transitions = new ArrayList<>();
@ -140,16 +141,18 @@ public class ZoneInfoCompiler {
millis = next;
int nextOffset = tz.getOffset(millis);
int nextStdOffset = tz.getStandardOffset(millis);
if (offset == nextOffset) {
if (offset == nextOffset && stdOffset == nextStdOffset) {
System.out.println("*d* Error in " + tz.getID() + " "
+ new DateTime(millis, ISOChronology.getInstanceUTC()));
return false;
}
transitions.add(Long.valueOf(millis));
transitions.add(millis);
offset = nextOffset;
stdOffset = nextStdOffset;
}
// Now verify that reverse transitions match up.
@ -165,7 +168,7 @@ public class ZoneInfoCompiler {
millis = prev;
long trans = transitions.get(i).longValue();
long trans = transitions.get(i);
if (trans - 1 != millis) {
System.out.println("*r* Error in " + tz.getID() + " "
@ -203,11 +206,10 @@ public class ZoneInfoCompiler {
Map<String, Zone> sourceMap = new TreeMap<>();
// write out the standard entries
for (int i = 0; i < iZones.size(); i++) {
Zone zone = iZones.get(i);
for (Zone zone : iZones) {
DateTimeZoneBuilder builder = new DateTimeZoneBuilder();
zone.addToBuilder(builder, iRuleSets);
StorableDateTimeZone tz = builder.toDateTimeZone(zone.iName, true);
StorableDateTimeZone tz = new DateTimeZoneBuilder.RuleBasedZone(zone.iName, builder);
if (test(tz.getID(), tz)) {
map.put(tz.getID(), tz);
sourceMap.put(tz.getID(), zone);
@ -225,7 +227,7 @@ public class ZoneInfoCompiler {
} else {
DateTimeZoneBuilder builder = new DateTimeZoneBuilder();
sourceZone.addToBuilder(builder, iRuleSets);
StorableDateTimeZone revived = builder.toDateTimeZone(alias, true);
StorableDateTimeZone revived = new DateTimeZoneBuilder.RuleBasedZone(alias, builder);
if (test(revived.getID(), revived)) {
map.put(revived.getID(), revived);
}
@ -604,7 +606,7 @@ public class ZoneInfoCompiler {
return str;
}
return str + "...\n" + iNext.toString();
return str + "...\n" + iNext;
}
}
}