Skip to content

Commit

Permalink
Lenient toInstant (#21)
Browse files Browse the repository at this point in the history
* easy to use best effort toInstant

---------

Co-authored-by: Morten Haraldsen <[email protected]>
  • Loading branch information
ethlo and Morten Haraldsen authored Jan 22, 2024
1 parent ad450e9 commit ef545e1
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 8 deletions.
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand Down
35 changes: 30 additions & 5 deletions src/main/java/com/ethlo/time/DateTime.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -38,6 +39,7 @@
import java.util.Objects;
import java.util.Optional;

import com.ethlo.time.internal.DateTimeMath;
import com.ethlo.time.internal.LimitedCharArrayIntegerUtil;

/**
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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);
}

/**
* <p>This method will attempt to create an Instant from whatever granularity is available in the parsed year/date/date-time.</p>
* <p>Missing fields will be replaced by their lowest allowed value: 1 for month and day, 0 for any missing time component.</p>
* <p>NOTE: If there is no time-zone defined, UTC will be assumed</p>
*
* @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;
}
}
27 changes: 27 additions & 0 deletions src/main/java/com/ethlo/time/internal/DateTimeMath.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.ethlo.time.internal;

/**
* CREDIT: <a href="https://howardhinnant.github.io/date_algorithms.html">Public domain math for converting between epoch and date-time</a>
*/
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<Int>::min()/366, numeric_limits<Int>::max()/366]
// Exact range of validity is:
// [civil_from_days(numeric_limits<Int>::min()),
// civil_from_days(numeric_limits<Int>::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;
}
}
2 changes: 1 addition & 1 deletion src/main/java/com/ethlo/time/internal/EthloITU.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
72 changes: 72 additions & 0 deletions src/test/java/com/ethlo/time/LenientParseTests.java
Original file line number Diff line number Diff line change
@@ -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());
}
}

0 comments on commit ef545e1

Please sign in to comment.