Skip to content

Commit

Permalink
WIP on ls datetime
Browse files Browse the repository at this point in the history
  • Loading branch information
jclausen committed Jan 15, 2024
1 parent a61fa01 commit b3c4842
Show file tree
Hide file tree
Showing 4 changed files with 317 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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;
}

}
1 change: 1 addition & 0 deletions src/main/java/ortus/boxlang/runtime/scopes/Key.java
Original file line number Diff line number Diff line change
Expand Up @@ -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" );
Expand Down
56 changes: 56 additions & 0 deletions src/main/java/ortus/boxlang/runtime/types/DateTime.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 );
}

}

0 comments on commit b3c4842

Please sign in to comment.