/** * Copyright (C) 2009 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.util.time; import static org.threeten.bp.temporal.ChronoField.DAY_OF_MONTH; import static org.threeten.bp.temporal.ChronoField.MONTH_OF_YEAR; import static org.threeten.bp.temporal.ChronoField.YEAR; import static org.threeten.bp.temporal.ChronoUnit.DAYS; import static org.threeten.bp.temporal.ChronoUnit.MONTHS; import java.util.GregorianCalendar; import java.util.TimeZone; import org.fudgemsg.FudgeMsg; import org.fudgemsg.types.FudgeDate; import org.threeten.bp.Clock; import org.threeten.bp.DayOfWeek; import org.threeten.bp.Duration; import org.threeten.bp.Instant; import org.threeten.bp.LocalDate; import org.threeten.bp.LocalDateTime; import org.threeten.bp.Period; import org.threeten.bp.ZoneId; import org.threeten.bp.ZoneOffset; import org.threeten.bp.ZonedDateTime; import org.threeten.bp.format.DateTimeFormatter; import org.threeten.bp.format.DateTimeFormatterBuilder; import org.threeten.bp.format.SignStyle; import org.threeten.bp.temporal.Temporal; import com.opengamma.OpenGammaRuntimeException; import com.opengamma.util.ArgumentChecker; /** * Utility class for dates. * <p> * This is a thread-safe static utility class. */ public final class DateUtils { /** * The original JVM time-zone. */ public static final ZoneId ORIGINAL_TIME_ZONE = Clock.systemDefaultZone().getZone(); static { // essential that OpenGamm runs in a default time-zone that has no Daylight Savings // UTC is desirable for many other reasons, so use it here TimeZone.setDefault(TimeZone.getTimeZone("UTC")); } /** * The number of seconds in one day. */ public static final long SECONDS_PER_DAY = 86400L; /** * The number of days in one year (estimated as 365.25). */ //TODO change this to 365.2425 to be consistent with JSR-310 public static final double DAYS_PER_YEAR = 365.25; /** * The number of milliseconds in one day. */ public static final long MILLISECONDS_PER_DAY = SECONDS_PER_DAY * 1000; /** * The number of seconds in one year. */ public static final long SECONDS_PER_YEAR = (long) (SECONDS_PER_DAY * DAYS_PER_YEAR); /** * The number of milliseconds in one year. */ public static final long MILLISECONDS_PER_YEAR = SECONDS_PER_YEAR * 1000; /** * A formatter for yyyyMMdd. */ private static final DateTimeFormatter YYYYMMDD_LOCAL_DATE; static { YYYYMMDD_LOCAL_DATE = new DateTimeFormatterBuilder() .appendValue(YEAR, 4, 10, SignStyle.EXCEEDS_PAD) .appendValue(MONTH_OF_YEAR, 2) .appendValue(DAY_OF_MONTH, 2) .toFormatter(); } /** * A formatter for MM-dd */ private static final DateTimeFormatter MM_DD_LOCAL_DATE; static { MM_DD_LOCAL_DATE = new DateTimeFormatterBuilder() .appendValue(MONTH_OF_YEAR, 2) .appendLiteral("-") .appendValue(DAY_OF_MONTH, 2) .toFormatter(); } /** * Restricted constructor. */ private DateUtils() { } //------------------------------------------------------------------------- /** * Initializes the default time-zone to UTC. * <p> * This method actually does nothing, as the code is in a static initializer. */ public static void initTimeZone() { } /** * Gets the original time-zone before it was set to UTC. * * @return the original time-zone, not null */ public static TimeZone originalTimeZone() { return TimeZone.getTimeZone(ORIGINAL_TIME_ZONE.getId()); } //------------------------------------------------------------------------- /** * Returns endDate - startDate in years, where a year is defined as 365.25 days. * * @param startDate the start date, not null * @param endDate the end date, not null * @return the difference in years * @throws IllegalArgumentException if either date is null */ public static double getDifferenceInYears(final Instant startDate, final Instant endDate) { if (startDate == null) { throw new IllegalArgumentException("Start date was null"); } if (endDate == null) { throw new IllegalArgumentException("End date was null"); } return (double) (endDate.toEpochMilli() - startDate.toEpochMilli()) / MILLISECONDS_PER_YEAR; } /** * Returns endDate - startDate in years, where a year is defined as 365.25 days. * * @param startDate the start date, not null * @param endDate the end date, not null * @return the difference in years * @throws IllegalArgumentException if either date is null */ public static double getDifferenceInYears(final ZonedDateTime startDate, final ZonedDateTime endDate) { if (startDate == null) { throw new IllegalArgumentException("Start date was null"); } if (endDate == null) { throw new IllegalArgumentException("End date was null"); } return (double) (endDate.toInstant().toEpochMilli() - startDate.toInstant().toEpochMilli()) / MILLISECONDS_PER_YEAR; } /** * Returns endDate - startDate in years, where a year is defined as 365.25 days. * * @param startDate the start date, not null * @param endDate the end date, not null * @return the difference in years * @throws IllegalArgumentException if either date is null */ public static double getDifferenceInYears(final LocalDate startDate, final LocalDate endDate) { if (startDate == null) { throw new IllegalArgumentException("Start date was null"); } if (endDate == null) { throw new IllegalArgumentException("End date was null"); } double diff = endDate.toEpochDay() - startDate.toEpochDay(); return diff / DAYS_PER_YEAR; } /** * Returns endDate - startDate in years, where a year-length is specified. * * @param startDate the start date, not null * @param endDate the end date, not null * @param daysPerYear the number of days in the year for calculation * @return the difference in years * @throws IllegalArgumentException if either date is null */ public static double getDifferenceInYears(final Instant startDate, final Instant endDate, final double daysPerYear) { if (startDate == null) { throw new IllegalArgumentException("Start date was null"); } if (endDate == null) { throw new IllegalArgumentException("End date was null"); } return (endDate.toEpochMilli() - startDate.toEpochMilli()) / MILLISECONDS_PER_DAY / daysPerYear; } //------------------------------------------------------------------------- /** * Method that allows a fraction of a year to be added to a date. If the yearFraction that is used does not give an integer number of seconds, it is rounded to the nearest nanosecond. Note that the * number of days in a year is defined to be 365.25. * * @param startDate the start date, not null * @param yearFraction the fraction of a year * @return the calculated instant, not null * @throws IllegalArgumentException if the date is null */ public static Instant getDateOffsetWithYearFraction(final Instant startDate, final double yearFraction) { if (startDate == null) { throw new IllegalArgumentException("Date was null"); } final long nanos = Math.round(1e9 * SECONDS_PER_YEAR * yearFraction); return startDate.plusNanos(nanos); } /** * Method that allows a fraction of a year to be added to a date. If the yearFraction that is used does not give an integer number of seconds, it is rounded to the nearest nanosecond. Note that the * number of days in a year is defined to be 365.25. * * @param startDate the start date, not null * @param yearFraction the fraction of a year * @return the calculated date-time, not null * @throws IllegalArgumentException if the date is null */ public static ZonedDateTime getDateOffsetWithYearFraction(final ZonedDateTime startDate, final double yearFraction) { if (startDate == null) { throw new IllegalArgumentException("Date was null"); } final Instant instant = startDate.toInstant(); final Instant offsetDate = getDateOffsetWithYearFraction(instant, yearFraction); return ZonedDateTime.ofInstant(offsetDate, startDate.getZone()); } /** * Method that allows a fraction of a year to be added to a date. If the yearFraction that is used does not give an integer number of seconds, it is rounded to the nearest nanosecond. * * @param startDate the start date, not null * @param yearFraction the fraction of a year * @param daysPerYear the number of days in the year for calculation * @return the calculated instant, not null * @throws IllegalArgumentException if the date is null */ public static Instant getDateOffsetWithYearFraction(final Instant startDate, final double yearFraction, final double daysPerYear) { if (startDate == null) { throw new IllegalArgumentException("Date was null"); } final long nanos = Math.round(1e9 * SECONDS_PER_DAY * daysPerYear * yearFraction); return startDate.plusNanos(nanos); } /** * Method that allows a fraction of a year to be added to a date. If the yearFraction that is used does not give an integer number of seconds, it is rounded to the nearest nanosecond. * * @param startDate the start date, not null * @param yearFraction the fraction of a year * @param daysPerYear the number of days in the year for calculation * @return the calculated date-time, not null * @throws IllegalArgumentException if the date is null */ public static ZonedDateTime getDateOffsetWithYearFraction(final ZonedDateTime startDate, final double yearFraction, final double daysPerYear) { if (startDate == null) { throw new IllegalArgumentException("Date was null"); } final Instant instant = startDate.toInstant(); final Instant offsetDate = getDateOffsetWithYearFraction(instant, yearFraction, daysPerYear); return ZonedDateTime.ofInstant(offsetDate, startDate.getZone()); } //------------------------------------------------------------------------- /** * Returns a UTC date given year, month, day with the time set to midnight (UTC). * * @param year the year * @param month the month * @param day the day of month * @return the date-time, not null */ public static ZonedDateTime getUTCDate(final int year, final int month, final int day) { return LocalDate.of(year, month, day).atStartOfDay(ZoneOffset.UTC); } /** * Returns a UTC date given year, month, day, hour and minutes. * * @param year the year * @param month the month * @param day the day of month * @param hour the hour * @param minute the minute * @return the date-time, not null */ public static ZonedDateTime getUTCDate(final int year, final int month, final int day, final int hour, final int minute) { return ZonedDateTime.of(LocalDateTime.of(year, month, day, hour, minute), ZoneOffset.UTC); } //------------------------------------------------------------------------- /** * Calculates the exact number of 24 hour days in between two dates. Accounts for dates being in different time zones. * * @param startDate the start date, not null * @param endDate the end date, not null * @return the exact fraction of days between two dates * @throws IllegalArgumentException if the date is null */ public static double getExactDaysBetween(final ZonedDateTime startDate, final ZonedDateTime endDate) { // TODO: was 24-hour days intended? if (startDate == null) { throw new IllegalArgumentException("Start date was null"); } if (endDate == null) { throw new IllegalArgumentException("End date was null"); } return (endDate.toInstant().getEpochSecond() - startDate.toInstant().getEpochSecond()) / (double) SECONDS_PER_DAY; } /** * Calculates the number of days in between two dates. * * @param startDate the start date, not null * @param endDate the end date, not null * @return the number of days between two dates * @throws IllegalArgumentException if the date is null */ public static int getDaysBetween(final Temporal startDate, final Temporal endDate) { return getDaysBetween(startDate, true, endDate, false); } /** * Calculates the number of days in between two dates. * * @param startDate the start date, not null * @param includeStart whether to include the start * @param endDate the end date, not null * @param includeEnd whether to include the end * @return the number of days between two dates * @throws IllegalArgumentException if the date is null */ public static int getDaysBetween(final Temporal startDate, final boolean includeStart, final Temporal endDate, final boolean includeEnd) { if (startDate == null) { throw new IllegalArgumentException("Start date was null"); } if (endDate == null) { throw new IllegalArgumentException("End date was null"); } int daysBetween = (int) Math.abs(DAYS.between(startDate, endDate)); if (includeStart && includeEnd) { daysBetween++; } else if (!includeStart && !includeEnd) { daysBetween--; } return daysBetween; } /** * Prints the date in yyyyMMdd format. * * @param date the date, not null * @return the date as a string, not null * @throws IllegalArgumentException if the date is null */ public static String printYYYYMMDD(Temporal date) { if (date == null) { throw new IllegalArgumentException("date was null"); } return YYYYMMDD_LOCAL_DATE.format(date); } /** * Prints the date in MM-dd format. * * @param date the date, not null * @return the date as a string, not null * @throws IllegalArgumentException if the date is null */ public static String printMMDD(Temporal date) { if (date == null) { throw new IllegalArgumentException("date was null"); } return MM_DD_LOCAL_DATE.format(date); } /** * Gets the previous Monday to Friday week-day before now. * * @return the date, not null */ public static LocalDate previousWeekDay() { Clock clock = Clock.systemUTC(); return previousWeekDay(LocalDate.now(clock)); } /** * Gets the next Monday to Friday week-day after now. * * @return the date, not null */ public static LocalDate nextWeekDay() { Clock clock = Clock.systemUTC(); return nextWeekDay(LocalDate.now(clock)); } /** * Gets the next Monday to Friday week-day after now. * * @param startDate the date to start from * @return the date, not null */ public static LocalDate nextWeekDay(LocalDate startDate) { if (startDate == null) { throw new IllegalArgumentException("date was null"); } LocalDate next = null; DayOfWeek dayOfWeek = startDate.getDayOfWeek(); switch (dayOfWeek) { case FRIDAY: next = startDate.plusDays(3); break; case SATURDAY: next = startDate.plusDays(2); break; case MONDAY: case TUESDAY: case WEDNESDAY: case THURSDAY: case SUNDAY: next = startDate.plusDays(1); break; default: throw new OpenGammaRuntimeException("Unrecognised day of the week"); } return next; } /** * Gets the previous Monday to Friday week-day before now. * * @param startDate the date to start from * @return the date, not null */ public static LocalDate previousWeekDay(LocalDate startDate) { if (startDate == null) { throw new IllegalArgumentException("date was null"); } LocalDate previous = null; DayOfWeek dayOfWeek = startDate.getDayOfWeek(); switch (dayOfWeek) { case MONDAY: previous = startDate.minusDays(3); break; case TUESDAY: case WEDNESDAY: case THURSDAY: case FRIDAY: case SATURDAY: previous = startDate.minusDays(1); break; case SUNDAY: previous = startDate.minusDays(2); break; default: throw new OpenGammaRuntimeException("Unrecognised day of the week"); } return previous; } /** * Converts a date in integer YYYYMMDD representation to epoch millis. * * @param date in integer YYYYMMDD representation * @return the epoch millis */ public static long getUTCEpochMilis(int date) { LocalDate localDate = LocalDate.parse(String.valueOf(date), YYYYMMDD_LOCAL_DATE); return localDate.toEpochDay() * 24 * 60 * 60 * 1000; } /** * Converts a date in integer YYYYMMDD representation to a UTC date-time. * * @param date in integer YYYYMMDD representation * @return the date-time, not null */ public static ZonedDateTime toZonedDateTimeUTC(int date) { LocalDate localDate = LocalDate.parse(String.valueOf(date), YYYYMMDD_LOCAL_DATE); ZonedDateTime zonedDateTime = getUTCDate(localDate.getYear(), localDate.getMonthValue(), localDate.getDayOfMonth()); return zonedDateTime; } /** * Converts a date in integer YYYYMMDD representation to a date. * * @param date in integer YYYYMMDD representation * @return the date, not null */ public static LocalDate toLocalDate(int date) { return toLocalDate(String.valueOf(date)); } /** * Converts a date in string YYYYMMDD representation to epoch millis. * * @param date in YYYYMMDD representation, not null * @return the date */ public static LocalDate toLocalDate(String date) { ArgumentChecker.notNull(date, "date"); return LocalDate.parse(date, YYYYMMDD_LOCAL_DATE); } /** * Constructs a LocalDate from a <code>java.util.Date</code> using exactly the same field values. * <p> * Each field is queried from the Date and assigned to the LocalDate. This is useful if you have been using the Date as a local date, ignoring the zone. * * @param date the Date to extract fields from * @return the created LocalDate * @throws IllegalArgumentException if the calendar is null * @throws IllegalArgumentException if the date is invalid for the ISO chronology */ @SuppressWarnings("deprecation") public static LocalDate fromDateFields(java.util.Date date) { if (date == null) { throw new IllegalArgumentException("The date must not be null"); } return LocalDate.of(date.getYear() + 1900, date.getMonth() + 1, date.getDate()); } /** * Constructs a LocalDate from a Function Requirement / Input passed over the wire via {@link FudgeMsg} <p> * Example usage: LocalDate nextDividendDate = DateUtils.toLocalDate(inputs.getValue(MarketDataRequirementNames.NEXT_DIVIDEND_DATE)); * * @param date an Object * @return the created LocalDate * @throws IllegalArgumentException if the date is not a recognized type */ public static LocalDate toLocalDate(Object date) { if (date instanceof LocalDate) { return (LocalDate) date; } if (date instanceof FudgeDate) { return ((FudgeDate) date).toLocalDate(); } throw new IllegalArgumentException(date.toString() + " is not a date"); } //------------------------------------------------------------------------- /** * Creates a clock with a fixed time-source and UTC time-zone. * * @param instant the instant to be provided by the clock, not null * @return the clock, not null */ public static Clock fixedClockUTC(Instant instant) { return Clock.fixed(instant, ZoneOffset.UTC); } //------------------------------------------------------------------------- /** * Gets the estimated duration of the period. * * @param period the period to estimate the duration of, not null * @return the estimated duration, not null */ public static Duration estimatedDuration(Period period) { Duration monthsDuration = MONTHS.getDuration().multipliedBy(period.toTotalMonths()); Duration daysDuration = DAYS.getDuration().multipliedBy(period.getDays()); return monthsDuration.plus(daysDuration); } /** * Converts GregorianCalendar to ZonedDateTime * * @param calendar the calendar, not null * @return the zoned-date-time, not null */ public static ZonedDateTime toZonedDateTime(GregorianCalendar calendar) { ZoneId zone = ZoneId.of(calendar.getTimeZone().getID()); Instant instant = Instant.ofEpochMilli(calendar.getTimeInMillis()); return ZonedDateTime.ofInstant(instant, zone); } /** * Converts a string to a period, allowing the old format of {@code PT0S} for {@code P0D}. * * @param period the period to parse, not null * @return the parsed period, not null * @deprecated Don't rely on this, fix the source of data where the PT0S values are coming from */ @Deprecated public static Period toPeriod(final String period) { if ("PT0S".equals(period)) { return Period.ZERO; } else { return Period.parse(period); } } }