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  import io.github.tonywasher.joceanus.oceanus.convert.OceanusDataConverter;
21  import io.github.tonywasher.joceanus.oceanus.event.OceanusEventManager;
22  import io.github.tonywasher.joceanus.oceanus.event.OceanusEventRegistrar;
23  import io.github.tonywasher.joceanus.oceanus.event.OceanusEventRegistrar.OceanusEventProvider;
24  
25  import java.text.ParseException;
26  import java.text.SimpleDateFormat;
27  import java.time.LocalDate;
28  import java.time.format.DateTimeFormatter;
29  import java.time.format.DateTimeParseException;
30  import java.util.Calendar;
31  import java.util.Date;
32  import java.util.Locale;
33  
34  /**
35   * Formatter for Date objects.
36   *
37   * @author Tony Washer
38   */
39  public class OceanusDateFormatter
40          implements OceanusEventProvider<OceanusDateEvent> {
41      /**
42       * Date Byte length.
43       */
44      public static final int BYTE_LEN = Long.BYTES;
45  
46      /**
47       * As of Java 16.0.2 the short format for September is Sept instead of Sep.
48       */
49      private static final int PATCH_JAVA_VER = 16;
50  
51      /**
52       * As of Java 16.0.2 the short format for September is Sept instead of Sep.
53       */
54      private static final String PATCH_SEPT_NEW = "-Sept-";
55  
56      /**
57       * As of Java 16.0.2 the short format for September is Sept instead of Sep.
58       */
59      private static final String PATCH_SEPT_OLD = "-Sep-";
60  
61      /**
62       * The default format.
63       */
64      private static final String DEFAULT_FORMAT = "dd-MMM-yyyy";
65  
66      /**
67       * One hundred years.
68       */
69      private static final int YEARS_CENTURY = 100;
70  
71      /**
72       * The Event Manager.
73       */
74      private final OceanusEventManager<OceanusDateEvent> theEventManager;
75  
76      /**
77       * The locale.
78       */
79      private Locale theLocale;
80  
81      /**
82       * The Simple Date format for the locale and format string.
83       */
84      private String theFormat;
85  
86      /**
87       * The Simple Date format for the locale and format string.
88       */
89      private SimpleDateFormat theDateFormat;
90  
91      /**
92       * The DateTime format for the locale and format string.
93       */
94      private DateTimeFormatter theLocalDateFormat;
95  
96      /**
97       * Constructor.
98       */
99      public OceanusDateFormatter() {
100         /* Use default locale */
101         this(OceanusLocale.getDefaultLocale());
102     }
103 
104     /**
105      * Constructor.
106      *
107      * @param pLocale the locale
108      */
109     public OceanusDateFormatter(final Locale pLocale) {
110         /* Store locale */
111         theLocale = pLocale;
112         theEventManager = new OceanusEventManager<>();
113         setFormat(DEFAULT_FORMAT);
114     }
115 
116     @Override
117     public OceanusEventRegistrar<OceanusDateEvent> getEventRegistrar() {
118         return theEventManager.getEventRegistrar();
119     }
120 
121     /**
122      * Set the date format.
123      *
124      * @param pFormat the format string
125      */
126     public final void setFormat(final String pFormat) {
127         /* If the format is the same */
128         if (pFormat.equals(theFormat)) {
129             /* Ignore */
130             return;
131         }
132 
133         /* Create the simple date format */
134         theFormat = pFormat;
135         theDateFormat = new SimpleDateFormat(theFormat, theLocale);
136         theLocalDateFormat = DateTimeFormatter.ofPattern(theFormat, theLocale);
137 
138         /* Notify of the change */
139         theEventManager.fireEvent(OceanusDateEvent.FORMATCHANGED);
140     }
141 
142     /**
143      * Set the locale.
144      *
145      * @param pLocale the locale
146      */
147     public final void setLocale(final Locale pLocale) {
148         /* If the locale is the same */
149         if (theLocale.equals(pLocale)) {
150             /* Ignore */
151             return;
152         }
153 
154         /* Store the locale */
155         theLocale = pLocale;
156         final String pFormat = theFormat;
157         theFormat = null;
158         setFormat(pFormat);
159     }
160 
161     /**
162      * Format a calendar Date.
163      *
164      * @param pDate the date to format
165      * @return the formatted date
166      */
167     public String formatCalendarDay(final Calendar pDate) {
168         /* Handle null */
169         if (pDate == null) {
170             return null;
171         }
172 
173         /* Format the date */
174         return formatJavaDate(pDate.getTime());
175     }
176 
177     /**
178      * Format a local Date.
179      *
180      * @param pDate the date to format
181      * @return the formatted date
182      */
183     public String formatLocalDate(final LocalDate pDate) {
184         /* Handle null */
185         if (pDate == null) {
186             return null;
187         }
188 
189         /* Format the date */
190         return pDate.format(theLocalDateFormat);
191     }
192 
193     /**
194      * Format a java Date.
195      *
196      * @param pDate the date to format
197      * @return the formatted date
198      */
199     public String formatJavaDate(final Date pDate) {
200         /* Handle null */
201         if (pDate == null) {
202             return null;
203         }
204 
205         /* Format the date */
206         return theDateFormat.format(pDate);
207     }
208 
209     /**
210      * Format a Date.
211      *
212      * @param pDate the date to format
213      * @return the formatted date
214      */
215     public String formatDate(final OceanusDate pDate) {
216         /* Handle null */
217         if (pDate == null) {
218             return null;
219         }
220 
221         /* Format the date */
222         return formatLocalDate(pDate.getDate());
223     }
224 
225     /**
226      * Format a DateRange.
227      *
228      * @param pRange the range to format
229      * @return the formatted date
230      */
231     public String formatDateRange(final OceanusDateRange pRange) {
232         /* Handle null */
233         if (pRange == null) {
234             return null;
235         }
236 
237         /* Access components */
238         final OceanusDate myStart = pRange.getStart();
239         final OceanusDate myEnd = pRange.getEnd();
240 
241         /* Build range description */
242         return ((myStart == null)
243                 ? OceanusDateRange.DESC_UNBOUNDED
244                 : formatDate(myStart))
245                 + OceanusDateRange.CHAR_BLANK
246                 + OceanusDateRange.DESC_LINK
247                 + OceanusDateRange.CHAR_BLANK
248                 + ((myEnd == null)
249                 ? OceanusDateRange.DESC_UNBOUNDED
250                 : formatDate(myEnd));
251     }
252 
253     /**
254      * Parse Java Date.
255      *
256      * @param pValue Formatted Date
257      * @return the Date
258      * @throws IllegalArgumentException on error
259      */
260     public Date parseJavaDate(final String pValue) {
261         /* Parse the date */
262         try {
263             return theDateFormat.parse(pValue);
264         } catch (ParseException e) {
265             throw new IllegalArgumentException("Invalid date: "
266                     + pValue, e);
267         }
268     }
269 
270     /**
271      * Parse LocalDate.
272      *
273      * @param pValue Formatted Date
274      * @return the Date
275      * @throws IllegalArgumentException on error
276      */
277     public LocalDate parseLocalDate(final String pValue) {
278         /* Parse the date */
279         try {
280             return LocalDate.parse(pValue, theLocalDateFormat);
281         } catch (DateTimeParseException e) {
282             /* Handle Patch for Java 16.0.2 change for September */
283             final int myVersion = Runtime.version().feature();
284             if (pValue.contains(PATCH_SEPT_OLD)
285                     && myVersion >= PATCH_JAVA_VER) {
286                 return parsePatchedLocalDate(pValue.replace(PATCH_SEPT_OLD, PATCH_SEPT_NEW));
287             }
288             if (pValue.contains(PATCH_SEPT_NEW)
289                     && Runtime.version().feature() < PATCH_JAVA_VER) {
290                 return parsePatchedLocalDate(pValue.replace(PATCH_SEPT_NEW, PATCH_SEPT_OLD));
291             }
292 
293             /* throw exception */
294             throw new IllegalArgumentException("Invalid date: "
295                     + pValue, e);
296         }
297     }
298 
299     /**
300      * Parse LocalDate.
301      *
302      * @param pValue Formatted Date
303      * @return the Date
304      * @throws IllegalArgumentException on error
305      */
306     private LocalDate parsePatchedLocalDate(final String pValue) {
307         /* Parse the date */
308         try {
309             return LocalDate.parse(pValue, theLocalDateFormat);
310         } catch (DateTimeParseException e) {
311             /* throw exception */
312             throw new IllegalArgumentException("Invalid date: "
313                     + pValue, e);
314         }
315     }
316 
317     /**
318      * Parse CalendarDay.
319      *
320      * @param pValue Formatted CalendarDay
321      * @return the CalendarDay
322      * @throws IllegalArgumentException on error
323      */
324     public Calendar parseCalendarDay(final String pValue) {
325         final Date myDate = parseJavaDate(pValue);
326         final Calendar myCalendar = Calendar.getInstance(theLocale);
327         myCalendar.setTime(myDate);
328         return myCalendar;
329     }
330 
331     /**
332      * Parse Date.
333      *
334      * @param pValue Formatted Date
335      * @return the DateDay
336      * @throws IllegalArgumentException on error
337      */
338     public OceanusDate parseDate(final String pValue) {
339         final LocalDate myDate = parseLocalDate(pValue);
340         return new OceanusDate(myDate);
341     }
342 
343     /**
344      * Parse Date using the passed year as base date.
345      * <p>
346      * This is used when a two digit year is utilised
347      *
348      * @param pValue    Formatted DateDay
349      * @param pBaseYear the base year
350      * @return the DateDay
351      * @throws IllegalArgumentException on error
352      */
353     public OceanusDate parseDateBase(final String pValue,
354                                      final int pBaseYear) {
355         LocalDate myDate = parseLocalDate(pValue);
356         if (myDate.getYear() >= pBaseYear + YEARS_CENTURY) {
357             myDate = myDate.minusYears(YEARS_CENTURY);
358         }
359         return new OceanusDate(myDate);
360     }
361 
362     /**
363      * Obtain the locale.
364      *
365      * @return the locale
366      */
367     public Locale getLocale() {
368         return theLocale;
369     }
370 
371     /**
372      * Create a byte array representation of a date.
373      *
374      * @param pDate the date to process
375      * @return the processed date
376      */
377     public byte[] toBytes(final OceanusDate pDate) {
378         final long myEpoch = pDate.getDate().toEpochDay();
379         return OceanusDataConverter.longToByteArray(myEpoch);
380     }
381 
382     /**
383      * Parse a byte array representation of a date.
384      *
385      * @param pBuffer the byte representation
386      * @return the date
387      */
388     public OceanusDate fromBytes(final byte[] pBuffer) {
389         if (pBuffer == null || pBuffer.length < Long.BYTES) {
390             throw new IllegalArgumentException();
391         }
392         final long myEpoch = OceanusDataConverter.byteArrayToLong(pBuffer);
393         final LocalDate myDate = LocalDate.ofEpochDay(myEpoch);
394         return new OceanusDate(myDate);
395     }
396 }