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"] + } + ], + }, +]);