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}