Skip to content

Commit

Permalink
implments LSParseDateTime BIF
Browse files Browse the repository at this point in the history
  • Loading branch information
jclausen committed Jan 16, 2024
1 parent b3c4842 commit 470f793
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ public LSParseDateTime() {
*
* @argument.date the date, datetime string or an object
*
* @argument.the ISO locale string ( e.g. en-US, en_US, es-SA, es_ES, ru-RU, etc )
*
* @argument.format the format mask to use in parsing
*
* @argument.timezone the timezone to apply to the parsed datetime
Expand All @@ -52,9 +54,20 @@ public Object invoke( IBoxContext context, ArgumentsScope arguments ) {
String locale = arguments.getAsString( Key.locale );
String timezone = arguments.getAsString( Key.timezone );
String format = arguments.getAsString( Key.format );
Locale localeObj = null;
if ( locale != null ) {
var localeParts = locale.split( "-|_| " );
String ISOLang = localeParts[ 0 ];
String ISOCountry = null;
if ( localeParts.length > 1 ) {
ISOCountry = localeParts[ 1 ];
}
localeObj = ISOCountry == null ? new Locale( ISOLang ) : new Locale( ISOLang, ISOCountry );
} else {
localeObj = Locale.getDefault();
}

Locale localeObj = locale == null ? Locale.getDefault() : new Locale( locale.split( " " )[ 0 ] );
ZoneId zoneId = null;
ZoneId zoneId = null;
try {
zoneId = timezone != null ? ZoneId.of( timezone ) : ZoneId.systemDefault();
} catch ( ZoneRulesException e ) {
Expand Down
213 changes: 132 additions & 81 deletions src/main/java/ortus/boxlang/runtime/types/DateTime.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.FormatStyle;
import java.time.temporal.ChronoUnit;
import java.util.Locale;
import java.util.Map;
Expand Down Expand Up @@ -143,7 +144,12 @@ public DateTime( String dateTime, String mask ) {
parsed = ZonedDateTime.of( LocalDateTime.parse( dateTime, getFormatter( mask ) ), 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, getFormatter( mask ) ), LocalTime.MIN ), ZoneId.systemDefault() );
try {
parsed = ZonedDateTime.of( LocalDateTime.of( LocalDate.parse( dateTime, getFormatter( mask ) ), LocalTime.MIN ), ZoneId.systemDefault() );
// last fallback - this is a time only value
} catch ( java.time.format.DateTimeParseException z ) {
parsed = ZonedDateTime.of( LocalDate.MIN, LocalTime.parse( dateTime, getFormatter( mask ) ), ZoneId.systemDefault() );
}
} catch ( Exception x ) {
throw new BoxRuntimeException(
String.format(
Expand All @@ -163,28 +169,52 @@ public DateTime( String dateTime, String mask ) {
}

/**
* Constructor to create DateTime from a datetime string from a specific locale
* Constructor to create DateTime from a string, using the system locale and timezone
*
* @param dateTime - a string representing the date and time
*/
public DateTime( String dateTime ) {
this( dateTime, Locale.getDefault(), ZoneId.systemDefault() );
}

/**
* Constructor to create DateTime from a string with a specified timezone, using the system locale
*
* @param dateTime - a string representing the date and time
* @param timezone - the timezone string
*/
public DateTime( String dateTime, ZoneId timezone ) {
this( dateTime, Locale.getDefault(), timezone );
}

/**
* Constructor to create DateTime from a datetime string from a specific locale and timezone
*
* @param dateTime - a string representing the date and time
* @param locale - a locale object used to assist in parsing the string
* @param timezone The timezone to assign to the string, if an offset or zone is not provided in the value
*/
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 );
parsed = ZonedDateTime.parse( dateTime, getLocaleZonedDateTimeParsers( locale ) );
} 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() );
parsed = ZonedDateTime.of( LocalDateTime.parse( dateTime, getLocaleDateTimeParsers( locale ) ),
timezone );
// 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() );
try {
parsed = ZonedDateTime.of(
LocalDateTime.of( LocalDate.parse( dateTime, getLocaleDateParsers( locale ) ), LocalTime.MIN ),
timezone );
// last fallback - this is a time only value
} catch ( java.time.format.DateTimeParseException z ) {
parsed = ZonedDateTime.of( LocalDate.MIN, LocalTime.parse( dateTime, getLocaleTimeParsers( locale ) ), ZoneId.systemDefault() );
}
} catch ( Exception x ) {
throw new BoxRuntimeException(
String.format(
Expand All @@ -204,78 +234,6 @@ public DateTime( String dateTime, Locale locale, ZoneId timezone ) {
this.wrapped = parsed;
}

/**
* Constructor to create DateTime from a string with a specified timezone
*
* @param dateTime - a string representing the date and time
* @param timezone - the timezone string
*/
public DateTime( String dateTime, ZoneId timezone ) {
ZonedDateTime parsed = null;
try {
parsed = ZonedDateTime.of( LocalDateTime.parse( dateTime ), timezone );
// Second fallback - it is only a date and we need to supply a time
} catch ( java.time.format.DateTimeParseException e ) {
// First fallback - it has a time without a zone
try {
parsed = ZonedDateTime.of( LocalDateTime.of( LocalDate.parse( dateTime ), LocalTime.MIN ), timezone );
// Second fallback - it is only a date and we need to supply a time
} catch ( java.time.format.DateTimeParseException x ) {
parsed = dateTime.contains( "/" )
? ZonedDateTime.of(
LocalDateTime.of( LocalDate.parse( dateTime, getFormatter( dateTime.length() == 10 ? "MM/dd/yyyy" : "MM/dd/yy" ) ), LocalTime.MIN ),
ZoneId.systemDefault() )
: ZonedDateTime.of( LocalDateTime.of( LocalDate.parse( dateTime ), 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 datetime",
dateTime
),
x
);
}
} catch ( Exception e ) {
throw new BoxRuntimeException(
String.format(
"The the date time value of [%s] could not be parsed as a valid date or datetime",
dateTime
), e );
}
this.wrapped = parsed;
}

/**
* Constructor to create DateTime from a string
*
* @param dateTime - a string representing the date and time
*/
public DateTime( String dateTime ) {
ZonedDateTime parsed = null;
try {
parsed = ZonedDateTime.parse( dateTime );
} catch ( java.time.format.DateTimeParseException e ) {
// First fallback - it has a time without a zone
try {
parsed = ZonedDateTime.of( LocalDateTime.parse( dateTime ), ZoneId.systemDefault() );
// Second fallback - it is only a date and we need to supply a time
} catch ( java.time.format.DateTimeParseException x ) {
parsed = dateTime.contains( "/" )
? ZonedDateTime.of(
LocalDateTime.of( LocalDate.parse( dateTime, getFormatter( dateTime.length() == 10 ? "MM/dd/yyyy" : "MM/dd/yy" ) ), LocalTime.MIN ),
ZoneId.systemDefault() )
: ZonedDateTime.of( LocalDateTime.of( LocalDate.parse( dateTime ), 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 datetime",
dateTime
) );
}
}
this.wrapped = parsed;
}

/**
* Constructor to create DateTime from a numerics through millisecond
*
Expand Down Expand Up @@ -715,4 +673,97 @@ public Object dereferenceAndInvoke( IBoxContext context, Key name, Map<Key, Obje
}
}

/**
* Returns a localized set of ZonedDateTime parsers
*
* @param locale the Locale object which informs the formatters/parsers
*
* @return the localized DateTimeFormatter object
*/

private static DateTimeFormatter getLocaleZonedDateTimeParsers( Locale locale ) {
DateTimeFormatterBuilder formatBuilder = new DateTimeFormatterBuilder();
return formatBuilder.parseLenient()
// Localized styles
.appendOptional( DateTimeFormatter.ISO_ZONED_DATE_TIME.withLocale( locale ) )
.appendOptional( DateTimeFormatter.ISO_ZONED_DATE_TIME )
.appendOptional( DateTimeFormatter.ISO_OFFSET_DATE_TIME )
.toFormatter( locale );
}

/**
* Returns a localized set of DateTime parsers
*
* @param locale the Locale object which informs the formatters/parsers
*
* @return the localized DateTimeFormatter object
*/
private static DateTimeFormatter getLocaleDateTimeParsers( Locale locale ) {
DateTimeFormatterBuilder formatBuilder = new DateTimeFormatterBuilder();
return formatBuilder.parseLenient()
.appendOptional( DateTimeFormatter.ofLocalizedDateTime( FormatStyle.SHORT, FormatStyle.SHORT ).withLocale( locale ) )
.appendOptional( DateTimeFormatter.ofLocalizedDateTime( FormatStyle.MEDIUM, FormatStyle.MEDIUM ).withLocale( locale ) )
.appendOptional( DateTimeFormatter.ofLocalizedDateTime( FormatStyle.LONG, FormatStyle.LONG ).withLocale( locale ) )
.appendOptional( DateTimeFormatter.ofLocalizedDateTime( FormatStyle.FULL, FormatStyle.FULL ).withLocale( locale ) )
.appendOptional( DateTimeFormatter.ofLocalizedDate( FormatStyle.SHORT ).withLocale( locale ) )
.appendOptional( DateTimeFormatter.ofLocalizedDate( FormatStyle.MEDIUM ).withLocale( locale ) )
.appendOptional( DateTimeFormatter.ofLocalizedDate( FormatStyle.LONG ).withLocale( locale ) )
.appendOptional( DateTimeFormatter.ofLocalizedDate( FormatStyle.FULL ).withLocale( locale ) )
.appendOptional( DateTimeFormatter.ofLocalizedTime( FormatStyle.SHORT ).withLocale( locale ) )
.appendOptional( DateTimeFormatter.ofLocalizedTime( FormatStyle.MEDIUM ).withLocale( locale ) )
.appendOptional( DateTimeFormatter.ofLocalizedTime( FormatStyle.LONG ).withLocale( locale ) )
.appendOptional( DateTimeFormatter.ofLocalizedTime( FormatStyle.FULL ).withLocale( locale ) )
// Generic styles
.appendOptional( DateTimeFormatter.ofPattern( "yyyy-MM-dd'T'HH:mm:ss.SSS" ) )
.appendOptional( DateTimeFormatter.ofPattern( DEFAULT_DATETIME_FORMAT_MASK ) )
.appendOptional( DateTimeFormatter.ofPattern( ODBC_FORMAT_MASK ) )
.appendOptional( DateTimeFormatter.ISO_INSTANT )
.appendOptional( DateTimeFormatter.ISO_DATE_TIME )
.appendOptional( DateTimeFormatter.ISO_LOCAL_DATE_TIME )
.toFormatter( locale );
}

/**
* Returns a localized set of Date parsers
*
* @param locale the Locale object which informs the formatters/parsers
*
* @return the localized DateTimeFormatter object
*/

private static DateTimeFormatter getLocaleDateParsers( Locale locale ) {
DateTimeFormatterBuilder formatBuilder = new DateTimeFormatterBuilder();
return formatBuilder.parseLenient()
.appendOptional( DateTimeFormatter.ofLocalizedDate( FormatStyle.SHORT ).withLocale( locale ) )
.appendOptional( DateTimeFormatter.ofLocalizedDate( FormatStyle.MEDIUM ).withLocale( locale ) )
.appendOptional( DateTimeFormatter.ofLocalizedDate( FormatStyle.LONG ).withLocale( locale ) )
.appendOptional( DateTimeFormatter.ofLocalizedDate( FormatStyle.FULL ).withLocale( locale ) )
// The ISO date methods don't account for leading zeros :(
.appendOptional( DateTimeFormatter.ofPattern( "yyyy-MM-dd" ) )
.appendOptional( DateTimeFormatter.ofPattern( DEFAULT_DATE_FORMAT_MASK ) )
.appendOptional( DateTimeFormatter.ISO_DATE )
.appendOptional( DateTimeFormatter.ISO_LOCAL_DATE )
.appendOptional( DateTimeFormatter.BASIC_ISO_DATE )
.toFormatter( locale );
}

/**
* Returns a localized set of Time parsers
*
* @param locale the Locale object which informs the formatters/parsers
*
* @return the localized DateTimeFormatter object
*/

private static DateTimeFormatter getLocaleTimeParsers( Locale locale ) {
DateTimeFormatterBuilder formatBuilder = new DateTimeFormatterBuilder();
return formatBuilder.parseLenient()
.appendOptional( DateTimeFormatter.ofLocalizedTime( FormatStyle.SHORT ).withLocale( locale ) )
.appendOptional( DateTimeFormatter.ofLocalizedTime( FormatStyle.MEDIUM ).withLocale( locale ) )
.appendOptional( DateTimeFormatter.ofLocalizedTime( FormatStyle.LONG ).withLocale( locale ) )
.appendOptional( DateTimeFormatter.ofLocalizedTime( FormatStyle.FULL ).withLocale( locale ) )
.appendOptional( DateTimeFormatter.ofPattern( DEFAULT_TIME_FORMAT_MASK ) )
.appendOptional( DateTimeFormatter.ISO_TIME )
.toFormatter( locale );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,48 @@ public void testLSParseDateTimeFullISOLocale() {
public void testLSParseDateTimeRussian() {
instance.executeSource(
"""
result = lsParseDateTime( "14.01.2024", "ru-RU", "dd.MM.yyyy" );
result = lsParseDateTime( "14.01.2024", "ru_RU" );
""",
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 using a localized, Spanish long-form format" )
@Test
public void testLSParseDateTimeSpain() {

instance.executeSource(
"""
result = lsParseDateTime( "14 de enero de 2024", "es-ES" );
""",
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 using traditional chinese format" )
@Test
public void testLSParseDateTimeChinese() {
instance.executeSource(
"""
result = lsParseDateTime( "2024年1月14日", "zh-Hant" );
""",
context );
DateTime result = ( DateTime ) variables.get( Key.of( "result" ) );
Expand Down

0 comments on commit 470f793

Please sign in to comment.