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 }