diff --git a/src/main/java/org/unlaxer/tinyexpression/AbstractCalculationContext.java b/src/main/java/org/unlaxer/tinyexpression/AbstractCalculationContext.java index d59e72d5..c4f89657 100644 --- a/src/main/java/org/unlaxer/tinyexpression/AbstractCalculationContext.java +++ b/src/main/java/org/unlaxer/tinyexpression/AbstractCalculationContext.java @@ -63,8 +63,14 @@ public void set(String name,float value) { */ @Override public Optional getValue(String name) { - - return Optional.ofNullable((Float)valueByName.get(name)); + Number value = valueByName.get(name); + if (value == null) { + return Optional.empty(); + } + if (value instanceof Float f) { + return Optional.of(f); + } + return Optional.of(value.floatValue()); } /* (non-Javadoc) @@ -183,7 +189,10 @@ public boolean inDayTimeRange( boolean withinTime = false; if (fromDayInclusive.getValue() == toDayInclusive.getValue()) { - withinTime = nowHour >= fromDayHourInclusive && nowHour < toDayHourExclusive; + boolean spansMidnight = fromDayHourInclusive > toDayHourExclusive; + withinTime = spansMidnight + ? (nowHour >= fromDayHourInclusive) || (nowHour < toDayHourExclusive) + : (nowHour >= fromDayHourInclusive) && (nowHour < toDayHourExclusive); } else if (fromDayInclusive.getValue() == nowDayOfWeek) { withinTime = nowHour >= fromDayHourInclusive; } else if (toDayInclusive.getValue() == nowDayOfWeek) { diff --git a/src/main/java/org/unlaxer/tinyexpression/evaluator/ast/P4TypedAstEvaluator.java b/src/main/java/org/unlaxer/tinyexpression/evaluator/ast/P4TypedAstEvaluator.java index bb4e54dd..bb102102 100644 --- a/src/main/java/org/unlaxer/tinyexpression/evaluator/ast/P4TypedAstEvaluator.java +++ b/src/main/java/org/unlaxer/tinyexpression/evaluator/ast/P4TypedAstEvaluator.java @@ -1433,9 +1433,11 @@ private static int normalizeIndex(int index, int len) { protected Object evalToNumExpr(ToNumExpr node) { Object strVal = eval(node.value()); try { - return Double.parseDouble(String.valueOf(strVal)); + return castToNumberType(Double.parseDouble(String.valueOf(strVal))); } catch (NumberFormatException e) { - return ((Number) eval(node.defaultValue())).doubleValue(); + Object defaultValue = eval(node.defaultValue()); + double dv = (defaultValue instanceof Number n) ? n.doubleValue() : 0.0; + return castToNumberType(dv); } } diff --git a/src/test/java/org/unlaxer/tinyexpression/Issue90GetValueCastTest.java b/src/test/java/org/unlaxer/tinyexpression/Issue90GetValueCastTest.java new file mode 100644 index 00000000..9b818f59 --- /dev/null +++ b/src/test/java/org/unlaxer/tinyexpression/Issue90GetValueCastTest.java @@ -0,0 +1,57 @@ +package org.unlaxer.tinyexpression; + +import static org.junit.Assert.assertEquals; + +import java.math.BigInteger; +import java.util.Optional; + +import org.junit.Test; + +/** + * Regression for tinyexpression#90: + * {@code set(String, Number)} stores arbitrary {@link Number} subtypes into + * {@code valueByName}, but {@code getValue(String)} cast the stored value to + * {@link Float} unconditionally, causing {@link ClassCastException} for + * {@code Double}/{@code Integer}/{@code BigInteger} values. + */ +public class Issue90GetValueCastTest { + + @Test + public void getValueAfterSetDoubleDoesNotThrow() { + CalculationContext ctx = CalculationContext.newConcurrentContext(); + ctx.set("x", Double.valueOf(3.14)); + Optional v = ctx.getValue("x"); + assertEquals(3.14f, v.get(), 0.001f); + } + + @Test + public void getValueAfterSetIntegerDoesNotThrow() { + CalculationContext ctx = CalculationContext.newConcurrentContext(); + ctx.set("n", Integer.valueOf(42)); + Optional v = ctx.getValue("n"); + assertEquals(42f, v.get(), 0.001f); + } + + @Test + public void getValueAfterSetBigIntegerDoesNotThrow() { + CalculationContext ctx = CalculationContext.newConcurrentContext(); + ctx.set("b", BigInteger.valueOf(7)); + Optional v = ctx.getValue("b"); + assertEquals(7f, v.get(), 0.001f); + } + + @Test + public void getValueAfterSetFloatPreservesExactValue() { + CalculationContext ctx = CalculationContext.newConcurrentContext(); + ctx.set("f", 1.25f); + Optional v = ctx.getValue("f"); + assertEquals(1.25f, v.get(), 0.0f); + } + + @Test + public void getValueForAbsentReturnsEmpty() { + CalculationContext ctx = CalculationContext.newConcurrentContext(); + Optional v = ctx.getValue("absent"); + assertEquals(Optional.empty(), v); + } +} diff --git a/src/test/java/org/unlaxer/tinyexpression/Issue91ToNumTest.java b/src/test/java/org/unlaxer/tinyexpression/Issue91ToNumTest.java new file mode 100644 index 00000000..569cf23b --- /dev/null +++ b/src/test/java/org/unlaxer/tinyexpression/Issue91ToNumTest.java @@ -0,0 +1,81 @@ +package org.unlaxer.tinyexpression; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.unlaxer.tinyexpression.evaluator.javacode.SpecifiedExpressionTypes; +import org.unlaxer.tinyexpression.loader.model.CalculatorCreatorRegistry; +import org.unlaxer.tinyexpression.parser.ExpressionTypes; + +/** + * Regression for tinyexpression#91: + *
    + *
  • {@code toNum('abc', 'xyz')} with a non-{@link Number} default value + * threw {@link ClassCastException} via {@code (Number) eval(...)}.
  • + *
  • {@code toNum(...)} returned {@code Double} regardless of the + * configured {@code resultType}, because {@code castToNumberType} + * was not applied.
  • + *
+ */ +public class Issue91ToNumTest { + + private final ClassLoader cl = Thread.currentThread().getContextClassLoader(); + + private Object eval(String formula, SpecifiedExpressionTypes types) { + Calculator c = CalculatorCreatorRegistry.astEvaluatorCreator() + .create(new Source(formula), "Issue91_" + Math.abs(formula.hashCode()), types, cl); + return c.apply(CalculationContext.newConcurrentContext()); + } + + // --- Problem 1: non-Number default value must not throw ClassCastException --- + + @Test + public void toNumWithStringDefaultReturnsZeroInsteadOfThrowing() { + Object r = eval("toNum('abc', 'xyz')", + new SpecifiedExpressionTypes(ExpressionTypes._float, ExpressionTypes._float)); + // non-Number default falls back to 0.0, cast through numberType -> Float + assertEquals(Float.valueOf(0.0f), r); + } + + @Test + public void toNumWithBooleanDefaultReturnsZeroInsteadOfThrowing() { + Object r = eval("toNum('abc', true)", + new SpecifiedExpressionTypes(ExpressionTypes._float, ExpressionTypes._float)); + assertEquals(Float.valueOf(0.0f), r); + } + + @Test + public void toNumWithNumberDefaultReturnsDefaultValue() { + Object r = eval("toNum('abc', 42)", + new SpecifiedExpressionTypes(ExpressionTypes._float, ExpressionTypes._float)); + assertEquals(Float.valueOf(42.0f), r); + } + + // --- Problem 2: resultType must be respected via castToNumberType --- + + @Test + public void toNumReturnsFloatWhenResultTypeIsFloat() { + Object r = eval("toNum('3.14', 0)", + new SpecifiedExpressionTypes(ExpressionTypes._float, ExpressionTypes._float)); + assertEquals(Float.class, r.getClass()); + assertEquals(3.14f, ((Number) r).floatValue(), 0.001f); + } + + @Test + public void toNumReturnsDoubleWhenResultTypeIsDouble() { + // numberType (2nd arg) drives castToNumberType; set it to _double so the + // returned value is a Double rather than the default Float. + Object r = eval("toNum('3.14', 0)", + new SpecifiedExpressionTypes(ExpressionTypes._float, ExpressionTypes._double)); + assertEquals(Double.class, r.getClass()); + assertEquals(3.14, ((Number) r).doubleValue(), 0.001); + } + + @Test + public void toNumDefaultValueCastThroughNumberType() { + Object r = eval("toNum('abc', 42)", + new SpecifiedExpressionTypes(ExpressionTypes._float, ExpressionTypes._float)); + assertEquals(Float.class, r.getClass()); + assertEquals(42.0f, ((Number) r).floatValue(), 0.001f); + } +} diff --git a/src/test/java/org/unlaxer/tinyexpression/Issue92InDayTimeRangeTest.java b/src/test/java/org/unlaxer/tinyexpression/Issue92InDayTimeRangeTest.java new file mode 100644 index 00000000..5e3825ef --- /dev/null +++ b/src/test/java/org/unlaxer/tinyexpression/Issue92InDayTimeRangeTest.java @@ -0,0 +1,82 @@ +package org.unlaxer.tinyexpression; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.time.DayOfWeek; + +import org.junit.Test; + +/** + * Regression for tinyexpression#92: + * {@code inDayTimeRange} returned {@code false} for the same day when + * {@code fromHour > toHour} (a midnight-spanning range), inconsistent with + * {@link org.unlaxer.tinyexpression.function.EmbeddedFunction#inTimeRange}. + */ +public class Issue92InDayTimeRangeTest { + + private static CalculationContext ctx(int dayOfWeek, float hour) { + CalculationContext ctx = CalculationContext.newConcurrentContext(); + ctx.set("nowDayOfWeek", (float) dayOfWeek); + ctx.set("nowHour", hour); + return ctx; + } + + // --- Same-day midnight span: MONDAY 22 -> MONDAY 6 --- + + @Test + public void sameDayMidnightSpan_lateHourIsIncluded() { + CalculationContext ctx = ctx(DayOfWeek.MONDAY.getValue(), 23f); + assertTrue(ctx.inDayTimeRange(DayOfWeek.MONDAY, 22f, DayOfWeek.MONDAY, 6f)); + } + + @Test + public void sameDayMidnightSpan_earlyHourIsIncluded() { + CalculationContext ctx = ctx(DayOfWeek.MONDAY.getValue(), 3f); + assertTrue(ctx.inDayTimeRange(DayOfWeek.MONDAY, 22f, DayOfWeek.MONDAY, 6f)); + } + + @Test + public void sameDayMidnightSpan_boundaryFromHourIsIncluded() { + CalculationContext ctx = ctx(DayOfWeek.MONDAY.getValue(), 22f); + assertTrue(ctx.inDayTimeRange(DayOfWeek.MONDAY, 22f, DayOfWeek.MONDAY, 6f)); + } + + @Test + public void sameDayMidnightSpan_boundaryToHourIsExcluded() { + CalculationContext ctx = ctx(DayOfWeek.MONDAY.getValue(), 6f); + assertFalse(ctx.inDayTimeRange(DayOfWeek.MONDAY, 22f, DayOfWeek.MONDAY, 6f)); + } + + @Test + public void sameDayMidnightSpan_gapHourIsExcluded() { + CalculationContext ctx = ctx(DayOfWeek.MONDAY.getValue(), 12f); + assertFalse(ctx.inDayTimeRange(DayOfWeek.MONDAY, 22f, DayOfWeek.MONDAY, 6f)); + } + + // --- Consistency with inTimeRange for the same hour pair --- + + @Test + public void sameDayMidnightSpan_matchesInTimeRange() { + CalculationContext ctx = ctx(DayOfWeek.MONDAY.getValue(), 23f); + boolean fromDayRange = ctx.inDayTimeRange(DayOfWeek.MONDAY, 22f, DayOfWeek.MONDAY, 6f); + boolean fromTimeRange = org.unlaxer.tinyexpression.function.EmbeddedFunction + .inTimeRange(ctx, 22f, 6f); + assertEquals(fromTimeRange, fromDayRange); + } + + // --- Normal same-day (non-midnight) range still works --- + + @Test + public void sameDayNormalRange_included() { + CalculationContext ctx = ctx(DayOfWeek.MONDAY.getValue(), 12f); + assertTrue(ctx.inDayTimeRange(DayOfWeek.MONDAY, 10f, DayOfWeek.MONDAY, 18f)); + } + + @Test + public void sameDayNormalRange_excluded() { + CalculationContext ctx = ctx(DayOfWeek.MONDAY.getValue(), 20f); + assertFalse(ctx.inDayTimeRange(DayOfWeek.MONDAY, 10f, DayOfWeek.MONDAY, 18f)); + } +}