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:
parent
199ffd2654
commit
bcbb24acd2
@ -24,6 +24,7 @@
|
||||
},
|
||||
dependencies: {
|
||||
'bigint-isqrt': '^0.2.1',
|
||||
'fraction.js': '^4.2.0',
|
||||
'typed-function': '^3.0.0',
|
||||
},
|
||||
}
|
||||
|
@ -2,11 +2,13 @@ lockfileVersion: 5.4
|
||||
|
||||
specifiers:
|
||||
bigint-isqrt: ^0.2.1
|
||||
fraction.js: ^4.2.0
|
||||
mocha: ^10.0.0
|
||||
typed-function: ^3.0.0
|
||||
|
||||
dependencies:
|
||||
bigint-isqrt: 0.2.1
|
||||
fraction.js: 4.2.0
|
||||
typed-function: 3.0.0
|
||||
|
||||
devDependencies:
|
||||
@ -192,6 +194,10 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/fraction.js/4.2.0:
|
||||
resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==}
|
||||
dev: false
|
||||
|
||||
/fs.realpath/1.0.0:
|
||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||
dev: true
|
||||
|
@ -263,6 +263,7 @@ export default class PocomathInstance {
|
||||
* @param {{test: any => bool, // the predicate for the type
|
||||
* from: Record<string, <that type> => <type name>> // conversions
|
||||
* before: string[] // lower priority types
|
||||
* refines: string // The type this is a subtype of
|
||||
* }} specification
|
||||
*
|
||||
* The second parameter of this function specifies the structure of the
|
||||
@ -711,7 +712,12 @@ export default class PocomathInstance {
|
||||
if (argType === 'any') {
|
||||
throw TypeError(
|
||||
`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)
|
||||
}
|
||||
@ -947,7 +953,7 @@ export default class PocomathInstance {
|
||||
} else {
|
||||
throw new Error(
|
||||
'Implement inexact self-reference in typed-function for '
|
||||
+ neededSig)
|
||||
+ `${name}(${neededSig})`)
|
||||
}
|
||||
}
|
||||
const refs = imps[aSignature].builtRefs
|
||||
|
88
src/generic/Types/adapted.mjs
Normal file
88
src/generic/Types/adapted.mjs
Normal 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
7
src/generic/abs.mjs
Normal 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
6
src/generic/absquare.mjs
Normal file
@ -0,0 +1,6 @@
|
||||
export const absquare = {
|
||||
T: ({
|
||||
'square(T)': sq,
|
||||
'abs(T)': abval
|
||||
}) => t => sq(abval(t))
|
||||
}
|
@ -1,2 +1,22 @@
|
||||
import {adapted} from './Types/adapted.mjs'
|
||||
import Fraction from 'fraction.js/bigfraction.js'
|
||||
|
||||
export * from './arithmetic.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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -2,14 +2,18 @@ import {reducingOperation} from './reducingOperation.mjs'
|
||||
|
||||
export * from './Types/generic.mjs'
|
||||
|
||||
export {abs} from './abs.mjs'
|
||||
export {absquare} from './absquare.mjs'
|
||||
export const add = reducingOperation
|
||||
export {divide} from './divide.mjs'
|
||||
export const gcd = reducingOperation
|
||||
export {identity} from './identity.mjs'
|
||||
export {lcm} from './lcm.mjs'
|
||||
export {mean} from './mean.mjs'
|
||||
export {mod} from './mod.mjs'
|
||||
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 {sqrt} from './sqrt.mjs'
|
||||
export {square} from './square.mjs'
|
||||
|
3
src/generic/quotient.mjs
Normal file
3
src/generic/quotient.mjs
Normal file
@ -0,0 +1,3 @@
|
||||
export const quotient = {
|
||||
'T,T': ({'floor(T)': flr, 'divide(T,T)':div}) => (n,d) => flr(div(n,d))
|
||||
}
|
@ -3,7 +3,8 @@ export const compare = {
|
||||
}
|
||||
|
||||
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 = {
|
||||
@ -33,18 +34,20 @@ export const unequal = {
|
||||
export const larger = {
|
||||
'T,T': ({
|
||||
'compare(T,T)': cmp,
|
||||
'one(T)' : uno
|
||||
}) => (x,y) => cmp(x,y) === uno(y)
|
||||
'one(T)' : uno,
|
||||
'equalTT(T,T)' : eq
|
||||
}) => (x,y) => eq(cmp(x,y), uno(y))
|
||||
}
|
||||
|
||||
export const largerEq = {
|
||||
'T,T': ({
|
||||
'compare(T,T)': cmp,
|
||||
'one(T)' : uno,
|
||||
'isZero(T)' : isZ
|
||||
'isZero(T)' : isZ,
|
||||
'equalTT(T,T)': eq
|
||||
}) => (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': ({
|
||||
'compare(T,T)': cmp,
|
||||
'one(T)' : uno,
|
||||
'isZero(T)' : isZ
|
||||
'isZero(T)' : isZ,
|
||||
unequal
|
||||
}) => (x,y) => {
|
||||
const c = cmp(x,y)
|
||||
return !isZ(c) && c !== uno(y)
|
||||
return !isZ(c) && unequal(c, uno(y))
|
||||
}
|
||||
}
|
||||
|
||||
export const smallerEq = {
|
||||
'T,T': ({
|
||||
'compare(T,T)': cmp,
|
||||
'one(T)' : uno
|
||||
}) => (x,y) => cmp(x,y) !== uno(y)
|
||||
'one(T)' : uno,
|
||||
unequal
|
||||
}) => (x,y) => unequal(cmp(x,y), uno(y))
|
||||
}
|
||||
|
3
src/generic/roundquotient.mjs
Normal file
3
src/generic/roundquotient.mjs
Normal file
@ -0,0 +1,3 @@
|
||||
export const roundquotient = {
|
||||
'T,T': ({'round(T)': rnd, 'divide(T,T)':div}) => (n,d) => rnd(div(n,d))
|
||||
}
|
@ -1,3 +1,6 @@
|
||||
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
|
||||
}
|
||||
|
@ -1,8 +1,15 @@
|
||||
export * from './Types/number.mjs'
|
||||
|
||||
export const quotient = {
|
||||
'number,number': () => (n,d) => {
|
||||
if (d === 0) return d
|
||||
return Math.floor(n/d)
|
||||
}
|
||||
const intquotient = () => (n,d) => {
|
||||
if (d === 0) return 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
|
||||
}
|
||||
|
@ -21,5 +21,10 @@ export const floor = {
|
||||
|
||||
// OK to include a type totally not in Pocomath yet, it'll never be
|
||||
// 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
95
test/generic/fraction.mjs
Normal 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))
|
||||
})
|
||||
|
||||
})
|
Loading…
Reference in New Issue
Block a user