From 2571f82f717ea4ff8934d95609300ee96fc2a918 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sat, 1 Nov 2025 21:58:49 +0100 Subject: [PATCH 1/6] replace mxparser --- dependencies.gradle | 5 + .../com/cleanroommc/modularui/ModularUI.java | 6 - .../modularui/api/drawable/IDrawable.java | 1 - .../modularui/utils/MathUtils.java | 51 +- .../modularui/utils/ParseResult.java | 29 +- .../cleanroommc/modularui/utils/SIPrefix.java | 13 + .../utils/math/PostfixPercentOperator.java | 29 + .../widgets/textfield/TextFieldWidget.java | 6 +- .../com/ezylang/evalex/BaseException.java | 42 ++ .../ezylang/evalex/EvaluationException.java | 36 + .../java/com/ezylang/evalex/Expression.java | 469 +++++++++++++ src/main/java/com/ezylang/evalex/LICENSE | 201 ++++++ src/main/java/com/ezylang/evalex/README.md | 298 +++++++++ .../config/ExpressionConfiguration.java | 514 ++++++++++++++ .../evalex/config/FunctionDictionaryIfc.java | 76 +++ .../config/MapBasedFunctionDictionary.java | 69 ++ .../config/MapBasedOperatorDictionary.java | 109 +++ .../evalex/config/OperatorDictionaryIfc.java | 156 +++++ .../ezylang/evalex/data/DataAccessorIfc.java | 40 ++ .../ezylang/evalex/data/EvaluationValue.java | 576 ++++++++++++++++ .../evalex/data/MapBasedDataAccessor.java | 39 ++ .../data/conversion/ArrayConverter.java | 159 +++++ .../data/conversion/BinaryConverter.java | 38 ++ .../data/conversion/BooleanConverter.java | 35 + .../evalex/data/conversion/ConverterIfc.java | 47 ++ .../data/conversion/DateTimeConverter.java | 111 ++++ .../DefaultEvaluationValueConverter.java | 94 +++ .../data/conversion/DurationConverter.java | 37 ++ .../EvaluationValueConverterIfc.java | 36 + .../conversion/ExpressionNodeConverter.java | 36 + .../data/conversion/NumberConverter.java | 67 ++ .../data/conversion/StringConverter.java | 45 ++ .../data/conversion/StructureConverter.java | 43 ++ .../evalex/functions/AbstractFunction.java | 103 +++ .../ezylang/evalex/functions/FunctionIfc.java | 94 +++ .../evalex/functions/FunctionParameter.java | 58 ++ .../FunctionParameterDefinition.java | 57 ++ .../evalex/functions/FunctionParameters.java | 33 + .../evalex/functions/basic/AbsFunction.java | 37 ++ .../basic/AbstractMinMaxFunction.java | 45 ++ .../functions/basic/AverageFunction.java | 82 +++ .../functions/basic/CeilingFunction.java | 40 ++ .../functions/basic/CoalesceFunction.java | 41 ++ .../evalex/functions/basic/FactFunction.java | 45 ++ .../evalex/functions/basic/FloorFunction.java | 40 ++ .../evalex/functions/basic/IfFunction.java | 46 ++ .../evalex/functions/basic/Log10Function.java | 38 ++ .../evalex/functions/basic/LogFunction.java | 38 ++ .../evalex/functions/basic/MaxFunction.java | 41 ++ .../evalex/functions/basic/MinFunction.java | 41 ++ .../evalex/functions/basic/NotFunction.java | 38 ++ .../functions/basic/RandomFunction.java | 38 ++ .../evalex/functions/basic/RoundFunction.java | 46 ++ .../evalex/functions/basic/SqrtFunction.java | 66 ++ .../evalex/functions/basic/SumFunction.java | 57 ++ .../functions/basic/SwitchFunction.java | 102 +++ .../datetime/DateTimeFormatFunction.java | 74 +++ .../datetime/DateTimeNewFunction.java | 124 ++++ .../datetime/DateTimeNowFunction.java | 47 ++ .../datetime/DateTimeParseFunction.java | 85 +++ .../datetime/DateTimeToEpochFunction.java | 35 + .../datetime/DateTimeTodayFunction.java | 67 ++ .../datetime/DurationFromMillisFunction.java | 39 ++ .../datetime/DurationNewFunction.java | 58 ++ .../datetime/DurationParseFunction.java | 39 ++ .../datetime/DurationToMillisFunction.java | 35 + .../functions/datetime/ZoneIdConverter.java | 53 ++ .../functions/string/StringContains.java | 42 ++ .../string/StringEndsWithFunction.java | 40 ++ .../string/StringFormatFunction.java | 87 +++ .../functions/string/StringLeftFunction.java | 64 ++ .../string/StringLengthFunction.java | 39 ++ .../functions/string/StringLowerFunction.java | 35 + .../string/StringMatchesFunction.java | 42 ++ .../functions/string/StringRightFunction.java | 64 ++ .../functions/string/StringSplitFunction.java | 53 ++ .../string/StringStartsWithFunction.java | 40 ++ .../string/StringSubstringFunction.java | 64 ++ .../functions/string/StringTrimFunction.java | 39 ++ .../functions/string/StringUpperFunction.java | 35 + .../functions/trigonometric/AcosFunction.java | 52 ++ .../trigonometric/AcosHFunction.java | 43 ++ .../trigonometric/AcosRFunction.java | 53 ++ .../functions/trigonometric/AcotFunction.java | 39 ++ .../trigonometric/AcotHFunction.java | 38 ++ .../trigonometric/AcotRFunction.java | 38 ++ .../functions/trigonometric/AsinFunction.java | 52 ++ .../trigonometric/AsinHFunction.java | 38 ++ .../trigonometric/AsinRFunction.java | 56 ++ .../trigonometric/Atan2Function.java | 41 ++ .../trigonometric/Atan2RFunction.java | 40 ++ .../functions/trigonometric/AtanFunction.java | 37 ++ .../trigonometric/AtanHFunction.java | 43 ++ .../trigonometric/AtanRFunction.java | 37 ++ .../functions/trigonometric/CosFunction.java | 37 ++ .../functions/trigonometric/CosHFunction.java | 37 ++ .../functions/trigonometric/CosRFunction.java | 37 ++ .../functions/trigonometric/CotFunction.java | 38 ++ .../functions/trigonometric/CotHFunction.java | 38 ++ .../functions/trigonometric/CotRFunction.java | 38 ++ .../functions/trigonometric/CscFunction.java | 38 ++ .../functions/trigonometric/CscHFunction.java | 38 ++ .../functions/trigonometric/CscRFunction.java | 38 ++ .../functions/trigonometric/DegFunction.java | 38 ++ .../functions/trigonometric/RadFunction.java | 38 ++ .../functions/trigonometric/SecFunction.java | 38 ++ .../functions/trigonometric/SecHFunction.java | 38 ++ .../functions/trigonometric/SecRFunction.java | 38 ++ .../functions/trigonometric/SinFunction.java | 37 ++ .../functions/trigonometric/SinHFunction.java | 37 ++ .../functions/trigonometric/SinRFunction.java | 37 ++ .../functions/trigonometric/TanFunction.java | 37 ++ .../functions/trigonometric/TanHFunction.java | 37 ++ .../functions/trigonometric/TanRFunction.java | 37 ++ .../evalex/operators/AbstractOperator.java | 94 +++ .../evalex/operators/InfixOperator.java | 46 ++ .../OperatorAnnotationNotFoundException.java | 27 + .../ezylang/evalex/operators/OperatorIfc.java | 157 +++++ .../evalex/operators/PostfixOperator.java | 43 ++ .../evalex/operators/PrefixOperator.java | 43 ++ .../arithmetic/InfixDivisionOperator.java | 57 ++ .../arithmetic/InfixMinusOperator.java | 70 ++ .../arithmetic/InfixModuloOperator.java | 57 ++ .../InfixMultiplicationOperator.java | 50 ++ .../arithmetic/InfixPlusOperator.java | 60 ++ .../arithmetic/InfixPowerOfOperator.java | 80 +++ .../arithmetic/PrefixMinusOperator.java | 44 ++ .../arithmetic/PrefixPlusOperator.java | 44 ++ .../operators/booleans/InfixAndOperator.java | 41 ++ .../booleans/InfixEqualsOperator.java | 43 ++ .../booleans/InfixGreaterEqualsOperator.java | 37 ++ .../booleans/InfixGreaterOperator.java | 37 ++ .../booleans/InfixLessEqualsOperator.java | 37 ++ .../operators/booleans/InfixLessOperator.java | 37 ++ .../booleans/InfixNotEqualsOperator.java | 43 ++ .../operators/booleans/InfixOrOperator.java | 41 ++ .../operators/booleans/PrefixNotOperator.java | 35 + .../com/ezylang/evalex/parser/ASTNode.java | 73 ++ .../ezylang/evalex/parser/ParseException.java | 42 ++ .../evalex/parser/ShuntingYardConverter.java | 292 ++++++++ .../java/com/ezylang/evalex/parser/Token.java | 80 +++ .../com/ezylang/evalex/parser/Tokenizer.java | 628 ++++++++++++++++++ 142 files changed, 9689 insertions(+), 46 deletions(-) create mode 100644 src/main/java/com/cleanroommc/modularui/utils/math/PostfixPercentOperator.java create mode 100644 src/main/java/com/ezylang/evalex/BaseException.java create mode 100644 src/main/java/com/ezylang/evalex/EvaluationException.java create mode 100644 src/main/java/com/ezylang/evalex/Expression.java create mode 100644 src/main/java/com/ezylang/evalex/LICENSE create mode 100644 src/main/java/com/ezylang/evalex/README.md create mode 100644 src/main/java/com/ezylang/evalex/config/ExpressionConfiguration.java create mode 100644 src/main/java/com/ezylang/evalex/config/FunctionDictionaryIfc.java create mode 100644 src/main/java/com/ezylang/evalex/config/MapBasedFunctionDictionary.java create mode 100644 src/main/java/com/ezylang/evalex/config/MapBasedOperatorDictionary.java create mode 100644 src/main/java/com/ezylang/evalex/config/OperatorDictionaryIfc.java create mode 100644 src/main/java/com/ezylang/evalex/data/DataAccessorIfc.java create mode 100644 src/main/java/com/ezylang/evalex/data/EvaluationValue.java create mode 100644 src/main/java/com/ezylang/evalex/data/MapBasedDataAccessor.java create mode 100644 src/main/java/com/ezylang/evalex/data/conversion/ArrayConverter.java create mode 100644 src/main/java/com/ezylang/evalex/data/conversion/BinaryConverter.java create mode 100644 src/main/java/com/ezylang/evalex/data/conversion/BooleanConverter.java create mode 100644 src/main/java/com/ezylang/evalex/data/conversion/ConverterIfc.java create mode 100644 src/main/java/com/ezylang/evalex/data/conversion/DateTimeConverter.java create mode 100644 src/main/java/com/ezylang/evalex/data/conversion/DefaultEvaluationValueConverter.java create mode 100644 src/main/java/com/ezylang/evalex/data/conversion/DurationConverter.java create mode 100644 src/main/java/com/ezylang/evalex/data/conversion/EvaluationValueConverterIfc.java create mode 100644 src/main/java/com/ezylang/evalex/data/conversion/ExpressionNodeConverter.java create mode 100644 src/main/java/com/ezylang/evalex/data/conversion/NumberConverter.java create mode 100644 src/main/java/com/ezylang/evalex/data/conversion/StringConverter.java create mode 100644 src/main/java/com/ezylang/evalex/data/conversion/StructureConverter.java create mode 100644 src/main/java/com/ezylang/evalex/functions/AbstractFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/FunctionIfc.java create mode 100644 src/main/java/com/ezylang/evalex/functions/FunctionParameter.java create mode 100644 src/main/java/com/ezylang/evalex/functions/FunctionParameterDefinition.java create mode 100644 src/main/java/com/ezylang/evalex/functions/FunctionParameters.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/AbsFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/AbstractMinMaxFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/AverageFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/CeilingFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/CoalesceFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/FactFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/FloorFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/IfFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/Log10Function.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/LogFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/MaxFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/MinFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/NotFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/RandomFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/RoundFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/SqrtFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/SumFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/basic/SwitchFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/datetime/DateTimeFormatFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/datetime/DateTimeNewFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/datetime/DateTimeNowFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/datetime/DateTimeParseFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/datetime/DateTimeToEpochFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/datetime/DateTimeTodayFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/datetime/DurationFromMillisFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/datetime/DurationNewFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/datetime/DurationParseFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/datetime/DurationToMillisFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/datetime/ZoneIdConverter.java create mode 100644 src/main/java/com/ezylang/evalex/functions/string/StringContains.java create mode 100644 src/main/java/com/ezylang/evalex/functions/string/StringEndsWithFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/string/StringFormatFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/string/StringLeftFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/string/StringLengthFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/string/StringLowerFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/string/StringMatchesFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/string/StringRightFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/string/StringSplitFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/string/StringStartsWithFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/string/StringSubstringFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/string/StringTrimFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/string/StringUpperFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/AcosFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/AcosHFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/AcosRFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/AcotFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/AcotHFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/AcotRFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/AsinFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/AsinHFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/AsinRFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/Atan2Function.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/Atan2RFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/AtanFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/AtanHFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/AtanRFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/CosFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/CosHFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/CosRFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/CotFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/CotHFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/CotRFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/CscFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/CscHFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/CscRFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/DegFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/RadFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/SecFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/SecHFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/SecRFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/SinFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/SinHFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/SinRFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/TanFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/TanHFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/trigonometric/TanRFunction.java create mode 100644 src/main/java/com/ezylang/evalex/operators/AbstractOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/InfixOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/OperatorAnnotationNotFoundException.java create mode 100644 src/main/java/com/ezylang/evalex/operators/OperatorIfc.java create mode 100644 src/main/java/com/ezylang/evalex/operators/PostfixOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/PrefixOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/arithmetic/InfixDivisionOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/arithmetic/InfixMinusOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/arithmetic/InfixModuloOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/arithmetic/InfixMultiplicationOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/arithmetic/InfixPlusOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/arithmetic/InfixPowerOfOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/arithmetic/PrefixMinusOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/arithmetic/PrefixPlusOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/booleans/InfixAndOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/booleans/InfixEqualsOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/booleans/InfixGreaterEqualsOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/booleans/InfixGreaterOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/booleans/InfixLessEqualsOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/booleans/InfixLessOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/booleans/InfixNotEqualsOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/booleans/InfixOrOperator.java create mode 100644 src/main/java/com/ezylang/evalex/operators/booleans/PrefixNotOperator.java create mode 100644 src/main/java/com/ezylang/evalex/parser/ASTNode.java create mode 100644 src/main/java/com/ezylang/evalex/parser/ParseException.java create mode 100644 src/main/java/com/ezylang/evalex/parser/ShuntingYardConverter.java create mode 100644 src/main/java/com/ezylang/evalex/parser/Token.java create mode 100644 src/main/java/com/ezylang/evalex/parser/Tokenizer.java diff --git a/dependencies.gradle b/dependencies.gradle index 9f9a1707d..4265f2912 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -27,6 +27,11 @@ dependencies { embed2('org.joml:joml:1.10.8') { transitive = false } embedSources("org.joml:joml:1.10.8:sources") { transitive = false } + compileOnly("org.projectlombok:lombok:1.18.42") + annotationProcessor("org.projectlombok:lombok:1.18.42") + testCompileOnly("org.projectlombok:lombok:1.18.42") + testAnnotationProcessor("org.projectlombok:lombok:1.18.42") + implementation(rfg.deobf("curse.maven:baubles-227083:2518667")) compileOnlyApi rfg.deobf("curse.maven:neverenoughanimation-1062347:6533650-sources-6533651") testImplementation rfg.deobf("curse.maven:neverenoughanimation-1062347:6533650-sources-6533651") diff --git a/src/main/java/com/cleanroommc/modularui/ModularUI.java b/src/main/java/com/cleanroommc/modularui/ModularUI.java index dd4cbdfe7..6a6f49d4b 100644 --- a/src/main/java/com/cleanroommc/modularui/ModularUI.java +++ b/src/main/java/com/cleanroommc/modularui/ModularUI.java @@ -11,7 +11,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.Nullable; -import org.mariuszgromada.math.mxparser.License; import java.util.function.Predicate; @@ -40,11 +39,6 @@ public class ModularUI { @Mod.Instance public static ModularUI INSTANCE; - static { - // confirm mXparser license - License.iConfirmNonCommercialUse("CleanroomMC"); - } - @Mod.EventHandler public void preInit(FMLPreInitializationEvent event) { proxy.preInit(event); diff --git a/src/main/java/com/cleanroommc/modularui/api/drawable/IDrawable.java b/src/main/java/com/cleanroommc/modularui/api/drawable/IDrawable.java index 4c5154a25..35214eb3d 100644 --- a/src/main/java/com/cleanroommc/modularui/api/drawable/IDrawable.java +++ b/src/main/java/com/cleanroommc/modularui/api/drawable/IDrawable.java @@ -111,7 +111,6 @@ default void drawAtZeroPadded(GuiContext context, Area area, WidgetTheme widgetT draw(context, area.getPadding().getLeft(), area.getPadding().getTop(), area.paddedWidth(), area.paddedHeight(), widgetTheme); } - /** * @return if theme color can be applied on this drawable */ diff --git a/src/main/java/com/cleanroommc/modularui/utils/MathUtils.java b/src/main/java/com/cleanroommc/modularui/utils/MathUtils.java index e85037d55..a5b19ce8f 100644 --- a/src/main/java/com/cleanroommc/modularui/utils/MathUtils.java +++ b/src/main/java/com/cleanroommc/modularui/utils/MathUtils.java @@ -1,9 +1,16 @@ package com.cleanroommc.modularui.utils; +import com.cleanroommc.modularui.utils.math.PostfixPercentOperator; + import net.minecraft.util.math.MathHelper; -import org.mariuszgromada.math.mxparser.Constant; -import org.mariuszgromada.math.mxparser.Expression; +import com.ezylang.evalex.BaseException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; +import org.apache.commons.lang3.tuple.Pair; + +import java.math.BigDecimal; public class MathUtils { @@ -12,23 +19,12 @@ public class MathUtils { public static final float PI_HALF = PI / 2f; public static final float PI_QUART = PI / 4f; - // SI prefixes - public static final Constant k = new Constant("k", 1e3); - public static final Constant M = new Constant("M", 1e6); - public static final Constant G = new Constant("G", 1e9); - public static final Constant T = new Constant("T", 1e12); - public static final Constant P = new Constant("P", 1e15); - public static final Constant E = new Constant("E", 1e18); - public static final Constant Z = new Constant("Z", 1e21); - public static final Constant Y = new Constant("Y", 1e24); - public static final Constant m = new Constant("m", 1e-3); - public static final Constant u = new Constant("u", 1e-6); - public static final Constant n = new Constant("n", 1e-9); - public static final Constant p = new Constant("p", 1e-12); - public static final Constant f = new Constant("f", 1e-15); - public static final Constant a = new Constant("a", 1e-18); - public static final Constant z = new Constant("z", 1e-21); - public static final Constant y = new Constant("y", 1e-24); + public static final ExpressionConfiguration MATH_CFG = ExpressionConfiguration.builder() + .arraysAllowed(false) + .structuresAllowed(false) + .stripTrailingZeros(true) + .build() + .withAdditionalOperators(Pair.of("%", new PostfixPercentOperator())); public static ParseResult parseExpression(String expression) { return parseExpression(expression, Double.NaN, false); @@ -43,16 +39,19 @@ public static ParseResult parseExpression(String expression, double defaultValue } public static ParseResult parseExpression(String expression, double defaultValue, boolean useSiPrefixes) { - if (expression == null || expression.isEmpty()) return ParseResult.success(defaultValue); - Expression e = new Expression(expression); + if (expression == null || expression.isEmpty()) { + return ParseResult.success(EvaluationValue.numberValue(new BigDecimal(defaultValue))); + } + + Expression e = new Expression(expression, MATH_CFG); if (useSiPrefixes) { - e.addConstants(k, M, G, T, P, E, Z, Y, m, u, n, p, f, a, z, y); + SIPrefix.addAllToExpression(e); } - double result = e.calculate(); - if (Double.isNaN(result)) { - return ParseResult.failure(defaultValue, e.getErrorMessage()); + try { + return ParseResult.success(e.evaluate()); + } catch (BaseException exception) { + return ParseResult.failure(exception); } - return ParseResult.success(result); } public static int clamp(int v, int min, int max) { diff --git a/src/main/java/com/cleanroommc/modularui/utils/ParseResult.java b/src/main/java/com/cleanroommc/modularui/utils/ParseResult.java index 12cc7bf7f..30ac3e252 100644 --- a/src/main/java/com/cleanroommc/modularui/utils/ParseResult.java +++ b/src/main/java/com/cleanroommc/modularui/utils/ParseResult.java @@ -1,25 +1,27 @@ package com.cleanroommc.modularui.utils; +import com.ezylang.evalex.BaseException; +import com.ezylang.evalex.data.EvaluationValue; import org.jetbrains.annotations.NotNull; public class ParseResult { - private final double result; - private final String error; + private final EvaluationValue result; + private final BaseException error; - public static ParseResult success(double result) { + public static ParseResult success(EvaluationValue result) { return new ParseResult(result, null); } - public static ParseResult failure(@NotNull String error) { - return failure(Double.NaN, error); + public static ParseResult failure(@NotNull BaseException error) { + return failure(null, error); } - public static ParseResult failure(double value, @NotNull String error) { + public static ParseResult failure(EvaluationValue value, @NotNull BaseException error) { return new ParseResult(value, error); } - private ParseResult(double result, String error) { + private ParseResult(EvaluationValue result, BaseException error) { this.result = result; this.error = error; } @@ -33,14 +35,21 @@ public boolean isFailure() { } public boolean hasValue() { - return !Double.isNaN(this.result); + return this.result != null; } - public double getResult() { + public EvaluationValue getResult() { return result; } - public String getError() { + public BaseException getError() { return error; } + + public String getErrorMessage() { + return isFailure() ? + String.format("%s for Token %s at %d:%d", + this.error.getMessage(), this.error.getTokenString(), + this.error.getStartPosition(), this.error.getEndPosition()) : null; + } } diff --git a/src/main/java/com/cleanroommc/modularui/utils/SIPrefix.java b/src/main/java/com/cleanroommc/modularui/utils/SIPrefix.java index ba874769d..02c198b78 100644 --- a/src/main/java/com/cleanroommc/modularui/utils/SIPrefix.java +++ b/src/main/java/com/cleanroommc/modularui/utils/SIPrefix.java @@ -1,5 +1,7 @@ package com.cleanroommc.modularui.utils; +import com.ezylang.evalex.Expression; + public enum SIPrefix { Quetta('Q', 30), @@ -39,6 +41,11 @@ public boolean isOne() { return this == One; } + public void addToExpression(Expression e) { + e.with(String.valueOf(this.symbol), this.factor); + } + + public static final SIPrefix[] VALUES = values(); public static final SIPrefix[] HIGH = new SIPrefix[values().length / 2]; public static final SIPrefix[] LOW = new SIPrefix[values().length / 2]; @@ -49,4 +56,10 @@ public boolean isOne() { LOW[i] = values[HIGH.length + 1 + i]; } } + + public static void addAllToExpression(Expression e) { + for (SIPrefix siPrefix : VALUES) { + siPrefix.addToExpression(e); + } + } } diff --git a/src/main/java/com/cleanroommc/modularui/utils/math/PostfixPercentOperator.java b/src/main/java/com/cleanroommc/modularui/utils/math/PostfixPercentOperator.java new file mode 100644 index 000000000..75f095638 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/utils/math/PostfixPercentOperator.java @@ -0,0 +1,29 @@ +package com.cleanroommc.modularui.utils.math; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.OperatorIfc; +import com.ezylang.evalex.operators.PostfixOperator; +import com.ezylang.evalex.parser.Token; + +import java.math.BigDecimal; + +@PostfixOperator(precedence = OperatorIfc.OPERATOR_PRECEDENCE_MULTIPLICATIVE - 1) +public class PostfixPercentOperator extends AbstractOperator { + + public static final BigDecimal HUNDRED = new BigDecimal(100); + + @Override + public EvaluationValue evaluate(Expression expression, Token operatorToken, EvaluationValue... operands) throws EvaluationException { + EvaluationValue operand = operands[0]; + + if (operand.isNumberValue()) { + return expression.convertValue( + operand.getNumberValue().divide(HUNDRED, expression.getConfiguration().getMathContext())); + } else { + throw EvaluationException.ofUnsupportedDataTypeInOperation(operatorToken); + } + } +} diff --git a/src/main/java/com/cleanroommc/modularui/widgets/textfield/TextFieldWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/textfield/TextFieldWidget.java index d72b05519..36b5baf9a 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/textfield/TextFieldWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/textfield/TextFieldWidget.java @@ -30,12 +30,12 @@ public class TextFieldWidget extends BaseTextFieldWidget { public double parse(String num) { ParseResult result = MathUtils.parseExpression(num, this.defaultNumber, true); - double value = result.getResult(); if (result.isFailure()) { - this.mathFailMessage = result.getError(); + this.mathFailMessage = result.getErrorMessage(); ModularUI.LOGGER.error("Math expression error in {}: {}", this, this.mathFailMessage); + return defaultNumber; } - return value; + return result.getResult().getNumberValue().doubleValue(); } public IStringValue createMathFailMessageValue() { diff --git a/src/main/java/com/ezylang/evalex/BaseException.java b/src/main/java/com/ezylang/evalex/BaseException.java new file mode 100644 index 000000000..9ce958100 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/BaseException.java @@ -0,0 +1,42 @@ +/* + 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; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +/** + * Base exception class used in EvalEx. + */ +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +@ToString +@Getter +public class BaseException extends Exception { + + @EqualsAndHashCode.Include private final int startPosition; + @EqualsAndHashCode.Include private final int endPosition; + @EqualsAndHashCode.Include private final String tokenString; + @EqualsAndHashCode.Include private final String message; + + public BaseException(int startPosition, int endPosition, String tokenString, String message) { + super(message); + this.startPosition = startPosition; + this.endPosition = endPosition; + this.tokenString = tokenString; + this.message = super.getMessage(); + } +} diff --git a/src/main/java/com/ezylang/evalex/EvaluationException.java b/src/main/java/com/ezylang/evalex/EvaluationException.java new file mode 100644 index 000000000..4f5509ee9 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/EvaluationException.java @@ -0,0 +1,36 @@ +/* + 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; + +import com.ezylang.evalex.parser.Token; + +/** + * Exception while evaluating the parsed expression. + */ +public class EvaluationException extends BaseException { + + public EvaluationException(Token token, String message) { + super( + token.getStartPosition(), + token.getStartPosition() + token.getValue().length(), + token.getValue(), + message); + } + + public static EvaluationException ofUnsupportedDataTypeInOperation(Token token) { + return new EvaluationException(token, "Unsupported data types in operation"); + } +} diff --git a/src/main/java/com/ezylang/evalex/Expression.java b/src/main/java/com/ezylang/evalex/Expression.java new file mode 100644 index 000000000..d3c0263ef --- /dev/null +++ b/src/main/java/com/ezylang/evalex/Expression.java @@ -0,0 +1,469 @@ +/* + 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; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.DataAccessorIfc; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.FunctionIfc; +import com.ezylang.evalex.operators.OperatorIfc; +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 lombok.Getter; + +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; + +/** + * Main class that allow creating, parsing, passing parameters and evaluating an expression string. + * + * @see EvalEx Homepage + */ +public class Expression { + + @Getter private final ExpressionConfiguration configuration; + + @Getter private final String expressionString; + + @Getter private final DataAccessorIfc dataAccessor; + + @Getter + private final Map constants = + new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + private ASTNode abstractSyntaxTree; + + /** + * Creates a new expression with the default configuration. The expression is not parsed until it + * is first evaluated or validated. + * + * @param expressionString A string holding an expression. + */ + public Expression(String expressionString) { + this(expressionString, ExpressionConfiguration.defaultConfiguration()); + } + + /** + * Creates a new expression with a custom configuration. The expression is not parsed until it is + * first evaluated or validated. + * + * @param expressionString A string holding an expression. + */ + public Expression(String expressionString, ExpressionConfiguration configuration) { + this.expressionString = expressionString; + this.configuration = configuration; + this.dataAccessor = configuration.getDataAccessorSupplier().get(); + this.constants.putAll(configuration.getDefaultConstants()); + } + + /** + * Creates a copy with the same expression string, configuration and syntax tree from an existing + * expression. The existing expression will be parsed to populate the syntax tree. + * + * @param expression An existing expression. + * @throws ParseException If there were problems while parsing the existing expression. + */ + public Expression(Expression expression) throws ParseException { + this(expression.getExpressionString(), expression.getConfiguration()); + this.abstractSyntaxTree = expression.getAbstractSyntaxTree(); + } + + /** + * Evaluates the expression by parsing it (if not done before) and the evaluating it. + * + * @return The evaluation result value. + * @throws EvaluationException If there were problems while evaluating the expression. + * @throws ParseException If there were problems while parsing the expression. + */ + public EvaluationValue evaluate() throws EvaluationException, ParseException { + EvaluationValue result = evaluateSubtree(getAbstractSyntaxTree(), 0); + if (result.isNumberValue()) { + BigDecimal bigDecimal = result.getNumberValue(); + if (configuration.getDecimalPlacesResult() + != ExpressionConfiguration.DECIMAL_PLACES_ROUNDING_UNLIMITED) { + bigDecimal = roundValue(bigDecimal, configuration.getDecimalPlacesResult()); + } + + if (configuration.isStripTrailingZeros()) { + bigDecimal = bigDecimal.stripTrailingZeros(); + } + + result = EvaluationValue.numberValue(bigDecimal); + } + + return result; + } + + /** + * Evaluates only a subtree of the abstract syntax tree. + * + * @param startNode The {@link ASTNode} to start evaluation from. + * @return The evaluation result value. + * @throws EvaluationException If there were problems while evaluating the expression. + */ + public EvaluationValue evaluateSubtree(ASTNode startNode) throws EvaluationException { + return evaluateSubtree(startNode, 0); + } + + /** + * Evaluates only a subtree of the abstract syntax tree. + * + * @param startNode The {@link ASTNode} to start evaluation from. + * @param depth The current depth, to track recursion level and secure it does not exceed the + * maximum level defined by {@link ExpressionConfiguration#getMaxRecursionDepth()} + * @return The evaluation result value. + * @throws EvaluationException If there were problems while evaluating the expression. + */ + private EvaluationValue evaluateSubtree(ASTNode startNode, int depth) throws EvaluationException { + if (depth > configuration.getMaxRecursionDepth()) { + throw new EvaluationException(startNode.getToken(), "Max recursion depth exceeded"); + } + + Token token = startNode.getToken(); + EvaluationValue result; + switch (token.getType()) { + case NUMBER_LITERAL: + result = EvaluationValue.numberOfString(token.getValue(), configuration.getMathContext()); + break; + case STRING_LITERAL: + result = EvaluationValue.stringValue(token.getValue()); + break; + case VARIABLE_OR_CONSTANT: + result = getVariableOrConstant(token); + if (result.isExpressionNode()) { + result = evaluateSubtree(result.getExpressionNode(), depth + 1); + } + break; + case PREFIX_OPERATOR: + case POSTFIX_OPERATOR: + result = + token + .getOperatorDefinition() + .evaluate( + this, token, evaluateSubtree(startNode.getParameters().get(0), depth + 1)); + break; + case INFIX_OPERATOR: + result = evaluateInfixOperator(startNode, token, depth + 1); + break; + case ARRAY_INDEX: + result = evaluateArrayIndex(startNode, depth + 1); + break; + case STRUCTURE_SEPARATOR: + result = evaluateStructureSeparator(startNode, depth + 1); + break; + case FUNCTION: + result = evaluateFunction(startNode, token, depth + 1); + break; + default: + throw new EvaluationException(token, "Unexpected evaluation token: " + token); + } + if (result.isNumberValue() + && configuration.getDecimalPlacesRounding() + != ExpressionConfiguration.DECIMAL_PLACES_ROUNDING_UNLIMITED) { + return EvaluationValue.numberValue( + roundValue(result.getNumberValue(), configuration.getDecimalPlacesRounding())); + } + + return result; + } + + private EvaluationValue getVariableOrConstant(Token token) throws EvaluationException { + EvaluationValue result = constants.get(token.getValue()); + if (result == null) { + result = getDataAccessor().getData(token.getValue()); + } + if (result == null) { + if (configuration.isLenientMode()) { + return EvaluationValue.UNDEFINED; + } + throw new EvaluationException( + token, String.format("Variable or constant value for '%s' not found", token.getValue())); + } + return result; + } + + private EvaluationValue evaluateFunction(ASTNode startNode, Token token, int depth) + throws EvaluationException { + List parameterResults = new ArrayList<>(); + for (int i = 0; i < startNode.getParameters().size(); i++) { + if (token.getFunctionDefinition().isParameterLazy(i)) { + parameterResults.add(convertValue(startNode.getParameters().get(i))); + } else { + parameterResults.add(evaluateSubtree(startNode.getParameters().get(i), depth + 1)); + } + } + + EvaluationValue[] parameters = parameterResults.toArray(new EvaluationValue[0]); + + FunctionIfc function = token.getFunctionDefinition(); + + function.validatePreEvaluation(token, parameters); + + return function.evaluate(this, token, parameters); + } + + private EvaluationValue evaluateArrayIndex(ASTNode startNode, int depth) + throws EvaluationException { + EvaluationValue array = evaluateSubtree(startNode.getParameters().get(0), depth + 1); + EvaluationValue index = evaluateSubtree(startNode.getParameters().get(1), depth + 1); + + if (array.isArrayValue() && index.isNumberValue()) { + if (index.getNumberValue().intValue() < 0 + || index.getNumberValue().intValue() >= array.getArrayValue().size()) { + throw new EvaluationException( + startNode.getToken(), + String.format( + "Index %d out of bounds for array of length %d", + index.getNumberValue().intValue(), array.getArrayValue().size())); + } + return array.getArrayValue().get(index.getNumberValue().intValue()); + } else { + throw EvaluationException.ofUnsupportedDataTypeInOperation(startNode.getToken()); + } + } + + private EvaluationValue evaluateStructureSeparator(ASTNode startNode, int depth) + throws EvaluationException { + EvaluationValue structure = evaluateSubtree(startNode.getParameters().get(0), depth + 1); + Token nameToken = startNode.getParameters().get(1).getToken(); + String name = nameToken.getValue(); + + if (structure.isStructureValue()) { + if (!structure.getStructureValue().containsKey(name)) { + throw new EvaluationException( + nameToken, String.format("Field '%s' not found in structure", name)); + } + return structure.getStructureValue().get(name); + } else { + throw EvaluationException.ofUnsupportedDataTypeInOperation(startNode.getToken()); + } + } + + private EvaluationValue evaluateInfixOperator(ASTNode startNode, Token token, int depth) + throws EvaluationException { + EvaluationValue left; + EvaluationValue right; + + OperatorIfc op = token.getOperatorDefinition(); + if (op.isOperandLazy()) { + left = convertValue(startNode.getParameters().get(0)); + right = convertValue(startNode.getParameters().get(1)); + } else { + left = evaluateSubtree(startNode.getParameters().get(0), depth + 1); + right = evaluateSubtree(startNode.getParameters().get(1), depth + 1); + } + return op.evaluate(this, token, left, right); + } + + /** + * Rounds the given value. + * + * @param value The input value. + * @param decimalPlaces The number of decimal places to round to. + * @return The rounded value, or the input value if rounding is not configured or possible. + */ + private BigDecimal roundValue(BigDecimal value, int decimalPlaces) { + value = value.setScale(decimalPlaces, configuration.getMathContext().getRoundingMode()); + return value; + } + + /** + * Returns the root ode of the parsed abstract syntax tree. + * + * @return The abstract syntax tree root node. + * @throws ParseException If there were problems while parsing the expression. + */ + public ASTNode getAbstractSyntaxTree() throws ParseException { + if (abstractSyntaxTree == null) { + Tokenizer tokenizer = new Tokenizer(expressionString, configuration); + ShuntingYardConverter converter = + new ShuntingYardConverter(expressionString, tokenizer.parse(), configuration); + abstractSyntaxTree = converter.toAbstractSyntaxTree(); + } + + return abstractSyntaxTree; + } + + /** + * Validates the expression by parsing it and throwing an exception, if the parser fails. + * + * @throws ParseException If there were problems while parsing the expression. + */ + public void validate() throws ParseException { + getAbstractSyntaxTree(); + } + + /** + * Adds a variable value to the expression data storage. If a value with the same name already + * exists, it is overridden. The data type will be determined by examining the passed value + * object. An exception is thrown, if he found data type is not supported. + * + * @param variable The variable name. + * @param value The variable value. + * @return The Expression instance, to allow chaining of methods. + */ + public Expression with(String variable, Object value) { + if (constants.containsKey(variable)) { + if (configuration.isAllowOverwriteConstants()) { + constants.remove(variable); + } else { + throw new UnsupportedOperationException( + String.format("Can't set value for constant '%s'", variable)); + } + } + getDataAccessor().setData(variable, convertValue(value)); + return this; + } + + /** + * Adds a variable value to the expression data storage. If a value with the same name already + * exists, it is overridden. The data type will be determined by examining the passed value + * object. An exception is thrown, if he found data type is not supported. + * + * @param variable The variable name. + * @param value The variable value. + * @return The Expression instance, to allow chaining of methods. + */ + public Expression and(String variable, Object value) { + return with(variable, value); + } + + /** + * Adds all variables values defined in the map with their name (key) and value to the data + * storage.If a value with the same name already exists, it is overridden. The data type will be + * determined by examining the passed value object. An exception is thrown, if he found data type + * is not supported. + * + * @param values A map with variable values. + * @return The Expression instance, to allow chaining of methods. + */ + public Expression withValues(Map values) { + for (Map.Entry entry : values.entrySet()) { + with(entry.getKey(), entry.getValue()); + } + return this; + } + + /** + * Return a copy of the expression using the copy constructor {@link Expression(Expression)}. + * + * @return The copied Expression instance. + * @throws ParseException If there were problems while parsing the existing expression. + */ + public Expression copy() throws ParseException { + return new Expression(this); + } + + /** + * Create an AST representation for an expression string. The node can then be used as a + * sub-expression. Subexpressions are not cached. + * + * @param expression The expression string. + * @return The root node of the expression AST representation. + * @throws ParseException On any parsing error. + */ + public ASTNode createExpressionNode(String expression) throws ParseException { + Tokenizer tokenizer = new Tokenizer(expression, configuration); + ShuntingYardConverter converter = + new ShuntingYardConverter(expression, tokenizer.parse(), configuration); + return converter.toAbstractSyntaxTree(); + } + + /** + * Converts a double value to an {@link EvaluationValue} by considering the configured {@link + * java.math.MathContext}. + * + * @param value The double value to covert. + * @return An {@link EvaluationValue} of type {@link EvaluationValue.DataType#NUMBER}. + */ + public EvaluationValue convertDoubleValue(double value) { + 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 EvaluationValue.of(value, configuration); + } + + /** + * Returns the list of all nodes of the abstract syntax tree. + * + * @return The list of all nodes in the parsed expression. + * @throws ParseException If there were problems while parsing the expression. + */ + public List getAllASTNodes() throws ParseException { + return getAllASTNodesForNode(getAbstractSyntaxTree()); + } + + private List getAllASTNodesForNode(ASTNode node) { + List nodes = new ArrayList<>(); + nodes.add(node); + for (ASTNode child : node.getParameters()) { + nodes.addAll(getAllASTNodesForNode(child)); + } + return nodes; + } + + /** + * Returns all variables that are used i the expression, excluding the constants like e.g. + * PI or TRUE and FALSE. + * + * @return All used variables excluding constants. + * @throws ParseException If there were problems while parsing the expression. + */ + public Set getUsedVariables() throws ParseException { + Set variables = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + + for (ASTNode node : getAllASTNodes()) { + if (node.getToken().getType() == Token.TokenType.VARIABLE_OR_CONSTANT + && !constants.containsKey(node.getToken().getValue())) { + variables.add(node.getToken().getValue()); + } + } + + return variables; + } + + /** + * Returns all variables that are used in the expression, but have no value assigned. + * + * @return All variables that have no value assigned. + * @throws ParseException If there were problems while parsing the expression. + */ + public Set getUndefinedVariables() throws ParseException { + Set variables = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + for (String variable : getUsedVariables()) { + if (getDataAccessor().getData(variable) == null) { + variables.add(variable); + } + } + return variables; + } +} diff --git a/src/main/java/com/ezylang/evalex/LICENSE b/src/main/java/com/ezylang/evalex/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/src/main/java/com/ezylang/evalex/README.md b/src/main/java/com/ezylang/evalex/README.md new file mode 100644 index 000000000..ef43839cc --- /dev/null +++ b/src/main/java/com/ezylang/evalex/README.md @@ -0,0 +1,298 @@ +EvalEx - Java Expression Evaluator +========== + +[![Build](https://github.com/ezylang/EvalEx/actions/workflows/build.yml/badge.svg)](https://github.com/ezylang/EvalEx/actions/workflows/build.yml) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ezylang_EvalEx&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=ezylang_EvalEx) +[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=ezylang_EvalEx&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=ezylang_EvalEx) +[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=ezylang_EvalEx&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=ezylang_EvalEx) +[![Maven Central](https://img.shields.io/maven-central/v/com.ezylang/EvalEx.svg?label=Maven%20Central)](https://search.maven.org/search?q=a:%22EvalEx%22) + +| For a complete documentation, see [the documentation site](https://ezylang.github.io/EvalEx/). | +|------------------------------------------------------------------------------------------------| + +EvalEx is a handy expression evaluator for Java, that allows to parse and evaluate expression strings. + +## Key Features: + +- Supports numerical, boolean, string, date time, duration, array and structure expressions, operations and variables. +- Array and structure support: Arrays and structures can be mixed, building arbitrary data + structures. +- Supports the NULL datatype. +- Uses BigDecimal for numerical calculations. +- MathContext and number of decimal places can be configured, with optional automatic rounding. +- No dependencies to external libraries. +- Easy integration into existing systems to access data. +- Predefined boolean and mathematical operators. +- Predefined mathematical, boolean and string functions. +- Custom functions and operators can be added. +- Functions can be defined with a variable number of arguments (see MIN, MAX and SUM functions). +- Supports hexadecimal and scientific notations of numbers. +- Supports implicit multiplication, e.g. 2x, 2sin(x), (a+b)(a-b) or 2(x-y) which equals to (a+b)\*(a-b) or 2\*( + x-y) +- Lazy evaluation of function parameters (see the IF function) and support of sub-expressions. +- Requires minimum Java version 11. + +## Documentation + +The full documentation for EvalEx can be found +on [GitHub Pages](https://ezylang.github.io/EvalEx/) + +## Discussion + +For announcements, questions and ideas visit +the [Discussions area](https://github.com/ezylang/EvalEx/discussions). + +## Download / Including + +You can download the binaries, source code and JavaDoc jars from +[Maven Central](https://central.sonatype.com/artifact/com.ezylang/EvalEx).\ +You will find there also copy/paste templates for including EvalEx in your project with build +systems like Maven or Gradle. + +### Maven + +To include it in your Maven project, add the dependency to your pom. For example: + +```xml + + + + com.ezylang + EvalEx + 3.5.0 + + +``` + +### Gradle + +If you're using gradle add the dependencies to your project's app build.gradle: + +```gradle +dependencies { + compile 'com.ezylang:EvalEx:3.5.0' +} +``` + +## Examples + +### A simple example, that shows how it works in general: + +```java +Expression expression = new Expression("1 + 2 / (4 * SQRT(4))"); + +EvaluationValue result = expression.evaluate(); + +System.out. + +println(result.getNumberValue()); // prints 1.25 +``` + +### Variables can be specified in the expression and their values can be passed for evaluation: + +```java +Expression expression = new Expression("(a + b) * (a - b)"); + +EvaluationValue result = expression + .with("a", 3.5) + .and("b", 2.5) + .evaluate(); + +System.out. + +println(result.getNumberValue()); // prints 6.00 +``` + +### Expression can be copied and evaluated with a different set of values: + +Using a copy of the expression allows a thread-safe evaluation of that copy, without parsing the expression again. +The copy uses the same expression string, configuration and syntax tree. +The existing expression will be parsed to populate the syntax tree. + +Make sure each thread has its own copy of the original expression. + +```java +Expression expression = new Expression("a + b").with("a", 1).and("b", 2); +Expression copiedExpression = expression.copy().with("a", 3).and("b", 4); + +EvaluationValue result = expression.evaluate(); +EvaluationValue copiedResult = copiedExpression.evaluate(); + +System.out. + +println(result.getNumberValue()); // prints 3 + System.out. + +println(copiedResult.getNumberValue()); // prints 7 +``` + +### Values can be passed in a map + +Instead of specifying the variable values one by one, they can be set by defining a map with names and values and then +passing it to the _withValues()_ method: + +The data conversion of the passed values will automatically be performed through a customizable converter. + +It is also possible to configure a custom data accessor to read and write values. + +```java +Expression expression = new Expression("a+b+c"); + +Map values = new HashMap<>(); +values. + +put("a",true); +values. + +put("b"," : "); +values. + +put("c",24.7); + +EvaluationValue result = expression.withValues(values).evaluate(); + +System.out. + +println(result.getStringValue()); // prints "true : 24.7" +``` + +See chapter [Data Types](https://ezylang.github.io/EvalEx/concepts/datatypes.html) for details on the conversion. + +Another option to have EvalEx use your data is to define a custom data accessor. + +See chapter [Data Access](https://ezylang.github.io/EvalEx/customization/data_access.html) for details. + +### Boolean expressions produce a boolean result: + +```java +Expression expression = new Expression("level > 2 || level <= 0"); + +EvaluationValue result = expression + .with("level", 3.5) + .evaluate(); + +System.out. + +println(result.getBooleanValue()); // prints true +``` + +### Like in Java, strings and text can be mixed: + +```java +Expression expression = new Expression("\"Hello \" + name + \", you are \" + age") + .with("name", "Frank") + .and("age", 38); + +System.out. + +println(expression.evaluate(). + +getStringValue()); // prints Hello Frank, you are 38 +``` + +### Arrays (also multidimensional) are supported and can be passed as Java _Lists_ or instances of Java arrays. + +See the [Documentation](https://ezylang.github.io/EvalEx/concepts/datatypes.html#array) +for more details. + +```java +Expression expression = new Expression("values[i-1] * factors[i-1]"); + +EvaluationValue result = expression + .with("values", List.of(2, 3, 4)) + .and("factors", new Object[]{2, 4, 6}) + .and("i", 1) + .evaluate(); + +System.out. + +println(result.getNumberValue()); // prints 4 +``` + +### Structures are supported and can be passed as Java _Maps_. + +Arrays and Structures can be combined to build arbitrary data structures. See +the [Documentation](https://ezylang.github.io/EvalEx/concepts/datatypes.html#structure) +for more details. + +```java +Map order = new HashMap<>(); +order. + +put("id",12345); +order. + +put("name","Mary"); + +Map position = new HashMap<>(); +position. + +put("article",3114); +position. + +put("amount",3); +position. + +put("price",new BigDecimal("14.95")); + + order. + +put("positions",List.of(position)); + +Expression expression = new Expression("order.positions[x].amount * order.positions[x].price") + .with("order", order) + .and("x", 0); + +BigDecimal result = expression.evaluate().getNumberValue(); + +System.out. + +println(result); // prints 44.85 +``` + +### Calculating with date-time and duration + +Date-tme and duration values are supported. There are functions to create, parse and format these values. +Additionally, the plus and minus operators can be used to e.g. add or subtract durations, or to calculate the +difference between two dates: + +```java +Instant start = Instant.parse("2023-12-05T11:20:00.00Z"); +Instant end = Instant.parse("2023-12-04T23:15:30.00Z"); + +Expression expression = new Expression("start - end"); +EvaluationValue result = expression + .with("start", start) + .and("end", end) + .evaluate(); +System.out. + +println(result); // will print "EvaluationValue(value=PT12H4M30S, dataType=DURATION)" +``` + +See the [Documentation](https://ezylang.github.io/EvalEx/concepts/date_time_duration.html) for more details. + +## EvalEx-big-math + +[Big-math](https://github.com/eobermuhlner/big-math) is a library by Eric Obermühlner. It provides +advanced Java BigDecimal math functions using an arbitrary precision. + +[EvalEx-big-math](https://github.com/ezylang/EvalEx-big-math) adds the advanced math functions from +big-math to EvalEx. + +## Author and License + +Copyright 2012-2023 by Udo Klimaschewski + +**Thanks to all who contributed to this +project: [Contributors](https://github.com/ezylang/EvalEx/graphs/contributors)** + +The software is licensed under the Apache License, Version 2.0 ( +see [LICENSE](https://raw.githubusercontent.com/ezylang/EvalEx/main/LICENSE) file). + +* The *power of* operator (^) implementation was copied + from [Stack Overflow](http://stackoverflow.com/questions/3579779/how-to-do-a-fractional-power-on-bigdecimal-in-java) + Thanks to Gene Marin +* The SQRT() function implementation was taken from the + book [The Java Programmers Guide To numerical Computing](http://www.amazon.de/Java-Number-Cruncher-Programmers-Numerical/dp/0130460419) ( + Ronald Mak, 2002) diff --git a/src/main/java/com/ezylang/evalex/config/ExpressionConfiguration.java b/src/main/java/com/ezylang/evalex/config/ExpressionConfiguration.java new file mode 100644 index 000000000..f2ae40c29 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/config/ExpressionConfiguration.java @@ -0,0 +1,514 @@ +/* + 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.config; + +import com.ezylang.evalex.data.DataAccessorIfc; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.data.MapBasedDataAccessor; +import com.ezylang.evalex.data.conversion.DefaultEvaluationValueConverter; +import com.ezylang.evalex.data.conversion.EvaluationValueConverterIfc; +import com.ezylang.evalex.functions.FunctionIfc; +import com.ezylang.evalex.functions.basic.AbsFunction; +import com.ezylang.evalex.functions.basic.AverageFunction; +import com.ezylang.evalex.functions.basic.CeilingFunction; +import com.ezylang.evalex.functions.basic.CoalesceFunction; +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.SwitchFunction; +import com.ezylang.evalex.functions.datetime.DateTimeFormatFunction; +import com.ezylang.evalex.functions.datetime.DateTimeNewFunction; +import com.ezylang.evalex.functions.datetime.DateTimeNowFunction; +import com.ezylang.evalex.functions.datetime.DateTimeParseFunction; +import com.ezylang.evalex.functions.datetime.DateTimeToEpochFunction; +import com.ezylang.evalex.functions.datetime.DateTimeTodayFunction; +import com.ezylang.evalex.functions.datetime.DurationFromMillisFunction; +import com.ezylang.evalex.functions.datetime.DurationNewFunction; +import com.ezylang.evalex.functions.datetime.DurationParseFunction; +import com.ezylang.evalex.functions.datetime.DurationToMillisFunction; +import com.ezylang.evalex.functions.string.StringContains; +import com.ezylang.evalex.functions.string.StringEndsWithFunction; +import com.ezylang.evalex.functions.string.StringFormatFunction; +import com.ezylang.evalex.functions.string.StringLeftFunction; +import com.ezylang.evalex.functions.string.StringLengthFunction; +import com.ezylang.evalex.functions.string.StringLowerFunction; +import com.ezylang.evalex.functions.string.StringMatchesFunction; +import com.ezylang.evalex.functions.string.StringRightFunction; +import com.ezylang.evalex.functions.string.StringSplitFunction; +import com.ezylang.evalex.functions.string.StringStartsWithFunction; +import com.ezylang.evalex.functions.string.StringSubstringFunction; +import com.ezylang.evalex.functions.string.StringTrimFunction; +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.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 lombok.Builder; +import lombok.Getter; +import org.apache.commons.lang3.tuple.Pair; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.Supplier; + +/** + * The expression configuration can be used to configure various aspects of expression parsing and + * evaluation.
+ * A Builder is provided to create custom configurations, e.g.:
+ * + *
+ *   ExpressionConfiguration config = ExpressionConfiguration.builder().mathContext(MathContext.DECIMAL32).arraysAllowed(false).build();
+ * 
+ * + *
+ * Additional operators and functions can be added to an existing configuration:
+ * + *
+ *     ExpressionConfiguration.defaultConfiguration()
+ *        .withAdditionalOperators(
+ *            Pair.of("++", new PrefixPlusPlusOperator()),
+ *            Pair.of("++", new PostfixPlusPlusOperator()))
+ *        .withAdditionalFunctions(Pair.of("save", new SaveFunction()),
+ *            Pair.of("update", new UpdateFunction()));
+ * 
+ */ +@Builder(toBuilder = true) +@Getter +public class ExpressionConfiguration { + + /** + * The standard set constants for EvalEx. + */ + public static final Map StandardConstants = + Collections.unmodifiableMap(getStandardConstants()); + + /** + * Setting the decimal places to unlimited, will disable intermediate rounding. + */ + public static final int DECIMAL_PLACES_ROUNDING_UNLIMITED = -1; + + /** + * The default math context has a precision of 68 and {@link RoundingMode#HALF_EVEN}. + */ + public static final MathContext DEFAULT_MATH_CONTEXT = + new MathContext(68, RoundingMode.HALF_EVEN); + + /** + * The default maximum depth for recursion is 2000 levels. + */ + public static final int DEFAULT_MAX_RECURSION_DEPTH = 2_000; + + /** + * The default date time formatters used when parsing a date string. Each format will be tried and + * the first matching will be used. + * + *
    + *
  • {@link DateTimeFormatter#ISO_DATE_TIME} + *
  • {@link DateTimeFormatter#ISO_DATE} + *
  • {@link DateTimeFormatter#ISO_LOCAL_DATE_TIME} + *
  • {@link DateTimeFormatter#ISO_LOCAL_DATE} + *
+ */ + protected static final List DEFAULT_DATE_TIME_FORMATTERS = + new ArrayList<>( + Arrays.asList( + DateTimeFormatter.ISO_DATE_TIME, + DateTimeFormatter.ISO_DATE, + DateTimeFormatter.ISO_LOCAL_DATE_TIME, + DateTimeFormatter.ISO_LOCAL_DATE, + DateTimeFormatter.RFC_1123_DATE_TIME)); + + /** + * The operator dictionary holds all operators that will be allowed in an expression. + */ + @Builder.Default + @SuppressWarnings("unchecked") + private final OperatorDictionaryIfc operatorDictionary = + MapBasedOperatorDictionary.ofOperators( + // arithmetic + Pair.of("+", new PrefixPlusOperator()), + Pair.of("-", new PrefixMinusOperator()), + Pair.of("+", new InfixPlusOperator()), + Pair.of("-", new InfixMinusOperator()), + Pair.of("*", new InfixMultiplicationOperator()), + Pair.of("/", new InfixDivisionOperator()), + Pair.of("^", new InfixPowerOfOperator()), + Pair.of("%", new InfixModuloOperator()), + // booleans + Pair.of("=", new InfixEqualsOperator()), + Pair.of("==", new InfixEqualsOperator()), + Pair.of("!=", new InfixNotEqualsOperator()), + Pair.of("<>", new InfixNotEqualsOperator()), + Pair.of(">", new InfixGreaterOperator()), + Pair.of(">=", new InfixGreaterEqualsOperator()), + Pair.of("<", new InfixLessOperator()), + Pair.of("<=", new InfixLessEqualsOperator()), + Pair.of("&&", new InfixAndOperator()), + Pair.of("||", new InfixOrOperator()), + Pair.of("!", new PrefixNotOperator())); + + /** + * The function dictionary holds all functions that will be allowed in an expression. + */ + @Builder.Default + @SuppressWarnings("unchecked") + private final FunctionDictionaryIfc functionDictionary = + MapBasedFunctionDictionary.ofFunctions( + // basic functions + Pair.of("ABS", new AbsFunction()), + Pair.of("AVERAGE", new AverageFunction()), + Pair.of("CEILING", new CeilingFunction()), + Pair.of("COALESCE", new CoalesceFunction()), + Pair.of("FACT", new FactFunction()), + Pair.of("FLOOR", new FloorFunction()), + Pair.of("IF", new IfFunction()), + Pair.of("LOG", new LogFunction()), + Pair.of("LOG10", new Log10Function()), + Pair.of("MAX", new MaxFunction()), + Pair.of("MIN", new MinFunction()), + Pair.of("NOT", new NotFunction()), + Pair.of("RANDOM", new RandomFunction()), + Pair.of("ROUND", new RoundFunction()), + Pair.of("SQRT", new SqrtFunction()), + Pair.of("SUM", new SumFunction()), + Pair.of("SWITCH", new SwitchFunction()), + // trigonometric + Pair.of("ACOS", new AcosFunction()), + Pair.of("ACOSH", new AcosHFunction()), + Pair.of("ACOSR", new AcosRFunction()), + Pair.of("ACOT", new AcotFunction()), + Pair.of("ACOTH", new AcotHFunction()), + Pair.of("ACOTR", new AcotRFunction()), + Pair.of("ASIN", new AsinFunction()), + Pair.of("ASINH", new AsinHFunction()), + Pair.of("ASINR", new AsinRFunction()), + Pair.of("ATAN", new AtanFunction()), + Pair.of("ATAN2", new Atan2Function()), + Pair.of("ATAN2R", new Atan2RFunction()), + Pair.of("ATANH", new AtanHFunction()), + Pair.of("ATANR", new AtanRFunction()), + Pair.of("COS", new CosFunction()), + Pair.of("COSH", new CosHFunction()), + Pair.of("COSR", new CosRFunction()), + Pair.of("COT", new CotFunction()), + Pair.of("COTH", new CotHFunction()), + Pair.of("COTR", new CotRFunction()), + Pair.of("CSC", new CscFunction()), + Pair.of("CSCH", new CscHFunction()), + Pair.of("CSCR", new CscRFunction()), + Pair.of("DEG", new DegFunction()), + Pair.of("RAD", new RadFunction()), + Pair.of("SIN", new SinFunction()), + Pair.of("SINH", new SinHFunction()), + Pair.of("SINR", new SinRFunction()), + Pair.of("SEC", new SecFunction()), + Pair.of("SECH", new SecHFunction()), + Pair.of("SECR", new SecRFunction()), + Pair.of("TAN", new TanFunction()), + Pair.of("TANH", new TanHFunction()), + Pair.of("TANR", new TanRFunction()), + // string functions + Pair.of("STR_CONTAINS", new StringContains()), + Pair.of("STR_ENDS_WITH", new StringEndsWithFunction()), + Pair.of("STR_FORMAT", new StringFormatFunction()), + Pair.of("STR_LEFT", new StringLeftFunction()), + Pair.of("STR_LENGTH", new StringLengthFunction()), + Pair.of("STR_LOWER", new StringLowerFunction()), + Pair.of("STR_MATCHES", new StringMatchesFunction()), + Pair.of("STR_RIGHT", new StringRightFunction()), + Pair.of("STR_SPLIT", new StringSplitFunction()), + Pair.of("STR_STARTS_WITH", new StringStartsWithFunction()), + Pair.of("STR_SUBSTRING", new StringSubstringFunction()), + Pair.of("STR_TRIM", new StringTrimFunction()), + Pair.of("STR_UPPER", new StringUpperFunction()), + // date time functions + Pair.of("DT_DATE_NEW", new DateTimeNewFunction()), + Pair.of("DT_DATE_PARSE", new DateTimeParseFunction()), + Pair.of("DT_DATE_FORMAT", new DateTimeFormatFunction()), + Pair.of("DT_DATE_TO_EPOCH", new DateTimeToEpochFunction()), + Pair.of("DT_DURATION_NEW", new DurationNewFunction()), + Pair.of("DT_DURATION_FROM_MILLIS", new DurationFromMillisFunction()), + Pair.of("DT_DURATION_TO_MILLIS", new DurationToMillisFunction()), + Pair.of("DT_DURATION_PARSE", new DurationParseFunction()), + Pair.of("DT_NOW", new DateTimeNowFunction()), + Pair.of("DT_TODAY", new DateTimeTodayFunction())); + + /** + * The math context to use. + */ + @Builder.Default private final MathContext mathContext = DEFAULT_MATH_CONTEXT; + + /** + * The data accessor is responsible for accessing variable and constant values in an expression. + * The supplier will be called once for each new expression, the default is to create a new {@link + * MapBasedDataAccessor} instance for each expression, providing a new storage for each + * expression. + */ + @Builder.Default + private final Supplier dataAccessorSupplier = MapBasedDataAccessor::new; + + /** + * Default constants will be added automatically to each expression and can be used in expression + * evaluation. + */ + @Builder.Default + private final Map defaultConstants = getStandardConstants(); + + /** + * Support for arrays in expressions are allowed or not. + */ + @Builder.Default private final boolean arraysAllowed = true; + + /** + * Support for structures in expressions are allowed or not. + */ + @Builder.Default private final boolean structuresAllowed = true; + + /** + * Support for the binary (undefined) data type is allowed or not. + * + * @since 3.3.0 + */ + @Builder.Default private final boolean binaryAllowed = false; + + /** + * Support for implicit multiplication, like in (a+b)(b+c) are allowed or not. + */ + @Builder.Default private final boolean implicitMultiplicationAllowed = true; + + /** + * Support for single quote string literals, like in 'Hello World' are allowed or not. + */ + @Builder.Default private final boolean singleQuoteStringLiteralsAllowed = false; + + /** + * Allow for expressions to evaluate without errors when variables are not defined. + * + * @since 3.6.0 + */ + @Builder.Default private final boolean lenientMode = false; + + /** + * The power of operator precedence, can be set higher {@link + * OperatorIfc#OPERATOR_PRECEDENCE_POWER_HIGHER} or to a custom value. + */ + @Builder.Default private final int powerOfPrecedence = OperatorIfc.OPERATOR_PRECEDENCE_POWER; + + /** + * If specified, only the final result of the evaluation will be rounded to the specified number + * of decimal digits, using the MathContexts rounding mode. + * + *

The default value of _DECIMAL_PLACES_ROUNDING_UNLIMITED_ will disable rounding. + */ + @Builder.Default private final int decimalPlacesResult = DECIMAL_PLACES_ROUNDING_UNLIMITED; + + /** + * If specified, all results from operations and functions will be rounded to the specified number + * of decimal digits, using the MathContexts rounding mode. + * + *

Automatic rounding is disabled by default. When enabled, EvalEx will round all input + * variables, constants, intermediate operation and function results and the final result to the + * specified number of decimal digits, using the current rounding mode. Using a value of + * _DECIMAL_PLACES_ROUNDING_UNLIMITED_ will disable automatic rounding. + */ + @Builder.Default private final int decimalPlacesRounding = DECIMAL_PLACES_ROUNDING_UNLIMITED; + + /** + * If set to true (default), then the trailing decimal zeros in a number result will be stripped. + */ + @Builder.Default private final boolean stripTrailingZeros = true; + + /** + * If set to true (default), then variables can be set that have the name of a constant. In that + * case, the constant value will be removed and a variable value will be set. + */ + @Builder.Default private final boolean allowOverwriteConstants = true; + + /** + * The time zone id. By default, the system default zone ID is used. + */ + @Builder.Default private final ZoneId zoneId = ZoneId.systemDefault(); + + /** + * The locale. By default, the system default locale is used. + */ + @Builder.Default private final Locale locale = Locale.getDefault(); + + /** + * The maximum recursion depth allowed for nested expressions. + */ + @Builder.Default private final int maxRecursionDepth = DEFAULT_MAX_RECURSION_DEPTH; + + /** + * The date-time formatters. When parsing, each format will be tried and the first matching will + * be used. For formatting, only the first will be used. + * + *

By default, the {@link ExpressionConfiguration#DEFAULT_DATE_TIME_FORMATTERS} are used. + */ + @Builder.Default + private final List dateTimeFormatters = DEFAULT_DATE_TIME_FORMATTERS; + + /** + * The converter to use when converting different data types to an {@link EvaluationValue}. + */ + @Builder.Default + private final EvaluationValueConverterIfc evaluationValueConverter = + new DefaultEvaluationValueConverter(); + + /** + * Convenience method to create a default configuration. + * + * @return A configuration with default settings. + */ + public static ExpressionConfiguration defaultConfiguration() { + return ExpressionConfiguration.builder().build(); + } + + private static Map getStandardConstants() { + + Map constants = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + constants.put("TRUE", EvaluationValue.TRUE); + constants.put("FALSE", EvaluationValue.FALSE); + constants.put( + "PI", + EvaluationValue.numberValue( + new BigDecimal( + "3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679"))); + constants.put( + "E", + EvaluationValue.numberValue( + new BigDecimal( + "2.71828182845904523536028747135266249775724709369995957496696762772407663"))); + constants.put("NULL", EvaluationValue.NULL_VALUE); + + constants.put( + "DT_FORMAT_ISO_DATE_TIME", + EvaluationValue.stringValue("yyyy-MM-dd'T'HH:mm:ss[.SSS][XXX]['['VV']']")); + constants.put( + "DT_FORMAT_LOCAL_DATE_TIME", EvaluationValue.stringValue("yyyy-MM-dd'T'HH:mm:ss[.SSS]")); + constants.put("DT_FORMAT_LOCAL_DATE", EvaluationValue.stringValue("yyyy-MM-dd")); + + return constants; + } + + /** + * Adds additional operators to this configuration. + * + * @param operators variable number of arguments with a map entry holding the operator name and + * implementation.
+ * Example: + * ExpressionConfiguration.defaultConfiguration() .withAdditionalOperators( + * Pair.of("++", new PrefixPlusPlusOperator()), Pair.of("++", new + * PostfixPlusPlusOperator())); + * + * @return The modified configuration, to allow chaining of methods. + */ + @SafeVarargs + public final ExpressionConfiguration withAdditionalOperators( + Map.Entry... operators) { + Arrays.stream(operators) + .forEach(entry -> operatorDictionary.addOperator(entry.getKey(), entry.getValue())); + return this; + } + + /** + * Adds additional functions to this configuration. + * + * @param functions variable number of arguments with a map entry holding the functions name and + * implementation.
+ * Example: + * ExpressionConfiguration.defaultConfiguration() .withAdditionalFunctions( + * Pair.of("save", new SaveFunction()), Pair.of("update", new + * UpdateFunction())); + * + * @return The modified configuration, to allow chaining of methods. + */ + @SafeVarargs + public final ExpressionConfiguration withAdditionalFunctions( + Map.Entry... functions) { + Arrays.stream(functions) + .forEach(entry -> functionDictionary.addFunction(entry.getKey(), entry.getValue())); + return this; + } +} diff --git a/src/main/java/com/ezylang/evalex/config/FunctionDictionaryIfc.java b/src/main/java/com/ezylang/evalex/config/FunctionDictionaryIfc.java new file mode 100644 index 000000000..9ce11bb1c --- /dev/null +++ b/src/main/java/com/ezylang/evalex/config/FunctionDictionaryIfc.java @@ -0,0 +1,76 @@ +/* + 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.config; + +import com.ezylang.evalex.functions.FunctionIfc; + +import java.util.Set; + +/** + * A function dictionary holds all the functions, that can be used in an expression.
+ * The default implementation is the {@link MapBasedFunctionDictionary}. + */ +public interface FunctionDictionaryIfc { + + /** + * Allows to add a function to the dictionary. Implementation is optional, if you have a fixed set + * of functions, this method can throw an exception. + * + * @param functionName The function name. + * @param function The function implementation. + */ + void addFunction(String functionName, FunctionIfc function); + + /** + * Check if the dictionary has a function with that name. + * + * @param functionName The function name to look for. + * @return true if a function was found or false if not. + */ + default boolean hasFunction(String functionName) { + return getFunction(functionName) != null; + } + + /** + * Get the function definition for a function name. + * + * @param functionName The name of the function. + * @return The function definition or null if no function was found. + */ + FunctionIfc getFunction(String functionName); + + /** + * Get all function names in current configuration. + * + * @return A set of all defined function names. + * @throws UnsupportedOperationException when this operation is not supported by the + * implementation. + */ + default Set getAvailableFunctionNames() { + throw new UnsupportedOperationException("Operation not supported"); + } + + /** + * Get all functions in current configuration. + * + * @return A set of all defined functions. + * @throws UnsupportedOperationException when this operation is not supported by the + * implementation. + */ + default Set getAvailableFunctions() { + throw new UnsupportedOperationException("Operation not supported"); + } +} diff --git a/src/main/java/com/ezylang/evalex/config/MapBasedFunctionDictionary.java b/src/main/java/com/ezylang/evalex/config/MapBasedFunctionDictionary.java new file mode 100644 index 000000000..26278a514 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/config/MapBasedFunctionDictionary.java @@ -0,0 +1,69 @@ +/* + 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.config; + +import com.ezylang.evalex.functions.FunctionIfc; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import static java.util.Arrays.stream; + +/** + * A default case-insensitive implementation of the function dictionary that uses a local + * Map.Entry<String, FunctionIfc> for storage. + */ +public class MapBasedFunctionDictionary implements FunctionDictionaryIfc { + + private final Map functions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + /** + * Creates a new function dictionary with the specified list of functions. + * + * @param functions variable number of arguments that specify the function names and definitions + * that will initially be added. + * @return A newly created function dictionary with the specified functions. + */ + @SuppressWarnings({"unchecked", "varargs"}) + public static FunctionDictionaryIfc ofFunctions(Map.Entry... functions) { + FunctionDictionaryIfc dictionary = new MapBasedFunctionDictionary(); + stream(functions).forEach(entry -> dictionary.addFunction(entry.getKey(), entry.getValue())); + return dictionary; + } + + @Override + public FunctionIfc getFunction(String functionName) { + return functions.get(functionName); + } + + @Override + public Set getAvailableFunctionNames() { + return Collections.unmodifiableSet(functions.keySet()); + } + + @Override + public Set getAvailableFunctions() { + return new ObjectOpenHashSet<>(functions.values()); + } + + @Override + public void addFunction(String functionName, FunctionIfc function) { + functions.put(functionName, function); + } +} diff --git a/src/main/java/com/ezylang/evalex/config/MapBasedOperatorDictionary.java b/src/main/java/com/ezylang/evalex/config/MapBasedOperatorDictionary.java new file mode 100644 index 000000000..0a3b863a1 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/config/MapBasedOperatorDictionary.java @@ -0,0 +1,109 @@ +/* + 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.config; + +import com.ezylang.evalex.operators.OperatorIfc; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import static java.util.Arrays.stream; + +/** + * A default case-insensitive implementation of the operator dictionary that uses a local + * Map.Entry<String,OperatorIfc> for storage. + */ +public class MapBasedOperatorDictionary implements OperatorDictionaryIfc { + + final Map prefixOperators = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + final Map postfixOperators = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + final Map infixOperators = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + /** + * Creates a new operator dictionary with the specified list of operators. + * + * @param operators variable number of arguments that specify the operator names and definitions + * that will initially be added. + * @return A newly created operator dictionary with the specified operators. + */ + @SuppressWarnings({"unchecked", "varargs"}) + public static OperatorDictionaryIfc ofOperators(Map.Entry... operators) { + OperatorDictionaryIfc dictionary = new MapBasedOperatorDictionary(); + stream(operators).forEach(entry -> dictionary.addOperator(entry.getKey(), entry.getValue())); + return dictionary; + } + + @Override + public void addOperator(String operatorString, OperatorIfc operator) { + if (operator.isPrefix()) { + prefixOperators.put(operatorString, operator); + } else if (operator.isPostfix()) { + postfixOperators.put(operatorString, operator); + } else { + infixOperators.put(operatorString, operator); + } + } + + @Override + public OperatorIfc getPrefixOperator(String operatorString) { + return prefixOperators.get(operatorString); + } + + @Override + public OperatorIfc getPostfixOperator(String operatorString) { + return postfixOperators.get(operatorString); + } + + @Override + public OperatorIfc getInfixOperator(String operatorString) { + return infixOperators.get(operatorString); + } + + @Override + public Set getAvailablePrefixOperatorNames() { + return Collections.unmodifiableSet(prefixOperators.keySet()); + } + + @Override + public Set getAvailablePostfixOperatorNames() { + return Collections.unmodifiableSet(postfixOperators.keySet()); + } + + @Override + public Set getAvailableInfixOperatorNames() { + return Collections.unmodifiableSet(infixOperators.keySet()); + } + + @Override + public Set getAvailablePrefixOperators() { + return new ObjectOpenHashSet<>(prefixOperators.values()); + } + + @Override + public Set getAvailablePostfixOperators() { + return new ObjectOpenHashSet<>(postfixOperators.values()); + } + + @Override + public Set getAvailableInfixOperators() { + return new ObjectOpenHashSet<>(infixOperators.values()); + } +} diff --git a/src/main/java/com/ezylang/evalex/config/OperatorDictionaryIfc.java b/src/main/java/com/ezylang/evalex/config/OperatorDictionaryIfc.java new file mode 100644 index 000000000..9fde252d4 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/config/OperatorDictionaryIfc.java @@ -0,0 +1,156 @@ +/* + 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.config; + +import com.ezylang.evalex.operators.OperatorIfc; + +import java.util.Set; + +/** + * An operator dictionary holds all the operators, that can be used in an expression.
+ * The default implementation is the {@link MapBasedOperatorDictionary}. + */ +public interface OperatorDictionaryIfc { + + /** + * Allows to add an operator to the dictionary. Implementation is optional, if you have a fixed + * set of operators, this method can throw an exception. + * + * @param operatorString The operator name. + * @param operator The operator implementation. + */ + void addOperator(String operatorString, OperatorIfc operator); + + /** + * Check if the dictionary has a prefix operator with that name. + * + * @param operatorString The operator name to look for. + * @return true if an operator was found or false if not. + */ + default boolean hasPrefixOperator(String operatorString) { + return getPrefixOperator(operatorString) != null; + } + + /** + * Check if the dictionary has a postfix operator with that name. + * + * @param operatorString The operator name to look for. + * @return true if an operator was found or false if not. + */ + default boolean hasPostfixOperator(String operatorString) { + return getPostfixOperator(operatorString) != null; + } + + /** + * Check if the dictionary has an infix operator with that name. + * + * @param operatorString The operator name to look for. + * @return true if an operator was found or false if not. + */ + default boolean hasInfixOperator(String operatorString) { + return getInfixOperator(operatorString) != null; + } + + /** + * Get the operator definition for a prefix operator name. + * + * @param operatorString The name of the operator. + * @return The operator definition or null if no operator was found. + */ + OperatorIfc getPrefixOperator(String operatorString); + + /** + * Get the operator definition for a postfix operator name. + * + * @param operatorString The name of the operator. + * @return The operator definition or null if no operator was found. + */ + OperatorIfc getPostfixOperator(String operatorString); + + /** + * Get the operator definition for an infix operator name. + * + * @param operatorString The name of the operator. + * @return The operator definition or null if no operator was found. + */ + OperatorIfc getInfixOperator(String operatorString); + + /** + * Get all prefix operator names in current configuration. + * + * @return A set of all defined prefix operator names. + * @throws UnsupportedOperationException when this operation is not supported by the + * implementation. + */ + default Set getAvailablePrefixOperatorNames() { + throw new UnsupportedOperationException("Operation not supported"); + } + + /** + * Get all postfix operator names in current configuration. + * + * @return A set of all defined postfix operator names. + * @throws UnsupportedOperationException when this operation is not supported by the + * implementation. + */ + default Set getAvailablePostfixOperatorNames() { + throw new UnsupportedOperationException("Operation not supported"); + } + + /** + * Get all infix operator names in current configuration. + * + * @return A set of all defined infix operator names. + * @throws UnsupportedOperationException when this operation is not supported by the + * implementation. + */ + default Set getAvailableInfixOperatorNames() { + throw new UnsupportedOperationException("Operation not supported"); + } + + /** + * Get all prefix operators in current configuration. + * + * @return A set of all defined prefix operators. + * @throws UnsupportedOperationException when this operation is not supported by the + * implementation. + */ + default Set getAvailablePrefixOperators() { + throw new UnsupportedOperationException("Operation not supported"); + } + + /** + * Get all postfix operators in current configuration. + * + * @return A set of all defined postfix operators. + * @throws UnsupportedOperationException when this operation is not supported by the + * implementation. + */ + default Set getAvailablePostfixOperators() { + throw new UnsupportedOperationException("Operation not supported"); + } + + /** + * Get all infix operators in current configuration. + * + * @return A set of all defined infix operators. + * @throws UnsupportedOperationException when this operation is not supported by the + * implementation. + */ + default Set getAvailableInfixOperators() { + throw new UnsupportedOperationException("Operation not supported"); + } +} diff --git a/src/main/java/com/ezylang/evalex/data/DataAccessorIfc.java b/src/main/java/com/ezylang/evalex/data/DataAccessorIfc.java new file mode 100644 index 000000000..434c278f3 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/DataAccessorIfc.java @@ -0,0 +1,40 @@ +/* + 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.data; + +/** + * A data accessor is responsible for accessing data, e.g. variable and constant values during an + * expression evaluation. The default implementation for setting and reading local data is the + * {@link MapBasedDataAccessor}. + */ +public interface DataAccessorIfc { + + /** + * Retrieves a data value. + * + * @param variable The variable name, e.g. a variable or constant name. + * @return The data value, or null if not found. + */ + EvaluationValue getData(String variable); + + /** + * Sets a data value. + * + * @param variable The variable name, e.g. a variable or constant name. + * @param value The value to set. + */ + void setData(String variable, EvaluationValue value); +} diff --git a/src/main/java/com/ezylang/evalex/data/EvaluationValue.java b/src/main/java/com/ezylang/evalex/data/EvaluationValue.java new file mode 100644 index 000000000..594676e86 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/EvaluationValue.java @@ -0,0 +1,576 @@ +/* + 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.data; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.parser.ASTNode; +import lombok.Value; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.MathContext; +import java.time.DateTimeException; +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * The representation of the final or intermediate evaluation result value. The representation + * consists of a data type and data value. Depending on the type, the value will be stored in a + * corresponding object type. + */ +@Value +public class EvaluationValue implements Comparable { + + /** + * A pre-built, immutable, null value. + */ + public static final EvaluationValue NULL_VALUE = new EvaluationValue(null, DataType.NULL); + + /** + * A pre-built, immutable value for an undefined variable, when the lenient mode is enabled. + * + * @since 3.6.0 + */ + public static final EvaluationValue UNDEFINED = new EvaluationValue(null, DataType.UNDEFINED); + + /** + * A pre-built, immutable, false boolean value. + */ + public static final EvaluationValue FALSE = new EvaluationValue(false, DataType.BOOLEAN); + + /** + * A pre-built, immutable, true boolean value. + */ + public static final EvaluationValue TRUE = new EvaluationValue(true, DataType.BOOLEAN); + + /** + * 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}. + */ + STRING, + /** + * Any number, stored as {@link BigDecimal}. + */ + 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, + /** + * A structure with pairs of name/value members. Name is a string and the value is a {@link + * EvaluationValue}. Stored as a {@link java.util.Map}. + */ + STRUCTURE, + /** + * Used for lazy parameter evaluation, stored as an {@link ASTNode}, which can be evaluated on + * demand. + */ + EXPRESSION_NODE, + /** + * A null value + */ + NULL, + /** + * Raw (undefined) type, stored as an {@link Object}. + */ + BINARY, + /** + * Applicable for undeclared variables when lenient mode is enabled. + * + * @since 3.6.0 + */ + UNDEFINED + } + + Object value; + + DataType dataType; + + /** + * Creates a new evaluation value by using the configured converter and configuration. + * + * @param value One of the supported data types. + * @param configuration The expression configuration to use. + * @throws IllegalArgumentException if the data type can't be mapped. + * @see ExpressionConfiguration#getEvaluationValueConverter() + * @deprecated Use {@link EvaluationValue#of(Object, ExpressionConfiguration)} instead. + */ + @Deprecated + public EvaluationValue(Object value, ExpressionConfiguration configuration) { + + EvaluationValue converted = + configuration.getEvaluationValueConverter().convertObject(value, configuration); + + this.value = converted.getValue(); + this.dataType = converted.getDataType(); + } + + /** + * Private constructor to directly create an instance with a given type and value. + * + * @param value The value to set, no conversion will be done. + * @param dataType The data type to set. + */ + private EvaluationValue(Object value, DataType dataType) { + this.dataType = dataType; + this.value = value; + } + + /** + * Creates a new evaluation value by using the configured converter and configuration. + * + * @param value One of the supported data types. + * @param configuration The expression configuration to use; not null + * @throws IllegalArgumentException if the data type can't be mapped. + * @see ExpressionConfiguration#getEvaluationValueConverter() + */ + public static EvaluationValue of(Object value, ExpressionConfiguration configuration) { + return configuration.getEvaluationValueConverter().convertObject(value, configuration); + } + + /** + * Returns an immutable null value. + * + * @return A null value. + * @deprecated Use {@link EvaluationValue#NULL_VALUE} instead + */ + @Deprecated + public static EvaluationValue nullValue() { + return NULL_VALUE; + } + + /** + * Creates a new number value. + * + * @param value The BigDecimal value to use. + * @return the new number value. + */ + public static EvaluationValue numberValue(BigDecimal value) { + return new EvaluationValue(value, DataType.NUMBER); + } + + /** + * Creates a new string value. + * + * @param value The String value to use. + * @return the new string value. + */ + public static EvaluationValue stringValue(String value) { + return new EvaluationValue(value, DataType.STRING); + } + + /** + * Creates a new boolean value. + * + * @param value The Boolean value to use. + * @return the new boolean value. + */ + public static EvaluationValue booleanValue(Boolean value) { + return value != null && value ? TRUE : FALSE; + } + + /** + * Creates a new date-time value. + * + * @param value The Instant value to use. + * @return the new date-time value. + */ + public static EvaluationValue dateTimeValue(Instant value) { + return new EvaluationValue(value, DataType.DATE_TIME); + } + + /** + * Creates a new duration value. + * + * @param value The Duration value to use. + * @return the new duration value. + */ + public static EvaluationValue durationValue(Duration value) { + return new EvaluationValue(value, DataType.DURATION); + } + + /** + * Creates a new expression node value. + * + * @param value The ASTNode value to use. + * @return the new expression node value. + */ + public static EvaluationValue expressionNodeValue(ASTNode value) { + return new EvaluationValue(value, DataType.EXPRESSION_NODE); + } + + /** + * Creates a new array value. + * + * @param value The List value to use. + * @return the new array value. + */ + public static EvaluationValue arrayValue(List value) { + return new EvaluationValue(value, DataType.ARRAY); + } + + /** + * Creates a new structure value. + * + * @param value The Map value to use. + * @return the new structure value. + */ + public static EvaluationValue structureValue(Map value) { + return new EvaluationValue(value, DataType.STRUCTURE); + } + + /** + * Creates a new binary (raw) value. + * + * @param value The Object to use. + * @return the new binary value. + * @since 3.3.0 + */ + public static EvaluationValue binaryValue(Object value) { + return new EvaluationValue(value, DataType.BINARY); + } + + /** + * Checks if the value is of type {@link DataType#NUMBER}. + * + * @return true or false. + */ + public boolean isNumberValue() { + return getDataType() == DataType.NUMBER; + } + + /** + * Checks if the value is of type {@link DataType#STRING}. + * + * @return true or false. + */ + public boolean isStringValue() { + return getDataType() == DataType.STRING; + } + + /** + * Checks if the value is of type {@link DataType#BOOLEAN}. + * + * @return true or false. + */ + public boolean isBooleanValue() { + 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}. + * + * @return true or false. + */ + public boolean isArrayValue() { + return getDataType() == DataType.ARRAY; + } + + /** + * Checks if the value is of type {@link DataType#STRUCTURE}. + * + * @return true or false. + */ + public boolean isStructureValue() { + return getDataType() == DataType.STRUCTURE; + } + + /** + * Checks if the value is of type {@link DataType#EXPRESSION_NODE}. + * + * @return true or false. + */ + public boolean isExpressionNode() { + return getDataType() == DataType.EXPRESSION_NODE; + } + + public boolean isNullValue() { + return getDataType() == DataType.NULL; + } + + /** + * Checks if the value is of type {@link DataType#BINARY}. + * + * @return true or false. + * @since 3.3.0 + */ + public boolean isBinaryValue() { + return getDataType() == DataType.BINARY; + } + + /** + * Creates a {@link DataType#NUMBER} value from a {@link String}. + * + * @param value The {@link String} value. + * @param mathContext The math context to use for creation of the {@link BigDecimal} storage. + */ + public static EvaluationValue numberOfString(String value, MathContext mathContext) { + if (value.startsWith("0x") || value.startsWith("0X")) { + BigInteger hexToInteger = new BigInteger(value.substring(2), 16); + return EvaluationValue.numberValue(new BigDecimal(hexToInteger, mathContext)); + } else { + return EvaluationValue.numberValue(new BigDecimal(value, mathContext)); + } + } + + /** + * Gets a {@link BigDecimal} representation of the value. If possible and needed, a conversion + * will be made. + * + *

    + *
  • Boolean true will return a {@link BigDecimal#ONE}, else {@link + * BigDecimal#ZERO}. + *
+ * + * @return The {@link BigDecimal} representation of the value, or {@link BigDecimal#ZERO} if + * conversion is not possible. + */ + public BigDecimal getNumberValue() { + switch (getDataType()) { + case NUMBER: + return (BigDecimal) value; + case BOOLEAN: + 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; + } + } + + /** + * Gets a {@link String} representation of the value. If possible and needed, a conversion will be + * made. + * + *
    + *
  • Number values will be returned as {@link BigDecimal#toPlainString()}. + *
  • The {@link Object#toString()} will be used in all other cases. + *
+ * + * @return The {@link String} representation of the value. + */ + public String getStringValue() { + switch (getDataType()) { + case NUMBER: + return ((BigDecimal) value).toPlainString(); + case NULL: + return null; + default: + return value.toString(); + } + } + + /** + * Gets a {@link Boolean} representation of the value. If possible and needed, a conversion will + * be made. + * + *
    + *
  • Any non-zero number value will return true. + *
  • Any string with the value "true" (case ignored) will return true. + *
+ * + * @return The {@link Boolean} representation of the value. + */ + public Boolean getBooleanValue() { + switch (getDataType()) { + case NUMBER: + return getNumberValue().compareTo(BigDecimal.ZERO) != 0; + case BOOLEAN: + return (Boolean) value; + case STRING: + return Boolean.parseBoolean((String) value); + case NULL: + return NULL_BOOLEAN; + default: + return Boolean.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. + * + * @return The {@link List} representation of the value or an empty list, if no + * conversion is possible. + */ + @SuppressWarnings("unchecked") + public List getArrayValue() { + if (isArrayValue()) { + return (List) value; + } else if (isNullValue()) { + return NULL_ARRAY; + } else { + return Collections.emptyList(); + } + } + + /** + * Gets a {@link Map} representation of the value. + * + * @return The {@link Map} representation of the value or an empty list, if no conversion is + * possible. + */ + @SuppressWarnings("unchecked") + public Map getStructureValue() { + if (isStructureValue()) { + return (Map) value; + } else if (isNullValue()) { + return NULL_STRUCTURE; + } else { + return Collections.emptyMap(); + } + } + + /** + * Gets the expression node, if this value is of type {@link DataType#EXPRESSION_NODE}. + * + * @return The expression node, or null for any other data type. + */ + public ASTNode getExpressionNode() { + return isExpressionNode() ? ((ASTNode) getValue()) : null; + } + + @Override + public int compareTo(EvaluationValue toCompare) { + switch (getDataType()) { + case NUMBER: + 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/data/MapBasedDataAccessor.java b/src/main/java/com/ezylang/evalex/data/MapBasedDataAccessor.java new file mode 100644 index 000000000..b38a36052 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/MapBasedDataAccessor.java @@ -0,0 +1,39 @@ +/* + 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.data; + +import java.util.Map; +import java.util.TreeMap; + +/** + * A default case-insensitive implementation of the data accessor that uses a local + * Map.Entry<String, EvaluationValue> for storage. + */ +public class MapBasedDataAccessor implements DataAccessorIfc { + + private final Map variables = + new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + @Override + public EvaluationValue getData(String variable) { + return variables.get(variable); + } + + @Override + public void setData(String variable, EvaluationValue value) { + variables.put(variable, value); + } +} diff --git a/src/main/java/com/ezylang/evalex/data/conversion/ArrayConverter.java b/src/main/java/com/ezylang/evalex/data/conversion/ArrayConverter.java new file mode 100644 index 000000000..58a315762 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/conversion/ArrayConverter.java @@ -0,0 +1,159 @@ +/* + 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.conversion; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Converter to convert to the ARRAY data type. + */ +public class ArrayConverter implements ConverterIfc { + + @Override + public EvaluationValue convert(Object object, ExpressionConfiguration configuration) { + List list; + + if (object.getClass().isArray()) { + list = convertArray(object, configuration); + } else if (object instanceof List) { + list = convertList((List) object, configuration); + } else { + throw illegalArgument(object); + } + + return EvaluationValue.arrayValue(list); + } + + @Override + public boolean canConvert(Object object) { + return object instanceof List || object.getClass().isArray(); + } + + private static List convertList( + List object, ExpressionConfiguration configuration) { + return object.stream() + .map(element -> EvaluationValue.of(element, configuration)) + .collect(Collectors.toList()); + } + + private List convertArray(Object array, ExpressionConfiguration configuration) { + if (array instanceof int[]) { + return convertIntArray((int[]) array, configuration); + } else if (array instanceof long[]) { + return convertLongArray((long[]) array, configuration); + } else if (array instanceof double[]) { + return convertDoubleArray((double[]) array, configuration); + } else if (array instanceof float[]) { + return convertFloatArray((float[]) array, configuration); + } else if (array instanceof short[]) { + return convertShortArray((short[]) array, configuration); + } else if (array instanceof char[]) { + return convertCharArray((char[]) array, configuration); + } else if (array instanceof byte[]) { + return convertByteArray((byte[]) array, configuration); + } else if (array instanceof boolean[]) { + return convertBooleanArray((boolean[]) array, configuration); + } else { + return convertObjectArray((Object[]) array, configuration); + } + } + + private List convertIntArray( + int[] array, ExpressionConfiguration configuration) { + List list = new ArrayList<>(); + for (int i : array) { + list.add(EvaluationValue.of(i, configuration)); + } + return list; + } + + private List convertLongArray( + long[] array, ExpressionConfiguration configuration) { + List list = new ArrayList<>(); + for (long l : array) { + list.add(EvaluationValue.of(l, configuration)); + } + return list; + } + + private List convertDoubleArray( + double[] array, ExpressionConfiguration configuration) { + List list = new ArrayList<>(); + for (double d : array) { + list.add(EvaluationValue.of(d, configuration)); + } + return list; + } + + private List convertFloatArray( + float[] array, ExpressionConfiguration configuration) { + List list = new ArrayList<>(); + for (float f : array) { + list.add(EvaluationValue.of(f, configuration)); + } + return list; + } + + private List convertShortArray( + short[] array, ExpressionConfiguration configuration) { + List list = new ArrayList<>(); + for (short s : array) { + list.add(EvaluationValue.of(s, configuration)); + } + return list; + } + + private List convertCharArray( + char[] array, ExpressionConfiguration configuration) { + List list = new ArrayList<>(); + for (char c : array) { + list.add(EvaluationValue.of(c, configuration)); + } + return list; + } + + private List convertByteArray( + byte[] array, ExpressionConfiguration configuration) { + List list = new ArrayList<>(); + for (byte b : array) { + list.add(EvaluationValue.of(b, configuration)); + } + return list; + } + + private List convertBooleanArray( + boolean[] array, ExpressionConfiguration configuration) { + List list = new ArrayList<>(); + for (boolean b : array) { + list.add(EvaluationValue.of(b, configuration)); + } + return list; + } + + private List convertObjectArray( + Object[] array, ExpressionConfiguration configuration) { + List list = new ArrayList<>(); + for (Object o : array) { + list.add(EvaluationValue.of(o, configuration)); + } + return list; + } +} diff --git a/src/main/java/com/ezylang/evalex/data/conversion/BinaryConverter.java b/src/main/java/com/ezylang/evalex/data/conversion/BinaryConverter.java new file mode 100644 index 000000000..8af35f4c2 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/conversion/BinaryConverter.java @@ -0,0 +1,38 @@ +/* + Copyright 2012-2024 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.conversion; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; + +/** + * Converter to convert to the BINARY data type. + * + * @author oswaldobapvicjr + * @since 3.3.0 + */ +public class BinaryConverter implements ConverterIfc { + + @Override + public EvaluationValue convert(Object object, ExpressionConfiguration configuration) { + return EvaluationValue.binaryValue(object); + } + + @Override + public boolean canConvert(Object object) { + return true; + } +} diff --git a/src/main/java/com/ezylang/evalex/data/conversion/BooleanConverter.java b/src/main/java/com/ezylang/evalex/data/conversion/BooleanConverter.java new file mode 100644 index 000000000..ebfb3b2e4 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/conversion/BooleanConverter.java @@ -0,0 +1,35 @@ +/* + 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.conversion; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; + +/** + * Converter to convert to the BOOLEAN data type. + */ +public class BooleanConverter implements ConverterIfc { + + @Override + public EvaluationValue convert(Object object, ExpressionConfiguration configuration) { + return EvaluationValue.booleanValue((Boolean) object); + } + + @Override + public boolean canConvert(Object object) { + return object instanceof Boolean; + } +} diff --git a/src/main/java/com/ezylang/evalex/data/conversion/ConverterIfc.java b/src/main/java/com/ezylang/evalex/data/conversion/ConverterIfc.java new file mode 100644 index 000000000..ee1b9f44c --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/conversion/ConverterIfc.java @@ -0,0 +1,47 @@ +/* + 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.conversion; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; + +/** + * Converter interface used by the {@link DefaultEvaluationValueConverter}. + */ +public interface ConverterIfc { + + /** + * Called to convert a previously checked data type. + * + * @param object The object to convert. + * @param configuration The current expression configuration. + * @return The converted value. + */ + EvaluationValue convert(Object object, ExpressionConfiguration configuration); + + /** + * Checks, if a given object can be converted by this converter. + * + * @param object The object to convert. + * @return true if the object can be converted, false otherwise. + */ + boolean canConvert(Object object); + + default IllegalArgumentException illegalArgument(Object object) { + return new IllegalArgumentException( + "Unsupported data type '" + object.getClass().getName() + "'"); + } +} diff --git a/src/main/java/com/ezylang/evalex/data/conversion/DateTimeConverter.java b/src/main/java/com/ezylang/evalex/data/conversion/DateTimeConverter.java new file mode 100644 index 000000000..2474273d9 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/conversion/DateTimeConverter.java @@ -0,0 +1,111 @@ +/* + 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.conversion; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; + +import java.time.DateTimeException; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalQueries; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +/** + * Converter to convert to the DATE_TIME data type. + */ +public class DateTimeConverter implements ConverterIfc { + + @Override + public EvaluationValue convert(Object object, ExpressionConfiguration configuration) { + + Instant instant; + + if (object instanceof Instant) { + instant = (Instant) object; + } else if (object instanceof ZonedDateTime) { + instant = ((ZonedDateTime) object).toInstant(); + } else if (object instanceof OffsetDateTime) { + instant = ((OffsetDateTime) object).toInstant(); + } else if (object instanceof LocalDate) { + instant = ((LocalDate) object).atStartOfDay().atZone(configuration.getZoneId()).toInstant(); + } else if (object instanceof LocalDateTime) { + instant = ((LocalDateTime) object).atZone(configuration.getZoneId()).toInstant(); + } else if (object instanceof Date) { + instant = ((Date) object).toInstant(); + } else if (object instanceof Calendar) { + instant = ((Calendar) object).toInstant(); + } else { + throw illegalArgument(object); + } + return EvaluationValue.dateTimeValue(instant); + } + + /** + * Tries to parse a date-time string by trying out each format in the list. The first matching + * result is returned. If none of the formats can be used to parse the string, null + * is returned. + * + * @param value The string to parse. + * @param zoneId The {@link ZoneId} to use for parsing. + * @param formatters The list of formatters. + * @return A parsed {@link Instant} if parsing was successful, else null. + */ + public Instant parseDateTime(String value, ZoneId zoneId, List formatters) { + for (DateTimeFormatter formatter : formatters) { + try { + return parseToInstant(value, zoneId, formatter); + } catch (DateTimeException ignored) { + // ignore + } + } + return null; + } + + private Instant parseToInstant(String value, ZoneId zoneId, DateTimeFormatter formatter) { + TemporalAccessor ta = formatter.parse(value); + ZoneId parsedZoneId = ta.query(TemporalQueries.zone()); + if (parsedZoneId == null) { + LocalDate parsedDate = ta.query(TemporalQueries.localDate()); + LocalTime parsedTime = ta.query(TemporalQueries.localTime()); + if (parsedTime == null) { + parsedTime = parsedDate.atStartOfDay().toLocalTime(); + } + ta = ZonedDateTime.of(parsedDate, parsedTime, zoneId); + } + return Instant.from(ta); + } + + @Override + public boolean canConvert(Object object) { + return (object instanceof Instant + || object instanceof ZonedDateTime + || object instanceof OffsetDateTime + || object instanceof LocalDate + || object instanceof LocalDateTime + || object instanceof Date + || object instanceof Calendar); + } +} diff --git a/src/main/java/com/ezylang/evalex/data/conversion/DefaultEvaluationValueConverter.java b/src/main/java/com/ezylang/evalex/data/conversion/DefaultEvaluationValueConverter.java new file mode 100644 index 000000000..69381d3b6 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/conversion/DefaultEvaluationValueConverter.java @@ -0,0 +1,94 @@ +/* + 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.conversion; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; + +import java.util.Arrays; +import java.util.List; + +/** + * The default implementation of the {@link EvaluationValueConverterIfc}, used in the standard + * configuration. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Input typeConverter used
BigDecimalNumberConverter
Long, longNumberConverter
Integer, intNumberConverter
Short, shortNumberConverter
Byte, byteNumberConverter
Double, doubleNumberConverter *
Float, floatNumberConverter *
CharSequence , StringStringConverter
Boolean, booleanBooleanConverter
InstantDateTimeConverter
DateDateTimeConverter
CalendarDateTimeConverter
ZonedDateTimeDateTimeConverter
LocalDateDateTimeConverter - the configured zone ID will be used for conversion
LocalDateTimeDateTimeConverter - the configured zone ID will be used for conversion
OffsetDateTimeDateTimeConverter
DurationDurationConverter
ASTNodeASTNode
List<?>ArrayConverter - each entry will be converted
Map<?,?>StructureConverter - each entry will be converted
+ * + * * 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 + */ +public class DefaultEvaluationValueConverter implements EvaluationValueConverterIfc { + + static final List converters = + Arrays.asList( + new NumberConverter(), + new StringConverter(), + new BooleanConverter(), + new DateTimeConverter(), + new DurationConverter(), + new ExpressionNodeConverter(), + new ArrayConverter(), + new StructureConverter()); + + @Override + public EvaluationValue convertObject(Object object, ExpressionConfiguration configuration) { + + if (object == null) { + return EvaluationValue.NULL_VALUE; + } + + if (object instanceof EvaluationValue) { + return (EvaluationValue) object; + } + + for (ConverterIfc converter : converters) { + if (converter.canConvert(object)) { + return converter.convert(object, configuration); + } + } + + if (configuration.isBinaryAllowed()) { + return EvaluationValue.binaryValue(object); + } + + throw new IllegalArgumentException( + "Unsupported data type '" + object.getClass().getName() + "'"); + } +} diff --git a/src/main/java/com/ezylang/evalex/data/conversion/DurationConverter.java b/src/main/java/com/ezylang/evalex/data/conversion/DurationConverter.java new file mode 100644 index 000000000..f2594947c --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/conversion/DurationConverter.java @@ -0,0 +1,37 @@ +/* + 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.conversion; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; + +import java.time.Duration; + +/** + * Converter to convert to the DURATION data type. + */ +public class DurationConverter implements ConverterIfc { + + @Override + public EvaluationValue convert(Object object, ExpressionConfiguration configuration) { + return EvaluationValue.durationValue((Duration) object); + } + + @Override + public boolean canConvert(Object object) { + return object instanceof Duration; + } +} diff --git a/src/main/java/com/ezylang/evalex/data/conversion/EvaluationValueConverterIfc.java b/src/main/java/com/ezylang/evalex/data/conversion/EvaluationValueConverterIfc.java new file mode 100644 index 000000000..31daf2f16 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/conversion/EvaluationValueConverterIfc.java @@ -0,0 +1,36 @@ +/* + 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.conversion; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; + +/** + * Converter interface to be implemented by configurable evaluation value converters. Converts an + * arbitrary object to an {@link EvaluationValue}, using the specified configuration. + */ +public interface EvaluationValueConverterIfc { + + /** + * Called whenever an object has to be converted to an {@link EvaluationValue}. + * + * @param object The object holding the value. + * @param configuration The configuration to use. + * @return The converted {@link EvaluationValue}. + * @throws IllegalArgumentException if the object can't be converted. + */ + EvaluationValue convertObject(Object object, ExpressionConfiguration configuration); +} diff --git a/src/main/java/com/ezylang/evalex/data/conversion/ExpressionNodeConverter.java b/src/main/java/com/ezylang/evalex/data/conversion/ExpressionNodeConverter.java new file mode 100644 index 000000000..4ebbfb265 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/conversion/ExpressionNodeConverter.java @@ -0,0 +1,36 @@ +/* + 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.conversion; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.parser.ASTNode; + +/** + * Converter to convert to the EXPRESSION_NODE data type. + */ +public class ExpressionNodeConverter implements ConverterIfc { + + @Override + public EvaluationValue convert(Object object, ExpressionConfiguration configuration) { + return EvaluationValue.expressionNodeValue((ASTNode) object); + } + + @Override + public boolean canConvert(Object object) { + return object instanceof ASTNode; + } +} diff --git a/src/main/java/com/ezylang/evalex/data/conversion/NumberConverter.java b/src/main/java/com/ezylang/evalex/data/conversion/NumberConverter.java new file mode 100644 index 000000000..4cc77736e --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/conversion/NumberConverter.java @@ -0,0 +1,67 @@ +/* + 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.conversion; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * Converter to convert to the NUMBER data type. + */ +public class NumberConverter implements ConverterIfc { + + @Override + public EvaluationValue convert(Object object, ExpressionConfiguration configuration) { + BigDecimal bigDecimal; + + if (object instanceof BigDecimal) { + bigDecimal = (BigDecimal) object; + } else if (object instanceof BigInteger) { + bigDecimal = new BigDecimal((BigInteger) object, configuration.getMathContext()); + } else if (object instanceof Double) { + bigDecimal = new BigDecimal(Double.toString((double) object), configuration.getMathContext()); + } else if (object instanceof Float) { + bigDecimal = BigDecimal.valueOf((float) object); + } else if (object instanceof Integer) { + bigDecimal = BigDecimal.valueOf((int) object); + } else if (object instanceof Long) { + bigDecimal = BigDecimal.valueOf((long) object); + } else if (object instanceof Short) { + bigDecimal = BigDecimal.valueOf((short) object); + } else if (object instanceof Byte) { + bigDecimal = BigDecimal.valueOf((byte) object); + } else { + throw illegalArgument(object); + } + + return EvaluationValue.numberValue(bigDecimal); + } + + @Override + public boolean canConvert(Object object) { + return (object instanceof BigDecimal + || object instanceof BigInteger + || object instanceof Double + || object instanceof Float + || object instanceof Integer + || object instanceof Long + || object instanceof Short + || object instanceof Byte); + } +} diff --git a/src/main/java/com/ezylang/evalex/data/conversion/StringConverter.java b/src/main/java/com/ezylang/evalex/data/conversion/StringConverter.java new file mode 100644 index 000000000..d90d9c4f5 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/conversion/StringConverter.java @@ -0,0 +1,45 @@ +/* + 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.conversion; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; + +/** + * Converter to convert to the STRING data type. + */ +public class StringConverter implements ConverterIfc { + + @Override + public EvaluationValue convert(Object object, ExpressionConfiguration configuration) { + String string; + + if (object instanceof CharSequence) { + string = ((CharSequence) object).toString(); + } else if (object instanceof Character) { + string = ((Character) object).toString(); + } else { + throw illegalArgument(object); + } + + return EvaluationValue.stringValue(string); + } + + @Override + public boolean canConvert(Object object) { + return (object instanceof CharSequence || object instanceof Character); + } +} diff --git a/src/main/java/com/ezylang/evalex/data/conversion/StructureConverter.java b/src/main/java/com/ezylang/evalex/data/conversion/StructureConverter.java new file mode 100644 index 000000000..8c89874b5 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/data/conversion/StructureConverter.java @@ -0,0 +1,43 @@ +/* + 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.conversion; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; + +import java.util.HashMap; +import java.util.Map; + +/** + * Converter to convert to the STRUCTURE data type. + */ +public class StructureConverter implements ConverterIfc { + + @Override + public EvaluationValue convert(Object object, ExpressionConfiguration configuration) { + Map structure = new HashMap<>(); + for (Map.Entry entry : ((Map) object).entrySet()) { + String name = entry.getKey().toString(); + structure.put(name, EvaluationValue.of(entry.getValue(), configuration)); + } + return EvaluationValue.structureValue(structure); + } + + @Override + public boolean canConvert(Object object) { + return object instanceof Map; + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/AbstractFunction.java b/src/main/java/com/ezylang/evalex/functions/AbstractFunction.java new file mode 100644 index 000000000..8f19daa5f --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/AbstractFunction.java @@ -0,0 +1,103 @@ +/* + 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; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.parser.Token; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import static java.math.BigDecimal.valueOf; + +/** + * Abstract implementation of the {@link FunctionIfc}, used as base class for function + * implementations. + */ +public abstract class AbstractFunction implements FunctionIfc { + + protected static final BigDecimal MINUS_ONE = valueOf(-1); + private final List functionParameterDefinitions = new ArrayList<>(); + + private final boolean hasVarArgs; + + /** + * Creates a new function and uses the {@link FunctionParameter} annotations to create the + * parameter definitions. + */ + protected AbstractFunction() { + FunctionParameter[] parameterAnnotations = + getClass().getAnnotationsByType(FunctionParameter.class); + + boolean varArgParameterFound = false; + + for (FunctionParameter parameter : parameterAnnotations) { + if (varArgParameterFound) { + throw new IllegalArgumentException( + "Only last parameter may be defined as variable argument"); + } + if (parameter.isVarArg()) { + varArgParameterFound = true; + } + functionParameterDefinitions.add( + FunctionParameterDefinition.builder() + .name(parameter.name()) + .isVarArg(parameter.isVarArg()) + .isLazy(parameter.isLazy()) + .nonZero(parameter.nonZero()) + .nonNegative(parameter.nonNegative()) + .build()); + } + + hasVarArgs = varArgParameterFound; + } + + @Override + public void validatePreEvaluation(Token token, EvaluationValue... parameterValues) + throws EvaluationException { + + for (int i = 0; i < parameterValues.length; i++) { + FunctionParameterDefinition definition = getParameterDefinitionForParameter(i); + if (definition.isNonZero() && parameterValues[i].getNumberValue().equals(BigDecimal.ZERO)) { + throw new EvaluationException(token, "Parameter must not be zero"); + } + if (definition.isNonNegative() && parameterValues[i].getNumberValue().signum() < 0) { + throw new EvaluationException(token, "Parameter must not be negative"); + } + } + } + + @Override + public List getFunctionParameterDefinitions() { + return functionParameterDefinitions; + } + + @Override + public boolean hasVarArgs() { + return hasVarArgs; + } + + private FunctionParameterDefinition getParameterDefinitionForParameter(int index) { + + if (hasVarArgs && index >= functionParameterDefinitions.size()) { + index = functionParameterDefinitions.size() - 1; + } + + return functionParameterDefinitions.get(index); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/FunctionIfc.java b/src/main/java/com/ezylang/evalex/functions/FunctionIfc.java new file mode 100644 index 000000000..5d0070052 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/FunctionIfc.java @@ -0,0 +1,94 @@ +/* + 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; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.parser.Token; + +import java.util.List; + +/** + * Interface that is required for all functions in a function dictionary for evaluation of + * expressions. + */ +public interface FunctionIfc { + + /** + * Returns the list of parameter definitions. Is never empty or null. + * + * @return The parameter definition list. + */ + List getFunctionParameterDefinitions(); + + /** + * Performs the function logic and returns an evaluation result. + * + * @param expression The expression, where this function is executed. Can be used to access the + * expression configuration. + * @param functionToken The function token from the parsed expression. + * @param parameterValues The parameter values. + * @return The evaluation result in form of a {@link EvaluationValue}. + * @throws EvaluationException In case there were problems during evaluation. + */ + EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException; + + /** + * Validates the evaluation parameters, called before the actual evaluation. + * + * @param token The function token. + * @param parameterValues The parameter values + * @throws EvaluationException in case of any validation error + */ + void validatePreEvaluation(Token token, EvaluationValue... parameterValues) + throws EvaluationException; + + /** + * Checks whether the function has a variable number of arguments parameter. + * + * @return true or false: + */ + boolean hasVarArgs(); + + /** + * Checks if the parameter is a lazy parameter. + * + * @param parameterIndex The parameter index, starts at 0 for the first parameter. If the index is + * bigger than the list of parameter definitions, the last parameter definition will be + * checked. + * @return true if the specified parameter is defined as lazy. + */ + default boolean isParameterLazy(int parameterIndex) { + if (parameterIndex >= getFunctionParameterDefinitions().size()) { + parameterIndex = getFunctionParameterDefinitions().size() - 1; + } + return getFunctionParameterDefinitions().get(parameterIndex).isLazy(); + } + + /** + * Returns the count of non-var-arg parameters defined by this function. If the function has + * var-args, the result is the count of parameter definitions - 1. + * + * @return the count of non-var-arg parameters defined by this function. + */ + default int getCountOfNonVarArgParameters() { + int numOfParameters = getFunctionParameterDefinitions().size(); + return hasVarArgs() ? numOfParameters - 1 : numOfParameters; + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/FunctionParameter.java b/src/main/java/com/ezylang/evalex/functions/FunctionParameter.java new file mode 100644 index 000000000..37b8b93f1 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/FunctionParameter.java @@ -0,0 +1,58 @@ +/* + 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; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to define a function parameter. + */ +@Documented +@Target(ElementType.TYPE) +@Repeatable(FunctionParameters.class) +@Retention(RetentionPolicy.RUNTIME) +public @interface FunctionParameter { + + /** + * The parameter name. + */ + String name(); + + /** + * If the parameter is lazily evaluated. Defaults to false. + */ + boolean isLazy() default false; + + /** + * If the parameter is a variable arg type (repeatable). Defaults to false. + */ + boolean isVarArg() default false; + + /** + * If the parameter does not allow zero values. + */ + boolean nonZero() default false; + + /** + * If the parameter does not allow negative values. + */ + boolean nonNegative() default false; +} diff --git a/src/main/java/com/ezylang/evalex/functions/FunctionParameterDefinition.java b/src/main/java/com/ezylang/evalex/functions/FunctionParameterDefinition.java new file mode 100644 index 000000000..329443b3f --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/FunctionParameterDefinition.java @@ -0,0 +1,57 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions; + +import lombok.Builder; +import lombok.Value; + +/** + * Definition of a function parameter. + */ +@Value +@Builder +public class FunctionParameterDefinition { + + /** + * Name of the parameter, useful for error messages etc. + */ + String name; + + /** + * Whether this parameter is a variable argument parameter (can be repeated). + * + * @see com.ezylang.evalex.functions.basic.MinFunction for an example. + */ + boolean isVarArg; + + /** + * Set to true, the parameter will not be evaluated in advance, but the corresponding {@link + * com.ezylang.evalex.parser.ASTNode} will be passed as a parameter value. + * + * @see com.ezylang.evalex.functions.basic.IfFunction for an example. + */ + boolean isLazy; + + /** + * If the parameter does not allow zero values. + */ + boolean nonZero; + + /** + * If the parameter does not allow negative values. + */ + boolean nonNegative; +} diff --git a/src/main/java/com/ezylang/evalex/functions/FunctionParameters.java b/src/main/java/com/ezylang/evalex/functions/FunctionParameters.java new file mode 100644 index 000000000..1376a2045 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/FunctionParameters.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; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Collator for repeatable {@link FunctionParameter} annotations. + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface FunctionParameters { + + FunctionParameter[] value(); +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/AbsFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/AbsFunction.java new file mode 100644 index 000000000..df0b2483d --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/AbsFunction.java @@ -0,0 +1,37 @@ +/* + 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.basic; + +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; + +/** + * Absolute (non-negative) value. + */ +@FunctionParameter(name = "value") +public class AbsFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertValue( + parameterValues[0].getNumberValue().abs(expression.getConfiguration().getMathContext())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/AbstractMinMaxFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/AbstractMinMaxFunction.java new file mode 100644 index 000000000..afb8e0370 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/AbstractMinMaxFunction.java @@ -0,0 +1,45 @@ +/* + Copyright 2012-2024 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.basic; + +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; + +import java.math.BigDecimal; + +@FunctionParameter(name = "value", isVarArg = true) +public abstract class AbstractMinMaxFunction extends AbstractFunction { + + BigDecimal findMinOrMax(BigDecimal current, EvaluationValue parameter, boolean findMin) { + if (parameter.isArrayValue()) { + for (EvaluationValue element : parameter.getArrayValue()) { + current = findMinOrMax(current, element, findMin); + } + } else { + current = compareAndAssign(current, parameter.getNumberValue(), findMin); + } + return current; + } + + BigDecimal compareAndAssign(BigDecimal current, BigDecimal newValue, boolean findMin) { + if (current == null + || (findMin ? newValue.compareTo(current) < 0 : newValue.compareTo(current) > 0)) { + current = newValue; + } + return current; + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/AverageFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/AverageFunction.java new file mode 100644 index 000000000..63f1092a6 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/AverageFunction.java @@ -0,0 +1,82 @@ +/* + Copyright 2012-2024 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.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.math.BigDecimal; +import java.math.MathContext; + +/** + * Returns the average (arithmetic mean) of the numeric arguments, with recursive support for + * arrays. + * + * @author oswaldo.bapvic.jr + */ +@FunctionParameter(name = "firstValue") +@FunctionParameter(name = "additionalValues", isVarArg = true) +public class AverageFunction extends AbstractMinMaxFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + MathContext mathContext = expression.getConfiguration().getMathContext(); + BigDecimal average = average(mathContext, parameterValues); + return expression.convertValue(average); + } + + private BigDecimal average(MathContext mathContext, EvaluationValue... parameterValues) { + SumAndCount aux = new SumAndCount(); + for (EvaluationValue parameter : parameterValues) { + aux = aux.plus(recursiveSumAndCount(parameter)); + } + + return aux.sum.divide(aux.count, mathContext); + } + + private SumAndCount recursiveSumAndCount(EvaluationValue parameter) { + SumAndCount aux = new SumAndCount(); + if (parameter.isArrayValue()) { + for (EvaluationValue element : parameter.getArrayValue()) { + aux = aux.plus(recursiveSumAndCount(element)); + } + return aux; + } + return new SumAndCount(parameter.getNumberValue(), BigDecimal.ONE); + } + + private final class SumAndCount { + + private final BigDecimal sum; + private final BigDecimal count; + + private SumAndCount() { + this(BigDecimal.ZERO, BigDecimal.ZERO); + } + + private SumAndCount(BigDecimal sum, BigDecimal count) { + this.sum = sum; + this.count = count; + } + + private SumAndCount plus(SumAndCount other) { + return new SumAndCount(sum.add(other.sum), count.add(other.count)); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/CeilingFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/CeilingFunction.java new file mode 100644 index 000000000..189f655d5 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/CeilingFunction.java @@ -0,0 +1,40 @@ +/* + 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.basic; + +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.RoundingMode; + +/** + * Rounds the given value to an integer using the rounding mode {@link RoundingMode#CEILING} + */ +@FunctionParameter(name = "value") +public class CeilingFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + EvaluationValue value = parameterValues[0]; + + return expression.convertValue(value.getNumberValue().setScale(0, RoundingMode.CEILING)); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/CoalesceFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/CoalesceFunction.java new file mode 100644 index 000000000..584e6fdde --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/CoalesceFunction.java @@ -0,0 +1,41 @@ +/* + 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.basic; + +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; + +/** + * Returns the first non-null parameter, or {@link EvaluationValue#NULL_VALUE} if all parameters are + * null. + */ +@FunctionParameter(name = "value", isVarArg = true) +public class CoalesceFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + for (EvaluationValue parameter : parameterValues) { + if (!parameter.isNullValue()) { + return parameter; + } + } + return EvaluationValue.NULL_VALUE; + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/FactFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/FactFunction.java new file mode 100644 index 000000000..b66198a18 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/FactFunction.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.basic; + +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; + +/** + * Factorial function, calculates the factorial of a base value. + */ +@FunctionParameter(name = "base") +public class FactFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + int number = parameterValues[0].getNumberValue().intValue(); + BigDecimal factorial = BigDecimal.ONE; + for (int i = 1; i <= number; i++) { + factorial = + factorial.multiply( + new BigDecimal(i, expression.getConfiguration().getMathContext()), + expression.getConfiguration().getMathContext()); + } + return expression.convertValue(factorial); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/FloorFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/FloorFunction.java new file mode 100644 index 000000000..d7d6e4758 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/FloorFunction.java @@ -0,0 +1,40 @@ +/* + 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.basic; + +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.RoundingMode; + +/** + * Rounds the given value to an integer using the rounding mode {@link RoundingMode#FLOOR} + */ +@FunctionParameter(name = "value") +public class FloorFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + EvaluationValue value = parameterValues[0]; + + return expression.convertValue(value.getNumberValue().setScale(0, RoundingMode.FLOOR)); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/IfFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/IfFunction.java new file mode 100644 index 000000000..b6106b3fe --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/IfFunction.java @@ -0,0 +1,46 @@ +/* + 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.basic; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Conditional evaluation function. If parameter condition is true, the + * resultIfTrue value is returned, else the resultIfFalse value. + * resultIfTrue and resultIfFalse are only evaluated (lazily evaluated), + * after the condition was evaluated. + */ +@FunctionParameter(name = "condition") +@FunctionParameter(name = "resultIfTrue", isLazy = true) +@FunctionParameter(name = "resultIfFalse", isLazy = true) +public class IfFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + if (Boolean.TRUE.equals(parameterValues[0].getBooleanValue())) { + return expression.evaluateSubtree(parameterValues[1].getExpressionNode()); + } else { + return expression.evaluateSubtree(parameterValues[2].getExpressionNode()); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/Log10Function.java b/src/main/java/com/ezylang/evalex/functions/basic/Log10Function.java new file mode 100644 index 000000000..35c078a7a --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/Log10Function.java @@ -0,0 +1,38 @@ +/* + 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.basic; + +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; + +/** + * The base 10 logarithm of a value + */ +@FunctionParameter(name = "value", nonZero = true, nonNegative = true) +public class Log10Function extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + double d = parameterValues[0].getNumberValue().doubleValue(); + + return expression.convertDoubleValue(Math.log10(d)); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/LogFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/LogFunction.java new file mode 100644 index 000000000..e0110aec3 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/LogFunction.java @@ -0,0 +1,38 @@ +/* + 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.basic; + +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; + +/** + * The natural logarithm (base e) of a value + */ +@FunctionParameter(name = "value", nonZero = true, nonNegative = true) +public class LogFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + double d = parameterValues[0].getNumberValue().doubleValue(); + + return expression.convertDoubleValue(Math.log(d)); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/MaxFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/MaxFunction.java new file mode 100644 index 000000000..2beefa4b8 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/MaxFunction.java @@ -0,0 +1,41 @@ +/* + 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.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.math.BigDecimal; + +/** + * Returns the maximum value of all parameters. + */ +@FunctionParameter(name = "firstValue") +@FunctionParameter(name = "value", isVarArg = true) +public class MaxFunction extends AbstractMinMaxFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + BigDecimal min = null; + for (EvaluationValue parameter : parameterValues) { + min = findMinOrMax(min, parameter, false); + } + return expression.convertValue(min); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/MinFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/MinFunction.java new file mode 100644 index 000000000..44c7c8d17 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/MinFunction.java @@ -0,0 +1,41 @@ +/* + 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.basic; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.math.BigDecimal; + +/** + * Returns the minimum value of all parameters. + */ +@FunctionParameter(name = "firstValue") +@FunctionParameter(name = "value", isVarArg = true) +public class MinFunction extends AbstractMinMaxFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + BigDecimal min = null; + for (EvaluationValue parameter : parameterValues) { + min = findMinOrMax(min, parameter, true); + } + return expression.convertValue(min); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/NotFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/NotFunction.java new file mode 100644 index 000000000..f7c590390 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/NotFunction.java @@ -0,0 +1,38 @@ +/* + 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.basic; + +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; + +/** + * Boolean negation function. + */ +@FunctionParameter(name = "value") +public class NotFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + boolean result = parameterValues[0].getBooleanValue(); + + return expression.convertValue(!result); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/RandomFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/RandomFunction.java new file mode 100644 index 000000000..8e047e759 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/RandomFunction.java @@ -0,0 +1,38 @@ +/* + 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.basic; + +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.security.SecureRandom; + +/** + * Random function produces a random value between 0 and 1. + */ +public class RandomFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + SecureRandom secureRandom = new SecureRandom(); + + return expression.convertDoubleValue(secureRandom.nextDouble()); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/RoundFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/RoundFunction.java new file mode 100644 index 000000000..449580f03 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/RoundFunction.java @@ -0,0 +1,46 @@ +/* + 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.basic; + +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; + +/** + * Rounds the given value to the specified scale, using the {@link java.math.MathContext} of the + * expression configuration. + */ +@FunctionParameter(name = "value") +@FunctionParameter(name = "scale") +public class RoundFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + EvaluationValue value = parameterValues[0]; + EvaluationValue precision = parameterValues[1]; + + return expression.convertValue( + value + .getNumberValue() + .setScale( + precision.getNumberValue().intValue(), + expression.getConfiguration().getMathContext().getRoundingMode())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/SqrtFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/SqrtFunction.java new file mode 100644 index 000000000..2ad0bd175 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/SqrtFunction.java @@ -0,0 +1,66 @@ +/* + 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.basic; + +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.math.BigInteger; +import java.math.MathContext; + +/** + * Square root function, uses the implementation from The Java Programmers Guide To numerical + * Computing by Ronald Mak, 2002. + */ +@FunctionParameter(name = "value", nonNegative = true) +public class SqrtFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* + * From The Java Programmers Guide To numerical Computing + * (Ronald Mak, 2002) + */ + + BigDecimal x = parameterValues[0].getNumberValue(); + MathContext mathContext = expression.getConfiguration().getMathContext(); + + if (x.compareTo(BigDecimal.ZERO) == 0) { + return expression.convertValue(BigDecimal.ZERO); + } + BigInteger n = x.movePointRight(mathContext.getPrecision() << 1).toBigInteger(); + + int bits = (n.bitLength() + 1) >> 1; + BigInteger ix = n.shiftRight(bits); + BigInteger ixPrev; + BigInteger test; + do { + ixPrev = ix; + ix = ix.add(n.divide(ix)).shiftRight(1); + // Give other threads a chance to work + Thread.yield(); + test = ix.subtract(ixPrev).abs(); + } while (test.compareTo(BigInteger.ZERO) != 0 && test.compareTo(BigInteger.ONE) != 0); + + return expression.convertValue(new BigDecimal(ix, mathContext.getPrecision())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/SumFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/SumFunction.java new file mode 100644 index 000000000..74ed2db2e --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/SumFunction.java @@ -0,0 +1,57 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.basic; + +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; + +/** + * Returns the sum value of all parameters. + */ +@FunctionParameter(name = "value", isVarArg = true) +public class SumFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + BigDecimal sum = BigDecimal.ZERO; + for (EvaluationValue parameter : parameterValues) { + sum = + sum.add( + recursiveSum(parameter, expression), expression.getConfiguration().getMathContext()); + } + return expression.convertValue(sum); + } + + private BigDecimal recursiveSum(EvaluationValue parameter, Expression expression) { + BigDecimal sum = BigDecimal.ZERO; + if (parameter.isArrayValue()) { + for (EvaluationValue element : parameter.getArrayValue()) { + sum = + sum.add( + recursiveSum(element, expression), expression.getConfiguration().getMathContext()); + } + } else { + sum = sum.add(parameter.getNumberValue(), expression.getConfiguration().getMathContext()); + } + return sum; + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/SwitchFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/SwitchFunction.java new file mode 100644 index 000000000..5755e374c --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/SwitchFunction.java @@ -0,0 +1,102 @@ +/* + Copyright 2012-2024 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.basic; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * A function that evaluates one value (or expression) against a list of values, and returns the + * result corresponding to the first matching value. If there is no match, an optional default value + * may be returned. + * + *

Syntax: + * + *

+ *

+ * {@code SWITCH(expression, value1, result1, [value, result, ...], [default])} + * + *

+ * + *

Examples: + * + *

1. The following function will return either "Sunday", "Monday", or "Tuesday", depending on + * the result of the variable {@code weekday}. Since no default value was specified, the function + * will return a null value if there is no match: + * + *

+ *

+ * {@code SWITCH(weekday, 1, "Sunday", 2, "Monday", 3, "Tuesday")} + * + *

+ * + *

2. The following function will return either "Sunday", "Monday", "Tuesday", or "No match", + * depending on the result of the variable {@code weekday}: + * + *

+ *

+ * {@code SWITCH(weekday, 1, "Sunday", 2, "Monday", 3, "Tuesday", "No match")} + * + *

+ * + * @author oswaldo.bapvic.jr + */ +@FunctionParameter(name = "expression") +@FunctionParameter(name = "value1") +@FunctionParameter(name = "result1", isLazy = true) +@FunctionParameter(name = "additionalValues", isLazy = true, isVarArg = true) +public class SwitchFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + + EvaluationValue result = EvaluationValue.NULL_VALUE; + + // First get the first parameter + EvaluationValue value = parameterValues[0]; + + // Iterate through the parameters to parse the pairs of value-result and the default result if + // present. + int index = 1; + while (index < parameterValues.length) { + int next = index + 1; + if (next < parameterValues.length) { + if (value.equals(evaluateParameter(expression, parameterValues[index]))) { + result = parameterValues[next]; + break; + } + index += 2; + } else { + // The default result + result = parameterValues[index++]; + } + } + return evaluateParameter(expression, result); + } + + private EvaluationValue evaluateParameter(Expression expression, EvaluationValue parameter) + throws EvaluationException { + return parameter.isExpressionNode() + ? expression.evaluateSubtree(parameter.getExpressionNode()) + : parameter; + } +} 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 000000000..2b2302936 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeFormatFunction.java @@ -0,0 +1,74 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.datetime; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +/** + * Function to format a DATE_TIME vale. Required parameter is the DATE_TIME value to format. First + * optional parameter is the format to use, using a pattern used by {@link DateTimeFormatter}. If no + * format is given, the first format defined in the configured formats is used. Second optional + * parameter is the zone-id to use with formatting. Default is the configured zone-id. + */ +@FunctionParameter(name = "value") +@FunctionParameter(name = "parameters", isVarArg = true) +public class DateTimeFormatFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + + DateTimeFormatter formatter = expression.getConfiguration().getDateTimeFormatters().get(0); + if (parameterValues.length > 1) { + formatter = + DateTimeFormatter.ofPattern(parameterValues[1].getStringValue()) + .withLocale(expression.getConfiguration().getLocale()); + } + + ZoneId zoneId = expression.getConfiguration().getZoneId(); + if (parameterValues.length == 3) { + zoneId = ZoneIdConverter.convert(functionToken, parameterValues[2].getStringValue()); + } + + return expression.convertValue( + parameterValues[0].getDateTimeValue().atZone(zoneId).format(formatter)); + } + + @Override + public void validatePreEvaluation(Token token, EvaluationValue... parameterValues) + throws EvaluationException { + super.validatePreEvaluation(token, parameterValues); + if (parameterValues.length > 3) { + throw new EvaluationException(token, "Too many parameters"); + } + if (!parameterValues[0].isDateTimeValue()) { + throw new EvaluationException( + token, + String.format( + "Unable to format a '%s' type as a date-time", + parameterValues[0].getDataType().name())); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeNewFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeNewFunction.java new file mode 100644 index 000000000..5812256cd --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeNewFunction.java @@ -0,0 +1,124 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.datetime; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; +import org.apache.commons.lang3.ArrayUtils; + +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.TimeZone; + +/** + * Creates a new DATE_TIME value with the given parameters. If only one parameter is given, it is + * treated as the time in milliseconds from the epoch of 1970-01-01T00:00:00Z and a corresponding + * date/time value is created. Else, A minimum of three parameters (year, month, day) must be + * specified. Optionally the hour, minute, second and nanosecond can be specified. If the last + * parameter is a string value, it is treated as a zone ID. If no zone ID is specified, the + * configured zone ID is used. + */ +@FunctionParameter(name = "values", isVarArg = true, nonNegative = true) +public class DateTimeNewFunction extends AbstractFunction { + + private static String[] timeZones = null; + + public static String[] getTimeZones() { + if (timeZones == null) { + timeZones = TimeZone.getAvailableIDs(); + } + return timeZones; + } + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + + int parameterLength = parameterValues.length; + + if (parameterLength == 1) { + BigDecimal millis = parameterValues[0].getNumberValue(); + return expression.convertValue(Instant.ofEpochMilli(millis.longValue())); + } + + ZoneId zoneId = expression.getConfiguration().getZoneId(); + if (parameterValues[parameterLength - 1].isStringValue()) { + zoneId = + ZoneIdConverter.convert( + functionToken, parameterValues[parameterLength - 1].getStringValue()); + parameterLength--; + } + + int year = parameterValues[0].getNumberValue().intValue(); + int month = parameterValues[1].getNumberValue().intValue(); + int day = parameterValues[2].getNumberValue().intValue(); + int hour = parameterLength >= 4 ? parameterValues[3].getNumberValue().intValue() : 0; + int minute = parameterLength >= 5 ? parameterValues[4].getNumberValue().intValue() : 0; + int second = parameterLength >= 6 ? parameterValues[5].getNumberValue().intValue() : 0; + int nanoOfs = parameterLength == 7 ? parameterValues[6].getNumberValue().intValue() : 0; + + return expression.convertValue( + LocalDateTime.of(year, month, day, hour, minute, second, nanoOfs) + .atZone(zoneId) + .toInstant()); + } + + @Override + public void validatePreEvaluation(Token token, EvaluationValue... parameterValues) + throws EvaluationException { + + super.validatePreEvaluation(token, parameterValues); + + int parameterLength = parameterValues.length; + + if (parameterLength == 0) { + throw new EvaluationException(token, "Not enough parameters for function"); + } + + if (parameterLength == 1) { + if (!parameterValues[0].isNumberValue()) { + throw new EvaluationException( + token, "Expected a number value for the time in milliseconds since the epoch"); + } else { + return; + } + } + + if (parameterValues[parameterLength - 1].isStringValue()) { + if (!ArrayUtils.contains(getTimeZones(), parameterValues[parameterLength - 1].getStringValue())) { + throw new EvaluationException(token, "Time zone with id '" + + parameterValues[parameterLength - 1].getStringValue() + "' not found"); + } + parameterLength--; + } + + if (parameterLength < 3) { + throw new EvaluationException( + token, "A minimum of 3 parameters (year, month, day) is required"); + } + + if (parameterLength > 7) { + throw new EvaluationException(token, "Too many parameters to function"); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeNowFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeNowFunction.java new file mode 100644 index 000000000..e3a2960ca --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeNowFunction.java @@ -0,0 +1,47 @@ +/* + Copyright 2012-2024 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; + +/** + * Produces a new DATE_TIME that represents the current date and time. + * + *

It is useful to calculate a value based on the current date and time. For example, if you know + * the start DATE_TIME of a running process, you might use the following expression to find the + * DURATION that represents the process age: + * + *

+ *

+ * {@code DT_NOW() - startDateTime} + * + *

+ * + * @author oswaldobapvicjr + */ +public class DateTimeNowFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + return expression.convertValue(Instant.now()); + } +} 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 000000000..dda9d60d9 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeParseFunction.java @@ -0,0 +1,85 @@ +/* + Copyright 2012-2023 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.datetime; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.data.conversion.DateTimeConverter; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +/** + * Parses a date-time string to a {@link EvaluationValue.DataType#DATE_TIME} value. + * + *

Optional arguments are the time zone and a list of {@link java.time.format.DateTimeFormatter} + * patterns. Each pattern will be tried to convert the string to a date-time. The first matching + * pattern will be used. If NULL is specified for the time zone, the currently + * configured zone is used. If no formatter is specified, the function will use the formatters + * defined at the {@link ExpressionConfiguration}. + */ +@FunctionParameter(name = "value") +@FunctionParameter(name = "parameters", isVarArg = true) +public class DateTimeParseFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + + String value = parameterValues[0].getStringValue(); + + ZoneId zoneId = expression.getConfiguration().getZoneId(); + if (parameterValues.length > 1 && !parameterValues[1].isNullValue()) { + zoneId = ZoneIdConverter.convert(functionToken, parameterValues[1].getStringValue()); + } + + List formatters; + + if (parameterValues.length > 2) { + formatters = new ArrayList<>(); + for (int i = 2; i < parameterValues.length; i++) { + try { + formatters.add(DateTimeFormatter.ofPattern(parameterValues[i].getStringValue())); + } catch (IllegalArgumentException ex) { + throw new EvaluationException( + functionToken, + String.format( + "Illegal date-time format in parameter %d: '%s'", + i + 1, parameterValues[i].getStringValue())); + } + } + } else { + formatters = expression.getConfiguration().getDateTimeFormatters(); + } + DateTimeConverter converter = new DateTimeConverter(); + Instant instant = converter.parseDateTime(value, zoneId, formatters); + + if (instant == null) { + throw new EvaluationException( + functionToken, String.format("Unable to parse date-time string '%s'", value)); + } + return EvaluationValue.dateTimeValue(instant); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeToEpochFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeToEpochFunction.java new file mode 100644 index 000000000..3e0be8af8 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeToEpochFunction.java @@ -0,0 +1,35 @@ +/* + 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; + +/** + * Function to convert a DATE_TIME value to milliseconds in the epoch of 1970-01-01T00:00:00Z. + */ +@FunctionParameter(name = "value") +public class DateTimeToEpochFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + return expression.convertValue(parameterValues[0].getDateTimeValue().toEpochMilli()); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeTodayFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeTodayFunction.java new file mode 100644 index 000000000..f91e6c667 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DateTimeTodayFunction.java @@ -0,0 +1,67 @@ +/* + Copyright 2012-2024 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.datetime; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.config.ExpressionConfiguration; +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.Instant; +import java.time.LocalDate; +import java.time.ZoneId; + +/** + * Produces a new DATE_TIME that represents the current date, at midnight (00:00). + * + *

It is useful for DATE_TIME comparison, when the current time must not be considered. For + * example, in the expression: + * + *

+ *

+ * {@code IF(expiryDate > DT_TODAY(), "expired", "valid")} + * + *

+ * + *

This function may accept an optional time zone to be applied. If no zone ID is specified, the + * default zone ID defined at the {@link ExpressionConfiguration} will be used. + * + * @author oswaldobapvicjr + */ +@FunctionParameter(name = "parameters", isVarArg = true) +public class DateTimeTodayFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + ZoneId zoneId = parseZoneId(expression, functionToken, parameterValues); + Instant today = LocalDate.now().atStartOfDay(zoneId).toInstant(); + return expression.convertValue(today); + } + + private ZoneId parseZoneId( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + if (parameterValues.length > 0 && !parameterValues[0].isNullValue()) { + return ZoneIdConverter.convert(functionToken, parameterValues[0].getStringValue()); + } + return expression.getConfiguration().getZoneId(); + } +} 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 000000000..ecffc04ec --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DurationFromMillisFunction.java @@ -0,0 +1,39 @@ +/* + 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; + +/** + * Converts the given milliseconds to a DURATION value. + */ +@FunctionParameter(name = "value") +public class DurationFromMillisFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + BigDecimal millis = parameterValues[0].getNumberValue(); + return expression.convertValue(Duration.ofMillis(millis.longValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DurationNewFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DurationNewFunction.java new file mode 100644 index 000000000..471662f86 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DurationNewFunction.java @@ -0,0 +1,58 @@ +/* + 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; + +/** + * Function to create a new Duration. First parameter is required and specifies the number of days. + * All other parameters are optional and specify hours, minutes, seconds, milliseconds and + * nanoseconds. + */ +@FunctionParameter(name = "days") +@FunctionParameter(name = "parameters", isVarArg = true) +public class DurationNewFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + int parameterLength = parameterValues.length; + + int days = parameterValues[0].getNumberValue().intValue(); + int hours = parameterLength >= 2 ? parameterValues[1].getNumberValue().intValue() : 0; + int minutes = parameterLength >= 3 ? parameterValues[2].getNumberValue().intValue() : 0; + int seconds = parameterLength >= 4 ? parameterValues[3].getNumberValue().intValue() : 0; + int millis = parameterLength >= 5 ? parameterValues[4].getNumberValue().intValue() : 0; + int nanos = parameterLength == 6 ? parameterValues[5].getNumberValue().intValue() : 0; + + Duration duration = + Duration.ofDays(days) + .plusHours(hours) + .plusMinutes(minutes) + .plusSeconds(seconds) + .plusMillis(millis) + .plusNanos(nanos); + + return expression.convertValue(duration); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DurationParseFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DurationParseFunction.java new file mode 100644 index 000000000..c4137092d --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DurationParseFunction.java @@ -0,0 +1,39 @@ +/* + 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; + +/** + * Converts the given ISO-8601 duration string representation to a duration value. E.g. "P2DT3H4M" + * parses 2 days, 3 hours and 4 minutes. + */ +@FunctionParameter(name = "value") +public class DurationParseFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + String text = parameterValues[0].getStringValue(); + return expression.convertValue(Duration.parse(text)); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/DurationToMillisFunction.java b/src/main/java/com/ezylang/evalex/functions/datetime/DurationToMillisFunction.java new file mode 100644 index 000000000..d4aac1cb5 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/DurationToMillisFunction.java @@ -0,0 +1,35 @@ +/* + 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; + +/** + * Converts a DURATION value to the amount of milliseconds. + */ +@FunctionParameter(name = "value") +public class DurationToMillisFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + return expression.convertValue(parameterValues[0].getDurationValue().toMillis()); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/datetime/ZoneIdConverter.java b/src/main/java/com/ezylang/evalex/functions/datetime/ZoneIdConverter.java new file mode 100644 index 000000000..842671451 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/datetime/ZoneIdConverter.java @@ -0,0 +1,53 @@ +/* + Copyright 2012-2023 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.datetime; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.parser.Token; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.time.DateTimeException; +import java.time.ZoneId; + +/** + * Validates and converts a zone ID. + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ZoneIdConverter { + + /** + * Converts a zone ID string to a {@link ZoneId}. Throws an {@link EvaluationException} if + * conversion fails. + * + * @param referenceToken The token for the error message, usually the function token. + * @param zoneIdString The zone IDS string to convert. + * @return The converted {@link ZoneId}. + * @throws EvaluationException In case the zone ID can't be converted. + */ + public static ZoneId convert(Token referenceToken, String zoneIdString) + throws EvaluationException { + try { + return ZoneId.of(zoneIdString); + } catch (DateTimeException exception) { + throw new EvaluationException( + referenceToken, + String.format( + "Unable to convert zone string '%s' to a zone ID: %s", + referenceToken.getValue(), exception.getMessage())); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringContains.java b/src/main/java/com/ezylang/evalex/functions/string/StringContains.java new file mode 100644 index 000000000..d10842fe6 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringContains.java @@ -0,0 +1,42 @@ +/* + 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.string; + +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; + +/** + * Returns true if the string contains the substring (case-insensitive). + */ +@FunctionParameter(name = "string") +@FunctionParameter(name = "substring") +public class StringContains extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + String string = parameterValues[0].getStringValue(); + String substring = parameterValues[1].getStringValue(); + boolean result = + string != null + && substring != null + && string.toUpperCase().contains(substring.toUpperCase()); + return expression.convertValue(result); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringEndsWithFunction.java b/src/main/java/com/ezylang/evalex/functions/string/StringEndsWithFunction.java new file mode 100644 index 000000000..0b66002ac --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringEndsWithFunction.java @@ -0,0 +1,40 @@ +/* + Copyright 2012-2024 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.string; + +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; + +/** + * Returns true if the string ends with the substring (case-sensitive). + * + * @author oswaldobapvicjr + */ +@FunctionParameter(name = "string") +@FunctionParameter(name = "substring") +public class StringEndsWithFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + String string = parameterValues[0].getStringValue(); + String substring = parameterValues[1].getStringValue(); + return expression.convertValue(string.endsWith(substring)); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringFormatFunction.java b/src/main/java/com/ezylang/evalex/functions/string/StringFormatFunction.java new file mode 100644 index 000000000..f92ffab6b --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringFormatFunction.java @@ -0,0 +1,87 @@ +/* + Copyright 2012-2024 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.string; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.config.ExpressionConfiguration; +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.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.stream.IntStream; + +/** + * Returns a formatted string using the specified format string and arguments, using the configured + * locale. + * + *

For example: + * + *

+ *

+ * {@code STR_FORMAT("Welcome to %s!", "EvalEx")} + * + *

+ * + *

The result is produced using {@link String#format(String, Object...)}. + * + * @author oswaldobapvicjr + */ +@FunctionParameter(name = "format") +@FunctionParameter(name = "arguments", isVarArg = true) +public class StringFormatFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + String format = parameterValues[0].getStringValue(); + Object[] arguments = getFormatArguments(parameterValues, expression.getConfiguration()); + return expression.convertValue( + String.format(expression.getConfiguration().getLocale(), format, arguments)); + } + + private Object[] getFormatArguments( + EvaluationValue[] parameterValues, ExpressionConfiguration configuration) { + if (parameterValues.length > 1) { + return convertParametersToObjects(parameterValues, configuration); + } + return new Object[0]; + } + + private Object[] convertParametersToObjects( + EvaluationValue[] parameterValues, ExpressionConfiguration configuration) { + return IntStream.range(1, parameterValues.length) + .mapToObj(i -> convertParameterToObject(parameterValues[i], configuration)) + .toArray(); + } + + private Object convertParameterToObject( + EvaluationValue parameterValue, ExpressionConfiguration configuration) { + if (parameterValue.isDateTimeValue()) { + return convertInstantToLocalDateTime( + parameterValue.getDateTimeValue(), configuration.getZoneId()); + } else { + return parameterValue.getValue(); + } + } + + private ZonedDateTime convertInstantToLocalDateTime(Instant instant, ZoneId zoneId) { + return instant.atZone(zoneId); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringLeftFunction.java b/src/main/java/com/ezylang/evalex/functions/string/StringLeftFunction.java new file mode 100644 index 000000000..3e6db26ac --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringLeftFunction.java @@ -0,0 +1,64 @@ +/* + Copyright 2012-2024 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.string; + +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; + +/** + * Represents a function that extracts a substring from the left side of a given string. This class + * extends the {@link AbstractFunction} and implements the logic for the `LEFT` string function, + * which returns a specified number of characters from the beginning (left) of the input string. + * + *

Two parameters are required for this function: + * + *

    + *
  • string - The input string from which the substring will be extracted. + *
  • length - The number of characters to extract from the left side of the string. If + * the specified length is greater than the string's length, the entire string is returned. If + * the length is negative or zero, an empty string is returned. + *
+ * + *

Example usage: If the input string is "hello" and the length is 2, the result will be "he". + */ +@FunctionParameter(name = "string") +@FunctionParameter(name = "length") +public class StringLeftFunction extends AbstractFunction { + + /** + * Evaluates the `LEFT` string function by extracting a substring from the left side of the given + * string. + * + * @param expression the current expression being evaluated + * @param functionToken the token representing the function being called + * @param parameterValues the parameters passed to the function; expects exactly two parameters: a + * string and a numeric value for length + * @return the substring extracted from the left side of the input string as an {@link + * EvaluationValue} + */ + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + String string = parameterValues[0].getStringValue(); + int length = + Math.max(0, Math.min(parameterValues[1].getNumberValue().intValue(), string.length())); + String substr = string.substring(0, length); + return expression.convertValue(substr); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringLengthFunction.java b/src/main/java/com/ezylang/evalex/functions/string/StringLengthFunction.java new file mode 100644 index 000000000..51abe6983 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringLengthFunction.java @@ -0,0 +1,39 @@ +/* + Copyright 2012-2024 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.string; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the length of the string. + * + * @author HSGamer + */ +@FunctionParameter(name = "string") +public class StringLengthFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + return expression.convertValue(parameterValues[0].getStringValue().length()); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringLowerFunction.java b/src/main/java/com/ezylang/evalex/functions/string/StringLowerFunction.java new file mode 100644 index 000000000..edb0a9699 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringLowerFunction.java @@ -0,0 +1,35 @@ +/* + 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.string; + +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; + +/** + * Converts the given value to lower case. + */ +@FunctionParameter(name = "value") +public class StringLowerFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + return expression.convertValue(parameterValues[0].getStringValue().toLowerCase()); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringMatchesFunction.java b/src/main/java/com/ezylang/evalex/functions/string/StringMatchesFunction.java new file mode 100644 index 000000000..fceb02426 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringMatchesFunction.java @@ -0,0 +1,42 @@ +/* + Copyright 2012-2024 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.string; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns true if the string matches the pattern. + * + * @author HSGamer + */ +@FunctionParameter(name = "string") +@FunctionParameter(name = "pattern") +public class StringMatchesFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + String string = parameterValues[0].getStringValue(); + String pattern = parameterValues[1].getStringValue(); + return expression.convertValue(string.matches(pattern)); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringRightFunction.java b/src/main/java/com/ezylang/evalex/functions/string/StringRightFunction.java new file mode 100644 index 000000000..8de28a3cf --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringRightFunction.java @@ -0,0 +1,64 @@ +/* + Copyright 2012-2024 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.string; + +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; + +/** + * Represents a function that extracts a substring from the right side of a given string. This class + * extends the {@link AbstractFunction} and implements the logic for the `RIGHT` string function, + * which returns a specified number of characters from the end (right) of the input string. + * + *

Two parameters are required for this function: + * + *

    + *
  • string - The input string from which the substring will be extracted. + *
  • length - The number of characters to extract from the right side of the string. If + * the specified length is greater than the string's length, the entire string is returned. If + * the length is negative or zero, an empty string is returned. + *
+ * + *

Example usage: If the input string is "hello" and the length is 2, the result will be "lo". + */ +@FunctionParameter(name = "string") +@FunctionParameter(name = "length") +public class StringRightFunction extends AbstractFunction { + + /** + * Evaluates the `RIGHT` string function by extracting a substring from the right side of the + * given string. + * + * @param expression the current expression being evaluated + * @param functionToken the token representing the function being called + * @param parameterValues the parameters passed to the function; expects exactly two parameters: a + * string and a numeric value for length + * @return the substring extracted from the right side of the input string as an {@link + * EvaluationValue} + */ + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + String string = parameterValues[0].getStringValue(); + int length = + Math.max(0, Math.min(parameterValues[1].getNumberValue().intValue(), string.length())); + String substr = string.substring(string.length() - length); + return expression.convertValue(substr); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringSplitFunction.java b/src/main/java/com/ezylang/evalex/functions/string/StringSplitFunction.java new file mode 100644 index 000000000..d48449264 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringSplitFunction.java @@ -0,0 +1,53 @@ +/* + Copyright 2012-2025 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.string; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.util.regex.Pattern; + +/** + * A function that splits a string into an array, separators specified. + * + *

For example: + * + *

+ * + *

+ * STR_SPLIT("2024/07/15", "/")  = ["2024", "07", "15"]
+ * STR_SPLIT("myFile.json", ".") = ["myFile", "json"]
+ * 
+ * + * @author oswaldobapvicjr + */ +@FunctionParameter(name = "string") +@FunctionParameter(name = "separator") +public class StringSplitFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + String string = parameterValues[0].getStringValue(); + String separator = parameterValues[1].getStringValue(); + return expression.convertValue(string.split(Pattern.quote(separator))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringStartsWithFunction.java b/src/main/java/com/ezylang/evalex/functions/string/StringStartsWithFunction.java new file mode 100644 index 000000000..621f9aa71 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringStartsWithFunction.java @@ -0,0 +1,40 @@ +/* + Copyright 2012-2024 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.string; + +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; + +/** + * Returns true if the string starts with the substring (case-sensitive). + * + * @author oswaldobapvicjr + */ +@FunctionParameter(name = "string") +@FunctionParameter(name = "substring") +public class StringStartsWithFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + String string = parameterValues[0].getStringValue(); + String substring = parameterValues[1].getStringValue(); + return expression.convertValue(string.startsWith(substring)); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringSubstringFunction.java b/src/main/java/com/ezylang/evalex/functions/string/StringSubstringFunction.java new file mode 100644 index 000000000..ab6120c6a --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringSubstringFunction.java @@ -0,0 +1,64 @@ +/* + Copyright 2012-2024 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.string; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns a substring of a string. + * + * @author HSGamer + */ +@FunctionParameter(name = "string") +@FunctionParameter(name = "start", nonNegative = true) +@FunctionParameter(name = "end", isVarArg = true, nonNegative = true) +public class StringSubstringFunction extends AbstractFunction { + + @Override + public void validatePreEvaluation(Token token, EvaluationValue... parameterValues) + throws EvaluationException { + super.validatePreEvaluation(token, parameterValues); + if (parameterValues.length > 2 + && parameterValues[2].getNumberValue().intValue() + < parameterValues[1].getNumberValue().intValue()) { + throw new EvaluationException( + token, "End index must be greater than or equal to start index"); + } + } + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + String string = parameterValues[0].getStringValue(); + int start = parameterValues[1].getNumberValue().intValue(); + String result; + if (parameterValues.length > 2) { + int end = parameterValues[2].getNumberValue().intValue(); + int length = string.length(); + int finalEnd = Math.min(end, length); + result = string.substring(start, finalEnd); + } else { + result = string.substring(start); + } + return expression.convertValue(result); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringTrimFunction.java b/src/main/java/com/ezylang/evalex/functions/string/StringTrimFunction.java new file mode 100644 index 000000000..d0a958e3a --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringTrimFunction.java @@ -0,0 +1,39 @@ +/* + Copyright 2012-2024 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.string; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the given string with all leading and trailing space removed. + * + * @author LeonardoSoaresDev + */ +@FunctionParameter(name = "string") +public class StringTrimFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + return expression.convertValue(parameterValues[0].getStringValue().trim()); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringUpperFunction.java b/src/main/java/com/ezylang/evalex/functions/string/StringUpperFunction.java new file mode 100644 index 000000000..e777f1c2d --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringUpperFunction.java @@ -0,0 +1,35 @@ +/* + 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.string; + +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; + +/** + * Converts the given value to upper case. + */ +@FunctionParameter(name = "value") +public class StringUpperFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + return expression.convertValue(parameterValues[0].getStringValue().toUpperCase()); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AcosFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcosFunction.java new file mode 100644 index 000000000..bb37fa273 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcosFunction.java @@ -0,0 +1,52 @@ +/* + 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.trigonometric; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.math.BigDecimal; + +import static java.math.BigDecimal.ONE; + +/** + * Returns the arc-cosine (in degrees). + */ +@FunctionParameter(name = "value") +public class AcosFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + + BigDecimal parameterValue = parameterValues[0].getNumberValue(); + + if (parameterValue.compareTo(ONE) > 0) { + throw new EvaluationException( + functionToken, "Illegal acos(x) for x > 1: x = " + parameterValue); + } + if (parameterValue.compareTo(MINUS_ONE) < 0) { + throw new EvaluationException( + functionToken, "Illegal acos(x) for x < -1: x = " + parameterValue); + } + return expression.convertDoubleValue(Math.toDegrees(Math.acos(parameterValue.doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AcosHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcosHFunction.java new file mode 100644 index 000000000..6a1c6c993 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcosHFunction.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.trigonometric; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the hyperbolic arc-cosine. + */ +@FunctionParameter(name = "value") +public class AcosHFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + + /* Formula: acosh(x) = ln(x + sqrt(x^2 - 1)) */ + double value = parameterValues[0].getNumberValue().doubleValue(); + if (Double.compare(value, 1) < 0) { + throw new EvaluationException(functionToken, "Value must be greater or equal to one"); + } + return expression.convertDoubleValue(Math.log(value + (Math.sqrt(Math.pow(value, 2) - 1)))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AcosRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcosRFunction.java new file mode 100644 index 000000000..37fc70e3a --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcosRFunction.java @@ -0,0 +1,53 @@ +/* + 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.trigonometric; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.math.BigDecimal; + +import static java.math.BigDecimal.ONE; + +/** + * Returns the arc-cosine (in radians). + */ +@FunctionParameter(name = "cosine") +public class AcosRFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + + BigDecimal parameterValue = parameterValues[0].getNumberValue(); + + if (parameterValue.compareTo(ONE) > 0) { + throw new EvaluationException( + functionToken, "Illegal acosr(x) for x > 1: x = " + parameterValue); + } + if (parameterValue.compareTo(MINUS_ONE) < 0) { + throw new EvaluationException( + functionToken, "Illegal acosr(x) for x < -1: x = " + parameterValue); + } + + return expression.convertDoubleValue(Math.acos(parameterValue.doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AcotFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcotFunction.java new file mode 100644 index 000000000..b0153d328 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcotFunction.java @@ -0,0 +1,39 @@ +/* + 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.trigonometric; + +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; + +/** + * Returns the arc-co-tangent (in degrees). + */ +@FunctionParameter(name = "value", nonZero = true) +public class AcotFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: acot(x) = (pi / 2) - atan(x) */ + return expression.convertDoubleValue( + Math.toDegrees( + (Math.PI / 2) - Math.atan(parameterValues[0].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AcotHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcotHFunction.java new file mode 100644 index 000000000..e5def354d --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcotHFunction.java @@ -0,0 +1,38 @@ +/* + 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.trigonometric; + +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; + +/** + * Returns the arc hyperbolic cotangent. + */ +@FunctionParameter(name = "value") +public class AcotHFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: acoth(x) = log((x + 1) / (x - 1)) * 0.5 */ + double value = parameterValues[0].getNumberValue().doubleValue(); + return expression.convertDoubleValue(Math.log((value + 1) / (value - 1)) * 0.5); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AcotRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcotRFunction.java new file mode 100644 index 000000000..32a004c3d --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AcotRFunction.java @@ -0,0 +1,38 @@ +/* + 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.trigonometric; + +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; + +/** + * Returns the arc-co-tangent (in radians). + */ +@FunctionParameter(name = "value", nonZero = true) +public class AcotRFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: acot(x) = (pi / 2) - atan(x) */ + return expression.convertDoubleValue( + (Math.PI / 2) - Math.atan(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AsinFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AsinFunction.java new file mode 100644 index 000000000..63cd79e7f --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AsinFunction.java @@ -0,0 +1,52 @@ +/* + 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.trigonometric; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.math.BigDecimal; + +import static java.math.BigDecimal.ONE; + +/** + * Returns the arc-sine (in degrees). + */ +@FunctionParameter(name = "value") +public class AsinFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + + BigDecimal parameterValue = parameterValues[0].getNumberValue(); + + if (parameterValue.compareTo(ONE) > 0) { + throw new EvaluationException( + functionToken, "Illegal asin(x) for x > 1: x = " + parameterValue); + } + if (parameterValue.compareTo(MINUS_ONE) < 0) { + throw new EvaluationException( + functionToken, "Illegal asin(x) for x < -1: x = " + parameterValue); + } + return expression.convertDoubleValue(Math.toDegrees(Math.asin(parameterValue.doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AsinHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AsinHFunction.java new file mode 100644 index 000000000..5555c09b9 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AsinHFunction.java @@ -0,0 +1,38 @@ +/* + 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.trigonometric; + +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; + +/** + * Returns the hyperbolic arc-sine. + */ +@FunctionParameter(name = "value") +public class AsinHFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: asinh(x) = ln(x + sqrt(x^2 + 1)) */ + double value = parameterValues[0].getNumberValue().doubleValue(); + return expression.convertDoubleValue(Math.log(value + (Math.sqrt(Math.pow(value, 2) + 1)))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AsinRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AsinRFunction.java new file mode 100644 index 000000000..8fdfccda0 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AsinRFunction.java @@ -0,0 +1,56 @@ +/* + 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.trigonometric; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +import java.math.BigDecimal; + +import static java.math.BigDecimal.ONE; +import static java.math.BigDecimal.valueOf; + +/** + * Returns the arc-sine (in radians). + */ +@FunctionParameter(name = "value") +public class AsinRFunction extends AbstractFunction { + + private static final BigDecimal MINUS_ONE = valueOf(-1); + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + + BigDecimal parameterValue = parameterValues[0].getNumberValue(); + + if (parameterValue.compareTo(ONE) > 0) { + throw new EvaluationException( + functionToken, "Illegal asinr(x) for x > 1: x = " + parameterValue); + } + if (parameterValue.compareTo(MINUS_ONE) < 0) { + throw new EvaluationException( + functionToken, "Illegal asinr(x) for x < -1: x = " + parameterValue); + } + return expression.convertDoubleValue( + Math.asin(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/Atan2Function.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/Atan2Function.java new file mode 100644 index 000000000..43f1529d1 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/Atan2Function.java @@ -0,0 +1,41 @@ +/* + 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.trigonometric; + +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; + +/** + * Returns the angle of atan2 (in degrees). + */ +@FunctionParameter(name = "y") +@FunctionParameter(name = "x") +public class Atan2Function extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.toDegrees( + Math.atan2( + parameterValues[0].getNumberValue().doubleValue(), + parameterValues[1].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/Atan2RFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/Atan2RFunction.java new file mode 100644 index 000000000..6b17758c9 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/Atan2RFunction.java @@ -0,0 +1,40 @@ +/* + 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.trigonometric; + +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; + +/** + * Returns the angle of atan2 (in radians). + */ +@FunctionParameter(name = "y") +@FunctionParameter(name = "x") +public class Atan2RFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.atan2( + parameterValues[0].getNumberValue().doubleValue(), + parameterValues[1].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AtanFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AtanFunction.java new file mode 100644 index 000000000..0a4ee7ca1 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AtanFunction.java @@ -0,0 +1,37 @@ +/* + 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.trigonometric; + +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; + +/** + * Returns the arc-tangent (in degrees). + */ +@FunctionParameter(name = "value") +public class AtanFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.toDegrees(Math.atan(parameterValues[0].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AtanHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AtanHFunction.java new file mode 100644 index 000000000..344733235 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AtanHFunction.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.trigonometric; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Returns the hyperbolic arc-sine. + */ +@FunctionParameter(name = "value") +public class AtanHFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) + throws EvaluationException { + + /* Formula: atanh(x) = 0.5*ln((1 + x)/(1 - x)) */ + double value = parameterValues[0].getNumberValue().doubleValue(); + if (Math.abs(value) >= 1) { + throw new EvaluationException(functionToken, "Absolute value must be less than 1"); + } + return expression.convertDoubleValue(0.5 * Math.log((1 + value) / (1 - value))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/AtanRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/AtanRFunction.java new file mode 100644 index 000000000..67927c54a --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/AtanRFunction.java @@ -0,0 +1,37 @@ +/* + 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.trigonometric; + +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; + +/** + * Returns the arc-tangent (in radians). + */ +@FunctionParameter(name = "value") +public class AtanRFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.atan(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/CosFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/CosFunction.java new file mode 100644 index 000000000..7282f55c0 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/CosFunction.java @@ -0,0 +1,37 @@ +/* + 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.trigonometric; + +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; + +/** + * Returns the trigonometric cosine of an angle (in degrees). + */ +@FunctionParameter(name = "value") +public class CosFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.cos(Math.toRadians(parameterValues[0].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/CosHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/CosHFunction.java new file mode 100644 index 000000000..ac0176fc5 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/CosHFunction.java @@ -0,0 +1,37 @@ +/* + 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.trigonometric; + +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; + +/** + * Returns the hyperbolic cosine of a value. + */ +@FunctionParameter(name = "value") +public class CosHFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.cosh(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/CosRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/CosRFunction.java new file mode 100644 index 000000000..6cf5916f9 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/CosRFunction.java @@ -0,0 +1,37 @@ +/* + 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.trigonometric; + +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; + +/** + * Returns the trigonometric cosine of an angle (in radians). + */ +@FunctionParameter(name = "value") +public class CosRFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.cos(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/CotFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/CotFunction.java new file mode 100644 index 000000000..a75697242 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/CotFunction.java @@ -0,0 +1,38 @@ +/* + 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.trigonometric; + +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; + +/** + * Returns the co-tangent of an angle (in degrees). + */ +@FunctionParameter(name = "value", nonZero = true) +public class CotFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: cot(x) = cos(x) / sin(x) = 1 / tan(x) */ + return expression.convertDoubleValue( + 1 / Math.tan(Math.toRadians(parameterValues[0].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/CotHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/CotHFunction.java new file mode 100644 index 000000000..269ba570c --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/CotHFunction.java @@ -0,0 +1,38 @@ +/* + 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.trigonometric; + +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; + +/** + * Returns the hyperbolic co-tangent of a value. + */ +@FunctionParameter(name = "value", nonZero = true) +public class CotHFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: coth(x) = 1 / tanh(x) */ + return expression.convertDoubleValue( + 1 / Math.tanh(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/CotRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/CotRFunction.java new file mode 100644 index 000000000..b63619558 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/CotRFunction.java @@ -0,0 +1,38 @@ +/* + 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.trigonometric; + +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; + +/** + * Returns the trigonometric co-tangent of an angle (in radians). + */ +@FunctionParameter(name = "value", nonZero = true) +public class CotRFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: cot(x) = cos(x) / sin(x) = 1 / tan(x) */ + return expression.convertDoubleValue( + 1 / Math.tan(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/CscFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/CscFunction.java new file mode 100644 index 000000000..ffcef00c5 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/CscFunction.java @@ -0,0 +1,38 @@ +/* + 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.trigonometric; + +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; + +/** + * Returns the co-secant (in degrees). + */ +@FunctionParameter(name = "value", nonZero = true) +public class CscFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: csc(x) = 1 / sin(x) */ + return expression.convertDoubleValue( + 1 / Math.sin(Math.toRadians(parameterValues[0].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/CscHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/CscHFunction.java new file mode 100644 index 000000000..3f5ece892 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/CscHFunction.java @@ -0,0 +1,38 @@ +/* + 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.trigonometric; + +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; + +/** + * Returns the co-secant. + */ +@FunctionParameter(name = "value", nonZero = true) +public class CscHFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: csch(x) = 1 / sinh(x) */ + return expression.convertDoubleValue( + 1 / Math.sinh(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/CscRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/CscRFunction.java new file mode 100644 index 000000000..efeb3df57 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/CscRFunction.java @@ -0,0 +1,38 @@ +/* + 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.trigonometric; + +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; + +/** + * Returns the co-secant (in radians). + */ +@FunctionParameter(name = "value", nonZero = true) +public class CscRFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: csc(x) = 1 / sin(x) */ + return expression.convertDoubleValue( + 1 / Math.sin(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/DegFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/DegFunction.java new file mode 100644 index 000000000..52eecd888 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/DegFunction.java @@ -0,0 +1,38 @@ +/* + 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.trigonometric; + +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; + +/** + * Converts an angle measured in radians to an approximately equivalent angle measured in degrees. + */ +@FunctionParameter(name = "radians") +public class DegFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + double rad = Math.toDegrees(parameterValues[0].getNumberValue().doubleValue()); + + return expression.convertDoubleValue(rad); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/RadFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/RadFunction.java new file mode 100644 index 000000000..a0fb6ee94 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/RadFunction.java @@ -0,0 +1,38 @@ +/* + 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.trigonometric; + +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; + +/** + * Converts an angle measured in degrees to an approximately equivalent angle measured in radians. + */ +@FunctionParameter(name = "degrees") +public class RadFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + double deg = Math.toRadians(parameterValues[0].getNumberValue().doubleValue()); + + return expression.convertDoubleValue(deg); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/SecFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/SecFunction.java new file mode 100644 index 000000000..1c3035413 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/SecFunction.java @@ -0,0 +1,38 @@ +/* + 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.trigonometric; + +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; + +/** + * Returns the secant (in degrees). + */ +@FunctionParameter(name = "value", nonZero = true) +public class SecFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: sec(x) = 1 / cos(x) */ + return expression.convertDoubleValue( + 1 / Math.cos(Math.toRadians(parameterValues[0].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/SecHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/SecHFunction.java new file mode 100644 index 000000000..8f452fa11 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/SecHFunction.java @@ -0,0 +1,38 @@ +/* + 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.trigonometric; + +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; + +/** + * Returns the hyperbolic secant. + */ +@FunctionParameter(name = "value", nonZero = true) +public class SecHFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: sech(x) = 1 / cosh(x) */ + return expression.convertDoubleValue( + 1 / Math.cosh(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/SecRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/SecRFunction.java new file mode 100644 index 000000000..950d8dc04 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/SecRFunction.java @@ -0,0 +1,38 @@ +/* + 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.trigonometric; + +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; + +/** + * Returns the secant (in radians). + */ +@FunctionParameter(name = "value", nonZero = true) +public class SecRFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + /* Formula: sec(x) = 1 / cos(x) */ + return expression.convertDoubleValue( + 1 / Math.cos(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/SinFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/SinFunction.java new file mode 100644 index 000000000..7390426d8 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/SinFunction.java @@ -0,0 +1,37 @@ +/* + 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.trigonometric; + +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; + +/** + * Returns the trigonometric sine of an angle (in degrees). + */ +@FunctionParameter(name = "value") +public class SinFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.sin(Math.toRadians(parameterValues[0].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/SinHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/SinHFunction.java new file mode 100644 index 000000000..911501538 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/SinHFunction.java @@ -0,0 +1,37 @@ +/* + 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.trigonometric; + +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; + +/** + * Returns the hyperbolic sine of a value. + */ +@FunctionParameter(name = "value") +public class SinHFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.sinh(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/SinRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/SinRFunction.java new file mode 100644 index 000000000..b5b017095 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/SinRFunction.java @@ -0,0 +1,37 @@ +/* + 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.trigonometric; + +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; + +/** + * Returns the trigonometric sine of an angle (in radians). + */ +@FunctionParameter(name = "value") +public class SinRFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.sin(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/TanFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/TanFunction.java new file mode 100644 index 000000000..994efefec --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/TanFunction.java @@ -0,0 +1,37 @@ +/* + 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.trigonometric; + +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; + +/** + * Returns the trigonometric tangent of an angle (in degrees). + */ +@FunctionParameter(name = "value") +public class TanFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.tan(Math.toRadians(parameterValues[0].getNumberValue().doubleValue()))); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/TanHFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/TanHFunction.java new file mode 100644 index 000000000..aad6232c8 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/TanHFunction.java @@ -0,0 +1,37 @@ +/* + 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.trigonometric; + +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; + +/** + * Returns the hyperbolic tangent of a value. + */ +@FunctionParameter(name = "value") +public class TanHFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.tanh(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/trigonometric/TanRFunction.java b/src/main/java/com/ezylang/evalex/functions/trigonometric/TanRFunction.java new file mode 100644 index 000000000..e82a4d85e --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/trigonometric/TanRFunction.java @@ -0,0 +1,37 @@ +/* + 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.trigonometric; + +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; + +/** + * Returns the trigonometric tangent of an angle (in radians). + */ +@FunctionParameter(name = "value") +public class TanRFunction extends AbstractFunction { + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + + return expression.convertDoubleValue( + Math.tan(parameterValues[0].getNumberValue().doubleValue())); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/AbstractOperator.java b/src/main/java/com/ezylang/evalex/operators/AbstractOperator.java new file mode 100644 index 000000000..1aeaad18c --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/AbstractOperator.java @@ -0,0 +1,94 @@ +/* + 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.operators; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import lombok.Getter; + +import static com.ezylang.evalex.operators.OperatorIfc.OperatorType.PREFIX_OPERATOR; + +/** + * Abstract implementation of the {@link OperatorIfc}, used as base class for operator + * implementations. + */ +public abstract class AbstractOperator implements OperatorIfc { + + @Getter private final int precedence; + + private final boolean leftAssociative; + + private final boolean operandsLazy; + + OperatorType type; + + /** + * Creates a new operator and uses the {@link InfixOperator} annotation to create the operator + * definition. + */ + protected AbstractOperator() { + InfixOperator infixAnnotation = getClass().getAnnotation(InfixOperator.class); + PrefixOperator prefixAnnotation = getClass().getAnnotation(PrefixOperator.class); + PostfixOperator postfixAnnotation = getClass().getAnnotation(PostfixOperator.class); + if (infixAnnotation != null) { + this.type = OperatorType.INFIX_OPERATOR; + this.precedence = infixAnnotation.precedence(); + this.leftAssociative = infixAnnotation.leftAssociative(); + this.operandsLazy = infixAnnotation.operandsLazy(); + } else if (prefixAnnotation != null) { + this.type = PREFIX_OPERATOR; + this.precedence = prefixAnnotation.precedence(); + this.leftAssociative = prefixAnnotation.leftAssociative(); + this.operandsLazy = false; + } else if (postfixAnnotation != null) { + this.type = OperatorType.POSTFIX_OPERATOR; + this.precedence = postfixAnnotation.precedence(); + this.leftAssociative = postfixAnnotation.leftAssociative(); + this.operandsLazy = false; + } else { + throw new OperatorAnnotationNotFoundException(this.getClass().getName()); + } + } + + @Override + public int getPrecedence(ExpressionConfiguration configuration) { + return getPrecedence(); + } + + @Override + public boolean isLeftAssociative() { + return leftAssociative; + } + + @Override + public boolean isOperandLazy() { + return operandsLazy; + } + + @Override + public boolean isPrefix() { + return type == PREFIX_OPERATOR; + } + + @Override + public boolean isPostfix() { + return type == OperatorType.POSTFIX_OPERATOR; + } + + @Override + public boolean isInfix() { + return type == OperatorType.INFIX_OPERATOR; + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/InfixOperator.java b/src/main/java/com/ezylang/evalex/operators/InfixOperator.java new file mode 100644 index 000000000..261c4f34b --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/InfixOperator.java @@ -0,0 +1,46 @@ +/* + 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.operators; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The infix operator annotation + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface InfixOperator { + + /** + * Operator precedence, usually one from the constants in {@link OperatorIfc}. + */ + int precedence(); + + /** + * Operator associativity, defaults to true. + */ + boolean leftAssociative() default true; + + /** + * Operands are evaluated lazily, defaults to false. + */ + boolean operandsLazy() default false; +} diff --git a/src/main/java/com/ezylang/evalex/operators/OperatorAnnotationNotFoundException.java b/src/main/java/com/ezylang/evalex/operators/OperatorAnnotationNotFoundException.java new file mode 100644 index 000000000..b241cff07 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/OperatorAnnotationNotFoundException.java @@ -0,0 +1,27 @@ +/* + 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.operators; + +/** + * Operator properties are defined through a class annotation, this exception is thrown if no + * annotation was found when creating the operator instance. + */ +public class OperatorAnnotationNotFoundException extends RuntimeException { + + public OperatorAnnotationNotFoundException(String className) { + super("Operator annotation for '" + className + "' not found"); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/OperatorIfc.java b/src/main/java/com/ezylang/evalex/operators/OperatorIfc.java new file mode 100644 index 000000000..ba1289c3b --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/OperatorIfc.java @@ -0,0 +1,157 @@ +/* + 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.operators; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.parser.Token; + +/** + * Interface that is required for all operators in an operator dictionary for evaluation of + * expressions. There are three operator type: prefix, postfix and infix. Every operator has a + * precedence, which defines the order of operator evaluation. The associativity of an operator is a + * property that determines how operators of the same precedence are grouped in the absence of + * parentheses. + */ +public interface OperatorIfc { + + /** + * The operator type. + */ + enum OperatorType { + /** + * Unary prefix operator, like -x + */ + PREFIX_OPERATOR, + /** + * Unary postfix operator,like x! + */ + POSTFIX_OPERATOR, + /** + * Binary infix operator, like x+y + */ + INFIX_OPERATOR + } + + /** + * Or operator precedence: || + */ + int OPERATOR_PRECEDENCE_OR = 2; + + /** + * And operator precedence: && + */ + int OPERATOR_PRECEDENCE_AND = 4; + + /** + * Equality operators precedence: =, ==, !=, <> + */ + int OPERATOR_PRECEDENCE_EQUALITY = 7; + + /** + * Comparative operators precedence: <, >, <=, >= + */ + int OPERATOR_PRECEDENCE_COMPARISON = 10; + + /** + * Additive operators precedence: + and - + */ + int OPERATOR_PRECEDENCE_ADDITIVE = 20; + + /** + * Multiplicative operators precedence: *, /, % + */ + int OPERATOR_PRECEDENCE_MULTIPLICATIVE = 30; + + /** + * Power operator precedence: ^ + */ + int OPERATOR_PRECEDENCE_POWER = 40; + + /** + * Unary operators precedence: + and - as prefix + */ + int OPERATOR_PRECEDENCE_UNARY = 60; + + /** + * An optional higher power operator precedence, higher than the unary prefix, e.g. -2^2 equals to + * 4 or -4, depending on precedence configuration. + */ + int OPERATOR_PRECEDENCE_POWER_HIGHER = 80; + + /** + * @return The operator's precedence. + */ + int getPrecedence(); + + /** + * If operators with same precedence are evaluated from left to right. + * + * @return The associativity. + */ + boolean isLeftAssociative(); + + /** + * If it is a prefix operator. + * + * @return true if it is a prefix operator. + */ + boolean isPrefix(); + + /** + * If it is a postfix operator. + * + * @return true if it is a postfix operator. + */ + boolean isPostfix(); + + /** + * If it is an infix operator. + * + * @return true if it is an infix operator. + */ + boolean isInfix(); + + /** + * Called during parsing, can be implemented to return a customized precedence. + * + * @param configuration The expression configuration. + * @return The default precedence from the operator annotation, or a customized value. + */ + int getPrecedence(ExpressionConfiguration configuration); + + /** + * Checks if the operand is lazy. + * + * @return true if operands are defined as lazy. + */ + boolean isOperandLazy(); + + /** + * Performs the operator logic and returns an evaluation result. + * + * @param expression The expression, where this function is executed. Can be used to access the + * expression configuration. + * @param operatorToken The operator token from the parsed expression. + * @param operands The operands, one for prefix and postfix operators, two for infix operators. + * @return The evaluation result in form of a {@link EvaluationValue}. + * @throws EvaluationException In case there were problems during evaluation. + */ + EvaluationValue evaluate(Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException; +} diff --git a/src/main/java/com/ezylang/evalex/operators/PostfixOperator.java b/src/main/java/com/ezylang/evalex/operators/PostfixOperator.java new file mode 100644 index 000000000..12619335b --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/PostfixOperator.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.operators; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_UNARY; + +/** + * The postfix operator annotation + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface PostfixOperator { + + /** + * Operator precedence, usually one from the constants in {@link OperatorIfc}. + */ + int precedence() default OPERATOR_PRECEDENCE_UNARY; + + /** + * Operator associativity, defaults to true. + */ + boolean leftAssociative() default true; +} diff --git a/src/main/java/com/ezylang/evalex/operators/PrefixOperator.java b/src/main/java/com/ezylang/evalex/operators/PrefixOperator.java new file mode 100644 index 000000000..41d2c296d --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/PrefixOperator.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.operators; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_UNARY; + +/** + * The prefix operator annotation + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface PrefixOperator { + + /** + * Operator precedence, usually one from the constants in {@link OperatorIfc}. + */ + int precedence() default OPERATOR_PRECEDENCE_UNARY; + + /** + * Operator associativity, defaults to true. + */ + boolean leftAssociative() default true; +} diff --git a/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixDivisionOperator.java b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixDivisionOperator.java new file mode 100644 index 000000000..dee428702 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixDivisionOperator.java @@ -0,0 +1,57 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.arithmetic; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +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.math.BigDecimal; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_MULTIPLICATIVE; + +/** + * Division of two numbers. + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_MULTIPLICATIVE) +public class InfixDivisionOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException { + EvaluationValue leftOperand = operands[0]; + EvaluationValue rightOperand = operands[1]; + + if (leftOperand.isNumberValue() && rightOperand.isNumberValue()) { + + if (rightOperand.getNumberValue().equals(BigDecimal.ZERO)) { + throw new EvaluationException(operatorToken, "Division by zero"); + } + + return expression.convertValue( + leftOperand + .getNumberValue() + .divide( + rightOperand.getNumberValue(), expression.getConfiguration().getMathContext())); + } else { + throw EvaluationException.ofUnsupportedDataTypeInOperation(operatorToken); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixMinusOperator.java b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixMinusOperator.java new file mode 100644 index 000000000..8a1769030 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixMinusOperator.java @@ -0,0 +1,70 @@ +/* + 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.operators.arithmetic; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +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; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_ADDITIVE; + +/** + * Subtraction of two numbers. + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_ADDITIVE) +public class InfixMinusOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException { + EvaluationValue leftOperand = operands[0]; + EvaluationValue rightOperand = operands[1]; + + if (leftOperand.isNumberValue() && rightOperand.isNumberValue()) { + return expression.convertValue( + leftOperand + .getNumberValue() + .subtract( + rightOperand.getNumberValue(), expression.getConfiguration().getMathContext())); + + } else if (leftOperand.isDateTimeValue() && rightOperand.isDateTimeValue()) { + return expression.convertValue( + Duration.ofMillis( + leftOperand.getDateTimeValue().toEpochMilli() + - rightOperand.getDateTimeValue().toEpochMilli())); + + } else if (leftOperand.isDateTimeValue() && rightOperand.isDurationValue()) { + return expression.convertValue( + leftOperand.getDateTimeValue().minus(rightOperand.getDurationValue())); + } else if (leftOperand.isDurationValue() && rightOperand.isDurationValue()) { + return expression.convertValue( + leftOperand.getDurationValue().minus(rightOperand.getDurationValue())); + } else if (leftOperand.isDateTimeValue() && rightOperand.isNumberValue()) { + return expression.convertValue( + leftOperand + .getDateTimeValue() + .minus(Duration.ofMillis(rightOperand.getNumberValue().longValue()))); + } else { + throw EvaluationException.ofUnsupportedDataTypeInOperation(operatorToken); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixModuloOperator.java b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixModuloOperator.java new file mode 100644 index 000000000..b3e85df8d --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixModuloOperator.java @@ -0,0 +1,57 @@ +/* + Copyright 2012-2022 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.operators.arithmetic; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +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.math.BigDecimal; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_MULTIPLICATIVE; + +/** + * Remainder (modulo) of two numbers. + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_MULTIPLICATIVE) +public class InfixModuloOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException { + EvaluationValue leftOperand = operands[0]; + EvaluationValue rightOperand = operands[1]; + + if (leftOperand.isNumberValue() && rightOperand.isNumberValue()) { + + if (rightOperand.getNumberValue().equals(BigDecimal.ZERO)) { + throw new EvaluationException(operatorToken, "Division by zero"); + } + + return expression.convertValue( + leftOperand + .getNumberValue() + .remainder( + rightOperand.getNumberValue(), expression.getConfiguration().getMathContext())); + } else { + throw EvaluationException.ofUnsupportedDataTypeInOperation(operatorToken); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixMultiplicationOperator.java b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixMultiplicationOperator.java new file mode 100644 index 000000000..ed4675c69 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixMultiplicationOperator.java @@ -0,0 +1,50 @@ +/* + 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.operators.arithmetic; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +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 static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_MULTIPLICATIVE; + +/** + * Multiplication of two numbers. + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_MULTIPLICATIVE) +public class InfixMultiplicationOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException { + EvaluationValue leftOperand = operands[0]; + EvaluationValue rightOperand = operands[1]; + + if (leftOperand.isNumberValue() && rightOperand.isNumberValue()) { + return expression.convertValue( + leftOperand + .getNumberValue() + .multiply( + rightOperand.getNumberValue(), expression.getConfiguration().getMathContext())); + } 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 new file mode 100644 index 000000000..e9cc974bb --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixPlusOperator.java @@ -0,0 +1,60 @@ +/* + 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.operators.arithmetic; + +import com.ezylang.evalex.Expression; +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; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_ADDITIVE; + +/** + * Addition of numbers and strings. If one operand is a string, a string concatenation is performed. + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_ADDITIVE) +public class InfixPlusOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + EvaluationValue leftOperand = operands[0]; + EvaluationValue rightOperand = operands[1]; + + if (leftOperand.isNumberValue() && rightOperand.isNumberValue()) { + return expression.convertValue( + leftOperand + .getNumberValue() + .add(rightOperand.getNumberValue(), expression.getConfiguration().getMathContext())); + } else if (leftOperand.isDateTimeValue() && rightOperand.isDurationValue()) { + return expression.convertValue( + leftOperand.getDateTimeValue().plus(rightOperand.getDurationValue())); + } else if (leftOperand.isDurationValue() && rightOperand.isDurationValue()) { + return expression.convertValue( + leftOperand.getDurationValue().plus(rightOperand.getDurationValue())); + } else if (leftOperand.isDateTimeValue() && rightOperand.isNumberValue()) { + return expression.convertValue( + leftOperand + .getDateTimeValue() + .plus(Duration.ofMillis(rightOperand.getNumberValue().longValue()))); + } else { + return expression.convertValue(leftOperand.getStringValue() + rightOperand.getStringValue()); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixPowerOfOperator.java b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixPowerOfOperator.java new file mode 100644 index 000000000..d7d768a40 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/arithmetic/InfixPowerOfOperator.java @@ -0,0 +1,80 @@ +/* + 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.operators.arithmetic; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.config.ExpressionConfiguration; +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.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; + +import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_POWER; + +/** + * Power of operator, calculates the power of right operand of left operand. The precedence is read + * from the configuration during parsing. + * + * @see #getPrecedence(ExpressionConfiguration) + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_POWER, leftAssociative = false) +public class InfixPowerOfOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException { + EvaluationValue leftOperand = operands[0]; + EvaluationValue rightOperand = operands[1]; + + if (leftOperand.isNumberValue() && rightOperand.isNumberValue()) { + /*- + * Thanks to Gene Marin: + * http://stackoverflow.com/questions/3579779/how-to-do-a-fractional-power-on-bigdecimal-in-java + */ + + MathContext mathContext = expression.getConfiguration().getMathContext(); + BigDecimal v1 = leftOperand.getNumberValue(); + BigDecimal v2 = rightOperand.getNumberValue(); + + int signOf2 = v2.signum(); + double dn1 = v1.doubleValue(); + v2 = v2.multiply(new BigDecimal(signOf2)); // n2 is now positive + BigDecimal remainderOf2 = v2.remainder(BigDecimal.ONE); + BigDecimal n2IntPart = v2.subtract(remainderOf2); + BigDecimal intPow = v1.pow(n2IntPart.intValueExact(), mathContext); + BigDecimal doublePow = BigDecimal.valueOf(Math.pow(dn1, remainderOf2.doubleValue())); + + BigDecimal result = intPow.multiply(doublePow, mathContext); + if (signOf2 == -1) { + result = BigDecimal.ONE.divide(result, mathContext.getPrecision(), RoundingMode.HALF_UP); + } + return expression.convertValue(result); + } else { + throw EvaluationException.ofUnsupportedDataTypeInOperation(operatorToken); + } + } + + @Override + public int getPrecedence(ExpressionConfiguration configuration) { + return configuration.getPowerOfPrecedence(); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/arithmetic/PrefixMinusOperator.java b/src/main/java/com/ezylang/evalex/operators/arithmetic/PrefixMinusOperator.java new file mode 100644 index 000000000..18edbb79e --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/arithmetic/PrefixMinusOperator.java @@ -0,0 +1,44 @@ +/* + 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.operators.arithmetic; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.PrefixOperator; +import com.ezylang.evalex.parser.Token; + +/** + * Unary prefix minus. + */ +@PrefixOperator(leftAssociative = false) +public class PrefixMinusOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException { + EvaluationValue operand = operands[0]; + + if (operand.isNumberValue()) { + return expression.convertValue( + operand.getNumberValue().negate(expression.getConfiguration().getMathContext())); + } else { + throw EvaluationException.ofUnsupportedDataTypeInOperation(operatorToken); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/arithmetic/PrefixPlusOperator.java b/src/main/java/com/ezylang/evalex/operators/arithmetic/PrefixPlusOperator.java new file mode 100644 index 000000000..0534afc76 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/arithmetic/PrefixPlusOperator.java @@ -0,0 +1,44 @@ +/* + 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.operators.arithmetic; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.PrefixOperator; +import com.ezylang.evalex.parser.Token; + +/** + * Unary prefix plus. + */ +@PrefixOperator(leftAssociative = false) +public class PrefixPlusOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException { + EvaluationValue operator = operands[0]; + + if (operator.isNumberValue()) { + return expression.convertValue( + operator.getNumberValue().plus(expression.getConfiguration().getMathContext())); + } else { + throw EvaluationException.ofUnsupportedDataTypeInOperation(operatorToken); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/InfixAndOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/InfixAndOperator.java new file mode 100644 index 000000000..7369df47d --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/booleans/InfixAndOperator.java @@ -0,0 +1,41 @@ +/* + 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.operators.booleans; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +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 static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_AND; + +/** + * Boolean AND of two values. + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_AND, operandsLazy = true) +public class InfixAndOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException { + return expression.convertValue( + expression.evaluateSubtree(operands[0].getExpressionNode()).getBooleanValue() + && expression.evaluateSubtree(operands[1].getExpressionNode()).getBooleanValue()); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/InfixEqualsOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/InfixEqualsOperator.java new file mode 100644 index 000000000..3943958d4 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/booleans/InfixEqualsOperator.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.operators.booleans; + +import com.ezylang.evalex.Expression; +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 static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_EQUALITY; + +/** + * Equality of two values. + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_EQUALITY) +public class InfixEqualsOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + if (operands[0].getDataType() != operands[1].getDataType()) { + return EvaluationValue.FALSE; + } + if (operands[0].isNullValue() && operands[1].isNullValue()) { + return EvaluationValue.TRUE; + } + return expression.convertValue(operands[0].compareTo(operands[1]) == 0); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/InfixGreaterEqualsOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/InfixGreaterEqualsOperator.java new file mode 100644 index 000000000..98efb053a --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/booleans/InfixGreaterEqualsOperator.java @@ -0,0 +1,37 @@ +/* + 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.operators.booleans; + +import com.ezylang.evalex.Expression; +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 static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_COMPARISON; + +/** + * Greater or equals of two values. + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_COMPARISON) +public class InfixGreaterEqualsOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + return expression.convertValue(operands[0].compareTo(operands[1]) >= 0); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/InfixGreaterOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/InfixGreaterOperator.java new file mode 100644 index 000000000..b35da474f --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/booleans/InfixGreaterOperator.java @@ -0,0 +1,37 @@ +/* + 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.operators.booleans; + +import com.ezylang.evalex.Expression; +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 static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_COMPARISON; + +/** + * Greater of two values. + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_COMPARISON) +public class InfixGreaterOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + return expression.convertValue(operands[0].compareTo(operands[1]) > 0); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/InfixLessEqualsOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/InfixLessEqualsOperator.java new file mode 100644 index 000000000..993f94b46 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/booleans/InfixLessEqualsOperator.java @@ -0,0 +1,37 @@ +/* + 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.operators.booleans; + +import com.ezylang.evalex.Expression; +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 static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_COMPARISON; + +/** + * Less or equals of two values. + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_COMPARISON) +public class InfixLessEqualsOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + return expression.convertValue(operands[0].compareTo(operands[1]) <= 0); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/InfixLessOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/InfixLessOperator.java new file mode 100644 index 000000000..e77e4b8bc --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/booleans/InfixLessOperator.java @@ -0,0 +1,37 @@ +/* + 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.operators.booleans; + +import com.ezylang.evalex.Expression; +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 static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_COMPARISON; + +/** + * Less of two values. + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_COMPARISON) +public class InfixLessOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + return expression.convertValue(operands[0].compareTo(operands[1]) < 0); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/InfixNotEqualsOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/InfixNotEqualsOperator.java new file mode 100644 index 000000000..9d0a9e054 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/booleans/InfixNotEqualsOperator.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.operators.booleans; + +import com.ezylang.evalex.Expression; +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 static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_EQUALITY; + +/** + * No equality of two values. + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_EQUALITY) +public class InfixNotEqualsOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + if (operands[0].getDataType() != operands[1].getDataType()) { + return EvaluationValue.TRUE; + } + if (operands[0].isNullValue() && operands[1].isNullValue()) { + return EvaluationValue.FALSE; + } + return expression.convertValue(operands[0].compareTo(operands[1]) != 0); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/InfixOrOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/InfixOrOperator.java new file mode 100644 index 000000000..cd6b58370 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/booleans/InfixOrOperator.java @@ -0,0 +1,41 @@ +/* + 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.operators.booleans; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +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 static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_OR; + +/** + * Boolean OR of two values. + */ +@InfixOperator(precedence = OPERATOR_PRECEDENCE_OR, operandsLazy = true) +public class InfixOrOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException { + return expression.convertValue( + expression.evaluateSubtree(operands[0].getExpressionNode()).getBooleanValue() + || expression.evaluateSubtree(operands[1].getExpressionNode()).getBooleanValue()); + } +} diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/PrefixNotOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/PrefixNotOperator.java new file mode 100644 index 000000000..8aa990e76 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/operators/booleans/PrefixNotOperator.java @@ -0,0 +1,35 @@ +/* + 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.operators.booleans; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.operators.AbstractOperator; +import com.ezylang.evalex.operators.PrefixOperator; +import com.ezylang.evalex.parser.Token; + +/** + * Boolean negation of value. + */ +@PrefixOperator +public class PrefixNotOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) { + return expression.convertValue(!operands[0].getBooleanValue()); + } +} diff --git a/src/main/java/com/ezylang/evalex/parser/ASTNode.java b/src/main/java/com/ezylang/evalex/parser/ASTNode.java new file mode 100644 index 000000000..f1ccf417e --- /dev/null +++ b/src/main/java/com/ezylang/evalex/parser/ASTNode.java @@ -0,0 +1,73 @@ +/* + 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.parser; + +import lombok.Value; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Expressions are parsed into an abstract syntax tree (AST). The tree has one root node and each + * node has zero or more children (parameters), depending on the operation. A leaf node is a + * numerical or string constant that has no more children (parameters). Other nodes define + * operators, functions and special operations like array index and structure separation. + * + *

The tree is evaluated from bottom (leafs) to top, in a recursive way, until the root node is + * evaluated, which then holds the result of the complete expression. + * + *

To be able to visualize the tree, a toJSON method is provided. The produced JSON + * string can be used to visualize the tree. OE.g. with this online tool: + * + *

Online JSON to Tree Diagram Converter + */ +@Value +public class ASTNode { + + /** + * The children od the tree. + */ + List parameters; + + /** + * The token associated with this tree node. + */ + Token token; + + public ASTNode(Token token, ASTNode... parameters) { + this.token = token; + this.parameters = Arrays.asList(parameters); + } + + /** + * Produces a JSON string representation of this node ad all its children. + * + * @return A JSON string of the tree structure starting at this node. + */ + public String toJSON() { + if (parameters.isEmpty()) { + return String.format( + "{" + "\"type\":\"%s\",\"value\":\"%s\"}", token.getType(), token.getValue()); + } else { + String childrenJson = + parameters.stream().map(ASTNode::toJSON).collect(Collectors.joining(",")); + return String.format( + "{" + "\"type\":\"%s\",\"value\":\"%s\",\"children\":[%s]}", + token.getType(), token.getValue(), childrenJson); + } + } +} diff --git a/src/main/java/com/ezylang/evalex/parser/ParseException.java b/src/main/java/com/ezylang/evalex/parser/ParseException.java new file mode 100644 index 000000000..f7ff219a4 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/parser/ParseException.java @@ -0,0 +1,42 @@ +/* + 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.parser; + +import com.ezylang.evalex.BaseException; +import lombok.ToString; + +/** + * Exception while parsing the expression. + */ +@ToString(callSuper = true) +public class ParseException extends BaseException { + + public ParseException(int startPosition, int endPosition, String tokenString, String message) { + super(startPosition, endPosition, tokenString, message); + } + + public ParseException(String expression, String message) { + super(1, expression.length(), expression, message); + } + + public ParseException(Token token, String message) { + super( + token.getStartPosition(), + token.getStartPosition() + token.getValue().length() - 1, + token.getValue(), + message); + } +} diff --git a/src/main/java/com/ezylang/evalex/parser/ShuntingYardConverter.java b/src/main/java/com/ezylang/evalex/parser/ShuntingYardConverter.java new file mode 100644 index 000000000..0ceac8ddf --- /dev/null +++ b/src/main/java/com/ezylang/evalex/parser/ShuntingYardConverter.java @@ -0,0 +1,292 @@ +/* + 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.parser; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.functions.FunctionIfc; +import com.ezylang.evalex.operators.OperatorIfc; +import com.ezylang.evalex.parser.Token.TokenType; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; + +import static com.ezylang.evalex.parser.Token.TokenType.ARRAY_INDEX; +import static com.ezylang.evalex.parser.Token.TokenType.ARRAY_OPEN; +import static com.ezylang.evalex.parser.Token.TokenType.BRACE_OPEN; +import static com.ezylang.evalex.parser.Token.TokenType.FUNCTION; +import static com.ezylang.evalex.parser.Token.TokenType.STRUCTURE_SEPARATOR; + +/** + * The shunting yard algorithm can be used to convert a mathematical expression from an infix + * notation into either a postfix notation (RPN, reverse polish notation), or into an abstract + * syntax tree (AST). + * + *

Here it is used to parse and convert a list of already parsed expression tokens into an AST. + * + * @see Shunting yard algorithm + * @see Abstract syntax tree + */ +public class ShuntingYardConverter { + + private final List expressionTokens; + + private final String originalExpression; + + private final ExpressionConfiguration configuration; + + private final Deque operatorStack = new ArrayDeque<>(); + private final Deque operandStack = new ArrayDeque<>(); + + public ShuntingYardConverter( + String originalExpression, + List expressionTokens, + ExpressionConfiguration configuration) { + this.originalExpression = originalExpression; + this.expressionTokens = expressionTokens; + this.configuration = configuration; + } + + public ASTNode toAbstractSyntaxTree() throws ParseException { + + Token previousToken = null; + for (Token currentToken : expressionTokens) { + switch (currentToken.getType()) { + case VARIABLE_OR_CONSTANT: + case NUMBER_LITERAL: + case STRING_LITERAL: + operandStack.push(new ASTNode(currentToken)); + break; + case FUNCTION: + operatorStack.push(currentToken); + break; + case COMMA: + processOperatorsFromStackUntilTokenType(BRACE_OPEN); + break; + case INFIX_OPERATOR: + case PREFIX_OPERATOR: + case POSTFIX_OPERATOR: + processOperator(currentToken); + break; + case BRACE_OPEN: + processBraceOpen(previousToken, currentToken); + break; + case BRACE_CLOSE: + processBraceClose(); + break; + case ARRAY_OPEN: + processArrayOpen(currentToken); + break; + case ARRAY_CLOSE: + processArrayClose(); + break; + case STRUCTURE_SEPARATOR: + processStructureSeparator(currentToken); + break; + default: + throw new ParseException( + currentToken, "Unexpected token of type '" + currentToken.getType() + "'"); + } + previousToken = currentToken; + } + + while (!operatorStack.isEmpty()) { + Token token = operatorStack.pop(); + createOperatorNode(token); + } + + if (operandStack.isEmpty()) { + throw new ParseException(this.originalExpression, "Empty expression"); + } + + if (operandStack.size() > 1) { + throw new ParseException(this.originalExpression, "Too many operands"); + } + + return operandStack.pop(); + } + + private void processStructureSeparator(Token currentToken) throws ParseException { + Token nextToken = operatorStack.isEmpty() ? null : operatorStack.peek(); + while (nextToken != null && nextToken.getType() == STRUCTURE_SEPARATOR) { + Token token = operatorStack.pop(); + createOperatorNode(token); + nextToken = operatorStack.peek(); + } + operatorStack.push(currentToken); + } + + private void processBraceOpen(Token previousToken, Token currentToken) { + if (previousToken != null && previousToken.getType() == FUNCTION) { + // start of parameter list, marker for variable number of arguments + Token paramStart = + new Token( + currentToken.getStartPosition(), + currentToken.getValue(), + TokenType.FUNCTION_PARAM_START); + operandStack.push(new ASTNode(paramStart)); + } + operatorStack.push(currentToken); + } + + private void processBraceClose() throws ParseException { + processOperatorsFromStackUntilTokenType(BRACE_OPEN); + operatorStack.pop(); // throw away the marker + if (!operatorStack.isEmpty() && operatorStack.peek().getType() == FUNCTION) { + Token functionToken = operatorStack.pop(); + ArrayList parameters = new ArrayList<>(); + while (true) { + // add all parameters in reverse order from stack to the parameter array + ASTNode node = operandStack.pop(); + if (node.getToken().getType() == TokenType.FUNCTION_PARAM_START) { + break; + } + parameters.add(0, node); + } + validateFunctionParameters(functionToken, parameters); + operandStack.push(new ASTNode(functionToken, parameters.toArray(new ASTNode[0]))); + } + } + + private void validateFunctionParameters(Token functionToken, ArrayList parameters) + throws ParseException { + FunctionIfc function = functionToken.getFunctionDefinition(); + if (parameters.size() < function.getCountOfNonVarArgParameters()) { + throw new ParseException(functionToken, "Not enough parameters for function"); + } + if (!function.hasVarArgs() + && parameters.size() > function.getFunctionParameterDefinitions().size()) { + throw new ParseException(functionToken, "Too many parameters for function"); + } + } + + /** + * Array index is treated like a function with two parameters. First parameter is the array (name + * or evaluation result). Second parameter is the array index. + * + * @param currentToken The current ARRAY_OPEN ("[") token. + */ + private void processArrayOpen(Token currentToken) throws ParseException { + Token nextToken = operatorStack.isEmpty() ? null : operatorStack.peek(); + while (nextToken != null && (nextToken.getType() == STRUCTURE_SEPARATOR)) { + Token token = operatorStack.pop(); + createOperatorNode(token); + nextToken = operatorStack.isEmpty() ? null : operatorStack.peek(); + } + // create ARRAY_INDEX operator (just like a function name) and push it to the operator stack + Token arrayIndex = + new Token(currentToken.getStartPosition(), currentToken.getValue(), ARRAY_INDEX); + operatorStack.push(arrayIndex); + + // push the ARRAY_OPEN to the operators, too (to later match the ARRAY_CLOSE) + operatorStack.push(currentToken); + } + + /** + * Follows the logic for a function, but with two fixed parameters. + * + * @throws ParseException If there were problems while processing the stacks. + */ + private void processArrayClose() throws ParseException { + processOperatorsFromStackUntilTokenType(ARRAY_OPEN); + operatorStack.pop(); // throw away the marker + Token arrayToken = operatorStack.pop(); + ArrayList operands = new ArrayList<>(); + + // second parameter of the "ARRAY_INDEX" function is the index (first on stack) + ASTNode index = operandStack.pop(); + operands.add(0, index); + + // first parameter of the "ARRAY_INDEX" function is the array (name or evaluation result) + // (second on stack) + ASTNode array = operandStack.pop(); + operands.add(0, array); + + operandStack.push(new ASTNode(arrayToken, operands.toArray(new ASTNode[0]))); + } + + private void processOperatorsFromStackUntilTokenType(TokenType untilTokenType) + throws ParseException { + while (!operatorStack.isEmpty() && operatorStack.peek().getType() != untilTokenType) { + Token token = operatorStack.pop(); + createOperatorNode(token); + } + } + + private void createOperatorNode(Token token) throws ParseException { + if (operandStack.isEmpty()) { + throw new ParseException(token, "Missing operand for operator"); + } + + ASTNode operand1 = operandStack.pop(); + + if (token.getType() == TokenType.PREFIX_OPERATOR + || token.getType() == TokenType.POSTFIX_OPERATOR) { + operandStack.push(new ASTNode(token, operand1)); + } else { + if (operandStack.isEmpty()) { + throw new ParseException(token, "Missing second operand for operator"); + } + ASTNode operand2 = operandStack.pop(); + operandStack.push(new ASTNode(token, operand2, operand1)); + } + } + + private void processOperator(Token currentToken) throws ParseException { + Token nextToken = operatorStack.isEmpty() ? null : operatorStack.peek(); + while (isOperator(nextToken) + && isNextOperatorOfHigherPrecedence( + currentToken.getOperatorDefinition(), nextToken.getOperatorDefinition())) { + Token token = operatorStack.pop(); + createOperatorNode(token); + nextToken = operatorStack.isEmpty() ? null : operatorStack.peek(); + } + operatorStack.push(currentToken); + } + + private boolean isNextOperatorOfHigherPrecedence( + OperatorIfc currentOperator, OperatorIfc nextOperator) { + // structure operator (null) has always a higher precedence than other operators + if (nextOperator == null) { + return true; + } + + if (currentOperator.isLeftAssociative()) { + return currentOperator.getPrecedence(configuration) + <= nextOperator.getPrecedence(configuration); + } else { + return currentOperator.getPrecedence(configuration) + < nextOperator.getPrecedence(configuration); + } + } + + private boolean isOperator(Token token) { + if (token == null) { + return false; + } + TokenType tokenType = token.getType(); + switch (tokenType) { + case INFIX_OPERATOR: + case PREFIX_OPERATOR: + case POSTFIX_OPERATOR: + case STRUCTURE_SEPARATOR: + return true; + default: + return false; + } + } +} diff --git a/src/main/java/com/ezylang/evalex/parser/Token.java b/src/main/java/com/ezylang/evalex/parser/Token.java new file mode 100644 index 000000000..cf4b776d2 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/parser/Token.java @@ -0,0 +1,80 @@ +/* + 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.parser; + +import com.ezylang.evalex.functions.FunctionIfc; +import com.ezylang.evalex.operators.OperatorIfc; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import lombok.Value; + +/** + * A token represents a single part of an expression, like an operator, number literal, or a brace. + * Each token has a unique type, a value (its representation) and a position (starting with 1) in + * the original expression string. + * + *

For operators and functions, the operator and function definition is also set during parsing. + */ +@Value +@AllArgsConstructor +@EqualsAndHashCode(doNotUseGetters = true) +public class Token { + + public enum TokenType { + BRACE_OPEN, + BRACE_CLOSE, + COMMA, + STRING_LITERAL, + NUMBER_LITERAL, + VARIABLE_OR_CONSTANT, + INFIX_OPERATOR, + PREFIX_OPERATOR, + POSTFIX_OPERATOR, + FUNCTION, + FUNCTION_PARAM_START, + ARRAY_OPEN, + ARRAY_CLOSE, + ARRAY_INDEX, + STRUCTURE_SEPARATOR + } + + int startPosition; + + String value; + + TokenType type; + + @EqualsAndHashCode.Exclude + @ToString.Exclude + FunctionIfc functionDefinition; + + @EqualsAndHashCode.Exclude + @ToString.Exclude + OperatorIfc operatorDefinition; + + public Token(int startPosition, String value, TokenType type) { + this(startPosition, value, type, null, null); + } + + public Token(int startPosition, String value, TokenType type, FunctionIfc functionDefinition) { + this(startPosition, value, type, functionDefinition, null); + } + + public Token(int startPosition, String value, TokenType type, OperatorIfc operatorDefinition) { + this(startPosition, value, type, null, operatorDefinition); + } +} diff --git a/src/main/java/com/ezylang/evalex/parser/Tokenizer.java b/src/main/java/com/ezylang/evalex/parser/Tokenizer.java new file mode 100644 index 000000000..5f2c08824 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/parser/Tokenizer.java @@ -0,0 +1,628 @@ +/* + 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.parser; + +import com.ezylang.evalex.config.ExpressionConfiguration; +import com.ezylang.evalex.config.FunctionDictionaryIfc; +import com.ezylang.evalex.config.OperatorDictionaryIfc; +import com.ezylang.evalex.functions.FunctionIfc; +import com.ezylang.evalex.operators.OperatorIfc; +import com.ezylang.evalex.parser.Token.TokenType; + +import java.util.ArrayList; +import java.util.List; + +import static com.ezylang.evalex.parser.Token.TokenType.BRACE_CLOSE; +import static com.ezylang.evalex.parser.Token.TokenType.BRACE_OPEN; +import static com.ezylang.evalex.parser.Token.TokenType.FUNCTION; +import static com.ezylang.evalex.parser.Token.TokenType.INFIX_OPERATOR; +import static com.ezylang.evalex.parser.Token.TokenType.NUMBER_LITERAL; +import static com.ezylang.evalex.parser.Token.TokenType.STRUCTURE_SEPARATOR; +import static com.ezylang.evalex.parser.Token.TokenType.VARIABLE_OR_CONSTANT; + +/** + * The tokenizer is responsible to parse a string and return a list of tokens. The order of tokens + * will follow the infix expression notation, skipping any blank characters. + */ +public class Tokenizer { + + private final String expressionString; + + private final OperatorDictionaryIfc operatorDictionary; + + private final FunctionDictionaryIfc functionDictionary; + + private final ExpressionConfiguration configuration; + + private final List tokens = new ArrayList<>(); + + private int currentColumnIndex = 0; + + private int currentChar = -2; + + private int braceBalance; + + private int arrayBalance; + + public Tokenizer(String expressionString, ExpressionConfiguration configuration) { + this.expressionString = expressionString; + this.configuration = configuration; + this.operatorDictionary = configuration.getOperatorDictionary(); + this.functionDictionary = configuration.getFunctionDictionary(); + } + + /** + * Parse the given expression and return a list of tokens, representing the expression. + * + * @return A list of expression tokens. + * @throws ParseException When the expression can't be parsed. + */ + public List parse() throws ParseException { + Token currentToken = getNextToken(); + while (currentToken != null) { + if (implicitMultiplicationPossible(currentToken)) { + if (configuration.isImplicitMultiplicationAllowed()) { + Token multiplication = + new Token( + currentToken.getStartPosition(), + "*", + TokenType.INFIX_OPERATOR, + operatorDictionary.getInfixOperator("*")); + tokens.add(multiplication); + } else { + throw new ParseException(currentToken, "Missing operator"); + } + } + validateToken(currentToken); + tokens.add(currentToken); + currentToken = getNextToken(); + } + + if (braceBalance > 0) { + throw new ParseException(expressionString, "Closing brace not found"); + } + + if (arrayBalance > 0) { + throw new ParseException(expressionString, "Closing array not found"); + } + + return tokens; + } + + private boolean implicitMultiplicationPossible(Token currentToken) { + Token previousToken = getPreviousToken(); + + if (previousToken == null) { + return false; + } + + return ((previousToken.getType() == BRACE_CLOSE && currentToken.getType() == BRACE_OPEN) + || (previousToken.getType() == NUMBER_LITERAL + && currentToken.getType() == VARIABLE_OR_CONSTANT) + || (previousToken.getType() == NUMBER_LITERAL && currentToken.getType() == FUNCTION) + || (previousToken.getType() == NUMBER_LITERAL && currentToken.getType() == BRACE_OPEN)); + } + + private void validateToken(Token currentToken) throws ParseException { + + if (currentToken.getType() == STRUCTURE_SEPARATOR && getPreviousToken() == null) { + throw new ParseException(currentToken, "Misplaced structure operator"); + } + + Token previousToken = getPreviousToken(); + if (previousToken != null + && previousToken.getType() == INFIX_OPERATOR + && invalidTokenAfterInfixOperator(currentToken)) { + throw new ParseException(currentToken, "Unexpected token after infix operator"); + } + } + + private boolean invalidTokenAfterInfixOperator(Token token) { + switch (token.getType()) { + case INFIX_OPERATOR: + case BRACE_CLOSE: + case COMMA: + return true; + default: + return false; + } + } + + private Token getNextToken() throws ParseException { + + // blanks are always skipped. + skipBlanks(); + + // end of input + if (currentChar == -1) { + return null; + } + + // we have a token start, identify and parse it + if (isAtStringLiteralStart()) { + return parseStringLiteral(); + } else if (currentChar == '(') { + return parseBraceOpen(); + } else if (currentChar == ')') { + return parseBraceClose(); + } else if (currentChar == '[' && configuration.isArraysAllowed()) { + return parseArrayOpen(); + } else if (currentChar == ']' && configuration.isArraysAllowed()) { + return parseArrayClose(); + } else if (currentChar == '.' + && !isNextCharNumberChar() + && configuration.isStructuresAllowed()) { + return parseStructureSeparator(); + } else if (currentChar == ',') { + Token token = new Token(currentColumnIndex, ",", TokenType.COMMA); + consumeChar(); + return token; + } else if (isAtIdentifierStart()) { + return parseIdentifier(); + } else if (isAtNumberStart()) { + return parseNumberLiteral(); + } else { + return parseOperator(); + } + } + + private Token parseStructureSeparator() throws ParseException { + Token token = new Token(currentColumnIndex, ".", TokenType.STRUCTURE_SEPARATOR); + if (arrayOpenOrStructureSeparatorNotAllowed()) { + throw new ParseException(token, "Structure separator not allowed here"); + } + consumeChar(); + return token; + } + + private Token parseArrayClose() throws ParseException { + Token token = new Token(currentColumnIndex, "]", TokenType.ARRAY_CLOSE); + if (!arrayCloseAllowed()) { + throw new ParseException(token, "Array close not allowed here"); + } + consumeChar(); + arrayBalance--; + if (arrayBalance < 0) { + throw new ParseException(token, "Unexpected closing array"); + } + return token; + } + + private Token parseArrayOpen() throws ParseException { + Token token = new Token(currentColumnIndex, "[", TokenType.ARRAY_OPEN); + if (arrayOpenOrStructureSeparatorNotAllowed()) { + throw new ParseException(token, "Array open not allowed here"); + } + consumeChar(); + arrayBalance++; + return token; + } + + private Token parseBraceClose() throws ParseException { + Token token = new Token(currentColumnIndex, ")", TokenType.BRACE_CLOSE); + consumeChar(); + braceBalance--; + if (braceBalance < 0) { + throw new ParseException(token, "Unexpected closing brace"); + } + return token; + } + + private Token parseBraceOpen() { + Token token = new Token(currentColumnIndex, "(", BRACE_OPEN); + consumeChar(); + braceBalance++; + return token; + } + + private Token getPreviousToken() { + return tokens.isEmpty() ? null : tokens.get(tokens.size() - 1); + } + + private Token parseOperator() throws ParseException { + int tokenStartIndex = currentColumnIndex; + StringBuilder tokenValue = new StringBuilder(); + while (true) { + tokenValue.append((char) currentChar); + String tokenString = tokenValue.toString(); + String possibleNextOperator = tokenString + (char) peekNextChar(); + boolean possibleNextOperatorFound = + (prefixOperatorAllowed() && operatorDictionary.hasPrefixOperator(possibleNextOperator)) + || (postfixOperatorAllowed() + && operatorDictionary.hasPostfixOperator(possibleNextOperator)) + || (infixOperatorAllowed() + && operatorDictionary.hasInfixOperator(possibleNextOperator)); + consumeChar(); + if (!possibleNextOperatorFound) { + break; + } + } + String tokenString = tokenValue.toString(); + if (prefixOperatorAllowed() && operatorDictionary.hasPrefixOperator(tokenString)) { + OperatorIfc operator = operatorDictionary.getPrefixOperator(tokenString); + return new Token(tokenStartIndex, tokenString, TokenType.PREFIX_OPERATOR, operator); + } else if (postfixOperatorAllowed() && operatorDictionary.hasPostfixOperator(tokenString)) { + OperatorIfc operator = operatorDictionary.getPostfixOperator(tokenString); + return new Token(tokenStartIndex, tokenString, TokenType.POSTFIX_OPERATOR, operator); + } else if (operatorDictionary.hasInfixOperator(tokenString)) { + OperatorIfc operator = operatorDictionary.getInfixOperator(tokenString); + return new Token(tokenStartIndex, tokenString, TokenType.INFIX_OPERATOR, operator); + } else if (tokenString.equals(".") && configuration.isStructuresAllowed()) { + return new Token(tokenStartIndex, tokenString, STRUCTURE_SEPARATOR); + } + throw new ParseException( + tokenStartIndex, + tokenStartIndex + tokenString.length() - 1, + tokenString, + "Undefined operator '" + tokenString + "'"); + } + + private boolean arrayOpenOrStructureSeparatorNotAllowed() { + Token previousToken = getPreviousToken(); + + if (previousToken == null) { + return true; + } + + switch (previousToken.getType()) { + case BRACE_CLOSE: + case VARIABLE_OR_CONSTANT: + case ARRAY_CLOSE: + case STRING_LITERAL: + return false; + default: + return true; + } + } + + private boolean arrayCloseAllowed() { + Token previousToken = getPreviousToken(); + + if (previousToken == null) { + return false; + } + + switch (previousToken.getType()) { + case BRACE_OPEN: + case INFIX_OPERATOR: + case PREFIX_OPERATOR: + case FUNCTION: + case COMMA: + case ARRAY_OPEN: + return false; + default: + return true; + } + } + + private boolean prefixOperatorAllowed() { + Token previousToken = getPreviousToken(); + + if (previousToken == null) { + return true; + } + + switch (previousToken.getType()) { + case BRACE_OPEN: + case INFIX_OPERATOR: + case COMMA: + case PREFIX_OPERATOR: + case ARRAY_OPEN: + return true; + default: + return false; + } + } + + private boolean postfixOperatorAllowed() { + Token previousToken = getPreviousToken(); + + if (previousToken == null) { + return false; + } + + switch (previousToken.getType()) { + case BRACE_CLOSE: + case NUMBER_LITERAL: + case VARIABLE_OR_CONSTANT: + case STRING_LITERAL: + return true; + default: + return false; + } + } + + private boolean infixOperatorAllowed() { + Token previousToken = getPreviousToken(); + + if (previousToken == null) { + return false; + } + + switch (previousToken.getType()) { + case BRACE_CLOSE: + case VARIABLE_OR_CONSTANT: + case STRING_LITERAL: + case POSTFIX_OPERATOR: + case NUMBER_LITERAL: + case ARRAY_CLOSE: + return true; + default: + return false; + } + } + + private Token parseNumberLiteral() throws ParseException { + int nextChar = peekNextChar(); + if (currentChar == '0' && (nextChar == 'x' || nextChar == 'X')) { + return parseHexNumberLiteral(); + } else { + return parseDecimalNumberLiteral(); + } + } + + private Token parseDecimalNumberLiteral() throws ParseException { + int tokenStartIndex = currentColumnIndex; + StringBuilder tokenValue = new StringBuilder(); + + int lastChar = -1; + boolean scientificNotation = false; + boolean dotEncountered = false; + while (currentChar != -1 && isAtNumberChar()) { + if (currentChar == '.' && dotEncountered) { + tokenValue.append((char) currentChar); + throw new ParseException( + new Token(tokenStartIndex, tokenValue.toString(), TokenType.NUMBER_LITERAL), + "Number contains more than one decimal point"); + } + if (currentChar == '.') { + dotEncountered = true; + } + if (currentChar == 'e' || currentChar == 'E') { + scientificNotation = true; + } + tokenValue.append((char) currentChar); + lastChar = currentChar; + consumeChar(); + } + // illegal scientific format literal + if (scientificNotation + && (lastChar == 'e' + || lastChar == 'E' + || lastChar == '+' + || lastChar == '-' + || lastChar == '.')) { + throw new ParseException( + new Token(tokenStartIndex, tokenValue.toString(), TokenType.NUMBER_LITERAL), + "Illegal scientific format"); + } + return new Token(tokenStartIndex, tokenValue.toString(), TokenType.NUMBER_LITERAL); + } + + private Token parseHexNumberLiteral() { + int tokenStartIndex = currentColumnIndex; + StringBuilder tokenValue = new StringBuilder(); + + // hexadecimal number, consume "0x" + tokenValue.append((char) currentChar); + consumeChar(); + do { + tokenValue.append((char) currentChar); + consumeChar(); + } while (currentChar != -1 && isAtHexChar()); + return new Token(tokenStartIndex, tokenValue.toString(), TokenType.NUMBER_LITERAL); + } + + private Token parseIdentifier() throws ParseException { + int tokenStartIndex = currentColumnIndex; + StringBuilder tokenValue = new StringBuilder(); + while (currentChar != -1 && isAtIdentifierChar()) { + tokenValue.append((char) currentChar); + consumeChar(); + } + String tokenName = tokenValue.toString(); + + if (prefixOperatorAllowed() && operatorDictionary.hasPrefixOperator(tokenName)) { + return new Token( + tokenStartIndex, + tokenName, + TokenType.PREFIX_OPERATOR, + operatorDictionary.getPrefixOperator(tokenName)); + } else if (postfixOperatorAllowed() && operatorDictionary.hasPostfixOperator(tokenName)) { + return new Token( + tokenStartIndex, + tokenName, + TokenType.POSTFIX_OPERATOR, + operatorDictionary.getPostfixOperator(tokenName)); + } else if (operatorDictionary.hasInfixOperator(tokenName)) { + return new Token( + tokenStartIndex, + tokenName, + TokenType.INFIX_OPERATOR, + operatorDictionary.getInfixOperator(tokenName)); + } + + skipBlanks(); + if (currentChar == '(') { + if (!functionDictionary.hasFunction(tokenName)) { + throw new ParseException( + tokenStartIndex, + currentColumnIndex, + tokenName, + "Undefined function '" + tokenName + "'"); + } + FunctionIfc function = functionDictionary.getFunction(tokenName); + return new Token(tokenStartIndex, tokenName, TokenType.FUNCTION, function); + } else { + return new Token(tokenStartIndex, tokenName, TokenType.VARIABLE_OR_CONSTANT); + } + } + + Token parseStringLiteral() throws ParseException { + int startChar = currentChar; + int tokenStartIndex = currentColumnIndex; + StringBuilder tokenValue = new StringBuilder(); + // skip starting quote + consumeChar(); + boolean inQuote = true; + while (inQuote && currentChar != -1) { + if (currentChar == '\\') { + consumeChar(); + tokenValue.append(escapeCharacter(currentChar)); + } else if (currentChar == startChar) { + inQuote = false; + } else { + tokenValue.append((char) currentChar); + } + consumeChar(); + } + if (inQuote) { + throw new ParseException( + tokenStartIndex, currentColumnIndex, tokenValue.toString(), "Closing quote not found"); + } + return new Token(tokenStartIndex, tokenValue.toString(), TokenType.STRING_LITERAL); + } + + private char escapeCharacter(int character) throws ParseException { + switch (character) { + case '\'': + return '\''; + case '"': + return '"'; + case '\\': + return '\\'; + case 'n': + return '\n'; + case 'r': + return '\r'; + case 't': + return '\t'; + case 'b': + return '\b'; + case 'f': + return '\f'; + default: + throw new ParseException( + currentColumnIndex, 1, "\\" + (char) character, "Unknown escape character"); + } + } + + private boolean isAtNumberStart() { + if (Character.isDigit(currentChar)) { + return true; + } + return currentChar == '.' && Character.isDigit(peekNextChar()); + } + + private boolean isAtNumberChar() { + int previousChar = peekPreviousChar(); + + if ((previousChar == 'e' || previousChar == 'E') && currentChar != '.') { + return Character.isDigit(currentChar) || currentChar == '+' || currentChar == '-'; + } + + if (previousChar == '.' && currentChar != '.') { + return Character.isDigit(currentChar) || currentChar == 'e' || currentChar == 'E'; + } + + return Character.isDigit(currentChar) + || currentChar == '.' + || currentChar == 'e' + || currentChar == 'E'; + } + + private boolean isNextCharNumberChar() { + if (peekNextChar() == -1) { + return false; + } + consumeChar(); + boolean isAtNumber = isAtNumberChar(); + currentColumnIndex--; + currentChar = expressionString.charAt(currentColumnIndex - 1); + return isAtNumber; + } + + private boolean isAtHexChar() { + switch (currentChar) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case 'a': + case 'b': + case 'c': + case 'd': + case 'e': + case 'f': + case 'A': + case 'B': + case 'C': + case 'D': + case 'E': + case 'F': + return true; + default: + return false; + } + } + + private boolean isAtIdentifierStart() { + return Character.isLetter(currentChar) || currentChar == '_'; + } + + private boolean isAtIdentifierChar() { + return Character.isLetter(currentChar) || Character.isDigit(currentChar) || currentChar == '_'; + } + + private boolean isAtStringLiteralStart() { + return currentChar == '"' + || currentChar == '\'' && configuration.isSingleQuoteStringLiteralsAllowed(); + } + + private void skipBlanks() { + if (currentChar == -2) { + // consume first character of expression + consumeChar(); + } + while (currentChar != -1 && Character.isWhitespace(currentChar)) { + consumeChar(); + } + } + + private int peekNextChar() { + return currentColumnIndex == expressionString.length() + ? -1 + : expressionString.charAt(currentColumnIndex); + } + + private int peekPreviousChar() { + return currentColumnIndex == 1 ? -1 : expressionString.charAt(currentColumnIndex - 2); + } + + private void consumeChar() { + if (currentColumnIndex == expressionString.length()) { + currentChar = -1; + } else { + currentChar = expressionString.charAt(currentColumnIndex++); + } + } +} From e0b8433fa0bb0d94c3e70bf2efa5b56f1fca0097 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sat, 1 Nov 2025 22:29:20 +0100 Subject: [PATCH 2/6] . --- dependencies.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 4265f2912..6045d89d2 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -23,7 +23,6 @@ */ dependencies { - embed2 'org.mariuszgromada.math:MathParser.org-mXparser:6.1.0' embed2('org.joml:joml:1.10.8') { transitive = false } embedSources("org.joml:joml:1.10.8:sources") { transitive = false } From 206d9897f8305b971d65ca0f16d6a3c778e4e145 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sun, 2 Nov 2025 10:22:53 +0100 Subject: [PATCH 3/6] fix postfix op overwriting infix op --- .../com/ezylang/evalex/parser/Tokenizer.java | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/ezylang/evalex/parser/Tokenizer.java b/src/main/java/com/ezylang/evalex/parser/Tokenizer.java index 5f2c08824..b9d24e6a6 100644 --- a/src/main/java/com/ezylang/evalex/parser/Tokenizer.java +++ b/src/main/java/com/ezylang/evalex/parser/Tokenizer.java @@ -30,6 +30,7 @@ import static com.ezylang.evalex.parser.Token.TokenType.FUNCTION; import static com.ezylang.evalex.parser.Token.TokenType.INFIX_OPERATOR; import static com.ezylang.evalex.parser.Token.TokenType.NUMBER_LITERAL; +import static com.ezylang.evalex.parser.Token.TokenType.POSTFIX_OPERATOR; import static com.ezylang.evalex.parser.Token.TokenType.STRUCTURE_SEPARATOR; import static com.ezylang.evalex.parser.Token.TokenType.VARIABLE_OR_CONSTANT; @@ -86,6 +87,7 @@ public List parse() throws ParseException { throw new ParseException(currentToken, "Missing operator"); } } + isInfixInsteadOfPostfix(currentToken); validateToken(currentToken); tokens.add(currentToken); currentToken = getNextToken(); @@ -116,6 +118,17 @@ private boolean implicitMultiplicationPossible(Token currentToken) { || (previousToken.getType() == NUMBER_LITERAL && currentToken.getType() == BRACE_OPEN)); } + private void isInfixInsteadOfPostfix(Token currentToken) { + if (currentToken == null || invalidTokenAfterInfixOperator(currentToken)) return; + Token previousToken = getPreviousToken(); + if (previousToken == null || previousToken.getType() != POSTFIX_OPERATOR) return; + String opString = previousToken.getValue(); + if (operatorDictionary.hasInfixOperator(opString)) { + OperatorIfc op = operatorDictionary.getInfixOperator(opString); + setPreviousToken(new Token(previousToken.getStartPosition(), opString, INFIX_OPERATOR, op)); + } + } + private void validateToken(Token currentToken) throws ParseException { if (currentToken.getType() == STRUCTURE_SEPARATOR && getPreviousToken() == null) { @@ -133,7 +146,9 @@ && invalidTokenAfterInfixOperator(currentToken)) { private boolean invalidTokenAfterInfixOperator(Token token) { switch (token.getType()) { case INFIX_OPERATOR: + case POSTFIX_OPERATOR: case BRACE_CLOSE: + case ARRAY_CLOSE: case COMMA: return true; default: @@ -232,18 +247,27 @@ private Token getPreviousToken() { return tokens.isEmpty() ? null : tokens.get(tokens.size() - 1); } + private void setPreviousToken(Token token) { + if (!tokens.isEmpty()) { + tokens.set(tokens.size() - 1, token); + } + } + private Token parseOperator() throws ParseException { int tokenStartIndex = currentColumnIndex; StringBuilder tokenValue = new StringBuilder(); + boolean prefixAllowed = prefixOperatorAllowed(); + boolean postfixAllowed = postfixOperatorAllowed(); + boolean infixAllowed = infixOperatorAllowed(); while (true) { tokenValue.append((char) currentChar); String tokenString = tokenValue.toString(); String possibleNextOperator = tokenString + (char) peekNextChar(); boolean possibleNextOperatorFound = - (prefixOperatorAllowed() && operatorDictionary.hasPrefixOperator(possibleNextOperator)) - || (postfixOperatorAllowed() + (prefixAllowed && operatorDictionary.hasPrefixOperator(possibleNextOperator)) + || (postfixAllowed && operatorDictionary.hasPostfixOperator(possibleNextOperator)) - || (infixOperatorAllowed() + || (infixAllowed && operatorDictionary.hasInfixOperator(possibleNextOperator)); consumeChar(); if (!possibleNextOperatorFound) { @@ -251,10 +275,10 @@ private Token parseOperator() throws ParseException { } } String tokenString = tokenValue.toString(); - if (prefixOperatorAllowed() && operatorDictionary.hasPrefixOperator(tokenString)) { + if (prefixAllowed && operatorDictionary.hasPrefixOperator(tokenString)) { OperatorIfc operator = operatorDictionary.getPrefixOperator(tokenString); return new Token(tokenStartIndex, tokenString, TokenType.PREFIX_OPERATOR, operator); - } else if (postfixOperatorAllowed() && operatorDictionary.hasPostfixOperator(tokenString)) { + } else if (postfixAllowed && operatorDictionary.hasPostfixOperator(tokenString)) { OperatorIfc operator = operatorDictionary.getPostfixOperator(tokenString); return new Token(tokenStartIndex, tokenString, TokenType.POSTFIX_OPERATOR, operator); } else if (operatorDictionary.hasInfixOperator(tokenString)) { From 8efe4931cfcdd11a2e2dc4832224669f643f5f31 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sun, 2 Nov 2025 10:26:30 +0100 Subject: [PATCH 4/6] use X instead of E for exa si prefix to avoid variable clashing --- src/main/java/com/cleanroommc/modularui/utils/SIPrefix.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/cleanroommc/modularui/utils/SIPrefix.java b/src/main/java/com/cleanroommc/modularui/utils/SIPrefix.java index 02c198b78..d531709a6 100644 --- a/src/main/java/com/cleanroommc/modularui/utils/SIPrefix.java +++ b/src/main/java/com/cleanroommc/modularui/utils/SIPrefix.java @@ -8,7 +8,7 @@ public enum SIPrefix { Ronna('R', 27), Yotta('Y', 24), Zetta('Z', 21), - Exa('E', 18), + Exa('X', 18), // this should actually be E, but this clashes with euler's number e = 2.71... Peta('P', 15), Tera('T', 12), Giga('G', 9), From 635dd94c3246973dc808e1c7e7b70e6d50f60575 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sun, 2 Nov 2025 18:37:01 +0100 Subject: [PATCH 5/6] some big decimal things --- .../modularui/utils/NumberFormat.java | 31 ++++++++++++++++++- .../cleanroommc/modularui/utils/SIPrefix.java | 14 +++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/cleanroommc/modularui/utils/NumberFormat.java b/src/main/java/com/cleanroommc/modularui/utils/NumberFormat.java index 36860a56b..fc6bcdfbc 100644 --- a/src/main/java/com/cleanroommc/modularui/utils/NumberFormat.java +++ b/src/main/java/com/cleanroommc/modularui/utils/NumberFormat.java @@ -1,5 +1,6 @@ package com.cleanroommc.modularui.utils; +import java.math.BigDecimal; import java.math.RoundingMode; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; @@ -10,6 +11,8 @@ */ public class NumberFormat { + public static final BigDecimal TEN_THOUSAND = new BigDecimal(10_000); + public static final Params DEFAULT = paramsBuilder() .roundingMode(RoundingMode.HALF_UP) .maxLength(4) @@ -205,9 +208,35 @@ public static SIPrefix findBestPrefix(double number) { return prefix; } + public static SIPrefix findBestPrefix(BigDecimal number) { + if (number.compareTo(BigDecimal.ONE) >= 0 && number.compareTo(TEN_THOUSAND) < 0) return SIPrefix.One; + SIPrefix[] high = SIPrefix.HIGH; + SIPrefix[] low = SIPrefix.LOW; + int n = high.length - 1; + SIPrefix prefix; + if (number.compareTo(TEN_THOUSAND) >= 0) { + int index; + for (index = 0; index < n; index++) { + if (number.compareTo(high[index + 1].bigFactor) < 0) { + break; + } + } + prefix = high[index]; + } else { + int index; + for (index = 0; index < n; index++) { + if (number.compareTo(low[index].bigFactor) >= 0) { + break; + } + } + prefix = low[index]; + } + return prefix; + } + private static String formatInternal(double number, int maxLength, Params params) { SIPrefix prefix = findBestPrefix(number); - return formatToString(number * prefix.oneOverFactor, prefix.symbol, maxLength, params); + return formatToString(number * prefix.oneOverFactor, prefix.getCharSymbol(), maxLength, params); } private static String formatToString(double value, char prefix, int maxLength, Params params) { diff --git a/src/main/java/com/cleanroommc/modularui/utils/SIPrefix.java b/src/main/java/com/cleanroommc/modularui/utils/SIPrefix.java index d531709a6..6520d3517 100644 --- a/src/main/java/com/cleanroommc/modularui/utils/SIPrefix.java +++ b/src/main/java/com/cleanroommc/modularui/utils/SIPrefix.java @@ -2,6 +2,8 @@ import com.ezylang.evalex.Expression; +import java.math.BigDecimal; + public enum SIPrefix { Quetta('Q', 30), @@ -27,14 +29,22 @@ public enum SIPrefix { Quecto('q', -30); - public final char symbol; + public final String symbol; public final double factor; public final double oneOverFactor; + public final BigDecimal bigFactor; + public final BigDecimal bigOneOverFactor; SIPrefix(char symbol, int powerOfTen) { - this.symbol = symbol; + this.symbol = String.valueOf(symbol); this.factor = Math.pow(10, powerOfTen); this.oneOverFactor = 1 / this.factor; + this.bigFactor = new BigDecimal(this.factor); + this.bigOneOverFactor = new BigDecimal(this.oneOverFactor); + } + + public char getCharSymbol() { + return symbol.charAt(0); } public boolean isOne() { From 67156fa17af61792c55a94c2ec6997139ab1c997 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sun, 4 Jan 2026 11:25:02 +0100 Subject: [PATCH 6/6] fix --- src/main/java/com/cleanroommc/modularui/utils/NumberFormat.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/cleanroommc/modularui/utils/NumberFormat.java b/src/main/java/com/cleanroommc/modularui/utils/NumberFormat.java index fc6bcdfbc..e61d728ee 100644 --- a/src/main/java/com/cleanroommc/modularui/utils/NumberFormat.java +++ b/src/main/java/com/cleanroommc/modularui/utils/NumberFormat.java @@ -236,7 +236,7 @@ public static SIPrefix findBestPrefix(BigDecimal number) { private static String formatInternal(double number, int maxLength, Params params) { SIPrefix prefix = findBestPrefix(number); - return formatToString(number * prefix.oneOverFactor, prefix.getCharSymbol(), maxLength, params); + return formatToString(number * prefix.oneOverFactor, prefix.symbol, maxLength, params); } private static String formatToString(double value, char prefix, int maxLength, Params params) {