Skip to content

Commit

Permalink
add invmod (modular multiplicative inverse) (#2368)
Browse files Browse the repository at this point in the history
* add invmod (modular multiplicative inverse)

* implement (most) suggestions

* style error

* fix NaN tests

Co-authored-by: Jos de Jong <wjosdejong@gmail.com>
  • Loading branch information
thetazero and josdejong authored Jan 15, 2022
1 parent af0758a commit 7beac55
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/expression/embeddedDocs/embeddedDocs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -369,6 +370,7 @@ export const embeddedDocs = {
unaryMinus: unaryMinusDocs,
unaryPlus: unaryPlusDocs,
xgcd: xgcdDocs,
invmod: invmodDocs,

// functions - bitwise
bitAnd: bitAndDocs,
Expand Down
14 changes: 14 additions & 0 deletions src/expression/embeddedDocs/function/arithmetic/invmod.js
Original file line number Diff line number Diff line change
@@ -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']
}
1 change: 1 addition & 0 deletions src/factoriesAny.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
47 changes: 47 additions & 0 deletions src/function/arithmetic/invmod.js
Original file line number Diff line number Diff line change
@@ -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
}
})
73 changes: 73 additions & 0 deletions test/unit-tests/function/arithmetic/invmod.test.js
Original file line number Diff line number Diff line change
@@ -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)')
})
})

0 comments on commit 7beac55

Please sign in to comment.