From 7beac55201f73bfe2c01615b758069e0f15dddfd Mon Sep 17 00:00:00 2001 From: thetazero Date: Sat, 15 Jan 2022 02:40:46 -0800 Subject: [PATCH] add invmod (modular multiplicative inverse) (#2368) * add invmod (modular multiplicative inverse) * implement (most) suggestions * style error * fix NaN tests Co-authored-by: Jos de Jong --- src/expression/embeddedDocs/embeddedDocs.js | 2 + .../function/arithmetic/invmod.js | 14 ++++ src/factoriesAny.js | 1 + src/function/arithmetic/invmod.js | 47 ++++++++++++ .../function/arithmetic/invmod.test.js | 73 +++++++++++++++++++ 5 files changed, 137 insertions(+) create mode 100644 src/expression/embeddedDocs/function/arithmetic/invmod.js create mode 100644 src/function/arithmetic/invmod.js create mode 100644 test/unit-tests/function/arithmetic/invmod.test.js diff --git a/src/expression/embeddedDocs/embeddedDocs.js b/src/expression/embeddedDocs/embeddedDocs.js index 7ffd19b75..3d3787947 100644 --- a/src/expression/embeddedDocs/embeddedDocs.js +++ b/src/expression/embeddedDocs/embeddedDocs.js @@ -136,6 +136,7 @@ import { bitOrDocs } from './function/bitwise/bitOr.js' import { bitNotDocs } from './function/bitwise/bitNot.js' import { bitAndDocs } from './function/bitwise/bitAnd.js' import { xgcdDocs } from './function/arithmetic/xgcd.js' +import { invmodDocs } from './function/arithmetic/invmod.js' import { unaryPlusDocs } from './function/arithmetic/unaryPlus.js' import { unaryMinusDocs } from './function/arithmetic/unaryMinus.js' import { squareDocs } from './function/arithmetic/square.js' @@ -369,6 +370,7 @@ export const embeddedDocs = { unaryMinus: unaryMinusDocs, unaryPlus: unaryPlusDocs, xgcd: xgcdDocs, + invmod: invmodDocs, // functions - bitwise bitAnd: bitAndDocs, diff --git a/src/expression/embeddedDocs/function/arithmetic/invmod.js b/src/expression/embeddedDocs/function/arithmetic/invmod.js new file mode 100644 index 000000000..0a03ed11f --- /dev/null +++ b/src/expression/embeddedDocs/function/arithmetic/invmod.js @@ -0,0 +1,14 @@ +export const invmodDocs = { + name: 'invmod', + category: 'Arithmetic', + syntax: [ + 'invmod(a, b)' + ], + description: 'Calculate the (modular) multiplicative inverse of a modulo b. Solution to the equation ax ≣ 1 (mod b)', + examples: [ + 'invmod(8, 12)=NaN', + 'invmod(7, 13)=2', + 'math.invmod(15151, 15122)=10429' + ], + seealso: ['gcd', 'xgcd'] +} diff --git a/src/factoriesAny.js b/src/factoriesAny.js index 208c8a3ee..cceef3655 100644 --- a/src/factoriesAny.js +++ b/src/factoriesAny.js @@ -53,6 +53,7 @@ export { createSqrt } from './function/arithmetic/sqrt.js' export { createSquare } from './function/arithmetic/square.js' export { createSubtract } from './function/arithmetic/subtract.js' export { createXgcd } from './function/arithmetic/xgcd.js' +export { createInvmod } from './function/arithmetic/invmod.js' export { createDotMultiply } from './function/arithmetic/dotMultiply.js' export { createBitAnd } from './function/bitwise/bitAnd.js' export { createBitNot } from './function/bitwise/bitNot.js' diff --git a/src/function/arithmetic/invmod.js b/src/function/arithmetic/invmod.js new file mode 100644 index 000000000..e90f69b3b --- /dev/null +++ b/src/function/arithmetic/invmod.js @@ -0,0 +1,47 @@ +import { factory } from '../../utils/factory.js' + +const name = 'invmod' +const dependencies = ['typed', 'config', 'BigNumber', 'xgcd', 'equal', 'smaller', 'mod', 'add', 'isInteger'] + +export const createInvmod = /* #__PURE__ */ factory(name, dependencies, ({ typed, config, BigNumber, xgcd, equal, smaller, mod, add, isInteger }) => { + /** + * Calculate the (modular) multiplicative inverse of a modulo b. Solution to the equation `ax ≣ 1 (mod b)` + * See https://en.wikipedia.org/wiki/Modular_multiplicative_inverse. + * + * Syntax: + * + * math.invmod(a, b) + * + * Examples: + * + * math.invmod(8, 12) // returns NaN + * math.invmod(7, 13) // return 2 + * math.invmod(15151, 15122) // returns 10429 + * + * See also: + * + * gcd, xgcd + * + * @param {number | BigNumber} a An integer number + * @param {number | BigNumber} b An integer number + * @return {number | BigNumber } Returns an integer number + * where `invmod(a,b)*a ≣ 1 (mod b)` + */ + return typed(name, { + 'number, number': invmod, + 'BigNumber, BigNumber': invmod + }) + + function invmod (a, b) { + if (!isInteger(a) || !isInteger(b)) throw new Error('Parameters in function invmod must be integer numbers') + a = mod(a, b) + if (equal(b, 0)) throw new Error('Divisor must be non zero') + let res = xgcd(a, b) + res = res.valueOf() + let [gcd, inv] = res + if (!equal(gcd, BigNumber(1))) return NaN + inv = mod(inv, b) + if (smaller(inv, BigNumber(0))) inv = add(inv, b) + return inv + } +}) diff --git a/test/unit-tests/function/arithmetic/invmod.test.js b/test/unit-tests/function/arithmetic/invmod.test.js new file mode 100644 index 000000000..5072d6743 --- /dev/null +++ b/test/unit-tests/function/arithmetic/invmod.test.js @@ -0,0 +1,73 @@ +// test invmod +import assert from 'assert' + +import math from '../../../../src/defaultInstance.js' +const { invmod, complex, bignumber } = math + +describe('invmod', function () { + it('should find the multiplicative inverse for basic cases', () => { + assert.strictEqual(invmod(2, 7), 4) + assert.strictEqual(invmod(3, 11), 4) + assert.strictEqual(invmod(10, 17), 12) + }) + + it('should return NaN when there is no multiplicative inverse', () => { + assert(isNaN(invmod(3, 15))) + assert(isNaN(invmod(14, 7))) + assert(isNaN(invmod(42, 1200))) + }) + + it('should work when a≥b', () => { + assert.strictEqual(invmod(4, 3), 1) + assert(isNaN(invmod(7, 7))) + }) + + it('should work for negative values', () => { + assert.strictEqual(invmod(-2, 7), 3) + assert.strictEqual(invmod(-2000000, 21), 10) + }) + + it('should calculate invmod for BigNumbers', () => { + assert.deepStrictEqual(invmod(bignumber(13), bignumber(25)), bignumber(2)) + assert.deepStrictEqual(invmod(bignumber(-7), bignumber(48)), bignumber(41)) + }) + + it('should calculate invmod for mixed BigNumbers and Numbers', () => { + assert.deepStrictEqual(invmod(bignumber(44), 7), bignumber(4)) + assert.deepStrictEqual(invmod(4, math.bignumber(15)), bignumber(4)) + }) + + it('should throw an error if b is zero', function () { + assert.throws(function () { invmod(1, 0) }, /Divisor must be non zero/) + }) + + it('should throw an error if only one argument', function () { + assert.throws(function () { invmod(1) }, /TypeError: Too few arguments/) + }) + + it('should throw an error for non-integer numbers', function () { + assert.throws(function () { invmod(2, 4.1) }, /Parameters in function invmod must be integer numbers/) + assert.throws(function () { invmod(2.3, 4) }, /Parameters in function invmod must be integer numbers/) + }) + + it('should throw an error with complex numbers', function () { + assert.throws(function () { invmod(complex(1, 3), 2) }, /TypeError: Unexpected type of argument/) + }) + + it('should convert strings to numbers', function () { + assert.strictEqual(invmod('7', '15'), 13) + assert.strictEqual(invmod(7, '15'), 13) + assert.strictEqual(invmod('7', 15), 13) + + assert.throws(function () { invmod('a', 8) }, /Cannot convert "a" to a number/) + }) + + it('should throw an error with units', function () { + assert.throws(function () { invmod(math.unit('5cm'), 2) }, /TypeError: Unexpected type of argument/) + }) + + it('should LaTeX invmod', function () { + const expression = math.parse('invmod(2,3)') + assert.strictEqual(expression.toTex(), '\\mathrm{invmod}\\left(2,3\\right)') + }) +})