diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index aac71f0b..9ab28df3 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -245,6 +245,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 5fbc5771..d951253c 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/AvroToJSON.mjs b/src/core/operations/AvroToJSON.mjs index 497a3872..585bd872 100644 --- a/src/core/operations/AvroToJSON.mjs +++ b/src/core/operations/AvroToJSON.mjs @@ -39,7 +39,7 @@ class AvroToJSON extends Operation { * @param {Object[]} args * @returns {string} */ - run(input, args) { + async run(input, args) { if (input.byteLength <= 0) { throw new OperationError("Please provide an input."); } diff --git a/src/core/operations/Bzip2Compress.mjs b/src/core/operations/Bzip2Compress.mjs index 45dbfae6..b5105e98 100644 --- a/src/core/operations/Bzip2Compress.mjs +++ b/src/core/operations/Bzip2Compress.mjs @@ -47,7 +47,7 @@ class Bzip2Compress extends Operation { * @param {Object[]} args * @returns {File} */ - run(input, args) { + async run(input, args) { const [blockSize, workFactor] = args; if (input.byteLength <= 0) { throw new OperationError("Please provide an input."); diff --git a/src/core/operations/FromDecimal.mjs b/src/core/operations/FromDecimal.mjs index 16af47e9..f718fb50 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 00000000..e50b339a --- /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/node/tests/operations.mjs b/tests/node/tests/operations.mjs index 0046574c..c6f0e44f 100644 --- a/tests/node/tests/operations.mjs +++ b/tests/node/tests/operations.mjs @@ -264,6 +264,18 @@ Full hash: $2a$10$ODeP1.6fMsb.ENk2ngPUCO7qTGVPyHA9TqDVcyupyed8FjsiF65L6`; assert.strictEqual(result.toString(), "Fit as a Fiddle"); }), + it("Bzip2 Compress: round-trips through the Node API", async () => { + const compressed = await chef.bzip2Compress("The quick brown fox."); + const result = await chef.bzip2Decompress(compressed); + assert.strictEqual(result.toString(), "The quick brown fox."); + }), + + it("Avro to JSON: decodes an object container file through the Node API", async () => { + const avro = chef.fromHex("4f626a0104166176726f2e736368656d6196017b2274797065223a227265636f7264222c226e616d65223a22736d616c6c222c226669656c6473223a5b7b226e616d65223a226e616d65222c2274797065223a22737472696e67227d5d7d146176726f2e636f646563086e756c6c004e0247632e3702e5b75cdab9a62f1541020e0c6d796e616d654e0247632e3702e5b75cdab9a62f1541"); + const result = await chef.avroToJSON(avro); + assert.strictEqual(result.toString(), "{\n \"name\": \"myname\"\n}"); + }), + it("cartesianProduct: binary string", () => { const result = cartesianProduct("1:2\\n\\n3:4", { itemDelimiter: ":", diff --git a/tests/operations/tests/Hash.mjs b/tests/operations/tests/Hash.mjs index 1ffb749c..a805b94f 100644 --- a/tests/operations/tests/Hash.mjs +++ b/tests/operations/tests/Hash.mjs @@ -722,6 +722,39 @@ TestRegister.addTests([ } ] }, + { + name: "HMAC: Decimal key space-delimited matches Latin1 key (fromDecimal Auto delimiter regression)", + input: "Hello, World!", + expectedOutput: "9b641a008211bb0464a10899d5b37a24ab08ffcf839f5e74d17d99f856530957", + recipeConfig: [ + { + "op": "HMAC", + "args": [{"option": "Decimal", "string": "65 65"}, "SHA256"] + } + ] + }, + { + name: "HMAC: Latin1 key matches space-delimited Decimal key (fromDecimal Auto delimiter regression)", + input: "Hello, World!", + expectedOutput: "9b641a008211bb0464a10899d5b37a24ab08ffcf839f5e74d17d99f856530957", + recipeConfig: [ + { + "op": "HMAC", + "args": [{"option": "Latin1", "string": "AA"}, "SHA256"] + } + ] + }, + { + name: "HMAC: Decimal key comma-delimited matches Latin1 key (fromDecimal Auto delimiter regression)", + input: "Hello, World!", + expectedOutput: "9b641a008211bb0464a10899d5b37a24ab08ffcf839f5e74d17d99f856530957", + recipeConfig: [ + { + "op": "HMAC", + "args": [{"option": "Decimal", "string": "65,65"}, "SHA256"] + } + ] + }, { name: "MD5: Complex bytes", input: "10dc10e32010de10d010dc10d810d910d010e12e", diff --git a/tests/operations/tests/MOD.mjs b/tests/operations/tests/MOD.mjs new file mode 100644 index 00000000..2ecd25f6 --- /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"] + } + ], + }, +]);