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