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.Clock;
22  import java.time.DayOfWeek;
23  import java.time.LocalDate;
24  import java.time.Month;
25  import java.time.format.DateTimeFormatter;
26  import java.time.format.DateTimeParseException;
27  import java.time.temporal.ChronoUnit;
28  import java.time.temporal.TemporalUnit;
29  import java.util.Locale;
30  
31  /**
32   * Represents a Date object that is fixed to a particular day. There is no concept of time within
33   * the day Calendar objects that are built to represent the Date are set to noon on the day in
34   * question.
35   */
36  public class OceanusDate
37          implements Comparable<OceanusDate> {
38      /**
39       * Standard clock.
40       */
41      public static final Clock CLOCK = Clock.systemDefaultZone();
42  
43      /**
44       * The Hash prime.
45       */
46      protected static final int HASH_PRIME = 17;
47  
48      /**
49       * The Year shift for DateDay Id. This is 9 corresponding to (1 shiftLeft 9 places) = 512
50       */
51      protected static final int SHIFT_ID_YEAR = 9;
52  
53      /**
54       * The Number of months in a Quarter.
55       */
56      protected static final int MONTHS_IN_QUARTER = 3;
57  
58      /**
59       * Text for Null Date Error.
60       */
61      private static final String ERROR_NULLDATE = OceanusDateResource.ERROR_NULLDATE.getValue();
62  
63      /**
64       * Text for Null Locale Error.
65       */
66      private static final String ERROR_NULLLOCALE = OceanusDateResource.ERROR_NULLLOCALE.getValue();
67  
68      /**
69       * Text for Bad Format Error.
70       */
71      private static final String ERROR_BADFORMAT = OceanusDateResource.ERROR_BADFORMAT.getValue();
72  
73      /**
74       * The format to be used.
75       */
76      private static final String FORMAT_DEFAULT = "dd-MMM-yyyy";
77  
78      /**
79       * The locale to be used.
80       */
81      private Locale theLocale;
82  
83      /**
84       * The format to be used.
85       */
86      private String theFormat = FORMAT_DEFAULT;
87  
88      /**
89       * The Simple Date format for the locale and format string.
90       */
91      private DateTimeFormatter theDateFormat;
92  
93      /**
94       * The Date format.
95       */
96      private String theFormattedDate;
97  
98      /**
99       * The Date object in underlying Java form.
100      */
101     private LocalDate theDate;
102 
103     /**
104      * The year of the date.
105      */
106     private int theYear;
107 
108     /**
109      * The month of the date.
110      */
111     private int theMonth;
112 
113     /**
114      * The day of the date.
115      */
116     private int theDay;
117 
118     /**
119      * The day id.
120      */
121     private int theId;
122 
123     /**
124      * Construct a new Date and initialise with today's date.
125      */
126     public OceanusDate() {
127         this(OceanusLocale.getDefaultLocale());
128     }
129 
130     /**
131      * Construct a new Date and initialise with today's date.
132      *
133      * @param pLocale the locale
134      */
135     public OceanusDate(final Locale pLocale) {
136         this(LocalDate.now(CLOCK), pLocale);
137     }
138 
139     /**
140      * Construct a new Date and initialise from a java date.
141      *
142      * @param pDate the java date to initialise from
143      */
144     public OceanusDate(final LocalDate pDate) {
145         this(pDate, OceanusLocale.getDefaultLocale());
146     }
147 
148     /**
149      * Construct a new Date and initialise from a java date.
150      *
151      * @param pDate   the java date to initialise from
152      * @param pLocale the locale for this date
153      */
154     public OceanusDate(final LocalDate pDate,
155                        final Locale pLocale) {
156         buildDateDay(pDate, pLocale);
157     }
158 
159     /**
160      * Construct a new Date and initialise from a date.
161      *
162      * @param pDate the finance date to initialise from
163      */
164     public OceanusDate(final OceanusDate pDate) {
165         /* Null dates not allowed */
166         if (pDate == null) {
167             throw new IllegalArgumentException(ERROR_NULLDATE);
168         }
169 
170         /* Create the Date */
171         buildDateDay(pDate.getYear(), pDate.getMonth(), pDate.getDay(), pDate.getLocale());
172     }
173 
174     /**
175      * Construct an explicit Date.
176      *
177      * @param pYear  the year
178      * @param pMonth the month (1 to 12 etc)
179      * @param pDay   the day of the month
180      */
181     public OceanusDate(final int pYear,
182                        final int pMonth,
183                        final int pDay) {
184         this(pYear, pMonth, pDay, OceanusLocale.getDefaultLocale());
185     }
186 
187     /**
188      * Construct an explicit Date.
189      *
190      * @param pYear  the year
191      * @param pMonth the month (Month.JUNE etc)
192      * @param pDay   the day of the month
193      */
194     public OceanusDate(final int pYear,
195                        final Month pMonth,
196                        final int pDay) {
197         this(pYear, pMonth.getValue(), pDay);
198     }
199 
200     /**
201      * Construct an explicit Date for a locale.
202      *
203      * @param pYear   the year
204      * @param pMonth  the month (1 to 12 etc)
205      * @param pDay    the day of the month
206      * @param pLocale the locale for this date
207      */
208     public OceanusDate(final int pYear,
209                        final int pMonth,
210                        final int pDay,
211                        final Locale pLocale) {
212         buildDateDay(pYear, pMonth, pDay, pLocale);
213     }
214 
215     /**
216      * Construct an explicit Date for a locale.
217      *
218      * @param pYear   the year
219      * @param pMonth  the month (Month.JUNE etc)
220      * @param pDay    the day of the month
221      * @param pLocale the locale for this date
222      */
223     public OceanusDate(final int pYear,
224                        final Month pMonth,
225                        final int pDay,
226                        final Locale pLocale) {
227         this(pYear, pMonth.getValue(), pDay, pLocale);
228     }
229 
230     /**
231      * Construct a Date from a formatted string.
232      *
233      * @param pValue the formatted string
234      */
235     public OceanusDate(final String pValue) {
236         this(pValue, OceanusLocale.getDefaultLocale());
237     }
238 
239     /**
240      * Construct a Date from a formatted string.
241      *
242      * @param pValue  the formatted string
243      * @param pLocale the locale for this date
244      */
245     public OceanusDate(final String pValue,
246                        final Locale pLocale) {
247         /* Parse using default format */
248         this(pValue, pLocale, FORMAT_DEFAULT);
249     }
250 
251     /**
252      * Construct a Date from a formatted string.
253      *
254      * @param pValue  the formatted string
255      * @param pLocale the locale for this date
256      * @param pFormat the format to use for parsing
257      */
258     public OceanusDate(final String pValue,
259                        final Locale pLocale,
260                        final String pFormat) {
261         /* Null dates not allowed */
262         if (pValue == null) {
263             throw new IllegalArgumentException(ERROR_NULLDATE);
264         }
265 
266         try {
267             /* Access the date format */
268             theFormat = pFormat;
269             theDateFormat = DateTimeFormatter.ofPattern(theFormat, pLocale);
270 
271             /* Parse and build the date */
272             final LocalDate myDate = LocalDate.parse(pValue, theDateFormat);
273             buildDateDay(myDate, pLocale);
274         } catch (DateTimeParseException e) {
275             throw new IllegalArgumentException(ERROR_BADFORMAT
276                     + " "
277                     + pValue, e);
278         }
279     }
280 
281     /**
282      * Get the year of the date.
283      *
284      * @return the year of the date
285      */
286     public int getYear() {
287         return theYear;
288     }
289 
290     /**
291      * Get the month of the date.
292      *
293      * @return the month of the date
294      */
295     public int getMonth() {
296         return theMonth;
297     }
298 
299     /**
300      * Get the day of the date.
301      *
302      * @return the day of the date
303      */
304     public int getDay() {
305         return theDay;
306     }
307 
308     /**
309      * Get the day of the week.
310      *
311      * @return the day of the week
312      */
313     public DayOfWeek getDayOfWeek() {
314         return theDate.getDayOfWeek();
315     }
316 
317     /**
318      * Get the month value.
319      *
320      * @return the month value
321      */
322     public Month getMonthValue() {
323         return theDate.getMonth();
324     }
325 
326     /**
327      * Get the id of the date. This is a unique integer representation of the date usable as an id
328      * for the date.
329      *
330      * @return the id of the date
331      */
332     public int getId() {
333         return theId;
334     }
335 
336     /**
337      * Get the Date associated with this object.
338      *
339      * @return the date
340      */
341     public LocalDate getDate() {
342         return theDate;
343     }
344 
345     /**
346      * Get the locale associated with this object.
347      *
348      * @return the java locale
349      */
350     public Locale getLocale() {
351         return theLocale;
352     }
353 
354     /**
355      * Construct a date from a java date.
356      *
357      * @param pDate   the java date to initialise from
358      * @param pLocale the locale for this date
359      */
360     private void buildDateDay(final LocalDate pDate,
361                               final Locale pLocale) {
362         /* Null dates not allowed */
363         if (pDate == null) {
364             throw new IllegalArgumentException(ERROR_NULLDATE);
365         }
366 
367         /* Null locale not allowed */
368         if (pLocale == null) {
369             throw new IllegalArgumentException(ERROR_NULLLOCALE);
370         }
371 
372         /* Build date values */
373         theLocale = pLocale;
374         theDate = pDate;
375         obtainValues();
376     }
377 
378     /**
379      * Construct an explicit Date for a locale.
380      *
381      * @param pYear   the year
382      * @param pMonth  the month (1 to 12)
383      * @param pDay    the day of the month
384      * @param pLocale the locale for this date
385      */
386     private void buildDateDay(final int pYear,
387                               final int pMonth,
388                               final int pDay,
389                               final Locale pLocale) {
390         /* Build the date day */
391         buildDateDay(LocalDate.of(pYear, pMonth, pDay), pLocale);
392     }
393 
394     /**
395      * Set locale for the DateDay.
396      *
397      * @param pLocale the locale
398      */
399     public void setLocale(final Locale pLocale) {
400         /* Record the locale */
401         theLocale = pLocale;
402 
403         /* rebuild the date into the new locale */
404         buildDateDay(theYear, theMonth, theDay, pLocale);
405 
406         /* Reset the date format */
407         theDateFormat = null;
408     }
409 
410     /**
411      * Set the date format.
412      *
413      * @param pFormat the format string
414      */
415     public void setFormat(final String pFormat) {
416         /* Store the format string */
417         theFormat = pFormat;
418 
419         /* Reset the date format */
420         theDateFormat = null;
421         theFormattedDate = null;
422     }
423 
424     /**
425      * Adjust the date by a number of years.
426      *
427      * @param iYear the number of years to adjust by
428      */
429     public void adjustYear(final int iYear) {
430         theDate = theDate.plusYears(iYear);
431         obtainValues();
432     }
433 
434     /**
435      * Adjust the date by a number of months.
436      *
437      * @param iMonth the number of months to adjust by
438      */
439     public void adjustMonth(final int iMonth) {
440         theDate = theDate.plusMonths(iMonth);
441         obtainValues();
442     }
443 
444     /**
445      * Adjust the date by a number of days.
446      *
447      * @param iDay the number of days to adjust by
448      */
449     public void adjustDay(final int iDay) {
450         theDate = theDate.plusDays(iDay);
451         obtainValues();
452     }
453 
454     /**
455      * Adjust the date by a determined amount.
456      *
457      * @param iField the field to adjust
458      * @param iUnits the number of units to adjust by
459      */
460     public void adjustField(final TemporalUnit iField,
461                             final int iUnits) {
462         theDate = theDate.plus(iUnits, iField);
463         obtainValues();
464     }
465 
466     /**
467      * Adjust the date by a period in a forward direction.
468      *
469      * @param pPeriod the period to adjust by
470      */
471     public void adjustForwardByPeriod(final OceanusDatePeriod pPeriod) {
472         if (pPeriod == OceanusDatePeriod.ALLDATES) {
473             return;
474         }
475         adjustField(pPeriod.getField(), pPeriod.getAmount(true));
476     }
477 
478     /**
479      * Adjust the date by a period in a backward direction.
480      *
481      * @param pPeriod the period to adjust by
482      */
483     public void adjustBackwardByPeriod(final OceanusDatePeriod pPeriod) {
484         if (pPeriod == OceanusDatePeriod.ALLDATES) {
485             return;
486         }
487         adjustField(pPeriod.getField(), pPeriod.getAmount(false));
488     }
489 
490     /**
491      * Calculate the days until the specified date.
492      *
493      * @param pDate the date for which to days until
494      * @return the days until that date
495      */
496     public long daysUntil(final OceanusDate pDate) {
497         /* Calculate the initial age assuming same date in year */
498         return theDate.until(pDate.theDate, ChronoUnit.DAYS);
499     }
500 
501     /**
502      * Copy a date from another DateDay.
503      *
504      * @param pDate the date to copy from
505      */
506     public void copyDate(final OceanusDate pDate) {
507         buildDateDay(pDate.getDate(), theLocale);
508         obtainValues();
509     }
510 
511     /**
512      * Obtain the year,month and day values from the date.
513      */
514     private void obtainValues() {
515         /* Access date details */
516         theYear = theDate.getYear();
517         theMonth = theDate.getMonthValue();
518         theDay = theDate.getDayOfMonth();
519 
520         /* Calculate the id (512*year + dayofYear) */
521         theId = (theYear << SHIFT_ID_YEAR)
522                 + theDate.getDayOfYear();
523 
524         /* Reset formatted date */
525         theFormattedDate = null;
526     }
527 
528     @Override
529     public String toString() {
530         /* If we already have a formatted date */
531         if (theFormattedDate != null) {
532             return theFormattedDate;
533         }
534 
535         /* If we have not obtained the date format */
536         if (theDateFormat == null) {
537             /* Create the simple date format */
538             theDateFormat = DateTimeFormatter.ofPattern(theFormat, theLocale);
539         }
540 
541         /* Format the date */
542         theFormattedDate = theDate.format(theDateFormat);
543 
544         /* Return the date */
545         return theFormattedDate;
546     }
547 
548     @Override
549     public int compareTo(final OceanusDate pThat) {
550         /* Handle trivial compares */
551         if (this.equals(pThat)) {
552             return 0;
553         } else if (pThat == null) {
554             return -1;
555         }
556 
557         /* Compare the year, month and date */
558         int iDiff = theYear
559                 - pThat.theYear;
560         if (iDiff != 0) {
561             return iDiff;
562         }
563         iDiff = theMonth
564                 - pThat.theMonth;
565         if (iDiff != 0) {
566             return iDiff;
567         }
568         return theDay
569                 - pThat.theDay;
570     }
571 
572     @Override
573     public boolean equals(final Object pThat) {
574         /* Handle the trivial cases */
575         if (this == pThat) {
576             return true;
577         }
578         if (pThat == null) {
579             return false;
580         }
581 
582         /* Make sure that the object is a TethysDate */
583         if (pThat.getClass() != this.getClass()) {
584             return false;
585         }
586 
587         /* Access the object as a TethysDate */
588         final OceanusDate myThat = (OceanusDate) pThat;
589 
590         /* Check components */
591         return theYear == myThat.theYear
592                 && theMonth == myThat.theMonth
593                 && theDay == myThat.theDay;
594     }
595 
596     @Override
597     public int hashCode() {
598         /* Calculate hash based on Year/Month/Day */
599         int iHash = theYear;
600         iHash *= HASH_PRIME;
601         iHash += theMonth + 1;
602         iHash *= HASH_PRIME;
603         iHash += theDay;
604         return iHash;
605     }
606 
607     /**
608      * Determine whether two DateDay objects differ.
609      *
610      * @param pCurr The current Date
611      * @param pNew  The new Date
612      * @return <code>true</code> if the objects differ, <code>false</code> otherwise
613      */
614     public static boolean isDifferent(final OceanusDate pCurr,
615                                       final OceanusDate pNew) {
616         /* Handle case where current value is null */
617         if (pCurr == null) {
618             return pNew != null;
619         }
620 
621         /* Handle case where new value is null */
622         if (pNew == null) {
623             return true;
624         }
625 
626         /* Handle Standard cases */
627         return !pCurr.equals(pNew);
628     }
629 }