package de.invesdwin.util.time.fdate; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.TimeZone; import javax.annotation.concurrent.ThreadSafe; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.joda.time.DurationFieldType; import org.joda.time.LocalDateTime; import org.joda.time.MutableDateTime; import org.joda.time.ReadableDateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import de.invesdwin.norva.marker.IDate; import de.invesdwin.util.error.UnknownArgumentException; import de.invesdwin.util.lang.ADelegateComparator; import de.invesdwin.util.lang.Strings; import de.invesdwin.util.math.Integers; import de.invesdwin.util.math.decimal.scaled.Percent; import de.invesdwin.util.time.duration.Duration; import de.jollyday.HolidayCalendar; import de.jollyday.HolidayManager; import de.jollyday.HolidayType; import de.jollyday.ManagerParameters; /** * FDate stands for an immutable Fast Date implementation by utilizing heavy caching. */ @ThreadSafe public class FDate implements IDate, Serializable, Cloneable, Comparable<Object> { public static final ADelegateComparator<FDate> COMPARATOR = new ADelegateComparator<FDate>() { @Override protected Comparable<?> getCompareCriteria(final FDate e) { return e; } }; /** * Somehow leveldb-jni does not like going higher than this year... */ public static final int MAX_YEAR = 5555; public static final int MIN_YEAR = 1; public static final FDate MIN_DATE = FDateBuilder.newDate(MIN_YEAR); public static final FDate MAX_DATE = FDateBuilder.newDate(MAX_YEAR); public static final int COUNT_NANOSECONDS_IN_MILLISECOND = Integers.checkedCast(FTimeUnit.MILLISECONDS.toNanos(1)); public static final int COUNT_NANOSECONDS_IN_MICROSECOND = Integers.checkedCast(FTimeUnit.MICROSECONDS.toNanos(1)); public static final int COUNT_WEEKEND_DAYS_IN_WEEK = 2; public static final int COUNT_WORKDAYS_IN_YEAR = FTimeUnit.DAYS_IN_YEAR - FTimeUnit.WEEKS_IN_YEAR * COUNT_WEEKEND_DAYS_IN_WEEK; public static final int COUNT_WORKDAYS_IN_MONTH = COUNT_WORKDAYS_IN_YEAR / FTimeUnit.MONTHS_IN_YEAR; public static final int COUNT_WORKDAYS_IN_WEEK = COUNT_WORKDAYS_IN_MONTH / FTimeUnit.WEEKS_IN_MONTH; /** * https://en.wikipedia.org/wiki/Trading_day */ public static final int COUNT_TRADING_HOLIDAYS_PER_YEAR = 9; public static final int COUNT_TRADING_DAYS_IN_YEAR = COUNT_WORKDAYS_IN_YEAR - COUNT_TRADING_HOLIDAYS_PER_YEAR; public static final int COUNT_DAYS_WITHOUT_TRADING_IN_YEAR = FTimeUnit.DAYS_IN_YEAR - COUNT_TRADING_DAYS_IN_YEAR; public static final Percent PERCENT_DAYS_WITHOUT_TRADING_IN_YEAR = new Percent( FDate.COUNT_DAYS_WITHOUT_TRADING_IN_YEAR, FTimeUnit.DAYS_IN_YEAR); public static final Percent PERCENT_TRADING_DAYS_IN_YEAR = new Percent(FDate.COUNT_TRADING_DAYS_IN_YEAR, FTimeUnit.DAYS_IN_YEAR); /* * ISO 8601 date-time format, example: "2003-04-01T13:01:02" */ public static final String FORMAT_ISO_DATE = "yyyy-MM-dd"; public static final String FORMAT_ISO_TIME = "HH:mm:ss"; public static final String FORMAT_ISO_TIME_MS = FORMAT_ISO_TIME + ".SSS"; public static final String FORMAT_ISO_DATE_TIME = FORMAT_ISO_DATE + "'T'" + FORMAT_ISO_TIME; public static final String FORMAT_ISO_DATE_TIME_SPACE = FORMAT_ISO_DATE + " " + FORMAT_ISO_TIME; public static final String FORMAT_ISO_DATE_TIME_MS = FORMAT_ISO_DATE + "'T'" + FORMAT_ISO_TIME_MS; public static final String FORMAT_TIMESTAMP_NUMBER = "yyyyMMddHHmmssSSS"; public static final String FORMAT_TIMESTAMP_UNDERSCORE = "yyyy_MM_dd_HH_mm_ss_SSS"; public static final String FORMAT_GERMAN_DATE = "dd.MM.yyyy"; public static final String FORMAT_GERMAN_DATE_TIME = FORMAT_GERMAN_DATE + " " + FORMAT_ISO_TIME; public static final ADelegateComparator<FDate> DATE_COMPARATOR = new ADelegateComparator<FDate>() { @Override protected Comparable<?> getCompareCriteria(final FDate e) { return e; } }; private final long millis; private final int hashCode; public FDate() { this(System.currentTimeMillis()); } public FDate(final long millis) { this.millis = millis; this.hashCode = Long.hashCode(millis); } protected FDate(final FDate date) { this.millis = date.millis; this.hashCode = date.hashCode; } public FDate(final ReadableDateTime jodaTime) { this(jodaTime.getMillis()); } public FDate(final LocalDateTime jodaTime) { this(jodaTime.toDateTime().getMillis()); } public FDate(final Calendar calendar) { this(calendar.getTime()); } public FDate(final Date date) { this(date.getTime()); } public int getYear() { return get(FDateField.Year); } public int getMonth() { //no conversion needed since joda time has same index return get(FDateField.Month); } public FMonth getFMonth() { return FMonth.valueOfIndex(getMonth()); } public int getDay() { return get(FDateField.Day); } public int getWeekday() { //no conversion needed since joda time has same index return get(FDateField.Weekday); } public FWeekday getFWeekday() { return FWeekday.valueOfIndex(getWeekday()); } public int getHour() { return get(FDateField.Hour); } public int getMinute() { return get(FDateField.Minute); } public int getSecond() { return get(FDateField.Second); } public int getMillisecond() { return get(FDateField.Millisecond); } public TimeZone getTimeZone() { return TimeZone.getDefault(); } public FDate setYear(final int year) { return set(FDateField.Year, year); } public FDate setMonth(final int month) { return set(FDateField.Month, month); } public FDate setFMonth(final FMonth month) { return setMonth(month.jodaTimeValue()); } public FDate setDay(final int day) { return set(FDateField.Day, day); } public FDate setWeekday(final int weekday) { return set(FDateField.Weekday, weekday); } public FDate setFWeekday(final FWeekday weekday) { return setWeekday(weekday.jodaTimeValue()); } public FDate setHour(final int hour) { return set(FDateField.Hour, hour); } public FDate setMinute(final int minute) { return set(FDateField.Minute, minute); } public FDate setSecond(final int second) { return set(FDateField.Second, second); } public FDate setMillisecond(final int millisecond) { return set(FDateField.Millisecond, millisecond); } public FDate addYears(final int years) { return add(FTimeUnit.YEARS, years); } public FDate addMonths(final int months) { return add(FTimeUnit.MONTHS, months); } public FDate addDays(final int days) { return add(FTimeUnit.DAYS, days); } public FDate addWeeks(final int weeks) { return addDays(weeks * FTimeUnit.DAYS_IN_WEEK); } public FDate addHours(final int hours) { return add(FTimeUnit.HOURS, hours); } public FDate addMinutes(final int minutes) { return add(FTimeUnit.MINUTES, minutes); } public FDate addSeconds(final int seconds) { return add(FTimeUnit.SECONDS, seconds); } public FDate addMilliseconds(final long milliseconds) { return new FDate(millis + milliseconds); } public int get(final FDateField field) { final MutableDateTime delegate = newMutableDateTime(); return delegate.get(field.jodaTimeValue()); } public FDate set(final FDateField field, final int value) { final MutableDateTime delegate = newMutableDateTime(); delegate.set(field.jodaTimeValue(), value); return new FDate(delegate); } public FDate add(final FTimeUnit field, final int amount) { final MutableDateTime delegate = newMutableDateTime(); final int usedAmount; final DurationFieldType usedField; switch (field) { case MILLENIA: usedField = FTimeUnit.YEARS.jodaTimeValue(); usedAmount = amount * FTimeUnit.YEARS_IN_MILLENIUM; break; case CENTURIES: usedField = FTimeUnit.YEARS.jodaTimeValue(); usedAmount = amount * FTimeUnit.YEARS_IN_CENTURY; break; case DECADES: usedField = FTimeUnit.YEARS.jodaTimeValue(); usedAmount = amount * FTimeUnit.YEARS_IN_DECADE; break; default: usedField = field.jodaTimeValue(); usedAmount = amount; break; } delegate.add(usedField, usedAmount); return new FDate(delegate); } public FDate add(final Duration duration) { return duration.addTo(this); } public FDate subtract(final Duration duration) { return duration.subtractFrom(this); } public FDate truncate(final FDateField field) { final MutableDateTime delegate = newMutableDateTime(); delegate.setRounding(field.jodaTimeValue().getField(delegate.getChronology())); final FDate truncated = new FDate(delegate); return truncated; } public FDate truncate(final FTimeUnit timeUnit) { switch (timeUnit) { case MILLENIA: return truncate(FDateField.Year).addYears(getYear() % FTimeUnit.YEARS_IN_MILLENIUM); case CENTURIES: return truncate(FDateField.Year).addYears(getYear() % FTimeUnit.YEARS_IN_CENTURY); case DECADES: return truncate(FDateField.Year).addYears(getYear() % FTimeUnit.YEARS_IN_DECADE); case YEARS: return truncate(FDateField.Year); case MONTHS: return truncate(FDateField.Month); case WEEKS: return setFWeekday(FWeekday.Monday); case DAYS: return truncate(FDateField.Day); case HOURS: return truncate(FDateField.Hour); case MINUTES: return truncate(FDateField.Minute); case SECONDS: return truncate(FDateField.Second); case MILLISECONDS: return truncate(FDateField.Millisecond); default: throw UnknownArgumentException.newInstance(FTimeUnit.class, timeUnit); } } /** * sets hour, minute, second and millisecond each to 0. */ public FDate withoutTime() { return truncate(FDateField.Day); } /** * sets hour, minute, second and millisecond each to 23:59:999. */ public FDate atEndOfDay() { return withoutTime().addDays(1).addMilliseconds(-1); } /** * Returns the number of milliseconds since January 1, 1970, 00:00:00 GMT. */ public long millisValue() { return millis; } public Date dateValue() { return calendarValue().getTime(); } public long longValue(final FTimeUnit timeUnit) { return timeUnit.convert(millis, FTimeUnit.MILLISECONDS); } public Calendar calendarValue() { final Calendar cal = FDates.newCalendar(); cal.setTimeInMillis(millis); return cal; } public LocalDateTime jodaTimeValue() { return new LocalDateTime(millis); } public static FDate valueOf(final Long millis) { if (millis != null) { return new FDate(millis); } else { return null; } } public static FDate valueOf(final Long value, final FTimeUnit timeUnit) { if (value != null) { return new FDate(timeUnit.toMillis(value)); } else { return null; } } public static FDate valueOf(final Date date) { if (date != null) { return new FDate(date); } else { return null; } } public static FDate valueOf(final Calendar calendar) { if (calendar != null) { return new FDate(calendar); } else { return null; } } public static FDate valueOf(final ReadableDateTime jodaTime) { if (jodaTime != null) { return new FDate(jodaTime); } else { return null; } } public static FDate valueOf(final LocalDateTime jodaTime) { if (jodaTime != null) { return new FDate(jodaTime); } else { return null; } } public static FDate valueOf(final String str, final String... parsePatterns) { return valueOf(str, null, null, parsePatterns); } public static FDate valueOf(final String str, final String parsePattern) { return valueOf(str, null, null, parsePattern); } public static FDate valueOf(final String str, final Locale locale, final String... parsePatterns) { return valueOf(str, null, locale, parsePatterns); } public static FDate valueOf(final String str, final TimeZone timeZone, final String... parsePatterns) { return valueOf(str, timeZone, null, parsePatterns); } public static FDate valueOf(final String str, final TimeZone timeZone, final Locale locale, final String... parsePatterns) { if (parsePatterns == null || parsePatterns.length == 0) { throw new IllegalArgumentException("atleast one parsePattern is needed"); } for (final String parsePattern : parsePatterns) { try { return valueOf(str, timeZone, locale, parsePattern); } catch (final IllegalArgumentException e) { continue; } } throw new IllegalArgumentException("None of the parsePatterns [" + Arrays.toString(parsePatterns) + "] matches the date string [" + str + "]"); } public static FDate valueOf(final String str, final TimeZone timeZone, final String parsePattern) { return valueOf(str, timeZone, null, parsePattern); } public static FDate valueOf(final String str, final Locale locale, final String parsePattern) { return valueOf(str, null, locale, parsePattern); } public static FDate valueOf(final String str, final TimeZone timeZone, final Locale locale, final String parsePattern) { if (Strings.isBlank(str)) { return null; } DateTimeFormatter df = DateTimeFormat.forPattern(parsePattern); if (timeZone != null) { df = df.withZone(DateTimeZone.forTimeZone(timeZone)); } if (locale != null) { df = df.withLocale(locale); } final DateTime date = df.parseDateTime(str); return new FDate(date); } public static List<FDate> valueOf(final Collection<Date> list) { final List<FDate> dates = new ArrayList<FDate>(list.size()); for (final Date e : list) { dates.add(valueOf(e)); } return dates; } public static List<FDate> valueOf(final Date... list) { final List<FDate> dates = new ArrayList<FDate>(list.length); for (final Date e : list) { dates.add(valueOf(e)); } return dates; } public static FDate now() { return new FDate(); } public static FDate today() { return now().withoutTime(); } @Override public int hashCode() { return hashCode; } @Override public int compareTo(final Object o) { if (o instanceof FDate) { final FDate cO = (FDate) o; return Long.compare(millis, cO.millis); } else { return 1; } } @Override public boolean equals(final Object obj) { if (obj instanceof FDate) { final FDate cObj = (FDate) obj; return millis == cObj.millis; } else { return false; } } @Override public String toString() { return toString(FORMAT_ISO_DATE_TIME_MS); } public String toString(final TimeZone timeZone) { return toString(FORMAT_ISO_DATE_TIME_MS, timeZone); } public String toString(final String format) { return toString(format, null); } public String toString(final String format, final TimeZone timeZone) { final MutableDateTime delegate = newMutableDateTime(); DateTimeFormatter df = DateTimeFormat.forPattern(format); if (timeZone != null) { df = df.withZone(DateTimeZone.forTimeZone(timeZone)); } else { df = df.withZone(FDates.getDefaultDateTimeZone()); } return df.print(delegate); } private MutableDateTime newMutableDateTime() { return new MutableDateTime(millis, FDates.getDefaultDateTimeZone()); } public boolean isBefore(final FDate other) { return other != null && compareTo(other) < 0; } public boolean isBeforeOrEqualTo(final FDate other) { return other != null && !isAfter(other); } public boolean isAfter(final FDate other) { return other != null && compareTo(other) > 0; } public boolean isAfterOrEqualTo(final FDate other) { return other != null && !isBefore(other); } //CHECKSTYLE:OFF @Override public FDate clone() { return new FDate(millis); } //CHECKSTYLE:ON public FDate getFirstWeekdayOfMonth(final FWeekday weekday) { final FDate firstWeekDay = withoutTime().setDay(1).setFWeekday(weekday); if (!FDates.isSameMonth(this, firstWeekDay)) { return firstWeekDay.addDays(FTimeUnit.DAYS_IN_WEEK); } else { return firstWeekDay; } } public FDate getFirstWorkdayOfMonth(final FDate key, final HolidayCalendar holidayCalendar) { FDate firstWorkdayDay = withoutTime().setDay(1); while (firstWorkdayDay.getFWeekday().isWeekend() || firstWorkdayDay.isHoliday(holidayCalendar)) { firstWorkdayDay = firstWorkdayDay.addDays(1); } return firstWorkdayDay; } public boolean isHoliday(final HolidayCalendar holidayCalendar) { if (holidayCalendar != null) { final HolidayManager holidayManager = HolidayManager.getInstance(ManagerParameters.create(holidayCalendar)); return holidayManager.isHoliday(calendarValue(), HolidayType.OFFICIAL_HOLIDAY); } else { return false; } } public FDate addWorkdays(final int workdays, final HolidayCalendar holidayCalendar) { int workdaysToShift = Math.abs(workdays); if (getFWeekday().isWeekend() || isHoliday(holidayCalendar)) { if (workdaysToShift > 1) { workdaysToShift--; } } final int shiftUnit; if (workdays >= 0) { shiftUnit = 1; } else { shiftUnit = -1; } int workdaysShifted = 0; FDate cur = this; while (workdaysShifted < workdaysToShift) { if (!cur.getFWeekday().isWeekend() && !cur.isHoliday(holidayCalendar)) { workdaysShifted++; } cur = cur.addDays(shiftUnit); } return cur; } }