OceanusDate.java
/*******************************************************************************
* Oceanus: Java Utilities
* Copyright 2012,2025 Tony Washer
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package net.sourceforge.joceanus.oceanus.date;
import java.time.DayOfWeek;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
/**
* Represents a Date object that is fixed to a particular day. There is no concept of time within
* the day Calendar objects that are built to represent the Date are set to noon on the day in
* question.
*/
public class OceanusDate
implements Comparable<OceanusDate> {
/**
* The Hash prime.
*/
protected static final int HASH_PRIME = 17;
/**
* The Year shift for DateDay Id. This is 9 corresponding to (1 shiftLeft 9 places) = 512
*/
protected static final int SHIFT_ID_YEAR = 9;
/**
* The Number of months in a Quarter.
*/
protected static final int MONTHS_IN_QUARTER = 3;
/**
* Text for Null Date Error.
*/
private static final String ERROR_NULLDATE = OceanusDateResource.ERROR_NULLDATE.getValue();
/**
* Text for Null Locale Error.
*/
private static final String ERROR_NULLLOCALE = OceanusDateResource.ERROR_NULLLOCALE.getValue();
/**
* Text for Bad Format Error.
*/
private static final String ERROR_BADFORMAT = OceanusDateResource.ERROR_BADFORMAT.getValue();
/**
* The format to be used.
*/
private static final String FORMAT_DEFAULT = "dd-MMM-yyyy";
/**
* The locale to be used.
*/
private Locale theLocale;
/**
* The format to be used.
*/
private String theFormat = FORMAT_DEFAULT;
/**
* The Simple Date format for the locale and format string.
*/
private DateTimeFormatter theDateFormat;
/**
* The Date format.
*/
private String theFormattedDate;
/**
* The Date object in underlying Java form.
*/
private LocalDate theDate;
/**
* The year of the date.
*/
private int theYear;
/**
* The month of the date.
*/
private int theMonth;
/**
* The day of the date.
*/
private int theDay;
/**
* The day id.
*/
private int theId;
/**
* Construct a new Date and initialise with todays date.
*/
public OceanusDate() {
this(Locale.getDefault());
}
/**
* Construct a new Date and initialise with todays date.
* @param pLocale the locale
*/
public OceanusDate(final Locale pLocale) {
this(LocalDate.now(), pLocale);
}
/**
* Construct a new Date and initialise from a java date.
* @param pDate the java date to initialise from
*/
public OceanusDate(final LocalDate pDate) {
this(pDate, Locale.getDefault());
}
/**
* Construct a new Date and initialise from a java date.
* @param pDate the java date to initialise from
* @param pLocale the locale for this date
*/
public OceanusDate(final LocalDate pDate,
final Locale pLocale) {
buildDateDay(pDate, pLocale);
}
/**
* Construct a new Date and initialise from a java date.
* @param pDate the java calendar to initialise from
*/
public OceanusDate(final Date pDate) {
this(pDate, Locale.getDefault());
}
/**
* Construct a new Date and initialise from a java date.
* @param pDate the java date to initialise from
* @param pLocale the locale for this date
*/
public OceanusDate(final Date pDate,
final Locale pLocale) {
/* Null dates not allowed */
if (pDate == null) {
throw new IllegalArgumentException(ERROR_NULLDATE);
}
/* Create the Date */
final Instant myInstant = Instant.ofEpochMilli(pDate.getTime());
final LocalDateTime myDateTime = LocalDateTime.ofInstant(myInstant, ZoneId.systemDefault());
buildDateDay(myDateTime.toLocalDate(), pLocale);
}
/**
* Construct a new Date and initialise from a date.
* @param pDate the finance date to initialise from
*/
public OceanusDate(final OceanusDate pDate) {
/* Null dates not allowed */
if (pDate == null) {
throw new IllegalArgumentException(ERROR_NULLDATE);
}
/* Create the Date */
buildDateDay(pDate.getYear(), pDate.getMonth(), pDate.getDay(), pDate.getLocale());
}
/**
* Construct an explicit Date.
* @param pYear the year
* @param pMonth the month (1 to 12 etc)
* @param pDay the day of the month
*/
public OceanusDate(final int pYear,
final int pMonth,
final int pDay) {
this(pYear, pMonth, pDay, Locale.getDefault());
}
/**
* Construct an explicit Date.
* @param pYear the year
* @param pMonth the month (Month.JUNE etc)
* @param pDay the day of the month
*/
public OceanusDate(final int pYear,
final Month pMonth,
final int pDay) {
this(pYear, pMonth.getValue(), pDay);
}
/**
* Construct an explicit Date for a locale.
* @param pYear the year
* @param pMonth the month (1 to 12 etc)
* @param pDay the day of the month
* @param pLocale the locale for this date
*/
public OceanusDate(final int pYear,
final int pMonth,
final int pDay,
final Locale pLocale) {
buildDateDay(pYear, pMonth, pDay, pLocale);
}
/**
* Construct an explicit Date for a locale.
* @param pYear the year
* @param pMonth the month (Month.JUNE etc)
* @param pDay the day of the month
* @param pLocale the locale for this date
*/
public OceanusDate(final int pYear,
final Month pMonth,
final int pDay,
final Locale pLocale) {
this(pYear, pMonth.getValue(), pDay, pLocale);
}
/**
* Construct a Date from a formatted string.
* @param pValue the formatted string
*/
public OceanusDate(final String pValue) {
this(pValue, Locale.getDefault());
}
/**
* Construct a Date from a formatted string.
* @param pValue the formatted string
* @param pLocale the locale for this date
*/
public OceanusDate(final String pValue,
final Locale pLocale) {
/* Parse using default format */
this(pValue, pLocale, FORMAT_DEFAULT);
}
/**
* Construct a Date from a formatted string.
* @param pValue the formatted string
* @param pLocale the locale for this date
* @param pFormat the format to use for parsing
*/
public OceanusDate(final String pValue,
final Locale pLocale,
final String pFormat) {
/* Null dates not allowed */
if (pValue == null) {
throw new IllegalArgumentException(ERROR_NULLDATE);
}
try {
/* Access the date format */
theFormat = pFormat;
theDateFormat = DateTimeFormatter.ofPattern(theFormat, pLocale);
/* Parse and build the date */
final LocalDate myDate = LocalDate.parse(pValue, theDateFormat);
buildDateDay(myDate, pLocale);
} catch (DateTimeParseException e) {
throw new IllegalArgumentException(ERROR_BADFORMAT
+ " "
+ pValue, e);
}
}
/**
* Get the year of the date.
* @return the year of the date
*/
public int getYear() {
return theYear;
}
/**
* Get the month of the date.
* @return the month of the date
*/
public int getMonth() {
return theMonth;
}
/**
* Get the day of the date.
* @return the day of the date
*/
public int getDay() {
return theDay;
}
/**
* Get the day of the week.
* @return the day of the week
*/
public DayOfWeek getDayOfWeek() {
return theDate.getDayOfWeek();
}
/**
* Get the month value.
* @return the month value
*/
public Month getMonthValue() {
return theDate.getMonth();
}
/**
* Get the id of the date. This is a unique integer representation of the date usable as an id
* for the date.
* @return the id of the date
*/
public int getId() {
return theId;
}
/**
* Get the Date associated with this object.
* @return the date
*/
public LocalDate getDate() {
return theDate;
}
/**
* Get the locale associated with this object.
* @return the java locale
*/
public Locale getLocale() {
return theLocale;
}
/**
* Construct a date from a java date.
* @param pDate the java date to initialise from
* @param pLocale the locale for this date
*/
private void buildDateDay(final LocalDate pDate,
final Locale pLocale) {
/* Null dates not allowed */
if (pDate == null) {
throw new IllegalArgumentException(ERROR_NULLDATE);
}
/* Null locale not allowed */
if (pLocale == null) {
throw new IllegalArgumentException(ERROR_NULLLOCALE);
}
/* Build date values */
theLocale = pLocale;
theDate = pDate;
obtainValues();
}
/**
* Construct an explicit Date for a locale.
* @param pYear the year
* @param pMonth the month (1 to 12)
* @param pDay the day of the month
* @param pLocale the locale for this date
*/
private void buildDateDay(final int pYear,
final int pMonth,
final int pDay,
final Locale pLocale) {
/* Build the date day */
buildDateDay(LocalDate.of(pYear, pMonth, pDay), pLocale);
}
/**
* Set locale for the DateDay.
* @param pLocale the locale
*/
public void setLocale(final Locale pLocale) {
/* Record the locale */
theLocale = pLocale;
/* rebuild the date into the new locale */
buildDateDay(theYear, theMonth, theDay, pLocale);
/* Reset the date format */
theDateFormat = null;
}
/**
* Set the date format.
* @param pFormat the format string
*/
public void setFormat(final String pFormat) {
/* Store the format string */
theFormat = pFormat;
/* Reset the date format */
theDateFormat = null;
theFormattedDate = null;
}
/**
* Adjust the date by a number of years.
* @param iYear the number of years to adjust by
*/
public void adjustYear(final int iYear) {
theDate = theDate.plusYears(iYear);
obtainValues();
}
/**
* Adjust the date by a number of months.
* @param iMonth the number of months to adjust by
*/
public void adjustMonth(final int iMonth) {
theDate = theDate.plusMonths(iMonth);
obtainValues();
}
/**
* Adjust the date by a number of days.
* @param iDay the number of days to adjust by
*/
public void adjustDay(final int iDay) {
theDate = theDate.plusDays(iDay);
obtainValues();
}
/**
* Adjust the date by a determined amount.
* @param iField the field to adjust
* @param iUnits the number of units to adjust by
*/
public void adjustField(final TemporalUnit iField,
final int iUnits) {
theDate = theDate.plus(iUnits, iField);
obtainValues();
}
/**
* Adjust the date by a period in a forward direction.
* @param pPeriod the period to adjust by
*/
public void adjustForwardByPeriod(final OceanusDatePeriod pPeriod) {
if (pPeriod == OceanusDatePeriod.ALLDATES) {
return;
}
adjustField(pPeriod.getField(), pPeriod.getAmount(true));
}
/**
* Adjust the date by a period in a backward direction.
* @param pPeriod the period to adjust by
*/
public void adjustBackwardByPeriod(final OceanusDatePeriod pPeriod) {
if (pPeriod == OceanusDatePeriod.ALLDATES) {
return;
}
adjustField(pPeriod.getField(), pPeriod.getAmount(false));
}
/**
* Adjust the date to the start of the period.
* @param pPeriod the period to adjust by
*/
public void startPeriod(final OceanusDatePeriod pPeriod) {
switch (pPeriod) {
case CALENDARMONTH:
startCalendarMonth();
break;
case CALENDARQUARTER:
startCalendarQuarter();
break;
case CALENDARYEAR:
startCalendarYear();
break;
case FISCALYEAR:
startFiscalYear();
break;
default:
break;
}
}
/**
* Adjust the date to the end of the following month.
*/
public void endNextMonth() {
/* Move to the first of the current month */
theDate = theDate.withDayOfMonth(1);
/* Add two months and move back a day */
theDate = theDate.plusMonths(2);
theDate = theDate.minusDays(1);
obtainValues();
}
/**
* Adjust the date to the start of the month.
*/
public void startCalendarMonth() {
/* Move to the first of the current month */
theDate = theDate.withDayOfMonth(1);
obtainValues();
}
/**
* Adjust the date to the end of the month.
*/
public void endCalendarMonth() {
/* Move to the first of the next month and then one day before */
theDate = theDate.withDayOfMonth(1);
theDate = theDate.plusMonths(1);
theDate = theDate.minusDays(1);
obtainValues();
}
/**
* Adjust the date to the start of the quarter.
*/
public void startCalendarQuarter() {
/* Determine the month in quarter */
final int myMiQ = (theMonth - 1)
% MONTHS_IN_QUARTER;
/* Move to the first of the current month */
theDate = theDate.withDayOfMonth(1);
/* Move to the first of the quarter */
theDate = theDate.minusMonths(myMiQ);
obtainValues();
}
/**
* Adjust the date to the start of the year.
*/
public void startCalendarYear() {
/* Move to the first of the current year */
theDate = theDate.withDayOfMonth(1);
theDate = theDate.withMonth(Month.JANUARY.getValue());
obtainValues();
}
/**
* Adjust the date to the end of the year.
*/
public void endCalendarYear() {
/* Move to the first of the current year */
theDate = theDate.plusYears(1);
theDate = theDate.withDayOfMonth(1);
theDate = theDate.withMonth(Month.JANUARY.getValue());
theDate = theDate.minusDays(1);
obtainValues();
}
/**
* Adjust the date to the start of the fiscal year.
*/
public void startFiscalYear() {
/* Determine Fiscal year type */
final OceanusFiscalYear myFiscal = OceanusFiscalYear.determineFiscalYear(theLocale);
final int myMonth = myFiscal.getFirstMonth().getValue();
final int myDay = myFiscal.getFirstDay();
/* Determine which year we are in */
if (theMonth < myMonth
|| (theMonth == myMonth && theDay < myDay)) {
theDate = theDate.minusYears(1);
}
/* Move to the first of the current year */
theDate = theDate.withDayOfMonth(myDay);
theDate = theDate.withMonth(myMonth);
obtainValues();
}
/**
* Calculate the age that someone born on this date will be on a given date.
* @param pDate the date for which to calculate the age
* @return the age on that date
*/
public int ageOn(final OceanusDate pDate) {
/* Calculate the initial age assuming same date in year */
int myAge = pDate.theDate.getYear();
myAge -= theDate.getYear();
/* Check whether we are later in the year */
int myDelta = theDate.getMonthValue()
- pDate.theDate.getMonthValue();
if (myDelta == 0) {
myDelta = theDate.getDayOfMonth()
- pDate.theDate.getDayOfMonth();
}
/* If so then subtract one from the year */
if (myDelta > 0) {
myAge--;
}
/* Return to caller */
return myAge;
}
/**
* Calculate the days until the specified date.
* @param pDate the date for which to days until
* @return the days until that date
*/
public long daysUntil(final OceanusDate pDate) {
/* Calculate the initial age assuming same date in year */
return theDate.until(pDate.theDate, ChronoUnit.DAYS);
}
/**
* Copy a date from another DateDay.
* @param pDate the date to copy from
*/
public void copyDate(final OceanusDate pDate) {
buildDateDay(pDate.getDate(), theLocale);
obtainValues();
}
/**
* Obtain the year,month and day values from the date.
*/
private void obtainValues() {
/* Access date details */
theYear = theDate.getYear();
theMonth = theDate.getMonthValue();
theDay = theDate.getDayOfMonth();
/* Calculate the id (512*year + dayofYear) */
theId = (theYear << SHIFT_ID_YEAR)
+ theDate.getDayOfYear();
/* Reset formatted date */
theFormattedDate = null;
}
@Override
public String toString() {
/* If we already have a formatted date */
if (theFormattedDate != null) {
return theFormattedDate;
}
/* If we have not obtained the date format */
if (theDateFormat == null) {
/* Create the simple date format */
theDateFormat = DateTimeFormatter.ofPattern(theFormat, theLocale);
}
/* Format the date */
theFormattedDate = theDate.format(theDateFormat);
/* Return the date */
return theFormattedDate;
}
@Override
public int compareTo(final OceanusDate pThat) {
/* Handle trivial compares */
if (this.equals(pThat)) {
return 0;
} else if (pThat == null) {
return -1;
}
/* Compare the year, month and date */
int iDiff = theYear
- pThat.theYear;
if (iDiff != 0) {
return iDiff;
}
iDiff = theMonth
- pThat.theMonth;
if (iDiff != 0) {
return iDiff;
}
return theDay
- pThat.theDay;
}
/**
* Compare this date to a range.
* @param pRange the range to compare to
* @return -1 if date is before range, 0 if date is within range, 1 if date is after range
*/
public int compareToRange(final OceanusDateRange pRange) {
/* Check start of range */
final OceanusDate myStart = pRange.getStart();
if (myStart != null
&& compareTo(myStart) < 0) {
return -1;
}
/* Check end of range */
final OceanusDate myEnd = pRange.getEnd();
if (myEnd != null
&& compareTo(myEnd) > 0) {
return 1;
}
/* Must be within range */
return 0;
}
@Override
public boolean equals(final Object pThat) {
/* Handle the trivial cases */
if (this == pThat) {
return true;
}
if (pThat == null) {
return false;
}
/* Make sure that the object is a TethysDate */
if (pThat.getClass() != this.getClass()) {
return false;
}
/* Access the object as a TethysDate */
final OceanusDate myThat = (OceanusDate) pThat;
/* Check components */
return theYear == myThat.theYear
&& theMonth == myThat.theMonth
&& theDay == myThat.theDay;
}
@Override
public int hashCode() {
/* Calculate hash based on Year/Month/Day */
int iHash = theYear;
iHash *= HASH_PRIME;
iHash += theMonth + 1;
iHash *= HASH_PRIME;
iHash += theDay;
return iHash;
}
/**
* Convert the LocalDate to a Date.
* @return the associated date
*/
public Date toDate() {
final Instant myInstant = theDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant();
return Date.from(myInstant);
}
/**
* Convert the LocalDate to a Calendar.
* @return the Calendar
*/
public Calendar toCalendar() {
final Calendar myCalendar = Calendar.getInstance(theLocale);
myCalendar.setTime(toDate());
return myCalendar;
}
/**
* Determine whether two DateDay objects differ.
* @param pCurr The current Date
* @param pNew The new Date
* @return <code>true</code> if the objects differ, <code>false</code> otherwise
*/
public static boolean isDifferent(final OceanusDate pCurr,
final OceanusDate pNew) {
/* Handle case where current value is null */
if (pCurr == null) {
return pNew != null;
}
/* Handle case where new value is null */
if (pNew == null) {
return true;
}
/* Handle Standard cases */
return !pCurr.equals(pNew);
}
}