From bcbb24acd258477ed6145716a510c7018fd02262 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 7 Aug 2022 09:19:27 -0700 Subject: [PATCH] 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. --- package.json5 | 1 + pnpm-lock.yaml | 6 +++ src/core/PocomathInstance.mjs | 10 +++- src/generic/Types/adapted.mjs | 88 ++++++++++++++++++++++++++++++++ src/generic/abs.mjs | 7 +++ src/generic/absquare.mjs | 6 +++ src/generic/all.mjs | 20 ++++++++ src/generic/arithmetic.mjs | 6 ++- src/generic/quotient.mjs | 3 ++ src/generic/relational.mjs | 23 +++++---- src/generic/roundquotient.mjs | 3 ++ src/number/isZero.mjs | 5 +- src/number/quotient.mjs | 17 +++++-- src/ops/floor.mjs | 7 ++- test/generic/fraction.mjs | 95 +++++++++++++++++++++++++++++++++++ 15 files changed, 278 insertions(+), 19 deletions(-) create mode 100644 src/generic/Types/adapted.mjs create mode 100644 src/generic/abs.mjs create mode 100644 src/generic/absquare.mjs create mode 100644 src/generic/quotient.mjs create mode 100644 src/generic/roundquotient.mjs create mode 100644 test/generic/fraction.mjs diff --git a/package.json5 b/package.json5 index a0647ae..1862178 100644 --- a/package.json5 +++ b/package.json5 @@ -24,6 +24,7 @@ }, dependencies: { 'bigint-isqrt': '^0.2.1', + 'fraction.js': '^4.2.0', 'typed-function': '^3.0.0', }, } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4063068..eef07ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index bb013d1..1add7eb 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -263,6 +263,7 @@ export default class PocomathInstance { * @param {{test: any => bool, // the predicate for the type * from: Record => > // 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 diff --git a/src/generic/Types/adapted.mjs b/src/generic/Types/adapted.mjs new file mode 100644 index 0000000..ba20889 --- /dev/null +++ b/src/generic/Types/adapted.mjs @@ -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} diff --git a/src/generic/abs.mjs b/src/generic/abs.mjs new file mode 100644 index 0000000..84ebc31 --- /dev/null +++ b/src/generic/abs.mjs @@ -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) +} diff --git a/src/generic/absquare.mjs b/src/generic/absquare.mjs new file mode 100644 index 0000000..26d6717 --- /dev/null +++ b/src/generic/absquare.mjs @@ -0,0 +1,6 @@ +export const absquare = { + T: ({ + 'square(T)': sq, + 'abs(T)': abval + }) => t => sq(abval(t)) +} diff --git a/src/generic/all.mjs b/src/generic/all.mjs index 19ba165..7132944 100644 --- a/src/generic/all.mjs +++ b/src/generic/all.mjs @@ -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 + } + } + } +}) diff --git a/src/generic/arithmetic.mjs b/src/generic/arithmetic.mjs index 00faddb..51f0da9 100644 --- a/src/generic/arithmetic.mjs +++ b/src/generic/arithmetic.mjs @@ -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' diff --git a/src/generic/quotient.mjs b/src/generic/quotient.mjs new file mode 100644 index 0000000..54e000a --- /dev/null +++ b/src/generic/quotient.mjs @@ -0,0 +1,3 @@ +export const quotient = { + 'T,T': ({'floor(T)': flr, 'divide(T,T)':div}) => (n,d) => flr(div(n,d)) +} diff --git a/src/generic/relational.mjs b/src/generic/relational.mjs index 939ae19..a1639a1 100644 --- a/src/generic/relational.mjs +++ b/src/generic/relational.mjs @@ -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)) } diff --git a/src/generic/roundquotient.mjs b/src/generic/roundquotient.mjs new file mode 100644 index 0000000..5346882 --- /dev/null +++ b/src/generic/roundquotient.mjs @@ -0,0 +1,3 @@ +export const roundquotient = { + 'T,T': ({'round(T)': rnd, 'divide(T,T)':div}) => (n,d) => rnd(div(n,d)) +} diff --git a/src/number/isZero.mjs b/src/number/isZero.mjs index ac98ce2..c15549e 100644 --- a/src/number/isZero.mjs +++ b/src/number/isZero.mjs @@ -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 +} diff --git a/src/number/quotient.mjs b/src/number/quotient.mjs index d86b832..e8ed83a 100644 --- a/src/number/quotient.mjs +++ b/src/number/quotient.mjs @@ -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 } diff --git a/src/ops/floor.mjs b/src/ops/floor.mjs index 71c8e70..5fdb9e7 100644 --- a/src/ops/floor.mjs +++ b/src/ops/floor.mjs @@ -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() + } diff --git a/test/generic/fraction.mjs b/test/generic/fraction.mjs new file mode 100644 index 0000000..9af3b8d --- /dev/null +++ b/test/generic/fraction.mjs @@ -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)) + }) + +})