From 72ce98065399b925a393bbb788b37af6927f24c1 Mon Sep 17 00:00:00 2001 From: Morten Haraldsen Date: Mon, 22 Jan 2024 09:05:16 +0100 Subject: [PATCH 1/6] easy to use best effort toInstant --- src/main/java/com/ethlo/time/DateTime.java | 30 ++++++++ .../com/ethlo/time/internal/DateTimeMath.java | 27 +++++++ .../com/ethlo/time/internal/EthloITU.java | 2 +- .../com/ethlo/time/LenientParseTests.java | 71 +++++++++++++++++++ 4 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/ethlo/time/internal/DateTimeMath.java create mode 100644 src/test/java/com/ethlo/time/LenientParseTests.java diff --git a/src/main/java/com/ethlo/time/DateTime.java b/src/main/java/com/ethlo/time/DateTime.java index 64fbac0..2179fde 100644 --- a/src/main/java/com/ethlo/time/DateTime.java +++ b/src/main/java/com/ethlo/time/DateTime.java @@ -26,6 +26,7 @@ import static com.ethlo.time.internal.EthloITU.finish; import java.time.DateTimeException; +import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.OffsetDateTime; @@ -38,6 +39,7 @@ import java.util.Objects; import java.util.Optional; +import com.ethlo.time.internal.DateTimeMath; import com.ethlo.time.internal.LimitedCharArrayIntegerUtil; /** @@ -506,6 +508,34 @@ else if (temporalField.equals(ChronoField.NANO_OF_SECOND)) { return nano; } + else if (temporalField.equals(ChronoField.INSTANT_SECONDS)) + { + if (offset != null) + { + return toEpochSeconds(); + } + } + throw new UnsupportedTemporalTypeException("Unsupported field: " + temporalField); } + + /** + *

This method will attempt to create an Instant from whatever granularity is available in the parsed year/date/date-time.

+ *

Missing fields will be replaced by their lowest allowed value, like 1 for month and day, 0 for any time component.

+ *

NOTE: If there is no time-zone defined, UTC will be assumed

+ * + * @return An instant representing the point in time. + */ + public Instant toInstant() + { + return Instant.ofEpochSecond(toEpochSeconds(), nano); + } + + private long toEpochSeconds() + { + final long secsSinceMidnight = hour * 3600L + minute * 60L + second; + final long daysInSeconds = DateTimeMath.daysFromCivil(year, month != 0 ? month : 1, day != 0 ? day : 1) * 86_400; + final long tsOffset = offset != null ? offset.getTotalSeconds() : 0; + return daysInSeconds + secsSinceMidnight - tsOffset; + } } diff --git a/src/main/java/com/ethlo/time/internal/DateTimeMath.java b/src/main/java/com/ethlo/time/internal/DateTimeMath.java new file mode 100644 index 0000000..86c590d --- /dev/null +++ b/src/main/java/com/ethlo/time/internal/DateTimeMath.java @@ -0,0 +1,27 @@ +package com.ethlo.time.internal; + +/** + * CREDIT: Public domain math for converting between epoch and date-time + */ +public class DateTimeMath +{ + public static long daysFromCivil(int y, final int m, final int d) + { + // Returns number of days since civil 1970-01-01. Negative values indicate + // days prior to 1970-01-01. + // Preconditions: y-m-d represents a date in the civil (Gregorian) calendar + // m is in [1, 12] + // d is in [1, last_day_of_month(y, m)] + // y is "approximately" in + // [numeric_limits::min()/366, numeric_limits::max()/366] + // Exact range of validity is: + // [civil_from_days(numeric_limits::min()), + // civil_from_days(numeric_limits::max()-719468)] + y -= m <= 2 ? 1 : 0; + final long era = (y >= 0 ? y : y - 399) / 400; + final long yoe = y - era * 400; // [0, 399] + final long doy = (153L * (m > 2 ? m - 3 : m + 9) + 2) / 5 + d - 1; // [0, 365] + final long doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096] + return era * 146097 + doe - 719468; + } +} diff --git a/src/main/java/com/ethlo/time/internal/EthloITU.java b/src/main/java/com/ethlo/time/internal/EthloITU.java index 5d286c5..608d18b 100644 --- a/src/main/java/com/ethlo/time/internal/EthloITU.java +++ b/src/main/java/com/ethlo/time/internal/EthloITU.java @@ -476,7 +476,7 @@ else if (remaining == 1 && (c == ZULU_UPPER || c == ZULU_LOWER)) } else if (c == PLUS || c == MINUS) { - // No fractional sections + // No fractional seconds offset = parseTimezone(chars, 19); } else diff --git a/src/test/java/com/ethlo/time/LenientParseTests.java b/src/test/java/com/ethlo/time/LenientParseTests.java new file mode 100644 index 0000000..aa303de --- /dev/null +++ b/src/test/java/com/ethlo/time/LenientParseTests.java @@ -0,0 +1,71 @@ +package com.ethlo.time; + +/*- + * #%L + * Internet Time Utility + * %% + * Copyright (C) 2017 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 org.assertj.core.api.Assertions.assertThat; + +import java.time.Instant; +import java.time.OffsetDateTime; + +import org.junit.jupiter.api.Test; + +public class LenientParseTests +{ + @Test + public void testParseTimestampAndUseTemporalAccessor() + { + final String s = "2018-11-01T14:45:59.12356789+04:00"; + final DateTime a = ITU.parseLenient(s); + assertThat(Instant.from(a)).isEqualTo(OffsetDateTime.parse(s).toInstant()); + } + + @Test + public void testParseTimestamp() + { + final String s = "2018-11-01T14:45:59.12356789+04:00"; + final DateTime a = ITU.parseLenient(s); + assertThat(a.toInstant()).isEqualTo(OffsetDateTime.parse(s).toInstant()); + } + + @Test + public void testParseYearMonth() + { + final String s = "2018-11"; + final DateTime a = ITU.parseLenient(s); + assertThat(a.toInstant()).isEqualTo(OffsetDateTime.parse(s + "-01T00:00:00Z").toInstant()); + } + + @Test + public void testParseYearDayMonth() + { + final String s = "1997-11-30"; + final DateTime a = ITU.parseLenient(s); + assertThat(a.toInstant()).isEqualTo(OffsetDateTime.parse(s + "T00:00:00Z").toInstant()); + } + + @Test + public void testParseYearMonthHourMinutes() + { + final String s = "2018-11-27T12:30"; + final DateTime a = ITU.parseLenient(s); + assertThat(a.toInstant()).isEqualTo(OffsetDateTime.parse(s + ":00Z").toInstant()); + } +} From a185a32dd8c4290c5c5f134f5d03073005fa6922 Mon Sep 17 00:00:00 2001 From: Morten Haraldsen Date: Mon, 22 Jan 2024 09:05:56 +0100 Subject: [PATCH 2/6] Formatting --- src/test/java/com/ethlo/time/LenientParseTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/ethlo/time/LenientParseTests.java b/src/test/java/com/ethlo/time/LenientParseTests.java index aa303de..736b116 100644 --- a/src/test/java/com/ethlo/time/LenientParseTests.java +++ b/src/test/java/com/ethlo/time/LenientParseTests.java @@ -36,7 +36,7 @@ public void testParseTimestampAndUseTemporalAccessor() final DateTime a = ITU.parseLenient(s); assertThat(Instant.from(a)).isEqualTo(OffsetDateTime.parse(s).toInstant()); } - + @Test public void testParseTimestamp() { From e9a4a3c19afd09f39faddc7819b2dfc0df14170c Mon Sep 17 00:00:00 2001 From: Morten Haraldsen Date: Mon, 22 Jan 2024 09:17:11 +0100 Subject: [PATCH 3/6] Cleanup --- src/main/java/com/ethlo/time/DateTime.java | 5 ----- src/test/java/com/ethlo/time/LenientParseTests.java | 3 ++- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/ethlo/time/DateTime.java b/src/main/java/com/ethlo/time/DateTime.java index 2179fde..fb1aeda 100644 --- a/src/main/java/com/ethlo/time/DateTime.java +++ b/src/main/java/com/ethlo/time/DateTime.java @@ -276,11 +276,6 @@ public LocalDateTime toLocalDatetime() public OffsetDateTime toOffsetDatetime() { assertMinGranularity(Field.MINUTE); - return toOffsetDatetimeNoGranularityCheck(); - } - - public OffsetDateTime toOffsetDatetimeNoGranularityCheck() - { if (offset != null) { return OffsetDateTime.of(year, month, day, hour, minute, second, nano, offset.toZoneOffset()); diff --git a/src/test/java/com/ethlo/time/LenientParseTests.java b/src/test/java/com/ethlo/time/LenientParseTests.java index 736b116..2f8b6e5 100644 --- a/src/test/java/com/ethlo/time/LenientParseTests.java +++ b/src/test/java/com/ethlo/time/LenientParseTests.java @@ -32,8 +32,9 @@ public class LenientParseTests @Test public void testParseTimestampAndUseTemporalAccessor() { - final String s = "2018-11-01T14:45:59.12356789+04:00"; + final String s = "2018-11-01T14:45:59.123456789+04:00"; final DateTime a = ITU.parseLenient(s); + assertThat(a.getFractionDigits()).isEqualTo(9); assertThat(Instant.from(a)).isEqualTo(OffsetDateTime.parse(s).toInstant()); } From 9b105483fd7f409f81c059f8e07e367b2ef5a9f2 Mon Sep 17 00:00:00 2001 From: Morten Haraldsen Date: Mon, 22 Jan 2024 09:36:51 +0100 Subject: [PATCH 4/6] Cleanup --- src/main/java/com/ethlo/time/DateTime.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/ethlo/time/DateTime.java b/src/main/java/com/ethlo/time/DateTime.java index fb1aeda..25d3296 100644 --- a/src/main/java/com/ethlo/time/DateTime.java +++ b/src/main/java/com/ethlo/time/DateTime.java @@ -516,7 +516,7 @@ else if (temporalField.equals(ChronoField.INSTANT_SECONDS)) /** *

This method will attempt to create an Instant from whatever granularity is available in the parsed year/date/date-time.

- *

Missing fields will be replaced by their lowest allowed value, like 1 for month and day, 0 for any time component.

+ *

Missing fields will be replaced by their lowest allowed value: 1 for month and day, 0 for any missing time component.

*

NOTE: If there is no time-zone defined, UTC will be assumed

* * @return An instant representing the point in time. From 6637a98c74ca26e9ca8fafad9e956639a17eb490 Mon Sep 17 00:00:00 2001 From: Morten Haraldsen Date: Mon, 22 Jan 2024 10:20:33 +0100 Subject: [PATCH 5/6] Cleanup --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 298829d..53b1281 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![Maven Central](https://img.shields.io/maven-central/v/com.ethlo.time/itu.svg)](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.ethlo.time%22%20a%3A%22itu%22) [![javadoc](https://javadoc.io/badge2/com.ethlo.time/itu/javadoc.svg)](https://javadoc.io/doc/com.ethlo.time/itu/latest/com/ethlo/time/ITU.html) [![Hex.pm](https://img.shields.io/hexpm/l/plug.svg)](LICENSE) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/598913bc1fe9405c82be73d9a4f105c8)](https://app.codacy.com/gh/ethlo/itu/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) An extremely fast parser and formatter of ISO format date-times. From a1dd74d49c99bd80012d5a9831fa86ffebb6ed7f Mon Sep 17 00:00:00 2001 From: Morten Haraldsen Date: Mon, 22 Jan 2024 10:38:12 +0100 Subject: [PATCH 6/6] Update docs --- README.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 53b1281..2178800 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ class Test { ``` ### Handle different granularity (ISO format) -Validate to different required granularity: +#### Validate with specified granularity ```java import com.ethlo.time.ITU; import com.ethlo.time.TemporalType; @@ -120,7 +120,7 @@ class Test { } ``` -Allowing handling different levels of granularity: +#### Handling different levels of granularity explicitly ```java import com.ethlo.time.ITU; import com.ethlo.time.TemporalHandler; @@ -147,7 +147,20 @@ class Test { }); } } +``` +#### Parsing leniently to a timestamp +In some real world scenarios, the need to parse a best-effort timestamp is needed. To ease this, we can use `ITU.parseLenient()` with `DateTime.toInstant()` like this: +```java +import com.ethlo.time.ITU; +import com.ethlo.time.TemporalHandler; +import java.time.temporal.TemporalAccessor; + +class Test { + void parseTest() { + final Instant instant = ITU.parseLenient("2017-12-06").toInstant(); + } +} ``` ## Q & A