From 5e98a2b1a8ab1565474e8955dd048532d47307d3 Mon Sep 17 00:00:00 2001 From: Morten Haraldsen Date: Sat, 3 Feb 2024 10:42:51 +0100 Subject: [PATCH 1/7] Make it possible to build a parser by tokens --- src/main/java/com/ethlo/time/Field.java | 3 +- .../java/com/ethlo/time/TimezoneOffset.java | 10 ++ .../internal/LimitedCharArrayIntegerUtil.java | 3 +- .../token/ConfigurableDateTimeParser.java | 102 ++++++++++++++++++ .../com/ethlo/time/token/DateTimeParser.java | 30 ++++++ .../com/ethlo/time/token/DateTimeToken.java | 32 ++++++ .../com/ethlo/time/token/DigitsToken.java | 53 +++++++++ .../com/ethlo/time/token/FourDigitToken.java | 31 ++++++ .../com/ethlo/time/token/FractionsToken.java | 60 +++++++++++ .../com/ethlo/time/token/SeparatorToken.java | 62 +++++++++++ .../com/ethlo/time/token/SeparatorsToken.java | 65 +++++++++++ .../ethlo/time/token/TimeZoneOffsetToken.java | 83 ++++++++++++++ .../com/ethlo/time/token/TwoDigitToken.java | 31 ++++++ .../token/ConfigurableDateTimeParserTest.java | 71 ++++++++++++ .../com/ethlo/time/token/DateTimeParsers.java | 80 ++++++++++++++ 15 files changed, 713 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/ethlo/time/token/ConfigurableDateTimeParser.java create mode 100644 src/main/java/com/ethlo/time/token/DateTimeParser.java create mode 100644 src/main/java/com/ethlo/time/token/DateTimeToken.java create mode 100644 src/main/java/com/ethlo/time/token/DigitsToken.java create mode 100644 src/main/java/com/ethlo/time/token/FourDigitToken.java create mode 100644 src/main/java/com/ethlo/time/token/FractionsToken.java create mode 100644 src/main/java/com/ethlo/time/token/SeparatorToken.java create mode 100644 src/main/java/com/ethlo/time/token/SeparatorsToken.java create mode 100644 src/main/java/com/ethlo/time/token/TimeZoneOffsetToken.java create mode 100644 src/main/java/com/ethlo/time/token/TwoDigitToken.java create mode 100644 src/test/java/com/ethlo/time/token/ConfigurableDateTimeParserTest.java create mode 100644 src/test/java/com/ethlo/time/token/DateTimeParsers.java 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/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/LimitedCharArrayIntegerUtil.java b/src/main/java/com/ethlo/time/internal/LimitedCharArrayIntegerUtil.java index 18cd9d2..81f39bd 100644 --- a/src/main/java/com/ethlo/time/internal/LimitedCharArrayIntegerUtil.java +++ b/src/main/java/com/ethlo/time/internal/LimitedCharArrayIntegerUtil.java @@ -52,7 +52,6 @@ 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); @@ -60,7 +59,7 @@ public static int parsePositiveInt(final String strNum, int startInclusive, int { ErrorUtil.raiseUnexpectedCharacter(strNum, i); } - 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..c8b4f4e --- /dev/null +++ b/src/main/java/com/ethlo/time/token/ConfigurableDateTimeParser.java @@ -0,0 +1,102 @@ +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 com.ethlo.time.DateTime; +import com.ethlo.time.Field; +import com.ethlo.time.TimezoneOffset; + +public class ConfigurableDateTimeParser implements DateTimeParser +{ + private final DateTimeToken[] tokens; + + public ConfigurableDateTimeParser(DateTimeToken... tokens) + { + this.tokens = tokens; + } + + public ConfigurableDateTimeParser combine(DateTimeToken... tokens) + { + return new ConfigurableDateTimeParser(combine(this.tokens, tokens)); + } + + private DateTimeToken[] combine(DateTimeToken[] a, DateTimeToken[] b) + { + final DateTimeToken[] result = new DateTimeToken[a.length + b.length]; + System.arraycopy(a, 0, result, 0, a.length); + System.arraycopy(b, 0, result, a.length, b.length); + return result; + } + + @Override + public DateTime parse(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..0d5f876 --- /dev/null +++ b/src/main/java/com/ethlo/time/token/DateTimeParser.java @@ -0,0 +1,30 @@ +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); +} 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..0e85fb1 --- /dev/null +++ b/src/main/java/com/ethlo/time/token/DateTimeToken.java @@ -0,0 +1,32 @@ +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); + + Field getField(); +} diff --git a/src/main/java/com/ethlo/time/token/DigitsToken.java b/src/main/java/com/ethlo/time/token/DigitsToken.java new file mode 100644 index 0000000..5db9c74 --- /dev/null +++ b/src/main/java/com/ethlo/time/token/DigitsToken.java @@ -0,0 +1,53 @@ +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.LimitedCharArrayIntegerUtil; + +import java.text.ParsePosition; + +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; + } +} diff --git a/src/main/java/com/ethlo/time/token/FourDigitToken.java b/src/main/java/com/ethlo/time/token/FourDigitToken.java new file mode 100644 index 0000000..2a49edb --- /dev/null +++ b/src/main/java/com/ethlo/time/token/FourDigitToken.java @@ -0,0 +1,31 @@ +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; + +public class FourDigitToken extends DigitsToken +{ + public FourDigitToken(Field field) + { + super(field, 4); + } +} diff --git a/src/main/java/com/ethlo/time/token/FractionsToken.java b/src/main/java/com/ethlo/time/token/FractionsToken.java new file mode 100644 index 0000000..82e5e80 --- /dev/null +++ b/src/main/java/com/ethlo/time/token/FractionsToken.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 static com.ethlo.time.internal.LimitedCharArrayIntegerUtil.DIGIT_9; +import static com.ethlo.time.internal.LimitedCharArrayIntegerUtil.ZERO; + +import java.text.ParsePosition; + +import com.ethlo.time.Field; + +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/token/SeparatorToken.java b/src/main/java/com/ethlo/time/token/SeparatorToken.java new file mode 100644 index 0000000..747e485 --- /dev/null +++ b/src/main/java/com/ethlo/time/token/SeparatorToken.java @@ -0,0 +1,62 @@ +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; +import com.ethlo.time.internal.ErrorUtil; + +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); + } + parsePosition.setIndex(index + 1); + return 1; + } + + @Override + public Field getField() + { + return null; + } +} diff --git a/src/main/java/com/ethlo/time/token/SeparatorsToken.java b/src/main/java/com/ethlo/time/token/SeparatorsToken.java new file mode 100644 index 0000000..f9c8644 --- /dev/null +++ b/src/main/java/com/ethlo/time/token/SeparatorsToken.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 java.text.ParsePosition; +import java.time.format.DateTimeParseException; +import java.util.Arrays; + +import com.ethlo.time.Field; +import com.ethlo.time.internal.ErrorUtil; + +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 Field getField() + { + return null; + } +} diff --git a/src/main/java/com/ethlo/time/token/TimeZoneOffsetToken.java b/src/main/java/com/ethlo/time/token/TimeZoneOffsetToken.java new file mode 100644 index 0000000..7cf9f19 --- /dev/null +++ b/src/main/java/com/ethlo/time/token/TimeZoneOffsetToken.java @@ -0,0 +1,83 @@ +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.internal.ErrorUtil.raiseUnexpectedCharacter; +import static com.ethlo.time.internal.LimitedCharArrayIntegerUtil.parsePositiveInt; + +import java.text.ParsePosition; +import java.time.format.DateTimeParseException; + +import com.ethlo.time.Field; + +public class TimeZoneOffsetToken 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') + { + return 0; + } + + final char sign = text.charAt(idx); + if (sign != '+' && sign != '-') + { + raiseUnexpectedCharacter(text, idx); + } + + 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); + } + } + + return hours * 3600 + minutes * 60; + } + + @Override + public Field getField() + { + return Field.ZONE_OFFSET; + } +} diff --git a/src/main/java/com/ethlo/time/token/TwoDigitToken.java b/src/main/java/com/ethlo/time/token/TwoDigitToken.java new file mode 100644 index 0000000..5e5b0c4 --- /dev/null +++ b/src/main/java/com/ethlo/time/token/TwoDigitToken.java @@ -0,0 +1,31 @@ +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; + +public class TwoDigitToken extends DigitsToken +{ + public TwoDigitToken(Field field) + { + super(field, 2); + } +} 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..5868204 --- /dev/null +++ b/src/test/java/com/ethlo/time/token/ConfigurableDateTimeParserTest.java @@ -0,0 +1,71 @@ +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 org.assertj.core.api.Assertions.assertThat; + +import java.text.ParsePosition; + +import org.junit.jupiter.api.Test; + +import com.ethlo.time.DateTime; +import com.ethlo.time.ITU; + +public class ConfigurableDateTimeParserTest +{ + @Test + void parseCustomFormat() + { + final ParsePosition pos = new ParsePosition(0); + final String input = "31-12-2000 235937"; + final DateTimeParser parser = new ConfigurableDateTimeParser( + new TwoDigitToken(DAY), + new SeparatorToken('-'), + new TwoDigitToken(MONTH), + new SeparatorToken('-'), + new FourDigitToken(YEAR), + new SeparatorToken(' '), + new TwoDigitToken(HOUR), + new TwoDigitToken(MINUTE), + new TwoDigitToken(SECOND) + ); + final DateTime result = parser.parse(input, pos); + assertThat(result).isEqualTo(DateTime.of(2000, 12, 31, 23, 59, 37, null)); + } + + @Test + void parseRfc3339Format() + { + 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 = DateTimeParsers.rfc3339().parse(input, pos); + assertThat(custom).isEqualTo(fixed); + assertThat(fixed.toString()).isEqualTo(input); + assertThat(custom.toString()).isEqualTo(input); + } +} 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..5a468f0 --- /dev/null +++ b/src/test/java/com/ethlo/time/token/DateTimeParsers.java @@ -0,0 +1,80 @@ +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; + +public class DateTimeParsers +{ + private static final ConfigurableDateTimeParser DATE = new ConfigurableDateTimeParser( + new FourDigitToken(YEAR), + new SeparatorToken('-'), + new TwoDigitToken(MONTH), + new SeparatorToken('-'), + new TwoDigitToken(DAY) + ); + + private static final ConfigurableDateTimeParser MINUTES = DATE.combine( + new SeparatorToken('T'), + new TwoDigitToken(HOUR), + new SeparatorToken(':'), + new TwoDigitToken(MINUTE) + ); + + private static final ConfigurableDateTimeParser LOCAL_TIME = MINUTES.combine( + new SeparatorToken(':'), + new TwoDigitToken(SECOND) + ); + + private static final ConfigurableDateTimeParser FRACTIONAL_SECONDS_LOCAL = LOCAL_TIME.combine( + new SeparatorToken('.'), + new FractionsToken() + ); + + private static final ConfigurableDateTimeParser FRACTIONAL_SECONDS_OFFSET = FRACTIONAL_SECONDS_LOCAL.combine( + new TimeZoneOffsetToken() + ); + + public static DateTimeParser rfc3339() + { + return FRACTIONAL_SECONDS_OFFSET; + } + + public static DateTimeParser minutes() + { + return MINUTES; + } + + public static DateTimeParser seconds() + { + return LOCAL_TIME; + } + + public static DateTimeParser fractionalSeconds() + { + return FRACTIONAL_SECONDS_LOCAL; + } +} From 0484a8a70df316bf46a4a71be94079182001356b Mon Sep 17 00:00:00 2001 From: Morten Haraldsen Date: Sat, 3 Feb 2024 12:22:59 +0100 Subject: [PATCH 2/7] Cleanup and more tests --- .../com/ethlo/time/internal/ITUParser.java | 22 ++++- .../com/ethlo/time/token/DateTimeParser.java | 5 ++ .../com/ethlo/time/token/DigitsToken.java | 10 +++ .../com/ethlo/time/token/FourDigitToken.java | 31 ------- .../com/ethlo/time/token/SeparatorToken.java | 7 +- .../com/ethlo/time/token/TwoDigitToken.java | 31 ------- .../token/ConfigurableDateTimeParserTest.java | 89 ++++++++++++++++--- .../com/ethlo/time/token/DateTimeParsers.java | 12 +-- 8 files changed, 126 insertions(+), 81 deletions(-) delete mode 100644 src/main/java/com/ethlo/time/token/FourDigitToken.java delete mode 100644 src/main/java/com/ethlo/time/token/TwoDigitToken.java diff --git a/src/main/java/com/ethlo/time/internal/ITUParser.java b/src/main/java/com/ethlo/time/internal/ITUParser.java index e32b3b8..6f68add 100644 --- a/src/main/java/com/ethlo/time/internal/ITUParser.java +++ b/src/main/java/com/ethlo/time/internal/ITUParser.java @@ -28,6 +28,7 @@ import static com.ethlo.time.internal.LimitedCharArrayIntegerUtil.ZERO; import static com.ethlo.time.internal.LimitedCharArrayIntegerUtil.parsePositiveInt; +import java.text.ParsePosition; import java.time.OffsetDateTime; import java.time.format.DateTimeParseException; import java.util.Arrays; @@ -36,8 +37,9 @@ import com.ethlo.time.Field; import com.ethlo.time.ParseConfig; import com.ethlo.time.TimezoneOffset; +import com.ethlo.time.token.DateTimeParser; -public class ITUParser +public class ITUParser implements DateTimeParser { public static final char DATE_SEPARATOR = '-'; public static final char TIME_SEPARATOR = ':'; @@ -324,4 +326,22 @@ public static OffsetDateTime parseDateTime(final String chars, int offset) final Field nextGranularity = Field.values()[field.ordinal() + 1]; throw new DateTimeParseException(String.format("Unexpected end of input, missing field %s: %s", nextGranularity, chars), chars, field.getRequiredLength()); } + + @Override + public DateTime parse(final String text, final ParsePosition position) + { + try + { + int offset = position.getIndex(); + final DateTime result = ITUParser.parseLenient(text, ParseConfig.DEFAULT, position.getIndex()); + position.setIndex(offset + result.getParseLength()); + return result; + } + catch (DateTimeParseException exc) + { + position.setErrorIndex(exc.getErrorIndex()); + position.setIndex(position.getErrorIndex()); + throw exc; + } + } } \ No newline at end of file diff --git a/src/main/java/com/ethlo/time/token/DateTimeParser.java b/src/main/java/com/ethlo/time/token/DateTimeParser.java index 0d5f876..b97d441 100644 --- a/src/main/java/com/ethlo/time/token/DateTimeParser.java +++ b/src/main/java/com/ethlo/time/token/DateTimeParser.java @@ -27,4 +27,9 @@ 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/DigitsToken.java b/src/main/java/com/ethlo/time/token/DigitsToken.java index 5db9c74..166bbcb 100644 --- a/src/main/java/com/ethlo/time/token/DigitsToken.java +++ b/src/main/java/com/ethlo/time/token/DigitsToken.java @@ -36,6 +36,16 @@ public DigitsToken(Field field, int length) this.length = length; } + public static DateTimeToken ofTwo(Field field) + { + return new DigitsToken(field, 2); + } + + public static DateTimeToken ofFour(Field field) + { + return new DigitsToken(field, 4); + } + @Override public int read(String text, ParsePosition parsePosition) { diff --git a/src/main/java/com/ethlo/time/token/FourDigitToken.java b/src/main/java/com/ethlo/time/token/FourDigitToken.java deleted file mode 100644 index 2a49edb..0000000 --- a/src/main/java/com/ethlo/time/token/FourDigitToken.java +++ /dev/null @@ -1,31 +0,0 @@ -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; - -public class FourDigitToken extends DigitsToken -{ - public FourDigitToken(Field field) - { - super(field, 4); - } -} diff --git a/src/main/java/com/ethlo/time/token/SeparatorToken.java b/src/main/java/com/ethlo/time/token/SeparatorToken.java index 747e485..0d3f42e 100644 --- a/src/main/java/com/ethlo/time/token/SeparatorToken.java +++ b/src/main/java/com/ethlo/time/token/SeparatorToken.java @@ -34,11 +34,16 @@ public SeparatorToken(char separator) this.separator = separator; } + public static DateTimeToken separator(char c) + { + return new SeparatorToken(c); + } + @Override public int read(final String text, final ParsePosition parsePosition) { final int index = parsePosition.getIndex(); - if (text.length() >= index && text.charAt(index) == separator) + if (text.length() > index && text.charAt(index) == separator) { parsePosition.setIndex(index + 1); } diff --git a/src/main/java/com/ethlo/time/token/TwoDigitToken.java b/src/main/java/com/ethlo/time/token/TwoDigitToken.java deleted file mode 100644 index 5e5b0c4..0000000 --- a/src/main/java/com/ethlo/time/token/TwoDigitToken.java +++ /dev/null @@ -1,31 +0,0 @@ -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; - -public class TwoDigitToken extends DigitsToken -{ - public TwoDigitToken(Field field) - { - super(field, 2); - } -} diff --git a/src/test/java/com/ethlo/time/token/ConfigurableDateTimeParserTest.java b/src/test/java/com/ethlo/time/token/ConfigurableDateTimeParserTest.java index 5868204..a28efc3 100644 --- a/src/test/java/com/ethlo/time/token/ConfigurableDateTimeParserTest.java +++ b/src/test/java/com/ethlo/time/token/ConfigurableDateTimeParserTest.java @@ -26,9 +26,14 @@ 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.DigitsToken.ofFour; +import static com.ethlo.time.token.DigitsToken.ofTwo; +import static com.ethlo.time.token.SeparatorToken.separator; 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; @@ -41,20 +46,22 @@ public class ConfigurableDateTimeParserTest void parseCustomFormat() { final ParsePosition pos = new ParsePosition(0); - final String input = "31-12-2000 235937"; + final String input = "31-12-2000 235937,123456"; final DateTimeParser parser = new ConfigurableDateTimeParser( - new TwoDigitToken(DAY), - new SeparatorToken('-'), - new TwoDigitToken(MONTH), - new SeparatorToken('-'), - new FourDigitToken(YEAR), - new SeparatorToken(' '), - new TwoDigitToken(HOUR), - new TwoDigitToken(MINUTE), - new TwoDigitToken(SECOND) + ofTwo(DAY), + separator('-'), + ofTwo(MONTH), + separator('-'), + ofFour(YEAR), + separator(' '), + ofTwo(HOUR), + ofTwo(MINUTE), + ofTwo(SECOND), + separator(','), + new FractionsToken() ); final DateTime result = parser.parse(input, pos); - assertThat(result).isEqualTo(DateTime.of(2000, 12, 31, 23, 59, 37, null)); + assertThat(result).isEqualTo(DateTime.of(2000, 12, 31, 23, 59, 37, 123456000, null, 6)); } @Test @@ -68,4 +75,64 @@ void parseRfc3339Format() assertThat(fixed.toString()).isEqualTo(input); assertThat(custom.toString()).isEqualTo(input); } + + @Test + void testInvalidSeparator() + { + final DateTimeParseException exc = assertThrows(DateTimeParseException.class, () -> new ConfigurableDateTimeParser(separator('X')).parse("12")); + assertThat(exc).hasMessage("Unexpected character 1 at position 1: 12"); + } + + @Test + void testEndOfTextSeparator() + { + final ParsePosition pos = new ParsePosition(0); + final DateTimeParseException exc = assertThrows(DateTimeParseException.class, ()->separator('-').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 TimeZoneOffsetToken().read("Z", pos)).isEqualTo(0); + } + + @Test + void readTimeZoneZuluLower() + { + final ParsePosition pos = new ParsePosition(0); + assertThat(new TimeZoneOffsetToken().read("z", pos)).isEqualTo(0); + } + + @Test + void readTimeZoneUnexpectedChar() + { + final ParsePosition pos = new ParsePosition(0); + final DateTimeParseException exc = assertThrows(DateTimeParseException.class, ()->new TimeZoneOffsetToken().read("X", pos)); + assertThat(exc).hasMessage("Unexpected character X at position 1: X"); + } + + @Test + void readTimeZoneTooShort() + { + final ParsePosition pos = new ParsePosition(0); + final DateTimeParseException exc = assertThrows(DateTimeParseException.class, ()->new TimeZoneOffsetToken().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 TimeZoneOffsetToken().read("-06:30", pos); + assertThat(secs).isEqualTo(-23400); + } } diff --git a/src/test/java/com/ethlo/time/token/DateTimeParsers.java b/src/test/java/com/ethlo/time/token/DateTimeParsers.java index 5a468f0..daa855b 100644 --- a/src/test/java/com/ethlo/time/token/DateTimeParsers.java +++ b/src/test/java/com/ethlo/time/token/DateTimeParsers.java @@ -30,23 +30,23 @@ public class DateTimeParsers { private static final ConfigurableDateTimeParser DATE = new ConfigurableDateTimeParser( - new FourDigitToken(YEAR), + new DigitsToken(YEAR, 4), new SeparatorToken('-'), - new TwoDigitToken(MONTH), + new DigitsToken(MONTH, 2), new SeparatorToken('-'), - new TwoDigitToken(DAY) + new DigitsToken(DAY, 2) ); private static final ConfigurableDateTimeParser MINUTES = DATE.combine( new SeparatorToken('T'), - new TwoDigitToken(HOUR), + new DigitsToken(HOUR, 2), new SeparatorToken(':'), - new TwoDigitToken(MINUTE) + new DigitsToken(MINUTE, 2) ); private static final ConfigurableDateTimeParser LOCAL_TIME = MINUTES.combine( new SeparatorToken(':'), - new TwoDigitToken(SECOND) + new DigitsToken(SECOND, 2) ); private static final ConfigurableDateTimeParser FRACTIONAL_SECONDS_LOCAL = LOCAL_TIME.combine( From c70bbad2a5efa53260fadbf3b9ac8920fecb2c2f Mon Sep 17 00:00:00 2001 From: Morten Haraldsen Date: Sat, 3 Feb 2024 16:20:07 +0100 Subject: [PATCH 3/7] Cleanup and more tests --- src/main/java/com/ethlo/time/DateTime.java | 18 +++--- src/main/java/com/ethlo/time/ITU.java | 4 +- src/main/java/com/ethlo/time/ParseConfig.java | 6 +- .../internal/{ => fixed}/ITUFormatter.java | 20 ++++--- .../time/internal/{ => fixed}/ITUParser.java | 31 +++++----- .../{ => internal}/token/DigitsToken.java | 23 +++----- .../{ => internal}/token/FractionsToken.java | 11 ++-- .../{ => internal}/token/SeparatorToken.java | 12 ++-- .../{ => internal}/token/SeparatorsToken.java | 12 +--- .../token/TimeZoneOffsetToken.java | 17 ++++-- .../ethlo/time/internal/util/ArrayUtils.java | 12 ++++ .../internal/{ => util}/DateTimeMath.java | 2 +- .../{ => util}/DefaultLeapSecondHandler.java | 2 +- .../time/internal/{ => util}/ErrorUtil.java | 9 +-- .../{ => util}/LeapSecondHandler.java | 2 +- .../LimitedCharArrayIntegerUtil.java | 4 +- .../token/ConfigurableDateTimeParser.java | 1 + .../com/ethlo/time/token/DateTimeToken.java | 5 +- .../com/ethlo/time/token/DateTimeTokens.java | 40 +++++++++++++ .../com/ethlo/time/CharArrayUtilTest.java | 2 +- .../token/ConfigurableDateTimeParserTest.java | 57 ++++++++++++------- .../com/ethlo/time/token/DateTimeParsers.java | 33 ++++++----- src/test/resources/test-data.json | 8 +-- 23 files changed, 200 insertions(+), 131 deletions(-) rename src/main/java/com/ethlo/time/internal/{ => fixed}/ITUFormatter.java (87%) rename src/main/java/com/ethlo/time/internal/{ => fixed}/ITUParser.java (92%) rename src/main/java/com/ethlo/time/{ => internal}/token/DigitsToken.java (81%) rename src/main/java/com/ethlo/time/{ => internal}/token/FractionsToken.java (85%) rename src/main/java/com/ethlo/time/{ => internal}/token/SeparatorToken.java (86%) rename src/main/java/com/ethlo/time/{ => internal}/token/SeparatorsToken.java (90%) rename src/main/java/com/ethlo/time/{ => internal}/token/TimeZoneOffsetToken.java (77%) create mode 100644 src/main/java/com/ethlo/time/internal/util/ArrayUtils.java rename src/main/java/com/ethlo/time/internal/{ => util}/DateTimeMath.java (98%) rename src/main/java/com/ethlo/time/internal/{ => util}/DefaultLeapSecondHandler.java (98%) rename src/main/java/com/ethlo/time/internal/{ => util}/ErrorUtil.java (86%) rename src/main/java/com/ethlo/time/internal/{ => util}/LeapSecondHandler.java (95%) rename src/main/java/com/ethlo/time/internal/{ => util}/LimitedCharArrayIntegerUtil.java (97%) create mode 100644 src/main/java/com/ethlo/time/token/DateTimeTokens.java diff --git a/src/main/java/com/ethlo/time/DateTime.java b/src/main/java/com/ethlo/time/DateTime.java index c62c03c..72dc7d0 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.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.fixed.ITUFormatter.finish; +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/ITU.java b/src/main/java/com/ethlo/time/ITU.java index 633e8c0..a2545b4 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. diff --git a/src/main/java/com/ethlo/time/ParseConfig.java b/src/main/java/com/ethlo/time/ParseConfig.java index ae61a22..1d4ef8d 100644 --- a/src/main/java/com/ethlo/time/ParseConfig.java +++ b/src/main/java/com/ethlo/time/ParseConfig.java @@ -20,9 +20,9 @@ * #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 static com.ethlo.time.internal.fixed.ITUParser.SEPARATOR_LOWER; +import static com.ethlo.time.internal.fixed.ITUParser.SEPARATOR_SPACE; +import static com.ethlo.time.internal.fixed.ITUParser.SEPARATOR_UPPER; import java.util.Arrays; import java.util.Optional; 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..2e10ee1 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 @@ -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 92% rename from src/main/java/com/ethlo/time/internal/ITUParser.java rename to src/main/java/com/ethlo/time/internal/fixed/ITUParser.java index 6f68add..ed374e6 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.text.ParsePosition; import java.time.OffsetDateTime; @@ -37,6 +37,7 @@ import com.ethlo.time.Field; import com.ethlo.time.ParseConfig; import com.ethlo.time.TimezoneOffset; +import com.ethlo.time.internal.util.ArrayUtils; import com.ethlo.time.token.DateTimeParser; public class ITUParser implements DateTimeParser @@ -46,11 +47,11 @@ public class ITUParser implements DateTimeParser 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; @@ -78,7 +79,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); } } @@ -111,7 +112,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) @@ -260,7 +261,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/token/DigitsToken.java b/src/main/java/com/ethlo/time/internal/token/DigitsToken.java similarity index 81% rename from src/main/java/com/ethlo/time/token/DigitsToken.java rename to src/main/java/com/ethlo/time/internal/token/DigitsToken.java index 166bbcb..17fed5d 100644 --- a/src/main/java/com/ethlo/time/token/DigitsToken.java +++ b/src/main/java/com/ethlo/time/internal/token/DigitsToken.java @@ -1,4 +1,4 @@ -package com.ethlo.time.token; +package com.ethlo.time.internal.token; /*- * #%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,11 +20,12 @@ * #L% */ -import com.ethlo.time.Field; -import com.ethlo.time.internal.LimitedCharArrayIntegerUtil; - 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; @@ -36,16 +37,6 @@ public DigitsToken(Field field, int length) this.length = length; } - public static DateTimeToken ofTwo(Field field) - { - return new DigitsToken(field, 2); - } - - public static DateTimeToken ofFour(Field field) - { - return new DigitsToken(field, 4); - } - @Override public int read(String text, ParsePosition parsePosition) { diff --git a/src/main/java/com/ethlo/time/token/FractionsToken.java b/src/main/java/com/ethlo/time/internal/token/FractionsToken.java similarity index 85% rename from src/main/java/com/ethlo/time/token/FractionsToken.java rename to src/main/java/com/ethlo/time/internal/token/FractionsToken.java index 82e5e80..9825403 100644 --- a/src/main/java/com/ethlo/time/token/FractionsToken.java +++ b/src/main/java/com/ethlo/time/internal/token/FractionsToken.java @@ -1,4 +1,4 @@ -package com.ethlo.time.token; +package com.ethlo.time.internal.token; /*- * #%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,12 +20,13 @@ * #L% */ -import static com.ethlo.time.internal.LimitedCharArrayIntegerUtil.DIGIT_9; -import static com.ethlo.time.internal.LimitedCharArrayIntegerUtil.ZERO; +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 { diff --git a/src/main/java/com/ethlo/time/token/SeparatorToken.java b/src/main/java/com/ethlo/time/internal/token/SeparatorToken.java similarity index 86% rename from src/main/java/com/ethlo/time/token/SeparatorToken.java rename to src/main/java/com/ethlo/time/internal/token/SeparatorToken.java index 0d3f42e..93253b3 100644 --- a/src/main/java/com/ethlo/time/token/SeparatorToken.java +++ b/src/main/java/com/ethlo/time/internal/token/SeparatorToken.java @@ -1,4 +1,4 @@ -package com.ethlo.time.token; +package com.ethlo.time.internal.token; /*- * #%L @@ -23,7 +23,8 @@ import java.text.ParsePosition; import com.ethlo.time.Field; -import com.ethlo.time.internal.ErrorUtil; +import com.ethlo.time.internal.util.ErrorUtil; +import com.ethlo.time.token.DateTimeToken; public class SeparatorToken implements DateTimeToken { @@ -34,11 +35,6 @@ public SeparatorToken(char separator) this.separator = separator; } - public static DateTimeToken separator(char c) - { - return new SeparatorToken(c); - } - @Override public int read(final String text, final ParsePosition parsePosition) { @@ -53,7 +49,7 @@ else if (text.length() <= index) } else if (text.charAt(index) != separator) { - ErrorUtil.raiseUnexpectedCharacter(text, index); + ErrorUtil.raiseUnexpectedCharacter(text, index, separator); } parsePosition.setIndex(index + 1); return 1; diff --git a/src/main/java/com/ethlo/time/token/SeparatorsToken.java b/src/main/java/com/ethlo/time/internal/token/SeparatorsToken.java similarity index 90% rename from src/main/java/com/ethlo/time/token/SeparatorsToken.java rename to src/main/java/com/ethlo/time/internal/token/SeparatorsToken.java index f9c8644..fc481ff 100644 --- a/src/main/java/com/ethlo/time/token/SeparatorsToken.java +++ b/src/main/java/com/ethlo/time/internal/token/SeparatorsToken.java @@ -1,4 +1,4 @@ -package com.ethlo.time.token; +package com.ethlo.time.internal.token; /*- * #%L @@ -24,8 +24,8 @@ import java.time.format.DateTimeParseException; import java.util.Arrays; -import com.ethlo.time.Field; -import com.ethlo.time.internal.ErrorUtil; +import com.ethlo.time.internal.util.ErrorUtil; +import com.ethlo.time.token.DateTimeToken; public class SeparatorsToken implements DateTimeToken { @@ -56,10 +56,4 @@ public int read(final String text, final ParsePosition parsePosition) } 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 Field getField() - { - return null; - } } diff --git a/src/main/java/com/ethlo/time/token/TimeZoneOffsetToken.java b/src/main/java/com/ethlo/time/internal/token/TimeZoneOffsetToken.java similarity index 77% rename from src/main/java/com/ethlo/time/token/TimeZoneOffsetToken.java rename to src/main/java/com/ethlo/time/internal/token/TimeZoneOffsetToken.java index 7cf9f19..2e9bcf2 100644 --- a/src/main/java/com/ethlo/time/token/TimeZoneOffsetToken.java +++ b/src/main/java/com/ethlo/time/internal/token/TimeZoneOffsetToken.java @@ -1,4 +1,4 @@ -package com.ethlo.time.token; +package com.ethlo.time.internal.token; /*- * #%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,13 +20,18 @@ * #L% */ -import static com.ethlo.time.internal.ErrorUtil.raiseUnexpectedCharacter; -import static com.ethlo.time.internal.LimitedCharArrayIntegerUtil.parsePositiveInt; +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 TimeZoneOffsetToken implements DateTimeToken { @@ -51,7 +56,7 @@ public int read(final String text, final ParsePosition parsePosition) final char sign = text.charAt(idx); if (sign != '+' && sign != '-') { - raiseUnexpectedCharacter(text, idx); + raiseUnexpectedCharacter(text, idx, ZULU_UPPER, ZULU_LOWER, PLUS, MINUS); } if (left < 6) 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..a8a0e34 --- /dev/null +++ b/src/main/java/com/ethlo/time/internal/util/ArrayUtils.java @@ -0,0 +1,12 @@ +package com.ethlo.time.internal.util; + +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 97% rename from src/main/java/com/ethlo/time/internal/LimitedCharArrayIntegerUtil.java rename to src/main/java/com/ethlo/time/internal/util/LimitedCharArrayIntegerUtil.java index 81f39bd..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 @@ -57,7 +57,7 @@ public static int parsePositiveInt(final String strNum, int startInclusive, int 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); } diff --git a/src/main/java/com/ethlo/time/token/ConfigurableDateTimeParser.java b/src/main/java/com/ethlo/time/token/ConfigurableDateTimeParser.java index c8b4f4e..67cc64e 100644 --- a/src/main/java/com/ethlo/time/token/ConfigurableDateTimeParser.java +++ b/src/main/java/com/ethlo/time/token/ConfigurableDateTimeParser.java @@ -28,6 +28,7 @@ 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 { diff --git a/src/main/java/com/ethlo/time/token/DateTimeToken.java b/src/main/java/com/ethlo/time/token/DateTimeToken.java index 0e85fb1..94c97b8 100644 --- a/src/main/java/com/ethlo/time/token/DateTimeToken.java +++ b/src/main/java/com/ethlo/time/token/DateTimeToken.java @@ -28,5 +28,8 @@ public interface DateTimeToken { int read(String text, ParsePosition parsePosition); - Field getField(); + 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..e862adf --- /dev/null +++ b/src/main/java/com/ethlo/time/token/DateTimeTokens.java @@ -0,0 +1,40 @@ +package com.ethlo.time.token; + +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.TimeZoneOffsetToken; + +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 timeZoneOffset() + { + return new TimeZoneOffsetToken(); + } +} 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/token/ConfigurableDateTimeParserTest.java b/src/test/java/com/ethlo/time/token/ConfigurableDateTimeParserTest.java index a28efc3..45ef0dd 100644 --- a/src/test/java/com/ethlo/time/token/ConfigurableDateTimeParserTest.java +++ b/src/test/java/com/ethlo/time/token/ConfigurableDateTimeParserTest.java @@ -26,9 +26,9 @@ 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.DigitsToken.ofFour; -import static com.ethlo.time.token.DigitsToken.ofTwo; -import static com.ethlo.time.token.SeparatorToken.separator; +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 org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -39,6 +39,8 @@ import com.ethlo.time.DateTime; import com.ethlo.time.ITU; +import com.ethlo.time.internal.token.FractionsToken; +import com.ethlo.time.internal.token.TimeZoneOffsetToken; public class ConfigurableDateTimeParserTest { @@ -48,17 +50,17 @@ void parseCustomFormat() final ParsePosition pos = new ParsePosition(0); final String input = "31-12-2000 235937,123456"; final DateTimeParser parser = new ConfigurableDateTimeParser( - ofTwo(DAY), - separator('-'), - ofTwo(MONTH), - separator('-'), - ofFour(YEAR), - separator(' '), - ofTwo(HOUR), - ofTwo(MINUTE), - ofTwo(SECOND), - separator(','), - new FractionsToken() + digits(DAY, 2), + separators('-'), + digits(MONTH, 2), + separators('-'), + digits(YEAR, 4), + separators(' '), + digits(HOUR, 2), + digits(MINUTE, 2), + digits(SECOND, 2), + separators(','), + fractions() ); final DateTime result = parser.parse(input, pos); assertThat(result).isEqualTo(DateTime.of(2000, 12, 31, 23, 59, 37, 123456000, null, 6)); @@ -76,18 +78,33 @@ void parseRfc3339Format() assertThat(custom.toString()).isEqualTo(input); } + @Test + void testInvalidSeparators() + { + final DateTimeParseException exc = assertThrows(DateTimeParseException.class, () -> new ConfigurableDateTimeParser(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, () -> new ConfigurableDateTimeParser(separator('X')).parse("12")); - assertThat(exc).hasMessage("Unexpected character 1 at position 1: 12"); + final DateTimeParseException exc = assertThrows(DateTimeParseException.class, () -> new ConfigurableDateTimeParser(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, ()->separator('-').read("", pos)); + final DateTimeParseException exc = assertThrows(DateTimeParseException.class, () -> separators('-').read("", pos)); assertThat(exc).hasMessage("Unexpected end of input: "); } @@ -116,15 +133,15 @@ void readTimeZoneZuluLower() void readTimeZoneUnexpectedChar() { final ParsePosition pos = new ParsePosition(0); - final DateTimeParseException exc = assertThrows(DateTimeParseException.class, ()->new TimeZoneOffsetToken().read("X", pos)); - assertThat(exc).hasMessage("Unexpected character X at position 1: X"); + final DateTimeParseException exc = assertThrows(DateTimeParseException.class, () -> new TimeZoneOffsetToken().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 TimeZoneOffsetToken().read("-06:0", pos)); + final DateTimeParseException exc = assertThrows(DateTimeParseException.class, () -> new TimeZoneOffsetToken().read("-06:0", pos)); assertThat(exc).hasMessage("Invalid timezone offset: -06:0"); } diff --git a/src/test/java/com/ethlo/time/token/DateTimeParsers.java b/src/test/java/com/ethlo/time/token/DateTimeParsers.java index daa855b..d3e4a4f 100644 --- a/src/test/java/com/ethlo/time/token/DateTimeParsers.java +++ b/src/test/java/com/ethlo/time/token/DateTimeParsers.java @@ -20,42 +20,47 @@ * #L% */ +import com.ethlo.time.internal.token.TimeZoneOffsetToken; + 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.*; +import static com.ethlo.time.token.DateTimeTokens.digits; +import static com.ethlo.time.token.DateTimeTokens.separators; public class DateTimeParsers { private static final ConfigurableDateTimeParser DATE = new ConfigurableDateTimeParser( - new DigitsToken(YEAR, 4), - new SeparatorToken('-'), - new DigitsToken(MONTH, 2), - new SeparatorToken('-'), - new DigitsToken(DAY, 2) + digits(YEAR, 4), + separators('-'), + digits(MONTH, 2), + separators('-'), + digits(DAY, 2) ); private static final ConfigurableDateTimeParser MINUTES = DATE.combine( - new SeparatorToken('T'), - new DigitsToken(HOUR, 2), - new SeparatorToken(':'), - new DigitsToken(MINUTE, 2) + separators('T'), + digits(HOUR, 2), + separators(':'), + digits(MINUTE, 2) ); private static final ConfigurableDateTimeParser LOCAL_TIME = MINUTES.combine( - new SeparatorToken(':'), - new DigitsToken(SECOND, 2) + separators(':'), + digits(SECOND, 2) ); private static final ConfigurableDateTimeParser FRACTIONAL_SECONDS_LOCAL = LOCAL_TIME.combine( - new SeparatorToken('.'), - new FractionsToken() + separators('.'), + fractions() ); private static final ConfigurableDateTimeParser FRACTIONAL_SECONDS_OFFSET = FRACTIONAL_SECONDS_LOCAL.combine( - new TimeZoneOffsetToken() + DateTimeTokens.timeZoneOffset() ); public static DateTimeParser rfc3339() 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" }, From 35b6b472abd22b0470908531228fdd19f1a05744 Mon Sep 17 00:00:00 2001 From: Morten Haraldsen Date: Sat, 3 Feb 2024 20:49:59 +0100 Subject: [PATCH 4/7] Headers --- .../ethlo/time/internal/util/ArrayUtils.java | 20 +++++++++++++++++++ .../com/ethlo/time/token/DateTimeTokens.java | 20 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/main/java/com/ethlo/time/internal/util/ArrayUtils.java b/src/main/java/com/ethlo/time/internal/util/ArrayUtils.java index a8a0e34..e478395 100644 --- a/src/main/java/com/ethlo/time/internal/util/ArrayUtils.java +++ b/src/main/java/com/ethlo/time/internal/util/ArrayUtils.java @@ -1,5 +1,25 @@ 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) diff --git a/src/main/java/com/ethlo/time/token/DateTimeTokens.java b/src/main/java/com/ethlo/time/token/DateTimeTokens.java index e862adf..ec0e501 100644 --- a/src/main/java/com/ethlo/time/token/DateTimeTokens.java +++ b/src/main/java/com/ethlo/time/token/DateTimeTokens.java @@ -1,5 +1,25 @@ 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; From b6df38b742db27b9b1261b11b75eef39ae88a852 Mon Sep 17 00:00:00 2001 From: Morten Haraldsen Date: Sun, 4 Feb 2024 09:46:52 +0100 Subject: [PATCH 5/7] Cleanup and add additional tests --- .../ethlo/time/internal/fixed/ITUParser.java | 22 +-------- .../time/internal/token/DigitsToken.java | 6 +++ .../time/internal/token/SeparatorToken.java | 5 +-- .../time/internal/token/SeparatorsToken.java | 6 +++ .../token/ConfigurableDateTimeParser.java | 25 ++++++----- .../com/ethlo/time/token/DateTimeTokens.java | 6 +-- .../token/ConfigurableDateTimeParserTest.java | 40 +++++++++++++++-- .../com/ethlo/time/token/DateTimeParsers.java | 45 ++++++------------- 8 files changed, 81 insertions(+), 74 deletions(-) diff --git a/src/main/java/com/ethlo/time/internal/fixed/ITUParser.java b/src/main/java/com/ethlo/time/internal/fixed/ITUParser.java index ed374e6..e608441 100644 --- a/src/main/java/com/ethlo/time/internal/fixed/ITUParser.java +++ b/src/main/java/com/ethlo/time/internal/fixed/ITUParser.java @@ -28,7 +28,6 @@ import static com.ethlo.time.internal.util.LimitedCharArrayIntegerUtil.ZERO; import static com.ethlo.time.internal.util.LimitedCharArrayIntegerUtil.parsePositiveInt; -import java.text.ParsePosition; import java.time.OffsetDateTime; import java.time.format.DateTimeParseException; import java.util.Arrays; @@ -38,9 +37,8 @@ import com.ethlo.time.ParseConfig; import com.ethlo.time.TimezoneOffset; import com.ethlo.time.internal.util.ArrayUtils; -import com.ethlo.time.token.DateTimeParser; -public class ITUParser implements DateTimeParser +public class ITUParser { public static final char DATE_SEPARATOR = '-'; public static final char TIME_SEPARATOR = ':'; @@ -327,22 +325,4 @@ public static OffsetDateTime parseDateTime(final String chars, int offset) final Field nextGranularity = Field.values()[field.ordinal() + 1]; throw new DateTimeParseException(String.format("Unexpected end of input, missing field %s: %s", nextGranularity, chars), chars, field.getRequiredLength()); } - - @Override - public DateTime parse(final String text, final ParsePosition position) - { - try - { - int offset = position.getIndex(); - final DateTime result = ITUParser.parseLenient(text, ParseConfig.DEFAULT, position.getIndex()); - position.setIndex(offset + result.getParseLength()); - return result; - } - catch (DateTimeParseException exc) - { - position.setErrorIndex(exc.getErrorIndex()); - position.setIndex(position.getErrorIndex()); - throw exc; - } - } } \ No newline at end of file diff --git a/src/main/java/com/ethlo/time/internal/token/DigitsToken.java b/src/main/java/com/ethlo/time/internal/token/DigitsToken.java index 17fed5d..1674c7f 100644 --- a/src/main/java/com/ethlo/time/internal/token/DigitsToken.java +++ b/src/main/java/com/ethlo/time/internal/token/DigitsToken.java @@ -51,4 +51,10 @@ public Field getField() { return field; } + + @Override + public String toString() + { + return "digits: " + field + "(" + length + ")"; + } } diff --git a/src/main/java/com/ethlo/time/internal/token/SeparatorToken.java b/src/main/java/com/ethlo/time/internal/token/SeparatorToken.java index 93253b3..f12ce1e 100644 --- a/src/main/java/com/ethlo/time/internal/token/SeparatorToken.java +++ b/src/main/java/com/ethlo/time/internal/token/SeparatorToken.java @@ -22,7 +22,6 @@ import java.text.ParsePosition; -import com.ethlo.time.Field; import com.ethlo.time.internal.util.ErrorUtil; import com.ethlo.time.token.DateTimeToken; @@ -56,8 +55,8 @@ else if (text.charAt(index) != separator) } @Override - public Field getField() + public String toString() { - return null; + 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 index fc481ff..1e6f656 100644 --- a/src/main/java/com/ethlo/time/internal/token/SeparatorsToken.java +++ b/src/main/java/com/ethlo/time/internal/token/SeparatorsToken.java @@ -56,4 +56,10 @@ public int read(final String text, final ParsePosition parsePosition) } 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/token/ConfigurableDateTimeParser.java b/src/main/java/com/ethlo/time/token/ConfigurableDateTimeParser.java index 67cc64e..6c4ed76 100644 --- a/src/main/java/com/ethlo/time/token/ConfigurableDateTimeParser.java +++ b/src/main/java/com/ethlo/time/token/ConfigurableDateTimeParser.java @@ -24,6 +24,9 @@ import static com.ethlo.time.Field.YEAR; import java.text.ParsePosition; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; import com.ethlo.time.DateTime; import com.ethlo.time.Field; @@ -34,22 +37,22 @@ public class ConfigurableDateTimeParser implements DateTimeParser { private final DateTimeToken[] tokens; - public ConfigurableDateTimeParser(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 ConfigurableDateTimeParser combine(DateTimeToken... tokens) - { - return new ConfigurableDateTimeParser(combine(this.tokens, tokens)); - } - - private DateTimeToken[] combine(DateTimeToken[] a, DateTimeToken[] b) + public static DateTimeParser of(DateTimeToken... tokens) { - final DateTimeToken[] result = new DateTimeToken[a.length + b.length]; - System.arraycopy(a, 0, result, 0, a.length); - System.arraycopy(b, 0, result, a.length, b.length); - return result; + return new ConfigurableDateTimeParser(tokens); } @Override diff --git a/src/main/java/com/ethlo/time/token/DateTimeTokens.java b/src/main/java/com/ethlo/time/token/DateTimeTokens.java index ec0e501..9d3a52e 100644 --- a/src/main/java/com/ethlo/time/token/DateTimeTokens.java +++ b/src/main/java/com/ethlo/time/token/DateTimeTokens.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. @@ -53,7 +53,7 @@ public static DateTimeToken fractions() return new FractionsToken(); } - public static DateTimeToken timeZoneOffset() + public static DateTimeToken zoneOffset() { return new TimeZoneOffsetToken(); } diff --git a/src/test/java/com/ethlo/time/token/ConfigurableDateTimeParserTest.java b/src/test/java/com/ethlo/time/token/ConfigurableDateTimeParserTest.java index 45ef0dd..118e40c 100644 --- a/src/test/java/com/ethlo/time/token/ConfigurableDateTimeParserTest.java +++ b/src/test/java/com/ethlo/time/token/ConfigurableDateTimeParserTest.java @@ -29,6 +29,7 @@ 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; @@ -49,7 +50,7 @@ void parseCustomFormat() { final ParsePosition pos = new ParsePosition(0); final String input = "31-12-2000 235937,123456"; - final DateTimeParser parser = new ConfigurableDateTimeParser( + final DateTimeParser parser = DateTimeParsers.of( digits(DAY, 2), separators('-'), digits(MONTH, 2), @@ -66,13 +67,44 @@ void parseCustomFormat() 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 = DateTimeParsers.rfc3339().parse(input, pos); + final DateTime custom = parser.parse(input, pos); + assertThat(custom).isEqualTo(fixed); assertThat(fixed.toString()).isEqualTo(input); assertThat(custom.toString()).isEqualTo(input); @@ -81,7 +113,7 @@ void parseRfc3339Format() @Test void testInvalidSeparators() { - final DateTimeParseException exc = assertThrows(DateTimeParseException.class, () -> new ConfigurableDateTimeParser(separators('X')).parse("12")); + final DateTimeParseException exc = assertThrows(DateTimeParseException.class, () -> DateTimeParsers.of(separators('X')).parse("12")); assertThat(exc).hasMessage("Expected character [X] at position 1, found 1: 12"); } @@ -96,7 +128,7 @@ void testSeparators() @Test void testInvalidSeparator() { - final DateTimeParseException exc = assertThrows(DateTimeParseException.class, () -> new ConfigurableDateTimeParser(separators('X')).parse("12")); + final DateTimeParseException exc = assertThrows(DateTimeParseException.class, () -> DateTimeParsers.of(separators('X')).parse("12")); assertThat(exc).hasMessage("Expected character [X] at position 1, found 1: 12"); } diff --git a/src/test/java/com/ethlo/time/token/DateTimeParsers.java b/src/test/java/com/ethlo/time/token/DateTimeParsers.java index d3e4a4f..fde8acb 100644 --- a/src/test/java/com/ethlo/time/token/DateTimeParsers.java +++ b/src/test/java/com/ethlo/time/token/DateTimeParsers.java @@ -20,21 +20,19 @@ * #L% */ -import com.ethlo.time.internal.token.TimeZoneOffsetToken; - 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.*; 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 = new ConfigurableDateTimeParser( + private static final ConfigurableDateTimeParser DATE = (ConfigurableDateTimeParser) DateTimeParsers.of( digits(YEAR, 4), separators('-'), digits(MONTH, 2), @@ -42,44 +40,27 @@ public class DateTimeParsers digits(DAY, 2) ); - private static final ConfigurableDateTimeParser MINUTES = DATE.combine( - separators('T'), + public static DateTimeParser of(DateTimeToken... tokens) + { + return ConfigurableDateTimeParser.of(tokens); + } + + private static final DateTimeParser LOCAL_TIME = of( digits(HOUR, 2), separators(':'), - digits(MINUTE, 2) - ); - - private static final ConfigurableDateTimeParser LOCAL_TIME = MINUTES.combine( + digits(MINUTE, 2), separators(':'), - digits(SECOND, 2) - ); - - private static final ConfigurableDateTimeParser FRACTIONAL_SECONDS_LOCAL = LOCAL_TIME.combine( - separators('.'), + digits(SECOND, 2), fractions() ); - private static final ConfigurableDateTimeParser FRACTIONAL_SECONDS_OFFSET = FRACTIONAL_SECONDS_LOCAL.combine( - DateTimeTokens.timeZoneOffset() - ); - - public static DateTimeParser rfc3339() - { - return FRACTIONAL_SECONDS_OFFSET; - } - - public static DateTimeParser minutes() + public static DateTimeParser localDate() { - return MINUTES; + return DATE; } - public static DateTimeParser seconds() + public static DateTimeParser localTime() { return LOCAL_TIME; } - - public static DateTimeParser fractionalSeconds() - { - return FRACTIONAL_SECONDS_LOCAL; - } } From 818f6dcbde467e868216d4a42890c10cf1bf8ce2 Mon Sep 17 00:00:00 2001 From: Morten Haraldsen Date: Sun, 4 Feb 2024 10:01:39 +0100 Subject: [PATCH 6/7] Add additional tests --- .../internal/token/TimeZoneOffsetToken.java | 2 + .../token/ConfigurableDateTimeParser.java | 16 ++++++++ .../token/ConfigurableDateTimeParserTest.java | 40 ++++++++++++++++++- 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/ethlo/time/internal/token/TimeZoneOffsetToken.java b/src/main/java/com/ethlo/time/internal/token/TimeZoneOffsetToken.java index 2e9bcf2..76a0cc3 100644 --- a/src/main/java/com/ethlo/time/internal/token/TimeZoneOffsetToken.java +++ b/src/main/java/com/ethlo/time/internal/token/TimeZoneOffsetToken.java @@ -50,6 +50,7 @@ public int read(final String text, final ParsePosition parsePosition) final char c = text.charAt(idx); if (c == 'Z' || c == 'z') { + parsePosition.setIndex(idx + 1); return 0; } @@ -77,6 +78,7 @@ public int read(final String text, final ParsePosition parsePosition) } } + parsePosition.setIndex(idx + 6); return hours * 3600 + minutes * 60; } diff --git a/src/main/java/com/ethlo/time/token/ConfigurableDateTimeParser.java b/src/main/java/com/ethlo/time/token/ConfigurableDateTimeParser.java index 6c4ed76..58246d9 100644 --- a/src/main/java/com/ethlo/time/token/ConfigurableDateTimeParser.java +++ b/src/main/java/com/ethlo/time/token/ConfigurableDateTimeParser.java @@ -24,6 +24,7 @@ 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; @@ -57,10 +58,25 @@ public static DateTimeParser of(DateTimeToken... 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(); diff --git a/src/test/java/com/ethlo/time/token/ConfigurableDateTimeParserTest.java b/src/test/java/com/ethlo/time/token/ConfigurableDateTimeParserTest.java index 118e40c..61a0365 100644 --- a/src/test/java/com/ethlo/time/token/ConfigurableDateTimeParserTest.java +++ b/src/test/java/com/ethlo/time/token/ConfigurableDateTimeParserTest.java @@ -45,11 +45,26 @@ 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 ParsePosition pos = new ParsePosition(0); - final String input = "31-12-2000 235937,123456"; final DateTimeParser parser = DateTimeParsers.of( digits(DAY, 2), separators('-'), @@ -63,6 +78,8 @@ void parseCustomFormat() 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)); } @@ -184,4 +201,23 @@ void readTimeZoneNegative() final int secs = new TimeZoneOffsetToken().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); + } } From ba80572ff7953b842e036be1c0ec8c30c23ea34e Mon Sep 17 00:00:00 2001 From: Morten Haraldsen Date: Sun, 4 Feb 2024 13:43:49 +0100 Subject: [PATCH 7/7] Cleanup --- src/main/java/com/ethlo/time/DateTime.java | 2 +- src/main/java/com/ethlo/time/ITU.java | 8 ++++---- src/main/java/com/ethlo/time/ParseConfig.java | 6 ++---- .../com/ethlo/time/internal/fixed/ITUFormatter.java | 4 ++-- ...TimeZoneOffsetToken.java => ZoneOffsetToken.java} | 2 +- .../com/ethlo/time/internal/util/ArrayUtils.java | 4 ++-- .../java/com/ethlo/time/token/DateTimeToken.java | 4 ++-- .../java/com/ethlo/time/token/DateTimeTokens.java | 4 ++-- src/test/java/com/ethlo/time/ParsePositionTest.java | 4 ++-- .../time/token/ConfigurableDateTimeParserTest.java | 12 ++++++------ .../java/com/ethlo/time/token/DateTimeParsers.java | 11 +++++------ 11 files changed, 29 insertions(+), 32 deletions(-) rename src/main/java/com/ethlo/time/internal/token/{TimeZoneOffsetToken.java => ZoneOffsetToken.java} (97%) diff --git a/src/main/java/com/ethlo/time/DateTime.java b/src/main/java/com/ethlo/time/DateTime.java index 72dc7d0..c28431f 100644 --- a/src/main/java/com/ethlo/time/DateTime.java +++ b/src/main/java/com/ethlo/time/DateTime.java @@ -20,10 +20,10 @@ * #L% */ +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.fixed.ITUFormatter.finish; import static com.ethlo.time.internal.util.LeapSecondHandler.LEAP_SECOND_SECONDS; import java.time.DateTimeException; diff --git a/src/main/java/com/ethlo/time/ITU.java b/src/main/java/com/ethlo/time/ITU.java index a2545b4..4d51227 100644 --- a/src/main/java/com/ethlo/time/ITU.java +++ b/src/main/java/com/ethlo/time/ITU.java @@ -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 1d4ef8d..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.fixed.ITUParser.SEPARATOR_LOWER; -import static com.ethlo.time.internal.fixed.ITUParser.SEPARATOR_SPACE; -import static com.ethlo.time.internal.fixed.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/internal/fixed/ITUFormatter.java b/src/main/java/com/ethlo/time/internal/fixed/ITUFormatter.java index 2e10ee1..1efe741 100644 --- a/src/main/java/com/ethlo/time/internal/fixed/ITUFormatter.java +++ b/src/main/java/com/ethlo/time/internal/fixed/ITUFormatter.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/main/java/com/ethlo/time/internal/token/TimeZoneOffsetToken.java b/src/main/java/com/ethlo/time/internal/token/ZoneOffsetToken.java similarity index 97% rename from src/main/java/com/ethlo/time/internal/token/TimeZoneOffsetToken.java rename to src/main/java/com/ethlo/time/internal/token/ZoneOffsetToken.java index 76a0cc3..a253872 100644 --- a/src/main/java/com/ethlo/time/internal/token/TimeZoneOffsetToken.java +++ b/src/main/java/com/ethlo/time/internal/token/ZoneOffsetToken.java @@ -33,7 +33,7 @@ import com.ethlo.time.Field; import com.ethlo.time.token.DateTimeToken; -public class TimeZoneOffsetToken implements DateTimeToken +public class ZoneOffsetToken implements DateTimeToken { @Override public int read(final String text, final ParsePosition parsePosition) diff --git a/src/main/java/com/ethlo/time/internal/util/ArrayUtils.java b/src/main/java/com/ethlo/time/internal/util/ArrayUtils.java index e478395..e2ae22d 100644 --- a/src/main/java/com/ethlo/time/internal/util/ArrayUtils.java +++ b/src/main/java/com/ethlo/time/internal/util/ArrayUtils.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/main/java/com/ethlo/time/token/DateTimeToken.java b/src/main/java/com/ethlo/time/token/DateTimeToken.java index 94c97b8..dc7b561 100644 --- a/src/main/java/com/ethlo/time/token/DateTimeToken.java +++ b/src/main/java/com/ethlo/time/token/DateTimeToken.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/main/java/com/ethlo/time/token/DateTimeTokens.java b/src/main/java/com/ethlo/time/token/DateTimeTokens.java index 9d3a52e..61ba826 100644 --- a/src/main/java/com/ethlo/time/token/DateTimeTokens.java +++ b/src/main/java/com/ethlo/time/token/DateTimeTokens.java @@ -25,7 +25,7 @@ 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.TimeZoneOffsetToken; +import com.ethlo.time.internal.token.ZoneOffsetToken; public class DateTimeTokens { @@ -55,6 +55,6 @@ public static DateTimeToken fractions() public static DateTimeToken zoneOffset() { - return new TimeZoneOffsetToken(); + return new ZoneOffsetToken(); } } 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 index 61a0365..3cf3e4c 100644 --- a/src/test/java/com/ethlo/time/token/ConfigurableDateTimeParserTest.java +++ b/src/test/java/com/ethlo/time/token/ConfigurableDateTimeParserTest.java @@ -41,7 +41,7 @@ import com.ethlo.time.DateTime; import com.ethlo.time.ITU; import com.ethlo.time.internal.token.FractionsToken; -import com.ethlo.time.internal.token.TimeZoneOffsetToken; +import com.ethlo.time.internal.token.ZoneOffsetToken; public class ConfigurableDateTimeParserTest { @@ -168,21 +168,21 @@ void reachEndOfFractions() void readTimeZoneZuluUpper() { final ParsePosition pos = new ParsePosition(0); - assertThat(new TimeZoneOffsetToken().read("Z", pos)).isEqualTo(0); + assertThat(new ZoneOffsetToken().read("Z", pos)).isEqualTo(0); } @Test void readTimeZoneZuluLower() { final ParsePosition pos = new ParsePosition(0); - assertThat(new TimeZoneOffsetToken().read("z", pos)).isEqualTo(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 TimeZoneOffsetToken().read("X", pos)); + final DateTimeParseException exc = assertThrows(DateTimeParseException.class, () -> new ZoneOffsetToken().read("X", pos)); assertThat(exc).hasMessage("Expected character [Z, z, +, -] at position 1, found X: X"); } @@ -190,7 +190,7 @@ void readTimeZoneUnexpectedChar() void readTimeZoneTooShort() { final ParsePosition pos = new ParsePosition(0); - final DateTimeParseException exc = assertThrows(DateTimeParseException.class, () -> new TimeZoneOffsetToken().read("-06:0", pos)); + final DateTimeParseException exc = assertThrows(DateTimeParseException.class, () -> new ZoneOffsetToken().read("-06:0", pos)); assertThat(exc).hasMessage("Invalid timezone offset: -06:0"); } @@ -198,7 +198,7 @@ void readTimeZoneTooShort() void readTimeZoneNegative() { final ParsePosition pos = new ParsePosition(0); - final int secs = new TimeZoneOffsetToken().read("-06:30", pos); + final int secs = new ZoneOffsetToken().read("-06:30", pos); assertThat(secs).isEqualTo(-23400); } diff --git a/src/test/java/com/ethlo/time/token/DateTimeParsers.java b/src/test/java/com/ethlo/time/token/DateTimeParsers.java index fde8acb..976ab63 100644 --- a/src/test/java/com/ethlo/time/token/DateTimeParsers.java +++ b/src/test/java/com/ethlo/time/token/DateTimeParsers.java @@ -39,12 +39,6 @@ public class DateTimeParsers separators('-'), digits(DAY, 2) ); - - public static DateTimeParser of(DateTimeToken... tokens) - { - return ConfigurableDateTimeParser.of(tokens); - } - private static final DateTimeParser LOCAL_TIME = of( digits(HOUR, 2), separators(':'), @@ -54,6 +48,11 @@ public static DateTimeParser of(DateTimeToken... tokens) fractions() ); + public static DateTimeParser of(DateTimeToken... tokens) + { + return ConfigurableDateTimeParser.of(tokens); + } + public static DateTimeParser localDate() { return DATE;