From 17c91c6d7b9f47fc03cb8f33e66944d73486652e Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Tue, 2 Apr 2024 21:37:59 +0200 Subject: [PATCH] many many many updates to the DateTimeCaster --- .../dynamic/casters/DateTimeCaster.java | 124 +++++++++- .../ortus/boxlang/runtime/types/DateTime.java | 40 ++++ .../dynamic/casters/DateTimeCasterTest.java | 223 ++++++++++++++++++ 3 files changed, 379 insertions(+), 8 deletions(-) create mode 100644 src/test/java/ortus/boxlang/runtime/dynamic/casters/DateTimeCasterTest.java diff --git a/src/main/java/ortus/boxlang/runtime/dynamic/casters/DateTimeCaster.java b/src/main/java/ortus/boxlang/runtime/dynamic/casters/DateTimeCaster.java index 084d848eb..25d20fda7 100644 --- a/src/main/java/ortus/boxlang/runtime/dynamic/casters/DateTimeCaster.java +++ b/src/main/java/ortus/boxlang/runtime/dynamic/casters/DateTimeCaster.java @@ -18,7 +18,10 @@ package ortus.boxlang.runtime.dynamic.casters; import java.time.ZoneId; -import java.time.format.DateTimeParseException; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +import org.apache.commons.lang3.time.DateUtils; import ortus.boxlang.runtime.interop.DynamicObject; import ortus.boxlang.runtime.types.DateTime; @@ -29,6 +32,58 @@ */ public class DateTimeCaster { + private static final String[] COMMON_PATTERNS = { + + // Localized Date/Time formats + "EEE, dd MMM yyyy HH:mm:ss zzz", // Full DateTime (e.g., Tue, 02 Apr 2024 21:01:00 CEST) - Similar to FULL_FULL + "dd MMM yyyy HH:mm:ss", // Long DateTime (e.g., 02 Apr 2024 21:01:00) - Similar to LONG_LONG + "dd-MMM-yyyy HH:mm:ss", // Medium DateTime (e.g., 02-Apr-2024 21:01:00) - Might need adjustment based on locale + "dd/MM/yyyy HH:mm:ss", // Short DateTime (e.g., 02/04/2024 21:01:00) - Might need adjustment based on locale + "dd.MM.yyyy HH:mm:ss", // Short DateTime (e.g., 02.04.2024 21:01:00) - Might need adjustment based on locale + + // ISO formats + "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", // Date-time with milliseconds and offset + "yyyy-MM-dd'T'HH:mm:ss.SSS", // Date-time with milliseconds + "yyyy-MM-dd'T'HH:mm:ssZ", // Date-time with offset (Z) + "yyyy-MM-dd'T'HH:mm:ssX", // Date-time with offset (X) + "yyyy-MM-dd'T'HH:mm:ss", // Date-time + + // ODBC formats + "yyyyMMddHHmmss", // OBCDateTime - Potential ODBC format + + // Localized Date formats + "EEE, dd MMM yyyy", // Full Date (e.g., Tue, 02 Apr 2024) - Similar to FULL + "dd MMM yyyy", // Long Date (e.g., 02 Apr 2024) - Similar to LONG + "dd-MMM-yyyy", // Medium Date (e.g., 02-Apr-2024) - Might need adjustment based on locale + "dd/MMM/yyyy", // Medium Date (e.g., 02-Apr-2024) - Might need adjustment based on locale + "dd.MMM.yyyy", // Medium Date (e.g., 02.Apr.2024) - Might need adjustment based on locale + + "dd MM yyyy", // Short Date (e.g., 02.04.2024) - Might need adjustment based on locale + "dd-MM-yyyy", // Short Date (e.g., 02-04-2024) - Might need adjustment based on locale + "dd/MM/yyyy", // Short Date (e.g., 02/04/2024) - Might need adjustment based on locale + "dd.MM.yyyy", // Short Date (e.g., 02.04.2024) - Might need adjustment based on locale + + // Localized Date formats - Month First + "MMM dd yyyy", // Long Date (e.g., Apr 02 2024) + "MMM-dd-yyyy", // Medium Date (e.g., Apr-02-2024) - Might need adjustment based on locale + "MMM/dd/yyyy", // Medium Date (e.g., Apr/02/2024) - Might need adjustment based on locale + "MMM.dd.yyyy", // Medium Date (e.g., Apr.02.2024) - Might need adjustment based on locale + + // Localized Date formats - Month First (Short) + "MM dd yyyy", // Short Date (e.g., 04 02 2024) - Might need adjustment based on locale + "MM-dd-yyyy", // Short Date (e.g., 04-02-2024) - Might need adjustment based on locale + "MM/dd/yyyy", // Short Date (e.g., 04/02/2024) - Might need adjustment based on locale + "MM.dd.yyyy", // Short Date (e.g., 04.02.2024) - Might need adjustment based on locale + + // ISO format + "yyyy-MM-dd", // ISODate (e.g., 2024-04-02) + "yyyy/MM/dd", // ISODate (e.g., 2024/04/02) + "yyyy.MM.dd", // ISODate (e.g., 2024.04.02) + + // ODBC format + "yyyyMMdd" // ODBCDate - Potential ODBC format + }; + /** * Tests to see if the value can be cast. * Returns a {@code CastAttempt} which will contain the result if casting was @@ -66,7 +121,9 @@ public static DateTime cast( Object object, Boolean fail ) { } /** - * Used to cast anything + * Used to cast anything to a DateTime object. We start off by testing the object + * against commonly known Java date objects, and then try to parse the object as a + * string. If we fail, we return null. * * @param object The value to cast * @param fail True to throw exception when failing. @@ -75,6 +132,8 @@ public static DateTime cast( Object object, Boolean fail ) { * @return The value, or null when cannot be cast */ public static DateTime cast( Object object, Boolean fail, ZoneId timezone ) { + + // Null is null if ( object == null ) { if ( fail ) { throw new BoxCastException( "Can't cast null to a DateTime." ); @@ -83,17 +142,66 @@ public static DateTime cast( Object object, Boolean fail, ZoneId timezone ) { } } + // Unwrap the object object = DynamicObject.unWrap( object ); + // We have a DateTime object + if ( object instanceof DateTime targetDateTime ) { + return targetDateTime; + } + + // We have a ZonedDateTime object + if ( object instanceof java.time.ZonedDateTime targetZonedDateTime ) { + return new DateTime( targetZonedDateTime ); + } + + // we have a Calendar object + if ( object instanceof java.util.Calendar targetCalendar ) { + return new DateTime( targetCalendar.toInstant().atZone( timezone ) ); + } + + // We have a LocalDateTime object + if ( object instanceof java.time.LocalDateTime targetLocalDateTime ) { + return new DateTime( targetLocalDateTime.atZone( timezone ) ); + } + + // We have a LocalDate object + if ( object instanceof java.time.LocalDate targetLocalDate ) { + return new DateTime( targetLocalDate.atStartOfDay( timezone ) ); + } + + // We have a java.util.Date object + if ( object instanceof java.util.Date targetDate ) { + return new DateTime( targetDate.toInstant().atZone( timezone ) ); + } + + // We have a java.sql.Date object + if ( object instanceof java.sql.Date targetDate ) { + return new DateTime( targetDate.toLocalDate().atStartOfDay( timezone ) ); + } + + // We have a java.sql.Timestamp object + if ( object instanceof java.sql.Timestamp targetTimestamp ) { + return new DateTime( targetTimestamp.toInstant().atZone( timezone ) ); + } + + // Try to cast it to a String and see if we can parse it + var targetString = StringCaster.cast( object, fail ); + + // Timestamp string "^\{ts ([^\}])*\}" - {ts 2023-01-01 12:00:00} + if ( targetString.matches( "^\\{ts ([^\\}]*)\\}" ) ) { + return new DateTime( + ZonedDateTime.parse( targetString, ( DateTimeFormatter ) DateTime.COMMON_FORMATTERS.get( "ODBCDateTime" ) ) + ); + } + + // Now let's go to Apache commons lang for its date parsing try { - return object instanceof DateTime - ? ( DateTime ) object - : new DateTime( ( String ) object, timezone ); - } catch ( DateTimeParseException | ClassCastException e ) { + return new DateTime( DateUtils.parseDate( targetString, COMMON_PATTERNS ) ); + } catch ( java.text.ParseException e ) { if ( fail ) { - throw e; + throw new BoxCastException( "Can't cast [" + object + "] to a DateTime.", e ); } - return null; } diff --git a/src/main/java/ortus/boxlang/runtime/types/DateTime.java b/src/main/java/ortus/boxlang/runtime/types/DateTime.java index 2ddfc3855..3042ea62a 100644 --- a/src/main/java/ortus/boxlang/runtime/types/DateTime.java +++ b/src/main/java/ortus/boxlang/runtime/types/DateTime.java @@ -45,6 +45,10 @@ import ortus.boxlang.runtime.types.meta.GenericMeta; import ortus.boxlang.runtime.util.LocalizationUtil; +/** + * A DateTime object that wraps a ZonedDateTime object and provides additional functionality + * for date time manipulation and formatting the BoxLang way. + */ public class DateTime implements IType, IReferenceable, Comparable, Serializable { /** @@ -83,10 +87,16 @@ public class DateTime implements IType, IReferenceable, Comparable, Se public static final String ODBC_DATE_FORMAT_MASK = "'{d '''yyyy-MM-dd'''}'"; public static final String ODBC_TIME_FORMAT_MASK = "'{t '''HH:mm:ss'''}'"; + /** + * Common Modes + */ public static final String MODE_DATE = "Date"; public static final String MODE_TIME = "Time"; public static final String MODE_DATETIME = "DateTime"; + /** + * Common Formatters Map so we can easily access them by name + */ public static final IStruct COMMON_FORMATTERS = Struct.of( "fullDateTime", DateTimeFormatter.ofLocalizedDateTime( FormatStyle.FULL, FormatStyle.FULL ), "longDateTime", DateTimeFormatter.ofLocalizedDateTime( FormatStyle.LONG, FormatStyle.LONG ), @@ -158,6 +168,36 @@ public DateTime( ZonedDateTime dateTime ) { this.wrapped = dateTime; } + /** + * Constructor to create DateTime from a java.util.Date object + * This will use the system default timezone + * + * @param date The date object + */ + public DateTime( java.util.Date date ) { + this( date.toInstant().atZone( ZoneId.systemDefault() ) ); + } + + /** + * Constructor to create DateTime from a LocalDateTime object + * This will use the system default timezone + * + * @param dateTime A local date time object + */ + public DateTime( LocalDateTime dateTime ) { + this( ZonedDateTime.of( dateTime, ZoneId.systemDefault() ) ); + } + + /** + * Constructor to create DateTime from a LocalDate object + * This will use the system default timezone + * + * @param date A local date object + */ + public DateTime( LocalDate date ) { + this( ZonedDateTime.of( date.atStartOfDay(), ZoneId.systemDefault() ) ); + } + /** * Constructor to create DateTime from a Instant * diff --git a/src/test/java/ortus/boxlang/runtime/dynamic/casters/DateTimeCasterTest.java b/src/test/java/ortus/boxlang/runtime/dynamic/casters/DateTimeCasterTest.java new file mode 100644 index 000000000..8370586b3 --- /dev/null +++ b/src/test/java/ortus/boxlang/runtime/dynamic/casters/DateTimeCasterTest.java @@ -0,0 +1,223 @@ +/** + * [BoxLang] + * + * Copyright [2023] [Ortus Solutions, Corp] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ortus.boxlang.runtime.dynamic.casters; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Calendar; +import java.util.Date; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import ortus.boxlang.runtime.types.DateTime; +import ortus.boxlang.runtime.types.exceptions.BoxCastException; + +public class DateTimeCasterTest { + + @DisplayName( "It can cast null to not be a date" ) + @Test + public void testNull() { + assertThat( DateTimeCaster.attempt( null ).wasSuccessful() ).isFalse(); + } + + @Test + @DisplayName( "Test casting ZonedDateTime to DateTime" ) + public void testCastZonedDateTime() { + ZonedDateTime zonedDateTime = ZonedDateTime.now(); + DateTime result = DateTimeCaster.cast( zonedDateTime ); + assertThat( result ).isNotNull(); + assertThat( result.getWrapped() ).isEqualTo( zonedDateTime ); + } + + @Test + @DisplayName( "Test casting Calendar to DateTime" ) + public void testCastCalendar() { + Calendar calendar = Calendar.getInstance(); + DateTime result = DateTimeCaster.cast( calendar ); + assertThat( result ).isNotNull(); + assertThat( result.getWrapped() ).isEqualTo( calendar.toInstant().atZone( ZoneId.systemDefault() ) ); + } + + @Test + @DisplayName( "Test casting Date to DateTime" ) + public void testCastDate() { + Date date = new Date(); + DateTime result = DateTimeCaster.cast( date ); + assertThat( result ).isNotNull(); + assertThat( result.getWrapped() ).isEqualTo( date.toInstant().atZone( ZoneId.systemDefault() ) ); + } + + @Test + @DisplayName( "Test casting LocalDateTime to DateTime" ) + public void testCastLocalDateTime() { + LocalDateTime localDateTime = LocalDateTime.now(); + DateTime result = DateTimeCaster.cast( localDateTime ); + assertThat( result ).isNotNull(); + assertThat( result.getWrapped() ).isEqualTo( localDateTime.atZone( ZoneId.systemDefault() ) ); + } + + @Test + @DisplayName( "Test casting LocalDate to DateTime" ) + public void testCastLocalDate() { + LocalDate localDate = LocalDate.now(); + DateTime result = DateTimeCaster.cast( localDate ); + assertThat( result ).isNotNull(); + assertThat( result.getWrapped() ).isEqualTo( localDate.atStartOfDay( ZoneId.systemDefault() ) ); + } + + @Test + @DisplayName( "Test casting valid string representation of date to DateTime" ) + public void testCastValidString() { + String dateString = "2024-04-02T12:00:00Z"; + DateTime result = DateTimeCaster.cast( dateString ); + assertThat( result ).isNotNull(); + } + + @Test + @DisplayName( "Test casting full DateTime string to DateTime" ) + public void testCastFullDateTimeString() { + String dateTimeString = "Tue, 02 Apr 2024 21:01:00 CEST"; + DateTime result = DateTimeCaster.cast( dateTimeString ); + assertThat( result ).isNotNull(); + } + + @Test + @DisplayName( "Test casting long DateTime string to DateTime" ) + public void testCastLongDateTimeString() { + String dateTimeString = "02 Apr 2024 21:01:00"; + DateTime result = DateTimeCaster.cast( dateTimeString ); + assertThat( result ).isNotNull(); + } + + @Test + @DisplayName( "Test casting medium DateTime string to DateTime" ) + public void testCastMediumDateTimeString() { + String dateTimeString = "02-Apr-2024 21:01:00"; + DateTime result = DateTimeCaster.cast( dateTimeString ); + assertThat( result ).isNotNull(); + } + + @Test + @DisplayName( "Test casting short DateTime string to DateTime" ) + public void testCastShortDateTimeString() { + String dateTimeString = "02/04/2024 21:01:00"; + DateTime result = DateTimeCaster.cast( dateTimeString ); + assertThat( result ).isNotNull(); + } + + @Test + @DisplayName( "Test casting ISO DateTime string to DateTime" ) + public void testCastISODateTimeString() { + String dateTimeString = "2024-04-02T21:01:00Z"; + DateTime result = DateTimeCaster.cast( dateTimeString ); + assertThat( result ).isNotNull(); + } + + @Test + @DisplayName( "Test casting ODBC DateTime string to DateTime" ) + public void testCastODBCDateTimeString() { + String dateTimeString = "20240402210100"; + DateTime result = DateTimeCaster.cast( dateTimeString ); + assertThat( result ).isNotNull(); + } + + @Test + @DisplayName( "Test casting full Date string to DateTime" ) + public void testCastFullDateString() { + String dateString = "Tue, 02 Apr 2024"; + DateTime result = DateTimeCaster.cast( dateString ); + assertThat( result ).isNotNull(); + } + + @Test + @DisplayName( "Test casting long Date string to DateTime" ) + public void testCastLongDateString() { + String dateString = "02 Apr 2024"; + DateTime result = DateTimeCaster.cast( dateString ); + assertThat( result ).isNotNull(); + } + + @Test + @DisplayName( "Test casting medium Date string to DateTime" ) + public void testCastMediumDateString() { + String dateString = "02-Apr-2024"; + DateTime result = DateTimeCaster.cast( dateString ); + assertThat( result ).isNotNull(); + } + + @Test + @DisplayName( "Test casting short Date string to DateTime" ) + public void testCastShortDateString() { + String dateString = "02/04/2024"; + DateTime result = DateTimeCaster.cast( dateString ); + assertThat( result ).isNotNull(); + } + + @Test + @DisplayName( "Test casting Month First long Date string to DateTime" ) + public void testCastMonthFirstLongDateString() { + String dateString = "Apr 02 2024"; + DateTime result = DateTimeCaster.cast( dateString ); + assertThat( result ).isNotNull(); + } + + @Test + @DisplayName( "Test casting Month First medium Date string to DateTime" ) + public void testCastMonthFirstMediumDateString() { + String dateString = "Apr-02-2024"; + DateTime result = DateTimeCaster.cast( dateString ); + assertThat( result ).isNotNull(); + } + + @Test + @DisplayName( "Test casting Month First short Date string to DateTime" ) + public void testCastMonthFirstShortDateString() { + String dateString = "04 02 2024"; + DateTime result = DateTimeCaster.cast( dateString ); + assertThat( result ).isNotNull(); + } + + @Test + @DisplayName( "Test casting ISO Date string to DateTime" ) + public void testCastISODateString() { + String dateString = "2024-04-02"; + DateTime result = DateTimeCaster.cast( dateString ); + assertThat( result ).isNotNull(); + } + + @Test + @DisplayName( "Test casting ODBC Date string to DateTime" ) + public void testCastODBCDateString() { + String dateString = "20240402"; + DateTime result = DateTimeCaster.cast( dateString ); + assertThat( result ).isNotNull(); + } + + @Test + @DisplayName( "Test casting invalid string representation of date to DateTime" ) + public void testCastInvalidString() { + String invalidDateString = "invalid_date_string"; + assertThrows( BoxCastException.class, () -> DateTimeCaster.cast( invalidDateString ) ); + } +}