diff --git a/src/main/java/com/ethlo/time/DateTime.java b/src/main/java/com/ethlo/time/DateTime.java index c62c03c..c28431f 100644 --- a/src/main/java/com/ethlo/time/DateTime.java +++ b/src/main/java/com/ethlo/time/DateTime.java @@ -20,11 +20,11 @@ * #L% */ -import static com.ethlo.time.internal.ITUParser.DATE_SEPARATOR; -import static com.ethlo.time.internal.ITUParser.SEPARATOR_UPPER; -import static com.ethlo.time.internal.ITUParser.TIME_SEPARATOR; -import static com.ethlo.time.internal.ITUFormatter.finish; -import static com.ethlo.time.internal.LeapSecondHandler.LEAP_SECOND_SECONDS; +import static com.ethlo.time.internal.fixed.ITUFormatter.finish; +import static com.ethlo.time.internal.fixed.ITUParser.DATE_SEPARATOR; +import static com.ethlo.time.internal.fixed.ITUParser.SEPARATOR_UPPER; +import static com.ethlo.time.internal.fixed.ITUParser.TIME_SEPARATOR; +import static com.ethlo.time.internal.util.LeapSecondHandler.LEAP_SECOND_SECONDS; import java.time.DateTimeException; import java.time.Instant; @@ -43,10 +43,10 @@ import java.util.Optional; import com.ethlo.time.internal.DateTimeFormatException; -import com.ethlo.time.internal.DateTimeMath; -import com.ethlo.time.internal.DefaultLeapSecondHandler; -import com.ethlo.time.internal.LeapSecondHandler; -import com.ethlo.time.internal.LimitedCharArrayIntegerUtil; +import com.ethlo.time.internal.util.DateTimeMath; +import com.ethlo.time.internal.util.DefaultLeapSecondHandler; +import com.ethlo.time.internal.util.LeapSecondHandler; +import com.ethlo.time.internal.util.LimitedCharArrayIntegerUtil; /** * Container class for parsed date/date-time data. The {@link #getMostGranularField()} contains the highest granularity field found, like MONTH, MINUTE, SECOND. diff --git a/src/main/java/com/ethlo/time/Field.java b/src/main/java/com/ethlo/time/Field.java index d131c5b..3ef84c1 100644 --- a/src/main/java/com/ethlo/time/Field.java +++ b/src/main/java/com/ethlo/time/Field.java @@ -40,7 +40,8 @@ public enum Field HOUR(13), MINUTE(16), SECOND(19), - NANO(20); + NANO(20), + ZONE_OFFSET(17); private final int requiredLength; diff --git a/src/main/java/com/ethlo/time/ITU.java b/src/main/java/com/ethlo/time/ITU.java index 633e8c0..4d51227 100644 --- a/src/main/java/com/ethlo/time/ITU.java +++ b/src/main/java/com/ethlo/time/ITU.java @@ -29,8 +29,8 @@ import java.time.YearMonth; import java.time.format.DateTimeParseException; -import com.ethlo.time.internal.ITUFormatter; -import com.ethlo.time.internal.ITUParser; +import com.ethlo.time.internal.fixed.ITUFormatter; +import com.ethlo.time.internal.fixed.ITUParser; /** * The main access to the parse and formatting functions in this library. @@ -71,7 +71,8 @@ public static DateTime parseLenient(String text) /** * Allows parsing leniently with {@link ParseConfig to control some aspects of the parsing} - * @param text The text to parse + * + * @param text The text to parse * @param parseConfig The configuration to use for parsing * @return The date-time parsed */ @@ -81,10 +82,9 @@ public static DateTime parseLenient(String text, ParseConfig parseConfig) } /** - * - * @param text The text to parse + * @param text The text to parse * @param parseConfig The configuration to use for parsing - * @param position The position to start parsing from. The index (and the errorIndex, if an error occurs) is updated after the parsing process has completed + * @param position The position to start parsing from. The index (and the errorIndex, if an error occurs) is updated after the parsing process has completed * @return The date-time parsed */ public static DateTime parseLenient(String text, ParseConfig parseConfig, ParsePosition position) diff --git a/src/main/java/com/ethlo/time/ParseConfig.java b/src/main/java/com/ethlo/time/ParseConfig.java index ae61a22..a038ee2 100644 --- a/src/main/java/com/ethlo/time/ParseConfig.java +++ b/src/main/java/com/ethlo/time/ParseConfig.java @@ -20,13 +20,11 @@ * #L% */ -import static com.ethlo.time.internal.ITUParser.SEPARATOR_LOWER; -import static com.ethlo.time.internal.ITUParser.SEPARATOR_SPACE; -import static com.ethlo.time.internal.ITUParser.SEPARATOR_UPPER; - import java.util.Arrays; import java.util.Optional; +import static com.ethlo.time.internal.fixed.ITUParser.*; + public class ParseConfig { private static final char[] DEFAULT_DATE_TIME_SEPARATORS = new char[]{SEPARATOR_UPPER, SEPARATOR_LOWER, SEPARATOR_SPACE}; diff --git a/src/main/java/com/ethlo/time/TimezoneOffset.java b/src/main/java/com/ethlo/time/TimezoneOffset.java index cdbe781..3a2a5c0 100644 --- a/src/main/java/com/ethlo/time/TimezoneOffset.java +++ b/src/main/java/com/ethlo/time/TimezoneOffset.java @@ -29,6 +29,9 @@ public class TimezoneOffset { public static final TimezoneOffset UTC = new TimezoneOffset(0, 0); + private static final int SECONDS_PER_HOUR = 3600; + private static final int SECONDS_PER_MINUTE = 60; + private static final int MINUTES_PER_HOUR = 60; private final int hours; private final int minutes; @@ -43,6 +46,13 @@ public static TimezoneOffset ofHoursMinutes(int hours, int minutes) return new TimezoneOffset(hours, minutes); } + public static TimezoneOffset ofTotalSeconds(int seconds) + { + final int absHours = seconds / SECONDS_PER_HOUR; + int absMinutes = (seconds / SECONDS_PER_MINUTE) % MINUTES_PER_HOUR; + return ofHoursMinutes(absHours, absMinutes); + } + public static TimezoneOffset of(ZoneOffset offset) { final int seconds = offset.getTotalSeconds(); diff --git a/src/main/java/com/ethlo/time/internal/ITUFormatter.java b/src/main/java/com/ethlo/time/internal/fixed/ITUFormatter.java similarity index 87% rename from src/main/java/com/ethlo/time/internal/ITUFormatter.java rename to src/main/java/com/ethlo/time/internal/fixed/ITUFormatter.java index 56c7ad2..1efe741 100644 --- a/src/main/java/com/ethlo/time/internal/ITUFormatter.java +++ b/src/main/java/com/ethlo/time/internal/fixed/ITUFormatter.java @@ -1,4 +1,4 @@ -package com.ethlo.time.internal; +package com.ethlo.time.internal.fixed; /*- * #%L @@ -9,9 +9,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,20 +20,22 @@ * #L% */ -import static com.ethlo.time.internal.ITUParser.DATE_SEPARATOR; -import static com.ethlo.time.internal.ITUParser.FRACTION_SEPARATOR; -import static com.ethlo.time.internal.ITUParser.MAX_FRACTION_DIGITS; -import static com.ethlo.time.internal.ITUParser.MINUS; -import static com.ethlo.time.internal.ITUParser.PLUS; -import static com.ethlo.time.internal.ITUParser.SEPARATOR_UPPER; -import static com.ethlo.time.internal.ITUParser.TIME_SEPARATOR; -import static com.ethlo.time.internal.ITUParser.ZULU_UPPER; +import static com.ethlo.time.internal.fixed.ITUParser.DATE_SEPARATOR; +import static com.ethlo.time.internal.fixed.ITUParser.FRACTION_SEPARATOR; +import static com.ethlo.time.internal.fixed.ITUParser.MAX_FRACTION_DIGITS; +import static com.ethlo.time.internal.fixed.ITUParser.MINUS; +import static com.ethlo.time.internal.fixed.ITUParser.PLUS; +import static com.ethlo.time.internal.fixed.ITUParser.SEPARATOR_UPPER; +import static com.ethlo.time.internal.fixed.ITUParser.TIME_SEPARATOR; +import static com.ethlo.time.internal.fixed.ITUParser.ZULU_UPPER; import java.time.OffsetDateTime; import java.time.ZoneOffset; import com.ethlo.time.Field; import com.ethlo.time.TimezoneOffset; +import com.ethlo.time.internal.DateTimeFormatException; +import com.ethlo.time.internal.util.LimitedCharArrayIntegerUtil; public class ITUFormatter { diff --git a/src/main/java/com/ethlo/time/internal/ITUParser.java b/src/main/java/com/ethlo/time/internal/fixed/ITUParser.java similarity index 91% rename from src/main/java/com/ethlo/time/internal/ITUParser.java rename to src/main/java/com/ethlo/time/internal/fixed/ITUParser.java index e32b3b8..e608441 100644 --- a/src/main/java/com/ethlo/time/internal/ITUParser.java +++ b/src/main/java/com/ethlo/time/internal/fixed/ITUParser.java @@ -1,4 +1,4 @@ -package com.ethlo.time.internal; +package com.ethlo.time.internal.fixed; /*- * #%L @@ -20,13 +20,13 @@ * #L% */ -import static com.ethlo.time.internal.ErrorUtil.assertFractionDigits; -import static com.ethlo.time.internal.ErrorUtil.assertPositionContains; -import static com.ethlo.time.internal.ErrorUtil.raiseUnexpectedCharacter; -import static com.ethlo.time.internal.ErrorUtil.raiseUnexpectedEndOfText; -import static com.ethlo.time.internal.LimitedCharArrayIntegerUtil.DIGIT_9; -import static com.ethlo.time.internal.LimitedCharArrayIntegerUtil.ZERO; -import static com.ethlo.time.internal.LimitedCharArrayIntegerUtil.parsePositiveInt; +import static com.ethlo.time.internal.util.ErrorUtil.assertFractionDigits; +import static com.ethlo.time.internal.util.ErrorUtil.assertPositionContains; +import static com.ethlo.time.internal.util.ErrorUtil.raiseUnexpectedCharacter; +import static com.ethlo.time.internal.util.ErrorUtil.raiseUnexpectedEndOfText; +import static com.ethlo.time.internal.util.LimitedCharArrayIntegerUtil.DIGIT_9; +import static com.ethlo.time.internal.util.LimitedCharArrayIntegerUtil.ZERO; +import static com.ethlo.time.internal.util.LimitedCharArrayIntegerUtil.parsePositiveInt; import java.time.OffsetDateTime; import java.time.format.DateTimeParseException; @@ -36,6 +36,7 @@ import com.ethlo.time.Field; import com.ethlo.time.ParseConfig; import com.ethlo.time.TimezoneOffset; +import com.ethlo.time.internal.util.ArrayUtils; public class ITUParser { @@ -44,11 +45,11 @@ public class ITUParser public static final char SEPARATOR_UPPER = 'T'; public static final char SEPARATOR_LOWER = 't'; public static final char SEPARATOR_SPACE = ' '; - static final char PLUS = '+'; - static final char MINUS = '-'; + public static final char PLUS = '+'; + public static final char MINUS = '-'; public static final char FRACTION_SEPARATOR = '.'; - static final char ZULU_UPPER = 'Z'; - private static final char ZULU_LOWER = 'z'; + public static final char ZULU_UPPER = 'Z'; + public static final char ZULU_LOWER = 'z'; public static final int MAX_FRACTION_DIGITS = 9; public static final int RADIX = 10; public static final int DIGITS_IN_NANO = 9; @@ -76,7 +77,7 @@ private static DateTime handleTime(final int offset, final ParseConfig parseConf return new DateTime(Field.MINUTE, year, month, day, hour, minute, 0, 0, zoneOffset, 0, charLength); default: - throw raiseUnexpectedCharacter(chars, offset + 16); + throw raiseUnexpectedCharacter(chars, offset + 16, TIME_SEPARATOR, ZULU_UPPER, ZULU_LOWER, PLUS, MINUS); } } @@ -109,7 +110,7 @@ private static TimezoneOffset parseTimezone(int offset, final ParseConfig parseC final char sign = chars.charAt(idx); if (sign != PLUS && sign != MINUS) { - raiseUnexpectedCharacter(chars, idx); + raiseUnexpectedCharacter(chars, idx, ZULU_UPPER, ZULU_LOWER, PLUS, MINUS); } if (left < 6) @@ -258,7 +259,7 @@ else if (c == PLUS || c == MINUS) } else { - throw raiseUnexpectedCharacter(chars, offset + 19); + throw raiseUnexpectedCharacter(chars, offset + 19, ArrayUtils.merge(parseConfig.getFractionSeparators(), new char[]{ZULU_UPPER, ZULU_LOWER, PLUS, MINUS})); } } else if (length == 19) diff --git a/src/main/java/com/ethlo/time/internal/token/DigitsToken.java b/src/main/java/com/ethlo/time/internal/token/DigitsToken.java new file mode 100644 index 0000000..1674c7f --- /dev/null +++ b/src/main/java/com/ethlo/time/internal/token/DigitsToken.java @@ -0,0 +1,60 @@ +package com.ethlo.time.internal.token; + +/*- + * #%L + * Internet Time Utility + * %% + * Copyright (C) 2017 - 2024 Morten Haraldsen (ethlo) + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import java.text.ParsePosition; + +import com.ethlo.time.Field; +import com.ethlo.time.internal.util.LimitedCharArrayIntegerUtil; +import com.ethlo.time.token.DateTimeToken; + +public class DigitsToken implements DateTimeToken +{ + private final Field field; + private final int length; + + public DigitsToken(Field field, int length) + { + this.field = field; + this.length = length; + } + + @Override + public int read(String text, ParsePosition parsePosition) + { + final int offset = parsePosition.getIndex(); + final int end = offset + length; + final int value = LimitedCharArrayIntegerUtil.parsePositiveInt(text, offset, end); + parsePosition.setIndex(end); + return value; + } + + public Field getField() + { + return field; + } + + @Override + public String toString() + { + return "digits: " + field + "(" + length + ")"; + } +} diff --git a/src/main/java/com/ethlo/time/internal/token/FractionsToken.java b/src/main/java/com/ethlo/time/internal/token/FractionsToken.java new file mode 100644 index 0000000..9825403 --- /dev/null +++ b/src/main/java/com/ethlo/time/internal/token/FractionsToken.java @@ -0,0 +1,61 @@ +package com.ethlo.time.internal.token; + +/*- + * #%L + * Internet Time Utility + * %% + * Copyright (C) 2017 - 2024 Morten Haraldsen (ethlo) + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import static com.ethlo.time.internal.util.LimitedCharArrayIntegerUtil.DIGIT_9; +import static com.ethlo.time.internal.util.LimitedCharArrayIntegerUtil.ZERO; + +import java.text.ParsePosition; + +import com.ethlo.time.Field; +import com.ethlo.time.token.DateTimeToken; + +public class FractionsToken implements DateTimeToken +{ + @Override + public int read(final String text, final ParsePosition parsePosition) + { + int idx = parsePosition.getIndex(); + final int length = text.length(); + int value = 0; + while (idx < length) + { + final char c = text.charAt(idx); + if (c < ZERO || c > DIGIT_9) + { + break; + } + else + { + value = value * 10 + (c - ZERO); + idx++; + } + } + parsePosition.setIndex(idx); + return value; + } + + @Override + public Field getField() + { + return Field.NANO; + } +} diff --git a/src/main/java/com/ethlo/time/internal/token/SeparatorToken.java b/src/main/java/com/ethlo/time/internal/token/SeparatorToken.java new file mode 100644 index 0000000..f12ce1e --- /dev/null +++ b/src/main/java/com/ethlo/time/internal/token/SeparatorToken.java @@ -0,0 +1,62 @@ +package com.ethlo.time.internal.token; + +/*- + * #%L + * Internet Time Utility + * %% + * Copyright (C) 2017 - 2024 Morten Haraldsen (ethlo) + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import java.text.ParsePosition; + +import com.ethlo.time.internal.util.ErrorUtil; +import com.ethlo.time.token.DateTimeToken; + +public class SeparatorToken implements DateTimeToken +{ + private final char separator; + + public SeparatorToken(char separator) + { + this.separator = separator; + } + + @Override + public int read(final String text, final ParsePosition parsePosition) + { + final int index = parsePosition.getIndex(); + if (text.length() > index && text.charAt(index) == separator) + { + parsePosition.setIndex(index + 1); + } + else if (text.length() <= index) + { + ErrorUtil.raiseUnexpectedEndOfText(text, text.length()); + } + else if (text.charAt(index) != separator) + { + ErrorUtil.raiseUnexpectedCharacter(text, index, separator); + } + parsePosition.setIndex(index + 1); + return 1; + } + + @Override + public String toString() + { + return "separator: " + separator; + } +} diff --git a/src/main/java/com/ethlo/time/internal/token/SeparatorsToken.java b/src/main/java/com/ethlo/time/internal/token/SeparatorsToken.java new file mode 100644 index 0000000..1e6f656 --- /dev/null +++ b/src/main/java/com/ethlo/time/internal/token/SeparatorsToken.java @@ -0,0 +1,65 @@ +package com.ethlo.time.internal.token; + +/*- + * #%L + * Internet Time Utility + * %% + * Copyright (C) 2017 - 2024 Morten Haraldsen (ethlo) + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import java.text.ParsePosition; +import java.time.format.DateTimeParseException; +import java.util.Arrays; + +import com.ethlo.time.internal.util.ErrorUtil; +import com.ethlo.time.token.DateTimeToken; + +public class SeparatorsToken implements DateTimeToken +{ + private final char[] separators; + + public SeparatorsToken(char... separators) + { + this.separators = separators; + } + + @Override + public int read(final String text, final ParsePosition parsePosition) + { + final int index = parsePosition.getIndex(); + if (text.length() <= index) + { + ErrorUtil.raiseUnexpectedEndOfText(text, text.length()); + } + + final char c = text.charAt(index); + for (char sep : separators) + { + if (c == sep) + { + parsePosition.setIndex(index + 1); + return 1; + } + } + throw new DateTimeParseException(String.format("Expected character %s at position %d, found %s: %s", Arrays.toString(separators), index + 1, text.charAt(index), text), text, index); + } + + @Override + public String toString() + { + return "separators: " + Arrays.toString(separators); + } +} diff --git a/src/main/java/com/ethlo/time/internal/token/ZoneOffsetToken.java b/src/main/java/com/ethlo/time/internal/token/ZoneOffsetToken.java new file mode 100644 index 0000000..a253872 --- /dev/null +++ b/src/main/java/com/ethlo/time/internal/token/ZoneOffsetToken.java @@ -0,0 +1,90 @@ +package com.ethlo.time.internal.token; + +/*- + * #%L + * Internet Time Utility + * %% + * Copyright (C) 2017 - 2024 Morten Haraldsen (ethlo) + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import static com.ethlo.time.internal.fixed.ITUParser.MINUS; +import static com.ethlo.time.internal.fixed.ITUParser.PLUS; +import static com.ethlo.time.internal.fixed.ITUParser.ZULU_LOWER; +import static com.ethlo.time.internal.fixed.ITUParser.ZULU_UPPER; +import static com.ethlo.time.internal.util.ErrorUtil.raiseUnexpectedCharacter; +import static com.ethlo.time.internal.util.LimitedCharArrayIntegerUtil.parsePositiveInt; + +import java.text.ParsePosition; +import java.time.format.DateTimeParseException; + +import com.ethlo.time.Field; +import com.ethlo.time.token.DateTimeToken; + +public class ZoneOffsetToken implements DateTimeToken +{ + @Override + public int read(final String text, final ParsePosition parsePosition) + { + final int idx = parsePosition.getIndex(); + final int len = text.length(); + final int left = len - idx; + + if (left < 1) + { + return -1; + } + + final char c = text.charAt(idx); + if (c == 'Z' || c == 'z') + { + parsePosition.setIndex(idx + 1); + return 0; + } + + final char sign = text.charAt(idx); + if (sign != '+' && sign != '-') + { + raiseUnexpectedCharacter(text, idx, ZULU_UPPER, ZULU_LOWER, PLUS, MINUS); + } + + if (left < 6) + { + throw new DateTimeParseException(String.format("Invalid timezone offset: %s", text), text, idx); + } + + int hours = parsePositiveInt(text, idx + 1, idx + 3); + int minutes = parsePositiveInt(text, idx + 4, idx + 4 + 2); + if (sign == '-') + { + hours = -hours; + minutes = -minutes; + + if (hours == 0 && minutes == 0) + { + throw new DateTimeParseException("Unknown 'Local Offset Convention' date-time not allowed", text, idx); + } + } + + parsePosition.setIndex(idx + 6); + return hours * 3600 + minutes * 60; + } + + @Override + public Field getField() + { + return Field.ZONE_OFFSET; + } +} diff --git a/src/main/java/com/ethlo/time/internal/util/ArrayUtils.java b/src/main/java/com/ethlo/time/internal/util/ArrayUtils.java new file mode 100644 index 0000000..e2ae22d --- /dev/null +++ b/src/main/java/com/ethlo/time/internal/util/ArrayUtils.java @@ -0,0 +1,32 @@ +package com.ethlo.time.internal.util; + +/*- + * #%L + * Internet Time Utility + * %% + * Copyright (C) 2017 - 2024 Morten Haraldsen (ethlo) + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +public class ArrayUtils +{ + public static char[] merge(char[] a, char[] b) + { + final char[] result = new char[a.length + b.length]; + System.arraycopy(a, 0, result, 0, a.length); + System.arraycopy(b, 0, result, a.length, b.length); + return result; + } +} diff --git a/src/main/java/com/ethlo/time/internal/DateTimeMath.java b/src/main/java/com/ethlo/time/internal/util/DateTimeMath.java similarity index 98% rename from src/main/java/com/ethlo/time/internal/DateTimeMath.java rename to src/main/java/com/ethlo/time/internal/util/DateTimeMath.java index 619e45f..af43cc2 100644 --- a/src/main/java/com/ethlo/time/internal/DateTimeMath.java +++ b/src/main/java/com/ethlo/time/internal/util/DateTimeMath.java @@ -1,4 +1,4 @@ -package com.ethlo.time.internal; +package com.ethlo.time.internal.util; /*- * #%L diff --git a/src/main/java/com/ethlo/time/internal/DefaultLeapSecondHandler.java b/src/main/java/com/ethlo/time/internal/util/DefaultLeapSecondHandler.java similarity index 98% rename from src/main/java/com/ethlo/time/internal/DefaultLeapSecondHandler.java rename to src/main/java/com/ethlo/time/internal/util/DefaultLeapSecondHandler.java index 44cdbe5..746bd32 100644 --- a/src/main/java/com/ethlo/time/internal/DefaultLeapSecondHandler.java +++ b/src/main/java/com/ethlo/time/internal/util/DefaultLeapSecondHandler.java @@ -1,4 +1,4 @@ -package com.ethlo.time.internal; +package com.ethlo.time.internal.util; /*- * #%L diff --git a/src/main/java/com/ethlo/time/internal/ErrorUtil.java b/src/main/java/com/ethlo/time/internal/util/ErrorUtil.java similarity index 86% rename from src/main/java/com/ethlo/time/internal/ErrorUtil.java rename to src/main/java/com/ethlo/time/internal/util/ErrorUtil.java index 036138b..57e4272 100644 --- a/src/main/java/com/ethlo/time/internal/ErrorUtil.java +++ b/src/main/java/com/ethlo/time/internal/util/ErrorUtil.java @@ -1,4 +1,4 @@ -package com.ethlo.time.internal; +package com.ethlo.time.internal.util; /*- * #%L @@ -20,9 +20,10 @@ * #L% */ -import static com.ethlo.time.internal.ITUParser.MAX_FRACTION_DIGITS; +import static com.ethlo.time.internal.fixed.ITUParser.MAX_FRACTION_DIGITS; import java.time.format.DateTimeParseException; +import java.util.Arrays; import com.ethlo.time.Field; @@ -32,9 +33,9 @@ private ErrorUtil() { } - public static DateTimeParseException raiseUnexpectedCharacter(String chars, int index) + public static DateTimeParseException raiseUnexpectedCharacter(String chars, int index, char... expected) { - throw new DateTimeParseException(String.format("Unexpected character %s at position %d: %s", chars.charAt(index), index + 1, chars), chars, index); + throw new DateTimeParseException(String.format("Expected character %s at position %d, found %s: %s", Arrays.toString(expected), index + 1, chars.charAt(index), chars), chars, index); } public static DateTimeParseException raiseUnexpectedEndOfText(final String chars, final int offset) diff --git a/src/main/java/com/ethlo/time/internal/LeapSecondHandler.java b/src/main/java/com/ethlo/time/internal/util/LeapSecondHandler.java similarity index 95% rename from src/main/java/com/ethlo/time/internal/LeapSecondHandler.java rename to src/main/java/com/ethlo/time/internal/util/LeapSecondHandler.java index 168e7af..922d380 100644 --- a/src/main/java/com/ethlo/time/internal/LeapSecondHandler.java +++ b/src/main/java/com/ethlo/time/internal/util/LeapSecondHandler.java @@ -1,4 +1,4 @@ -package com.ethlo.time.internal; +package com.ethlo.time.internal.util; /*- * #%L diff --git a/src/main/java/com/ethlo/time/internal/LimitedCharArrayIntegerUtil.java b/src/main/java/com/ethlo/time/internal/util/LimitedCharArrayIntegerUtil.java similarity index 96% rename from src/main/java/com/ethlo/time/internal/LimitedCharArrayIntegerUtil.java rename to src/main/java/com/ethlo/time/internal/util/LimitedCharArrayIntegerUtil.java index 18cd9d2..88960a3 100644 --- a/src/main/java/com/ethlo/time/internal/LimitedCharArrayIntegerUtil.java +++ b/src/main/java/com/ethlo/time/internal/util/LimitedCharArrayIntegerUtil.java @@ -1,4 +1,4 @@ -package com.ethlo.time.internal; +package com.ethlo.time.internal.util; /*- * #%L @@ -52,15 +52,14 @@ public static int parsePositiveInt(final String strNum, int startInclusive, int int result = 0; try { - for (int i = startInclusive; i < endExclusive; i++) { final char c = strNum.charAt(i); if (c < ZERO || c > DIGIT_9) { - ErrorUtil.raiseUnexpectedCharacter(strNum, i); + ErrorUtil.raiseUnexpectedCharacter(strNum, i, '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'); } - result = result * 10 + (c - ZERO); + result = (result * 10) + (c - ZERO); } } catch (StringIndexOutOfBoundsException exc) diff --git a/src/main/java/com/ethlo/time/token/ConfigurableDateTimeParser.java b/src/main/java/com/ethlo/time/token/ConfigurableDateTimeParser.java new file mode 100644 index 0000000..58246d9 --- /dev/null +++ b/src/main/java/com/ethlo/time/token/ConfigurableDateTimeParser.java @@ -0,0 +1,122 @@ +package com.ethlo.time.token; + +/*- + * #%L + * Internet Time Utility + * %% + * Copyright (C) 2017 - 2024 Morten Haraldsen (ethlo) + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import static com.ethlo.time.Field.NANO; +import static com.ethlo.time.Field.YEAR; + +import java.text.ParsePosition; +import java.time.format.DateTimeParseException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import com.ethlo.time.DateTime; +import com.ethlo.time.Field; +import com.ethlo.time.TimezoneOffset; +import com.ethlo.time.internal.token.FractionsToken; + +public class ConfigurableDateTimeParser implements DateTimeParser +{ + private final DateTimeToken[] tokens; + + private ConfigurableDateTimeParser(DateTimeToken... tokens) + { + final Set fieldsSeen = new HashSet<>(); + Arrays.asList(tokens).forEach(t -> + { + if (t.getField() != null && !fieldsSeen.add(t.getField())) + { + throw new IllegalArgumentException("Duplicate field " + t.getField() + " in list of tokens: " + Arrays.toString(tokens)); + } + }); + this.tokens = tokens; + } + + public static DateTimeParser of(DateTimeToken... tokens) + { + return new ConfigurableDateTimeParser(tokens); + } + + @Override + public DateTime parse(String text, ParsePosition parsePosition) + { + try + { + return doParse(text, parsePosition); + } + catch (DateTimeParseException exc) + { + parsePosition.setIndex(exc.getErrorIndex()); + parsePosition.setErrorIndex(exc.getErrorIndex()); + throw exc; + } + } + + private DateTime doParse(String text, ParsePosition parsePosition) + { + int fractionsLength = 0; + int highestOrdinal = YEAR.ordinal(); + final int[] values = new int[]{0, 1, 1, 0, 0, 0, 0, -1}; + + for (DateTimeToken token : tokens) + { + final int index = parsePosition.getIndex(); + final int value = token.read(text, parsePosition); + final Field field = token.getField(); + if (field != null) + { + final int ordinal = field.ordinal(); + values[ordinal] = value; + highestOrdinal = Math.max(ordinal, highestOrdinal); + if (token instanceof FractionsToken) + { + fractionsLength = parsePosition.getIndex() - index; + values[ordinal] = scale(value, fractionsLength); + } + } + } + + return new DateTime( + Field.values()[Math.min(highestOrdinal, NANO.ordinal())], + values[Field.YEAR.ordinal()], + values[Field.MONTH.ordinal()], + values[Field.DAY.ordinal()], + values[Field.HOUR.ordinal()], + values[Field.MINUTE.ordinal()], + values[Field.SECOND.ordinal()], + values[Field.NANO.ordinal()], + values[Field.ZONE_OFFSET.ordinal()] != -1 ? TimezoneOffset.ofTotalSeconds(values[Field.ZONE_OFFSET.ordinal()]) : null, + fractionsLength + ); + } + + private int scale(int value, int length) + { + int pos = length; + while (pos < 9) + { + value *= 10; + pos++; + } + return value; + } +} diff --git a/src/main/java/com/ethlo/time/token/DateTimeParser.java b/src/main/java/com/ethlo/time/token/DateTimeParser.java new file mode 100644 index 0000000..b97d441 --- /dev/null +++ b/src/main/java/com/ethlo/time/token/DateTimeParser.java @@ -0,0 +1,35 @@ +package com.ethlo.time.token; + +/*- + * #%L + * Internet Time Utility + * %% + * Copyright (C) 2017 - 2024 Morten Haraldsen (ethlo) + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import java.text.ParsePosition; + +import com.ethlo.time.DateTime; + +public interface DateTimeParser +{ + DateTime parse(String text, ParsePosition parsePosition); + + default DateTime parse(String text) + { + return parse(text, new ParsePosition(0)); + } +} diff --git a/src/main/java/com/ethlo/time/token/DateTimeToken.java b/src/main/java/com/ethlo/time/token/DateTimeToken.java new file mode 100644 index 0000000..dc7b561 --- /dev/null +++ b/src/main/java/com/ethlo/time/token/DateTimeToken.java @@ -0,0 +1,35 @@ +package com.ethlo.time.token; + +/*- + * #%L + * Internet Time Utility + * %% + * Copyright (C) 2017 - 2024 Morten Haraldsen (ethlo) + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import java.text.ParsePosition; + +import com.ethlo.time.Field; + +public interface DateTimeToken +{ + int read(String text, ParsePosition parsePosition); + + default Field getField() + { + return null; + } +} diff --git a/src/main/java/com/ethlo/time/token/DateTimeTokens.java b/src/main/java/com/ethlo/time/token/DateTimeTokens.java new file mode 100644 index 0000000..61ba826 --- /dev/null +++ b/src/main/java/com/ethlo/time/token/DateTimeTokens.java @@ -0,0 +1,60 @@ +package com.ethlo.time.token; + +/*- + * #%L + * Internet Time Utility + * %% + * Copyright (C) 2017 - 2024 Morten Haraldsen (ethlo) + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import com.ethlo.time.Field; +import com.ethlo.time.internal.token.DigitsToken; +import com.ethlo.time.internal.token.FractionsToken; +import com.ethlo.time.internal.token.SeparatorToken; +import com.ethlo.time.internal.token.SeparatorsToken; +import com.ethlo.time.internal.token.ZoneOffsetToken; + +public class DateTimeTokens +{ + public static DateTimeToken separators(char... anyOf) + { + if (anyOf == null || anyOf.length == 0) + { + throw new IllegalArgumentException("Need at least one separator character"); + } + + if (anyOf.length == 1) + { + return new SeparatorToken(anyOf[0]); + } + return new SeparatorsToken(anyOf); + } + + public static DateTimeToken digits(Field field, int length) + { + return new DigitsToken(field, length); + } + + public static DateTimeToken fractions() + { + return new FractionsToken(); + } + + public static DateTimeToken zoneOffset() + { + return new ZoneOffsetToken(); + } +} diff --git a/src/test/java/com/ethlo/time/CharArrayUtilTest.java b/src/test/java/com/ethlo/time/CharArrayUtilTest.java index ec2505d..931ab7b 100644 --- a/src/test/java/com/ethlo/time/CharArrayUtilTest.java +++ b/src/test/java/com/ethlo/time/CharArrayUtilTest.java @@ -26,7 +26,7 @@ import org.junit.jupiter.api.Test; -import com.ethlo.time.internal.LimitedCharArrayIntegerUtil; +import com.ethlo.time.internal.util.LimitedCharArrayIntegerUtil; public class CharArrayUtilTest { diff --git a/src/test/java/com/ethlo/time/ParsePositionTest.java b/src/test/java/com/ethlo/time/ParsePositionTest.java index 8e34738..22f0f03 100644 --- a/src/test/java/com/ethlo/time/ParsePositionTest.java +++ b/src/test/java/com/ethlo/time/ParsePositionTest.java @@ -9,9 +9,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/src/test/java/com/ethlo/time/token/ConfigurableDateTimeParserTest.java b/src/test/java/com/ethlo/time/token/ConfigurableDateTimeParserTest.java new file mode 100644 index 0000000..3cf3e4c --- /dev/null +++ b/src/test/java/com/ethlo/time/token/ConfigurableDateTimeParserTest.java @@ -0,0 +1,223 @@ +package com.ethlo.time.token; + +/*- + * #%L + * Internet Time Utility + * %% + * Copyright (C) 2017 - 2024 Morten Haraldsen (ethlo) + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import static com.ethlo.time.Field.DAY; +import static com.ethlo.time.Field.HOUR; +import static com.ethlo.time.Field.MINUTE; +import static com.ethlo.time.Field.MONTH; +import static com.ethlo.time.Field.SECOND; +import static com.ethlo.time.Field.YEAR; +import static com.ethlo.time.token.DateTimeTokens.digits; +import static com.ethlo.time.token.DateTimeTokens.fractions; +import static com.ethlo.time.token.DateTimeTokens.separators; +import static com.ethlo.time.token.DateTimeTokens.zoneOffset; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.text.ParsePosition; +import java.time.format.DateTimeParseException; + +import org.junit.jupiter.api.Test; + +import com.ethlo.time.DateTime; +import com.ethlo.time.ITU; +import com.ethlo.time.internal.token.FractionsToken; +import com.ethlo.time.internal.token.ZoneOffsetToken; + +public class ConfigurableDateTimeParserTest +{ + private final DateTimeParser rfc3339Parser = DateTimeParsers.of( + digits(YEAR, 4), + separators('-'), + digits(MONTH, 2), + separators('-'), + digits(DAY, 2), + separators('T', 't', ' '), + digits(HOUR, 2), + separators(':'), + digits(MINUTE, 2), + separators(':'), + digits(SECOND, 2), + separators('.'), + fractions(), + zoneOffset() + ); + + @Test + void parseCustomFormat() + { + final DateTimeParser parser = DateTimeParsers.of( + digits(DAY, 2), + separators('-'), + digits(MONTH, 2), + separators('-'), + digits(YEAR, 4), + separators(' '), + digits(HOUR, 2), + digits(MINUTE, 2), + digits(SECOND, 2), + separators(','), + fractions() + ); + final ParsePosition pos = new ParsePosition(0); + final String input = "31-12-2000 235937,123456"; + final DateTime result = parser.parse(input, pos); + assertThat(result).isEqualTo(DateTime.of(2000, 12, 31, 23, 59, 37, 123456000, null, 6)); + } + + @Test + void duplicateField() + { + final IllegalArgumentException exc = assertThrows(IllegalArgumentException.class, () -> DateTimeParsers.of( + digits(HOUR, 2), + separators('a', 'z'), + digits(HOUR, 4), + separators('X') + ) + ); + assertThat(exc).hasMessage("Duplicate field HOUR in list of tokens: [digits: HOUR(2), separators: [a, z], digits: HOUR(4), separator: X]"); + } + + @Test + void parseRfc3339Format() + { + final DateTimeParser parser = DateTimeParsers.of( + digits(YEAR, 4), + separators('-'), + digits(MONTH, 2), + separators('-'), + digits(DAY, 2), + separators('T', 't'), + digits(HOUR, 2), + separators(':'), + digits(MINUTE, 2), + separators(':'), + digits(SECOND, 2), + separators('.'), + fractions(), + zoneOffset() + ); + final String input = "2023-01-01T23:38:34.987654321+06:00"; + final DateTime fixed = ITU.parseLenient(input); + + final ParsePosition pos = new ParsePosition(0); + final DateTime custom = parser.parse(input, pos); + + assertThat(custom).isEqualTo(fixed); + assertThat(fixed.toString()).isEqualTo(input); + assertThat(custom.toString()).isEqualTo(input); + } + + @Test + void testInvalidSeparators() + { + final DateTimeParseException exc = assertThrows(DateTimeParseException.class, () -> DateTimeParsers.of(separators('X')).parse("12")); + assertThat(exc).hasMessage("Expected character [X] at position 1, found 1: 12"); + } + + @Test + void testSeparators() + { + final ParsePosition position = new ParsePosition(0); + final int result = separators('-', '_').read("_", position); + assertThat(result).isEqualTo(1); + } + + @Test + void testInvalidSeparator() + { + final DateTimeParseException exc = assertThrows(DateTimeParseException.class, () -> DateTimeParsers.of(separators('X')).parse("12")); + assertThat(exc).hasMessage("Expected character [X] at position 1, found 1: 12"); + } + + @Test + void testEndOfTextSeparator() + { + final ParsePosition pos = new ParsePosition(0); + final DateTimeParseException exc = assertThrows(DateTimeParseException.class, () -> separators('-').read("", pos)); + assertThat(exc).hasMessage("Unexpected end of input: "); + } + + @Test + void reachEndOfFractions() + { + final int value = new FractionsToken().read("123456X", new ParsePosition(0)); + assertThat(value).isEqualTo(123456); + } + + @Test + void readTimeZoneZuluUpper() + { + final ParsePosition pos = new ParsePosition(0); + assertThat(new ZoneOffsetToken().read("Z", pos)).isEqualTo(0); + } + + @Test + void readTimeZoneZuluLower() + { + final ParsePosition pos = new ParsePosition(0); + assertThat(new ZoneOffsetToken().read("z", pos)).isEqualTo(0); + } + + @Test + void readTimeZoneUnexpectedChar() + { + final ParsePosition pos = new ParsePosition(0); + final DateTimeParseException exc = assertThrows(DateTimeParseException.class, () -> new ZoneOffsetToken().read("X", pos)); + assertThat(exc).hasMessage("Expected character [Z, z, +, -] at position 1, found X: X"); + } + + @Test + void readTimeZoneTooShort() + { + final ParsePosition pos = new ParsePosition(0); + final DateTimeParseException exc = assertThrows(DateTimeParseException.class, () -> new ZoneOffsetToken().read("-06:0", pos)); + assertThat(exc).hasMessage("Invalid timezone offset: -06:0"); + } + + @Test + void readTimeZoneNegative() + { + final ParsePosition pos = new ParsePosition(0); + final int secs = new ZoneOffsetToken().read("-06:30", pos); + assertThat(secs).isEqualTo(-23400); + } + + @Test + void testOffset() + { + final ParsePosition pos = new ParsePosition(10); + final String text = "2019-12-31T22:20:14.123+05:30"; + rfc3339Parser.parse("123456789," + text + ",something", pos); + assertThat(pos.getIndex()).isEqualTo(10 + text.length()); + } + + @Test + void testOffsetError() + { + final ParsePosition pos = new ParsePosition(10); + final String text = "2019-12-31T22:20X14.123+05:30"; + assertThrows(DateTimeParseException.class, () -> rfc3339Parser.parse("123456789," + text + ",something", pos)); + assertThat(pos.getIndex()).isEqualTo(26); + assertThat(pos.getErrorIndex()).isEqualTo(26); + } +} diff --git a/src/test/java/com/ethlo/time/token/DateTimeParsers.java b/src/test/java/com/ethlo/time/token/DateTimeParsers.java new file mode 100644 index 0000000..976ab63 --- /dev/null +++ b/src/test/java/com/ethlo/time/token/DateTimeParsers.java @@ -0,0 +1,65 @@ +package com.ethlo.time.token; + +/*- + * #%L + * Internet Time Utility + * %% + * Copyright (C) 2017 - 2024 Morten Haraldsen (ethlo) + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import static com.ethlo.time.Field.DAY; +import static com.ethlo.time.Field.HOUR; +import static com.ethlo.time.Field.MINUTE; +import static com.ethlo.time.Field.MONTH; +import static com.ethlo.time.Field.SECOND; +import static com.ethlo.time.Field.YEAR; +import static com.ethlo.time.token.DateTimeTokens.digits; +import static com.ethlo.time.token.DateTimeTokens.fractions; +import static com.ethlo.time.token.DateTimeTokens.separators; + +public class DateTimeParsers +{ + private static final ConfigurableDateTimeParser DATE = (ConfigurableDateTimeParser) DateTimeParsers.of( + digits(YEAR, 4), + separators('-'), + digits(MONTH, 2), + separators('-'), + digits(DAY, 2) + ); + private static final DateTimeParser LOCAL_TIME = of( + digits(HOUR, 2), + separators(':'), + digits(MINUTE, 2), + separators(':'), + digits(SECOND, 2), + fractions() + ); + + public static DateTimeParser of(DateTimeToken... tokens) + { + return ConfigurableDateTimeParser.of(tokens); + } + + public static DateTimeParser localDate() + { + return DATE; + } + + public static DateTimeParser localTime() + { + return LOCAL_TIME; + } +} diff --git a/src/test/resources/test-data.json b/src/test/resources/test-data.json index ef7f736..26da8c5 100644 --- a/src/test/resources/test-data.json +++ b/src/test/resources/test-data.json @@ -91,7 +91,7 @@ }, { "input": "2020-22-12T12:11.56+04:30", - "error": "Unexpected character . at position 17: 2020-22-12T12:11.56+04:30", + "error": "Expected character [:, Z, z, +, -] at position 17, found .: 2020-22-12T12:11.56+04:30", "error_index": 16 }, { @@ -131,7 +131,7 @@ }, { "input": "2017-02-21T15:27:22~10:00", - "error": "Unexpected character ~ at position 20: 2017-02-21T15:27:22~10:00", + "error": "Expected character [., Z, z, +, -] at position 20, found ~: 2017-02-21T15:27:22~10:00", "error_index": 19 }, { @@ -181,7 +181,7 @@ }, { "input": "2017-12-21T12:20:45.9b7Z", - "error": "Unexpected character b at position 22: 2017-12-21T12:20:45.9b7Z", + "error": "Expected character [Z, z, +, -] at position 22, found b: 2017-12-21T12:20:45.9b7Z", "error_index": 21 }, { @@ -231,7 +231,7 @@ }, { "input": "199g-11-05T08:15:30-05:00", - "error": "Unexpected character g at position 4: 199g-11-05T08:15:30-05:00", + "error": "Expected character [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] at position 4, found g: 199g-11-05T08:15:30-05:00", "error_index": 3, "note": "Non-digit in year" },