Compare commits

...

10 Commits

Author SHA1 Message Date
Glen Whitney 36e7b750ce refactor: Make generics more strict via templates
This commit also adds a built-in `typeOf` function to every PocomathInstance.
  It also adds a notation (prefix '!') for "eager" templeates that
  instantiate themselves for all types immediately upon definition.
2022-08-01 03:02:38 -07:00
Glen Whitney fd3d6b2eb3 feat: Template implementations 2022-07-31 23:33:58 -07:00
Glen Whitney 1076c3c727 feat: add built-in typeOf operator to PocomathInstance 2022-07-31 21:26:16 -07:00
Glen Whitney 7bbdc049ce refactor: add stub for patch to generic template behavior 2022-07-31 21:08:28 -07:00
Glen Whitney 171b6941f4 refactor: add stub for instantiating the template 2022-07-31 11:10:41 -07:00
Glen Whitney 5cea71cb25 refactor: isolate in the code the point of template instantiation 2022-07-31 11:10:41 -07:00
Glen Whitney 0fbcbf661e refactor: separate the generation of a typed-function implementation 2022-07-31 11:10:41 -07:00
Glen Whitney 5c3716ff99 refactor: Avoid use of a "magic string " 'T' 2022-07-31 11:10:41 -07:00
Glen Whitney d4a4d8f331 refactor: track all the 'prior types' of a type as types are installed
These are exactly the types that might match before the given type, and
  hence which require to have their template instantiations defined when
  an instantiation with the given type is made.
2022-07-31 11:10:41 -07:00
Glen Whitney 883a351aa8 refactor: Add stubs for function templates
The stubs are so far all no-ops so basically at the moment the only
  allowed template parameter 'T' is just a synonym for 'any'. But at least
  this allows us to put the definition of the generic `subtract` into
  the exact form we actually want to support.
2022-07-31 11:10:41 -07:00
29 changed files with 480 additions and 119 deletions

View File

@ -1,5 +1,3 @@
export * from './Types/bigint.mjs'
export const add = {
'...bigint': () => addends => addends.reduce((x,y) => x+y, 0n)
}
export const add = {'bigint,bigint': () => (a,b) => a+b}

View File

@ -1,5 +1,5 @@
import PocomathInstance from '../core/PocomathInstance.mjs'
import * as bigints from './native.mjs'
import * as generic from '../generic/arithmetic.mjs'
import * as generic from '../generic/all.mjs'
export default PocomathInstance.merge('bigint', bigints, generic)

5
src/bigint/compare.mjs Normal file
View File

@ -0,0 +1,5 @@
export * from './Types/bigint.mjs'
export const compare = {
'bigint,bigint': () => (a,b) => a === b ? 0n : (a > b ? 1n : -1n)
}

View File

@ -3,6 +3,7 @@ import gcdType from '../generic/gcdType.mjs'
export * from './Types/bigint.mjs'
export {add} from './add.mjs'
export {compare} from './compare.mjs'
export {divide} from './divide.mjs'
export const gcd = gcdType('bigint')
export {isZero} from './isZero.mjs'

View File

@ -1,10 +1,22 @@
export * from './Types/Complex.mjs'
export const add = {
'...Complex': ({self}) => addends => {
if (addends.length === 0) return {re:0, im:0}
const seed = addends.shift()
return addends.reduce(
(w,z) => ({re: self(w.re, z.re), im: self(w.im, z.im)}), seed)
}
/* Relying on conversions for both complex + number and complex + bigint
* leads to an infinite loop when adding a number and a bigint, since they
* both convert to Complex.
*/
'Complex,number': ({
'self(number,number)': addNum,
'complex(any,any)': cplx
}) => (z,x) => cplx(addNum(z.re, x), z.im),
'Complex,bigint': ({
'self(bigint,bigint)': addBigInt,
'complex(any,any)': cplx
}) => (z,x) => cplx(addBigInt(z.re, x), z.im),
'Complex,Complex': ({
self,
'complex(any,any)': cplx
}) => (w,z) => cplx(self(w.re, z.re), self(w.im, z.im))
}

