feat: Generic numerical types #50
@ -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',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
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 './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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
@ -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
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 = {
|
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))
|
||||||
}
|
}
|
||||||
|
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 * 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 * 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
|
||||||
}
|
}
|
||||||
|
@ -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
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