package gov.nysenate.openleg.util; import com.google.common.collect.BoundType; import com.google.common.collect.Range; import org.postgresql.util.PGInterval; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.sql.Time; import java.sql.Timestamp; import java.time.*; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoField; import java.util.Date; import java.util.regex.Pattern; public abstract class DateUtils { private static final Logger logger = LoggerFactory.getLogger(DateUtils.class); /** --- Date Formats --- */ public final static DateTimeFormatter LRS_LAW_FILE_DATE = DateTimeFormatter.ofPattern("yyyyMMdd"); public final static DateTimeFormatter LRS_ACTIONS_DATE = DateTimeFormatter.ofPattern("MM/dd/yy"); public final static DateTimeFormatter LRS_DATE_ONLY_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); public final static DateTimeFormatter LRS_DATETIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH.mm.ss'Z'"); public final static DateTimeFormatter LRS_WEBSITE_DATETIME_FORMAT = DateTimeFormatter.ofPattern("MM/dd/yy hh:mm a"); public final static DateTimeFormatter PUBLIC_WEBSITE_DUMP_DATETIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); public final static DateTimeFormatter BASIC_ISO_DATE_TIME = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss"); public final static Pattern BASIC_ISO_DATE_TIME_REGEX = Pattern.compile("\\d{8}T\\d{6}"); /** --- Reference Dates --- */ public static final LocalDate LONG_AGO = LocalDate.of(1970, 1, 1); public static final LocalDate THE_FUTURE = LocalDate.of(2999, 12, 31); public static final Range<LocalDate> ALL_DATES = Range.closed(LONG_AGO, THE_FUTURE); public static final Range<LocalDateTime> ALL_DATE_TIMES = Range.closed(LONG_AGO.atStartOfDay(), atEndOfDay(THE_FUTURE)); /** --- Static Methods --- */ /** * Retrieve the year of the given date. */ public static Integer getYear(java.util.Date date) { return Year.from(date.toInstant()).getValue(); } /** * Shorthand method to return a LocalDateTime from 'millis since longAgo'. */ public static LocalDateTime getLocalDateTimeFromMillis(long millis) { return LocalDateTime.from(Instant.ofEpochMilli(millis)); } /** * A session year refers to that start of a 2 year legislative session period. * This method ensures that any given year will resolve to the correct session start year. */ public static int resolveSession(int year) { return (year % 2 == 0) ? year - 1 : year; } /** * Returns a LocalDateTime that represents the time just before the start of the next day. */ public static LocalDateTime atEndOfDay(LocalDate date) { return date.atTime(23, 59, 59, 999999999); } /** * Extract the LocalDate value from the LRS formatted date string. * @throws java.time.format.DateTimeParseException if unable to parse the requested result. */ public static LocalDate getLrsLocalDate(String lbdcDate) { return LocalDate.from(LRS_DATE_ONLY_FORMAT.parse(lbdcDate)); } /** * Extract the Date (with time) from the LRS formatted date/time string. * @throws java.time.format.DateTimeParseException if unable to parse the requested result. */ public static LocalDateTime getLrsDateTime(String lbdcDateTime) { return LocalDateTime.from(LRS_DATETIME_FORMAT.parse(lbdcDateTime)); } /** * Extract the Date (with time) from the LRS formatted date/time string. * @throws java.time.format.DateTimeParseException if unable to parse the requested result. */ public static LocalDateTime getLrsWebsiteDateTime(String lbdcDateTime) { return LocalDateTime.from(LRS_WEBSITE_DATETIME_FORMAT.parse(lbdcDateTime)); } /** * Convert a Date to a LocalDate at the system's default time zone. Returns null on null input. */ public static LocalDate getLocalDate(java.util.Date date) { if (date == null) return null; return getLocalDateTime(date).toLocalDate(); } /** * Convert a Date to a LocalTime at the system's default time zone. Returns null on null input. * * @param date * @return */ public static LocalTime getLocalTime(Date date) { if (date == null) return null; return getLocalDateTime(date).toLocalTime(); } /** * Convert a Date to a LocalDateTime at the system's default time zone. Returns null on null input. */ public static LocalDateTime getLocalDateTime(java.util.Date date) { if (date == null) return null; return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()); } /** * Convert a LocalDate to a Date. Returns null on null input. */ public static java.sql.Date toDate(LocalDate localDate) { if (localDate == null) return null; return java.sql.Date.valueOf(localDate); } /** * Convert a LocalDateTime to a Date. Returns null on null input. */ public static Timestamp toDate(LocalDateTime localDateTime) { if (localDateTime == null) return null; return Timestamp.valueOf(localDateTime); } public static Time toTime(LocalTime localTime) { if (localTime == null) return null; return Time.valueOf(localTime); } public static PGInterval toInterval(Period period, Duration duration) { if (duration == null || period == null) return null; return new PGInterval(period.getYears(), period.getMonths(), period.getDays(), (int) duration.toHours(), (int) duration.toMinutes() % 60, (double) (duration.toMillis() % (1000 * 60)) / 1000); } public static Period getPeriod(PGInterval interval) { if (interval == null) return null; return Period.of(interval.getYears(), interval.getMonths(), interval.getDays()); } public static Duration getDuration(PGInterval interval) { if (interval == null) return null; return Duration.ofMillis( (long) (interval.getSeconds() * 1000) + interval.getMinutes() * 60000 + interval.getHours() * 3600000); } /** --- Date Range methods --- */ /** * Converts a LocalDateTime range to a closed LocalDate range * The resulting LocalDate range includes all Dates that contain times that occurred within the given range * * @param dateTimeRange * @return */ public static Range<LocalDate> toDateRange(Range<LocalDateTime> dateTimeRange) { return Range.closed( startOfDateTimeRange(dateTimeRange).toLocalDate(), endOfDateTimeRange(dateTimeRange).toLocalDate() ); } /** * Converts a LocalDate range to a closed LocalDateTime range * The LocalDateTimeRange includes all times that occur within the included LocalDates * * @param dateRange * @return */ public static Range<LocalDateTime> toDateTimeRange(Range<LocalDate> dateRange) { return Range.closed( startOfDateRange(dateRange).atStartOfDay(), endOfDateRange(dateRange).plusDays(1).atStartOfDay() ); } /** * Given the LocalDate range, extract the lower bound LocalDate. If the lower bound is not set, * a really early date will be returned. If the bound is open, a single day will be added to the * LocalDate. If its closed, the date will remain as is. * * @param localDateRange Range<LocalDate> * @return LocalDate - Lower bound in the date range */ public static LocalDate startOfDateRange(Range<LocalDate> localDateRange) { if (localDateRange != null) { LocalDate lower; if (localDateRange.hasLowerBound()) { lower = (localDateRange.lowerBoundType().equals(BoundType.CLOSED)) ? localDateRange.lowerEndpoint() : localDateRange.lowerEndpoint().plusDays(1); } else { lower = LONG_AGO; } return lower; } throw new IllegalArgumentException("Supplied localDateRange is null."); } /** * Given the LocalDateTime range, extract the upper bound LocalDateTime. If the upper bound is not set, a * date far in the future will be returned. If the bound is open, a single day will be subtracted * from the LocalDateTime. If its closed, the date will remain as is. * * @param localDateRange Range<LocalDate> * @return LocalDate - Upper bound in the date range */ public static LocalDate endOfDateRange(Range<LocalDate> localDateRange) { if (localDateRange != null) { LocalDate upper; if (localDateRange.hasUpperBound()) { upper = (localDateRange.upperBoundType().equals(BoundType.CLOSED)) ? localDateRange.upperEndpoint() : localDateRange.upperEndpoint().minusDays(1); } else { upper = THE_FUTURE; } return upper; } throw new IllegalArgumentException("Supplied localDateRange is null."); } /** * Given the LocalDateTime range, extract the lower bound LocalDateTime. If the lower bound is not set, * a really early date will be returned. If the bound is open, a single microsecond will be added to the * LocalDateTime. If its closed, the dateTime will remain as is. * * @param dateTimeRange Range<LocalDateTime> * @return LocalDateTime - Lower bound in the dateTime range */ public static LocalDateTime startOfDateTimeRange(Range<LocalDateTime> dateTimeRange) { if (dateTimeRange != null) { LocalDateTime lower; if (dateTimeRange.hasLowerBound()) { lower = (dateTimeRange.lowerBoundType().equals(BoundType.CLOSED)) ? dateTimeRange.lowerEndpoint() : dateTimeRange.lowerEndpoint().plusNanos(1000); } else { lower = LONG_AGO.atStartOfDay(); } return lower; } throw new IllegalArgumentException("Supplied localDateTimeRange is null."); } /** * Given the LocalDateTime range, extract the upper bound LocalDateTime. If the upper bound is not set, a * date far in the future will be returned. If the bound is open, a single microsecond will be subtracted * from the LocalDateTime. If its closed, the date will remain as is. * * @param dateTimeRange Range<LocalDateTime> * @return LocalDateTime - Upper bound in the dateTime range */ public static LocalDateTime endOfDateTimeRange(Range<LocalDateTime> dateTimeRange) { if (dateTimeRange != null) { LocalDateTime upper; if (dateTimeRange.hasUpperBound()) { upper = (dateTimeRange.upperBoundType().equals(BoundType.CLOSED)) ? dateTimeRange.upperEndpoint() : dateTimeRange.upperEndpoint().minusNanos(1000); } else { upper = atEndOfDay(THE_FUTURE); } return upper; } throw new IllegalArgumentException("Supplied localDateTimeRange is null."); } }