19
src/complex/equal.mjs Normal file
View File

@ -0,0 +1,19 @@
export * from './Types/Complex.mjs'
export const equal = {
'Complex,number': ({
'isZero(number)': isZ,
'self(number,number)': eqNum
}) => (z, x) => eqNum(z.re, x) && isZ(z.im),
'Complex,bigint': ({
'isZero(bigint)': isZ,
'self(bigint,bigint)': eqBigInt
}) => (z, b) => eqBigInt(z.re, b) && isZ(z.im),
'Complex,Complex': ({self}) => (w,z) => self(w.re, z.re) && self(w.im, z.im),
'GaussianInteger,GaussianInteger': ({
'self(bigint,bigint)': eq
}) => (a,b) => eq(a.re, b.re) && eq(a.im, b.im)
}

View File

@ -7,6 +7,7 @@ export {absquare} from './absquare.mjs'
export {add} from './add.mjs'
export {conjugate} from './conjugate.mjs'
export {complex} from './complex.mjs'
export {equal} from './equal.mjs'
export {gcd} from './gcd.mjs'
export {invert} from './invert.mjs'
export {isZero} from './isZero.mjs'

View File

@ -3,7 +3,7 @@ export * from './Types/Complex.mjs'
export const sqrt = {
Complex: ({
config,
zero,
isZero,
sign,
one,
add,
@ -16,11 +16,8 @@ export const sqrt = {
}) => {
if (config.predictable) {
return z => {
const imZero = zero(z.im)
const imSign = sign(z.im)
const reOne = one(z.re)
const reSign = sign(z.re)
if (imSign === imZero && reSign === reOne) return complex(self(z.re))
if (isZero(z.im) && sign(z.re) === reOne) return complex(self(z.re))
const reTwo = add(reOne, reOne)
return complex(
multiply(sign(z.im), self(divide(add(abs(z),z.re), reTwo))),
@ -29,11 +26,8 @@ export const sqrt = {
}
}
return z => {
const imZero = zero(z.im)
const imSign = sign(z.im)
const reOne = one(z.re)
const reSign = sign(z.re)
if (imSign === imZero && reSign === reOne) return self(z.re)
if (isZero(z.im) && sign(z.re) === reOne) return self(z.re)
const reTwo = add(reOne, reOne)
return complex(
multiply(sign(z.im), self(divide(add(abs(z),z.re), reTwo))),

View File

@ -5,6 +5,16 @@ import {subsetOfKeys, typesOfSignature} from './utils.mjs'
const anySpec = {} // fixed dummy specification of 'any' type
const theTemplateParam = 'T' // First pass: only allow this one exact parameter
/* Returns a new signature just like sig but with the parameter replaced by
* the type
*/
function substituteInSig(sig, parameter, type) {
const pattern = new RegExp("\\b" + parameter + "\\b", 'g')
return sig.replaceAll(pattern, type)
}
export default class PocomathInstance {
/* Disallowed names for ops; beware, this is slightly non-DRY
* in that if a new top-level PocomathInstance method is added, its name
@ -16,6 +26,7 @@ export default class PocomathInstance {
'install',
'installType',
'name',
'typeOf',
'Types',
'undefinedTypes'
])
@ -26,11 +37,22 @@ export default class PocomathInstance {
this._affects = {}
this._typed = typed.create()
this._typed.clear()
this.Types = {any: anySpec} // dummy entry to track the default 'any' type
/* List of types installed in the instance. We start with just dummies
* for the 'any' type and for type parameters:
*/
this.Types = {any: anySpec}
this.Types[theTemplateParam] = anySpec
this._subtypes = {} // For each type, gives all of its (in)direct subtypes
/* The following gives for each type, a set of all types that could
* match in typed-function's dispatch algorithm before the given type.
* This is important because if we instantiate a template, we must
* instantiate it for all prior types as well, or else the wrong instance
* might match.
*/
this._priorTypes = {}
this._usedTypes = new Set() // all types that have occurred in a signature
this._doomed = new Set() // for detecting circular reference
this._config = {predictable: false}
this._config = {predictable: false, epsilon: 1e-12}
const self = this
this.config = new Proxy(this._config, {
get: (target, property) => target[property],
@ -85,6 +107,20 @@ export default class PocomathInstance {
* refer to just adding two numbers. In this case, it is of course
* necessary to specify an alias to be able to refer to the supplied
* operation in the body of the implementation.
*
* You can specify template implementations. If any item in the signature
* contains the word 'T' (currently the only allowed type parameter) then
* the signature/implementation is a template. The T can match any type
* of argument, and it may appear in the dependencies, where it is
* replaced by the matching type. A bare 'T' in the dependencies will be
* supplied with the name of the type as its value. See the implementation
* of `subtract` for an example.
* Usually templates are instantiated as needed, but for some heavily
* used functions, or functions with non-template signatures that refer
* to signatures generated from a template, it makes more sense to just
* instantiate the template immediately for all known types. This eager
* instantiation can be accomplished by prefixin the signature with an
* exclamation point.
*/
install(ops) {
if (ops instanceof PocomathInstance) {
@ -130,9 +166,16 @@ export default class PocomathInstance {
_installInstance(other) {
for (const [type, spec] of Object.entries(other.Types)) {
if (type === 'any' || this._templateParam(type)) continue
this.installType(type, spec)
}
this._installFunctions(other._imps)
const migrateImps = {}
for (const operator in other._imps) {
if (operator != 'typeOf') { // skip the builtin, we already have it
migrateImps[operator] = other._imps[operator]
}
}
this._installFunctions(migrateImps)
}
/**
@ -166,7 +209,12 @@ export default class PocomathInstance {
const mod = await import(modName)
this.install(mod)
} catch (err) {
// No such module, but that's OK
if (!(err.message.includes('find'))) {
// Not just a error because module doesn't exist
// So take it seriously
throw err
}
// We don't care if a module doesn't exist, so merely proceed
}
}
doneSet.add(name)
@ -200,6 +248,10 @@ export default class PocomathInstance {
* the corresponding changes to the _typed object immediately
*/
installType(type, spec) {
if (this._templateParam(type)) {
throw new SyntaxError(
`Type name '${type}' reserved for template parameter`)
}
if (type in this.Types) {
if (spec !== this.Types[type]) {
throw new SyntaxError(`Conflicting definitions of type ${type}`)
@ -227,6 +279,7 @@ export default class PocomathInstance {
}
this._typed.addTypes([{name: type, test: testFn}], beforeType)
this.Types[type] = spec
this._priorTypes[type] = new Set()
/* Now add conversions to this type */
for (const from in (spec.from || {})) {
if (from in this.Types) {
@ -235,6 +288,8 @@ export default class PocomathInstance {
while (nextSuper) {
this._typed.addConversion(
{from, to: nextSuper, convert: spec.from[from]})
this._invalidateDependents(':' + nextSuper)
this._priorTypes[nextSuper].add(from)
nextSuper = this.Types[nextSuper].refines
}
}
@ -253,23 +308,30 @@ export default class PocomathInstance {
to: nextSuper,
convert: this.Types[to].from[type]
})
this._invalidateDependents(':' + nextSuper)
this._priorTypes[nextSuper].add(type)
nextSuper = this.Types[nextSuper].refines
}
}
}
if (spec.refines) {
this._typed.addConversion(
{from: type, to: spec.refines, convert: x => x})
}
// Update all the subtype sets of supertypes up the chain, and
// while we are at it add trivial conversions from subtypes to supertypes
// to help typed-function match signatures properly:
this._subtypes[type] = new Set()
// Update all the subtype sets of supertypes up the chain:
let nextSuper = spec.refines
while (nextSuper) {
this._typed.addConversion(
{from: type, to: nextSuper, convert: x => x})
this._invalidateDependents(':' + nextSuper)
this._priorTypes[nextSuper].add(type)
this._subtypes[nextSuper].add(type)
nextSuper = this.Types[nextSuper].refines
}
// rebundle anything that uses the new type:
this._invalidateDependents(':' + type)
// update the typeOf function
const imp = {}
imp[type] = {uses: new Set(), does: () => () => type}
this._installFunctions({typeOf: imp})
}
/* Returns a list of all types that have been mentioned in the
@ -292,13 +354,17 @@ export default class PocomathInstance {
`Conflicting definitions of ${signature} for ${name}`)
}
} else {
opImps[signature] = behavior
// Must avoid aliasing into another instance:
opImps[signature] = {uses: behavior.uses, does: behavior.does}
for (const dep of behavior.uses) {
const depname = dep.split('(', 1)[0]
if (depname === 'self') continue
if (depname === 'self' || this._templateParam(depname)) {
continue
}
this._addAffect(depname, name)
}
for (const type of typesOfSignature(signature)) {
if (this._templateParam(type)) continue
this._usedTypes.add(type)
this._addAffect(':' + type, name)
}
@ -307,6 +373,12 @@ export default class PocomathInstance {
}
}
/* returns a boolean indicating whether t denotes a template parameter.
* We will start this out very simply: the special string `T` is always
* a template parameter, and that's the only one
*/
_templateParam(t) { return t === theTemplateParam }
_addAffect(dependency, dependent) {
if (dependency in this._affects) {
this._affects[dependency].add(dependent)
@ -366,75 +438,177 @@ export default class PocomathInstance {
}
Object.defineProperty(this, name, {configurable: true, value: 'limbo'})
const tf_imps = {}
for (const [signature, {uses, does}] of usableEntries) {
if (uses.length === 0) {
tf_imps[signature] = does()
} else {
const refs = {}
let full_self_referential = false
let part_self_references = []
for (const dep of uses) {
const [func, needsig] = dep.split(/[()]/)
if (func === 'self') {
if (needsig) {
if (full_self_referential) {
throw new SyntaxError(
'typed-function does not support mixed full and '
+ 'partial self-reference')
}
if (subsetOfKeys(typesOfSignature(needsig), this.Types)) {
part_self_references.push(needsig)
}
} else {
if (part_self_references.length) {
throw new SyntaxError(
'typed-function does not support mixed full and '
+ 'partial self-reference')
}
full_self_referential = true
}
} else {
if (this[func] === 'limbo') {
/* We are in the midst of bundling func, so have to use
* an indirect reference to func. And given that, there's
* really no helpful way to extract a specific signature
*/
const self = this
refs[dep] = function () { // is this the most efficient?
return self[func].apply(this, arguments)
}
} else {
// can bundle up func, and grab its signature if need be
let destination = this[func]
if (needsig) {
destination = this._typed.find(destination, needsig)
}
refs[dep] = destination
}
}
}
if (full_self_referential) {
tf_imps[signature] = this._typed.referToSelf(self => {
refs.self = self
return does(refs)
})
} else if (part_self_references.length) {
tf_imps[signature] = this._typed.referTo(
...part_self_references, (...impls) => {
for (let i = 0; i < part_self_references.length; ++i) {
refs[`self(${part_self_references[i]})`] = impls[i]
}
return does(refs)
}
)
} else {
tf_imps[signature] = does(refs)
for (const [rawSignature, behavior] of usableEntries) {
/* Check if it's an ordinary non-template signature */
let explicit = true
for (const type of typesOfSignature(rawSignature)) {
if (this._templateParam(type)) { // template types need better check
explicit = false
break
}
}
if (explicit) {
this._addTFimplementation(tf_imps, rawSignature, behavior)
continue
}
/* It's a template, have to instantiate */
/* First, add the known instantiations, gathering all types needed */
if (!('instantiations' in behavior)) {
behavior.instantiations = new Set()
}
let instantiationSet = new Set()
let trimSignature = rawSignature
if (rawSignature.charAt(0) === '!') {
trimSignature = trimSignature.slice(1)
instantiationSet = this._usedTypes
} else {
for (const instType of behavior.instantiations) {
instantiationSet.add(instType)
for (const other of this._priorTypes[instType]) {
instantiationSet.add(other)
}
}
}
for (const instType of instantiationSet) {
if (this.Types[instType] === anySpec) continue
const signature =
substituteInSig(trimSignature, theTemplateParam, instType)
/* Don't override an explicit implementation: */
if (signature in imps) continue
const uses = new Set()
for (const dep of behavior.uses) {
if (this._templateParam(dep)) continue
uses.add(substituteInSig(dep, theTemplateParam, instType))
}
const patch = (refs) => {
const innerRefs = {}
for (const dep of behavior.uses) {
if (this._templateParam(dep)) {
innerRefs[dep] = instType
} else {
const outerName = substituteInSig(
dep, theTemplateParam, instType)
innerRefs[dep] = refs[outerName]
}
}
const original = behavior.does(innerRefs)
return behavior.does(innerRefs)
}
this._addTFimplementation(tf_imps, signature, {uses, does: patch})
}
/* Now add the catchall signature */
const signature = substituteInSig(
trimSignature, theTemplateParam, 'any')
/* The catchall signature has to detect the actual type of the call
* and add the new instantiations
*/
const argTypes = trimSignature.split(',')
let exemplar = -1
for (let i = 0; i < argTypes.length; ++i) {
const argType = argTypes[i].trim()
if (argType === theTemplateParam) {
exemplar = i
break
}
}
if (exemplar < 0) {
throw new SyntaxError(
`Cannot find template parameter in ${rawSignature}`)
}
const self = this
const patch = (refs) => (...args) => {
const example = args[exemplar]
const instantiateFor = self.typeOf(example)
refs[theTemplateParam] = instantiateFor
behavior.instantiations.add(instantiateFor)
self._invalidate(name)
// And for now, we have to rely on the "any" implementation. Hope
// it matches the instantiated one!
return behavior.does(refs)(...args)
}
this._addTFimplementation(
tf_imps, signature, {uses: behavior.uses, does: patch})
}
const tf = this._typed(name, tf_imps)
Object.defineProperty(this, name, {configurable: true, value: tf})
return tf
}
/* Adapts Pocomath-style behavior specification (uses, does) for signature
* to typed-function implementations and inserts the result into plain object
* imps
*/
_addTFimplementation(imps, signature, behavior) {
const {uses, does} = behavior
if (uses.length === 0) {
imps[signature] = does()
return
}
const refs = {}
let full_self_referential = false
let part_self_references = []
for (const dep of uses) {
let [func, needsig] = dep.split(/[()]/)
const needTypes = needsig ? typesOfSignature(needsig) : new Set()
/* For now, punt on template parameters */
if (needTypes.has(theTemplateParam)) needsig = ''
if (func === 'self') {
if (needsig) {
if (full_self_referential) {
throw new SyntaxError(
'typed-function does not support mixed full and '
+ 'partial self-reference')
}
if (subsetOfKeys(typesOfSignature(needsig), this.Types)) {
part_self_references.push(needsig)
}
} else {
if (part_self_references.length) {
throw new SyntaxError(
'typed-function does not support mixed full and '
+ 'partial self-reference')
}
full_self_referential = true
}
} else {
if (this[func] === 'limbo') {
/* We are in the midst of bundling func, so have to use
* an indirect reference to func. And given that, there's
* really no helpful way to extract a specific signature
*/
const self = this
refs[dep] = function () { // is this the most efficient?
return self[func].apply(this, arguments)
}
} else {
// can bundle up func, and grab its signature if need be
let destination = this[func]
if (needsig) {
destination = this._typed.find(destination, needsig)
}
refs[dep] = destination
}
}
}
if (full_self_referential) {
imps[signature] = this._typed.referToSelf(self => {
refs.self = self
return does(refs)
})
return
}
if (part_self_references.length) {
imps[signature] = this._typed.referTo(
...part_self_references, (...impls) => {
for (let i = 0; i < part_self_references.length; ++i) {
refs[`self(${part_self_references[i]})`] = impls[i]
}
return does(refs)
}
)
return
}
imps[signature] = does(refs)
}
}

View File

@ -1 +1,2 @@
export * from './arithmetic.mjs'
export * from './relational.mjs'

View File

@ -1,8 +1,11 @@
import {reducingOperation} from './reducingOperation.mjs'
export * from './Types/generic.mjs'
export const add = reducingOperation
export {lcm} from './lcm.mjs'
export {mod} from './mod.mjs'
export {multiply} from './multiply.mjs'
export const multiply = reducingOperation
export {divide} from './divide.mjs'
export {sign} from './sign.mjs'
export {sqrt} from './sqrt.mjs'

View File

@ -1,4 +1,7 @@
export const divide = {
'any,any': ({multiply, invert}) => (x, y) => multiply(x, invert(y))
'T,T': ({
'multiply(T,T)': multT,
'invert(T)': invT
}) => (x, y) => multT(x, invT(y))
}

View File

@ -1,3 +1,7 @@
/* Note we do not use a template here so that we can explicitly control
* which types this is instantiated for, namely the "integer" types, and
* not simply allow Pocomath to generate instances for any type it encounters.
*/
/* Returns a object that defines the gcd for the given type */
export default function(type) {
const producer = refs => {

View File

@ -1,6 +1,7 @@
export const lcm = {
'any,any': ({
multiply,
quotient,
gcd}) => (a,b) => multiply(quotient(a, gcd(a,b)), b)
'T,T': ({
'multiply(T,T)': multT,
'quotient(T,T)': quotT,
'gcd(T,T)': gcdT
}) => (a,b) => multT(quotT(a, gcdT(a,b)), b)
}

View File

@ -1,6 +1,7 @@
export const mod = {
'any,any': ({
subtract,
multiply,
quotient}) => (a,m) => subtract(a, multiply(m, quotient(a,m)))
'T,T': ({
'subtract(T,T)': subT,
'multiply(T,T)': multT,
'quotient(T,T)': quotT
}) => (a,m) => subT(a, multT(m, quotT(a,m)))
}

View File

@ -3,6 +3,7 @@ export * from './Types/generic.mjs'
export const multiply = {
'undefined': () => u => u,
'undefined,...any': () => (u, rest) => u,
any: () => x => x,
'any,undefined': () => (x, u) => u,
'any,any,...any': ({self}) => (a,b,rest) => {
const later = [b, ...rest]

View File

@ -0,0 +1,12 @@
export * from './Types/generic.mjs'
export const reducingOperation = {
'undefined': () => u => u,
'undefined,...any': () => (u, rest) => u,
'any,undefined': () => (x, u) => u,
any: () => x => x,
'any,any,...any': ({
self
}) => (a,b,rest) => [b, ...rest].reduce((x,y) => self(x,y), a)
}

View File

@ -0,0 +1,54 @@
export const compare = {
'undefined,undefined': () => () => 0
}
export const isZero = {
'undefined': () => u => u === 0
}
export const equal = {
'!T,T': ({
'compare(T,T)': cmp,
'isZero(T)': isZ
}) => (x,y) => isZ(cmp(x,y))
}
export const unequal = {
'T,T': ({'equal(T.T)': eq}) => (x,y) => !(eq(x,y))
}
export const larger = {
'T,T': ({
'compare(T,T)': cmp,
'one(T)' : uno
}) => (x,y) => cmp(x,y) === uno(y)
}
export const largerEq = {
'T,T': ({
'compare(T,T)': cmp,
'one(T)' : uno,
'isZero(T)' : isZ
}) => (x,y) => {
const c = cmp(x,y)
return isZ(c) || c === uno(y)
}
}
export const smaller = {
'T,T': ({
'compare(T,T)': cmp,
'one(T)' : uno,
'isZero(T)' : isZ
}) => (x,y) => {
const c = cmp(x,y)
return !isZ(c) && c !== uno(y)
}
}
export const smallerEq = {
'T,T': ({
'compare(T,T)': cmp,
'one(T)' : uno
}) => (x,y) => cmp(x,y) !== uno(y)
}

View File

@ -1,6 +1,3 @@
export const sign = {
any: ({negate, divide, abs}) => x => {
if (x === negate(x)) return x // zero
return divide(x, abs(x))
}
T: ({'compare(T,T)': cmp, 'zero(T)': Z}) => x => cmp(x, Z(x))
}

View File

@ -1,3 +1,3 @@
export const square = {
any: ({multiply}) => x => multiply(x,x)
T: ({'multiply(T,T)': multT}) => x => multT(x,x)
}

View File

@ -1,3 +1,3 @@
export const subtract = {
'any,any': ({add, negate}) => (x,y) => add(x, negate(y))
'T,T': ({'add(T,T)': addT, 'negate(T)': negT}) => (x,y) => addT(x, negT(y))
}

View File

@ -1,5 +1,3 @@
export * from './Types/number.mjs'
export const add = {
'...number': () => addends => addends.reduce((x,y) => x+y, 0),
}
export const add = {'number,number': () => (m,n) => m+n}

View File

@ -1,6 +1,6 @@
import PocomathInstance from '../core/PocomathInstance.mjs'
import * as numbers from './native.mjs'
import * as generic from '../generic/arithmetic.mjs'
import * as generic from '../generic/all.mjs'
export default PocomathInstance.merge('number', numbers, generic)

52
src/number/compare.mjs Normal file
View File

@ -0,0 +1,52 @@
/* Lifted from mathjs/src/utils/number.js */
/**
* Minimum number added to one that makes the result different than one
*/
export const DBL_EPSILON = Number.EPSILON || 2.2204460492503130808472633361816E-16
/**
* Compares two floating point numbers.
* @param {number} x First value to compare
* @param {number} y Second value to compare
* @param {number} [epsilon] The maximum relative difference between x and y
* If epsilon is undefined or null, the function will
* test whether x and y are exactly equal.
* @return {boolean} whether the two numbers are nearly equal
*/
function nearlyEqual (x, y, epsilon) {
// if epsilon is null or undefined, test whether x and y are exactly equal
if (epsilon === null || epsilon === undefined) {
return x === y
}
if (x === y) {
return true
}
// NaN
if (isNaN(x) || isNaN(y)) {
return false
}
// at this point x and y should be finite
if (isFinite(x) && isFinite(y)) {
// check numbers are very close, needed when comparing numbers near zero
const diff = Math.abs(x - y)
if (diff < DBL_EPSILON) {
return true
} else {
// use relative error
return diff <= Math.max(Math.abs(x), Math.abs(y)) * epsilon
}
}
// Infinite and Number or negative Infinite and positive Infinite cases
return false
}
/* End of copied section */
export const compare = {
'number,number': ({
config
}) => (x,y) => nearlyEqual(x, y, config.epsilon) ? 0 : (x > y ? 1 : -1)
}

View File

@ -4,6 +4,7 @@ export * from './Types/number.mjs'
export {abs} from './abs.mjs'
export {add} from './add.mjs'
export {compare} from './compare.mjs'
export const gcd = gcdType('NumInt')
export {invert} from './invert.mjs'
export {isZero} from './isZero.mjs'

View File

@ -1,4 +1,5 @@
import assert from 'assert'
import PocomathInstance from '../src/core/PocomathInstance.mjs'
import math from '../src/pocomath.mjs'
describe('The default full pocomath instance "math"', () => {
@ -10,14 +11,25 @@ describe('The default full pocomath instance "math"', () => {
assert.strictEqual(undef.length, 0)
})
it('has a built-in typeOf operator', () => {
assert.strictEqual(math.typeOf(42), 'NumInt')
assert.strictEqual(math.typeOf(-1.5), 'number')
assert.strictEqual(math.typeOf(-42n), 'bigint')
assert.strictEqual(math.typeOf(undefined), 'undefined')
assert.strictEqual(math.typeOf({re: 15n, im: -2n}), 'GaussianInteger')
assert.strictEqual(math.typeOf({re: 6.28, im: 2.72}), 'Complex')
})
it('can subtract numbers', () => {
assert.strictEqual(math.subtract(12, 5), 7)
//assert.strictEqual(math.subtract(3n, 1.5), 1.5)
})
it('can add numbers', () => {
assert.strictEqual(math.add(3, 4), 7)
assert.strictEqual(math.add(1.5, 2.5, 3.5), 7.5)
assert.strictEqual(math.add(Infinity), Infinity)
assert.throws(() => math.add(3n, -1.5), TypeError)
})
it('can negate numbers', () => {
@ -26,16 +38,17 @@ describe('The default full pocomath instance "math"', () => {
})
it('can be extended', () => {
math.installType('stringK', {
const stretch = PocomathInstance.merge(math) // clone to not pollute math
stretch.installType('stringK', {
test: s => typeof s === 'string' && s.charAt(0) === 'K',
before: ['string']
})
math.install({
stretch.install({
add: {
'...stringK': () => addends => addends.reduce((x,y) => x+y, '')
},
})
assert.strictEqual(math.add('Kilroy','K is here'), 'KilroyK is here')
assert.strictEqual(stretch.add('Kilroy','K is here'), 'KilroyK is here')
})
it('handles complex numbers', () => {
@ -46,10 +59,10 @@ describe('The default full pocomath instance "math"', () => {
assert.deepStrictEqual(
math.subtract(math.complex(1,1), math.complex(2,-2)),
math.complex(-1,3))
assert.strictEqual(math.negate(math.complex(3, 8)).im, -8)
assert.deepStrictEqual(
math.subtract(16, math.add(3, math.complex(0,4), 2)),
math.complex(11, -4))
assert.strictEqual(math.negate(math.complex(3, 8)).im, -8)
})
it('handles bigints', () => {

View File

@ -38,6 +38,13 @@ describe('complex', () => {
math.complex(ms.negate(ms.sqrt(0.5)), ms.sqrt(0.5)))
})
it('checks for equality', () => {
assert.ok(math.equal(math.complex(3,0), 3))
assert.ok(math.equal(math.complex(3,2), math.complex(3, 2)))
assert.ok(!(math.equal(math.complex(45n, 3n), math.complex(45n, -3n))))
assert.ok(!(math.equal(math.complex(45n, 3n), 45n)))
})
it('computes gcd', () => {
assert.deepStrictEqual(
math.gcd(math.complex(53n, 56n), math.complex(47n, -13n)),

View File

@ -3,6 +3,7 @@ import math from '../src/pocomath.mjs'
import PocomathInstance from '../src/core/PocomathInstance.mjs'
import * as numbers from '../src/number/all.mjs'
import * as numberAdd from '../src/number/add.mjs'
import {add as genericAdd} from '../src/generic/arithmetic.mjs'
import * as complex from '../src/complex/all.mjs'
import * as complexAdd from '../src/complex/add.mjs'
import * as complexNegate from '../src/complex/negate.mjs'
@ -66,6 +67,7 @@ describe('A custom instance', () => {
const cherry = new PocomathInstance('cherry')
cherry.install(numberAdd)
await extendToComplex(cherry)
cherry.install({add: genericAdd})
/* Now we have an instance that supports addition for number and complex
and little else:
*/

View File

@ -30,4 +30,11 @@ describe('number', () => {
it('computes gcd', () => {
assert.strictEqual(math.gcd(15, 35), 5)
})
it('compares numbers', () => {
assert.ok(math.smaller(12,13.5))
assert.ok(math.equal(Infinity, Infinity))
assert.ok(math.largerEq(12.5, math.divide(25,2)))
})
})