From b3c484206fd97e46107bd1891e7ee3f73dcc775c Mon Sep 17 00:00:00 2001 From: jclausen Date: Mon, 15 Jan 2024 15:34:10 -0500 Subject: [PATCH] WIP on ls datetime --- .../bifs/global/temporal/LSParseDateTime.java | 97 +++++++++++ .../ortus/boxlang/runtime/scopes/Key.java | 1 + .../ortus/boxlang/runtime/types/DateTime.java | 56 ++++++ .../global/temporal/LSParseDateTimeTest.java | 163 ++++++++++++++++++ 4 files changed, 317 insertions(+) create mode 100644 src/main/java/ortus/boxlang/runtime/bifs/global/temporal/LSParseDateTime.java create mode 100644 src/test/java/ortus/boxlang/runtime/bifs/global/temporal/LSParseDateTimeTest.java diff --git a/src/main/java/ortus/boxlang/runtime/bifs/global/temporal/LSParseDateTime.java b/src/main/java/ortus/boxlang/runtime/bifs/global/temporal/LSParseDateTime.java new file mode 100644 index 000000000..4123c5f31 --- /dev/null +++ b/src/main/java/ortus/boxlang/runtime/bifs/global/temporal/LSParseDateTime.java @@ -0,0 +1,97 @@ + +package ortus.boxlang.runtime.bifs.global.temporal; + +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.zone.ZoneRulesException; +import java.util.Locale; + +import ortus.boxlang.runtime.bifs.BIF; +import ortus.boxlang.runtime.bifs.BoxBIF; +import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.dynamic.casters.DateTimeCaster; +import ortus.boxlang.runtime.dynamic.casters.IntegerCaster; +import ortus.boxlang.runtime.dynamic.casters.StringCaster; +import ortus.boxlang.runtime.scopes.ArgumentsScope; +import ortus.boxlang.runtime.scopes.Key; +import ortus.boxlang.runtime.types.Argument; +import ortus.boxlang.runtime.types.DateTime; +import ortus.boxlang.runtime.types.exceptions.BoxRuntimeException; + +@BoxBIF + +public class LSParseDateTime extends BIF { + + /** + * Constructor + */ + public LSParseDateTime() { + super(); + declaredArguments = new Argument[] { + new Argument( true, "any", Key.date ), + new Argument( false, "string", Key.locale ), + new Argument( false, "string", Key.timezone ), + new Argument( false, "string", Key.format ) + }; + } + + /** + * Parses a locale-specific datetime string or object + * + * @param context The context in which the BIF is being invoked. + * @param arguments Argument scope for the BIF. + * + * @argument.date the date, datetime string or an object + * + * @argument.format the format mask to use in parsing + * + * @argument.timezone the timezone to apply to the parsed datetime + */ + public Object invoke( IBoxContext context, ArgumentsScope arguments ) { + Object dateRef = arguments.get( Key.date ); + String locale = arguments.getAsString( Key.locale ); + String timezone = arguments.getAsString( Key.timezone ); + String format = arguments.getAsString( Key.format ); + + Locale localeObj = locale == null ? Locale.getDefault() : new Locale( locale.split( " " )[ 0 ] ); + ZoneId zoneId = null; + try { + zoneId = timezone != null ? ZoneId.of( timezone ) : ZoneId.systemDefault(); + } catch ( ZoneRulesException e ) { + // determine whether this is a format argument + if ( IntegerCaster.attempt( timezone.substring( 0, 1 ) ) != null ) { + format = timezone; + zoneId = ZoneId.systemDefault(); + timezone = null; + } else { + throw new BoxRuntimeException( + "The value [%s] is not a valid locale.", + e + ); + } + } + DateTime dateObj = null; + if ( dateRef instanceof DateTime ) { + dateObj = DateTimeCaster.cast( dateRef ); + dateObj.setFormat( DateTimeFormatter.ISO_LOCAL_DATE_TIME.withLocale( localeObj ) ); + if ( format != null ) { + dateObj.setFormat( format ); + } + if ( timezone != null ) { + dateObj.setTimezone( timezone ); + } + } + if ( format != null ) { + // If we have specified format then use that to parse + dateObj = new DateTime( StringCaster.cast( dateRef ), format ); + dateObj.setFormat( DateTimeFormatter.ISO_LOCAL_DATE_TIME.withLocale( localeObj ) ); + return timezone != null ? dateObj.setTimezone( timezone ) : dateObj; + } else { + // Otherwise attempt to auto-parse + dateObj = new DateTime( StringCaster.cast( dateRef ), localeObj, zoneId ); + } + + return dateObj; + } + +} diff --git a/src/main/java/ortus/boxlang/runtime/scopes/Key.java b/src/main/java/ortus/boxlang/runtime/scopes/Key.java index 4acd628ca..4e6c5279e 100644 --- a/src/main/java/ortus/boxlang/runtime/scopes/Key.java +++ b/src/main/java/ortus/boxlang/runtime/scopes/Key.java @@ -97,6 +97,7 @@ public class Key { public static final Key leaveIndex = Key.of( "leaveIndex" ); public static final Key length = Key.of( "length" ); public static final Key listInfo = Key.of( "listInfo" ); + public static final Key locale = Key.of( "locale" ); public static final Key localeSensitive = Key.of( "localeSensitive" ); public static final Key mappings = Key.of( "mappings" ); public static final Key mask = Key.of( "mask" ); diff --git a/src/main/java/ortus/boxlang/runtime/types/DateTime.java b/src/main/java/ortus/boxlang/runtime/types/DateTime.java index 686f3efae..9918cabe9 100644 --- a/src/main/java/ortus/boxlang/runtime/types/DateTime.java +++ b/src/main/java/ortus/boxlang/runtime/types/DateTime.java @@ -25,7 +25,9 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; import java.time.temporal.ChronoUnit; +import java.util.Locale; import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; @@ -160,6 +162,48 @@ public DateTime( String dateTime, String mask ) { this.wrapped = parsed; } + /** + * Constructor to create DateTime from a datetime string from a specific locale + * + * @param dateTime - a string representing the date and time + * @param locale - a locale object used to assist in parsing the string + */ + public DateTime( String dateTime, Locale locale, ZoneId timezone ) { + ZonedDateTime parsed = null; + this.formatter = DateTimeFormatter.ISO_ZONED_DATE_TIME.withLocale( locale ); + DateTimeFormatter parseFormatter = new DateTimeFormatterBuilder().parseLenient().toFormatter( locale ); + // try parsing if it fails then our time does not contain timezone info so we fall back to a local zoned date + try { + parsed = ZonedDateTime.parse( dateTime, this.formatter ); + } catch ( java.time.format.DateTimeParseException e ) { + // First fallback - it has a time without a zone + try { + parsed = ZonedDateTime.of( LocalDateTime.parse( dateTime, DateTimeFormatter.ISO_LOCAL_DATE_TIME.withLocale( locale ) ), + ZoneId.systemDefault() ); + // Second fallback - it is only a date and we need to supply a time + } catch ( java.time.format.DateTimeParseException x ) { + parsed = ZonedDateTime.of( + LocalDateTime.of( LocalDate.parse( dateTime, DateTimeFormatter.ISO_LOCAL_DATE.withLocale( locale ) ), LocalTime.MIN ), + ZoneId.systemDefault() ); + } catch ( Exception x ) { + throw new BoxRuntimeException( + String.format( + "The the date time value of [%s] could not be parsed as a valid date or datetimea locale of [%s]", + dateTime, + locale.getDisplayName() + ), x ); + } + } catch ( Exception e ) { + throw new BoxRuntimeException( + String.format( + "The the date time value of [%s] could not be parsed with a locale of [%s]", + dateTime, + locale.getDisplayName() + ), e ); + } + this.wrapped = parsed; + } + /** * Constructor to create DateTime from a string with a specified timezone * @@ -337,6 +381,18 @@ public DateTime setFormat( String mask ) { return this; } + /** + * Alternate format setter which accepts a DateTimeFormatter object + * + * @param formatter A DateTimeFormatter instance + * + * @return + */ + public DateTime setFormat( DateTimeFormatter formatter ) { + this.formatter = formatter; + return this; + } + /** * -------------------------------------------------------------------------- * IType Interface Methods diff --git a/src/test/java/ortus/boxlang/runtime/bifs/global/temporal/LSParseDateTimeTest.java b/src/test/java/ortus/boxlang/runtime/bifs/global/temporal/LSParseDateTimeTest.java new file mode 100644 index 000000000..2006937ae --- /dev/null +++ b/src/test/java/ortus/boxlang/runtime/bifs/global/temporal/LSParseDateTimeTest.java @@ -0,0 +1,163 @@ + +/** + * [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.bifs.global.temporal; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import ortus.boxlang.runtime.BoxRuntime; +import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.ScriptingBoxContext; +import ortus.boxlang.runtime.dynamic.casters.IntegerCaster; +import ortus.boxlang.runtime.scopes.IScope; +import ortus.boxlang.runtime.scopes.Key; +import ortus.boxlang.runtime.scopes.VariablesScope; +import ortus.boxlang.runtime.types.DateTime; + +public class LSParseDateTimeTest { + + static BoxRuntime instance; + static IBoxContext context; + static IScope variables; + static Key result = new Key( "result" ); + + @BeforeAll + public static void setUp() { + instance = BoxRuntime.getInstance( true ); + context = new ScriptingBoxContext( instance.getRuntimeContext() ); + variables = context.getScopeNearby( VariablesScope.name ); + } + + @AfterAll + public static void teardown() { + instance.shutdown(); + } + + @BeforeEach + public void setupEach() { + variables.clear(); + } + + @DisplayName( "It tests the BIF LSParseDateTime with a full ISO including offset" ) + @Test + public void testLSParseDateTimeFullISO() { + instance.executeSource( + """ + result = lsParseDateTime( "2024-01-14T00:00:01.0001Z" ); + """, + context ); + DateTime result = ( DateTime ) variables.get( Key.of( "result" ) ); + assertThat( result ).isInstanceOf( DateTime.class ); + assertThat( result.toString() ).isInstanceOf( String.class ); + assertThat( IntegerCaster.cast( result.format( "yyyy" ) ) ).isEqualTo( 2024 ); + assertThat( IntegerCaster.cast( result.format( "M" ) ) ).isEqualTo( 1 ); + assertThat( IntegerCaster.cast( result.format( "d" ) ) ).isEqualTo( 14 ); + assertThat( IntegerCaster.cast( result.format( "H" ) ) ).isEqualTo( 0 ); + assertThat( IntegerCaster.cast( result.format( "m" ) ) ).isEqualTo( 0 ); + assertThat( IntegerCaster.cast( result.format( "s" ) ) ).isEqualTo( 1 ); + assertThat( IntegerCaster.cast( result.format( "n" ) ) ).isEqualTo( 100000 ); + } + + @DisplayName( "It tests the BIF LSParseDateTime with a full ISO without offset" ) + @Test + public void testLSParseDateTimeNoOffset() { + instance.executeSource( + """ + result = lsParseDateTime( "2024-01-14T00:00:01.0001" ); + """, + context ); + DateTime result = ( DateTime ) variables.get( Key.of( "result" ) ); + assertThat( result ).isInstanceOf( DateTime.class ); + assertThat( result.toString() ).isInstanceOf( String.class ); + assertThat( IntegerCaster.cast( result.format( "yyyy" ) ) ).isEqualTo( 2024 ); + assertThat( IntegerCaster.cast( result.format( "M" ) ) ).isEqualTo( 1 ); + assertThat( IntegerCaster.cast( result.format( "d" ) ) ).isEqualTo( 14 ); + assertThat( IntegerCaster.cast( result.format( "H" ) ) ).isEqualTo( 0 ); + assertThat( IntegerCaster.cast( result.format( "m" ) ) ).isEqualTo( 0 ); + assertThat( IntegerCaster.cast( result.format( "s" ) ) ).isEqualTo( 1 ); + assertThat( IntegerCaster.cast( result.format( "n" ) ) ).isEqualTo( 100000 ); + } + + @DisplayName( "It tests the BIF LSParseDateTime with without any time" ) + @Test + public void testLSParseDateTimeNoTime() { + instance.executeSource( + """ + result = lsParseDateTime( "2024-01-14" ); + """, + context ); + DateTime result = ( DateTime ) variables.get( Key.of( "result" ) ); + assertThat( result ).isInstanceOf( DateTime.class ); + assertThat( result.toString() ).isInstanceOf( String.class ); + assertThat( IntegerCaster.cast( result.format( "yyyy" ) ) ).isEqualTo( 2024 ); + assertThat( IntegerCaster.cast( result.format( "M" ) ) ).isEqualTo( 1 ); + assertThat( IntegerCaster.cast( result.format( "d" ) ) ).isEqualTo( 14 ); + assertThat( IntegerCaster.cast( result.format( "H" ) ) ).isEqualTo( 0 ); + assertThat( IntegerCaster.cast( result.format( "m" ) ) ).isEqualTo( 0 ); + assertThat( IntegerCaster.cast( result.format( "s" ) ) ).isEqualTo( 0 ); + assertThat( IntegerCaster.cast( result.format( "n" ) ) ).isEqualTo( 0 ); + } + + @DisplayName( "It tests the BIF LSParseDateTime with a full ISO including offset and locale argument" ) + @Test + public void testLSParseDateTimeFullISOLocale() { + instance.executeSource( + """ + result = lsParseDateTime( "2024-01-14T00:00:01.0001Z", "en-US" ); + """, + context ); + DateTime result = ( DateTime ) variables.get( Key.of( "result" ) ); + assertThat( result ).isInstanceOf( DateTime.class ); + assertThat( result.toString() ).isInstanceOf( String.class ); + assertThat( IntegerCaster.cast( result.format( "yyyy" ) ) ).isEqualTo( 2024 ); + assertThat( IntegerCaster.cast( result.format( "M" ) ) ).isEqualTo( 1 ); + assertThat( IntegerCaster.cast( result.format( "d" ) ) ).isEqualTo( 14 ); + assertThat( IntegerCaster.cast( result.format( "H" ) ) ).isEqualTo( 0 ); + assertThat( IntegerCaster.cast( result.format( "m" ) ) ).isEqualTo( 0 ); + assertThat( IntegerCaster.cast( result.format( "s" ) ) ).isEqualTo( 1 ); + assertThat( IntegerCaster.cast( result.format( "n" ) ) ).isEqualTo( 100000 ); + } + + @DisplayName( "It tests the BIF LSParseDateTime using a localized russian format" ) + @Test + public void testLSParseDateTimeRussian() { + instance.executeSource( + """ + result = lsParseDateTime( "14.01.2024", "ru-RU", "dd.MM.yyyy" ); + """, + context ); + DateTime result = ( DateTime ) variables.get( Key.of( "result" ) ); + assertThat( result ).isInstanceOf( DateTime.class ); + assertThat( result.toString() ).isInstanceOf( String.class ); + assertThat( IntegerCaster.cast( result.format( "yyyy" ) ) ).isEqualTo( 2024 ); + assertThat( IntegerCaster.cast( result.format( "M" ) ) ).isEqualTo( 1 ); + assertThat( IntegerCaster.cast( result.format( "d" ) ) ).isEqualTo( 14 ); + assertThat( IntegerCaster.cast( result.format( "H" ) ) ).isEqualTo( 0 ); + assertThat( IntegerCaster.cast( result.format( "m" ) ) ).isEqualTo( 0 ); + assertThat( IntegerCaster.cast( result.format( "s" ) ) ).isEqualTo( 0 ); + assertThat( IntegerCaster.cast( result.format( "n" ) ) ).isEqualTo( 0 ); + } + +}