diff --git a/README.md b/README.md index e45b759..3fe12bc 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,17 @@ EvalEx - Java Expression Evaluator [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=ezylang_EvalEx&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=ezylang_EvalEx) [![Maven Central](https://img.shields.io/maven-central/v/com.ezylang/EvalEx.svg?label=Maven%20Central)](https://search.maven.org/search?q=a:%22EvalEx%22) -| :warning: Version 3 of EvalEx is a complete rewrite of the popular expression evaluator. See [the new documentation area](https://ezylang.github.io/EvalEx/concepts/changes.html) for an overview of the changes. | -|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| For a complete documentation, see [the documentation site](https://ezylang.github.io/EvalEx/). | +|------------------------------------------------------------------------------------------------| -EvalEx is a handy expression evaluator for Java, that allows to parse and evaluate expression -strings. +EvalEx is a handy expression evaluator for Java, that allows to parse and evaluate expression strings. ## Key Features: - Supports numerical, boolean, string, date time, duration, array and structure expressions, operations and variables. - Array and structure support: Arrays and structures can be mixed, building arbitrary data structures. +- Supports the NULL datatype. - Uses BigDecimal for numerical calculations. - MathContext and number of decimal places can be configured, with optional automatic rounding. - No dependencies to external libraries. @@ -97,6 +97,35 @@ EvaluationValue result = expression System.out.println(result.getNumberValue()); // prints 6.00 ``` +### Values can be passed in a map + +Instead of specifying the variable values one by one, they can be set by defining a map with names and values and then +passing it to the _withValues()_ method: + +The data conversion of the passed values will automatically be performed through a customizable converter. + +It is also possible to configure a custom data accessor to read and write values. + +```java +Expression expression = new Expression("a+b+c"); + +Map values = new HashMap<>(); +values.put("a", true); +values.put("b", " : "); +values.put("c", 24.7); + +EvaluationValue result = expression.withValues(values).evaluate(); + +System.out.println(result.getStringValue()); // prints "true : 24.7" +``` + +See chapter [Data Types](https://ezylang.github.io/EvalEx/concepts/datatypes.html) for details on the conversion. + +Another option to have EvalEx use your data is to define a custom data accessor. + +See chapter [Data Access](https://ezylang.github.io/EvalEx/customization/data_access.html) for details. + + ### Boolean expressions produce a boolean result: ```java @@ -163,6 +192,25 @@ BigDecimal result = expression.evaluate().getNumberValue(); System.out.println(result); // prints 44.85 ``` +### Calculating with date-time and duration + +Date-tme and duration values are supported. There are functions to create, parse and format these values. +Additionally, the plus and minus operators can be used to e.g. add or subtract durations, or to calculate the +difference between two dates: + +```java +Instant start = Instant.parse("2023-12-05T11:20:00.00Z"); +Instant end = Instant.parse("2023-12-04T23:15:30.00Z"); + +Expression expression = new Expression("start - end"); +EvaluationValue result = expression + .with("start", start) + .and("end", end) + .evaluate(); +System.out.println(result); // will print "EvaluationValue(value=PT12H4M30S, dataType=DURATION)" +``` +See the [Documentation](https://ezylang.github.io/EvalEx/concepts/date_time_duration.html) for more details. + ## EvalEx-big-math [Big-math](https://github.com/eobermuhlner/big-math) is a library by Eric Obermühlner. It provides diff --git a/docs/concepts/changes.md b/docs/concepts/changes.md index 93325ad..bef2712 100644 --- a/docs/concepts/changes.md +++ b/docs/concepts/changes.md @@ -15,9 +15,11 @@ documentation for more information. Most notably new features: * Better boolean support. -* Full support for string operations and functions. -* Full support for multidimensional arrays. -* Full support for nestable structures. +* Support for string operations and functions. +* Support for date-time and duration values and operations. +* Support for _null_ values. +* Support for multidimensional arrays. +* Support for nestable structures. * Structures and arrays can be combined to work with arbitrary data structures. * New data access support, connecting expressions with you data made easy. * New configuration concept. diff --git a/docs/concepts/datatypes.md b/docs/concepts/datatypes.md index 0817dc3..63b433e 100644 --- a/docs/concepts/datatypes.md +++ b/docs/concepts/datatypes.md @@ -66,9 +66,10 @@ object. ### DATE_TIME -Any instance of _java.time.LocalDate_, _java.time.LocalDateTime_, _java.time.ZoneDateTime_ or _java.time.OffsetDateTime_ will automatically be converted to -a _DATE_TIME_ datatype. Conversion will be done by using the current time zone id on the input -object. +Any instance of _java.time.Instant_, _java.time.LocalDate_, _java.time.LocalDateTime_, _java.time.ZoneDateTime_, +_java.time.OffsetDateTime_, _java.util.Date_ or _java.util.Calendar_, will automatically be converted to +a _DATE_TIME_ datatype. If the conversion requires a time zone and no time zone is given in the input object, +then the configured time zone is used. ### DURATION diff --git a/docs/concepts/date_time_duration.md b/docs/concepts/date_time_duration.md index 480aff6..d5045a6 100644 --- a/docs/concepts/date_time_duration.md +++ b/docs/concepts/date_time_duration.md @@ -15,9 +15,14 @@ _java.time.Duration_ values. A _DATE_TIME_ instant is instantaneous point on the time-line, it holds no information about the time zone. Time zones come into play, when converting local dates-times to instants and vice versa. The same instant can have different local date-time values, depending on the destination time zone. + +Functions and operations that require a time zone use either the configured time zone or a zone-id parameter. +Time zones are specified using _java.time.ZoneId_. + The precision of a _DATE_TIME_ is up to nanoseconds. A _DURATION_ is a certain amount of time, like e.g. "3 hours, 15 minutes and 6 seconds". +The textual representation of a _DURATION_ value is the IS-8601 format, e.g. "P2DT3H4M" means 2 days, 3 hours and 4 minutes The smallest amount of a duration is 1 nanosecond. ### Arithmetic operations with _DATE_TIME_ and _DURATION_. @@ -34,7 +39,7 @@ The outcome of the operation depends on the operator types: All other combinations of _DATE_TIME_ and _DURATION_ with other types will do a string concatenation. -Example. Adding a duration to a date-time: +Example: Adding a duration to a date-time: ```java Instant start = Instant.parse("2023-12-03T23:15:30.00Z"); Duration duration = Duration.ofHours(3); @@ -58,7 +63,7 @@ System.out.println(result); // will print "EvaluationValue(value=2023-12-04T02:1 All other combinations of _DATE_TIME_ and _DURATION_ with other types will throw an _EvaluationException_. -Example. Find out the duration between two date-times: +Example: Find out the duration between two date-times: ```java Instant start = Instant.parse("2023-12-05T11:20:00.00Z"); Instant end = Instant.parse("2023-12-04T23:15:30.00Z"); @@ -71,7 +76,7 @@ EvaluationValue result = expression System.out.println(result); // will print "EvaluationValue(value=PT12H4M30S, dataType=DURATION)" ``` -The string representation of a duration is here in SO format, meaning 12 hours, 4 minutes and 30 seconds. +The string representation of a duration is here in ISO-8601 format, meaning 12 hours, 4 minutes and 30 seconds. ### Passing other Date-Time Types as variables @@ -94,4 +99,16 @@ functions to work with date-times. Most of them allow to create, parse and forma See Chapter [Date Time Functions](../references/functions.html#date-time-Functions) -### Configuration Changes \ No newline at end of file +### Configuration Changes + +There are two new configuration parameters for date-time handling. +* Date time formatters: The formatters to use when parsing or formatting date-time values. +* Zone id: The time zone id. By default, the system default zone ID is used. + +See Chapter [configuration](../configuration/configuration.html) + +### New Constants + +There are also some new constants for common formatter patterns. + +See Chapter [Standard Constants](../references/constants.html#standard-constants) diff --git a/docs/concepts/parsing_evaluation.md b/docs/concepts/parsing_evaluation.md index f20ce5d..c9bbf7a 100644 --- a/docs/concepts/parsing_evaluation.md +++ b/docs/concepts/parsing_evaluation.md @@ -108,8 +108,7 @@ EvaluationValue result = expression.withValues(values).evaluate(); System.out.println(result.getNumberValue()); // prints 6.00 ``` -The data conversion of the passed values will automatically be performed through the created -_EvaluationObject_. +The data conversion of the passed values will automatically be performed through a customizable converter. The map can also hold data of different types: ```java @@ -117,12 +116,12 @@ Expression expression = new Expression("a+b+c"); Map values = new HashMap<>(); values.put("a", true); -values.put("b", " "); +values.put("b", " : "); values.put("c", 24.7); EvaluationValue result = expression.withValues(values).evaluate(); -System.out.println(result.getStringValue()); // prints "true 24.7" +System.out.println(result.getStringValue()); // prints "true : 24.7" ``` See chapter [Data Types](datatypes.html) for details on the conversion. diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md index 0509475..b24662e 100644 --- a/docs/configuration/configuration.md +++ b/docs/configuration/configuration.md @@ -54,6 +54,18 @@ The supplier is called whenever a new Expression is created, so that each expres instance of an accessor. Custom implementations of the supplier and data accessor may allow expressions to share the same space. +### Date Time Formatters + +The date-time formatters. When parsing, each format will be tried and the first matching will be used. +For formatting, only the first will be used.\ +By default, the _ExpressionConfiguration.DEFAULT_DATE_TIME_FORMATTERS_ are used: + +* _DateTimeFormatter.ISO_DATE_TIME_ +* _DateTimeFormatter.ISO_DATE_ +* _DateTimeFormatter.ISO_LOCAL_DATE_TIME_ +* _DateTimeFormatter.ISO_LOCAL_DATE_ +* _DateTimeFormatter.RFC_1123_DATE_TIME_ + ### Decimal Places Rounding Specifies the amount of decimal places to round to in each operation or function. @@ -66,6 +78,11 @@ name and _EvaluationValue_ as value. Each expression has a case-insensitive copy constants. See the reference chapter for a list: [Default Constants](../references/constants.html) +### Evaluation Value Converter + +The converter to use when converting different data types to an _EvaluationValue_. +The _DefaultEvaluationValueConverter_ is used by default. + ### Function Dictionary The function dictionary is used to look up the functions that are used in an expression. @@ -135,3 +152,13 @@ E.g. a _BigDecimal_ result of _9.0_ will become _9_ and _-2.120000_ will become Specifies if the structure separator ('.') operator is allowed (default is true). If set to false, the expression will throw a _ParseException_, if the a '.' is encountered in the expression and also no operator or function is defined for this character. + +### Zone Id + +The time zone id. By default, the system default zone ID is used. +```java +ExpressionConfiguration configuration=ExpressionConfiguration.builder() + .zoneId(ZoneId.of("Europe/Berlin")) + .build(); +``` + diff --git a/docs/index.md b/docs/index.md index 8c12ea0..bcc1bcd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,20 +4,14 @@ title: Welcome nav_order: 1 --- -# EvalEx - Java Expression Evaluator - -EvalEx is a handy expression evaluator for Java, that allows to parse and evaluate expression -strings. - -_Version 3 of EvalEx is a complete rewrite of the popular expression evaluator. -See [Major Changes](https://ezylang.github.io/EvalEx/concepts/changes.html) for an overview of the -changes._ +EvalEx is a handy expression evaluator for Java, that allows to parse and evaluate expression strings. ## Key Features: -- Supports numerical, boolean, string, array and structure expressions, operations and variables. +- Supports numerical, boolean, string, date time, duration, array and structure expressions, operations and variables. - Array and structure support: Arrays and structures can be mixed, building arbitrary data structures. +- Supports the NULL datatype. - Uses BigDecimal for numerical calculations. - MathContext and number of decimal places can be configured, with optional automatic rounding. - No dependencies to external libraries. @@ -31,6 +25,11 @@ changes._ x-y) - Lazy evaluation of function parameters (see the IF function) and support of sub-expressions. +## Documentation + +The full documentation for EvalEx can be found +on [GitHub Pages](https://ezylang.github.io/EvalEx/) + ## Discussion For announcements, questions and ideas visit @@ -69,7 +68,7 @@ dependencies { ## Examples -A simple example, that shows how it works in general: +### A simple example, that shows how it works in general: ```java Expression expression = new Expression("1 + 2 / (4 * SQRT(4))"); @@ -79,8 +78,7 @@ EvaluationValue result = expression.evaluate(); System.out.println(result.getNumberValue()); // prints 1.25 ``` -Of course, variables can be specified in the expression and their values can be passed for -evaluation: +### Variables can be specified in the expression and their values can be passed for evaluation: ```java Expression expression = new Expression("(a + b) * (a - b)"); @@ -93,7 +91,36 @@ EvaluationValue result = expression System.out.println(result.getNumberValue()); // prints 6.00 ``` -Boolean expressions produce a boolean result: +### Values can be passed in a map + +Instead of specifying the variable values one by one, they can be set by defining a map with names and values and then +passing it to the _withValues()_ method: + +The data conversion of the passed values will automatically be performed through a customizable converter. + +It is also possible to configure a custom data accessor to read and write values. + +```java +Expression expression = new Expression("a+b+c"); + +Map values = new HashMap<>(); +values.put("a", true); +values.put("b", " : "); +values.put("c", 24.7); + +EvaluationValue result = expression.withValues(values).evaluate(); + +System.out.println(result.getStringValue()); // prints "true : 24.7" +``` + +See chapter [Data Types](https://ezylang.github.io/EvalEx/concepts/datatypes.html) for details on the conversion. + +Another option to have EvalEx use your data is to define a custom data accessor. + +See chapter [Data Access](https://ezylang.github.io/EvalEx/customization/data_access.html) for details. + + +### Boolean expressions produce a boolean result: ```java Expression expression = new Expression("level > 2 || level <= 0"); @@ -105,7 +132,7 @@ EvaluationValue result = expression System.out.println(result.getBooleanValue()); // prints true ``` -Like in Java, strings and text can be mixed: +### Like in Java, strings and text can be mixed: ```java Expression expression = new Expression("\"Hello \" + name + \", you are \" + age") @@ -115,7 +142,10 @@ Expression expression = new Expression("\"Hello \" + name + \", you are \" + age System.out.println(expression.evaluate().getStringValue()); // prints Hello Frank, you are 38 ``` -Arrays (also multidimensional) are supported and can be passed as Java _Lists_: +### Arrays (also multidimensional) are supported and can be passed as Java _Lists_. + +See the [Documentation](https://ezylang.github.io/EvalEx/concepts/datatypes.html#array) +for more details. ```java Expression expression = new Expression("values[i-1] * factors[i-1]"); @@ -129,8 +159,11 @@ EvaluationValue result = expression System.out.println(result.getNumberValue()); // prints 4 ``` -Structures are supported and can be passed as Java _Maps_. -Arrays and Structures can be combined to build arbitrary data structures: +### Structures are supported and can be passed as Java _Maps_. + +Arrays and Structures can be combined to build arbitrary data structures. See +the [Documentation](https://ezylang.github.io/EvalEx/concepts/datatypes.html#structure) +for more details. ```java Map order = new HashMap<>(); @@ -153,6 +186,25 @@ BigDecimal result = expression.evaluate().getNumberValue(); System.out.println(result); // prints 44.85 ``` +### Calculating with date-time and duration + +Date-tme and duration values are supported. There are functions to create, parse and format these values. +Additionally, the plus and minus operators can be used to e.g. add or subtract durations, or to calculate the +difference between two dates: + +```java +Instant start = Instant.parse("2023-12-05T11:20:00.00Z"); +Instant end = Instant.parse("2023-12-04T23:15:30.00Z"); + +Expression expression = new Expression("start - end"); +EvaluationValue result = expression + .with("start", start) + .and("end", end) + .evaluate(); +System.out.println(result); // will print "EvaluationValue(value=PT12H4M30S, dataType=DURATION)" +``` +See the [Documentation](https://ezylang.github.io/EvalEx/concepts/date_time_duration.html) for more details. + ## EvalEx-big-math [Big-math](https://github.com/eobermuhlner/big-math) is a library by Eric Obermühlner. It provides diff --git a/docs/references/constants.md b/docs/references/constants.md index 059a3cf..d7b5bd9 100644 --- a/docs/references/constants.md +++ b/docs/references/constants.md @@ -9,12 +9,14 @@ parent: References Available through the _ExpressionConfiguration.StandardConstants_ constant: -| Name | Value | -|-------|---------------------------------------------------------------| -| TRUE | Boolean.TRUE | -| FALSE | Boolean.FALSE | -| PI | 3.14159265358979323846264338327950288419716939937510582097... | -| E | 2.71828182845904523536028747135266249775724709369995957496... | -| NULL | null | - +| Name | Value | +|---------------------------|---------------------------------------------------------------| +| TRUE | Boolean.TRUE | +| FALSE | Boolean.FALSE | +| PI | 3.14159265358979323846264338327950288419716939937510582097... | +| E | 2.71828182845904523536028747135266249775724709369995957496... | +| NULL | null | +| DT_FORMAT_ISO_DATE_TIME | yyyy-MM-dd'T'HH:mm:ss[.SSS][XXX]['['VV']'] | +| DT_FORMAT_LOCAL_DATE_TIME | yyyy-MM-dd'T'HH:mm:ss[.SSS] | +| DT_FORMAT_LOCAL_DATE | yyyy-MM-dd | diff --git a/docs/references/functions.md b/docs/references/functions.md index 00f8772..56910ae 100644 --- a/docs/references/functions.md +++ b/docs/references/functions.md @@ -78,14 +78,14 @@ Available through the _ExpressionConfiguration.StandardFunctionsDictionary_ cons ### Date Time Functions -| Name | Description | -|---------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| DT_DATE_TIME(year, month, day [, hour, minute, second, nano]) | Returns the corresponding date time value | -| DT_PARSE(value [, format]) | Converts the given string value to a date time value by using the optional format. Without a format, the [ISO_LOCAL_DATE_TIME ](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html#ISO_LOCAL_DATE_TIME), [ISO_LOCAL_DATE](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html#ISO_LOCAL_DATE_TIME) or [ISO_INSTANT](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html#ISO_INSTANT)(for more details on the format, see [JDK DateTimeFormatted documentation](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html) | -| DT_ZONED_PARSE(value [, format]) | Converts the given string zoned date time value to a date time type by using the optional format. Without a format, the [ISO representation is used](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html#ISO_ZONED_DATE_TIME) (for more details on the format, see [JDK DateTimeFormatted documentation](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html) | -| DT_FORMAT(value, [, format]) | Converts the given date time value to a string value by using the optional format. Without a format, the [ISO representation is used](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html#ISO_LOCAL_DATE_TIME). (for more details on the format, see [JDK DateTimeFormatted documentation](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html) | -| DT_EPOCH(value) | Converts the given value to epoch timestamp in millisecond | -| DT_DATE_TIME_EPOCH(value) | Converts the given epoch timestamp value to a date time value | -| DT_DURATION_MILLIS(value) | Converts the given value in millisecond to a duration value | -| DT_DURATION_DAYS(value) | Converts the given days quantity to a duration value | -| DT_DURATION_PARSE(value) | Converts the given duration representation to a duration value (see [JDK Duration#parse documentation](https://docs.oracle.com/javase/11/docs/api/java/time/Duration.html#parse-java.lang.CharSequence-) | +| Name | Description | +|--------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| DT_DATE_NEW(year, month, day [,hour, minute, second, millis, nanos] [,zoneId]) | Returns a new DATE_TIME value with the given parameters. Any optional time zone (string) can be specified, e.g. "Europe/Berlin", or "GMT+02:00. If no zone id is specified, the configured zone id is used. | +| DT_DATE_NEW(millis) | Returns a new DATE_TIME from the epoch of 1970-01-01T00:00:00Z in milliseconds. | +| DT_DATE_PARSE(value [,zoneId] [,format, ...]) | Converts the given string value to a date time value by using the optional time zone and formats. All formats are used until the first matching format. Without a format, the configured formats are used. Time zone can be NULL, the the configured time zone is used. | +| DT_DATE_FORMAT(value, [,format] [,zoneId]) | Formats the given date-time to a string using the given optional format and time zone. Without a format, the first configured format is used. The zone id defaults to the configured zone id. | +| DT_DATE_TO_EPOCH(value) | Converts the given value to epoch timestamp in millisecond. | +| DT_DURATION_NEW(days [,hours, minutes, seconds, nanos]) | Returns a new DURATION value with the given parameters. | +| DT_DURATION_PARSE(value) | Converts the given ISO-8601 duration string representation to a duration value. E.g. "P2DT3H4M" parses 2 days, 3 hours and 4 minutes. | +| DT_DURATION_FROM_MILLIS(millis) | Returns a new DURATION value with the given milliseconds. | +| DT_DURATION_TO_MILLIS(value) | Converts the given duration to a milliseconds value. | diff --git a/src/main/java/com/ezylang/evalex/config/ExpressionConfiguration.java b/src/main/java/com/ezylang/evalex/config/ExpressionConfiguration.java index 67f7785..1e94ac2 100644 --- a/src/main/java/com/ezylang/evalex/config/ExpressionConfiguration.java +++ b/src/main/java/com/ezylang/evalex/config/ExpressionConfiguration.java @@ -15,7 +15,9 @@ */ package com.ezylang.evalex.config; -import com.ezylang.evalex.data.*; +import com.ezylang.evalex.data.DataAccessorIfc; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.data.MapBasedDataAccessor; import com.ezylang.evalex.data.conversion.DefaultEvaluationValueConverter; import com.ezylang.evalex.data.conversion.EvaluationValueConverterIfc; import com.ezylang.evalex.functions.FunctionIfc; @@ -32,10 +34,8 @@ import java.math.BigDecimal; import java.math.MathContext; import java.math.RoundingMode; import java.time.ZoneId; -import java.util.Arrays; -import java.util.Collections; -import java.util.Map; -import java.util.TreeMap; +import java.time.format.DateTimeFormatter; +import java.util.*; import java.util.function.Supplier; import lombok.Builder; import lombok.Getter; @@ -75,8 +75,25 @@ public class ExpressionConfiguration { public static final MathContext DEFAULT_MATH_CONTEXT = new MathContext(68, RoundingMode.HALF_EVEN); - /** The default zone id is the systemd default zone ID. */ - public static final ZoneId DEFAULT_ZONE_ID = ZoneId.systemDefault(); + /** + * The default date time formatters used when parsing a date string. Each format will be tried and + * the first matching will be used. + * + *
    + *
  • {@link DateTimeFormatter#ISO_DATE_TIME} + *
  • {@link DateTimeFormatter#ISO_DATE} + *
  • {@link DateTimeFormatter#ISO_LOCAL_DATE_TIME} + *
  • {@link DateTimeFormatter#ISO_LOCAL_DATE} + *
+ */ + protected static final List DEFAULT_DATE_TIME_FORMATTERS = + new ArrayList<>( + List.of( + DateTimeFormatter.ISO_DATE_TIME, + DateTimeFormatter.ISO_DATE, + DateTimeFormatter.ISO_LOCAL_DATE_TIME, + DateTimeFormatter.ISO_LOCAL_DATE, + DateTimeFormatter.RFC_1123_DATE_TIME)); /** The operator dictionary holds all operators that will be allowed in an expression. */ @Builder.Default @@ -168,14 +185,13 @@ public class ExpressionConfiguration { Map.entry("STR_LOWER", new StringLowerFunction()), Map.entry("STR_UPPER", new StringUpperFunction()), // date time functions - Map.entry("DT_DATE_TIME", new DateTimeFunction()), - Map.entry("DT_PARSE", new DateTimeParseFunction()), - Map.entry("DT_ZONED_PARSE", new ZonedDateTimeParseFunction()), - Map.entry("DT_FORMAT", new DateTimeFormatFunction()), - Map.entry("DT_EPOCH", new DateTimeToEpochFunction()), - Map.entry("DT_DATE_TIME_EPOCH", new DateTimeFromEpochFunction()), - Map.entry("DT_DURATION_MILLIS", new DurationFromMillisFunction()), - Map.entry("DT_DURATION_DAYS", new DurationFromDaysFunction()), + Map.entry("DT_DATE_NEW", new DateTimeNewFunction()), + Map.entry("DT_DATE_PARSE", new DateTimeParseFunction()), + Map.entry("DT_DATE_FORMAT", new DateTimeFormatFunction()), + Map.entry("DT_DATE_TO_EPOCH", new DateTimeToEpochFunction()), + Map.entry("DT_DURATION_NEW", new DurationNewFunction()), + Map.entry("DT_DURATION_FROM_MILLIS", new DurationFromMillisFunction()), + Map.entry("DT_DURATION_TO_MILLIS", new DurationToMillisFunction()), Map.entry("DT_DURATION_PARSE", new DurationParseFunction())); /** The math context to use. */ @@ -216,6 +232,11 @@ public class ExpressionConfiguration { /** * If specified, all results from operations and functions will be rounded to the specified number * of decimal digits, using the MathContexts rounding mode. + * + *

Automatic scaling is disabled by default. When enabled, EvalEx will round all input + * variables, constants, intermediate operation and function results and the final result to the + * specified number of decimal digits, using the current rounding mode. Using a value of + * _DECIMAL_PLACES_ROUNDING_UNLIMITED_ will disable automatic scaling. */ @Builder.Default @Getter private final int decimalPlacesRounding = DECIMAL_PLACES_ROUNDING_UNLIMITED; @@ -231,8 +252,17 @@ public class ExpressionConfiguration { */ @Builder.Default @Getter private final boolean allowOverwriteConstants = true; - /** The time zone id. By default, the system default zone id is used. */ - @Builder.Default @Getter private final ZoneId zoneId = DEFAULT_ZONE_ID; + /** The time zone id. By default, the system default zone ID is used. */ + @Builder.Default @Getter private final ZoneId zoneId = ZoneId.systemDefault(); + + /** + * The date-time formatters. When parsing, each format will be tried and the first matching will + * be used. For formatting, only the first will be used. + * + *

By default, the {@link ExpressionConfiguration#DEFAULT_DATE_TIME_FORMATTERS} are used. + */ + @Builder.Default @Getter + private final List dateTimeFormatters = DEFAULT_DATE_TIME_FORMATTERS; /** The converter to use when converting different data types to an {@link EvaluationValue}. */ @Builder.Default @Getter @@ -308,6 +338,13 @@ public class ExpressionConfiguration { "2.71828182845904523536028747135266249775724709369995957496696762772407663"))); constants.put("NULL", EvaluationValue.nullValue()); + constants.put( + "DT_FORMAT_ISO_DATE_TIME", + EvaluationValue.stringValue("yyyy-MM-dd'T'HH:mm:ss[.SSS][XXX]['['VV']']")); + constants.put( + "DT_FORMAT_LOCAL_DATE_TIME", EvaluationValue.stringValue("yyyy-MM-dd'T'HH:mm:ss[.SSS]")); + constants.put("DT_FORMAT_LOCAL_DATE", EvaluationValue.stringValue("yyyy-MM-dd")); + return constants; } } diff --git a/src/main/java/com/ezylang/evalex/data/conversion/DateTimeConverter.java b/src/main/java/com/ezylang/evalex/data/conversion/DateTimeConverter.java index 95278db..ece92ec 100644 --- a/src/main/java/com/ezylang/evalex/data/conversion/DateTimeConverter.java +++ b/src/main/java/com/ezylang/evalex/data/conversion/DateTimeConverter.java @@ -18,8 +18,12 @@ package com.ezylang.evalex.data.conversion; import com.ezylang.evalex.config.ExpressionConfiguration; import com.ezylang.evalex.data.EvaluationValue; import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalQueries; import java.util.Calendar; import java.util.Date; +import java.util.List; /** Converter to convert to the DATE_TIME data type. */ public class DateTimeConverter implements ConverterIfc { @@ -49,6 +53,41 @@ public class DateTimeConverter implements ConverterIfc { return EvaluationValue.dateTimeValue(instant); } + /** + * Tries to parse a date-time string by trying out each format in the list. The first matching + * result is returned. If none of the formats can be used to parse the string, null + * is returned. + * + * @param value The string to parse. + * @param zoneId The {@link ZoneId} to use for parsing. + * @param formatters The list of formatters. + * @return A parsed {@link Instant} if parsing was successful, else null. + */ + public Instant parseDateTime(String value, ZoneId zoneId, List formatters) { + for (DateTimeFormatter formatter : formatters) { + try { + return parseToInstant(value, zoneId, formatter); + } catch (DateTimeException ignored) { + // ignore + } + } + return null; + } + + private Instant parseToInstant(String value, ZoneId zoneId, DateTimeFormatter formatter) { + TemporalAccessor ta = formatter.parse(value); + ZoneId parsedZoneId = ta.query(TemporalQueries.zone()); + if (parsedZoneId == null) { + LocalDate parsedDate = ta.query(TemporalQueries.localDate()); + LocalTime parsedTime = ta.query(TemporalQueries.localTime()); + if (parsedTime == null) { + parsedTime = parsedDate.atStartOfDay().toLocalTime(); + } + ta = ZonedDateTime.of(parsedDate, parsedTime, zoneId); + } + return Instant.from(ta); + } + @Override public boolean canConvert(Object object) { return (object instanceof Instant diff --git a/src/main/java/com/ezylang/evalex/data/conversion/DefaultEvaluationValueConverter.java b/src/main/java/com/ezylang/evalex/data/conversion/DefaultEvaluationValueConverter.java index b03fa55..6e6d65c 100644 --- a/src/main/java/com/ezylang/evalex/data/conversion/DefaultEvaluationValueConverter.java +++ b/src/main/java/com/ezylang/evalex/data/conversion/DefaultEvaluationValueConverter.java @@ -41,8 +41,8 @@ import java.util.List; * DateDateTimeConverter * CalendarDateTimeConverter * ZonedDateTimeDateTimeConverter - * LocalDateDateTimeConverter - the configured zone id will be used for conversion - * LocalDateTimeDateTimeConverter - the configured zone id will be used for conversion + * LocalDateDateTimeConverter - the configured zone ID will be used for conversion + * LocalDateTimeDateTimeConverter - the configured zone ID will be used for conversion * OffsetDateTimeDateTimeConverter * DurationDurationConverter * ASTNodeASTNode diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/AbstractDateTimeParseFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/AbstractDateTimeParseFunction.java deleted file mode 100644 index eefe27f..0000000 --- a/src/main/java/com/ezylang/evalex/functions/datetime/AbstractDateTimeParseFunction.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - Copyright 2012-2023 Udo Klimaschewski - - 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 com.ezylang.evalex.functions.datetime; - -import com.ezylang.evalex.Expression; -import com.ezylang.evalex.data.EvaluationValue; -import com.ezylang.evalex.functions.AbstractFunction; -import com.ezylang.evalex.parser.Token; -import java.time.Instant; -import java.time.ZoneId; - -public abstract class AbstractDateTimeParseFunction extends AbstractFunction { - @Override - public EvaluationValue evaluate( - Expression expression, Token functionToken, EvaluationValue... parameterValues) { - ZoneId zoneId = expression.getConfiguration().getZoneId(); - Instant instant; - - if (parameterValues.length < 2) { - instant = parse(parameterValues[0].getStringValue(), null, zoneId); - } else { - instant = - parse(parameterValues[0].getStringValue(), parameterValues[1].getStringValue(), zoneId); - } - return expression.convertValue(instant); - } - - protected abstract Instant parse(String value, String format, ZoneId zoneId); -} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeFormatFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeFormatFunction.java index 640a516..6cb7dfd 100644 --- a/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeFormatFunction.java +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeFormatFunction.java @@ -15,6 +15,7 @@ */ package com.ezylang.evalex.functions.datetime; +import com.ezylang.evalex.EvaluationException; import com.ezylang.evalex.Expression; import com.ezylang.evalex.data.EvaluationValue; import com.ezylang.evalex.functions.AbstractFunction; @@ -23,21 +24,46 @@ import com.ezylang.evalex.parser.Token; import java.time.ZoneId; import java.time.format.DateTimeFormatter; -@FunctionParameter(name = "value", isVarArg = true) +/** + * Function to format a DATE_TIME vale. Required parameter is the DATE_TIME value to format. First + * optional parameter is the format to use, using a pattern used by {@link DateTimeFormatter}. If no + * format is given, the first format defined in the configured formats is used. Second optional + * parameter is the zone-id to use wit formatting. Default is the configured zone-id. + */ +@FunctionParameter(name = "parameters", isVarArg = true) public class DateTimeFormatFunction extends AbstractFunction { @Override public EvaluationValue evaluate( - Expression expression, Token functionToken, EvaluationValue... parameterValues) { - String formatted; + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + + DateTimeFormatter formatter = expression.getConfiguration().getDateTimeFormatters().get(0); + if (parameterValues.length > 1) { + formatter = DateTimeFormatter.ofPattern(parameterValues[1].getStringValue()); + } + ZoneId zoneId = expression.getConfiguration().getZoneId(); - if (parameterValues.length < 2) { - formatted = parameterValues[0].getDateTimeValue().atZone(zoneId).toLocalDateTime().toString(); - } else { - DateTimeFormatter formatter = - DateTimeFormatter.ofPattern(parameterValues[1].getStringValue()); - formatted = - parameterValues[0].getDateTimeValue().atZone(zoneId).toLocalDateTime().format(formatter); + if (parameterValues.length == 3) { + zoneId = ZoneIdConverter.convert(functionToken, parameterValues[2].getStringValue()); + } + + return expression.convertValue( + parameterValues[0].getDateTimeValue().atZone(zoneId).format(formatter)); + } + + @Override + public void validatePreEvaluation(Token token, EvaluationValue... parameterValues) + throws EvaluationException { + super.validatePreEvaluation(token, parameterValues); + if (parameterValues.length > 3) { + throw new EvaluationException(token, "Too many parameters"); + } + if (!parameterValues[0].isDateTimeValue()) { + throw new EvaluationException( + token, + String.format( + "Unable to format a '%s' type as a date-time", + parameterValues[0].getDataType().name())); } - return expression.convertValue(formatted); } } diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeFromEpochFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeFromEpochFunction.java deleted file mode 100644 index a0465e4..0000000 --- a/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeFromEpochFunction.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - Copyright 2012-2022 Udo Klimaschewski - - 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 com.ezylang.evalex.functions.datetime; - -import com.ezylang.evalex.Expression; -import com.ezylang.evalex.data.EvaluationValue; -import com.ezylang.evalex.functions.AbstractFunction; -import com.ezylang.evalex.functions.FunctionParameter; -import com.ezylang.evalex.parser.Token; -import java.math.BigDecimal; -import java.time.Instant; - -@FunctionParameter(name = "value") -public class DateTimeFromEpochFunction extends AbstractFunction { - @Override - public EvaluationValue evaluate( - Expression expression, Token functionToken, EvaluationValue... parameterValues) { - BigDecimal millis = parameterValues[0].getNumberValue(); - return expression.convertValue(Instant.ofEpochMilli(millis.longValue())); - } -} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeFunction.java deleted file mode 100644 index 935a548..0000000 --- a/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeFunction.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - Copyright 2012-2022 Udo Klimaschewski - - 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 com.ezylang.evalex.functions.datetime; - -import com.ezylang.evalex.Expression; -import com.ezylang.evalex.data.EvaluationValue; -import com.ezylang.evalex.functions.AbstractFunction; -import com.ezylang.evalex.functions.FunctionParameter; -import com.ezylang.evalex.parser.Token; -import java.time.LocalDateTime; -import java.time.ZoneId; - -@FunctionParameter(name = "values", isVarArg = true, nonNegative = true) -public class DateTimeFunction extends AbstractFunction { - @Override - public EvaluationValue evaluate( - Expression expression, Token functionToken, EvaluationValue... parameterValues) { - int year = parameterValues[0].getNumberValue().intValue(); - int month = parameterValues[1].getNumberValue().intValue(); - int day = parameterValues[2].getNumberValue().intValue(); - int hour = parameterValues.length >= 4 ? parameterValues[3].getNumberValue().intValue() : 0; - int minute = parameterValues.length >= 5 ? parameterValues[4].getNumberValue().intValue() : 0; - int second = parameterValues.length >= 6 ? parameterValues[5].getNumberValue().intValue() : 0; - int nanoOfs = parameterValues.length >= 7 ? parameterValues[6].getNumberValue().intValue() : 0; - - ZoneId zoneId = expression.getConfiguration().getZoneId(); - return expression.convertValue( - LocalDateTime.of(year, month, day, hour, minute, second, nanoOfs) - .atZone(zoneId) - .toInstant()); - } -} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeNewFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeNewFunction.java new file mode 100644 index 0000000..81b16a5 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeNewFunction.java @@ -0,0 +1,113 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + 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 com.ezylang.evalex.functions.datetime; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Set; +import java.util.TimeZone; + +/** + * Creates a new DATE_TIME value with the given parameters. IIf only one parameter is given, it is + * treated as th time in milliseconds from the epoch of 1970-01-01T00:00:00Z and a corresponding + * date/time value is created. Else, A minimum of three parameters (year, month, day) must be + * specified. Optionally the hour, minute, second and nanosecond can be specified. If the last + * parameter is a string value, it is treated as a zone ID. If no zone ID is specified, the + * configured zone ID is used. + */ +@FunctionParameter(name = "values", isVarArg = true, nonNegative = true) +public class DateTimeNewFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + + int parameterLength = parameterValues.length; + + if (parameterLength == 1) { + BigDecimal millis = parameterValues[0].getNumberValue(); + return expression.convertValue(Instant.ofEpochMilli(millis.longValue())); + } + + ZoneId zoneId = expression.getConfiguration().getZoneId(); + if (parameterValues[parameterLength - 1].isStringValue()) { + zoneId = + ZoneIdConverter.convert( + functionToken, parameterValues[parameterLength - 1].getStringValue()); + parameterLength--; + } + + int year = parameterValues[0].getNumberValue().intValue(); + int month = parameterValues[1].getNumberValue().intValue(); + int day = parameterValues[2].getNumberValue().intValue(); + int hour = parameterLength >= 4 ? parameterValues[3].getNumberValue().intValue() : 0; + int minute = parameterLength >= 5 ? parameterValues[4].getNumberValue().intValue() : 0; + int second = parameterLength >= 6 ? parameterValues[5].getNumberValue().intValue() : 0; + int nanoOfs = parameterLength == 7 ? parameterValues[6].getNumberValue().intValue() : 0; + + return expression.convertValue( + LocalDateTime.of(year, month, day, hour, minute, second, nanoOfs) + .atZone(zoneId) + .toInstant()); + } + + @Override + public void validatePreEvaluation(Token token, EvaluationValue... parameterValues) + throws EvaluationException { + + super.validatePreEvaluation(token, parameterValues); + + int parameterLength = parameterValues.length; + + if (parameterLength == 1) { + if (!parameterValues[0].isNumberValue()) { + throw new EvaluationException( + token, "Expected a number value for the time in milliseconds since the epoch"); + } else { + return; + } + } + + if (parameterValues[parameterLength - 1].isStringValue()) { + if (!Set.of(TimeZone.getAvailableIDs()) + .contains(parameterValues[parameterLength - 1].getStringValue())) { + throw new EvaluationException( + token, + "Time zone with id '" + + parameterValues[parameterLength - 1].getStringValue() + + "' not found"); + } + parameterLength--; + } + + if (parameterLength < 3) { + throw new EvaluationException( + token, "A minimum of 3 parameters (year, month, day) is required"); + } + + if (parameterLength > 7) { + throw new EvaluationException(token, "Too many parameters to function"); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeParseFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeParseFunction.java index 65f3b19..fba7392 100644 --- a/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeParseFunction.java +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeParseFunction.java @@ -1,5 +1,5 @@ /* - Copyright 2012-2022 Udo Klimaschewski + Copyright 2012-2023 Udo Klimaschewski Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,50 +15,67 @@ */ package com.ezylang.evalex.functions.datetime; +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.data.conversion.DateTimeConverter; +import com.ezylang.evalex.functions.AbstractFunction; import com.ezylang.evalex.functions.FunctionParameter; -import java.time.*; +import com.ezylang.evalex.parser.Token; +import java.time.Instant; +import java.time.ZoneId; import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.util.Optional; +import java.util.ArrayList; +import java.util.List; -@FunctionParameter(name = "value", isVarArg = true) -public class DateTimeParseFunction extends AbstractDateTimeParseFunction { +/** + * Parses a date-time string to a {@link EvaluationValue.DataType#DATE_TIME} value. + * + *

Optional arguments is the time zone and a list of {@link java.time.format.DateTimeFormatter} + * patterns. Each pattern will be tried to convert the string to a date-time. The first matching + * pattern will be used. If NULL is specified for the time zone, the currently + * configured zone is used. If no formatters a + */ +@FunctionParameter(name = "parameters", isVarArg = true) +public class DateTimeParseFunction extends AbstractFunction { - protected Instant parse(String value, String format, ZoneId zoneId) { - return parseInstant(value) - .or(() -> parseLocalDateTime(value, format, zoneId)) - .or(() -> parseDate(value, format)) - .orElseThrow(() -> new IllegalArgumentException("Unable to parse date/time: " + value)); - } + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + + String value = parameterValues[0].getStringValue(); - private Optional parseLocalDateTime(String value, String format, ZoneId zoneId) { - try { - DateTimeFormatter formatter = - (format == null - ? DateTimeFormatter.ISO_LOCAL_DATE_TIME - : DateTimeFormatter.ofPattern(format)); - return Optional.of(LocalDateTime.parse(value, formatter).atZone(zoneId).toInstant()); - } catch (DateTimeParseException ex) { - return Optional.empty(); + ZoneId zoneId = expression.getConfiguration().getZoneId(); + if (parameterValues.length > 1 && !parameterValues[1].isNullValue()) { + zoneId = ZoneIdConverter.convert(functionToken, parameterValues[1].getStringValue()); } - } - private Optional parseDate(String value, String format) { - try { - DateTimeFormatter formatter = - (format == null ? DateTimeFormatter.ISO_LOCAL_DATE : DateTimeFormatter.ofPattern(format)); - LocalDate localDate = LocalDate.parse(value, formatter); - return Optional.of(localDate.atStartOfDay().atOffset(ZoneOffset.UTC).toInstant()); - } catch (DateTimeParseException ex) { - return Optional.empty(); + List formatters; + + if (parameterValues.length > 2) { + formatters = new ArrayList<>(); + for (int i = 2; i < parameterValues.length; i++) { + try { + formatters.add(DateTimeFormatter.ofPattern(parameterValues[i].getStringValue())); + } catch (IllegalArgumentException ex) { + throw new EvaluationException( + functionToken, + String.format( + "Illegal date-time format in parameter %d: '%s'", + i + 1, parameterValues[i].getStringValue())); + } + } + } else { + formatters = expression.getConfiguration().getDateTimeFormatters(); } - } + DateTimeConverter converter = new DateTimeConverter(); + Instant instant = converter.parseDateTime(value, zoneId, formatters); - private Optional parseInstant(String value) { - try { - return Optional.of(Instant.parse(value)); - } catch (DateTimeParseException ex) { - return Optional.empty(); + if (instant == null) { + throw new EvaluationException( + functionToken, String.format("Unable to parse date-time string '%s'", value)); } + return EvaluationValue.dateTimeValue(instant); } } diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeToEpochFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeToEpochFunction.java index 7728913..c383786 100644 --- a/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeToEpochFunction.java +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeToEpochFunction.java @@ -21,6 +21,7 @@ import com.ezylang.evalex.functions.AbstractFunction; import com.ezylang.evalex.functions.FunctionParameter; import com.ezylang.evalex.parser.Token; +/** Function to convert a DATE_TIME value to milliseconds sind the epoch of 1970-01-01T00:00:00Z. */ @FunctionParameter(name = "value") public class DateTimeToEpochFunction extends AbstractFunction { @Override diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DurationFromMillisFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DurationFromMillisFunction.java index c0ed23f..a91c011 100644 --- a/src/main/java/com/ezylang/evalex/functions/datetime/DurationFromMillisFunction.java +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DurationFromMillisFunction.java @@ -23,6 +23,7 @@ import com.ezylang.evalex.parser.Token; import java.math.BigDecimal; import java.time.Duration; +/** Converts the given milliseconds to a DURATION value. */ @FunctionParameter(name = "value") public class DurationFromMillisFunction extends AbstractFunction { @Override diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DurationNewFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DurationNewFunction.java new file mode 100644 index 0000000..6cf184b --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DurationNewFunction.java @@ -0,0 +1,57 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + 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 com.ezylang.evalex.functions.datetime; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; +import java.time.Duration; + +/** + * Function to create a new Duration. First parameter is required and specifies the number of days. + * All other parameters are optional and specify hours, minutes, seconds, milliseconds and + * nanoseconds. + */ +@FunctionParameter(name = "parameters", isVarArg = true) +public class DurationNewFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + + int parameterLength = parameterValues.length; + + int days = parameterValues[0].getNumberValue().intValue(); + int hours = parameterLength >= 2 ? parameterValues[1].getNumberValue().intValue() : 0; + int minutes = parameterLength >= 3 ? parameterValues[2].getNumberValue().intValue() : 0; + int seconds = parameterLength >= 4 ? parameterValues[3].getNumberValue().intValue() : 0; + int millis = parameterLength >= 5 ? parameterValues[4].getNumberValue().intValue() : 0; + int nanos = parameterLength == 6 ? parameterValues[5].getNumberValue().intValue() : 0; + + Duration duration = + Duration.ofDays(days) + .plusHours(hours) + .plusMinutes(minutes) + .plusSeconds(seconds) + .plusMillis(millis) + .plusNanos(nanos); + + return expression.convertValue(duration); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DurationParseFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DurationParseFunction.java index a5fda50..e74a7f1 100644 --- a/src/main/java/com/ezylang/evalex/functions/datetime/DurationParseFunction.java +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DurationParseFunction.java @@ -22,6 +22,10 @@ import com.ezylang.evalex.functions.FunctionParameter; import com.ezylang.evalex.parser.Token; import java.time.Duration; +/** + * Converts the given ISO-8601 duration string representation to a duration value. E.g. "P2DT3H4M" + * parses 2 days, 3 hours and 4 minutes. + */ @FunctionParameter(name = "value") public class DurationParseFunction extends AbstractFunction { @Override diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DurationFromDaysFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DurationToMillisFunction.java similarity index 80% rename from src/main/java/com/ezylang/evalex/functions/datetime/DurationFromDaysFunction.java rename to src/main/java/com/ezylang/evalex/functions/datetime/DurationToMillisFunction.java index fb27f18..1c6ce79 100644 --- a/src/main/java/com/ezylang/evalex/functions/datetime/DurationFromDaysFunction.java +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DurationToMillisFunction.java @@ -20,15 +20,13 @@ import com.ezylang.evalex.data.EvaluationValue; import com.ezylang.evalex.functions.AbstractFunction; import com.ezylang.evalex.functions.FunctionParameter; import com.ezylang.evalex.parser.Token; -import java.math.BigDecimal; -import java.time.Duration; +/** Converts a DURATION value to teh amount of milliseconds. */ @FunctionParameter(name = "value") -public class DurationFromDaysFunction extends AbstractFunction { +public class DurationToMillisFunction extends AbstractFunction { @Override public EvaluationValue evaluate( Expression expression, Token functionToken, EvaluationValue... parameterValues) { - BigDecimal days = parameterValues[0].getNumberValue(); - return expression.convertValue(Duration.ofDays(days.longValue())); + return expression.convertValue(parameterValues[0].getDurationValue().toMillis()); } } diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/ZoneIdConverter.java b/src/main/java/com/ezylang/evalex/functions/datetime/ZoneIdConverter.java new file mode 100644 index 0000000..392a436 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/ZoneIdConverter.java @@ -0,0 +1,50 @@ +/* + Copyright 2012-2023 Udo Klimaschewski + + 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 com.ezylang.evalex.functions.datetime; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.parser.Token; +import java.time.DateTimeException; +import java.time.ZoneId; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +/** Validates and converts a zone ID. */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ZoneIdConverter { + + /** + * Converts a zone ID string to a {@link ZoneId}. Throws an {@link EvaluationException} if + * conversion fails. + * + * @param referenceToken The token for the error message, usually the function token. + * @param zoneIdString The zone IDS string to convert. + * @return The converted {@link ZoneId}. + * @throws EvaluationException In case the zone ID can't be converted. + */ + public static ZoneId convert(Token referenceToken, String zoneIdString) + throws EvaluationException { + try { + return ZoneId.of(zoneIdString); + } catch (DateTimeException exception) { + throw new EvaluationException( + referenceToken, + String.format( + "Unable to convert zone string '%s' to a zone ID: %s", + referenceToken.getValue(), exception.getMessage())); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/ZonedDateTimeParseFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/ZonedDateTimeParseFunction.java deleted file mode 100644 index 5a2b2e3..0000000 --- a/src/main/java/com/ezylang/evalex/functions/datetime/ZonedDateTimeParseFunction.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - Copyright 2012-2022 Udo Klimaschewski - - 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 com.ezylang.evalex.functions.datetime; - -import com.ezylang.evalex.functions.FunctionParameter; -import java.time.Instant; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.util.Optional; - -@FunctionParameter(name = "value", isVarArg = true) -public class ZonedDateTimeParseFunction extends AbstractDateTimeParseFunction { - protected Instant parse(String value, String format, ZoneId zoneId) { - return parseZonedDateTime(value, format, zoneId) - .orElseThrow( - () -> new IllegalArgumentException("Unable to parse zoned date/time: " + value)); - } - - private Optional parseZonedDateTime(String value, String format, ZoneId zoneId) { - try { - DateTimeFormatter formatter = - (format == null - ? DateTimeFormatter.ISO_ZONED_DATE_TIME - : DateTimeFormatter.ofPattern(format)) - .withZone(zoneId); - ZonedDateTime zonedDateTime = ZonedDateTime.parse(value, formatter); - return Optional.of(zonedDateTime.toInstant()); - } catch (DateTimeParseException ex) { - return Optional.empty(); - } - } -} diff --git a/src/test/java/com/ezylang/evalex/BaseEvaluationTest.java b/src/test/java/com/ezylang/evalex/BaseEvaluationTest.java index fa73196..69a29c1 100644 --- a/src/test/java/com/ezylang/evalex/BaseEvaluationTest.java +++ b/src/test/java/com/ezylang/evalex/BaseEvaluationTest.java @@ -16,7 +16,6 @@ package com.ezylang.evalex; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.ezylang.evalex.config.ExpressionConfiguration; import com.ezylang.evalex.config.TestConfigurationProvider; @@ -42,11 +41,6 @@ public abstract class BaseEvaluationTest { .isEqualTo(expectedResult); } - protected void assertExpressionThrowsException( - String expression, String message, ExpressionConfiguration expressionConfiguration) { - assertThatThrownBy(() -> evaluate(expression, expressionConfiguration)).hasMessage(message); - } - private EvaluationValue evaluate(String expressionString, ExpressionConfiguration configuration) throws EvaluationException, ParseException { Expression expression = new Expression(expressionString, configuration); diff --git a/src/test/java/com/ezylang/evalex/ExpressionEvaluatorConstantsTest.java b/src/test/java/com/ezylang/evalex/ExpressionEvaluatorConstantsTest.java index 3b76246..61e19f6 100644 --- a/src/test/java/com/ezylang/evalex/ExpressionEvaluatorConstantsTest.java +++ b/src/test/java/com/ezylang/evalex/ExpressionEvaluatorConstantsTest.java @@ -32,14 +32,17 @@ class ExpressionEvaluatorConstantsTest extends BaseExpressionEvaluatorTest { @ParameterizedTest @CsvSource( - delimiter = ':', + delimiter = '|', value = { - "TRUE : true", - "true : true", - "False : false", - "PI : " + "TRUE | true", + "true | true", + "False | false", + "PI | " + " 3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679", - "e : 2.71828182845904523536028747135266249775724709369995957496696762772407663", + "e | 2.71828182845904523536028747135266249775724709369995957496696762772407663", + "DT_FORMAT_ISO_DATE_TIME | yyyy-MM-dd'T'HH:mm:ss[.SSS][XXX]['['VV']']", + "DT_FORMAT_LOCAL_DATE_TIME | yyyy-MM-dd'T'HH:mm:ss[.SSS]", + "DT_FORMAT_LOCAL_DATE | yyyy-MM-dd" }) void testDefaultConstants(String expression, String expectedResult) throws ParseException, EvaluationException { diff --git a/src/test/java/com/ezylang/evalex/config/TestConfigurationProvider.java b/src/test/java/com/ezylang/evalex/config/TestConfigurationProvider.java index 403d129..c8072ed 100644 --- a/src/test/java/com/ezylang/evalex/config/TestConfigurationProvider.java +++ b/src/test/java/com/ezylang/evalex/config/TestConfigurationProvider.java @@ -24,12 +24,15 @@ import com.ezylang.evalex.operators.PostfixOperator; import com.ezylang.evalex.operators.PrefixOperator; import com.ezylang.evalex.parser.Token; import java.math.BigDecimal; +import java.time.ZoneId; import java.util.Map; public class TestConfigurationProvider { public static final ExpressionConfiguration StandardConfigurationWithAdditionalTestOperators = - ExpressionConfiguration.defaultConfiguration() + ExpressionConfiguration.builder() + .zoneId(ZoneId.of("Europe/Berlin")) + .build() .withAdditionalOperators( Map.entry("++", new PrefixPlusPlusOperator()), Map.entry("++", new PostfixPlusPlusOperator()), diff --git a/src/test/java/com/ezylang/evalex/data/conversion/DuratinCoverterTest.java b/src/test/java/com/ezylang/evalex/data/conversion/DurationConverterTest.java similarity index 98% rename from src/test/java/com/ezylang/evalex/data/conversion/DuratinCoverterTest.java rename to src/test/java/com/ezylang/evalex/data/conversion/DurationConverterTest.java index 128cd75..dde75da 100644 --- a/src/test/java/com/ezylang/evalex/data/conversion/DuratinCoverterTest.java +++ b/src/test/java/com/ezylang/evalex/data/conversion/DurationConverterTest.java @@ -23,7 +23,7 @@ import java.math.BigDecimal; import java.time.Duration; import org.junit.jupiter.api.Test; -class DuratinCoverterTest { +class DurationConverterTest { private final ExpressionConfiguration defaultConfiguration = ExpressionConfiguration.defaultConfiguration(); diff --git a/src/test/java/com/ezylang/evalex/functions/datetime/DateTimeConstantsTest.java b/src/test/java/com/ezylang/evalex/functions/datetime/DateTimeConstantsTest.java new file mode 100644 index 0000000..436540d --- /dev/null +++ b/src/test/java/com/ezylang/evalex/functions/datetime/DateTimeConstantsTest.java @@ -0,0 +1,83 @@ +/* + Copyright 2012-2023 Udo Klimaschewski + + 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 com.ezylang.evalex.functions.datetime; + +import com.ezylang.evalex.BaseEvaluationTest; +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.config.TestConfigurationProvider; +import com.ezylang.evalex.parser.ParseException; +import java.time.ZoneId; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class DateTimeConstantsTest extends BaseEvaluationTest { + + private static final ExpressionConfiguration DateTimeTestConfiguration = + TestConfigurationProvider.StandardConfigurationWithAdditionalTestOperators.toBuilder() + .zoneId(ZoneId.of("Europe/Berlin")) + .build(); + + @ParameterizedTest + @CsvSource( + delimiter = '|', + value = { + "DT_DATE_PARSE(\"2011-12-03T10:15:30\", NULL, DT_FORMAT_ISO_DATE_TIME) |" + + " 2011-12-03T09:15:30Z", + "DT_DATE_PARSE(\"2011-12-03T10:15:30.123\", NULL, DT_FORMAT_ISO_DATE_TIME) |" + + " 2011-12-03T09:15:30.123Z", + "DT_DATE_PARSE(\"2011-12-03T10:15:30+03:30\", NULL, DT_FORMAT_ISO_DATE_TIME) |" + + " 2011-12-03T06:45:30Z", + "DT_DATE_PARSE(\"2011-12-03T10:15:30.456+03:30\", NULL, DT_FORMAT_ISO_DATE_TIME) |" + + " 2011-12-03T06:45:30.456Z", + "DT_DATE_PARSE(\"2011-12-03T10:15:30[America/New_York]\", NULL, DT_FORMAT_ISO_DATE_TIME) |" + + " 2011-12-03T15:15:30Z", + "DT_DATE_PARSE(\"2011-12-03T10:15:30.345[America/New_York]\", NULL," + + " DT_FORMAT_ISO_DATE_TIME) | 2011-12-03T15:15:30.345Z", + "DT_DATE_PARSE(\"2011-12-03T10:15:30.345-05:00[America/New_York]\", NULL," + + " DT_FORMAT_ISO_DATE_TIME) | 2011-12-03T15:15:30.345Z", + "DT_DATE_PARSE(\"2011-12-03T10:15:30-05:00[America/New_York]\", NULL," + + " DT_FORMAT_ISO_DATE_TIME) | 2011-12-03T15:15:30Z", + "DT_DATE_PARSE(\"2011-12-03\", NULL, DT_FORMAT_LOCAL_DATE) | 2011-12-02T23:00:00Z", + "DT_DATE_PARSE(\"2011-12-03\", \"Z\", DT_FORMAT_LOCAL_DATE) | 2011-12-03T00:00:00Z", + "DT_DATE_PARSE(\"2011-12-03T17:30:25\", NULL, DT_FORMAT_LOCAL_DATE_TIME) |" + + " 2011-12-03T16:30:25Z", + "DT_DATE_PARSE(\"2011-12-03T17:30:25.456\", NULL, DT_FORMAT_LOCAL_DATE_TIME) |" + + " 2011-12-03T16:30:25.456Z", + }) + void testDateTimeParse(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult, DateTimeTestConfiguration); + } + + @ParameterizedTest + @CsvSource( + delimiter = '|', + value = { + "DT_DATE_FORMAT(DT_DATE_PARSE(\"2011-12-03T10:15:30.345-05:00[America/New_York]\"),DT_FORMAT_ISO_DATE_TIME)" + + " | 2011-12-03T16:15:30.345+01:00[Europe/Berlin]", + "DT_DATE_FORMAT(DT_DATE_PARSE(\"2011-12-03T10:15:30.345-05:00[America/New_York]\"),DT_FORMAT_ISO_DATE_TIME," + + " \"America/New_York\") | 2011-12-03T10:15:30.345-05:00[America/New_York]", + "DT_DATE_FORMAT(DT_DATE_PARSE(\"2011-12-03T10:15:30.345-05:00[America/New_York]\"),DT_FORMAT_LOCAL_DATE)" + + " | 2011-12-03", + "DT_DATE_FORMAT(DT_DATE_PARSE(\"2011-12-03T10:15:30.345-05:00[America/New_York]\"),DT_FORMAT_LOCAL_DATE_TIME)" + + " | 2011-12-03T16:15:30.345", + }) + void testDateTimeFormat(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult, DateTimeTestConfiguration); + } +} diff --git a/src/test/java/com/ezylang/evalex/functions/datetime/DateTimeFunctionsTest.java b/src/test/java/com/ezylang/evalex/functions/datetime/DateTimeFunctionsTest.java index 521d544..cc465f5 100644 --- a/src/test/java/com/ezylang/evalex/functions/datetime/DateTimeFunctionsTest.java +++ b/src/test/java/com/ezylang/evalex/functions/datetime/DateTimeFunctionsTest.java @@ -15,61 +15,128 @@ */ package com.ezylang.evalex.functions.datetime; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + import com.ezylang.evalex.BaseEvaluationTest; import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; import com.ezylang.evalex.config.ExpressionConfiguration; import com.ezylang.evalex.config.TestConfigurationProvider; import com.ezylang.evalex.parser.ParseException; import java.time.ZoneId; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; class DateTimeFunctionsTest extends BaseEvaluationTest { private static final ExpressionConfiguration DateTimeTestConfiguration = TestConfigurationProvider.StandardConfigurationWithAdditionalTestOperators.toBuilder() - .zoneId(ZoneId.of("UTC+2")) + .zoneId(ZoneId.of("Europe/Berlin")) .build(); @ParameterizedTest @CsvSource( delimiter = '|', value = { - "DT_PARSE(\"2022-10-30T11:50:20Z\") | 2022-10-30T11:50:20Z", - "DT_PARSE(\"2022-10-30T11:50:20\") | 2022-10-30T09:50:20Z", - "DT_PARSE(\"2022-10-30T11:50:20.000000030\") | 2022-10-30T09:50:20.000000030Z", - "DT_PARSE(\"2022-10-30\") | 2022-10-30T00:00:00Z", - "DT_PARSE(\"30/10/2022 11:50:20\", \"dd/MM/yyyy HH:mm:ss\") | 2022-10-30T09:50:20Z", - "DT_PARSE(\"30/10/2022\",\"dd/MM/yyyy\") | 2022-10-30T00:00:00Z", + "DT_DATE_NEW(2022,10,30) | 2022-10-29T22:00:00Z", + "DT_DATE_NEW(2022,09,30,11) | 2022-09-30T09:00:00Z", + "DT_DATE_NEW(2022,10,30,11) | 2022-10-30T10:00:00Z", + "DT_DATE_NEW(2022,10,30,11,50,20) | 2022-10-30T10:50:20Z", + "DT_DATE_NEW(2022,10,30,11,50,20,30) | 2022-10-30T10:50:20.000000030Z", + "DT_DATE_NEW(2022,10,30,\"Africa/Nairobi\") | 2022-10-29T21:00:00Z", + "DT_DATE_NEW(2022,10,30,11,\"Africa/Nairobi\") | 2022-10-30T08:00:00Z", + "DT_DATE_NEW(2022,10,30,11,50,\"Africa/Nairobi\") | 2022-10-30T08:50:00Z", + "DT_DATE_NEW(2022,10,30,11,50,20,\"Africa/Nairobi\") | 2022-10-30T08:50:20Z", + "DT_DATE_NEW(2022,10,30,11,50,20,30,\"Africa/Nairobi\") | 2022-10-30T08:50:20.000000030Z" }) - void testDateTimeParse(String expression, String expectedResult) + void testDateTimeNew(String expression, String expectedResult) throws EvaluationException, ParseException { assertExpressionHasExpectedResult(expression, expectedResult, DateTimeTestConfiguration); } + @ParameterizedTest + @ValueSource( + strings = { + "DT_DATE_NEW(2022,10)", + "DT_DATE_NEW(2022,10,\"Africa/Nairobi\")", + "DT_DATE_NEW(2022,\"Africa/Nairobi\")" + }) + void testDateTimeNewTooFewParameters(String expression) { + assertThatThrownBy(() -> new Expression(expression).evaluate()) + .isInstanceOf(EvaluationException.class) + .hasMessage("A minimum of 3 parameters (year, month, day) is required"); + } + + @ParameterizedTest + @ValueSource( + strings = { + "DT_DATE_NEW(2022,10,30,11,50,20,30,40)", + "DT_DATE_NEW(2022,10,30,11,50,20,30,40,50)", + "DT_DATE_NEW(2022,10,30,11,50,20,30,40,\"Africa/Nairobi\")", + "DT_DATE_NEW(2022,10,30,11,50,20,30,40,50,\"Africa/Nairobi\")" + }) + void testDateTimeNewTooManyParameters(String expression) { + assertThatThrownBy(() -> new Expression(expression).evaluate()) + .isInstanceOf(EvaluationException.class) + .hasMessage("Too many parameters to function"); + } + + @Test + void testDateTimeNewWrongMillisNewDate() { + assertThatThrownBy(() -> new Expression("DT_DATE_NEW(\"nan\")").evaluate()) + .isInstanceOf(EvaluationException.class) + .hasMessage("Expected a number value for the time in milliseconds since the epoch"); + } + + @Test + void testDateTimeNewZoneIdNotFound() { + assertThatThrownBy( + () -> new Expression("DT_DATE_NEW(2022,10,30,\"Mars/Olympus-Mons\")").evaluate()) + .isInstanceOf(EvaluationException.class) + .hasMessage("Time zone with id 'Mars/Olympus-Mons' not found"); + } + @ParameterizedTest @CsvSource( delimiter = '|', value = { - "DT_PARSE(\"NOT A DATE\") | Unable to parse date/time: NOT A DATE", + "DT_DATE_PARSE(\"2022-09-30T11:50:20Z\") | 2022-09-30T11:50:20Z", + "DT_DATE_PARSE(\"2022-11-01T11:50:20Z\") | 2022-11-01T11:50:20Z", + "DT_DATE_PARSE(\"2022-09-30T11:50:20\") | 2022-09-30T09:50:20Z", + "DT_DATE_PARSE(\"2022-11-01T11:50:20\") | 2022-11-01T10:50:20Z", + "DT_DATE_PARSE(\"2022-11-01T11:50:20+03:00\") | 2022-11-01T08:50:20Z", + "DT_DATE_PARSE(\"2011-12-03T10:15:30+01:00[Europe/Paris]\") | 2011-12-03T09:15:30Z", + "DT_DATE_PARSE(\"2022-10-30T11:50:20.000000030\") | 2022-10-30T10:50:20.000000030Z", + "DT_DATE_PARSE(\"2022-06-13\") | 2022-06-12T22:00:00Z", + "DT_DATE_PARSE(\"2022-11-30\") | 2022-11-29T23:00:00Z", + "DT_DATE_PARSE(\"Tue, 3 Jun 2008 11:05:30 GMT\") | 2008-06-03T11:05:30Z" }) - void testDateTimeParseFailure(String expression, String message) { - assertExpressionThrowsException(expression, message, DateTimeTestConfiguration); + void testDateTimeParseDefault(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult, DateTimeTestConfiguration); } @ParameterizedTest @CsvSource( delimiter = '|', value = { - "DT_ZONED_PARSE(\"2022-10-30T11:50:20Z\") | 2022-10-30T11:50:20Z", - "DT_ZONED_PARSE(\"2011-12-03T10:15:30+01:00[Europe/Paris]\") | 2011-12-03T09:15:30Z", - "DT_ZONED_PARSE(\"2011-12-03T10:15:30+01:00\") | 2011-12-03T09:15:30Z", - "DT_ZONED_PARSE(\"03/12/2011 10:15:30 Europe/Paris\", \"dd/MM/yyyy HH:mm:ss v\") |" + "DT_DATE_PARSE(\"2022-09-30T11:50:20Z\", \"America/New_York\") | 2022-09-30T11:50:20Z", + "DT_DATE_PARSE(\"2022-11-10T11:50:20Z\", \"America/New_York\") | 2022-11-10T11:50:20Z", + "DT_DATE_PARSE(\"2022-09-30T11:50:20\", \"America/New_York\") | 2022-09-30T15:50:20Z", + "DT_DATE_PARSE(\"2022-11-10T11:50:20\", \"America/New_York\") | 2022-11-10T16:50:20Z", + "DT_DATE_PARSE(\"2022-11-10T11:50:20+03:00\", \"America/New_York\") | 2022-11-10T08:50:20Z", + "DT_DATE_PARSE(\"2011-12-03T10:15:30+01:00[Europe/Paris]\", \"America/New_York\") |" + " 2011-12-03T09:15:30Z", - "DT_ZONED_PARSE(\"03/08/2019T16:20:17:717+05:30\",\"dd/MM/uuuu'T'HH:mm:ss:SSSXXXXX\") |" - + " 2019-08-03T10:50:17.717Z", + "DT_DATE_PARSE(\"2022-10-30T11:50:20.000000030\", \"America/New_York\") |" + + " 2022-10-30T15:50:20.000000030Z", + "DT_DATE_PARSE(\"2022-06-13\", \"America/New_York\") | 2022-06-13T04:00:00Z", + "DT_DATE_PARSE(\"2022-11-30\", \"America/New_York\") | 2022-11-30T05:00:00Z", + "DT_DATE_PARSE(\"Tue, 3 Jun 2008 11:05:30 GMT\", \"America/New_York\") |" + + " 2008-06-03T11:05:30Z" }) - void testZonedDateTimeParse(String expression, String expectedResult) + void testDateTimeParseWithZone(String expression, String expectedResult) throws EvaluationException, ParseException { assertExpressionHasExpectedResult(expression, expectedResult, DateTimeTestConfiguration); } @@ -78,48 +145,104 @@ class DateTimeFunctionsTest extends BaseEvaluationTest { @CsvSource( delimiter = '|', value = { - "DT_ZONED_PARSE(\"NOT A DATE\") | Unable to parse zoned date/time: NOT A DATE", + "DT_DATE_PARSE(\"23.05.2023\", NULL, \"dd.MM.yyyy\") | 2023-05-22T22:00:00Z", + "DT_DATE_PARSE(\"23.11.2023\", NULL, \"dd.MM.yyyy\") | 2023-11-22T23:00:00Z", + "DT_DATE_PARSE(\"23.11.2023 14:35:10\", NULL, \"dd.MM.yyyy HH:mm:ss\") |" + + " 2023-11-23T13:35:10Z", + "DT_DATE_PARSE(\"23.11.2023 14:35:10 America/Chicago\", NULL, \"dd.MM.yyyy HH:mm:ss z\") |" + + " 2023-11-23T20:35:10Z", + "DT_DATE_PARSE(\"23.05.2023\", NULL, \"dd.MM.yyyy\", \"dd.MM.yyyy HH:mm:ss\", \"dd.MM.yyyy" + + " HH:mm:ss z\") | 2023-05-22T22:00:00Z", + "DT_DATE_PARSE(\"23.11.2023 14:35:10\", NULL, \"dd.MM.yyyy\", \"dd.MM.yyyy HH:mm:ss\"," + + " \"dd.MM.yyyy HH:mm:ss z\") | 2023-11-23T13:35:10Z", + "DT_DATE_PARSE(\"23.11.2023 14:35:10 America/Chicago\", NULL, \"dd.MM.yyyy\", \"dd.MM.yyyy" + + " HH:mm:ss\", \"dd.MM.yyyy HH:mm:ss z\") | 2023-11-23T20:35:10Z", + "DT_DATE_PARSE(\"23.11.2023 14:35:10\", \"America/Argentina/Cordoba\", \"dd.MM.yyyy" + + " HH:mm:ss\") | 2023-11-23T17:35:10Z", + "DT_DATE_PARSE(\"23.11.2023 14:35:10\", \"Australia/Canberra\", \"dd.MM.yyyy HH:mm:ss\") |" + + " 2023-11-23T03:35:10Z", }) - void testZonedDateTimeParseFailure(String expression, String message) { - assertExpressionThrowsException(expression, message, DateTimeTestConfiguration); + void testDateTimeParseWithFormats(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult, DateTimeTestConfiguration); + } + + @Test + void testDateTimeParseIllegalFormat() { + assertThatThrownBy( + () -> new Expression("DT_DATE_PARSE(\"2023-01-01\", NULL, \"defect\")").evaluate()) + .isInstanceOf(EvaluationException.class) + .hasMessage("Illegal date-time format in parameter 3: 'defect'"); + } + + @Test + void testDateTimeParseUnableToParse() { + assertThatThrownBy(() -> new Expression("DT_DATE_PARSE(\"2023-99-99\")").evaluate()) + .isInstanceOf(EvaluationException.class) + .hasMessage("Unable to parse date-time string '2023-99-99'"); } @ParameterizedTest @CsvSource( delimiter = '|', value = { - "DT_DATE_TIME(2022,10,30) | 2022-10-29T22:00:00Z", - "DT_DATE_TIME(2022,10,30,11) | 2022-10-30T09:00:00Z", - "DT_DATE_TIME(2022,10,30,11,50,20) | 2022-10-30T09:50:20Z", - "DT_DATE_TIME(2022,10,30,11,50,20,30) | 2022-10-30T09:50:20.000000030Z" + "DT_DATE_FORMAT(DT_DATE_PARSE(\"2022-10-20T11:50:20\")) |" + + " 2022-10-20T11:50:20+02:00[Europe/Berlin]", + "DT_DATE_FORMAT(DT_DATE_PARSE(\"2022-12-20T11:50:20\")) |" + + " 2022-12-20T11:50:20+01:00[Europe/Berlin]", + "DT_DATE_FORMAT(DT_DATE_PARSE(\"2022-12-20T11:50:20\"), \"dd.MM.yyyy HH:mm:ss\") |" + + " 20.12.2022 11:50:20", + "DT_DATE_FORMAT(DT_DATE_PARSE(\"2022-12-20T11:50:20\"), \"dd.MM.yyyy HH:mm:ss\"," + + " \"America/New_York\") | 20.12.2022 05:50:20" }) - void testDateTime(String expression, String expectedResult) + void testDateTimeFormat(String expression, String expectedResult) throws EvaluationException, ParseException { assertExpressionHasExpectedResult(expression, expectedResult, DateTimeTestConfiguration); } + @Test + void testDateTimeFormatTooManyParameters() { + assertThatThrownBy( + () -> + new Expression( + "DT_DATE_FORMAT(DT_DATE_PARSE(\"2022-12-20T11:50:20\"), \"dd.MM.yyyy\"," + + " \"America/New_York\", \"invalid\")") + .evaluate()) + .isInstanceOf(EvaluationException.class) + .hasMessage("Too many parameters"); + } + + @Test + void testDateTimeFormatNotADateTime() { + assertThatThrownBy(() -> new Expression("DT_DATE_FORMAT(value)").with("value", 23).evaluate()) + .isInstanceOf(EvaluationException.class) + .hasMessage("Unable to format a 'NUMBER' type as a date-time"); + } + @ParameterizedTest @CsvSource( delimiter = '|', value = { - "DT_FORMAT(DT_PARSE(\"2022-10-30T11:50:20\")) | 2022-10-30T11:50:20", - "DT_FORMAT(DT_PARSE(\"2022-10-30T11:50:20.000000030\"), \"dd/MM/yyyy\") | 30/10/2022", - "DT_FORMAT(DT_PARSE(\"2022-10-30T11:50:20.000000030\"), \"dd/MM/yyyy HH:mm:ss\") |" - + " 30/10/2022 11:50:20" + "DT_DATE_TO_EPOCH(DT_DATE_NEW(1667130620000)) | 1667130620000", + "DT_DATE_TO_EPOCH(DT_DATE_NEW(0)) | 0" }) - void testDateTimeFormat(String expression, String expectedResult) + void testDateTimeToEpoch(String expression, String expectedResult) throws EvaluationException, ParseException { - assertExpressionHasExpectedResult(expression, expectedResult, DateTimeTestConfiguration); + assertExpressionHasExpectedResult(expression, expectedResult); } @ParameterizedTest @CsvSource( delimiter = '|', value = { - "DT_EPOCH(DT_DATE_TIME_EPOCH(1667130620000)) | 1667130620000", - "DT_EPOCH(DT_DATE_TIME_EPOCH(0)) | 0" + "DT_DURATION_NEW(6) | PT144H", + "DT_DURATION_NEW(6,5) | PT149H", + "DT_DURATION_NEW(6,5,4) | PT149H4M", + "DT_DURATION_NEW(6,5,4,3) | PT149H4M3S", + "DT_DURATION_NEW(6,5,4,3,2) | PT149H4M3.002S", + "DT_DURATION_NEW(6,5,4,3,2,1) | PT149H4M3.002000001S" }) - void testDateTimeToEpoch(String expression, String expectedResult) + void testDurationNew(String expression, String expectedResult) throws EvaluationException, ParseException { assertExpressionHasExpectedResult(expression, expectedResult); } @@ -128,8 +251,8 @@ class DateTimeFunctionsTest extends BaseEvaluationTest { @CsvSource( delimiter = '|', value = { - "DT_DURATION_MILLIS(1667130620000) | PT463091H50M20S", - "DT_DURATION_MILLIS(0) | PT0S" + "DT_DURATION_FROM_MILLIS(1667130620000) | PT463091H50M20S", + "DT_DURATION_FROM_MILLIS(0) | PT0S" }) void testDurationFromMillis(String expression, String expectedResult) throws EvaluationException, ParseException { @@ -139,8 +262,11 @@ class DateTimeFunctionsTest extends BaseEvaluationTest { @ParameterizedTest @CsvSource( delimiter = '|', - value = {"DT_DURATION_DAYS(53216) | PT1277184H", "DT_DURATION_DAYS(1) | PT24H"}) - void testDurationFromDays(String expression, String expectedResult) + value = { + "DT_DURATION_TO_MILLIS(DT_DURATION_PARSE(\"PT463091H50M20S\")) | 1667130620000", + "DT_DURATION_TO_MILLIS(DT_DURATION_PARSE(\"PT0S\")) | 0" + }) + void testDurationToMillis(String expression, String expectedResult) throws EvaluationException, ParseException { assertExpressionHasExpectedResult(expression, expectedResult); } @@ -156,4 +282,10 @@ class DateTimeFunctionsTest extends BaseEvaluationTest { throws EvaluationException, ParseException { assertExpressionHasExpectedResult(expression, expectedResult); } + + @Test + void testDurationString() throws EvaluationException, ParseException { + assertExpressionHasExpectedResult( + "\"Value: \" + DT_DURATION_PARSE(\"P1DT3H4M5S\")", "Value: PT27H4M5S"); + } } diff --git a/src/test/java/com/ezylang/evalex/functions/datetime/ZoneIdConverterTest.java b/src/test/java/com/ezylang/evalex/functions/datetime/ZoneIdConverterTest.java new file mode 100644 index 0000000..0b70a62 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/functions/datetime/ZoneIdConverterTest.java @@ -0,0 +1,46 @@ +/* + Copyright 2012-2023 Udo Klimaschewski + + 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 com.ezylang.evalex.functions.datetime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.parser.Token; +import java.time.ZoneId; +import org.junit.jupiter.api.Test; + +class ZoneIdConverterTest { + + @Test + void testConversionOk() throws Exception { + Token testToken = new Token(1, "Europe/Berlin", Token.TokenType.STRING_LITERAL); + + assertThat(ZoneIdConverter.convert(testToken, testToken.getValue())) + .isEqualTo(ZoneId.of(testToken.getValue())); + } + + @Test + void testConversionFails() throws Exception { + Token testToken = new Token(1, "Mars/Olympus_Mons", Token.TokenType.STRING_LITERAL); + + assertThatThrownBy(() -> ZoneIdConverter.convert(testToken, testToken.getValue())) + .isInstanceOf(EvaluationException.class) + .hasMessage( + "Unable to convert zone string 'Mars/Olympus_Mons' to a zone ID: Unknown time-zone ID:" + + " Mars/Olympus_Mons"); + } +} diff --git a/src/test/java/com/ezylang/evalex/operators/arithmetic/ArithmeticOperatorsTest.java b/src/test/java/com/ezylang/evalex/operators/arithmetic/ArithmeticOperatorsTest.java index 3a8db50..d472abb 100644 --- a/src/test/java/com/ezylang/evalex/operators/arithmetic/ArithmeticOperatorsTest.java +++ b/src/test/java/com/ezylang/evalex/operators/arithmetic/ArithmeticOperatorsTest.java @@ -95,9 +95,9 @@ class ArithmeticOperatorsTest extends BaseEvaluationTest { @CsvSource( delimiter = '|', value = { - "DT_DATE_TIME(2022,10,30,11,50,30)-2000 | 2022-10-30T09:50:28Z", - "DT_DATE_TIME(2022,10,30,11,50,30)-DT_DATE_TIME(2022,10,30,11,50,28) | PT2S", - "DT_DATE_TIME(2022,10,30,11,50,30)-DT_DURATION_PARSE(\"PT2S\") | 2022-10-30T09:50:28Z", + "DT_DATE_NEW(2022,10,30,11,50,30)-2000 | 2022-10-30T09:50:28Z", + "DT_DATE_NEW(2022,10,30,11,50,30)-DT_DATE_NEW(2022,10,30,11,50,28) | PT2S", + "DT_DATE_NEW(2022,10,30,11,50,30)-DT_DURATION_PARSE(\"PT2S\") | 2022-10-30T09:50:28Z", "DT_DURATION_PARSE(\"PT5S\")-DT_DURATION_PARSE(\"PT2S\") | PT3S" }) void testInfixMinusDateTime(String expression, String expectedResult) @@ -190,8 +190,8 @@ class ArithmeticOperatorsTest extends BaseEvaluationTest { @CsvSource( delimiter = '|', value = { - "DT_DATE_TIME(2022,10,30,11,50,30)+2000 | 2022-10-30T09:50:32Z", - "DT_DATE_TIME(2022,10,30,11,50,30)+DT_DURATION_PARSE(\"PT2S\") | 2022-10-30T09:50:32Z", + "DT_DATE_NEW(2022,10,30,11,50,30)+2000 | 2022-10-30T09:50:32Z", + "DT_DATE_NEW(2022,10,30,11,50,30)+DT_DURATION_PARSE(\"PT2S\") | 2022-10-30T09:50:32Z", "DT_DURATION_PARSE(\"PT5S\")+DT_DURATION_PARSE(\"PT2S\") | PT7S" }) void testInfixPlusDateTime(String expression, String expectedResult) diff --git a/src/test/java/com/ezylang/evalex/operators/booleans/InfixEqualsOperatorTest.java b/src/test/java/com/ezylang/evalex/operators/booleans/InfixEqualsOperatorTest.java index 05fc27b..5539e74 100644 --- a/src/test/java/com/ezylang/evalex/operators/booleans/InfixEqualsOperatorTest.java +++ b/src/test/java/com/ezylang/evalex/operators/booleans/InfixEqualsOperatorTest.java @@ -45,8 +45,8 @@ class InfixEqualsOperatorTest extends BaseEvaluationTest { "\"abc\"=\"xyz\" : false", "1+2=4-1 : true", "-5.2=-5.2 : true", - "DT_DATE_TIME(2022,10,30)=DT_DATE_TIME(2022,10,30) : true", - "DT_DATE_TIME(2022,10,30)=DT_DATE_TIME(2022,10,01) : false", + "DT_DATE_NEW(2022,10,30)=DT_DATE_NEW(2022,10,30) : true", + "DT_DATE_NEW(2022,10,30)=DT_DATE_NEW(2022,10,01) : false", "DT_DURATION_PARSE(\"PT24H\")=DT_DURATION_PARSE(\"P1D\") : true", }) void testInfixEqualsLiterals(String expression, String expectedResult) diff --git a/src/test/java/com/ezylang/evalex/operators/booleans/InfixGreaterEqualsOperatorTest.java b/src/test/java/com/ezylang/evalex/operators/booleans/InfixGreaterEqualsOperatorTest.java index b6ce858..20d8eda 100644 --- a/src/test/java/com/ezylang/evalex/operators/booleans/InfixGreaterEqualsOperatorTest.java +++ b/src/test/java/com/ezylang/evalex/operators/booleans/InfixGreaterEqualsOperatorTest.java @@ -41,9 +41,9 @@ class InfixGreaterEqualsOperatorTest extends BaseEvaluationTest { "\"9\">=\"9\" : true", "-4>=-4 :true", "-4>=-5 :true", - "DT_DATE_TIME(2022,10,30)>=DT_DATE_TIME(2022,10,30) : true", - "DT_DATE_TIME(2022,10,30)>=DT_DATE_TIME(2022,10,28) : true", - "DT_DATE_TIME(2022,10,30)>=DT_DATE_TIME(2022,10,31) : false", + "DT_DATE_NEW(2022,10,30)>=DT_DATE_NEW(2022,10,30) : true", + "DT_DATE_NEW(2022,10,30)>=DT_DATE_NEW(2022,10,28) : true", + "DT_DATE_NEW(2022,10,30)>=DT_DATE_NEW(2022,10,31) : false", "DT_DURATION_PARSE(\"P2D\")>=DT_DURATION_PARSE(\"PT24H\") : true" }) void testInfixGreaterEqualsLiterals(String expression, String expectedResult) diff --git a/src/test/java/com/ezylang/evalex/operators/booleans/InfixGreaterOperatorTest.java b/src/test/java/com/ezylang/evalex/operators/booleans/InfixGreaterOperatorTest.java index 335b765..fc2b0a5 100644 --- a/src/test/java/com/ezylang/evalex/operators/booleans/InfixGreaterOperatorTest.java +++ b/src/test/java/com/ezylang/evalex/operators/booleans/InfixGreaterOperatorTest.java @@ -37,9 +37,9 @@ class InfixGreaterOperatorTest extends BaseEvaluationTest { "\"ABC\">\"abc\" : false", "\"9\">\"5\" : true", "-4>-5 :true", - "DT_DATE_TIME(2022,10,30)>DT_DATE_TIME(2022,10,30) : false", - "DT_DATE_TIME(2022,10,30)>DT_DATE_TIME(2022,10,28) : true", - "DT_DATE_TIME(2022,10,30)>DT_DATE_TIME(2022,10,31) : false", + "DT_DATE_NEW(2022,10,30)>DT_DATE_NEW(2022,10,30) : false", + "DT_DATE_NEW(2022,10,30)>DT_DATE_NEW(2022,10,28) : true", + "DT_DATE_NEW(2022,10,30)>DT_DATE_NEW(2022,10,31) : false", "DT_DURATION_PARSE(\"P2D\")>DT_DURATION_PARSE(\"PT24H\") : true" }) void testInfixGreaterLiterals(String expression, String expectedResult) diff --git a/src/test/java/com/ezylang/evalex/operators/booleans/InfixLessEqualsOperatorTest.java b/src/test/java/com/ezylang/evalex/operators/booleans/InfixLessEqualsOperatorTest.java index 6ba9ec9..4fc5840 100644 --- a/src/test/java/com/ezylang/evalex/operators/booleans/InfixLessEqualsOperatorTest.java +++ b/src/test/java/com/ezylang/evalex/operators/booleans/InfixLessEqualsOperatorTest.java @@ -41,9 +41,9 @@ class InfixLessEqualsOperatorTest extends BaseEvaluationTest { "\"9\"<=\"9\" : true", "-4<=-4 :true", "-5<=-4 :true", - "DT_DATE_TIME(2022,10,30)<=DT_DATE_TIME(2022,10,30) : true", - "DT_DATE_TIME(2022,10,30)<=DT_DATE_TIME(2022,10,28) : false", - "DT_DATE_TIME(2022,10,30)<=DT_DATE_TIME(2022,10,31) : true", + "DT_DATE_NEW(2022,10,30)<=DT_DATE_NEW(2022,10,30) : true", + "DT_DATE_NEW(2022,10,30)<=DT_DATE_NEW(2022,10,28) : false", + "DT_DATE_NEW(2022,10,30)<=DT_DATE_NEW(2022,10,31) : true", "DT_DURATION_PARSE(\"P2D\")<=DT_DURATION_PARSE(\"PT24H\") : false" }) void testInfixLessEqualsLiterals(String expression, String expectedResult) diff --git a/src/test/java/com/ezylang/evalex/operators/booleans/InfixLessOperatorTest.java b/src/test/java/com/ezylang/evalex/operators/booleans/InfixLessOperatorTest.java index 5402489..80a6c23 100644 --- a/src/test/java/com/ezylang/evalex/operators/booleans/InfixLessOperatorTest.java +++ b/src/test/java/com/ezylang/evalex/operators/booleans/InfixLessOperatorTest.java @@ -39,9 +39,9 @@ class InfixLessOperatorTest extends BaseEvaluationTest { "\"abc\"<\"ABC\" : false", "\"5\"<\"9\" : true", "-5<-4 :true", - "DT_DATE_TIME(2022,10,30)