001/*
002 * Licensed under the Apache License, Version 2.0 (the "License");
003 * you may not use this file except in compliance with the License.
004 * You may obtain a copy of the License at
005 *
006 *     http://www.apache.org/licenses/LICENSE-2.0
007 *
008 * Unless required by applicable law or agreed to in writing, software
009 * distributed under the License is distributed on an "AS IS" BASIS,
010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011 * See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014package org.gbif.common.parsers.date;
015
016import javax.annotation.Nullable;
017import java.time.LocalDate;
018import java.time.LocalDateTime;
019import java.time.OffsetDateTime;
020import java.time.Year;
021import java.time.YearMonth;
022import java.time.ZoneId;
023import java.time.ZoneOffset;
024import java.time.temporal.ChronoField;
025import java.time.temporal.Temporal;
026import java.time.temporal.TemporalAccessor;
027import java.time.temporal.TemporalQueries;
028import java.util.ArrayList;
029import java.util.Date;
030import java.util.List;
031import java.util.Optional;
032
033/**
034 * Utility methods to work with {@link TemporalAccessor}
035 */
036public class TemporalAccessorUtils {
037
038  public static ZoneId UTC_ZONE_ID = ZoneOffset.UTC;
039
040  /**
041   * Transform a {@link TemporalAccessor} to a {@link java.util.Date}.
042   * If the provided {@link TemporalAccessor} contains offset(timezone) information it will be used.
043   * See {@link #toDate(TemporalAccessor, boolean)} for more details.
044   *
045   * @return  the Date object or null if a Date object can not be created
046   */
047  public static Date toDate(TemporalAccessor temporalAccessor) {
048    return toDate(temporalAccessor, false);
049  }
050
051  /**
052   * Transform a {@link TemporalAccessor} to a {@link java.util.Date}, rounding a partial date/time to the start
053   * of the period.
054   *
055   * For {@link YearMonth}, the {@link java.util.Date} will represent the first day of the month.
056   * For {@link Year}, the {@link java.util.Date} will represent the first day of January.
057   *
058   * Remember that a {@link Date} object will always display the date in the current timezone.
059   *
060   * @param ignoreOffset in case offset information is available in the provided {@link TemporalAccessor}, should it
061   *                     be used ?
062   * @return the Date object or null if a Date object can not be created
063   */
064  public static Date toDate(TemporalAccessor temporalAccessor, boolean ignoreOffset) {
065    return Date.from(toEarliestLocalDateTime(temporalAccessor, ignoreOffset).toInstant(ZoneOffset.UTC));
066  }
067
068  /**
069   * Removes any time zone, optionally adjusting the time using the zone offset first.
070   */
071  public static TemporalAccessor stripOffsetOrZone(TemporalAccessor temporalAccessor, boolean ignoreOffset) {
072    if (temporalAccessor == null) {
073      return null;
074    }
075
076    // Use offset if present
077    if (!ignoreOffset && temporalAccessor.isSupported(ChronoField.OFFSET_SECONDS)) {
078      return temporalAccessor.query(OffsetDateTime::from).atZoneSameInstant(UTC_ZONE_ID).toLocalDateTime();
079    }
080
081    if (temporalAccessor.isSupported(ChronoField.SECOND_OF_DAY)) {
082      return temporalAccessor.query(LocalDateTime::from);
083    }
084
085    return temporalAccessor;
086  }
087
088  /**
089   * Transform a {@link TemporalAccessor} to a {@link java.time.LocalDateTime}, rounding a partial date/time to the
090   * start of the period.
091   *
092   * @param temporalAccessor
093   * @param ignoreOffset in case offset information is available in the provided {@link TemporalAccessor}, should it
094   *                     be used ?
095   * @return the LocalDateTime object or null if a LocalDateTime object can not be created
096   */
097  public static LocalDateTime toEarliestLocalDateTime(TemporalAccessor temporalAccessor, boolean ignoreOffset) {
098    if (temporalAccessor == null) {
099      return null;
100    }
101
102    // Use offset if present
103    if (!ignoreOffset && temporalAccessor.isSupported(ChronoField.OFFSET_SECONDS)) {
104      return temporalAccessor.query(OffsetDateTime::from).atZoneSameInstant(UTC_ZONE_ID).toLocalDateTime();
105    }
106
107    if (temporalAccessor.isSupported(ChronoField.SECOND_OF_DAY)) {
108      return temporalAccessor.query(LocalDateTime::from);
109    }
110
111    // this may return null in case of partial dates
112    LocalDate localDate = temporalAccessor.query(TemporalQueries.localDate());
113
114    // try YearMonth
115    if (localDate == null && temporalAccessor.isSupported(ChronoField.MONTH_OF_YEAR)) {
116      YearMonth yearMonth = YearMonth.from(temporalAccessor);
117      localDate = yearMonth.atDay(1);
118    }
119
120    // try Year
121    if (localDate == null && temporalAccessor.isSupported(ChronoField.YEAR)) {
122      Year year = Year.from(temporalAccessor);
123      localDate = year.atDay(1);
124    }
125
126    if (localDate != null) {
127      return LocalDateTime.from(localDate.atStartOfDay(UTC_ZONE_ID));
128    }
129
130    return null;
131  }
132
133  /**
134   * Transform a {@link TemporalAccessor} to a {@link java.time.LocalDateTime}, rounding a partial date/time to the
135   * end of the period.
136   *
137   * 1990 will be 1990-12-31,
138   * 1996-02 will be 1996-02-29
139   *
140   * @param temporalAccessor
141   * @param ignoreOffset in case offset information is available in the provided {@link TemporalAccessor}, should it
142   *                     be used ?
143   * @return the LocalDateTime object or null if a LocalDateTime object can not be created
144   */
145  public static LocalDateTime toLatestLocalDateTime(TemporalAccessor temporalAccessor, boolean ignoreOffset) {
146    if (temporalAccessor == null) {
147      return null;
148    }
149
150    // Use offset if present
151    if (!ignoreOffset && temporalAccessor.isSupported(ChronoField.OFFSET_SECONDS)) {
152      return temporalAccessor.query(OffsetDateTime::from).atZoneSameInstant(UTC_ZONE_ID).toLocalDateTime();
153    }
154
155    if (temporalAccessor.isSupported(ChronoField.SECOND_OF_DAY)) {
156      return temporalAccessor.query(LocalDateTime::from);
157    }
158
159    // this may return null in case of partial dates
160    LocalDate localDate = temporalAccessor.query(TemporalQueries.localDate());
161
162    // try YearMonth
163    if (localDate == null && temporalAccessor.isSupported(ChronoField.MONTH_OF_YEAR)) {
164      YearMonth yearMonth = YearMonth.from(temporalAccessor);
165      localDate = yearMonth.atEndOfMonth();
166    }
167
168    // try Year
169    if (localDate == null && temporalAccessor.isSupported(ChronoField.YEAR)) {
170      Year year = Year.from(temporalAccessor);
171      localDate = LocalDate.of(year.getValue(), 12, 31);
172    }
173
174    if (localDate != null) {
175      return LocalDateTime.from(localDate.atTime(23, 59, 59, 999_000_000));
176    }
177
178    return null;
179  }
180
181  /**
182   * The idea of "best resolution" TemporalAccessor is to get the TemporalAccessor that offers more resolution than
183   * the other, but they must NOT contradict.
184   * e.g. 2005-01 and 2005-01-01 will return 2005-01-01.
185   *
186   * Note that if one of the 2 parameters is null the other one will be considered having the best resolution
187   *
188   * @return TemporalAccessor representing the best resolution
189   */
190  public static Optional<TemporalAccessor> bestResolution(@Nullable TemporalAccessor ta1, @Nullable TemporalAccessor ta2) {
191    // handle nulls combinations
192    if (ta1 == null && ta2 == null) {
193      return Optional.empty();
194    }
195    if (ta1 == null) {
196      return Optional.of(ta2);
197    }
198    if (ta2 == null) {
199      return Optional.of(ta1);
200    }
201
202    AtomizedLocalDateTime ymd1 = AtomizedLocalDateTime.fromTemporalAccessor(ta1);
203    AtomizedLocalDateTime ymd2 = AtomizedLocalDateTime.fromTemporalAccessor(ta2);
204
205    // If they both provide the year, it must match
206    if (ymd1.getYear() != null && ymd2.getYear() != null && !ymd1.getYear().equals(ymd2.getYear())) {
207      return Optional.empty();
208    }
209    // If they both provide the month, it must match
210    if (ymd1.getMonth() != null && ymd2.getMonth() != null && !ymd1.getMonth().equals(ymd2.getMonth())) {
211      return Optional.empty();
212    }
213    // If they both provide the day, it must match
214    if (ymd1.getDay() != null && ymd2.getDay() != null && !ymd1.getDay().equals(ymd2.getDay())) {
215      return Optional.empty();
216    }
217    // If they both provide the hour, it must match
218    if (ymd1.getHour() != null && ymd2.getHour() != null && !ymd1.getHour().equals(ymd2.getHour())) {
219      return Optional.empty();
220    }
221    // If they both provide the minute, it must match
222    if (ymd1.getMinute() != null && ymd2.getMinute() != null && !ymd1.getMinute().equals(ymd2.getMinute())) {
223      return Optional.empty();
224    }
225    // If they both provide the second, it must match
226    if (ymd1.getSecond() != null && ymd2.getSecond() != null && !ymd1.getSecond().equals(ymd2.getSecond())) {
227      return Optional.empty();
228    }
229    // If they both provide the millisecond, it must match
230    if (ymd1.getMillisecond() != null && ymd2.getMillisecond() != null && !ymd1.getMillisecond().equals(ymd2.getMillisecond())) {
231      return Optional.empty();
232    }
233
234    if (ymd1.getResolution() > ymd2.getResolution()) {
235      return Optional.of(ta1);
236    }
237
238    return Optional.of(ta2);
239  }
240
241  public static TemporalAccessor limitToResolution(TemporalAccessor ta, int requiredResolution) {
242    if (ta == null) {
243      return null;
244    }
245
246    if (requiredResolution > 3) {
247      // TODO: Different minutes/seconds.
248      return ta.query(LocalDateTime::from);
249    } else if (requiredResolution == 3) {
250      return ta.query(LocalDate::from);
251    } else if (requiredResolution == 2) {
252      return ta.query(YearMonth::from);
253    } else {
254      return ta.query(Year::from);
255    }
256  }
257
258  public static Temporal limitToResolution(Temporal ta, int requiredResolution) {
259    if (ta == null) {
260      return null;
261    }
262
263    if (requiredResolution > 3) {
264      // TODO: Different minutes/seconds.
265      return ta.query(LocalDateTime::from);
266    } else if (requiredResolution == 3) {
267      return ta.query(LocalDate::from);
268    } else if (requiredResolution == 2) {
269      return ta.query(YearMonth::from);
270    } else {
271      return ta.query(Year::from);
272    }
273  }
274
275  /**
276   * The idea of "non-conflicting date parts" TemporalAccessor is to get as much of year, then month, then day as possible,
277   * ignoring null and stopping once there is a contradiction.  Times are ignored for comparison, but the argument with
278   * the highest resolution is returned.
279   *
280   * e.g. 2005-02, 2005-02-03, 2005-02-03T04:05:06 will return 2005-02-03T04:05:06.
281   *
282   * Null arguments are ignored.
283   *
284   * @return TemporalAccessor representing the best resolution
285   */
286  public static Optional<TemporalAccessor> nonConflictingDateParts(
287    @Nullable TemporalAccessor ta1, @Nullable TemporalAccessor ta2, @Nullable TemporalAccessor ta3) {
288    // handle nulls combinations
289    if (ta1 == null && ta2 == null && ta3 == null) {
290      return Optional.empty();
291    }
292    if (ta2 == null && ta3 == null) {
293      return Optional.of(ta1);
294    }
295    if (ta3 == null && ta1 == null) {
296      return Optional.of(ta2);
297    }
298    if (ta1 == null && ta2 == null) {
299      return Optional.of(ta3);
300    }
301
302    AtomizedLocalDateTime ymd1 = AtomizedLocalDateTime.fromTemporalAccessor(ta1);
303    AtomizedLocalDateTime ymd2 = AtomizedLocalDateTime.fromTemporalAccessor(ta2);
304    AtomizedLocalDateTime ymd3 = AtomizedLocalDateTime.fromTemporalAccessor(ta3);
305
306    List<AtomizedLocalDateTime> ymd = new ArrayList<>();
307    if (ymd1 != null) ymd.add(ymd1);
308    if (ymd2 != null) ymd.add(ymd2);
309    if (ymd3 != null) ymd.add(ymd3);
310
311    final Integer year, month;
312
313    // If they both provide the year, it must match
314    if (ymd.stream().map(m -> m.getYear()).distinct().count() == 1) {
315      year = ymd.stream().filter(m -> m.getYear() != null).findFirst().map(AtomizedLocalDateTime::getYear).orElse(null);
316    } else {
317      return Optional.empty();
318    }
319
320    // If they both provide the month, it must match
321    if (ymd.stream().map(m -> m.getMonth()).distinct().count() == 1) {
322      month = ymd.stream().filter(m -> m.getMonth() != null).findFirst().map(AtomizedLocalDateTime::getMonth).orElse(null);
323    } else {
324      return Optional.of(Year.of(year));
325    }
326
327    // If they both provide the day, it must match
328    if (ymd.stream().map(m -> m.getDay()).distinct().count() == 1) {
329      // Then return the one with the best resolution
330      return bestResolution(ta1, bestResolution(ta2, ta3).orElse(null));
331    } else {
332      if (month == null) {
333        return Optional.of(Year.of(year));
334      } else {
335        return Optional.of(YearMonth.of(year, month));
336      }
337    }
338  }
339
340  /**
341   * Given two TemporalAccessor with possibly different resolutions, this method checks if they represent the same
342   * date, or if one is contained within the other.  The comparison does not go beyond date resolution.
343   *
344   * If a null is provided, false will be returned.
345   */
346  public static boolean sameOrContained(@Nullable TemporalAccessor ta1, @Nullable TemporalAccessor ta2) {
347    // handle nulls combinations
348    if (ta1 == null || ta2 == null) {
349      return false;
350    }
351
352    AtomizedLocalDate ymd1 = AtomizedLocalDate.fromTemporalAccessor(ta1);
353    AtomizedLocalDate ymd2 = AtomizedLocalDate.fromTemporalAccessor(ta2);
354
355    // we only deal with complete Local Date
356    if (ymd1.isComplete() && ymd2.isComplete()) {
357      return ymd1.equals(ymd2);
358    }
359
360    // check for equal years
361    if (!ymd1.getYear().equals(ymd2.getYear())) {
362      return false;
363    }
364
365    // compare months
366    if (ymd1.getMonth() == null || ymd2.getMonth() == null) {
367      return true;
368    }
369    if (!ymd1.getMonth().equals(ymd2.getMonth())) {
370      return false;
371    }
372
373    // compare days
374    if (ymd1.getDay() == null || ymd2.getDay() == null) {
375      return true;
376    }
377    return ymd1.getDay().equals(ymd2.getDay());
378  }
379
380  /**
381   * Given two TemporalAccessor with possibly different resolutions, this method checks if they represent the same
382   * date, or if one is contained within the other.  The comparison does not go beyond date resolution.
383   *
384   * If a null is provided, true will be returned.
385   */
386  public static boolean sameOrContainedOrNull(@Nullable TemporalAccessor ta1, @Nullable TemporalAccessor ta2) {
387    // handle nulls combinations
388    if (ta1 == null || ta2 == null) {
389      return true;
390    }
391
392    return sameOrContained(ta1, ta2);
393  }
394
395  /**
396   * Given two TemporalAccessor with at least date resolution, this method checks if they represent the same
397   * date.
398   *
399   * If a null is provided, false will be returned.
400   */
401  public static boolean sameDate(@Nullable TemporalAccessor ta1, @Nullable TemporalAccessor ta2) {
402    // handle nulls combinations
403    if (ta1 == null || ta2 == null) {
404      return false;
405    }
406
407    AtomizedLocalDate ymd1 = AtomizedLocalDate.fromTemporalAccessor(ta1);
408    AtomizedLocalDate ymd2 = AtomizedLocalDate.fromTemporalAccessor(ta2);
409
410    // we only deal with complete Local Date
411    if (ymd1.isComplete() && ymd2.isComplete()) {
412      return ymd1.equals(ymd2);
413    }
414
415    return false;
416  }
417
418  /**
419   * Given two TemporalAccessor with at least day resolution (representing the bounds), and another TemporalAccessor
420   * of day resolution, this method checks if the bounds contain the third argument.
421   *
422   * If a null is provided, false will be returned.
423   */
424  public static boolean withinRange(@Nullable TemporalAccessor lowerBound, @Nullable TemporalAccessor upperBound, @Nullable TemporalAccessor queryTa) {
425    // handle nulls combinations
426    if (lowerBound == null || upperBound == null || queryTa == null) {
427      return false;
428    }
429
430    LocalDate earliest = toEarliestLocalDateTime(lowerBound, true).toLocalDate();
431    LocalDate latest = toLatestLocalDateTime(upperBound, true).toLocalDate();
432    LocalDate query = toEarliestLocalDateTime(queryTa, true).toLocalDate();
433
434    return earliest.compareTo(query) <= 0 && 0 <= latest.compareTo(query);
435  }
436
437  /**
438   * Returns a date from the list of dodgyTas which matches the reliableTa
439   */
440  public static Optional<TemporalAccessor> resolveAmbiguousDates(TemporalAccessor reliableTa, List<TemporalAccessor> dodgyTas) {
441    // A DD-MM-YYYY or MM/DD/YYYY date could be disambiguated by another date.
442    for (TemporalAccessor possibleTa : dodgyTas) {
443      if (TemporalAccessorUtils.sameDate(reliableTa, possibleTa)) {
444        return Optional.of(possibleTa);
445      }
446    }
447
448    return Optional.empty();
449  }
450
451  /**
452   * Returns the resolution of the TemporalAccessor: year=1, month=2, day=3.  0 for null/empty.
453   */
454  public static int resolution(TemporalAccessor ta) {
455    if (ta == null) {
456      return 0;
457    }
458
459    AtomizedLocalDate ymd = AtomizedLocalDate.fromTemporalAccessor(ta);
460
461    return ymd.getResolution();
462  }
463
464  /**
465   * Returns the resolution of the TemporalAccessor: year=1, month=2, day=3, hour=4, minutes=5, seconds=6.  0 for null/empty.
466   */
467  public static int resolutionToSeconds(TemporalAccessor ta) {
468    if (ta == null) {
469      return 0;
470    }
471
472    if (ta.isSupported(ChronoField.SECOND_OF_MINUTE)) {
473      return 6;
474    }
475    if (ta.isSupported(ChronoField.MINUTE_OF_HOUR)) {
476      return 5;
477    }
478    if (ta.isSupported(ChronoField.HOUR_OF_DAY)) {
479      return 4;
480    }
481
482    AtomizedLocalDate ymd = AtomizedLocalDate.fromTemporalAccessor(ta);
483
484    return ymd.getResolution();
485  }
486}