feat: Generic numerical types

Inspired by https://github.com/josdejong/mathjs/discussions/2212 and
  https://github.com/josdejong/mathjs/issues/2585.

  Provides a simple adapter function `adapted` which takes a class
  implementing an arithmetical datatype and returns a PocomathInstance
  with a new type for that class, invoking the methods of the class
  in a standard way for the Pocomath/mathjs operations.  (That instance
  can then be installed in another to add the new type to any instance
  you like, including the default one.)

  Uses this facility to bring fraction.js Fraction into Pocomath, and
  tests the resulting type.

  Currently the "standard" interface for an arithmetical type is heavily modeled
  after the design of fraction.js, but with experience with other 3rd-party types
  it could be streamlined to something pretty generic (and the Fraction
  adaptation could be patched to conform to the resulting "standard"). Or a
  proposal along the lines of https://github.com/josdejong/mathjs/discussions/2212
  could be adopted, and a shim could be added to fraction.js to conform to
  **that** standard.

  Resolves #30.
This commit is contained in:
Glen Whitney 2022-08-07 09:19:27 -07:00
parent 199ffd2654
commit bcbb24acd2
15 changed files with 278 additions and 19 deletions

View File

@ -24,6 +24,7 @@
}, },
dependencies: { dependencies: {
'bigint-isqrt': '^0.2.1', 'bigint-isqrt': '^0.2.1',
'fraction.js': '^4.2.0',
'typed-function': '^3.0.0', 'typed-function': '^3.0.0',
}, },
} }

View File

@ -2,11 +2,13 @@ lockfileVersion: 5.4
specifiers: specifiers:
bigint-isqrt: ^0.2.1 bigint-isqrt: ^0.2.1
fraction.js: ^4.2.0
mocha: ^10.0.0 mocha: ^10.0.0
typed-function: ^3.0.0 typed-function: ^3.0.0
dependencies: dependencies:
bigint-isqrt: 0.2.1 bigint-isqrt: 0.2.1
fraction.js: 4.2.0
typed-function: 3.0.0 typed-function: 3.0.0
devDependencies: devDependencies:
@ -192,6 +194,10 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/fraction.js/4.2.0:
resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==}
dev: false
/fs.realpath/1.0.0: /fs.realpath/1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
dev: true dev: true

View File

@ -263,6 +263,7 @@ export default class PocomathInstance {
* @param {{test: any => bool, // the predicate for the type * @param {{test: any => bool, // the predicate for the type
* from: Record<string, <that type> => <type name>> // conversions * from: Record<string, <that type> => <type name>> // conversions
* before: string[] // lower priority types * before: string[] // lower priority types
* refines: string // The type this is a subtype of
* }} specification * }} specification
* *
* The second parameter of this function specifies the structure of the * The second parameter of this function specifies the structure of the
@ -711,7 +712,12 @@ export default class PocomathInstance {
if (argType === 'any') { if (argType === 'any') {
throw TypeError( throw TypeError(
`In call to ${name}, incompatible template arguments: ` `In call to ${name}, incompatible template arguments: `
+ args.map(a => JSON.stringify(a)).join(', ')) // + args.map(a => JSON.stringify(a)).join(', ')
// unfortunately barfs on bigints. Need a better formatter
// wish we could just use the one that console.log uses;
// is that accessible somehow?
+ args.map(a => a.toString()).join(', ')
+ ' of types ' + argTypes.join(', ') + argType)
} }
argTypes.push(argType) argTypes.push(argType)
} }
@ -947,7 +953,7 @@ export default class PocomathInstance {
} else { } else {
throw new Error( throw new Error(
'Implement inexact self-reference in typed-function for ' 'Implement inexact self-reference in typed-function for '
+ neededSig) + `${name}(${neededSig})`)
} }
} }
const refs = imps[aSignature].builtRefs const refs = imps[aSignature].builtRefs

View File

