-
-
Notifications
You must be signed in to change notification settings - Fork 470
Rules DSL: Support arithmetic operations directly on Number Item's State #5618
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,9 +28,11 @@ | |
| import org.junit.jupiter.params.provider.ValueSource; | ||
| import org.openhab.core.library.types.DecimalType; | ||
| import org.openhab.core.library.types.HSBType; | ||
| import org.openhab.core.library.types.OnOffType; | ||
| import org.openhab.core.library.types.PercentType; | ||
| import org.openhab.core.library.types.QuantityType; | ||
| import org.openhab.core.library.unit.Units; | ||
| import org.openhab.core.types.State; | ||
| import org.openhab.core.types.Type; | ||
|
|
||
| /** | ||
|
|
@@ -53,12 +55,12 @@ public class NumberExtensionsTest { | |
|
|
||
| @Test | ||
| public void operatorPlusNumberNumber() { | ||
| assertThat(NumberExtensions.operator_plus(DECIMAL1, DECIMAL2), is(BigDecimal.valueOf(3))); | ||
| assertThat(NumberExtensions.operator_plus((Number) DECIMAL1, (Number) DECIMAL2), is(BigDecimal.valueOf(3))); | ||
| } | ||
|
|
||
| @Test | ||
| public void operatorPlusNumberQuantityOne() { | ||
| assertThat(NumberExtensions.operator_plus(Q_ONE_1, DECIMAL2), is(BigDecimal.valueOf(3))); | ||
| assertThat(NumberExtensions.operator_plus((Number) Q_ONE_1, (Number) DECIMAL2), is(BigDecimal.valueOf(3))); | ||
| } | ||
|
|
||
| @Test | ||
|
|
@@ -68,7 +70,7 @@ public void operatorPlusQuantityQuantity() { | |
|
|
||
| @Test | ||
| public void operatorMinusNumber() { | ||
| assertThat(NumberExtensions.operator_minus(DECIMAL1), is(BigDecimal.valueOf(-1))); | ||
| assertThat(NumberExtensions.operator_minus((Number) DECIMAL1), is(BigDecimal.valueOf(-1))); | ||
| } | ||
|
|
||
| @Test | ||
|
|
@@ -78,12 +80,12 @@ public void operatorMinusQuantity() { | |
|
|
||
| @Test | ||
| public void operatorMinusNumberNumber() { | ||
| assertThat(NumberExtensions.operator_minus(DECIMAL2, DECIMAL1), is(BigDecimal.ONE)); | ||
| assertThat(NumberExtensions.operator_minus((Number) DECIMAL2, (Number) DECIMAL1), is(BigDecimal.ONE)); | ||
| } | ||
|
|
||
| @Test | ||
| public void operatorMinusNumberQuantityOne() { | ||
| assertThat(NumberExtensions.operator_minus(Q_ONE_2, DECIMAL1), is(BigDecimal.ONE)); | ||
| assertThat(NumberExtensions.operator_minus((Number) Q_ONE_2, (Number) DECIMAL1), is(BigDecimal.ONE)); | ||
| } | ||
|
|
||
| @Test | ||
|
|
@@ -93,7 +95,8 @@ public void operatorMinusQuantityQuantity() { | |
|
|
||
| @Test | ||
| public void operatorMultiplyNumberQuantity() { | ||
| assertThat(NumberExtensions.operator_multiply(DECIMAL2, Q_LENGTH_2_CM), is(QuantityType.valueOf("4 cm"))); | ||
| assertThat(NumberExtensions.operator_multiply((Number) DECIMAL2, Q_LENGTH_2_CM), | ||
| is(QuantityType.valueOf("4 cm"))); | ||
| } | ||
|
|
||
| @Test | ||
|
|
@@ -103,7 +106,8 @@ public void operatorMultiplyQuantityQuantity() { | |
|
|
||
| @Test | ||
| public void operatorDivideQuantityNumber() { | ||
| assertThat(NumberExtensions.operator_divide(Q_LENGTH_1_M, DECIMAL2), is(QuantityType.valueOf("0.5 m"))); | ||
| assertThat(NumberExtensions.operator_divide(Q_LENGTH_1_M, (Number) DECIMAL2), | ||
| is(QuantityType.valueOf("0.5 m"))); | ||
| } | ||
|
|
||
| @Test | ||
|
|
@@ -113,7 +117,43 @@ public void operatorDivideQuantityQuantity() { | |
|
|
||
| @Test | ||
| public void operatorDivideNumberQuantity() { | ||
| assertThat(NumberExtensions.operator_divide(DECIMAL1, Q_LENGTH_2_CM), is(QuantityType.valueOf("0.5 one/cm"))); | ||
| assertThat(NumberExtensions.operator_divide((Number) DECIMAL1, Q_LENGTH_2_CM), | ||
| is(QuantityType.valueOf("0.5 one/cm"))); | ||
| } | ||
|
|
||
| @Test | ||
| public void operatorMinusStateState() { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These tests verify the runtime dispatch helpers, but they bypass the Rules DSL/Xbase operator resolution that this PR is intended to change by explicitly calling Could this also be covered by Rules DSL-level tests that parse/type-resolve and execute representative expressions? In particular, it would be useful to cover: NumberItem.state - OtherNumberItem.state
NumberItem.state * 1.5
QuantityItem.state - OtherQuantityItem.state
(QuantityItem.state - OtherQuantityItem.state) * 2as well as explicitly typed numeric variables, That would catch both overload-resolution regressions and cases where the static type of an intermediate expression differs from its runtime numeric type. |
||
| assertThat(NumberExtensions.operator_minus((State) DECIMAL2, (State) DECIMAL1), is(BigDecimal.ONE)); | ||
| } | ||
|
|
||
| @Test | ||
| public void operatorMinusQuantityStateState() { | ||
| assertThat(NumberExtensions.operator_minus((State) Q_LENGTH_1_M, (State) Q_LENGTH_2_CM), | ||
| is(QuantityType.valueOf("0.98 m"))); | ||
| } | ||
|
|
||
| @Test | ||
| public void operatorMultiplyStateNumber() { | ||
| assertThat(NumberExtensions.operator_multiply((State) DECIMAL2, BigDecimal.valueOf(1.5)), | ||
| is(BigDecimal.valueOf(3.0))); | ||
| } | ||
|
|
||
| @Test | ||
| public void operatorMultiplyQuantityStateNumber() { | ||
| assertThat(NumberExtensions.operator_multiply((State) Q_LENGTH_2_CM, (Number) DECIMAL2), | ||
| is(QuantityType.valueOf("4 cm"))); | ||
| } | ||
|
|
||
| @Test | ||
| public void operatorDivideNumberState() { | ||
| assertThat(NumberExtensions.operator_divide(BigDecimal.TEN, (State) DECIMAL2), | ||
| is(new BigDecimal("5.00000000"))); | ||
| } | ||
|
|
||
| @Test | ||
| public void operatorNonNumericState() { | ||
| assertThrows(IllegalArgumentException.class, | ||
| () -> NumberExtensions.operator_multiply(OnOffType.ON, (Number) DECIMAL2)); | ||
| } | ||
|
|
||
| @Test | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ | |
| import org.openhab.core.library.types.DecimalType; | ||
| import org.openhab.core.library.types.QuantityType; | ||
| import org.openhab.core.library.unit.Units; | ||
| import org.openhab.core.types.State; | ||
| import org.openhab.core.types.Type; | ||
|
|
||
| /** | ||
|
|
@@ -91,6 +92,60 @@ public static BigDecimal operator_divide(Number x, Number y) { | |
| return xValue.divide(yValue, 8, RoundingMode.HALF_UP); | ||
| } | ||
|
|
||
| // Calculation operators for states | ||
|
|
||
| public static Number operator_plus(State x, State y) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The For example, with two dimensional Number Items: (LengthA.state - LengthB.state) * 2the first operation can correctly produce a The same issue occurs when such a result is assigned to an inferred variable and used later. Could this be implemented without reducing a quantity result to |
||
| return plus(stateToNumber(x), stateToNumber(y)); | ||
| } | ||
|
|
||
| public static Number operator_plus(State x, Number y) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes expressions such as: Temperature.state + 1valid, but when the state is a The same problem applies to That seems particularly risky because previously discarding the unit required an explicit Could mixed quantity/plain-number addition and subtraction either be rejected or given explicitly defined unit-safe semantics instead? |
||
| return plus(stateToNumber(x), y); | ||
| } | ||
|
|
||
| public static Number operator_plus(Number x, State y) { | ||
| return plus(x, stateToNumber(y)); | ||
| } | ||
|
|
||
| public static Number operator_minus(State x) { | ||
| return minus(stateToNumber(x)); | ||
| } | ||
|
|
||
| public static Number operator_minus(State x, State y) { | ||
| return minus(stateToNumber(x), stateToNumber(y)); | ||
| } | ||
|
|
||
| public static Number operator_minus(State x, Number y) { | ||
| return minus(stateToNumber(x), y); | ||
| } | ||
|
|
||
| public static Number operator_minus(Number x, State y) { | ||
| return minus(x, stateToNumber(y)); | ||
| } | ||
|
|
||
| public static Number operator_multiply(State x, State y) { | ||
| return multiply(stateToNumber(x), stateToNumber(y)); | ||
| } | ||
|
|
||
| public static Number operator_multiply(State x, Number y) { | ||
| return multiply(stateToNumber(x), y); | ||
| } | ||
|
|
||
| public static Number operator_multiply(Number x, State y) { | ||
| return multiply(x, stateToNumber(y)); | ||
| } | ||
|
|
||
| public static Number operator_divide(State x, State y) { | ||
| return divide(stateToNumber(x), stateToNumber(y)); | ||
| } | ||
|
|
||
| public static Number operator_divide(State x, Number y) { | ||
| return divide(stateToNumber(x), y); | ||
| } | ||
|
|
||
| public static Number operator_divide(Number x, State y) { | ||
| return divide(x, stateToNumber(y)); | ||
| } | ||
|
|
||
| // Comparison operations between numbers | ||
|
|
||
| public static boolean operator_equals(Number left, Number right) { | ||
|
|
@@ -408,4 +463,62 @@ private static boolean oneIsQuantity(Number left, Number right) { | |
| private static boolean isAbstractUnitOne(QuantityType<?> left) { | ||
| return Units.ONE.equals(left.getUnit()); | ||
| } | ||
|
|
||
| private static Number stateToNumber(State state) { | ||
| if (state == null) { | ||
| return null; | ||
| } | ||
| if (state instanceof Number number) { | ||
| return number; | ||
| } | ||
| throw new IllegalArgumentException( | ||
| "State '" + state + "' of type '" + state.getClass().getSimpleName() + "' cannot be used as a number"); | ||
| } | ||
|
|
||
| private static Number plus(Number x, Number y) { | ||
| if (x instanceof QuantityType<?> qx && y instanceof QuantityType<?> qy) { | ||
| return operator_plus(qx, qy); | ||
| } | ||
| return operator_plus(x, y); | ||
| } | ||
|
|
||
| private static Number minus(Number x) { | ||
| if (x instanceof QuantityType<?> qx) { | ||
| return operator_minus(qx); | ||
| } | ||
| return operator_minus(x); | ||
| } | ||
|
|
||
| private static Number minus(Number x, Number y) { | ||
| if (x instanceof QuantityType<?> qx && y instanceof QuantityType<?> qy) { | ||
| return operator_minus(qx, qy); | ||
| } | ||
| return operator_minus(x, y); | ||
| } | ||
|
|
||
| private static Number multiply(Number x, Number y) { | ||
| if (x instanceof QuantityType<?> qx && y instanceof QuantityType<?> qy) { | ||
| return operator_multiply(qx, qy); | ||
| } | ||
| if (x instanceof QuantityType<?> qx) { | ||
| return operator_multiply(qx, y); | ||
| } | ||
| if (y instanceof QuantityType<?> qy) { | ||
| return operator_multiply(x, qy); | ||
| } | ||
| return operator_multiply(x, y); | ||
| } | ||
|
|
||
| private static Number divide(Number x, Number y) { | ||
| if (x instanceof QuantityType<?> qx && y instanceof QuantityType<?> qy) { | ||
| return operator_divide(qx, qy); | ||
| } | ||
| if (x instanceof QuantityType<?> qx) { | ||
| return operator_divide(qx, y); | ||
| } | ||
| if (y instanceof QuantityType<?> qy) { | ||
| return operator_divide(x, qy); | ||
| } | ||
| return operator_divide(x, y); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The need to add these casts to previously unambiguous calls looks like an API compatibility regression caused by the new overloads.
DecimalTypeimplements bothNumberandState, so after adding both:neither overload is more specific for a
DecimalTypeargument and Java requires an explicit cast.This package is exported by
org.openhab.core.model.script, so external Java code directly using these extension methods could also stop compiling after updating openHAB, even though existing binaries would continue to work.It would be preferable to avoid introducing this ambiguity rather than adapting the existing tests to it. It would also be useful to verify that equivalent Rules DSL expressions with variables statically typed as
DecimalType,PercentType, etc. do not become ambiguous for the same reason.