From 21ec87fb2f48b854c24c29e7ef7553f55b319a25 Mon Sep 17 00:00:00 2001 From: Thomas Nemer Date: Thu, 4 Sep 2025 19:05:27 +0200 Subject: [PATCH] feat: add modulus operation --- src/core/config/Categories.json | 1 + src/core/lib/Arithmetic.mjs | 13 ++ src/core/operations/MOD.mjs | 62 +++++++++ tests/operations/index.mjs | 1 + tests/operations/tests/MOD.mjs | 219 ++++++++++++++++++++++++++++++++ 5 files changed, 296 insertions(+) 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 434c8bb619..c25b91bc96 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -220,6 +220,7 @@ "Subtract", "Multiply", "Divide", + "MOD", "Mean", "Median", "Standard Deviation", diff --git a/src/core/lib/Arithmetic.mjs b/src/core/lib/Arithmetic.mjs index 7c10855fad..307619e927 100644 --- a/src/core/lib/Arithmetic.mjs +++ b/src/core/lib/Arithmetic.mjs @@ -121,6 +121,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/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/index.mjs b/tests/operations/index.mjs index f147e9e7c7..9b5bbbeda9 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -109,6 +109,7 @@ import "./tests/Magic.mjs"; import "./tests/Media.mjs"; import "./tests/MIMEDecoding.mjs"; import "./tests/Modhex.mjs"; +import "./tests/MOD.mjs"; import "./tests/MorseCode.mjs"; import "./tests/MS.mjs"; import "./tests/MultipleBombe.mjs"; diff --git a/tests/operations/tests/MOD.mjs b/tests/operations/tests/MOD.mjs new file mode 100644 index 0000000000..f2f4817d2e --- /dev/null +++ b/tests/operations/tests/MOD.mjs @@ -0,0 +1,219 @@ +/** + * 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: Tab-separated numbers", + input: "20\t14\t8\t35", + expectedOutput: "2 2 2 5", + recipeConfig: [ + { + "op": "MOD", + "args": [6, "Tab"] + } + ], + }, + { + 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"] + } + ], + }, +]);