001package com.hfg.datetime;
002
003
004import java.text.ParseException;
005import java.text.SimpleDateFormat;
006import java.time.Instant;
007import java.time.LocalDateTime;
008import java.time.ZoneId;
009import java.time.ZoneOffset;
010import java.time.ZonedDateTime;
011import java.time.format.DateTimeFormatter;
012import java.util.Calendar;
013import java.util.Date;
014import java.util.GregorianCalendar;
015import java.util.Map;
016import java.util.regex.Matcher;
017import java.util.regex.Pattern;
018
019import com.hfg.units.TimeUnit;
020import com.hfg.util.StringBuilderPlus;
021import com.hfg.util.collection.OrderedMap;
022
023
024//------------------------------------------------------------------------------
025/**
026 Date-related utility functions.
027 @author J. Alex Taylor, hairyfatguy.com
028 */
029//------------------------------------------------------------------------------
030// com.hfg XML/HTML Coding Library
031//
032// This library is free software; you can redistribute it and/or
033// modify it under the terms of the GNU Lesser General Public
034// License as published by the Free Software Foundation; either
035// version 2.1 of the License, or (at your option) any later version.
036//
037// This library is distributed in the hope that it will be useful,
038// but WITHOUT ANY WARRANTY; without even the implied warranty of
039// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
040// Lesser General Public License for more details.
041//
042// You should have received a copy of the GNU Lesser General Public
043// License along with this library; if not, write to the Free Software
044// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
045//
046// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
047// jataylor@hairyfatguy.com
048//------------------------------------------------------------------------------
049
050public class DateUtil
051{
052   /**
053    ISO 8601 datetime format. Ex: 2001-07-04T12:08:56.235-07:00
054    See <a href='http://www.w3.org/TR/NOTE-datetime'>http://www.w3.org/TR/NOTE-datetime</a>.
055    Not thread-safe on its own.
056    */
057   public static final DateTimeFormatter W3CDTF_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
058
059      
060   private static final ZoneId UTC_ZONE = ZoneId.of("UTC");
061   private static final DateTimeFormatter YYYYMMDD_FORMATTER            = DateTimeFormatter.ofPattern("yyyyMMdd");
062   private static final DateTimeFormatter YYYY_MM_DD_HH_mm_aa_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm a");
063   private static final DateTimeFormatter YYYY_MM_DD_HH_mm_ss_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
064   
065   private static Pattern sEscapeSequence = Pattern.compile("[^\\\\]((?:\\\\\\S)+)");
066
067   private static Map<String, String> sPhpSubstitutionMap = new OrderedMap<>(35);
068   static
069   {
070      sPhpSubstitutionMap.put("d", "dd");
071      sPhpSubstitutionMap.put("D", "EEE");
072      sPhpSubstitutionMap.put("j", "d");
073      sPhpSubstitutionMap.put("l", "E");
074      sPhpSubstitutionMap.put("N", "u");
075      sPhpSubstitutionMap.put("S", "");
076      sPhpSubstitutionMap.put("w", "");
077      sPhpSubstitutionMap.put("z", "D");
078      sPhpSubstitutionMap.put("W", "w");
079      sPhpSubstitutionMap.put("M", "MMM");
080      sPhpSubstitutionMap.put("F", "M");
081      sPhpSubstitutionMap.put("m", "MM");
082      sPhpSubstitutionMap.put("n", "MM");
083      sPhpSubstitutionMap.put("t", "");
084      sPhpSubstitutionMap.put("L", "");
085      sPhpSubstitutionMap.put("Y", "YYYY");
086      sPhpSubstitutionMap.put("o", "YYYY");
087      sPhpSubstitutionMap.put("y", "YY");
088      sPhpSubstitutionMap.put("a", "a");
089      sPhpSubstitutionMap.put("A", "a");
090      sPhpSubstitutionMap.put("g", "h");
091      sPhpSubstitutionMap.put("H", "HH");
092      sPhpSubstitutionMap.put("G", "H");
093      sPhpSubstitutionMap.put("h", "hh");
094      sPhpSubstitutionMap.put("i", "mm");
095      sPhpSubstitutionMap.put("s", "ss");
096      sPhpSubstitutionMap.put("u", "SSS");
097      sPhpSubstitutionMap.put("Z", "");
098      sPhpSubstitutionMap.put("O", "ZZ");
099      sPhpSubstitutionMap.put("P", "XXX");
100      sPhpSubstitutionMap.put("T", "zz");
101      sPhpSubstitutionMap.put("c", "");
102      sPhpSubstitutionMap.put("C", "");
103      sPhpSubstitutionMap.put("U", "");
104   }
105
106   //##########################################################################
107   // PUBLIC METHODS
108   //##########################################################################
109
110   //--------------------------------------------------------------------------
111   public static synchronized Date threadsafeParse(String inDateString, SimpleDateFormat inFormat)
112      throws ParseException
113   {
114      return inFormat.parse(inDateString);
115   }
116
117   //--------------------------------------------------------------------------
118   public static String generateElapsedTimeString(long inStartTime)
119   {
120      return generateElapsedTimeString(inStartTime, System.currentTimeMillis());
121   }
122
123   //--------------------------------------------------------------------------
124   public static int getCurrentYear()
125   {
126      return new GregorianCalendar().get(Calendar.YEAR);
127   }
128
129   //--------------------------------------------------------------------------
130   public static int getYear(Date inDate)
131   {
132      Calendar calendar = Calendar.getInstance();
133      calendar.setTime(inDate);
134      return calendar.get(Calendar.YEAR);
135   }
136
137   //--------------------------------------------------------------------------
138   public static Date roundToNextMinute(Date inStartTime)
139   {
140      Calendar calendar = new GregorianCalendar();
141      calendar.setTime(inStartTime);
142      calendar.set(Calendar.MILLISECOND, 0);
143      calendar.set(Calendar.SECOND, 0);
144      calendar.add(Calendar.MINUTE, 1);
145
146      return calendar.getTime();
147   }
148
149   //--------------------------------------------------------------------------
150   public static Date roundToNextHour(Date inStartTime)
151   {
152      Calendar calendar = new GregorianCalendar();
153      calendar.setTime(inStartTime);
154      calendar.set(Calendar.MILLISECOND, 0);
155      calendar.set(Calendar.SECOND, 0);
156      calendar.set(Calendar.MINUTE, 0);
157      calendar.add(Calendar.HOUR_OF_DAY, 1);
158
159      return calendar.getTime();
160   }
161
162   //--------------------------------------------------------------------------
163   public static String generateElapsedTimeString(long inStartTime, long inEndTime)
164   {
165      long elapsedMillis = inEndTime - inStartTime;
166
167      long elapsedSec  = 0;
168      long elapsedMin  = 0;
169      long elapsedHour = 0;
170
171      String output;
172
173      if (elapsedMillis >= TimeUnit.second.getMilliseconds())
174      {
175         elapsedSec = elapsedMillis / TimeUnit.second.getMilliseconds();
176
177         if (elapsedSec >= 60)
178         {
179            elapsedMin = elapsedSec / 60;
180            elapsedSec = elapsedSec % 60;
181
182            if (elapsedMin >= 60)
183            {
184               elapsedHour = elapsedMin / 60;
185               elapsedMin  = elapsedMin % 60;
186            }
187         }
188
189         output = String.format("%02d:%02d:%02d", elapsedHour, elapsedMin, elapsedSec);
190      }
191      else
192      {
193         output = String.format("00:00:00.%03d", elapsedMillis);
194      }
195
196      return output;
197   }
198
199   //--------------------------------------------------------------------------
200   public static LocalDateTime convertToLocalDateTime(Date inDate) 
201   {
202      return inDate.toInstant()
203            .atZone(ZoneId.systemDefault())
204            .toLocalDateTime();
205   }
206
207   //--------------------------------------------------------------------------
208   public static LocalDateTime convertToLocalDateTime(Instant inDate)
209   {
210      return inDate.atZone(ZoneId.systemDefault())
211            .toLocalDateTime();
212   }
213
214   //--------------------------------------------------------------------------
215   public static ZonedDateTime convertToZonedDateTime(Date inDate) 
216   {
217      return inDate.toInstant()
218            .atZone(ZoneId.systemDefault());
219   }
220
221   //--------------------------------------------------------------------------
222   public static ZonedDateTime convertToZonedDateTime(Instant inDate)
223   {
224      return inDate.atZone(ZoneId.systemDefault());
225   }
226
227   //--------------------------------------------------------------------------
228   public static Date convertToDate(LocalDateTime inValue) 
229   {
230      return Date.from(inValue.atZone(ZoneId.systemDefault()).toInstant());
231   }
232
233   //--------------------------------------------------------------------------
234   public static ZonedDateTime convertToUTCZonedDateTime(Instant inValue)
235   {
236      return ZonedDateTime.ofInstant(inValue, UTC_ZONE);
237   }
238   
239   //--------------------------------------------------------------------------
240   /**
241    Note that Date should really no longer be used! Use java.time.LocalDateTime instead.
242    @param  inDate the Date object to be adjusted
243    @param  inOffset the ZoneOffset to use for adjustment
244    @return a Date object that has been forcibly adjusted by the specified timezone offset.
245    */
246   public static Date applyZoneOffset(Date inDate, ZoneOffset inOffset)
247   {
248      ZoneOffset zoneOffset = ZoneId.systemDefault().getRules().getOffset(inDate.toInstant());
249
250      return new Date(inDate.getTime() + ((zoneOffset.getTotalSeconds() - inOffset.getTotalSeconds()) * -1000));
251   }
252
253   
254   //--------------------------------------------------------------------------
255   public static String getISO_8601_Date()
256   {
257      return getISO_8601_Date(LocalDateTime.now());
258   }
259
260   //--------------------------------------------------------------------------
261   public static String getISO_8601_Date(Date inDate)
262   {
263      return getISO_8601_Date(convertToLocalDateTime(inDate));
264   }
265
266   //--------------------------------------------------------------------------
267   public static String getISO_8601_Date(Instant inDate)
268   {
269      return getISO_8601_Date(convertToLocalDateTime(inDate));
270   }
271
272   //--------------------------------------------------------------------------
273   public static String getISO_8601_Date(LocalDateTime inValue)
274   {
275      return DateTimeFormatter.ISO_LOCAL_DATE.format(inValue);
276   }
277
278   //--------------------------------------------------------------------------
279   public static String getISO_8601_Date(ZonedDateTime inValue)
280   {
281      return DateTimeFormatter.ISO_LOCAL_DATE.format(inValue);
282   }
283
284
285   //--------------------------------------------------------------------------
286   public static String getYYYYMMDD()
287   {
288      return getYYYYMMDD(LocalDateTime.now());
289   }
290
291   //--------------------------------------------------------------------------
292   public static String getYYYYMMDD(Date inValue)
293   {
294      return getYYYYMMDD(convertToLocalDateTime(inValue));
295   }
296
297   //--------------------------------------------------------------------------
298   public static String getYYYYMMDD(LocalDateTime inValue)
299   {
300      return YYYYMMDD_FORMATTER.format(inValue);
301   }
302
303   //--------------------------------------------------------------------------
304   public static String getYYYYMMDD(ZonedDateTime inValue)
305   {
306      return YYYYMMDD_FORMATTER.format(inValue);
307   }
308
309   //--------------------------------------------------------------------------
310   public static String getYYYY_MM(Date inValue, char inSeparator)
311   {
312      return getYYYY_MM(convertToLocalDateTime(inValue), inSeparator);
313   }
314
315   //--------------------------------------------------------------------------
316   public static String getYYYY_MM(LocalDateTime inValue, char inSeparator)
317   {
318      return DateTimeFormatter.ofPattern("yyyy" + inSeparator + "MM").format(inValue);
319   }
320
321   //--------------------------------------------------------------------------
322   public static String getYYYY_MM_DD()
323   {
324      return new SimpleDateFormat("yyyy-MM-dd").format(new Date());
325   }
326
327   
328   //--------------------------------------------------------------------------
329   public static String getYYYY_MM_DD_HH_mm_aa()
330   {
331      return getYYYY_MM_DD_HH_mm_aa(LocalDateTime.now());
332   }
333
334   //--------------------------------------------------------------------------
335   public static String getYYYY_MM_DD_HH_mm_aa(Date inDate)
336   {
337      return getYYYY_MM_DD_HH_mm_aa(convertToLocalDateTime(inDate));
338   }
339
340   //--------------------------------------------------------------------------
341   public static String getYYYY_MM_DD_HH_mm_aa(LocalDateTime inValue)
342   {
343      return YYYY_MM_DD_HH_mm_aa_FORMATTER.format(inValue);
344   }
345
346   
347   //--------------------------------------------------------------------------
348   public static String getYYYY_MM_DD_HH_mm_ss()
349   {
350      return getYYYY_MM_DD_HH_mm_ss(LocalDateTime.now());
351   }
352
353   //--------------------------------------------------------------------------
354   public static String getYYYY_MM_DD_HH_mm_ss(Date inDate)
355   {
356      return getYYYY_MM_DD_HH_mm_ss(convertToLocalDateTime(inDate));
357   }
358
359   //--------------------------------------------------------------------------
360   public static String getYYYY_MM_DD_HH_mm_ss(LocalDateTime inValue)
361   {
362      return YYYY_MM_DD_HH_mm_ss_FORMATTER.format(inValue);
363   }
364
365   //--------------------------------------------------------------------------
366   public static Date addDaysToDate(Date inStartDate, Integer inDays)
367   {
368      Date date = null;
369      if (inStartDate != null && inDays != null)
370      {
371         Calendar c = Calendar.getInstance();
372         c.setTime(inStartDate);
373         c.add(Calendar.DATE, inDays);
374         date = c.getTime();
375      }
376      return date;
377   }
378
379   //--------------------------------------------------------------------------
380   /**
381    Makes a best attempt to convert php date format syntax into Java data format syntax.
382    Not all specifiers are supported.
383    @param inPhpFormatString the date format in php synatx
384    @return a SimpleDateFormat object
385    */
386   public static DateTimeFormatter getFormatterUsingPhpSyntax(String inPhpFormatString)
387   {
388      /*
389      Format      Description                                                               Example returned values
390       ------      -----------------------------------------------------------------------   -----------------------
391         d         Day of the month, 2 digits with leading zeros                             01 to 31
392         D         A short textual representation of the day of the week                     Mon to Sun
393         j         Day of the month without leading zeros                                    1 to 31
394         l         A full textual representation of the day of the week                      Sunday to Saturday
395         N         ISO-8601 numeric representation of the day of the week                    1 (for Monday) through 7 (for Sunday)
396         S         English ordinal suffix for the day of the month, 2 characters             st, nd, rd or th. Works well with j
397         w         Numeric representation of the day of the week                             0 (for Sunday) to 6 (for Saturday)
398         z         The day of the year (starting from 0)                                     0 to 364 (365 in leap years)
399         W         ISO-8601 week number of year, weeks starting on Monday                    01 to 53
400         F         A full textual representation of a month, such as January or March        January to December
401         m         Numeric representation of a month, with leading zeros                     01 to 12
402         M         A short textual representation of a month                                 Jan to Dec
403         n         Numeric representation of a month, without leading zeros                  1 to 12
404         t         Number of days in the given month                                         28 to 31
405         L         Whether it&#39;s a leap year                                                  1 if it is a leap year, 0 otherwise.
406         o         ISO-8601 year number (identical to (Y), but if the ISO week number (W)    Examples: 1998 or 2004
407                   belongs to the previous or next year, that year is used instead)
408         Y         A full numeric representation of a year, 4 digits                         Examples: 1999 or 2003
409         y         A two digit representation of a year                                      Examples: 99 or 03
410         a         Lowercase Ante meridiem and Post meridiem                                 am or pm
411         A         Uppercase Ante meridiem and Post meridiem                                 AM or PM
412         g         12-hour format of an hour without leading zeros                           1 to 12
413         G         24-hour format of an hour without leading zeros                           0 to 23
414         h         12-hour format of an hour with leading zeros                              01 to 12
415         H         24-hour format of an hour with leading zeros                              00 to 23
416         i         Minutes, with leading zeros                                               00 to 59
417         s         Seconds, with leading zeros                                               00 to 59
418         u         Decimal fraction of a second                                              Examples:
419                   (minimum 1 digit, arbitrary number of digits allowed)                     001 (i.e. 0.001s) or
420                                                                                             100 (i.e. 0.100s) or
421                                                                                             999 (i.e. 0.999s) or
422                                                                                             999876543210 (i.e. 0.999876543210s)
423         O         Difference to Greenwich time (GMT) in hours and minutes                   Example: +1030
424         P         Difference to Greenwich time (GMT) with colon between hours and minutes   Example: -08:00
425         T         Timezone abbreviation of the machine running the code                     Examples: EST, MDT, PDT ...
426         Z         Timezone offset in seconds (negative if west of UTC, positive if east)    -43200 to 50400
427         c         ISO 8601 date represented as the local time with an offset to UTC appended.
428                   Notes:                                                                    Examples:
429                   1) If unspecified, the month / day defaults to the current month / day,   1991 or
430                      the time defaults to midnight, while the timezone defaults to the      1992-10 or
431                      browser's timezone. If a time is specified, it must include both hours 1993-09-20 or
432                      and minutes. The "T" delimiter, seconds, milliseconds and timezone     1994-08-19T16:20+01:00 or
433                      are optional.                                                          1995-07-18T17:21:28-02:00 or
434                   2) The decimal fraction of a second, if specified, must contain at        1996-06-17T18:22:29.98765+03:00 or
435                      least 1 digit (there is no limit to the maximum number                 1997-05-16T19:23:30,12345-0400 or
436                      of digits allowed), and may be delimited by either a '.' or a ','      1998-04-15T20:24:31.2468Z or
437                   Refer to the examples on the right for the various levels of              1999-03-14T20:24:32Z or
438                   date-time granularity which are supported, or see                         2000-02-13T21:25:33
439                   http://www.w3.org/TR/NOTE-datetime for more info.                         2001-01-12 22:26:34
440         C         An ISO date string as implemented by the native Date object's             1962-06-17T09:21:34.125Z
441                   [Date.toISOString](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)
442                   method. This outputs the numeric part with *UTC* hour and minute
443                   values, and indicates this by appending the `'Z'` timezone
444                   identifier.
445         U         Seconds since the Unix Epoch (January 1 1970 00:00:00 GMT)                1193432466 or -2138434463
446         MS        Microsoft AJAX serialized dates                                           \/Date(1238606590509)\/ (i.e. UTC milliseconds since epoch) or
447                                                                                             \/Date(1238606590509+0800)\/
448         time      A javascript millisecond timestamp                                        1350024476440
449         timestamp A UNIX timestamp (same as U)                                              1350024866
450       */
451      StringBuilderPlus buffer = new StringBuilderPlus(inPhpFormatString);
452
453      // Note that the order of replacement is important
454      for (String target : sPhpSubstitutionMap.keySet())
455      {
456         String replacement = sPhpSubstitutionMap.get(target);
457         int index;
458         int fromIndex = 0;
459         while ((index = buffer.indexOf(target, fromIndex)) >= 0)
460         {
461            if (0 == index
462                || getEscapeCount(buffer, index)%2 != 1) // Escaped characters indicated static text in the php format
463            {
464               buffer.replace(index, index + target.length(), replacement);
465               fromIndex = index + replacement.length();
466            }
467            else
468            {
469               fromIndex++;
470            }
471         }
472      }
473
474      // Replace escaped characters (php syntax) with a single-quoted version (Java syntax)
475      Matcher m = sEscapeSequence.matcher(buffer);
476      int fromIndex = 0;
477      while (m.find(fromIndex))
478      {
479         String strippedValue = m.group(1).replaceAll("\\\\", "");
480         buffer.replace(m.start() + 1, m.end(), "'" + strippedValue + "'");
481      }
482
483      return DateTimeFormatter.ofPattern(buffer.toString());
484   }
485
486   //--------------------------------------------------------------------------
487   // Returns the number of '\' characters that preceded the character at the specified index
488   private static int getEscapeCount(CharSequence inString, int inIndex)
489   {
490      int count = 0;
491      for (int i = inIndex - 1; i >= 0; i--)
492      {
493         if (inString.charAt(i) == '\\')
494         {
495            count++;
496         }
497         else
498         {
499            break;
500         }
501      }
502
503      return count;
504   }
505}