From 1343a107cbfc7e9633493b53b62c8a5cf822b5f2 Mon Sep 17 00:00:00 2001
From: Thomas Nemer <80506610+thomasnemer@users.noreply.github.com>
Date: Thu, 23 Jul 2026 09:27:49 +0200
Subject: [PATCH] feat: add modulo operation (#2103)
Co-authored-by: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com> (Minor tweaks to pass CI)
---
src/core/config/Categories.json | 1 +
src/core/lib/Arithmetic.mjs | 13 ++
src/core/operations/FromDecimal.mjs | 2 +-
src/core/operations/MOD.mjs | 62 +++++++++
tests/operations/tests/MOD.mjs | 208 ++++++++++++++++++++++++++++
5 files changed, 285 insertions(+), 1 deletion(-)
create mode 100644 src/core/operations/MOD.mjs
create mode 100644 tests/operations/tests/MOD.mjs
diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json
index bf2c311806..4f4dedc2df 100644
--- a/src/core/config/Categories.json
+++ b/src/core/config/Categories.json
@@ -244,6 +244,7 @@
"Subtract",
"Multiply",
"Divide",
+ "MOD",
"Extended GCD",
"Modular Inverse",
"Mean",
diff --git a/src/core/lib/Arithmetic.mjs b/src/core/lib/Arithmetic.mjs
index 5fbc5771bd..d951253c43 100644
--- a/src/core/lib/Arithmetic.mjs
+++ b/src/core/lib/Arithmetic.mjs
@@ -124,6 +124,19 @@ export function median(data) {
}
+/**
+ * Computes modulo of two numbers and returns the value.
+ *
+ * @param {BigNumber[]} data
+ * @returns {BigNumber}
+ */
+export function mod(data) {
+ if (data.length > 0) {
+ return data.reduce((acc, curr) => acc.mod(curr));
+ }
+}
+
+
/**
* Computes standard deviation of a number array and returns the value.
*
diff --git a/src/core/operations/FromDecimal.mjs b/src/core/operations/FromDecimal.mjs
index 16af47e9e6..f718fb50f6 100644
--- a/src/core/operations/FromDecimal.mjs
+++ b/src/core/operations/FromDecimal.mjs
@@ -9,7 +9,7 @@ import {DELIM_OPTIONS} from "../lib/Delim.mjs";
import {fromDecimal} from "../lib/Decimal.mjs";
/**
- * From Decimal delimiters, plus auto-detection.
+ * From Decimal delimiters, plus auto-detection.
*/
const FROM_DECIMAL_DELIM_OPTIONS = [...DELIM_OPTIONS, "Auto"];
diff --git a/src/core/operations/MOD.mjs b/src/core/operations/MOD.mjs
new file mode 100644
index 0000000000..e50b339a42
--- /dev/null
+++ b/src/core/operations/MOD.mjs
@@ -0,0 +1,62 @@
+/**
+ * @license Apache-2.0
+ */
+
+import BigNumber from "bignumber.js";
+import Operation from "../Operation.mjs";
+import { createNumArray } from "../lib/Arithmetic.mjs";
+import { ARITHMETIC_DELIM_OPTIONS } from "../lib/Delim.mjs";
+
+
+/**
+ * MOD operation
+ */
+class MOD extends Operation {
+
+ /**
+ * MOD constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "MOD";
+ this.module = "Default";
+ this.description = "Computes the modulo of each number in a list with a given modulus value. Numbers are extracted from the input based on the delimiter, and non-numeric values are ignored.
e.g. 15 4 7 with modulus 3 becomes 0 1 1";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ "name": "Modulus",
+ "type": "number",
+ "value": 2
+ },
+ {
+ "name": "Delimiter",
+ "type": "option",
+ "value": ARITHMETIC_DELIM_OPTIONS,
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const modulus = new BigNumber(args[0]);
+ const delimiter = args[1];
+
+ if (modulus.isZero()) {
+ throw new Error("Modulus cannot be zero");
+ }
+
+ const numbers = createNumArray(input, delimiter);
+ const results = numbers.map(num => num.mod(modulus));
+
+ return results.join(" ");
+ }
+
+}
+
+export default MOD;
diff --git a/tests/operations/tests/MOD.mjs b/tests/operations/tests/MOD.mjs
new file mode 100644
index 0000000000..2ecd25f611
--- /dev/null
+++ b/tests/operations/tests/MOD.mjs
@@ -0,0 +1,208 @@
+/**
+ * MOD tests
+ *
+ * @license Apache-2.0
+ */
+import TestRegister from "../../lib/TestRegister.mjs";
+
+TestRegister.addTests([
+ {
+ name: "MOD: Basic modulo operation",
+ input: "15 4 7",
+ expectedOutput: "0 1 1",
+ recipeConfig: [
+ {
+ "op": "MOD",
+ "args": [3, "Space"]
+ }
+ ],
+ },
+ {
+ name: "MOD: Single number",
+ input: "10",
+ expectedOutput: "1",
+ recipeConfig: [
+ {
+ "op": "MOD",
+ "args": [3, "Space"]
+ }
+ ],
+ },
+ {
+ name: "MOD: Comma-separated numbers",
+ input: "15,8,23,16,5",
+ expectedOutput: "1 1 2 2 5",
+ recipeConfig: [
+ {
+ "op": "MOD",
+ "args": [7, "Comma"]
+ }
+ ],
+ },
+ {
+ name: "MOD: Line feed separated numbers",
+ input: "25\n13\n44\n7",
+ expectedOutput: "0 3 4 2",
+ recipeConfig: [
+ {
+ "op": "MOD",
+ "args": [5, "Line feed"]
+ }
+ ],
+ },
+ {
+ name: "MOD: Large numbers",
+ input: "123456789012345 987654321098765",
+ expectedOutput: "123456789012345 987654321098765",
+ recipeConfig: [
+ {
+ "op": "MOD",
+ "args": [1234567890123456, "Space"]
+ }
+ ],
+ },
+ {
+ name: "MOD: Mixed with non-numeric values",
+ input: "15 abc 4 def 7 xyz 23",
+ expectedOutput: "0 1 1 2",
+ recipeConfig: [
+ {
+ "op": "MOD",
+ "args": [3, "Space"]
+ }
+ ],
+ },
+ {
+ name: "MOD: Decimal numbers",
+ input: "10.5 15.7 8.2",
+ expectedOutput: "1.5 0.7 2.2",
+ recipeConfig: [
+ {
+ "op": "MOD",
+ "args": [3, "Space"]
+ }
+ ],
+ },
+ {
+ name: "MOD: Negative numbers",
+ input: "-15 -8 25 -10",
+ expectedOutput: "0 -2 1 -1",
+ recipeConfig: [
+ {
+ "op": "MOD",
+ "args": [3, "Space"]
+ }
+ ],
+ },
+ {
+ name: "MOD: Zero in input",
+ input: "0 5 10 15 20",
+ expectedOutput: "0 2 1 0 2",
+ recipeConfig: [
+ {
+ "op": "MOD",
+ "args": [3, "Space"]
+ }
+ ],
+ },
+ {
+ name: "MOD: Modulus of 2 (even/odd check)",
+ input: "1 2 3 4 5 6 7 8 9 10",
+ expectedOutput: "1 0 1 0 1 0 1 0 1 0",
+ recipeConfig: [
+ {
+ "op": "MOD",
+ "args": [2, "Space"]
+ }
+ ],
+ },
+ {
+ name: "MOD: Numbers with extra whitespace",
+ input: " 15 4 7 ",
+ expectedOutput: "0 1 1",
+ recipeConfig: [
+ {
+ "op": "MOD",
+ "args": [3, "Space"]
+ }
+ ],
+ },
+ {
+ name: "MOD: Empty input",
+ input: "",
+ expectedOutput: "",
+ recipeConfig: [
+ {
+ "op": "MOD",
+ "args": [3, "Space"]
+ }
+ ],
+ },
+ {
+ name: "MOD: Scientific notation",
+ input: "1e3 2e2 5e1",
+ expectedOutput: "1 2 2",
+ recipeConfig: [
+ {
+ "op": "MOD",
+ "args": [3, "Space"]
+ }
+ ],
+ },
+ {
+ name: "MOD: Floating point precision",
+ input: "10.123456789 20.987654321",
+ expectedOutput: "1.123456789 2.987654321",
+ recipeConfig: [
+ {
+ "op": "MOD",
+ "args": [3, "Space"]
+ }
+ ],
+ },
+ {
+ name: "MOD: Zero modulus error",
+ input: "15 4 7",
+ expectedError: true,
+ expectedOutput: "MOD - Modulus cannot be zero",
+ recipeConfig: [
+ {
+ "op": "MOD",
+ "args": [0, "Space"]
+ }
+ ],
+ },
+ {
+ name: "MOD: Semi-colon separated numbers",
+ input: "17;5;8;13",
+ expectedOutput: "2 0 3 3",
+ recipeConfig: [
+ {
+ "op": "MOD",
+ "args": [5, "Semi-colon"]
+ }
+ ],
+ },
+ {
+ name: "MOD: Colon separated numbers",
+ input: "25:9:14:7",
+ expectedOutput: "1 1 2 3",
+ recipeConfig: [
+ {
+ "op": "MOD",
+ "args": [4, "Colon"]
+ }
+ ],
+ },
+ {
+ name: "MOD: CRLF separated numbers",
+ input: "30\r\n18\r\n22\r\n11",
+ expectedOutput: "0 0 4 5",
+ recipeConfig: [
+ {
+ "op": "MOD",
+ "args": [6, "CRLF"]
+ }
+ ],
+ },
+]);