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'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}