TimeFormat.java
001 /*
002  * Java GPX Library (jpx-3.1.0).
003  * Copyright (c) 2016-2023 Franz Wilhelmstötter
004  *
005  * Licensed under the Apache License, Version 2.0 (the "License");
006  * you may not use this file except in compliance with the License.
007  * You may obtain a copy of the License at
008  *
009  *      http://www.apache.org/licenses/LICENSE-2.0
010  *
011  * Unless required by applicable law or agreed to in writing, software
012  * distributed under the License is distributed on an "AS IS" BASIS,
013  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014  * See the License for the specific language governing permissions and
015  * limitations under the License.
016  *
017  * Author:
018  *    Franz Wilhelmstötter (franz.wilhelmstoetter@gmail.com)
019  */
020 package io.jenetics.jpx;
021 
022 import static java.time.ZoneOffset.UTC;
023 import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME;
024 import static java.util.Objects.requireNonNull;
025 
026 import java.time.Instant;
027 import java.time.ZonedDateTime;
028 import java.time.format.DateTimeFormatter;
029 import java.time.format.DateTimeFormatterBuilder;
030 import java.time.format.DateTimeParseException;
031 import java.time.format.ResolverStyle;
032 import java.util.Optional;
033 import java.util.regex.Pattern;
034 import java.util.stream.Stream;
035 
036 /**
037  * Enumeration of the valid date time formats.
038  *
039  @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
040  @version 3.0
041  @since 1.0
042  */
043 enum TimeFormat {
044 
045     // http://books.xmlschemata.org/relaxng/ch19-77049.html
046 
047     ISO_DATE_TIME_UTC(
048         new DateTimeFormatterBuilder()
049             .append(ISO_LOCAL_DATE_TIME)
050             .optionalStart()
051             .appendOffsetId()
052             .toFormatter()
053             .withResolverStyle(ResolverStyle.LENIENT)
054             .withZone(UTC),
055         "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})*+Z*+$"
056     ),
057 
058     ISO_DATE_TIME_OFFSET(
059         new DateTimeFormatterBuilder()
060             .append(ISO_LOCAL_DATE_TIME)
061             .optionalStart()
062             .appendOffsetId()
063             .toFormatter(),
064         "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})*+([+-]\\d{2}:\\d{2})"
065     );
066 
067     // Default formatter used for formatting time strings.
068     private static final DateTimeFormatter FORMATTER =
069         DateTimeFormatter.ISO_DATE_TIME.withZone(UTC);
070 
071     private final DateTimeFormatter _formatter;
072     private final Pattern[] _patterns;
073 
074     TimeFormat(final DateTimeFormatter formatter, final String... patterns) {
075         _formatter = requireNonNull(formatter);
076         _patterns = Stream.of(patterns)
077             .map(Pattern::compile)
078             .toArray(Pattern[]::new);
079     }
080 
081     private boolean matches(final String time) {
082         for (var pattern : _patterns) {
083             if  (pattern.matcher(time).matches()) {
084                 return true;
085             }
086         }
087         return false;
088     }
089 
090     /**
091      * Parses the given time string with the current formatter.
092      *
093      @param time the time string to pare
094      @return the parsed zoned date time
095      *  @throws DateTimeParseException if the text cannot be parsed
096      */
097     public Instant formatParse(final String time) {
098         return time != null
099             ? ZonedDateTime.parse(time, _formatter).toInstant()
100             null;
101     }
102 
103     /**
104      * Return the default format of the given {@code ZonedDateTime}.
105      *
106      @param time the {@code ZonedDateTime} to format
107      @return the time string of the given zoned date time
108      */
109     public static String format(final Instant time) {
110         return time != null ? FORMATTER.format(timenull;
111     }
112 
113     static Optional<Instant> parseOptional(final String time) {
114         final var format = findFormat(time);
115         if (format != null) {
116             return Optional.of(format.formatParse(time));
117         else {
118             return Optional.empty();
119         }
120     }
121 
122     /**
123      * Finds the formatter which fits the given {@code time} string.
124      *
125      @param time the time string
126      @return the formatter which fits to the given {@code time} string, or
127      *         {@code Optional.empty()} of no formatter is found
128      */
129     static TimeFormat findFormat(final String time) {
130         if (ISO_DATE_TIME_UTC.matches(time)) {
131             return ISO_DATE_TIME_UTC;
132         else if (ISO_DATE_TIME_OFFSET.matches(time)) {
133             return ISO_DATE_TIME_OFFSET;
134         else {
135             return null;
136         }
137     }
138 
139     /**
140      * Parses the given object to a zoned data time object.
141      *
142      @param value the string to parse
143      @return the parsed object
144      */
145     static Instant parse(final String value) {
146         final String time = Strings.trim(value);
147 
148         if (time != null) {
149             final var format = findFormat(time);
150             if (format != null) {
151                 return format.formatParse(time);
152             else {
153                 throw new IllegalArgumentException(
154                     String.format("Can't parse time: '%s'", time)
155                 );
156             }
157         else {
158             return null;
159         }
160     }
161 
162 }