Skip to content

Commit

Permalink
- introduces a configuration for the date-time zone id
Browse files Browse the repository at this point in the history
- refactors EvaluationValue conversion to use a configurable converter
- deprecates evaluation value constructor not using a configuration
- deprecates EvaluationValue constructor using double value and MathContext
  • Loading branch information
uklimaschewski committed Sep 20, 2023
1 parent c25dc23 commit 71d1be9
Show file tree
Hide file tree
Showing 68 changed files with 1,558 additions and 232 deletions.
2 changes: 1 addition & 1 deletion docs/concepts/changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
layout: default
title: Major Changes
parent: Concepts
nav_order: 4
nav_order: 5
---

## Major Changes From Version 2 to 3
Expand Down
97 changes: 97 additions & 0 deletions docs/concepts/date_time_duration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
layout: default
title: Working with Date-Times and Duration
parent: Concepts
nav_order: 4
---

## Working with Date-Times and Duration

Since version 3.1.0 of EvalEx, there are two additional data types _DATE_TIME_ and _DURATION_.

_DATE_TIME_ values are stored internally as _java.time.Instant_ values, _DURATION_ values are stored as
_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.
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 smallest amount of a duration is 1 nanosecond.

### Arithmetic operations with _DATE_TIME_ and _DURATION_.

The infix plus and minus operators can be used to do calculations with _DATE_TIME_ and _DURATION_ values.
The outcome of the operation depends on the operator types:

#### Addition
| Left Operand | Right Operand | Result |
|--------------|---------------|------------------------------------------------------------------------|
| _DATE_TIME_ | _DURATION_ | A new _DATE_TIME_ where the duration is added to the date. |
| _DURATION_ | _DURATION_ | A new duration, which is the sum of both durations. |
| _DATE_TIME_ | _NUMBER_ | A new _DATE_TIME_ with the amount of a duration in milliseconds added. |

All other combinations of _DATE_TIME_ and _DURATION_ with other types will do a string concatenation.

Example. Adding a duration to a date-time:
```java
Instant start = Instant.parse("2023-12-03T23:15:30.00Z");
Duration duration = Duration.ofHours(3);

Expression expression = new Expression("start + duration");
EvaluationValue result =
expression
.with("start", start)
.and("duration", duration)
.evaluate();
System.out.println(result); // will print "EvaluationValue(value=2023-12-04T02:15:30Z, dataType=DATE_TIME)"
```

#### Subtraction
| Left Operand | Right Operand | Result |
|--------------|---------------|-----------------------------------------------------------------------------|
| _DATE_TIME_ | _DATE_TIME_ | A duration which reflects the difference between the two date-times. |
| _DATE_TIME_ | _DURATION_ | A new _DATE_TIME_ where the duration is subtracted from the date. |
| _DURATION_ | _DURATION_ | A new duration, which is the difference of both durations. |
| _DATE_TIME_ | _NUMBER_ | A new _DATE_TIME_ with the amount of a duration in milliseconds subtracted. |

