diff --git a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/functions/Add.java b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/functions/Add.java new file mode 100644 index 000000000000..c4ac0c3a8c92 --- /dev/null +++ b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/functions/Add.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.record.path.functions; + +import org.apache.nifi.record.path.FieldValue; +import org.apache.nifi.record.path.RecordPathEvaluationContext; +import org.apache.nifi.record.path.math.MathBinaryEvaluator; +import org.apache.nifi.record.path.paths.RecordPathSegment; + +import java.util.stream.Stream; + +public class Add extends RecordPathSegment { + private final RecordPathSegment lhsPath; + private final RecordPathSegment rhsPath; + private final MathBinaryEvaluator add = MathBinaryEvaluator.add(); + + public Add(final RecordPathSegment lhsPath, final RecordPathSegment rhsPath, final boolean absolute) { + super("add", null, absolute); + this.lhsPath = lhsPath; + this.rhsPath = rhsPath; + } + + @Override + public Stream evaluate(RecordPathEvaluationContext context) { + final FieldValue lhs = lhsPath.evaluate(context).findFirst().orElseThrow(() -> new IllegalArgumentException("add function requires a left-hand operand")); + final FieldValue rhs = rhsPath.evaluate(context).findFirst().orElseThrow(() -> new IllegalArgumentException("add function requires a right-hand operand")); + + if (lhs.getValue() == null || rhs.getValue() == null) { + return Stream.of(); + } + + return Stream.of(add.evaluate(lhs, rhs)); + } +} diff --git a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/functions/Subtract.java b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/functions/Subtract.java new file mode 100644 index 000000000000..11736575e554 --- /dev/null +++ b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/functions/Subtract.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.record.path.functions; + +import org.apache.nifi.record.path.FieldValue; +import org.apache.nifi.record.path.RecordPathEvaluationContext; +import org.apache.nifi.record.path.math.MathBinaryEvaluator; +import org.apache.nifi.record.path.paths.RecordPathSegment; + +import java.util.stream.Stream; + +public class Subtract extends RecordPathSegment { + private final RecordPathSegment lhsPath; + private final RecordPathSegment rhsPath; + private final MathBinaryEvaluator subtract = MathBinaryEvaluator.subtract(); + + public Subtract(final RecordPathSegment lhsPath, final RecordPathSegment rhsPath, final boolean absolute) { + super("subtract", null, absolute); + this.lhsPath = lhsPath; + this.rhsPath = rhsPath; + } + + @Override + public Stream evaluate(RecordPathEvaluationContext context) { + final FieldValue lhs = lhsPath.evaluate(context).findFirst().orElseThrow(() -> new IllegalArgumentException("subtract function requires a left-hand operand")); + final FieldValue rhs = rhsPath.evaluate(context).findFirst().orElseThrow(() -> new IllegalArgumentException("subtract function requires a right-hand operand")); + + if (lhs.getValue() == null || rhs.getValue() == null) { + return Stream.of(); + } + + return Stream.of(subtract.evaluate(lhs, rhs)); + } +} diff --git a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathAddOperator.java b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathAddOperator.java new file mode 100644 index 000000000000..0e28c50e426d --- /dev/null +++ b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathAddOperator.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.record.path.math; + +public class MathAddOperator implements MathBinaryOperator { + @Override + public Long operate(Long n, Long m) { + return n + m; + } + + @Override + public Double operate(Double n, Double m) { + return n + m; + } + + @Override + public String getFieldName() { + return "add"; + } +} diff --git a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathBinaryEvaluator.java b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathBinaryEvaluator.java index 140c8c30105d..14d20c39c14b 100644 --- a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathBinaryEvaluator.java +++ b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathBinaryEvaluator.java @@ -27,6 +27,10 @@ public MathBinaryEvaluator(MathBinaryOperator op) { super(op); } + public static MathBinaryEvaluator add() { + return new MathBinaryEvaluator(new MathAddOperator()); + } + public static MathBinaryEvaluator divide() { return new MathBinaryEvaluator(new MathDivideOperator()); } @@ -35,6 +39,10 @@ public static MathBinaryEvaluator multiply() { return new MathBinaryEvaluator(new MathMultiplyOperator()); } + public static MathBinaryEvaluator subtract() { + return new MathBinaryEvaluator(new MathSubtractOperator()); + } + public FieldValue evaluate(FieldValue lhs, FieldValue rhs) { final Number lhsValue = MathTypeUtils.coerceNumber(lhs); final Number rhsValue = MathTypeUtils.coerceNumber(rhs); diff --git a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathSubtractOperator.java b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathSubtractOperator.java new file mode 100644 index 000000000000..ad91fea7a247 --- /dev/null +++ b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/math/MathSubtractOperator.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.record.path.math; + +public class MathSubtractOperator implements MathBinaryOperator { + @Override + public Long operate(Long n, Long m) { + return n - m; + } + + @Override + public Double operate(Double n, Double m) { + return n - m; + } + + @Override + public String getFieldName() { + return "subtract"; + } +} diff --git a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/paths/RecordPathCompiler.java b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/paths/RecordPathCompiler.java index ca3691b53987..1552c31ccd7b 100644 --- a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/paths/RecordPathCompiler.java +++ b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/paths/RecordPathCompiler.java @@ -35,6 +35,7 @@ import org.apache.nifi.record.path.filter.NotFilter; import org.apache.nifi.record.path.filter.RecordPathFilter; import org.apache.nifi.record.path.filter.StartsWith; +import org.apache.nifi.record.path.functions.Add; import org.apache.nifi.record.path.functions.Anchored; import org.apache.nifi.record.path.functions.ArrayOf; import org.apache.nifi.record.path.functions.Base64Decode; @@ -62,6 +63,7 @@ import org.apache.nifi.record.path.functions.SubstringAfterLast; import org.apache.nifi.record.path.functions.SubstringBefore; import org.apache.nifi.record.path.functions.SubstringBeforeLast; +import org.apache.nifi.record.path.functions.Subtract; import org.apache.nifi.record.path.functions.ToBytes; import org.apache.nifi.record.path.functions.ToDate; import org.apache.nifi.record.path.functions.ToLowerCase; @@ -461,6 +463,10 @@ public static RecordPathSegment buildPath(final Tree tree, final RecordPathSegme final RecordPathSegment[] args = getArgPaths(argumentListTree, 2, functionName, absolute); return new Anchored(args[0], args[1], absolute); } + case "add" : { + final RecordPathSegment[] args = getArgPaths(argumentListTree, 2, functionName, absolute); + return new Add(args[0], args[1], absolute); + } case "multiply": { final RecordPathSegment[] args = getArgPaths(argumentListTree, 2, functionName, absolute); return new Multiply(args[0], args[1], absolute); @@ -469,6 +475,10 @@ public static RecordPathSegment buildPath(final Tree tree, final RecordPathSegme final RecordPathSegment[] args = getArgPaths(argumentListTree, 2, functionName, absolute); return new Divide(args[0], args[1], absolute); } + case "subtract" : { + final RecordPathSegment[] args = getArgPaths(argumentListTree, 2, functionName, absolute); + return new Subtract(args[0], args[1], absolute); + } case "toNumber": { final RecordPathSegment[] args = getArgPaths(argumentListTree, 1, functionName, absolute); return new ToNumber(args[0], absolute); diff --git a/nifi-commons/nifi-record-path/src/test/java/org/apache/nifi/record/path/TestRecordPath.java b/nifi-commons/nifi-record-path/src/test/java/org/apache/nifi/record/path/TestRecordPath.java index 9f107e008832..97d307e7bfda 100644 --- a/nifi-commons/nifi-record-path/src/test/java/org/apache/nifi/record/path/TestRecordPath.java +++ b/nifi-commons/nifi-record-path/src/test/java/org/apache/nifi/record/path/TestRecordPath.java @@ -2641,6 +2641,167 @@ public void throwsExceptionOnInvalidArityMissingRhs() { } } + @Nested + class Add { + @Test + public void supportsLhsLiteralRhsLiteral() { + final FieldValue fieldValue = evaluateSingleFieldValue("add(3, 2)", record); + + assertEquals("add", fieldValue.getField().getFieldName()); + assertEquals(RecordFieldType.LONG, fieldValue.getField().getDataType().getFieldType()); + assertEquals("5", fieldValue.getValue().toString()); + } + @Test + public void supportsLhsPathRhsLiteral() { + final FieldValue fieldValue = evaluateSingleFieldValue("add(/id, 2)", record); + + assertEquals(RecordFieldType.LONG, fieldValue.getField().getDataType().getFieldType()); + assertEquals("50", fieldValue.getValue().toString()); + } + @Test + public void supportsLhsLiteralRhsPath() { + final FieldValue fieldValue = evaluateSingleFieldValue("add(2, /id)", record); + + assertEquals(RecordFieldType.LONG, fieldValue.getField().getDataType().getFieldType()); + assertEquals("50", fieldValue.getValue().toString()); + } + @Test + public void supportsLhsPathRhsPath() { + final FieldValue fieldValue = evaluateSingleFieldValue("add(/id, /id)", record); + + assertEquals(RecordFieldType.LONG, fieldValue.getField().getDataType().getFieldType()); + assertEquals("96", fieldValue.getValue().toString()); + } + @Test + public void supportsLongOverflow() { + final String addWithLongOverflow = "add(%s, 1)".formatted(Long.MAX_VALUE); + final FieldValue fieldValue = evaluateSingleFieldValue(addWithLongOverflow, record); + + assertEquals(RecordFieldType.LONG, fieldValue.getField().getDataType().getFieldType()); + final String expected = "%s".formatted(Long.MIN_VALUE); + assertEquals(expected, fieldValue.getValue().toString()); + } + @Test + public void supportsDoubleOverflow() { + final String addWithDoubleOverflow = "add('%s', '1e292')".formatted(Double.MAX_VALUE); + final FieldValue fieldValue = evaluateSingleFieldValue(addWithDoubleOverflow, record); + + assertEquals(RecordFieldType.DOUBLE, fieldValue.getField().getDataType().getFieldType()); + assertEquals("Infinity", fieldValue.getValue().toString()); + } + @Test + public void supportsLhsNull() { + final List fieldValues = evaluateMultiFieldValue("add(/notAField, 0)", record); + assertTrue(fieldValues.isEmpty()); + } + @Test + public void supportsRhsNull() { + final List fieldValues = evaluateMultiFieldValue("add(0, /notAField)", record); + assertTrue(fieldValues.isEmpty()); + } + @Test + public void throwsExceptionOnInvalidArityMissingLhs() { + Exception exception = + assertThrows(Exception.class, () -> evaluateSingleFieldValue("add(add(/notAField, 0), add(/notAField, 0))", record)); + assertEquals("add function requires a left-hand operand", exception.getMessage()); + } + @Test + public void throwsExceptionOnInvalidArityMissingRhs() { + Exception exception = + assertThrows(Exception.class, () -> evaluateSingleFieldValue("add(0, add(/notAField, 0))", record)); + assertEquals("add function requires a right-hand operand", exception.getMessage()); + } + } + + @Nested + class Subtract { + @Test + public void supportsLhsLiteralRhsLiteral() { + final FieldValue fieldValue = evaluateSingleFieldValue("subtract(3, 2)", record); + + assertEquals("subtract", fieldValue.getField().getFieldName()); + assertEquals(RecordFieldType.LONG, fieldValue.getField().getDataType().getFieldType()); + assertEquals("1", fieldValue.getValue().toString()); + } + @Test + public void supportsLhsPathRhsLiteral() { + final FieldValue fieldValue = evaluateSingleFieldValue("subtract(/id, 2)", record); + + assertEquals(RecordFieldType.LONG, fieldValue.getField().getDataType().getFieldType()); + assertEquals("46", fieldValue.getValue().toString()); + } + @Test + public void supportsLhsLiteralRhsPath() { + final FieldValue fieldValue = evaluateSingleFieldValue("subtract(2, /id)", record); + + assertEquals(RecordFieldType.LONG, fieldValue.getField().getDataType().getFieldType()); + assertEquals("-46", fieldValue.getValue().toString()); + } + @Test + public void supportsLhsPathRhsPath() { + final FieldValue fieldValue = evaluateSingleFieldValue("subtract(/id, /id)", record); + + assertEquals(RecordFieldType.LONG, fieldValue.getField().getDataType().getFieldType()); + assertEquals("0", fieldValue.getValue().toString()); + } + @Test + public void supportsPositiveLongOverflow() { + final String subtractWithPositiveLongOverflow = "subtract(%s, -1)".formatted(Long.MAX_VALUE); + final FieldValue fieldValue = evaluateSingleFieldValue(subtractWithPositiveLongOverflow, record); + + assertEquals(RecordFieldType.LONG, fieldValue.getField().getDataType().getFieldType()); + final String expected = "%s".formatted(Long.MIN_VALUE); + assertEquals(expected, fieldValue.getValue().toString()); + } + @Test + public void supportsNegativeLongOverflow() { + final String subtractWithNegativeLongOverflow = "subtract(%s, 1)".formatted(Long.MIN_VALUE); + final FieldValue fieldValue = evaluateSingleFieldValue(subtractWithNegativeLongOverflow, record); + + assertEquals(RecordFieldType.LONG, fieldValue.getField().getDataType().getFieldType()); + final String expected = "%s".formatted(Long.MAX_VALUE); + assertEquals(expected, fieldValue.getValue().toString()); + } + @Test + public void supportsPostiveDoubleOverflow() { + final String subtractionWithPostiveDoubleOverflow = "subtract('%s', '%s')".formatted(Double.MAX_VALUE, -1.0e308); + final FieldValue fieldValue = evaluateSingleFieldValue(subtractionWithPostiveDoubleOverflow, record); + + assertEquals(RecordFieldType.DOUBLE, fieldValue.getField().getDataType().getFieldType()); + assertEquals("Infinity", fieldValue.getValue().toString()); + } + @Test + public void supportsNegativeDoubleOverflow() { + final String subtractionWithNegativeDoubleOverflow = "subtract('%s', '1e308')".formatted(-Double.MAX_VALUE); + final FieldValue fieldValue = evaluateSingleFieldValue(subtractionWithNegativeDoubleOverflow, record); + + assertEquals(RecordFieldType.DOUBLE, fieldValue.getField().getDataType().getFieldType()); + assertEquals("-Infinity", fieldValue.getValue().toString()); + } + @Test + public void supportsLhsNull() { + final List fieldValues = evaluateMultiFieldValue("subtract(/notAField, 0)", record); + assertTrue(fieldValues.isEmpty()); + } + @Test + public void supportsRhsNull() { + final List fieldValues = evaluateMultiFieldValue("subtract(0, /notAField)", record); + assertTrue(fieldValues.isEmpty()); + } + @Test + public void throwsExceptionOnInvalidArityMissingLhs() { + Exception exception = + assertThrows(Exception.class, () -> evaluateSingleFieldValue("subtract(subtract(/notAField, 0), subtract(/notAField, 0))", record)); + assertEquals("subtract function requires a left-hand operand", exception.getMessage()); + } + @Test + public void throwsExceptionOnInvalidArityMissingRhs() { + Exception exception = + assertThrows(Exception.class, () -> evaluateSingleFieldValue("subtract(0, subtract(/notAField, 0))", record)); + assertEquals("subtract function requires a right-hand operand", exception.getMessage()); + } + } + @Nested class ToNumber { @Test diff --git a/nifi-docs/src/main/asciidoc/record-path-guide.adoc b/nifi-docs/src/main/asciidoc/record-path-guide.adoc index cd824b402e5d..0abf0210cd76 100644 --- a/nifi-docs/src/main/asciidoc/record-path-guide.adoc +++ b/nifi-docs/src/main/asciidoc/record-path-guide.adoc @@ -1510,6 +1510,32 @@ Coerces a String or Date value to a number. If the argument is already a number | `toNumber(/date)` | 1768953600000 |============================================================================== +=== add + +Calculates the sum of two numbers. + +|============================================================================== +| RecordPath | Return value +| `add(2, 2)` | 4 +| `add('2.0', 2)` | 4.0 +| `add(/flags, 2)` | 9 +| `add(2, /temp)` | 57.0 +| `add(/load, /temp)` | 60.67 +|============================================================================== + +=== subtract + +Calculates the difference between two numbers. + +|============================================================================== +| RecordPath | Return value +| `subtract(2, 2)` | 0 +| `subtract('2.0', 2)` | 0.0 +| `subtract(/flags, 2)` | 5 +| `subtract(2, /temp)` | -53.0 +| `subtract(/load, /temp)` | -49.33 +|============================================================================== + === multiply Calculates the product of two numbers.