Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,14 @@ public void set(String name,float value) {
*/
@Override
public Optional<Float> 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)
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Float> 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<Float> 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<Float> v = ctx.getValue("b");
assertEquals(7f, v.get(), 0.001f);
}

@Test
public void getValueAfterSetFloatPreservesExactValue() {
CalculationContext ctx = CalculationContext.newConcurrentContext();
ctx.set("f", 1.25f);
Optional<Float> v = ctx.getValue("f");
assertEquals(1.25f, v.get(), 0.0f);
}

@Test
public void getValueForAbsentReturnsEmpty() {
CalculationContext ctx = CalculationContext.newConcurrentContext();
Optional<Float> v = ctx.getValue("absent");
assertEquals(Optional.empty(), v);
}
}
81 changes: 81 additions & 0 deletions src/test/java/org/unlaxer/tinyexpression/Issue91ToNumTest.java
Original file line number Diff line number Diff line change
@@ -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:
* <ul>
* <li>{@code toNum('abc', 'xyz')} with a non-{@link Number} default value
* threw {@link ClassCastException} via {@code (Number) eval(...)}.</li>
* <li>{@code toNum(...)} returned {@code Double} regardless of the
* configured {@code resultType}, because {@code castToNumberType}
* was not applied.</li>
* </ul>
*/
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);
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}
Loading