View Javadoc
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 }