1 /*
2 * Oceanus: Java Utilities
3 * Copyright 2012-2026. Tony Washer
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
6 * use this file except in compliance with the License. You may obtain a copy
7 * of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 * License for the specific language governing permissions and limitations under
15 * the License.
16 */
17 package io.github.tonywasher.joceanus.oceanus.date;
18
19 import io.github.tonywasher.joceanus.oceanus.base.OceanusLocale;
20
21 import java.time.DayOfWeek;
22 import java.time.Instant;
23 import java.time.LocalDate;
24 import java.time.LocalDateTime;
25 import java.time.Month;
26 import java.time.ZoneId;
27 import java.time.format.DateTimeFormatter;
28 import java.time.format.DateTimeParseException;
29 import java.time.temporal.ChronoUnit;
30 import java.time.temporal.TemporalUnit;
31 import java.util.Calendar;
32 import java.util.Date;
33 import java.util.Locale;
34
35 /**
36 * Represents a Date object that is fixed to a particular day. There is no concept of time within
37 * the day Calendar objects that are built to represent the Date are set to noon on the day in
38 * question.
39 */
40 public class OceanusDate
41 implements Comparable<OceanusDate> {
42 /**
43 * The Hash prime.
44 */
45 protected static final int HASH_PRIME = 17;
46
47 /**
48 * The Year shift for DateDay Id. This is 9 corresponding to (1 shiftLeft 9 places) = 512
49 */
50 protected static final int SHIFT_ID_YEAR = 9;
51
52 /**
53 * The Number of months in a Quarter.
54 */
55 protected static final int MONTHS_IN_QUARTER = 3;
56
57 /**
58 * Text for Null Date Error.
59 */
60 private static final String ERROR_NULLDATE = OceanusDateResource.ERROR_NULLDATE.getValue();
61
62 /**
63 * Text for Null Locale Error.
64 */
65 private static final String ERROR_NULLLOCALE = OceanusDateResource.ERROR_NULLLOCALE.getValue();
66
67 /**
68 * Text for Bad Format Error.
69 */
70 private static final String ERROR_BADFORMAT = OceanusDateResource.ERROR_BADFORMAT.getValue();
71
72 /**
73 * The format to be used.
74 */
75 private static final String FORMAT_DEFAULT = "dd-MMM-yyyy";
76
77 /**
78 * The locale to be used.
79 */
80 private Locale theLocale;
81
82 /**
83 * The format to be used.
84 */
85 private String theFormat = FORMAT_DEFAULT;
86
87 /**
88 * The Simple Date format for the locale and format string.
89 */
90 private DateTimeFormatter theDateFormat;
91
92 /**
93 * The Date format.
94 */
95 private String theFormattedDate;
96
97 /**
98 * The Date object in underlying Java form.
99 */
100 private LocalDate theDate;
101
102 /**
103 * The year of the date.
104 */
105 private int theYear;
106
107 /**
108 * The month of the date.
109 */
110 private int theMonth;
111
112 /**
113 * The day of the date.
114 */
115 private int theDay;
116
117 /**
118 * The day id.
119 */
120 private int theId;
121
122 /**
123 * Construct a new Date and initialise with today's date.
124 */
125 public OceanusDate() {
126 this(OceanusLocale.getDefaultLocale());
127 }
128
129 /**
130 * Construct a new Date and initialise with todays date.
131 *
132 * @param pLocale the locale
133 */
134 public OceanusDate(final Locale pLocale) {
135 this(LocalDate.now(), pLocale);
136 }
137
138 /**
139 * Construct a new Date and initialise from a java date.
140 *
141 * @param pDate the java date to initialise from
142 */
143 public OceanusDate(final LocalDate pDate) {
144 this(pDate, OceanusLocale.getDefaultLocale());
145 }
146
147 /**
148 * Construct a new Date and initialise from a java date.
149 *
150 * @param pDate the java date to initialise from
151 * @param pLocale the locale for this date
152 */
153 public OceanusDate(final LocalDate pDate,
154 final Locale pLocale) {
155 buildDateDay(pDate, pLocale);
156 }
157
158 /**
159 * Construct a new Date and initialise from a java date.
160 *
161 * @param pDate the java calendar to initialise from
162 */
163 public OceanusDate(final Date pDate) {
164 this(pDate, OceanusLocale.getDefaultLocale());
165 }
166
167 /**
168 * Construct a new Date and initialise from a java date.
169 *
170 * @param pDate the java date to initialise from
171 * @param pLocale the locale for this date
172 */
173 public OceanusDate(final Date pDate,
174 final Locale pLocale) {
175 /* Null dates not allowed */
176 if (pDate == null) {
177 throw new IllegalArgumentException(ERROR_NULLDATE);
178 }
179
180 /* Create the Date */
181 final Instant myInstant = Instant.ofEpochMilli(pDate.getTime());
182 final LocalDateTime myDateTime = LocalDateTime.ofInstant(myInstant, ZoneId.systemDefault());
183 buildDateDay(myDateTime.toLocalDate(), pLocale);
184 }
185
186 /**
187 * Construct a new Date and initialise from a date.
188 *
189 * @param pDate the finance date to initialise from
190 */
191 public OceanusDate(final OceanusDate pDate) {
192 /* Null dates not allowed */
193 if (pDate == null) {
194 throw new IllegalArgumentException(ERROR_NULLDATE);
195 }
196
197 /* Create the Date */
198 buildDateDay(pDate.getYear(), pDate.getMonth(), pDate.getDay(), pDate.getLocale());
199 }
200
201 /**
202 * Construct an explicit Date.
203 *
204 * @param pYear the year
205 * @param pMonth the month (1 to 12 etc)
206 * @param pDay the day of the month
207 */
208 public OceanusDate(final int pYear,
209 final int pMonth,
210 final int pDay) {
211 this(pYear, pMonth, pDay, OceanusLocale.getDefaultLocale());
212 }
213
214 /**
215 * Construct an explicit Date.
216 *
217 * @param pYear the year
218 * @param pMonth the month (Month.JUNE etc)
219 * @param pDay the day of the month
220 */
221 public OceanusDate(final int pYear,
222 final Month pMonth,
223 final int pDay) {
224 this(pYear, pMonth.getValue(), pDay);
225 }
226
227 /**
228 * Construct an explicit Date for a locale.
229 *
230 * @param pYear the year
231 * @param pMonth the month (1 to 12 etc)
232 * @param pDay the day of the month
233 * @param pLocale the locale for this date
234 */
235 public OceanusDate(final int pYear,
236 final int pMonth,
237 final int pDay,
238 final Locale pLocale) {
239 buildDateDay(pYear, pMonth, pDay, pLocale);
240 }
241
242 /**
243 * Construct an explicit Date for a locale.
244 *
245 * @param pYear the year
246 * @param pMonth the month (Month.JUNE etc)
247 * @param pDay the day of the month
248 * @param pLocale the locale for this date
249 */
250 public OceanusDate(final int pYear,
251 final Month pMonth,
252 final int pDay,
253 final Locale pLocale) {
254 this(pYear, pMonth.getValue(), pDay, pLocale);
255 }
256
257 /**
258 * Construct a Date from a formatted string.
259 *
260 * @param pValue the formatted string
261 */
262 public OceanusDate(final String pValue) {
263 this(pValue, OceanusLocale.getDefaultLocale());
264 }
265
266 /**
267 * Construct a Date from a formatted string.
268 *
269 * @param pValue the formatted string
270 * @param pLocale the locale for this date
271 */
272 public OceanusDate(final String pValue,
273 final Locale pLocale) {
274 /* Parse using default format */
275 this(pValue, pLocale, FORMAT_DEFAULT);
276 }
277
278 /**
279 * Construct a Date from a formatted string.
280 *
281 * @param pValue the formatted string
282 * @param pLocale the locale for this date
283 * @param pFormat the format to use for parsing
284 */
285 public OceanusDate(final String pValue,
286 final Locale pLocale,
287 final String pFormat) {
288 /* Null dates not allowed */
289 if (pValue == null) {
290 throw new IllegalArgumentException(ERROR_NULLDATE);
291 }
292
293 try {
294 /* Access the date format */
295 theFormat = pFormat;
296 theDateFormat = DateTimeFormatter.ofPattern(theFormat, pLocale);
297
298 /* Parse and build the date */
299 final LocalDate myDate = LocalDate.parse(pValue, theDateFormat);
300 buildDateDay(myDate, pLocale);
301 } catch (DateTimeParseException e) {
302 throw new IllegalArgumentException(ERROR_BADFORMAT
303 + " "
304 + pValue, e);
305 }
306 }
307
308 /**
309 * Get the year of the date.
310 *
311 * @return the year of the date
312 */
313 public int getYear() {
314 return theYear;
315 }
316
317 /**
318 * Get the month of the date.
319 *
320 * @return the month of the date
321 */
322 public int getMonth() {
323 return theMonth;
324 }
325
326 /**
327 * Get the day of the date.
328 *
329 * @return the day of the date
330 */
331 public int getDay() {
332 return theDay;
333 }
334
335 /**
336 * Get the day of the week.
337 *
338 * @return the day of the week
339 */
340 public DayOfWeek getDayOfWeek() {
341 return theDate.getDayOfWeek();
342 }
343
344 /**
345 * Get the month value.
346 *
347 * @return the month value
348 */
349 public Month getMonthValue() {
350 return theDate.getMonth();
351 }
352
353 /**
354 * Get the id of the date. This is a unique integer representation of the date usable as an id
355 * for the date.
356 *
357 * @return the id of the date
358 */
359 public int getId() {
360 return theId;
361 }
362
363 /**
364 * Get the Date associated with this object.
365 *
366 * @return the date
367 */
368 public LocalDate getDate() {
369 return theDate;
370 }
371
372 /**
373 * Get the locale associated with this object.
374 *
375 * @return the java locale
376 */
377 public Locale getLocale() {
378 return theLocale;
379 }
380
381 /**
382 * Construct a date from a java date.
383 *
384 * @param pDate the java date to initialise from
385 * @param pLocale the locale for this date
386 */
387 private void buildDateDay(final LocalDate pDate,
388 final Locale pLocale) {
389 /* Null dates not allowed */
390 if (pDate == null) {
391 throw new IllegalArgumentException(ERROR_NULLDATE);
392 }
393
394 /* Null locale not allowed */
395 if (pLocale == null) {
396 throw new IllegalArgumentException(ERROR_NULLLOCALE);
397 }
398
399 /* Build date values */
400 theLocale = pLocale;
401 theDate = pDate;
402 obtainValues();
403 }
404
405 /**
406 * Construct an explicit Date for a locale.
407 *
408 * @param pYear the year
409 * @param pMonth the month (1 to 12)
410 * @param pDay the day of the month
411 * @param pLocale the locale for this date
412 */
413 private void buildDateDay(final int pYear,
414 final int pMonth,
415 final int pDay,
416 final Locale pLocale) {
417 /* Build the date day */
418 buildDateDay(LocalDate.of(pYear, pMonth, pDay), pLocale);
419 }
420
421 /**
422 * Set locale for the DateDay.
423 *
424 * @param pLocale the locale
425 */
426 public void setLocale(final Locale pLocale) {
427 /* Record the locale */
428 theLocale = pLocale;
429
430 /* rebuild the date into the new locale */
431 buildDateDay(theYear, theMonth, theDay, pLocale);
432
433 /* Reset the date format */
434 theDateFormat = null;
435 }
436
437 /**
438 * Set the date format.
439 *
440 * @param pFormat the format string
441 */
442 public void setFormat(final String pFormat) {
443 /* Store the format string */
444 theFormat = pFormat;
445
446 /* Reset the date format */
447 theDateFormat = null;
448 theFormattedDate = null;
449 }
450
451 /**
452 * Adjust the date by a number of years.
453 *
454 * @param iYear the number of years to adjust by
455 */
456 public void adjustYear(final int iYear) {
457 theDate = theDate.plusYears(iYear);
458 obtainValues();
459 }
460
461 /**
462 * Adjust the date by a number of months.
463 *
464 * @param iMonth the number of months to adjust by
465 */
466 public void adjustMonth(final int iMonth) {
467 theDate = theDate.plusMonths(iMonth);
468 obtainValues();
469 }
470
471 /**
472 * Adjust the date by a number of days.
473 *
474 * @param iDay the number of days to adjust by
475 */
476 public void adjustDay(final int iDay) {
477 theDate = theDate.plusDays(iDay);
478 obtainValues();
479 }
480
481 /**
482 * Adjust the date by a determined amount.
483 *
484 * @param iField the field to adjust
485 * @param iUnits the number of units to adjust by
486 */
487 public void adjustField(final TemporalUnit iField,
488 final int iUnits) {
489 theDate = theDate.plus(iUnits, iField);
490 obtainValues();
491 }
492
493 /**
494 * Adjust the date by a period in a forward direction.
495 *
496 * @param pPeriod the period to adjust by
497 */
498 public void adjustForwardByPeriod(final OceanusDatePeriod pPeriod) {
499 if (pPeriod == OceanusDatePeriod.ALLDATES) {
500 return;
501 }
502 adjustField(pPeriod.getField(), pPeriod.getAmount(true));
503 }
504
505 /**
506 * Adjust the date by a period in a backward direction.
507 *
508 * @param pPeriod the period to adjust by
509 */
510 public void adjustBackwardByPeriod(final OceanusDatePeriod pPeriod) {
511 if (pPeriod == OceanusDatePeriod.ALLDATES) {
512 return;
513 }
514 adjustField(pPeriod.getField(), pPeriod.getAmount(false));
515 }
516
517 /**
518 * Adjust the date to the start of the period.
519 *
520 * @param pPeriod the period to adjust by
521 */
522 public void startPeriod(final OceanusDatePeriod pPeriod) {
523 switch (pPeriod) {
524 case CALENDARMONTH:
525 startCalendarMonth();
526 break;
527 case CALENDARQUARTER:
528 startCalendarQuarter();
529 break;
530 case CALENDARYEAR:
531 startCalendarYear();
532 break;
533 case FISCALYEAR:
534 startFiscalYear();
535 break;
536 default:
537 break;
538 }
539 }
540
541 /**
542 * Adjust the date to the end of the following month.
543 */
544 public void endNextMonth() {
545 /* Move to the first of the current month */
546 theDate = theDate.withDayOfMonth(1);
547
548 /* Add two months and move back a day */
549 theDate = theDate.plusMonths(2);
550 theDate = theDate.minusDays(1);
551 obtainValues();
552 }
553
554 /**
555 * Adjust the date to the start of the month.
556 */
557 public void startCalendarMonth() {
558 /* Move to the first of the current month */
559 theDate = theDate.withDayOfMonth(1);
560 obtainValues();
561 }
562
563 /**
564 * Adjust the date to the end of the month.
565 */
566 public void endCalendarMonth() {
567 /* Move to the first of the next month and then one day before */
568 theDate = theDate.withDayOfMonth(1);
569 theDate = theDate.plusMonths(1);
570 theDate = theDate.minusDays(1);
571 obtainValues();
572 }
573
574 /**
575 * Adjust the date to the start of the quarter.
576 */
577 public void startCalendarQuarter() {
578 /* Determine the month in quarter */
579 final int myMiQ = (theMonth - 1)
580 % MONTHS_IN_QUARTER;
581
582 /* Move to the first of the current month */
583 theDate = theDate.withDayOfMonth(1);
584
585 /* Move to the first of the quarter */
586 theDate = theDate.minusMonths(myMiQ);
587 obtainValues();
588 }
589
590 /**
591 * Adjust the date to the start of the year.
592 */
593 public void startCalendarYear() {
594 /* Move to the first of the current year */
595 theDate = theDate.withDayOfMonth(1);
596 theDate = theDate.withMonth(Month.JANUARY.getValue());
597 obtainValues();
598 }
599
600 /**
601 * Adjust the date to the end of the year.
602 */
603 public void endCalendarYear() {
604 /* Move to the first of the current year */
605 theDate = theDate.plusYears(1);
606 theDate = theDate.withDayOfMonth(1);
607 theDate = theDate.withMonth(Month.JANUARY.getValue());
608 theDate = theDate.minusDays(1);
609 obtainValues();
610 }
611
612 /**
613 * Adjust the date to the start of the fiscal year.
614 */
615 public void startFiscalYear() {
616 /* Determine Fiscal year type */
617 final OceanusFiscalYear myFiscal = OceanusFiscalYear.determineFiscalYear(theLocale);
618 final int myMonth = myFiscal.getFirstMonth().getValue();
619 final int myDay = myFiscal.getFirstDay();
620
621 /* Determine which year we are in */
622 if (theMonth < myMonth
623 || (theMonth == myMonth && theDay < myDay)) {
624 theDate = theDate.minusYears(1);
625 }
626
627 /* Move to the first of the current year */
628 theDate = theDate.withDayOfMonth(myDay);
629 theDate = theDate.withMonth(myMonth);
630 obtainValues();
631 }
632
633 /**
634 * Calculate the age that someone born on this date will be on a given date.
635 *
636 * @param pDate the date for which to calculate the age
637 * @return the age on that date
638 */
639 public int ageOn(final OceanusDate pDate) {
640 /* Calculate the initial age assuming same date in year */
641 int myAge = pDate.theDate.getYear();
642 myAge -= theDate.getYear();
643
644 /* Check whether we are later in the year */
645 int myDelta = theDate.getMonthValue()
646 - pDate.theDate.getMonthValue();
647 if (myDelta == 0) {
648 myDelta = theDate.getDayOfMonth()
649 - pDate.theDate.getDayOfMonth();
650 }
651
652 /* If so then subtract one from the year */
653 if (myDelta > 0) {
654 myAge--;
655 }
656
657 /* Return to caller */
658 return myAge;
659 }
660
661 /**
662 * Calculate the days until the specified date.
663 *
664 * @param pDate the date for which to days until
665 * @return the days until that date
666 */
667 public long daysUntil(final OceanusDate pDate) {
668 /* Calculate the initial age assuming same date in year */
669 return theDate.until(pDate.theDate, ChronoUnit.DAYS);
670 }
671
672 /**
673 * Copy a date from another DateDay.
674 *
675 * @param pDate the date to copy from
676 */
677 public void copyDate(final OceanusDate pDate) {
678 buildDateDay(pDate.getDate(), theLocale);
679 obtainValues();
680 }
681
682 /**
683 * Obtain the year,month and day values from the date.
684 */
685 private void obtainValues() {
686 /* Access date details */
687 theYear = theDate.getYear();
688 theMonth = theDate.getMonthValue();
689 theDay = theDate.getDayOfMonth();
690
691 /* Calculate the id (512*year + dayofYear) */
692 theId = (theYear << SHIFT_ID_YEAR)
693 + theDate.getDayOfYear();
694
695 /* Reset formatted date */
696 theFormattedDate = null;
697 }
698
699 @Override
700 public String toString() {
701 /* If we already have a formatted date */
702 if (theFormattedDate != null) {
703 return theFormattedDate;
704 }
705
706 /* If we have not obtained the date format */
707 if (theDateFormat == null) {
708 /* Create the simple date format */
709 theDateFormat = DateTimeFormatter.ofPattern(theFormat, theLocale);
710 }
711
712 /* Format the date */
713 theFormattedDate = theDate.format(theDateFormat);
714
715 /* Return the date */
716 return theFormattedDate;
717 }
718
719 @Override
720 public int compareTo(final OceanusDate pThat) {
721 /* Handle trivial compares */
722 if (this.equals(pThat)) {
723 return 0;
724 } else if (pThat == null) {
725 return -1;
726 }
727
728 /* Compare the year, month and date */
729 int iDiff = theYear
730 - pThat.theYear;
731 if (iDiff != 0) {
732 return iDiff;
733 }
734 iDiff = theMonth
735 - pThat.theMonth;
736 if (iDiff != 0) {
737 return iDiff;
738 }
739 return theDay
740 - pThat.theDay;
741 }
742
743 /**
744 * Compare this date to a range.
745 *
746 * @param pRange the range to compare to
747 * @return -1 if date is before range, 0 if date is within range, 1 if date is after range
748 */
749 public int compareToRange(final OceanusDateRange pRange) {
750 /* Check start of range */
751 final OceanusDate myStart = pRange.getStart();
752 if (myStart != null
753 && compareTo(myStart) < 0) {
754 return -1;
755 }
756
757 /* Check end of range */
758 final OceanusDate myEnd = pRange.getEnd();
759 if (myEnd != null
760 && compareTo(myEnd) > 0) {
761 return 1;
762 }
763
764 /* Must be within range */
765 return 0;
766 }
767
768 @Override
769 public boolean equals(final Object pThat) {
770 /* Handle the trivial cases */
771 if (this == pThat) {
772 return true;
773 }
774 if (pThat == null) {
775 return false;
776 }
777
778 /* Make sure that the object is a TethysDate */
779 if (pThat.getClass() != this.getClass()) {
780 return false;
781 }
782
783 /* Access the object as a TethysDate */
784 final OceanusDate myThat = (OceanusDate) pThat;
785
786 /* Check components */
787 return theYear == myThat.theYear
788 && theMonth == myThat.theMonth
789 && theDay == myThat.theDay;
790 }
791
792 @Override
793 public int hashCode() {
794 /* Calculate hash based on Year/Month/Day */
795 int iHash = theYear;
796 iHash *= HASH_PRIME;
797 iHash += theMonth + 1;
798 iHash *= HASH_PRIME;
799 iHash += theDay;
800 return iHash;
801 }
802
803 /**
804 * Convert the LocalDate to a Date.
805 *
806 * @return the associated date
807 */
808 public Date toDate() {
809 final Instant myInstant = theDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant();
810 return Date.from(myInstant);
811 }
812
813 /**
814 * Convert the LocalDate to a Calendar.
815 *
816 * @return the Calendar
817 */
818 public Calendar toCalendar() {
819 final Calendar myCalendar = Calendar.getInstance(theLocale);
820 myCalendar.setTime(toDate());
821 return myCalendar;
822 }
823
824 /**
825 * Determine whether two DateDay objects differ.
826 *
827 * @param pCurr The current Date
828 * @param pNew The new Date
829 * @return <code>true</code> if the objects differ, <code>false</code> otherwise
830 */
831 public static boolean isDifferent(final OceanusDate pCurr,
832 final OceanusDate pNew) {
833 /* Handle case where current value is null */
834 if (pCurr == null) {
835 return pNew != null;
836 }
837
838 /* Handle case where new value is null */
839 if (pNew == null) {
840 return true;
841 }
842
843 /* Handle Standard cases */
844 return !pCurr.equals(pNew);
845 }
846 }