From bdf78ac4d1d3d7baafc95907d3f6418131cfabb2 Mon Sep 17 00:00:00 2001 From: opaopa6969 Date: Sun, 30 Aug 2026 03:34:41 +0900 Subject: [PATCH] fix(context): make getValue() number-safe, inDayTimeRange midnight-consistent, toNum() cast through numberType Fix three glm-hunt bugs in CalculationContext / P4TypedAstEvaluator: #90: AbstractCalculationContext.getValue() did an unchecked (Float) cast on valueByName, so set(String, Number) with Double/Integer/BigInteger etc. caused ClassCastException on read. Convert via Number.floatValue() when the stored value is not already a Float; Float values are preserved as-is. #91: P4TypedAstEvaluator.evalToNumExpr() returned a raw Double and cast the default value via (Number), causing ClassCastException for non-Number defaults (e.g. toNum('abc','xyz')) and ignoring the configured numberType. Route both the parsed value and the default through castToNumberType so the result honors numberType, and fall back to 0.0 when the default is not a Number instead of throwing. #92: AbstractCalculationContext.inDayTimeRange() returned false for a same-day range with fromHour > toHour (a midnight-spanning range), inconsistent with EmbeddedFunction.inTimeRange(). When fromHour > toHour on the same day, evaluate as a midnight span (nowHour >= fromHour || nowHour < toHour), matching inTimeRange(). Each bug is covered by a dedicated regression test (Issue90/91/92*Test). Full test suite passes (703 tests, 0 failures). --- .../AbstractCalculationContext.java | 15 +++- .../evaluator/ast/P4TypedAstEvaluator.java | 6 +- .../Issue90GetValueCastTest.java | 57 +++++++++++++ .../tinyexpression/Issue91ToNumTest.java | 81 ++++++++++++++++++ .../Issue92InDayTimeRangeTest.java | 82 +++++++++++++++++++ 5 files changed, 236 insertions(+), 5 deletions(-) create mode 100644 src/test/java/org/unlaxer/tinyexpression/Issue90GetValueCastTest.java create mode 100644 src/test/java/org/unlaxer/tinyexpression/Issue91ToNumTest.java create mode 100644 src/test/java/org/unlaxer/tinyexpression/Issue92InDayTimeRangeTest.java 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)); + } +}