@ -0,0 +1,88 @@
import PocomathInstance from '../../core/PocomathInstance.mjs'
/* creates a PocomathInstance incorporating a new numeric type encapsulated
* as a class. (This instance can the be `install()ed` in another to add the
* type so created.)
*
* @param {string} name The name of the new type
* @param {class} Thing The class implementing the new type
* @param {object} overrides Patches to the auto-generated adaptation
*/
export default function adapted(name, Thing, overrides) {
const thing = new PocomathInstance('Adapted Thing')
const test = overrides.isa || Thing.isa || (x => x instanceof Thing)
thing.installType(name, {
test,
from: overrides.from || {},
before: overrides.before || [],
refines: overrides.refines || undefined
})
// Build the operations for Thing
const operations = {}
// first a creator function, with name depending on the name of the thing:
const creatorName = overrides.creatorName || name.toLowerCase()
const creator = overrides[creatorName]
? overrides[creatorName]('')
: Thing[creatorName]
? (Thing[creatorName])
: ((...args) => new Thing(...args))
const defaultCreatorImps = {
'': () => () => creator(),
'...any': () => args => creator(...args)
}
defaultCreatorImps[name] = () => x => x // x.clone(x)?
operations[creatorName] = overrides[creatorName] || defaultCreatorImps
// We make the default instance, just as a place to check for methods
const instance = overrides.instance || creator()
// Now adapt the methods to typed-function:
const unaryOps = {
abs: 'abs',
ceiling: 'ceil',
floor: 'floor',
invert: 'inverse',
round: 'round',
sqrt: 'sqrt',
negate: 'neg'
}
const binaryOps = {
add: 'add',
compare: 'compare',
divide: 'div',
equalTT: 'equals',
gcd: 'gcd',
lcm: 'lcm',
mod: 'mod',
multiply: 'mul',
subtract: 'sub'
}
for (const [mathname, standardname] of Object.entries(unaryOps)) {
if (standardname in instance) {
operations[mathname] = {}
operations[mathname][name] = () => t => t[standardname]()
}
}
operations.zero = {}
operations.zero[name] = () => t => creator()
operations.one = {}
operations.one[name] = () => t => creator(1)
operations.conjugate = {}
operations.conjugate[name] = () => t => t // or t.clone() ??
const binarySignature = `${name},${name}`
for (const [mathname, standardname] of Object.entries(binaryOps)) {
if (standardname in instance) {
operations[mathname] = {}
operations[mathname][binarySignature] = () => (t,u) => t[standardname](u)
}
}
if ('operations' in overrides) {
Object.assign(operations, overrides.operations)
}
thing.install(operations)
return thing
}
export {adapted}

7
src/generic/abs.mjs Normal file
View File

@ -0,0 +1,7 @@
export const abs = {
T: ({
'smaller(T,T)': lt,
'negate(T)': neg,
'zero(T)': zr
}) => t => (smaller(t, zr(t)) ? neg(t) : t)
}

6
src/generic/absquare.mjs Normal file
View File

@ -0,0 +1,6 @@
export const absquare = {
T: ({
'square(T)': sq,
'abs(T)': abval
}) => t => sq(abval(t))
}

View File

@ -1,2 +1,22 @@
import {adapted} from './Types/adapted.mjs'
import Fraction from 'fraction.js/bigfraction.js'
export * from './arithmetic.mjs' export * from './arithmetic.mjs'
export * from './relational.mjs' export * from './relational.mjs'
export const fraction = adapted('Fraction', Fraction, {
before: ['Complex'],
from: {number: n => new Fraction(n)},
operations: {
compare: {'Fraction,Fraction': () => (f,g) => new Fraction(f.compare(g))},
mod: {
'Fraction,Fraction': () => (n,d) => {
// patch for "mathematician's modulus"
// OK to use full public API of Fraction here
const fmod = n.mod(d)
if (fmod.s === -1n) return fmod.add(d.abs())
return fmod
}
}
}
})

View File

