diff --git a/README.md b/README.md index 298829d..2178800 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. @@ -107,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; @@ -119,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; @@ -146,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 diff --git a/src/main/java/com/ethlo/time/DateTime.java b/src/main/java/com/ethlo/time/DateTime.java index 64fbac0..25d3296 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; /** @@ -274,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()); @@ -506,6 +503,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: 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. + */ + 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..2f8b6e5 --- /dev/null +++ b/src/test/java/com/ethlo/time/LenientParseTests.java @@ -0,0 +1,72 @@ +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.123456789+04:00"; + final DateTime a = ITU.parseLenient(s); + assertThat(a.getFractionDigits()).isEqualTo(9); + 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()); + } +}