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

Configurable lenient + ParsePosition #25

Merged
merged 4 commits into from
Jan 28, 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
112 changes: 42 additions & 70 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ An extremely fast parser and formatter of specific ISO-8601 format date and date
This project's goal is to do one thing: Make it easy to
handle [RFC-3339 Timestamps](https://www.ietf.org/rfc/rfc3339.txt) and W3C [Date and Time Formats](https://www.w3.org/TR/NOTE-datetime) in Java.

⚠️ Important note: Version 1.7.4 to 1.7.7 have a known issue parsing very specific, errounous date-time strings. _Please upgrade to version [1.8.0](https://github.com/ethlo/itu/releases/tag/v1.8.0) or later!_

## Features
* Very easy to use.
* Aim for 100% specification compliance.
Expand Down Expand Up @@ -52,99 +50,73 @@ import java.time.OffsetDateTime;
import com.ethlo.time.DateTime;
import com.ethlo.time.ITU;

class Test {
void smokeTest() {
// Parse a string
final OffsetDateTime dateTime = ITU.parseDateTime("2012-12-27T19:07:22.123456789-03:00");
// Parse a string
final OffsetDateTime dateTime = ITU.parseDateTime("2012-12-27T19:07:22.123456789-03:00");

// Format with seconds (no fraction digits)
final String formatted = ITU.formatUtc(dateTime); // 2012-12-27T22:07:22Z
// Format with seconds (no fraction digits)
final String formatted = ITU.formatUtc(dateTime); // 2012-12-27T22:07:22Z

// Format with microsecond precision
final String formattedMicro = ITU.formatUtcMicro(dateTime); // 2012-12-27T22:07:22.123457Z
// Format with microsecond precision
final String formattedMicro = ITU.formatUtcMicro(dateTime); // 2012-12-27T22:07:22.123457Z

// Parse lenient, raw data
final DateTime dateTime = ITU.parseLenient("2012-12-27T19:07Z");
}
}
// Parse lenient
final DateTime dateTime = ITU.parseLenient("2012-12-27T19:07Z");
```

### Handle leap-seconds
```java
import com.ethlo.time.ITU;
import com.ethlo.time.LeapSecondException;
import java.time.OffsetDateTime;

class Test {
void testParseDateTime() {
try {
final OffsetDateTime dateTime = ITU.parseDateTime("1990-12-31T15:59:60-08:00");
} catch (LeapSecondException exc) {
// The following helper methods are available let you decide how to progress
exc.getSecondsInMinute(); // 60
exc.getNearestDateTime(); // 1991-01-01T00:00:00Z
exc.isVerifiedValidLeapYearMonth(); // true
}
}
try {
final OffsetDateTime dateTime = ITU.parseDateTime("1990-12-31T15:59:60-08:00");
} catch (LeapSecondException exc) {
// The following helper methods are available let you decide how to progress
exc.getSecondsInMinute(); // 60
exc.getNearestDateTime(); // 1991-01-01T00:00:00Z
exc.isVerifiedValidLeapYearMonth(); // true
}
```

### Parse lenient, configurable separators
```java
final ParseConfig config = ParseConfig.DEFAULT
.withFractionSeparators('.', ',')
.withDateTimeSeparators('T', '|');
ITU.parseLenient("1999-11-22|11:22:17,191", config);
```

### Parse with ParsePosition

```java
final ParsePosition pos = new ParsePosition(0);
ITU.parseLenient("1999-11-22T11:22", pos);
```

### Handle different granularity (ISO format)
#### Validate with specified granularity
```java
import com.ethlo.time.ITU;
import com.ethlo.time.TemporalType;

class Test {
void test() {
ITU.isValid("2017-12-06", TemporalType.LOCAL_DATE_TIME);
}
}
ITU.isValid("2017-12-06", TemporalType.LOCAL_DATE_TIME);
```

#### Handling different levels of granularity explicitly
```java
import com.ethlo.time.ITU;
import com.ethlo.time.TemporalHandler;
ITU.parse("2017-12-06", new TemporalHandler<>() {
@Override
public OffsetDateTime handle(final LocalDate localDate) {
return localDate.atTime(OffsetTime.of(LocalTime.of(0, 0), ZoneOffset.UTC));
}

import java.time.LocalDate;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZoneOffset;
import java.time.temporal.TemporalAccessor;

class Test {
TemporalAccessor extract() {
return ITU.parse("2017-12-06", new TemporalHandler<>() {
@Override
public OffsetDateTime handle(final LocalDate localDate) {
return localDate.atTime(OffsetTime.of(LocalTime.of(0, 0), ZoneOffset.UTC));
}

@Override
public OffsetDateTime handle(final OffsetDateTime offsetDateTime) {
return offsetDateTime;
}
});
@Override
public OffsetDateTime handle(final OffsetDateTime offsetDateTime) {
return offsetDateTime;
}
}
});
```
#### Parsing leniently to a timestamp
In some real world scenarios, it is useful to parse a best-effort timestamp. To ease usage, converting a raw `com.ethlo.time.DateTime` instance into `java.time.Instant`. Note the limitations and the assumption of UTC time-zone, as mentioned in the javadoc.
In some real world scenarios, it is useful to parse a best-effort timestamp. To ease usage, we can easily convert a raw `com.ethlo.time.DateTime` instance into `java.time.Instant`. Note the limitations and the assumption of UTC time-zone, as mentioned in the javadoc.

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();
}
}
final Instant instant = ITU.parseLenient("2017-12-06").toInstant();
```

## Q & A
Expand Down
44 changes: 44 additions & 0 deletions src/main/java/com/ethlo/time/ITU.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@
* #L%
*/

import java.text.ParsePosition;
import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.Year;
import java.time.YearMonth;
import java.time.format.DateTimeParseException;

import com.ethlo.time.internal.EthloITU;

Expand All @@ -51,6 +53,22 @@ public static OffsetDateTime parseDateTime(String text)
return delegate.parseDateTime(text);
}

public static OffsetDateTime parseDateTime(String text, ParsePosition position)
{
try
{
final OffsetDateTime result = delegate.parseDateTime(text);
position.setIndex(text.length());
return result;
}
catch (DateTimeParseException exc)
{
position.setErrorIndex(exc.getErrorIndex());
position.setIndex(position.getErrorIndex());
throw exc;
}
}

/**
* Parse an ISO formatted date and optionally time to a {@link DateTime}. The result has
* rudimentary checks for correctness, but will not be aware of number of days per specific month or leap-years.
Expand All @@ -63,6 +81,32 @@ public static DateTime parseLenient(String text)
return delegate.parse(text);
}

public static DateTime parseLenient(String text, ParsePosition position)
{
return parseLenient(text, ParseConfig.DEFAULT, position);
}

public static DateTime parseLenient(String text, ParseConfig parseConfig)
{
return delegate.parse(text, parseConfig);
}

public static DateTime parseLenient(String text, ParseConfig parseConfig, ParsePosition position)
{
try
{
final DateTime result = delegate.parse(text, parseConfig);
position.setIndex(text.length());
return result;
}
catch (DateTimeParseException exc)
{
position.setIndex(exc.getErrorIndex());
position.setErrorIndex(exc.getErrorIndex());
throw exc;
}
}

/**
* Check if the dateTime is valid according to the RFC-3339 specification
*
Expand Down
88 changes: 88 additions & 0 deletions src/main/java/com/ethlo/time/ParseConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.ethlo.time;

/*-
* #%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 ParseConfig
{
public static final ParseConfig DEFAULT = new ParseConfig(new char[]{'T', 't', ' '}, new char[]{'.'});

private final char[] allowedDateTimeSeparators;
private final char[] allowedFractionSeparators;

private ParseConfig(char[] allowedDateTimeSeparators, char[] allowedFractionSeparators)
{
this.allowedDateTimeSeparators = allowedDateTimeSeparators;
this.allowedFractionSeparators = allowedFractionSeparators;
}

public ParseConfig withDateTimeSeparators(char... allowed)
{
assertChars(allowed);
return new ParseConfig(allowed, allowedFractionSeparators);
}

private void assertChars(char[] allowed)
{
if (allowed == null)
{
throw new IllegalArgumentException("Cannot have null array of characters");
}
if (allowed.length == 0)
{
throw new IllegalArgumentException("Must have at least one character in allowed list");
}
}

public ParseConfig withFractionSeparators(char... allowed)
{
assertChars(allowed);
return new ParseConfig(allowedDateTimeSeparators, allowed);
}

public char[] getDateTimeSeparators()
{
return allowedDateTimeSeparators;
}

public boolean isDateTimeSeparator(char needle)
{
for (char c : allowedDateTimeSeparators)
{
if (c == needle)
{
return true;
}
}
return false;
}

public boolean isFractionSeparator(char needle)
{
for (char c : allowedFractionSeparators)
{
if (c == needle)
{
return true;
}
}
return false;
}
}
Loading
Loading