diff --git a/classlib/src/main/java/org/teavm/classlib/impl/tz/DateTimeZoneBuilder.java b/classlib/src/main/java/org/teavm/classlib/impl/tz/DateTimeZoneBuilder.java index 459a4edb2..773cebff6 100644 --- a/classlib/src/main/java/org/teavm/classlib/impl/tz/DateTimeZoneBuilder.java +++ b/classlib/src/main/java/org/teavm/classlib/impl/tz/DateTimeZoneBuilder.java @@ -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); - Base46.encode(sb, iDayOfWeek); - sb.append(iAdvance ? 'y' : 'n'); + if (iDayOfWeek != 0) { + Base46.encode(sb, iDayOfWeek); + } 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 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,17 +1133,38 @@ 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; } - writeTime(sb, transitions[start]); - for (int i = start + 1; i < transitions.length; ++i) { - writeTime(sb, transitions[i] - transitions[i - 1] - (365 * 3600 * 1000 / 2)); + 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)); @@ -1118,13 +1180,25 @@ 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]; - transitions[0] = readTime(flow); - for (int i = 1; i < length; ++i) { - transitions[i] = transitions[i - 1] + readTime(flow) + 365 * 3600 * 1000 / 2; + 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); @@ -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 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 filterRules(int fromYear, int toYear, List rules) { + List 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; + } + } } diff --git a/classlib/src/main/java/org/teavm/classlib/impl/tz/StorableDateTimeZone.java b/classlib/src/main/java/org/teavm/classlib/impl/tz/StorableDateTimeZone.java index 481260708..f77cf6003 100644 --- a/classlib/src/main/java/org/teavm/classlib/impl/tz/StorableDateTimeZone.java +++ b/classlib/src/main/java/org/teavm/classlib/impl/tz/StorableDateTimeZone.java @@ -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); } diff --git a/classlib/src/main/java/org/teavm/classlib/impl/tz/TimeZoneCache.java b/classlib/src/main/java/org/teavm/classlib/impl/tz/TimeZoneCache.java index aec3b3d5e..6886c3195 100644 --- a/classlib/src/main/java/org/teavm/classlib/impl/tz/TimeZoneCache.java +++ b/classlib/src/main/java/org/teavm/classlib/impl/tz/TimeZoneCache.java @@ -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; } diff --git a/classlib/src/main/java/org/teavm/classlib/impl/tz/ZoneInfoCompiler.java b/classlib/src/main/java/org/teavm/classlib/impl/tz/ZoneInfoCompiler.java index d79457c5f..da48af51e 100644 --- a/classlib/src/main/java/org/teavm/classlib/impl/tz/ZoneInfoCompiler.java +++ b/classlib/src/main/java/org/teavm/classlib/impl/tz/ZoneInfoCompiler.java @@ -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 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 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; } } }