diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bb8fe2b..5c33f7c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,9 +5,9 @@ name: Build on: push: - branches: [ "main" ] + branches: [ "main", "3.0.x", "3.1.x" ] pull_request: - branches: [ "main" ] + branches: [ "main", "3.0.x", "3.1.x" ] jobs: build: diff --git a/.gitignore b/.gitignore index f715a02..9de250c 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ target/ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* +/.sdkmanrc diff --git a/README.md b/README.md index 32c9d7c..9d6e62b 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ 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. - Uses BigDecimal for numerical calculations. diff --git a/docs/concepts/datatypes.md b/docs/concepts/datatypes.md index 2534728..0817dc3 100644 --- a/docs/concepts/datatypes.md +++ b/docs/concepts/datatypes.md @@ -14,9 +14,12 @@ EvalEx supports the following data types: | NUMBER | java.math.BigDecimal | | BOOLEAN | java.lang.Boolean | | STRING | java.lang.String | +| DATE_TIME | java.time.Instant | +| DURATION | java.time.Duration | | ARRAY | java.util.List | | STRUCTURE | java.util.Map | | EXPRESSION_NODE | com.ezylang.evalex.parser.ASTNode | +| NULL | null | Data is stored in an _EvaluationValue_, which holds the value and the data type. @@ -61,6 +64,16 @@ Any instance of _java.lang.CharSequence_ or _java.lang.Character_ will automatic a _STRING_ datatype. Conversion will be done by invoking the _toString()_ method on the input 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. + +### DURATION + +Duration are stored as a _java.time.Duration_. The duration values are useful for calculations with _DATE_TIME_ values. + ### ARRAY Arrays are stored internally as a _java.util.List<EvaluationValue>_. When passed as a @@ -194,3 +207,20 @@ System.out.println(result); // prints 14 Note that the above expression is not evaluated as "2 * 4 + 3", which would result in 11. Instead, the sub-expression "4 + 3" is calculated first, when it comes to finding the value of the variable _b_. Resulting in calculation of "2 * 7", which is 14. + +### NULL + +A representation for _null_ objects. + +This allows the handling of nulls inside the expression itself (for example using the _IF()_ function), +in case it can not be guaranteed that the passed variable values are not null before passing them. + +```java +Expression expression = new Expression("if(name == null, "unknown", name)"); + +EvaluationValue result = expression + .with("name", null) + .evaluate(); + +System.out.println(result); // prints unknown +``` diff --git a/docs/concepts/parsing_evaluation.md b/docs/concepts/parsing_evaluation.md index 07cc62b..f20ce5d 100644 --- a/docs/concepts/parsing_evaluation.md +++ b/docs/concepts/parsing_evaluation.md @@ -131,6 +131,14 @@ Another option to have EvalEx use your data is to define a custom data accessor. See chapter [Data Access](../customization/data_access.html) for details. +### Null value Handling + +Operations and Functions that can not handle null will throw either a _NullPointerException_ +or an _EvaluationException_ when encountering null. + +When passing null as variable values is a possibility the expression should be written to handle null +itself, for example using the _IF()_ function. + ### Exception Handling In EvalEx, there are two general exceptions: diff --git a/docs/references/constants.md b/docs/references/constants.md index 01f5021..059a3cf 100644 --- a/docs/references/constants.md +++ b/docs/references/constants.md @@ -15,5 +15,6 @@ Available through the _ExpressionConfiguration.StandardConstants_ constant: | FALSE | Boolean.FALSE | | PI | 3.14159265358979323846264338327950288419716939937510582097... | | E | 2.71828182845904523536028747135266249775724709369995957496... | +| NULL | null | diff --git a/docs/references/functions.md b/docs/references/functions.md index 8990383..7db0e91 100644 --- a/docs/references/functions.md +++ b/docs/references/functions.md @@ -75,3 +75,16 @@ Available through the _ExpressionConfiguration.StandardFunctionsDictionary_ cons | TANH(value) | Returns the hyperbolic tangent of a value | | TANR(value) | Returns the tangent of an angle (in radians) | +### DateTime 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-) | diff --git a/pom.xml b/pom.xml index b84f42e..e28df1e 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.ezylang EvalEx - 3.0.6-SNAPSHOT + 3.1.0-SNAPSHOT EvalEx diff --git a/src/main/java/com/ezylang/evalex/config/ExpressionConfiguration.java b/src/main/java/com/ezylang/evalex/config/ExpressionConfiguration.java index f3db66f..86fd4a6 100644 --- a/src/main/java/com/ezylang/evalex/config/ExpressionConfiguration.java +++ b/src/main/java/com/ezylang/evalex/config/ExpressionConfiguration.java @@ -19,78 +19,19 @@ import com.ezylang.evalex.data.DataAccessorIfc; import com.ezylang.evalex.data.EvaluationValue; import com.ezylang.evalex.data.MapBasedDataAccessor; import com.ezylang.evalex.functions.FunctionIfc; -import com.ezylang.evalex.functions.basic.AbsFunction; -import com.ezylang.evalex.functions.basic.CeilingFunction; -import com.ezylang.evalex.functions.basic.FactFunction; -import com.ezylang.evalex.functions.basic.FloorFunction; -import com.ezylang.evalex.functions.basic.IfFunction; -import com.ezylang.evalex.functions.basic.Log10Function; -import com.ezylang.evalex.functions.basic.LogFunction; -import com.ezylang.evalex.functions.basic.MaxFunction; -import com.ezylang.evalex.functions.basic.MinFunction; -import com.ezylang.evalex.functions.basic.NotFunction; -import com.ezylang.evalex.functions.basic.RandomFunction; -import com.ezylang.evalex.functions.basic.RoundFunction; -import com.ezylang.evalex.functions.basic.SqrtFunction; -import com.ezylang.evalex.functions.basic.SumFunction; +import com.ezylang.evalex.functions.basic.*; +import com.ezylang.evalex.functions.datetime.*; import com.ezylang.evalex.functions.string.StringContains; import com.ezylang.evalex.functions.string.StringLowerFunction; import com.ezylang.evalex.functions.string.StringUpperFunction; -import com.ezylang.evalex.functions.trigonometric.AcosFunction; -import com.ezylang.evalex.functions.trigonometric.AcosHFunction; -import com.ezylang.evalex.functions.trigonometric.AcosRFunction; -import com.ezylang.evalex.functions.trigonometric.AcotFunction; -import com.ezylang.evalex.functions.trigonometric.AcotHFunction; -import com.ezylang.evalex.functions.trigonometric.AcotRFunction; -import com.ezylang.evalex.functions.trigonometric.AsinFunction; -import com.ezylang.evalex.functions.trigonometric.AsinHFunction; -import com.ezylang.evalex.functions.trigonometric.AsinRFunction; -import com.ezylang.evalex.functions.trigonometric.Atan2Function; -import com.ezylang.evalex.functions.trigonometric.Atan2RFunction; -import com.ezylang.evalex.functions.trigonometric.AtanFunction; -import com.ezylang.evalex.functions.trigonometric.AtanHFunction; -import com.ezylang.evalex.functions.trigonometric.AtanRFunction; -import com.ezylang.evalex.functions.trigonometric.CosFunction; -import com.ezylang.evalex.functions.trigonometric.CosHFunction; -import com.ezylang.evalex.functions.trigonometric.CosRFunction; -import com.ezylang.evalex.functions.trigonometric.CotFunction; -import com.ezylang.evalex.functions.trigonometric.CotHFunction; -import com.ezylang.evalex.functions.trigonometric.CotRFunction; -import com.ezylang.evalex.functions.trigonometric.CscFunction; -import com.ezylang.evalex.functions.trigonometric.CscHFunction; -import com.ezylang.evalex.functions.trigonometric.CscRFunction; -import com.ezylang.evalex.functions.trigonometric.DegFunction; -import com.ezylang.evalex.functions.trigonometric.RadFunction; -import com.ezylang.evalex.functions.trigonometric.SecFunction; -import com.ezylang.evalex.functions.trigonometric.SecHFunction; -import com.ezylang.evalex.functions.trigonometric.SecRFunction; -import com.ezylang.evalex.functions.trigonometric.SinFunction; -import com.ezylang.evalex.functions.trigonometric.SinHFunction; -import com.ezylang.evalex.functions.trigonometric.SinRFunction; -import com.ezylang.evalex.functions.trigonometric.TanFunction; -import com.ezylang.evalex.functions.trigonometric.TanHFunction; -import com.ezylang.evalex.functions.trigonometric.TanRFunction; +import com.ezylang.evalex.functions.trigonometric.*; import com.ezylang.evalex.operators.OperatorIfc; -import com.ezylang.evalex.operators.arithmetic.InfixDivisionOperator; -import com.ezylang.evalex.operators.arithmetic.InfixMinusOperator; -import com.ezylang.evalex.operators.arithmetic.InfixModuloOperator; -import com.ezylang.evalex.operators.arithmetic.InfixMultiplicationOperator; -import com.ezylang.evalex.operators.arithmetic.InfixPlusOperator; -import com.ezylang.evalex.operators.arithmetic.InfixPowerOfOperator; -import com.ezylang.evalex.operators.arithmetic.PrefixMinusOperator; -import com.ezylang.evalex.operators.arithmetic.PrefixPlusOperator; -import com.ezylang.evalex.operators.booleans.InfixAndOperator; -import com.ezylang.evalex.operators.booleans.InfixEqualsOperator; -import com.ezylang.evalex.operators.booleans.InfixGreaterEqualsOperator; -import com.ezylang.evalex.operators.booleans.InfixGreaterOperator; -import com.ezylang.evalex.operators.booleans.InfixLessEqualsOperator; -import com.ezylang.evalex.operators.booleans.InfixLessOperator; -import com.ezylang.evalex.operators.booleans.InfixNotEqualsOperator; -import com.ezylang.evalex.operators.booleans.InfixOrOperator; -import com.ezylang.evalex.operators.booleans.PrefixNotOperator; +import com.ezylang.evalex.operators.arithmetic.*; +import com.ezylang.evalex.operators.booleans.*; 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; @@ -120,7 +61,7 @@ import lombok.Getter; * Map.entry("update", new UpdateFunction())); * */ -@Builder +@Builder(toBuilder = true) public class ExpressionConfiguration { /** The standard set constants for EvalEx. */ @@ -221,7 +162,17 @@ public class ExpressionConfiguration { // string functions Map.entry("STR_CONTAINS", new StringContains()), Map.entry("STR_LOWER", new StringLowerFunction()), - Map.entry("STR_UPPER", new StringUpperFunction())); + 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_DURATION_PARSE", new DurationParseFunction())); /** The math context to use. */ @Builder.Default @Getter private final MathContext mathContext = DEFAULT_MATH_CONTEXT; @@ -276,6 +227,8 @@ public class ExpressionConfiguration { */ @Builder.Default @Getter private final boolean allowOverwriteConstants = true; + /** Set the default zone id. By default, the system default zone id is used. */ + @Builder.Default @Getter private final ZoneId defaultZoneId = ZoneId.systemDefault(); /** * Convenience method to create a default configuration. * @@ -347,6 +300,7 @@ public class ExpressionConfiguration { new EvaluationValue( new BigDecimal( "2.71828182845904523536028747135266249775724709369995957496696762772407663"))); + constants.put("NULL", new EvaluationValue(null)); return constants; } diff --git a/src/main/java/com/ezylang/evalex/data/EvaluationValue.java b/src/main/java/com/ezylang/evalex/data/EvaluationValue.java index ce0485c..b3d5307 100644 --- a/src/main/java/com/ezylang/evalex/data/EvaluationValue.java +++ b/src/main/java/com/ezylang/evalex/data/EvaluationValue.java @@ -19,11 +19,8 @@ import com.ezylang.evalex.parser.ASTNode; import java.math.BigDecimal; import java.math.BigInteger; import java.math.MathContext; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.time.*; +import java.util.*; import java.util.Map.Entry; import lombok.Value; @@ -35,6 +32,15 @@ import lombok.Value; @Value public class EvaluationValue implements Comparable { + /** Return value for a null {@link DataType#BOOLEAN}. */ + private static final Boolean NULL_BOOLEAN = null; + + /** Return value for a null {@link DataType#ARRAY}. */ + private static final List NULL_ARRAY = null; + + /** Return value for a null {@link DataType#STRUCTURE}. */ + private static final Map NULL_STRUCTURE = null; + /** The supported data types. */ public enum DataType { /** A string of characters, stored as {@link String}. */ @@ -43,6 +49,10 @@ public class EvaluationValue implements Comparable { NUMBER, /** A boolean, stored as {@link Boolean}. */ BOOLEAN, + /** A date time value, stored as {@link java.time.Instant}. */ + DATE_TIME, + /** A period value, stored as {@link java.time.Duration}. */ + DURATION, /** A list evaluation values. Stored as {@link java.util.List}. */ ARRAY, /** @@ -54,7 +64,9 @@ public class EvaluationValue implements Comparable { * Used for lazy parameter evaluation, stored as an {@link ASTNode}, which can be evaluated on * demand. */ - EXPRESSION_NODE + EXPRESSION_NODE, + /** A null value */ + NULL } Object value; @@ -78,6 +90,11 @@ public class EvaluationValue implements Comparable { * Float, floatBigDecimal * * CharSequence , StringString * Boolean, booleanBoolean + * Instant, instantInstant + * ZonedDateTime, zonedDateTimeInstant + * LocalDate, localDateInstant + * OffsetDateTime, offsetDateTimeInstant + * Duration, durationDuration * ASTNodeASTNode * List<?>List<EvaluationValue> - each entry will be converted * Map<?,?>Map<String><EvaluationValue> - each entry will be converted. @@ -94,6 +111,9 @@ public class EvaluationValue implements Comparable { if (number != null) { this.dataType = DataType.NUMBER; this.value = number; + } else if (value == null) { + this.dataType = DataType.NULL; + this.value = null; } else if (value instanceof CharSequence) { this.dataType = DataType.STRING; this.value = ((CharSequence) value).toString(); @@ -103,6 +123,21 @@ public class EvaluationValue implements Comparable { } else if (value instanceof Boolean) { this.dataType = DataType.BOOLEAN; this.value = value; + } else if (value instanceof Instant) { + this.dataType = DataType.DATE_TIME; + this.value = value; + } else if (value instanceof ZonedDateTime) { + this.dataType = DataType.DATE_TIME; + this.value = ((ZonedDateTime) value).toInstant(); + } else if (value instanceof OffsetDateTime) { + this.dataType = DataType.DATE_TIME; + this.value = ((OffsetDateTime) value).toInstant(); + } else if (value instanceof LocalDate) { + this.dataType = DataType.DATE_TIME; + this.value = ((LocalDate) value).atStartOfDay().atOffset(ZoneOffset.UTC).toInstant(); + } else if (value instanceof Duration) { + this.dataType = DataType.DURATION; + this.value = value; } else if (value instanceof ASTNode) { this.dataType = DataType.EXPRESSION_NODE; this.value = value; @@ -126,6 +161,11 @@ public class EvaluationValue implements Comparable { this.value = new BigDecimal(Double.toString(value), mathContext); } + public EvaluationValue(LocalDateTime value, ZoneId zoneId) { + this.dataType = DataType.DATE_TIME; + this.value = value.atZone(zoneId).toInstant(); + } + /** * Converts a {@link Map} of objects to a {@link Map} of {@link EvaluationValue} values. * @@ -204,6 +244,23 @@ public class EvaluationValue implements Comparable { return getDataType() == DataType.BOOLEAN; } + /** + * Checks if the value is of type {@link DataType#DATE_TIME}. + * + * @return true or false. + */ + public boolean isDateTimeValue() { + return getDataType() == DataType.DATE_TIME; + } + + /** + * Checks if the value is of type {@link DataType#DURATION}. + * + * @return true or false. + */ + public boolean isDurationValue() { + return getDataType() == DataType.DURATION; + } /** * Checks if the value is of type {@link DataType#ARRAY}. * @@ -231,6 +288,10 @@ public class EvaluationValue implements Comparable { return getDataType() == DataType.EXPRESSION_NODE; } + public boolean isNullValue() { + return getDataType() == DataType.NULL; + } + /** * Creates a {@link DataType#NUMBER} value from a {@link String}. * @@ -266,6 +327,8 @@ public class EvaluationValue implements Comparable { return (Boolean.TRUE.equals(value) ? BigDecimal.ONE : BigDecimal.ZERO); case STRING: return Boolean.parseBoolean((String) value) ? BigDecimal.ONE : BigDecimal.ZERO; + case NULL: + return null; default: return BigDecimal.ZERO; } @@ -283,10 +346,14 @@ public class EvaluationValue implements Comparable { * @return The {@link String} representation of the value. */ public String getStringValue() { - if (getDataType() == DataType.NUMBER) { - return ((BigDecimal) value).toPlainString(); + switch (getDataType()) { + case NUMBER: + return ((BigDecimal) value).toPlainString(); + case NULL: + return null; + default: + return value.toString(); } - return value.toString(); } /** @@ -308,11 +375,74 @@ public class EvaluationValue implements Comparable { return (Boolean) value; case STRING: return Boolean.parseBoolean((String) value); + case NULL: + return NULL_BOOLEAN; default: return false; } } + /** + * Gets a {@link Instant} representation of the value. If possible and needed, a conversion will + * be made. + * + *
    + *
  • Any number value will return the instant from the epoc value. + *
  • Any string with the string representation of a LocalDateTime (ex: + * "2018-11-30T18:35:24.00") (case ignored) will return the current LocalDateTime. + *
  • The date {@link Instant#EPOCH} will return if a conversion error occurs or in all other + * cases. + *
+ * + * @return The {@link Instant} representation of the value. + */ + public Instant getDateTimeValue() { + try { + switch (getDataType()) { + case NUMBER: + return Instant.ofEpochMilli(((BigDecimal) value).longValue()); + case DATE_TIME: + return (Instant) value; + case STRING: + return Instant.parse((String) value); + default: + return Instant.EPOCH; + } + } catch (DateTimeException ex) { + return Instant.EPOCH; + } + } + + /** + * Gets a {@link Duration} representation of the value. If possible and needed, a conversion will + * be made. + * + *
    + *
  • Any non-zero number value will return the duration from the millisecond. + *
  • Any string with the string representation of an {@link Duration} (ex: + * "PnDTnHnMn.nS") (case ignored) will return the current instant. + *
  • The {@link Duration#ZERO} will return if a conversion error occurs or in all other cases. + *
+ * + * @return The {@link Duration} representation of the value. + */ + public Duration getDurationValue() { + try { + switch (getDataType()) { + case NUMBER: + return Duration.ofMillis(((BigDecimal) value).longValue()); + case DURATION: + return (Duration) value; + case STRING: + return Duration.parse((String) value); + default: + return Duration.ZERO; + } + } catch (DateTimeException ex) { + return Duration.ZERO; + } + } + /** * Gets a {@link List} representation of the value. * @@ -323,6 +453,8 @@ public class EvaluationValue implements Comparable { public List getArrayValue() { if (isArrayValue()) { return (List) value; + } else if (isNullValue()) { + return NULL_ARRAY; } else { return Collections.emptyList(); } @@ -338,6 +470,8 @@ public class EvaluationValue implements Comparable { public Map getStructureValue() { if (isStructureValue()) { return (Map) value; + } else if (isNullValue()) { + return NULL_STRUCTURE; } else { return Collections.emptyMap(); } @@ -359,6 +493,12 @@ public class EvaluationValue implements Comparable { return getNumberValue().compareTo(toCompare.getNumberValue()); case BOOLEAN: return getBooleanValue().compareTo(toCompare.getBooleanValue()); + case NULL: + throw new NullPointerException("Can not compare a null value"); + case DATE_TIME: + return getDateTimeValue().compareTo(toCompare.getDateTimeValue()); + case DURATION: + return getDurationValue().compareTo(toCompare.getDurationValue()); default: return getStringValue().compareTo(toCompare.getStringValue()); } diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/AbstractDateTimeParseFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/AbstractDateTimeParseFunction.java new file mode 100644 index 0000000..3a396ef --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/AbstractDateTimeParseFunction.java @@ -0,0 +1,42 @@ +/* + 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().getDefaultZoneId(); + Instant instant; + + if (parameterValues.length < 2) { + instant = parse(parameterValues[0].getStringValue(), null, zoneId); + } else { + instant = + parse(parameterValues[0].getStringValue(), parameterValues[1].getStringValue(), zoneId); + } + return new EvaluationValue(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 new file mode 100644 index 0000000..3596514 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeFormatFunction.java @@ -0,0 +1,43 @@ +/* + 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.ZoneId; +import java.time.format.DateTimeFormatter; + +@FunctionParameter(name = "value", isVarArg = true) +public class DateTimeFormatFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + String formatted; + ZoneId zoneId = expression.getConfiguration().getDefaultZoneId(); + 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); + } + return new EvaluationValue(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 new file mode 100644 index 0000000..6432156 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeFromEpochFunction.java @@ -0,0 +1,34 @@ +/* + 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 new EvaluationValue(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 new file mode 100644 index 0000000..56e1e25 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeFunction.java @@ -0,0 +1,45 @@ +/* + 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().getDefaultZoneId(); + return new EvaluationValue( + LocalDateTime.of(year, month, day, hour, minute, second, nanoOfs) + .atZone(zoneId) + .toInstant()); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeParseFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeParseFunction.java new file mode 100644 index 0000000..65f3b19 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeParseFunction.java @@ -0,0 +1,64 @@ +/* + 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.*; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Optional; + +@FunctionParameter(name = "value", isVarArg = true) +public class DateTimeParseFunction extends AbstractDateTimeParseFunction { + + 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)); + } + + 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(); + } + } + + 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(); + } + } + + private Optional parseInstant(String value) { + try { + return Optional.of(Instant.parse(value)); + } catch (DateTimeParseException ex) { + return Optional.empty(); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeToEpochFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeToEpochFunction.java new file mode 100644 index 0000000..2ee4f2e --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeToEpochFunction.java @@ -0,0 +1,31 @@ +/* + 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; + +@FunctionParameter(name = "value") +public class DateTimeToEpochFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + return new EvaluationValue(parameterValues[0].getDateTimeValue().toEpochMilli()); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DurationFromDaysFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DurationFromDaysFunction.java new file mode 100644 index 0000000..95fac80 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DurationFromDaysFunction.java @@ -0,0 +1,34 @@ +/* + 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.Duration; + +@FunctionParameter(name = "value") +public class DurationFromDaysFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + BigDecimal days = parameterValues[0].getNumberValue(); + return new EvaluationValue(Duration.ofDays(days.longValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DurationFromMillisFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DurationFromMillisFunction.java new file mode 100644 index 0000000..2d544f7 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DurationFromMillisFunction.java @@ -0,0 +1,34 @@ +/* + 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.Duration; + +@FunctionParameter(name = "value") +public class DurationFromMillisFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + BigDecimal millis = parameterValues[0].getNumberValue(); + return new EvaluationValue(Duration.ofMillis(millis.longValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DurationParseFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DurationParseFunction.java new file mode 100644 index 0000000..c7dc271 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DurationParseFunction.java @@ -0,0 +1,33 @@ +/* + 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.Duration; + +@FunctionParameter(name = "value") +public class DurationParseFunction extends AbstractFunction { + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + String text = parameterValues[0].getStringValue(); + return new EvaluationValue(Duration.parse(text)); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/ZonedDateTimeParseFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/ZonedDateTimeParseFunction.java new file mode 100644 index 0000000..5a2b2e3 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/ZonedDateTimeParseFunction.java @@ -0,0 +1,47 @@ +/* + 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/main/java/com/ezylang/evalex/operators/arithmetic/InfixMinusOperator.java b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixMinusOperator.java index 248966b..918d362 100644 --- a/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixMinusOperator.java +++ b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixMinusOperator.java @@ -23,6 +23,7 @@ import com.ezylang.evalex.data.EvaluationValue; import com.ezylang.evalex.operators.AbstractOperator; import com.ezylang.evalex.operators.InfixOperator; import com.ezylang.evalex.parser.Token; +import java.time.Duration; /** Subtraction of two numbers. */ @InfixOperator(precedence = OPERATOR_PRECEDENCE_ADDITIVE) @@ -41,6 +42,24 @@ public class InfixMinusOperator extends AbstractOperator { .getNumberValue() .subtract( rightOperand.getNumberValue(), expression.getConfiguration().getMathContext())); + + } else if (leftOperand.isDateTimeValue() && rightOperand.isDateTimeValue()) { + return new EvaluationValue( + Duration.ofMillis( + leftOperand.getDateTimeValue().toEpochMilli() + - rightOperand.getDateTimeValue().toEpochMilli())); + + } else if (leftOperand.isDateTimeValue() && rightOperand.isDurationValue()) { + return new EvaluationValue( + leftOperand.getDateTimeValue().minus(rightOperand.getDurationValue())); + } else if (leftOperand.isDurationValue() && rightOperand.isDurationValue()) { + return new EvaluationValue( + leftOperand.getDurationValue().minus(rightOperand.getDurationValue())); + } else if (leftOperand.isDateTimeValue() && rightOperand.isNumberValue()) { + return new EvaluationValue( + leftOperand + .getDateTimeValue() + .minus(Duration.ofMillis(rightOperand.getNumberValue().longValue()))); } else { throw EvaluationException.ofUnsupportedDataTypeInOperation(operatorToken); } diff --git a/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixPlusOperator.java b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixPlusOperator.java index fede27c..bcc795f 100644 --- a/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixPlusOperator.java +++ b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixPlusOperator.java @@ -22,6 +22,7 @@ import com.ezylang.evalex.data.EvaluationValue; import com.ezylang.evalex.operators.AbstractOperator; import com.ezylang.evalex.operators.InfixOperator; import com.ezylang.evalex.parser.Token; +import java.time.Duration; /** * Addition of numbers and strings. If one operand is a string, a string concatenation is performed. @@ -40,6 +41,17 @@ public class InfixPlusOperator extends AbstractOperator { leftOperand .getNumberValue() .add(rightOperand.getNumberValue(), expression.getConfiguration().getMathContext())); + } else if (leftOperand.isDateTimeValue() && rightOperand.isDurationValue()) { + return new EvaluationValue( + leftOperand.getDateTimeValue().plus(rightOperand.getDurationValue())); + } else if (leftOperand.isDurationValue() && rightOperand.isDurationValue()) { + return new EvaluationValue( + leftOperand.getDurationValue().plus(rightOperand.getDurationValue())); + } else if (leftOperand.isDateTimeValue() && rightOperand.isNumberValue()) { + return new EvaluationValue( + leftOperand + .getDateTimeValue() + .plus(Duration.ofMillis(rightOperand.getNumberValue().longValue()))); } else { return new EvaluationValue(leftOperand.getStringValue() + rightOperand.getStringValue()); } diff --git a/src/test/java/com/ezylang/evalex/BaseEvaluationTest.java b/src/test/java/com/ezylang/evalex/BaseEvaluationTest.java index 69a29c1..fa73196 100644 --- a/src/test/java/com/ezylang/evalex/BaseEvaluationTest.java +++ b/src/test/java/com/ezylang/evalex/BaseEvaluationTest.java @@ -16,6 +16,7 @@ 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; @@ -41,6 +42,11 @@ 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/ExpressionEvaluatorNullTest.java b/src/test/java/com/ezylang/evalex/ExpressionEvaluatorNullTest.java new file mode 100644 index 0000000..c44e108 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/ExpressionEvaluatorNullTest.java @@ -0,0 +1,82 @@ +/* + 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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.ezylang.evalex.parser.ParseException; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class ExpressionEvaluatorNullTest extends BaseExpressionEvaluatorTest { + + @Test + void testNullEquals() throws ParseException, EvaluationException { + Expression expression = createExpression("a == null"); + assertExpressionHasExpectedResult(expression.with("a", null), "true"); + assertExpressionHasExpectedResult(expression.with("a", 99), "false"); + } + + @Test + void testNullNotEquals() throws ParseException, EvaluationException { + Expression expression = new Expression("a != null"); + assertExpressionHasExpectedResult(expression.with("a", null), "false"); + assertExpressionHasExpectedResult(expression.with("a", 99), "true"); + } + + @Test + void testHandleWithIf() throws EvaluationException, ParseException { + Expression expression1 = createExpression("IF(a != null, a * 5, 1)"); + assertExpressionHasExpectedResult(expression1.with("a", null), "1"); + assertExpressionHasExpectedResult(expression1.with("a", 3), "15"); + + Expression expression2 = + createExpression("IF(a == null, \"Unknown name\", \"The name is \" + a)"); + assertExpressionHasExpectedResult(expression2.with("a", null), "Unknown name"); + assertExpressionHasExpectedResult(expression2.with("a", "Max"), "The name is Max"); + } + + @Test + void testHandleWithMaps() throws EvaluationException, ParseException { + Expression expression = createExpression("a == null && b == null"); + Map values = new HashMap<>(); + values.put("a", null); + values.put("b", null); + + assertExpressionHasExpectedResult(expression.withValues(values), "true"); + } + + @Test + void testFailWithNoHandling() { + Expression expression1 = createExpression("a * 5").with("a", null); + assertThatThrownBy(expression1::evaluate) + .isInstanceOf(EvaluationException.class) + .hasMessage("Unsupported data types in operation"); + + Expression expression2 = createExpression("FLOOR(a)").with("a", null); + assertThatThrownBy(expression2::evaluate).isInstanceOf(NullPointerException.class); + + Expression expression3 = createExpression("a > 5").with("a", null); + assertThatThrownBy(expression3::evaluate).isInstanceOf(NullPointerException.class); + } + + private void assertExpressionHasExpectedResult(Expression expression, String expectedResult) + throws EvaluationException, ParseException { + assertThat(expression.evaluate().getStringValue()).isEqualTo(expectedResult); + } +} diff --git a/src/test/java/com/ezylang/evalex/data/EvaluationValueTest.java b/src/test/java/com/ezylang/evalex/data/EvaluationValueTest.java index de8ffeb..bd0b06d 100644 --- a/src/test/java/com/ezylang/evalex/data/EvaluationValueTest.java +++ b/src/test/java/com/ezylang/evalex/data/EvaluationValueTest.java @@ -26,11 +26,8 @@ import com.ezylang.evalex.parser.Token; import com.ezylang.evalex.parser.Token.TokenType; import java.math.BigDecimal; import java.math.MathContext; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; +import java.time.*; +import java.util.*; import org.junit.jupiter.api.Test; class EvaluationValueTest { @@ -52,7 +49,9 @@ class EvaluationValueTest { assertThat(value.isStructureValue()).isFalse(); assertThat(value.isArrayValue()).isFalse(); assertThat(value.isExpressionNode()).isFalse(); - assertDataIsCorrect(value, "Hello World", BigDecimal.ZERO, false, String.class); + assertThat(value.isNullValue()).isFalse(); + assertDataIsCorrect( + value, "Hello World", BigDecimal.ZERO, false, Instant.EPOCH, Duration.ZERO, String.class); } @Test @@ -60,7 +59,14 @@ class EvaluationValueTest { EvaluationValue value = new EvaluationValue(new StringBuilder("Hello StringBuilder World")); assertThat(value.isStringValue()).isTrue(); - assertDataIsCorrect(value, "Hello StringBuilder World", BigDecimal.ZERO, false, String.class); + assertDataIsCorrect( + value, + "Hello StringBuilder World", + BigDecimal.ZERO, + false, + Instant.EPOCH, + Duration.ZERO, + String.class); } @Test @@ -68,7 +74,8 @@ class EvaluationValueTest { EvaluationValue value = new EvaluationValue('a'); assertThat(value.isStringValue()).isTrue(); - assertDataIsCorrect(value, "a", BigDecimal.ZERO, false, String.class); + assertDataIsCorrect( + value, "a", BigDecimal.ZERO, false, Instant.EPOCH, Duration.ZERO, String.class); } @Test @@ -81,7 +88,9 @@ class EvaluationValueTest { assertThat(value.isStructureValue()).isFalse(); assertThat(value.isArrayValue()).isFalse(); assertThat(value.isExpressionNode()).isFalse(); - assertDataIsCorrect(value, "true", BigDecimal.ONE, true, Boolean.class); + assertThat(value.isNullValue()).isFalse(); + assertDataIsCorrect( + value, "true", BigDecimal.ONE, true, Instant.EPOCH, Duration.ZERO, Boolean.class); } @Test @@ -89,7 +98,8 @@ class EvaluationValueTest { EvaluationValue value = new EvaluationValue(false); assertThat(value.isBooleanValue()).isTrue(); - assertDataIsCorrect(value, "false", BigDecimal.ZERO, false, Boolean.class); + assertDataIsCorrect( + value, "false", BigDecimal.ZERO, false, Instant.EPOCH, Duration.ZERO, Boolean.class); } @Test @@ -97,7 +107,8 @@ class EvaluationValueTest { EvaluationValue value = new EvaluationValue("true"); assertThat(value.isStringValue()).isTrue(); - assertDataIsCorrect(value, "true", BigDecimal.ONE, true, String.class); + assertDataIsCorrect( + value, "true", BigDecimal.ONE, true, Instant.EPOCH, Duration.ZERO, String.class); } @Test @@ -105,7 +116,124 @@ class EvaluationValueTest { EvaluationValue value = new EvaluationValue(BigDecimal.ZERO); assertThat(value.isNumberValue()).isTrue(); - assertDataIsCorrect(value, "0", BigDecimal.ZERO, false, BigDecimal.class); + assertDataIsCorrect( + value, "0", BigDecimal.ZERO, false, Instant.EPOCH, Duration.ZERO, BigDecimal.class); + } + + @Test + void testInstant() { + Instant instant = Instant.parse("2022-10-30T00:00:00Z"); + EvaluationValue value = new EvaluationValue(instant); + + assertThat(value.isDateTimeValue()).isTrue(); + assertDataIsCorrect( + value, instant.toString(), BigDecimal.ZERO, false, instant, Duration.ZERO, Instant.class); + } + + @Test + void testLocalDate() { + LocalDate localDate = LocalDate.parse("2022-10-30"); + EvaluationValue value = new EvaluationValue(localDate); + + assertThat(value.isDateTimeValue()).isTrue(); + assertDataIsCorrect( + value, + "2022-10-30T00:00:00Z", + BigDecimal.ZERO, + false, + Instant.parse("2022-10-30T00:00:00Z"), + Duration.ZERO, + Instant.class); + } + + @Test + void testLocalDateTime() { + ZoneId zoneId = ZoneId.of("UTC+2"); + LocalDateTime localDateTime = LocalDateTime.parse("2022-10-30T11:20:30"); + EvaluationValue value = new EvaluationValue(localDateTime, zoneId); + + assertThat(value.isDateTimeValue()).isTrue(); + assertDataIsCorrect( + value, + "2022-10-30T09:20:30Z", + BigDecimal.ZERO, + false, + localDateTime.atZone(zoneId).toInstant(), + Duration.ZERO, + Instant.class); + } + + @Test + void testZonedDateTime() { + ZonedDateTime zonedDateTime = + ZonedDateTime.of(LocalDateTime.of(2022, 10, 30, 11, 20, 30), ZoneId.of("GMT+05:30")); + EvaluationValue value = new EvaluationValue(zonedDateTime); + + assertThat(value.isDateTimeValue()).isTrue(); + assertDataIsCorrect( + value, + zonedDateTime.toInstant().toString(), + BigDecimal.ZERO, + false, + zonedDateTime.toInstant(), + Duration.ZERO, + Instant.class); + } + + @Test + void testOffsetDateTime() { + OffsetDateTime offsetDateTime = + OffsetDateTime.of(LocalDateTime.of(2022, 10, 30, 11, 20, 30), ZoneOffset.of("+05:30")); + EvaluationValue value = new EvaluationValue(offsetDateTime); + + assertThat(value.isDateTimeValue()).isTrue(); + assertDataIsCorrect( + value, + offsetDateTime.toInstant().toString(), + BigDecimal.ZERO, + false, + offsetDateTime.toInstant(), + Duration.ZERO, + Instant.class); + } + + @Test + void testStringDateTime() { + EvaluationValue value = new EvaluationValue("2022-10-30T11:20:30Z"); + + assertThat(value.isDateTimeValue()).isFalse(); + assertDataIsCorrect( + value, + "2022-10-30T11:20:30Z", + BigDecimal.ZERO, + false, + Instant.parse("2022-10-30T11:20:30Z"), + Duration.ZERO, + String.class); + } + + @Test + void testDuration() { + EvaluationValue value = new EvaluationValue(Duration.ofMinutes(1)); + + assertThat(value.isDurationValue()).isTrue(); + assertDataIsCorrect( + value, + "PT1M", + BigDecimal.ZERO, + false, + Instant.EPOCH, + Duration.ofMinutes(1), + Duration.class); + } + + @Test + void testStringDuration() { + EvaluationValue value = new EvaluationValue("PT24H"); + + assertThat(value.isDurationValue()).isFalse(); + assertDataIsCorrect( + value, "PT24H", BigDecimal.ZERO, false, Instant.EPOCH, Duration.ofHours(24), String.class); } @Test @@ -113,7 +241,14 @@ class EvaluationValueTest { EvaluationValue value = new EvaluationValue(new BigDecimal("123.5")); assertThat(value.isNumberValue()).isTrue(); - assertDataIsCorrect(value, "123.5", new BigDecimal("123.5"), true, BigDecimal.class); + assertDataIsCorrect( + value, + "123.5", + new BigDecimal("123.5"), + true, + Instant.ofEpochMilli(123), + Duration.ofMillis(123), + BigDecimal.class); } @Test @@ -121,7 +256,14 @@ class EvaluationValueTest { EvaluationValue value = new EvaluationValue((float) 4.5); assertThat(value.isNumberValue()).isTrue(); - assertDataIsCorrect(value, "4.5", BigDecimal.valueOf((float) 4.5), true, BigDecimal.class); + assertDataIsCorrect( + value, + "4.5", + BigDecimal.valueOf((float) 4.5), + true, + Instant.ofEpochMilli(4), + Duration.ofMillis(4), + BigDecimal.class); } @Test @@ -129,7 +271,14 @@ class EvaluationValueTest { EvaluationValue value = new EvaluationValue(8.5); assertThat(value.isNumberValue()).isTrue(); - assertDataIsCorrect(value, "8.5", BigDecimal.valueOf(8.5), true, BigDecimal.class); + assertDataIsCorrect( + value, + "8.5", + BigDecimal.valueOf(8.5), + true, + Instant.ofEpochMilli(8), + Duration.ofMillis(8), + BigDecimal.class); } @Test @@ -137,7 +286,14 @@ class EvaluationValueTest { EvaluationValue value = new EvaluationValue(6L); assertThat(value.isNumberValue()).isTrue(); - assertDataIsCorrect(value, "6", new BigDecimal(6), true, BigDecimal.class); + assertDataIsCorrect( + value, + "6", + new BigDecimal(6), + true, + Instant.ofEpochMilli(6), + Duration.ofMillis(6), + BigDecimal.class); } @Test @@ -145,7 +301,14 @@ class EvaluationValueTest { EvaluationValue value = new EvaluationValue(5); assertThat(value.isNumberValue()).isTrue(); - assertDataIsCorrect(value, "5", new BigDecimal(5), true, BigDecimal.class); + assertDataIsCorrect( + value, + "5", + new BigDecimal(5), + true, + Instant.ofEpochMilli(5), + Duration.ofMillis(5), + BigDecimal.class); } @Test @@ -153,7 +316,14 @@ class EvaluationValueTest { EvaluationValue value = new EvaluationValue((short) 4); assertThat(value.isNumberValue()).isTrue(); - assertDataIsCorrect(value, "4", new BigDecimal(4), true, BigDecimal.class); + assertDataIsCorrect( + value, + "4", + new BigDecimal(4), + true, + Instant.ofEpochMilli(4), + Duration.ofMillis(4), + BigDecimal.class); } @Test @@ -161,7 +331,14 @@ class EvaluationValueTest { EvaluationValue value = new EvaluationValue((byte) 3); assertThat(value.isNumberValue()).isTrue(); - assertDataIsCorrect(value, "3", new BigDecimal(3), true, BigDecimal.class); + assertDataIsCorrect( + value, + "3", + new BigDecimal(3), + true, + Instant.ofEpochMilli(3), + Duration.ofMillis(3), + BigDecimal.class); } @Test @@ -175,6 +352,7 @@ class EvaluationValueTest { assertThat(value.isStructureValue()).isFalse(); assertThat(value.isStringValue()).isFalse(); assertThat(value.isExpressionNode()).isFalse(); + assertThat(value.isNullValue()).isFalse(); assertThat(value.getArrayValue()).hasSize(2); assertThat(value.getArrayValue().get(0).getStringValue()).isEqualTo("1"); @@ -190,6 +368,13 @@ class EvaluationValueTest { assertThat(value.getArrayValue()).isEmpty(); } + @Test + void testArrayNull() { + EvaluationValue value = new EvaluationValue(null); + + assertThat(value.getArrayValue()).isNull(); + } + @Test void testStructure() { Map structure = new HashMap<>(); @@ -203,11 +388,11 @@ class EvaluationValueTest { assertThat(value.isStringValue()).isFalse(); assertThat(value.isArrayValue()).isFalse(); assertThat(value.isExpressionNode()).isFalse(); + assertThat(value.isNullValue()).isFalse(); assertThat(value.getStructureValue()).hasSize(2); assertThat(value.getStructureValue().get("a").getStringValue()).isEqualTo("Hello"); assertThat(value.getStructureValue().get("b").getStringValue()).isEqualTo("99"); - assertThat(value.getValue()).isInstanceOf(Map.class); } @@ -218,6 +403,13 @@ class EvaluationValueTest { assertThat(value.getStructureValue()).isEmpty(); } + @Test + void testStructureNull() { + EvaluationValue value = new EvaluationValue(null); + + assertThat(value.getStructureValue()).isNull(); + } + @Test void testExpressionNode() { ASTNode node = new ASTNode(new Token(1, "a", TokenType.VARIABLE_OR_CONSTANT)); @@ -229,12 +421,15 @@ class EvaluationValueTest { assertThat(value.isStructureValue()).isFalse(); assertThat(value.isArrayValue()).isFalse(); assertThat(value.isStringValue()).isFalse(); + assertThat(value.isNullValue()).isFalse(); assertDataIsCorrect( value, "ASTNode(parameters=[], token=Token(startPosition=1, value=a, type=VARIABLE_OR_CONSTANT))", BigDecimal.ZERO, false, + Instant.EPOCH, + Duration.ZERO, ASTNode.class); } @@ -277,6 +472,20 @@ class EvaluationValueTest { .isEqualByComparingTo("3.99"); } + @Test + void testNull() { + EvaluationValue value = new EvaluationValue(null); + + assertThat(value.isStringValue()).isFalse(); + assertThat(value.isNumberValue()).isFalse(); + assertThat(value.isBooleanValue()).isFalse(); + assertThat(value.isStructureValue()).isFalse(); + assertThat(value.isArrayValue()).isFalse(); + assertThat(value.isExpressionNode()).isFalse(); + assertThat(value.isNullValue()).isTrue(); + assertDataIsCorrect(value, null, null, null); + } + @Test void nestedEvaluationValue() { try { @@ -298,15 +507,27 @@ class EvaluationValueTest { } } + private void assertDataIsCorrect( + EvaluationValue value, String stringValue, BigDecimal numberValue, Boolean booleanValue) { + assertThat(value.getStringValue()).isEqualTo(stringValue); + assertThat(value.getNumberValue()).isEqualTo(numberValue); + assertThat(value.getBooleanValue()).isEqualTo(booleanValue); + } + private void assertDataIsCorrect( EvaluationValue value, String stringValue, BigDecimal numberValue, Boolean booleanValue, + Instant dateTimeValue, + Duration durationValue, Class valueInstance) { + assertDataIsCorrect(value, stringValue, numberValue, booleanValue); assertThat(value.getStringValue()).isEqualTo(stringValue); assertThat(value.getNumberValue()).isEqualTo(numberValue); assertThat(value.getBooleanValue()).isEqualTo(booleanValue); + assertThat(value.getDateTimeValue()).isEqualTo(dateTimeValue); + assertThat(value.getDurationValue()).isEqualTo(durationValue); assertThat(value.getValue()).isInstanceOf(valueInstance); } } diff --git a/src/test/java/com/ezylang/evalex/functions/datetime/DateTimeFunctionsTest.java b/src/test/java/com/ezylang/evalex/functions/datetime/DateTimeFunctionsTest.java new file mode 100644 index 0000000..0844f63 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/functions/datetime/DateTimeFunctionsTest.java @@ -0,0 +1,159 @@ +/* + 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.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 DateTimeFunctionsTest extends BaseEvaluationTest { + + private static final ExpressionConfiguration DateTimeTestConfiguration = + TestConfigurationProvider.StandardConfigurationWithAdditionalTestOperators.toBuilder() + .defaultZoneId(ZoneId.of("UTC+2")) + .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", + }) + void testDateTimeParse(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult, DateTimeTestConfiguration); + } + + @ParameterizedTest + @CsvSource( + delimiter = '|', + value = { + "DT_PARSE(\"NOT A DATE\") | Unable to parse date/time: NOT A DATE", + }) + void testDateTimeParseFailure(String expression, String message) { + assertExpressionThrowsException(expression, message, 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\") |" + + " 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", + }) + void testZonedDateTimeParse(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult, DateTimeTestConfiguration); + } + + @ParameterizedTest + @CsvSource( + delimiter = '|', + value = { + "DT_ZONED_PARSE(\"NOT A DATE\") | Unable to parse zoned date/time: NOT A DATE", + }) + void testZonedDateTimeParseFailure(String expression, String message) { + assertExpressionThrowsException(expression, message, DateTimeTestConfiguration); + } + + @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" + }) + void testDateTime(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult, DateTimeTestConfiguration); + } + + @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" + }) + void testDateTimeFormat(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult, DateTimeTestConfiguration); + } + + @ParameterizedTest + @CsvSource( + delimiter = '|', + value = { + "DT_EPOCH(DT_DATE_TIME_EPOCH(1667130620000)) | 1667130620000", + "DT_EPOCH(DT_DATE_TIME_EPOCH(0)) | 0" + }) + void testDateTimeToEpoch(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = '|', + value = { + "DT_DURATION_MILLIS(1667130620000) | PT463091H50M20S", + "DT_DURATION_MILLIS(0) | PT0S" + }) + void testDurationFromMillis(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = '|', + value = {"DT_DURATION_DAYS(53216) | PT1277184H", "DT_DURATION_DAYS(1) | PT24H"}) + void testDurationFromDays(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = '|', + value = { + "DT_DURATION_PARSE(\"PT1277184H\") | PT1277184H", + "DT_DURATION_PARSE(\"P1D\") | PT24H" + }) + void testDurationParse(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } +} 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 b57beff..ad1fe42 100644 --- a/src/test/java/com/ezylang/evalex/operators/arithmetic/ArithmeticOperatorsTest.java +++ b/src/test/java/com/ezylang/evalex/operators/arithmetic/ArithmeticOperatorsTest.java @@ -19,7 +19,9 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.ezylang.evalex.BaseEvaluationTest; import com.ezylang.evalex.EvaluationException; +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; @@ -84,11 +86,30 @@ class ArithmeticOperatorsTest extends BaseEvaluationTest { "1-89282 : -89281", "1.54321-1.5432 : 0.00001" }) - void testInfixMinus(String expression, String expectedResult) + void testInfixMinusNumber(String expression, String expectedResult) throws EvaluationException, ParseException { assertExpressionHasExpectedResult(expression, expectedResult); } + @ParameterizedTest + @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_DURATION_PARSE(\"PT5S\")-DT_DURATION_PARSE(\"PT2S\") | PT3S" + }) + void testInfixMinusDateTime(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult( + expression, + expectedResult, + TestConfigurationProvider.StandardConfigurationWithAdditionalTestOperators.toBuilder() + .defaultZoneId(ZoneId.of("UTC+2")) + .build()); + } + @ParameterizedTest @CsvSource( delimiter = ':', @@ -165,6 +186,24 @@ class ArithmeticOperatorsTest extends BaseEvaluationTest { assertExpressionHasExpectedResult(expression, expectedResult); } + @ParameterizedTest + @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_DURATION_PARSE(\"PT5S\")+DT_DURATION_PARSE(\"PT2S\") | PT7S" + }) + void testInfixPlusDateTime(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult( + expression, + expectedResult, + TestConfigurationProvider.StandardConfigurationWithAdditionalTestOperators.toBuilder() + .defaultZoneId(ZoneId.of("UTC+2")) + .build()); + } + @ParameterizedTest @CsvSource( delimiter = ':', 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 c3c56e9..05fc27b 100644 --- a/src/test/java/com/ezylang/evalex/operators/booleans/InfixEqualsOperatorTest.java +++ b/src/test/java/com/ezylang/evalex/operators/booleans/InfixEqualsOperatorTest.java @@ -44,7 +44,10 @@ class InfixEqualsOperatorTest extends BaseEvaluationTest { "\"abc\"=\"abc\" : true", "\"abc\"=\"xyz\" : false", "1+2=4-1 : true", - "-5.2=-5.2 :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_DURATION_PARSE(\"PT24H\")=DT_DURATION_PARSE(\"P1D\") : true", }) void testInfixEqualsLiterals(String expression, String expectedResult) throws EvaluationException, ParseException { 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 2c708a9..b6ce858 100644 --- a/src/test/java/com/ezylang/evalex/operators/booleans/InfixGreaterEqualsOperatorTest.java +++ b/src/test/java/com/ezylang/evalex/operators/booleans/InfixGreaterEqualsOperatorTest.java @@ -40,7 +40,11 @@ class InfixGreaterEqualsOperatorTest extends BaseEvaluationTest { "\"9\">=\"5\" : true", "\"9\">=\"9\" : true", "-4>=-4 :true", - "-4>=-5 :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_DURATION_PARSE(\"P2D\")>=DT_DURATION_PARSE(\"PT24H\") : true" }) void testInfixGreaterEqualsLiterals(String expression, String expectedResult) throws EvaluationException, ParseException { 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 5f40249..335b765 100644 --- a/src/test/java/com/ezylang/evalex/operators/booleans/InfixGreaterOperatorTest.java +++ b/src/test/java/com/ezylang/evalex/operators/booleans/InfixGreaterOperatorTest.java @@ -36,7 +36,11 @@ class InfixGreaterOperatorTest extends BaseEvaluationTest { "\"abc\">\"xyz\" : false", "\"ABC\">\"abc\" : false", "\"9\">\"5\" : true", - "-4>-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_DURATION_PARSE(\"P2D\")>DT_DURATION_PARSE(\"PT24H\") : true" }) void testInfixGreaterLiterals(String expression, String expectedResult) throws EvaluationException, ParseException { 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 9d4e200..6ba9ec9 100644 --- a/src/test/java/com/ezylang/evalex/operators/booleans/InfixLessEqualsOperatorTest.java +++ b/src/test/java/com/ezylang/evalex/operators/booleans/InfixLessEqualsOperatorTest.java @@ -40,7 +40,11 @@ class InfixLessEqualsOperatorTest extends BaseEvaluationTest { "\"5\"<=\"9\" : true", "\"9\"<=\"9\" : true", "-4<=-4 :true", - "-5<=-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_DURATION_PARSE(\"P2D\")<=DT_DURATION_PARSE(\"PT24H\") : false" }) void testInfixLessEqualsLiterals(String expression, String expectedResult) throws EvaluationException, ParseException { 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 4a1ca57..5402489 100644 --- a/src/test/java/com/ezylang/evalex/operators/booleans/InfixLessOperatorTest.java +++ b/src/test/java/com/ezylang/evalex/operators/booleans/InfixLessOperatorTest.java @@ -38,7 +38,11 @@ class InfixLessOperatorTest extends BaseEvaluationTest { "\"abc\"<\"xyz\" : true", "\"abc\"<\"ABC\" : false", "\"5\"<\"9\" : true", - "-5<-4 :true" + "-5<-4 :true", + "DT_DATE_TIME(2022,10,30)