Merge pull request 'feat: Generic numerical types' (#50) from numeric_adapter into main

Reviewed-on: #50
This commit is contained in:
Glen Whitney 2022-08-07 16:34:43 +00:00
commit 9267a8df60
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))
})
})