Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lenient toInstant #21

Merged
merged 6 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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());
}
}