Reputation: 599
From the ISO-8601 standards, there are 4 ways of expressing intervals/duration:
Start and end, such as "2007-03-01T13:00:00Z/2008-05-11T15:30:00Z"
Start and duration, such as "2007-03-01T13:00:00Z/P1Y2M10DT2H30M"
Duration and end, such as "P1Y2M10DT2H30M/2008-05-11T15:30:00Z"
Duration only, such as "P1Y2M10DT2H30M", with additional context information
Using only Java 8 (no Joda, extensions, etc), is there any elegant way of handling cases 1-3?
I'm aware of Duration.Parse()
and Period.Parse()
, but I'm wondering if there's an elegant way of handling the 4 cases. For example:
String datePeriod = "2016-07-21/P6D";
String twoDates = "2016-07-21/2016-07-25";
Duration d = Duration.parse(datePeriod); // DateTimeParseException
Duration p = Duration.parse(twoDates); // same
My current thought process is pretty sloppy, and I'm 100% sure there's a better way. Something like handling the 4 cases individually with nested try/catch blocks for each case, which seems a bit like an anti-pattern if anything. (Split on /
, parse first chunk for date, check for errors, parse first chunk for period, parse second chunk for date, check for errors... you get the idea)
Any tips would be greatly appreciated!
--
Also, the answers at ISO 8601 Time Interval Parsing in Java don't really help me in any way, as the top answer only cares about the PT...
stuff.
Upvotes: 11
Views: 2357
Reputation: 30696
I'm glad to solving your problem since it is a good example for introducing the Composite Design Pattern in Functional Programming. you can composing functions into a bigger and powerful single function. for example:
Function<String, Optional<Range<LocalDateTime>>> parser = anyOf(
both(), //case 1
starting(), //case 2
ending(), //case 3
since(LocalDateTime.now()) //case 4
);
Range<LocalDateTime> range = parser.apply("<INPUT>").orElse(null);
//OR using in stream as below
List<Range<LocalDateTime>> result = Stream.of(
"<Case 1>", "<Case 2>", "<Case 3>", "<Case 4>"
).map(parser).filter(Optional::isPresent).map(Optional::get).collect(toList());
the code below almost applies the most of Design Patterns in OOP. e.g: Composite, Proxy, Adapter, Factory Method Design Patterns and .etc.
factory: the both
method meet the 1st case as below:
static Function<String, Optional<Range<LocalDateTime>>> both() {
return parsing((first, second) -> new Range<>(
datetime(first),
datetime(second)
));
}
factory: the starting
method meet the 2nd case as below:
static Function<String, Optional<Range<LocalDateTime>>> starting() {
return parsing((first, second) -> {
LocalDateTime start = datetime(first);
return new Range<>(start, start.plus(amount(second)));
});
}
factory: the ending
method meet the 3rd case as below:
static Function<String, Optional<Range<LocalDateTime>>> ending() {
return parsing((first, second) -> {
LocalDateTime end = datetime(second);
return new Range<>(end.minus(amount(first)), end);
});
}
factory: the since
method meet the last case as below:
static Function<String,Optional<Range<LocalDateTime>>> since(LocalDateTime start) {
return parsing((amount, __) -> new Range<>(start, start.plus(amount(amount))));
}
composite : the responsibility of the anyOf
method is find the satisfied result among the Function
s as quickly as possible:
@SuppressWarnings("ConstantConditions")
static <T, R> Function<T, Optional<R>>
anyOf(Function<T, Optional<R>>... functions) {
return it -> Stream.of(functions).map(current -> current.apply(it))
.filter(Optional::isPresent)
.findFirst().get();
}
adapter: the responsibility of the parsing
method is create a parser for a certain input:
static <R> Function<String, Optional<R>>
parsing(BiFunction<String, String, R> parser) {
return splitting("/", exceptionally(optional(parser), Optional::empty));
}
proxy: the responsibility of the exceptionally
method is handling Exception
s:
static <T, U, R> BiFunction<T, U, R>
exceptionally(BiFunction<T, U, R> source, Supplier<R> exceptional) {
return (first, second) -> {
try {
return source.apply(first, second);
} catch (Exception ex) {
return exceptional.get();
}
};
}
adapter: the responsibility of the splitting
method is translates a BiFunction
to a Function
:
static <R> Function<String, R>
splitting(String regex, BiFunction<String, String, R> source) {
return value -> {
String[] parts = value.split(regex);
return source.apply(parts[0], parts.length == 1 ? "" : parts[1]);
};
}
adapter: the responsibility of the optional
method is create an Optional
for the final result:
static <R> BiFunction<String, String, Optional<R>>
optional(BiFunction<String, String, R> source) {
return (first, last) -> Optional.of(source.apply(first, last));
}
the Range
class for saving a ranged thing:
final class Range<T> {
public final T start;
public final T end;
public Range(T start, T end) {
this.start = start;
this.end = end;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Range)) {
return false;
}
Range<?> that = (Range<?>) o;
return Objects.equals(start, that.start) && Objects.equals(end, that.end);
}
@Override
public int hashCode() {
return Objects.hash(start) * 31 + Objects.hash(end);
}
@Override
public String toString() {
return String.format("[%s, %s]", start, end);
}
}
the datetime
method creates a LocalDateTime
from a String
:
static LocalDateTime datetime(String datetime) {
return LocalDateTime.parse(
datetime,
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss['Z']")
);
}
the amount
method creates a TemporalAmount
that takes both a Duration
and a Period
from a String
:
static TemporalAmount amount(String text) {
return splitting("T", (first, second) -> new TemporalAmount() {
private Period period= first.isEmpty() ? Period.ZERO : Period.parse(first);
private Duration duration = second.isEmpty() ? Duration.ZERO
: Duration.parse(String.format("PT%s", second));
@Override
public long get(TemporalUnit unit) {
return (period.getUnits().contains(unit) ? period.get(unit) : 0) +
(duration.getUnits().contains(unit) ? duration.get(unit) : 0);
}
@Override
public List<TemporalUnit> getUnits() {
return Stream.of(period, duration).map(TemporalAmount::getUnits)
.flatMap(List::stream)
.collect(toList());
}
@Override
public Temporal addTo(Temporal temporal) {
return period.addTo(duration.addTo(temporal));
}
@Override
public Temporal subtractFrom(Temporal temporal) {
return period.subtractFrom(duration.subtractFrom(temporal));
}
}).apply(text);
}
Upvotes: 2
Reputation: 106450
There's no real antipattern to splitting these things up. Oracle has split the responsibilities of these individual parsers up, and if we want to use them together in this sort of orchestration, it's up to us to ensure that we peel the pieces together again in a sensible fashion.
That said, I have a solution which works with core Java 8, and makes use of Function
and a few custom classes. I'll omit the custom beans for brevity as they are fairly basic, as well as the fact that the main lift is done in the Function
s.
Note that in order to get 'Z' to be recognized as a valid entry, you have to parse with DateTimeFormatter.ISO_DATE_TIME
. Also, to ensure that your durations are properly picked up, prepend "PT"
to the text that would fit in with durations. A more intelligent way to get that sort of detail from your existing string is an exercise I leave for the reader.
Function<String, Range> convertToRange = (dateString) -> {
String[] dateStringParts = dateString.split("/");
return new Range(LocalDateTime.parse(dateStringParts[0], DateTimeFormatter.ISO_DATE_TIME),
LocalDateTime.parse(dateStringParts[1], DateTimeFormatter.ISO_DATE_TIME));
};
Function<String, DurationAndDateTime> convertToDurationAndDateTime = (dateString) -> {
String[] dateStringParts = dateString.split("/");
String[] durationAndPeriodParts = dateStringParts[1].split("T");
return new DurationAndDateTime(Period.parse(durationAndPeriodParts[0]),
Duration.parse("PT" + durationAndPeriodParts[1]),
LocalDateTime.parse(dateStringParts[0], DateTimeFormatter.ISO_DATE_TIME));
};
Function<String, DurationAndDateTime> convertToDateTimeAndDuration = (dateString) -> {
String[] dateStringParts = dateString.split("/");
String[] durationAndPeriodParts = dateStringParts[0].split("T");
return new DurationAndDateTime(Period.parse(durationAndPeriodParts[0]),
Duration.parse("PT" + durationAndPeriodParts[1]),
LocalDateTime.parse(dateStringParts[1], DateTimeFormatter.ISO_DATE_TIME));
};
Function<String, DurationOnly> convertToDurationOnlyRelativeToCurrentTime = (dateString) -> {
String[] durationAndPeriodParts = dateString.split("T");
return new DurationOnly(Period.parse(durationAndPeriodParts[0]),
Duration.parse("PT" + durationAndPeriodParts[1]));
};
Upvotes: 6