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)) + }) + +})