@ -2,14 +2,18 @@ import {reducingOperation} from './reducingOperation.mjs'
export * from './Types/generic.mjs' export * from './Types/generic.mjs'
export {abs} from './abs.mjs'
export {absquare} from './absquare.mjs'
export const add = reducingOperation export const add = reducingOperation
export {divide} from './divide.mjs'
export const gcd = reducingOperation export const gcd = reducingOperation
export {identity} from './identity.mjs' export {identity} from './identity.mjs'
export {lcm} from './lcm.mjs' export {lcm} from './lcm.mjs'
export {mean} from './mean.mjs' export {mean} from './mean.mjs'
export {mod} from './mod.mjs' export {mod} from './mod.mjs'
export const multiply = reducingOperation export const multiply = reducingOperation
export {divide} from './divide.mjs' export {quotient} from './quotient.mjs'
export {roundquotient} from './roundquotient.mjs'
export {sign} from './sign.mjs' export {sign} from './sign.mjs'
export {sqrt} from './sqrt.mjs' export {sqrt} from './sqrt.mjs'
export {square} from './square.mjs' export {square} from './square.mjs'

3
src/generic/quotient.mjs Normal file
View File

@ -0,0 +1,3 @@
export const quotient = {
'T,T': ({'floor(T)': flr, 'divide(T,T)':div}) => (n,d) => flr(div(n,d))
}

View File