All other combinations of _DATE_TIME_ and _DURATION_ with other types will throw an _EvaluationException_.

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");

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)"
```

The string representation of a duration is here in SO format, meaning 12 hours, 4 minutes and 30 seconds.

### Passing other Date-Time Types as variables

Instead of passing _java.time.Instant_ values for _DATE_TIME_ values, you can pass also the following Java data types.
They will be converted automatically to _java.time.Instant_ values.

| Input type | Conversion note |
|----------------|---------------------------------------------------------------------------------------------------------------------------|
| ZonedDateTime | Directly converted. |
| OffsetDateTime | Directly converted. |
| LocalDate | Converted using the configured time zone. Defaults to the systems time zone.<br/>The time is set to beginning of the day. |
| LocalDateTime | Converted using the configured time zone. Defaults to the systems time zone. |
| Date | Directly converted. |
| Calendar | Directly converted. |

### New Date-Time Functions

In addition to the possibility to add and subtract with _DATE_TIME_ and _DURATION_ values, there are also several new
functions to work with date-times. Most of them allow to create, parse and format date-time and duration values.

See Chapter [Date Time Functions](../references/functions.html#date-time-Functions)

### Configuration Changes
4 changes: 2 additions & 2 deletions docs/references/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Available through the _ExpressionConfiguration.StandardFunctionsDictionary_ cons
| STR_LOWER(value) | Converts the given value to lower case |
| STR_UPPER(value) | Converts the given value to upper case |

### trigonometric Functions
### Trigonometric Functions

| Name | Description |
|--------------|------------------------------------------------------------------------------------------------|
Expand Down Expand Up @@ -75,7 +75,7 @@ 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
### Date Time Functions

| Name | Description |
|---------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
Expand Down
34 changes: 18 additions & 16 deletions src/main/java/com/ezylang/evalex/Expression.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,9 @@
import com.ezylang.evalex.data.DataAccessorIfc;
import com.ezylang.evalex.data.EvaluationValue;
import com.ezylang.evalex.functions.FunctionIfc;
import com.ezylang.evalex.parser.ASTNode;
import com.ezylang.evalex.parser.ParseException;
import com.ezylang.evalex.parser.ShuntingYardConverter;
import com.ezylang.evalex.parser.Token;
import com.ezylang.evalex.parser.Tokenizer;
import com.ezylang.evalex.parser.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.*;
import lombok.Getter;

/**
Expand Down Expand Up @@ -100,7 +91,7 @@ public EvaluationValue evaluateSubtree(ASTNode startNode) throws EvaluationExcep
result = EvaluationValue.numberOfString(token.getValue(), configuration.getMathContext());
break;
case STRING_LITERAL:
result = new EvaluationValue(token.getValue());
result = EvaluationValue.stringValue(token.getValue());
break;
case VARIABLE_OR_CONSTANT:
result = getVariableOrConstant(token);
Expand Down Expand Up @@ -158,7 +149,7 @@ private EvaluationValue evaluateFunction(ASTNode startNode, Token token)
List<EvaluationValue> parameterResults = new ArrayList<>();
for (int i = 0; i < startNode.getParameters().size(); i++) {
if (token.getFunctionDefinition().isParameterLazy(i)) {
parameterResults.add(new EvaluationValue(startNode.getParameters().get(i)));
parameterResults.add(convertValue(startNode.getParameters().get(i)));
} else {
parameterResults.add(evaluateSubtree(startNode.getParameters().get(i)));
}
Expand Down Expand Up @@ -219,7 +210,7 @@ private EvaluationValue roundAndStripZerosIfNeeded(EvaluationValue value) {
if (configuration.isStripTrailingZeros()) {
bigDecimal = bigDecimal.stripTrailingZeros();
}
return new EvaluationValue(bigDecimal);
return EvaluationValue.numberValue(bigDecimal);
}

/**
Expand Down Expand Up @@ -266,7 +257,7 @@ public Expression with(String variable, Object value) {
String.format("Can't set value for constant '%s'", variable));
}
}
getDataAccessor().setData(variable, new EvaluationValue(value));
getDataAccessor().setData(variable, convertValue(value));
return this;
}

Expand Down Expand Up @@ -322,7 +313,18 @@ public ASTNode createExpressionNode(String expression) throws ParseException {
* @return An {@link EvaluationValue} of type {@link EvaluationValue.DataType#NUMBER}.
*/
public EvaluationValue convertDoubleValue(double value) {
return new EvaluationValue(value, configuration.getMathContext());
return convertValue(value);
}

/**
* Converts an object value to an {@link EvaluationValue} by considering the configuration {@link
* EvaluationValue(Object, ExpressionConfiguration)}.
*
* @param value The object value to covert.
* @return An {@link EvaluationValue} of the detected type and value.
*/
public EvaluationValue convertValue(Object value) {
return new EvaluationValue(value, configuration);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@
*/
package com.ezylang.evalex.config;

import com.ezylang.evalex.data.DataAccessorIfc;
import com.ezylang.evalex.data.EvaluationValue;
import com.ezylang.evalex.data.MapBasedDataAccessor;
import com.ezylang.evalex.data.*;
import com.ezylang.evalex.functions.FunctionIfc;
import com.ezylang.evalex.functions.basic.*;
import com.ezylang.evalex.functions.datetime.*;
Expand Down Expand Up @@ -75,6 +73,9 @@ 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 operator dictionary holds all operators that will be allowed in an expression. */
@Builder.Default
@Getter
Expand Down Expand Up @@ -227,8 +228,14 @@ 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();
/** The time zone id. By default, the system default zone id is used. */
@Builder.Default @Getter private final ZoneId zoneId = DEFAULT_ZONE_ID;

/** The converter to use when converting different data types to an {@link EvaluationValue}. */
@Builder.Default @Getter
private final EvaluationValueConverterIfc evaluationValueConverter =
new DefaultEvaluationValueConverter();

/**
* Convenience method to create a default configuration.
*
Expand All @@ -243,14 +250,12 @@ public static ExpressionConfiguration defaultConfiguration() {
*
* @param operators variable number of arguments with a map entry holding the operator name and
* implementation. <br>
* Example:
* <pre>
* ExpressionConfiguration.defaultConfiguration()
* .withAdditionalOperators(
* Map.entry("++", new PrefixPlusPlusOperator()),
* Map.entry("++", new PostfixPlusPlusOperator()));
* </pre>
*
* Example: <code>
* ExpressionConfiguration.defaultConfiguration()
* .withAdditionalOperators(
* Map.entry("++", new PrefixPlusPlusOperator()),
* Map.entry("++", new PostfixPlusPlusOperator()));
* </code>
* @return The modified configuration, to allow chaining of methods.
*/
@SafeVarargs
Expand All @@ -266,14 +271,12 @@ public final ExpressionConfiguration withAdditionalOperators(
*
* @param functions variable number of arguments with a map entry holding the functions name and
* implementation. <br>
* Example:
* <pre>
* ExpressionConfiguration.defaultConfiguration()
* .withAdditionalFunctions(
* Map.entry("save", new SaveFunction()),
* Map.entry("update", new UpdateFunction()));
* </pre>
*
* Example: <code>
* ExpressionConfiguration.defaultConfiguration()
* .withAdditionalFunctions(
* Map.entry("save", new SaveFunction()),
* Map.entry("update", new UpdateFunction()));
* </code>
* @return The modified configuration, to allow chaining of methods.
*/
@SafeVarargs
Expand All @@ -288,19 +291,19 @@ private static Map<String, EvaluationValue> getStandardConstants() {

Map<String, EvaluationValue> constants = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);

constants.put("TRUE", new EvaluationValue(true));
constants.put("FALSE", new EvaluationValue(false));
constants.put("TRUE", EvaluationValue.booleanValue(true));
constants.put("FALSE", EvaluationValue.booleanValue(false));
constants.put(
"PI",
new EvaluationValue(
EvaluationValue.numberValue(
new BigDecimal(
"3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679")));
constants.put(
"E",
new EvaluationValue(
EvaluationValue.numberValue(
new BigDecimal(
"2.71828182845904523536028747135266249775724709369995957496696762772407663")));
constants.put("NULL", new EvaluationValue(null));
constants.put("NULL", EvaluationValue.nullValue());

return constants;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
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.data;

import com.ezylang.evalex.config.ExpressionConfiguration;
import com.ezylang.evalex.data.conversion.*;
import java.util.Arrays;
import java.util.List;

/**
* The default implementation of the {@link EvaluationValueConverterIfc}, used in the standard
* configuration.
*
* <table>
* <tr>
* <th>Input type</th><th>Converter used</th>
* </tr>
* <tr><td>BigDecimal</td><td>NumberConverter</td></tr>
* <tr><td>Long, long</td><td>NumberConverter</td></tr>
* <tr><td>Integer, int</td><td>NumberConverter</td></tr>
* <tr><td>Short, short</td><td>NumberConverter</td></tr>
* <tr><td>Byte, byte</td><td>NumberConverter</td></tr>
* <tr><td>Double, double</td><td>NumberConverter *</td></tr>
* <tr><td>Float, float</td><td>NumberConverter *</td></tr>
* <tr><td>CharSequence , String</td><td>StringConverter</td></tr>
* <tr><td>Boolean, boolean</td><td>BooleanConverter</td></tr>
* <tr><td>Instant</td><td>DateTimeConverter</td></tr>
* <tr><td>Date</td><td>DateTimeConverter</td></tr>
* <tr><td>Calendar</td><td>DateTimeConverter</td></tr>
* <tr><td>ZonedDateTime</td><td>DateTimeConverter</td></tr>
* <tr><td>LocalDate</td><td>DateTimeConverter - the configured zone id will be used for conversion</td></tr>
* <tr><td>LocalDateTime</td><td>DateTimeConverter - the configured zone id will be used for conversion</td></tr>
* <tr><td>OffsetDateTime</td><td>DateTimeConverter</td></tr>
* <tr><td>Duration</td><td>DurationConverter</td></tr>
* <tr><td>ASTNode</td><td>ASTNode</td></tr>
* <tr><td>List&lt;?&gt;</td><td>ArrayConverter - each entry will be converted</td></tr>
* <tr><td>Map&lt?,?&gt;</td><td>StructureConverter - each entry will be converted.</td></tr>
* </table>
*
* <i>* Be careful with conversion problems when using float or double, which are fractional
* numbers. A (float)0.1 is e.g. converted to 0.10000000149011612</i>
*/
public class DefaultEvaluationValueConverter implements EvaluationValueConverterIfc {

static List<ConverterIfc> converters =
Arrays.asList(
new NumberConverter(),
new StringConverter(),
new BooleanConverter(),
new DateTimeConverter(),
new DurationConverter(),
new ExpressionNodeConverter(),
new ArrayConverter(),
new StructureConverter());

public EvaluationValue convertObject(Object object, ExpressionConfiguration configuration) {

if (object == null) {
return EvaluationValue.nullValue();
}

for (ConverterIfc converter : converters) {
if (converter.canConvert(object)) {
return converter.convert(object, configuration);
}
}

throw new IllegalArgumentException(
"Unsupported data type '" + object.getClass().getName() + "'");
}
}
Loading

0 comments on commit 71d1be9

Please sign in to comment.