feat: Generic numerical types #50
@ -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) => {
|
||||
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