@ -3,7 +3,8 @@ export const compare = {
} }
export const isZero = { export const isZero = {
'undefined': () => u => u === 0 'undefined': () => u => u === 0,
T: ({'equal(T,T)': eq, 'zero(T)': zr}) => t => eq(t, zr(t))
} }
export const equal = { export const equal = {
@ -33,18 +34,20 @@ export const unequal = {
export const larger = { export const larger = {
'T,T': ({ 'T,T': ({
'compare(T,T)': cmp, 'compare(T,T)': cmp,
'one(T)' : uno 'one(T)' : uno,
}) => (x,y) => cmp(x,y) === uno(y) 'equalTT(T,T)' : eq
}) => (x,y) => eq(cmp(x,y), uno(y))
} }
export const largerEq = { export const largerEq = {
'T,T': ({ 'T,T': ({
'compare(T,T)': cmp, 'compare(T,T)': cmp,
'one(T)' : uno, 'one(T)' : uno,
'isZero(T)' : isZ 'isZero(T)' : isZ,
'equalTT(T,T)': eq
}) => (x,y) => { }) => (x,y) => {
const c = cmp(x,y) const c = cmp(x,y)
return isZ(c) || c === uno(y) return isZ(c) || eq(c, uno(y))
} }
} }
@ -52,16 +55,18 @@ export const smaller = {
'T,T': ({ 'T,T': ({
'compare(T,T)': cmp, 'compare(T,T)': cmp,
'one(T)' : uno, 'one(T)' : uno,
'isZero(T)' : isZ 'isZero(T)' : isZ,
unequal
}) => (x,y) => { }) => (x,y) => {
const c = cmp(x,y) const c = cmp(x,y)
return !isZ(c) && c !== uno(y) return !isZ(c) && unequal(c, uno(y))
} }
} }
export const smallerEq = { export const smallerEq = {
'T,T': ({ 'T,T': ({
'compare(T,T)': cmp, 'compare(T,T)': cmp,
'one(T)' : uno 'one(T)' : uno,
}) => (x,y) => cmp(x,y) !== uno(y) unequal
}) => (x,y) => unequal(cmp(x,y), uno(y))
} }

View File

@ -0,0 +1,3 @@
export const roundquotient = {
'T,T': ({'round(T)': rnd, 'divide(T,T)':div}) => (n,d) => rnd(div(n,d))
}

View File

@ -1,3 +1,6 @@
export * from './Types/number.mjs' export * from './Types/number.mjs'
export const isZero = {number: () => n => n === 0} export const isZero = {
number: () => n => n === 0,
NumInt: () => n => n === 0 // necessary because of generic template
}

View File

@ -1,8 +1,15 @@
export * from './Types/number.mjs' export * from './Types/number.mjs'
export const quotient = { const intquotient = () => (n,d) => {
'number,number': () => (n,d) => {
if (d === 0) return d if (d === 0) return d
return Math.floor(n/d) return Math.floor(n/d)
} }
export const quotient = {
// Hmmm, seem to need all of these because of the generic template version
// Should be a way around that
'NumInt,NumInt': intquotient,
'NumInt,number': intquotient,
'number,NumInt': intquotient,
'number,number': intquotient
} }

View File

@ -21,5 +21,10 @@ export const floor = {
// OK to include a type totally not in Pocomath yet, it'll never be // OK to include a type totally not in Pocomath yet, it'll never be
// activated. // activated.
Fraction: ({quotient}) => f => quotient(f.n, f.d), // Fraction: ({quotient}) => f => quotient(f.n, f.d), // oops have that now
BigNumber: ({
'round(BigNumber)': rnd,
'equal(BigNumber,BigNumber)': eq
}) => x => eq(x,round(x)) ? round(x) : x.floor()
} }

95
test/generic/fraction.mjs Normal file
View File

@ -0,0 +1,95 @@
import assert from 'assert'
import math from '../../src/pocomath.mjs'
import Fraction from 'fraction.js/bigfraction.js'
describe('fraction', () => {
const half = new Fraction(1/2)
const tf = new Fraction(3, 4)
let zero // will fill in during a test
const one = new Fraction(1)
it('supports typeOf', () => {
assert.strictEqual(math.typeOf(half), 'Fraction')
})
it('can be built', () => {
zero = math.fraction()
assert.deepStrictEqual(zero, new Fraction(0))
assert.deepStrictEqual(math.fraction(1/2), half)
assert.strictEqual(math.fraction(half), half) // maybe it should be a clone?
assert.strictEqual(math.fraction(9, 16).valueOf(), 9/16)
assert.strictEqual(math.fraction(9n, 16n).valueOf(), 9/16)
})
it('has abs and sign', () => {
assert.deepStrictEqual(math.abs(math.fraction('-1/2')), half)
assert.deepStrictEqual(math.sign(math.negate(tf)), math.negate(one))
})
it('can add and multiply', () => {
assert.strictEqual(math.add(half, 1).valueOf(), 1.5)
assert.strictEqual(math.multiply(2, half).valueOf(), 1)
})
it('can subtract and divide', () => {
assert.strictEqual(math.subtract(half,tf).valueOf(), -0.25)
assert.strictEqual(math.divide(tf,half).valueOf(), 1.5)
})
it('computes mod', () => {
assert.strictEqual(math.mod(tf, half).valueOf(), 0.25)
assert.strictEqual(math.mod(tf, math.negate(half)).valueOf(), 0.25)
assert.strictEqual(math.mod(math.negate(tf), half).valueOf(), 0.25)
assert.strictEqual(
math.mod(math.negate(tf), math.negate(half)).valueOf(),
0.25)
assert.deepStrictEqual(
math.mod(math.fraction(-1, 3), half),
math.fraction(1, 6))
})
it('supports conjugate', () => {
assert.strictEqual(math.conjugate(half), half)
})
it('can compare fractions', () => {
assert.deepStrictEqual(math.compare(tf, half), one)
assert.strictEqual(math.equal(half, math.fraction("2/4")), true)
assert.strictEqual(math.smaller(half, tf), true)
assert.strictEqual(math.larger(half, tf), false)
assert.strictEqual(math.smallerEq(tf, math.fraction(0.75)), true)
assert.strictEqual(math.largerEq(tf, half), true)
assert.strictEqual(math.unequal(half, tf), true)
assert.strictEqual(math.isZero(math.zero(tf)), true)
assert.strictEqual(math.isZero(half), false)
})
it('computes gcd and lcm', () => {
assert.strictEqual(math.gcd(half,tf).valueOf(), 0.25)
assert.strictEqual(math.lcm(half,tf).valueOf(), 1.5)
})
it('computes additive and multiplicative inverses', () => {
assert.strictEqual(math.negate(half).valueOf(), -0.5)
assert.deepStrictEqual(math.invert(tf), math.fraction('4/3'))
})
it('computes integer parts and quotients', () => {
assert.deepStrictEqual(math.floor(tf), zero)
assert.deepStrictEqual(math.round(tf), one)
assert.deepStrictEqual(math.ceiling(half), one)
assert.deepStrictEqual(math.quotient(tf, half), one)
assert.deepStrictEqual(
math.roundquotient(math.fraction(7/8), half),
math.multiply(2,math.one(tf)))
})
it('has no sqrt (although that should be patched)', () => {
assert.throws(() => math.sqrt(math.fraction(9/16)), TypeError)
})
it('but it can square', () => {
assert.deepStrictEqual(math.square(tf), math.fraction(9/16))
})
})