diff --git a/src/main/java/com/cedarsoftware/util/DateUtilities.java b/src/main/java/com/cedarsoftware/util/DateUtilities.java index 54ee55ac6..9f6f195be 100644 --- a/src/main/java/com/cedarsoftware/util/DateUtilities.java +++ b/src/main/java/com/cedarsoftware/util/DateUtilities.java @@ -97,7 +97,7 @@ public final class DateUtilities { private static final String tz_Hh_MM = "[+-]\\d{1,2}:\\d{2}"; private static final String tz_HHMM = "[+-]\\d{4}"; private static final String tz_Hh = "[+-]\\d{1,2}"; - private static final String tzNamed = ws + "\\[?[A-Za-z][A-Za-z0-9~/._+-]+]?"; + private static final String tzNamed = wsOp + "\\[?[A-Za-z][A-Za-z0-9~\\/._+-]+]?"; // Patterns defined in BNF-style using above named elements private static final Pattern isoDatePattern = Pattern.compile( // Regex's using | (OR) @@ -153,7 +153,8 @@ private DateUtilities() { } /** - * Main API. Retrieve date-time from passed in String. + * Main API. Retrieve date-time from passed in String. If the date-time given does not include a timezone or + * timezone offset, then ZoneId.systemDefault() will be used. * @param dateStr String containing a date. If there is excess content, it will be ignored. * @return Date instance that represents the passed in date. See comments at top of class for supported * formats. This API is intended to be super flexible in terms of what it can parse. If a null or empty String is @@ -163,7 +164,7 @@ public static Date parseDate(String dateStr) { if (StringUtilities.isEmpty(dateStr)) { return null; } - ZonedDateTime zonedDateTime = parseDate(dateStr, false); + ZonedDateTime zonedDateTime = parseDate(dateStr, ZoneId.systemDefault(), false); return new Date(zonedDateTime.toInstant().toEpochMilli()); } @@ -171,12 +172,13 @@ public static Date parseDate(String dateStr) { * Main API. Retrieve date-time from passed in String. The boolean enSureSoloDate, if set true, ensures that * no other non-date content existed in the String. That requires additional time to verify. * @param dateStr String containing a date. See DateUtilities class Javadoc for all the supported formats. - * @param ensureSoloDate If true, if there is excess non-Date content, it will throw an IllegalArgument exception. + * @param defaultZoneId ZoneId to use if no timezone or timezone offset is given. + * @param ensureDateTimeAlone If true, if there is excess non-Date content, it will throw an IllegalArgument exception. * @return ZonedDateTime instance converted from the passed in date String. See comments at top of class for supported * formats. This API is intended to be super flexible in terms of what it can parse. */ - public static ZonedDateTime parseDate(String dateStr, boolean ensureSoloDate) { - Convention.throwIfNullOrEmpty(dateStr, "dateString must not be null or empty String."); + public static ZonedDateTime parseDate(String dateStr, ZoneId defaultZoneId, boolean ensureDateTimeAlone) { + Convention.throwIfNullOrEmpty(dateStr, "'dateStr' must not be null or empty String."); dateStr = dateStr.trim(); if (allDigits.matcher(dateStr).matches()) { @@ -255,23 +257,19 @@ public static ZonedDateTime parseDate(String dateStr, boolean ensureSoloDate) { if (matcher.group(5) != null) { tz = stripBrackets(matcher.group(5).trim()); } - } else { - noTime = true; // indicates no "time" portion } - if (ensureSoloDate) { + if (ensureDateTimeAlone) { verifyNoGarbageLeft(remnant); } - // Set Timezone into Calendar - ZoneId zoneId = getTimeZone(tz); - ZonedDateTime zonedDateTime = getDate(dateStr, zoneId, noTime, year, month, day, hour, min, sec, milli); + ZoneId zoneId = StringUtilities.isEmpty(tz) ? defaultZoneId : getTimeZone(tz); + ZonedDateTime zonedDateTime = getDate(dateStr, zoneId, year, month, day, hour, min, sec, milli); return zonedDateTime; } private static ZonedDateTime getDate(String dateStr, ZoneId zoneId, - boolean noTime, String year, int month, String day, @@ -290,7 +288,7 @@ private static ZonedDateTime getDate(String dateStr, throw new IllegalArgumentException("Day must be between 1 and 31 inclusive, date: " + dateStr); } - if (noTime) { // no [valid] time portion + if (hour == null) { // no [valid] time portion return ZonedDateTime.of(y, month, d, 0, 0, 0, 0, zoneId); } else { // Regex prevents these from ever failing to parse. diff --git a/src/test/java/com/cedarsoftware/util/TestDateUtilities.java b/src/test/java/com/cedarsoftware/util/TestDateUtilities.java index 399af7db8..bfef6ebdf 100644 --- a/src/test/java/com/cedarsoftware/util/TestDateUtilities.java +++ b/src/test/java/com/cedarsoftware/util/TestDateUtilities.java @@ -3,6 +3,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; import java.text.SimpleDateFormat; +import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.Calendar; import java.util.Date; @@ -347,12 +348,12 @@ void testDayOfWeek() DateUtilities.parseDate(" Dec 25, 2014, thursday "); } try { - ZonedDateTime date = DateUtilities.parseDate("text Dec 25, 2014", true); + ZonedDateTime date = DateUtilities.parseDate("text Dec 25, 2014", ZoneId.systemDefault(), true); fail(); } catch (Exception ignored) { } try { - DateUtilities.parseDate("Dec 25, 2014 text", true); + DateUtilities.parseDate("Dec 25, 2014 text", ZoneId.systemDefault(), true); fail(); } catch (Exception ignored) { } } @@ -716,11 +717,11 @@ void testInconsistentDateSeparators() @Test void testBadTimeSeparators() { - assertThatThrownBy(() -> DateUtilities.parseDate("12/24/1996 12.49.58", true)) + assertThatThrownBy(() -> DateUtilities.parseDate("12/24/1996 12.49.58", ZoneId.systemDefault(), true)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Issue parsing date-time, other characters present: 12.49.58"); - assertThatThrownBy(() -> DateUtilities.parseDate("12.49.58 12/24/1996", true)) + assertThatThrownBy(() -> DateUtilities.parseDate("12.49.58 12/24/1996", ZoneId.systemDefault(), true)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Issue parsing date-time, other characters present: 12.49.58"); @@ -730,7 +731,7 @@ void testBadTimeSeparators() calendar.set(1996, Calendar.DECEMBER, 24, 12, 49, 58); assertEquals(calendar.getTime(), date); - assertThatThrownBy(() -> DateUtilities.parseDate("12/24/1996 12-49-58", true)) + assertThatThrownBy(() -> DateUtilities.parseDate("12/24/1996 12-49-58", ZoneId.systemDefault(), true)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Issue parsing date-time, other characters present: 12-49-58"); } @@ -765,8 +766,8 @@ void testEpochMillis() private static Stream provideTimeZones() { return Stream.of( - Arguments.of("2024-01-19T15:30:45[Europe/London]", 1705696245000L), - Arguments.of("2024-01-19T10:15:30[Asia/Tokyo]", 1705677330000L), + Arguments.of("2024-01-19T15:30:45[Europe/London]", 1705678245000L), + Arguments.of("2024-01-19T10:15:30[Asia/Tokyo]", 1705626930000L), Arguments.of("2024-01-19T20:45:00[America/New_York]", 1705715100000L), Arguments.of("2024-01-19T12:00:00-08:00[America/Los_Angeles]", 1705694400000L), Arguments.of("2024-01-19T22:30:00+01:00[Europe/Paris]", 1705699800000L), @@ -791,8 +792,8 @@ private static Stream provideTimeZones() Arguments.of("2024-01-19T21:45:00-05:00 America/Toronto", 1705718700000L), Arguments.of("2024-01-19T16:00:00+02:00 Africa/Cairo", 1705672800000L), Arguments.of("2024-01-19T07:30:00-07:00 America/Denver", 1705674600000L), - Arguments.of("2024-01-19T07:30GMT", 1705667400000L), - Arguments.of("2024-01-19T07:30[GMT]", 1705667400000L), + Arguments.of("2024-01-19T07:30GMT", 1705649400000L), + Arguments.of("2024-01-19T07:30[GMT]", 1705649400000L), Arguments.of("2024-01-19T07:30 GMT", 1705649400000L), Arguments.of("2024-01-19T07:30 [GMT]", 1705649400000L), Arguments.of("2024-01-19T07:30 GMT", 1705649400000L), @@ -801,23 +802,23 @@ private static Stream provideTimeZones() Arguments.of("2024-01-19T07:30 GMT ", 1705649400000L), Arguments.of("2024-01-19T07:30:01 GMT", 1705649401000L), Arguments.of("2024-01-19T07:30:01 [GMT]", 1705649401000L), - Arguments.of("2024-01-19T07:30:01GMT", 1705667401000L), - Arguments.of("2024-01-19T07:30:01[GMT]", 1705667401000L), + Arguments.of("2024-01-19T07:30:01GMT", 1705649401000L), + Arguments.of("2024-01-19T07:30:01[GMT]", 1705649401000L), Arguments.of("2024-01-19T07:30:01.1 GMT", 1705649401100L), Arguments.of("2024-01-19T07:30:01.1 [GMT]", 1705649401100L), - Arguments.of("2024-01-19T07:30:01.1GMT", 1705667401100L), - Arguments.of("2024-01-19T07:30:01.1[GMT]", 1705667401100L), - Arguments.of("2024-01-19T07:30:01.12GMT", 1705667401120L), + Arguments.of("2024-01-19T07:30:01.1GMT", 1705649401100L), + Arguments.of("2024-01-19T07:30:01.1[GMT]", 1705649401100L), + Arguments.of("2024-01-19T07:30:01.12GMT", 1705649401120L), - Arguments.of("2024-01-19T07:30:01.12[GMT]", 1705667401120L), + Arguments.of("2024-01-19T07:30:01.12[GMT]", 1705649401120L), Arguments.of("2024-01-19T07:30:01.12 GMT", 1705649401120L), Arguments.of("2024-01-19T07:30:01.12 [GMT]", 1705649401120L), - Arguments.of("2024-01-19T07:30:01.123GMT", 1705667401123L), - Arguments.of("2024-01-19T07:30:01.123[GMT]", 1705667401123L), + Arguments.of("2024-01-19T07:30:01.123GMT", 1705649401123L), + Arguments.of("2024-01-19T07:30:01.123[GMT]", 1705649401123L), Arguments.of("2024-01-19T07:30:01.123 GMT", 1705649401123L), Arguments.of("2024-01-19T07:30:01.123 [GMT]", 1705649401123L), - Arguments.of("2024-01-19T07:30:01.1234GMT", 1705667401123L), - Arguments.of("2024-01-19T07:30:01.1234[GMT]", 1705667401123L), + Arguments.of("2024-01-19T07:30:01.1234GMT", 1705649401123L), + Arguments.of("2024-01-19T07:30:01.1234[GMT]", 1705649401123L), Arguments.of("2024-01-19T07:30:01.1234 GMT", 1705649401123L), Arguments.of("2024-01-19T07:30:01.1234 [GMT]", 1705649401123L), @@ -884,6 +885,9 @@ void testTimeZoneParsing(String exampleZone, Long epochMilli) for (int i=0; i < 1; i++) { Date date = DateUtilities.parseDate(exampleZone); assertEquals(date.getTime(), epochMilli); + + ZonedDateTime date2 = DateUtilities.parseDate(exampleZone, ZoneId.systemDefault(), true); + assertEquals(date2.toInstant().toEpochMilli(), epochMilli); } } } \ No newline at end of file