feat: config and approximate equality (#19)
All checks were successful
/ test (push) Successful in 17s

Establishes a global config object for a TypeDispatcher instance, so far
  with just properties representing comparison tolerances. Begins a
  "relational" group of functions with basic approximate equality, and
  an initial primitive ordering comparison. Ensures that methods that
  depend on properties of `config` will be properly updated when those
  properties change.

Reviewed-on: #19
Co-authored-by: Glen Whitney <glen@studioinfinity.org>
Co-committed-by: Glen Whitney <glen@studioinfinity.org>
This commit is contained in:
Glen Whitney 2025-04-16 04:23:48 +00:00 committed by Glen Whitney
parent 27fa4b0193
commit 70ce01d12b
27 changed files with 788 additions and 218 deletions

View file

@ -4,4 +4,7 @@ import {BooleanT} from '#boolean/BooleanT.js'
export const NumberT = new Type(n => typeof n === 'number', {
from: onType(BooleanT, math => math.number.resolve([BooleanT])),
one: 1,
zero: 0,
nan: NaN
})

View file

@ -0,0 +1,33 @@
import assert from 'assert'
import math from '#nanomath'
const {exceeds, indistinguishable} = math
describe('number relational functions', () => {
it('orders numbers correctly', () => {
assert(exceeds(7, 2.2))
assert(exceeds(0, -1.1))
assert(exceeds(1e-35, 0))
assert(exceeds(Infinity, 1e99))
assert(exceeds(-1e101, -Infinity))
assert(!exceeds(NaN, 0))
assert(!exceeds(0, NaN))
assert(!exceeds(NaN, NaN))
assert(!exceeds(2, 2))
})
it('checks for exact equality', () => {
assert(indistinguishable(0, 0))
assert(indistinguishable(0, -0))
assert(!indistinguishable(0, 1e-35))
assert(!indistinguishable(NaN, NaN))
assert(indistinguishable(Infinity, Infinity))
})
it('checks for approximate equality', () => {
const rel = 1e-12
const abs = 1e-15
assert(indistinguishable(1, 1 + 0.9e-12, rel, abs))
assert(indistinguishable(0, 1e-16, rel, abs))
assert(!indistinguishable(1, 1 + 1.1e-12, rel, abs))
assert(!indistinguishable(0, 1e-14, rel, abs))
})
})

View file

@ -1,4 +1,5 @@
export * as typeDefinition from './NumberT.js'
export * as arithmetic from './arithmetic.js'
export * as relational from './relational.js'
export * as type from './type.js'
export * as utils from './utils.js'

View file

@ -6,4 +6,11 @@ import {Returns} from '#core/Type.js'
export const plain = f => onType(
Array(f.length).fill(NumberT), Returns(NumberT, f))
export const boolnum = Returns(NumberT, p => p ? 1 : 0)
// Takes a behavior returning boolean, and returns a factory
// that returns that behavior if the boolean type is present,
// and otherwise wraps the behavior to return 1 or 0.
export const boolnum = behavior => math => {
const {BooleanT} = math.types
if (BooleanT) return Returns(BooleanT, behavior)
return Returns(NumberT, (...args) => behavior(...args) ? 1 : 0)
}

37
src/number/relational.js Normal file
View file

@ -0,0 +1,37 @@
import {onType} from '#core/helpers.js'
import {Returns} from '#core/Type.js'
import {Optional} from '#core/TypePatterns.js'
import {boolnum} from './helpers.js'
import {NumberT} from './NumberT.js'
// In nanomath, we take the point of view that two comparators are primitive:
// indistinguishable(a, b, relTol, absTol), and exceeds(a, b). All others
// are defined generically in terms of these. They typically return BooleanT,
// but in a numbers-only bundle, they return 1 or 0.
// Notice a feature of TypedDispatcher: if you specify one tolerance, you must
// specify both.
export const indistinguishable = onType(
[NumberT, NumberT, Optional([NumberT, NumberT])],
boolnum((a, b, [tolerances = [0, 0]]) => {
const [relTol, absTol] = tolerances
if (relTol < 0 || absTol < 0) {
throw new RangeError(
`Tolerances (relative: ${relTol}, absolute: ${absTol}) `
+ 'must be nonnegative')
}
if (isNaN(a) || isNaN(b)) return false
if (a === b) return true
if (!isFinite(a) || !isFinite(b)) return false
// |a-b| <= absTol or |a-b| <= relTol*max(|a|, |b|)
const diff = Math.abs(a-b)
if (diff <= absTol) return true
const magnitude = Math.max(Math.abs(a), Math.abs(b))
return diff <= relTol * magnitude
})
)
// Returns truthy if a (interpreted as completely precise) represents a
// greater value than b (interpreted as completely precise). Note that even if
// so, a and b might be indistinguishable() to some tolerances.
export const exceeds = onType([NumberT, NumberT], boolnum((a, b) => a > b))

View file

@ -1,4 +1,4 @@
import {plain, boolnum} from './helpers.js'
import {plain} from './helpers.js'
import {BooleanT} from '#boolean/BooleanT.js'
import {Returns} from '#core/Type.js'
import {NumberT} from '#number/NumberT.js'
@ -7,6 +7,7 @@ const num = f => Returns(NumberT, f)
export const number = plain(a => a)
number.also(
BooleanT, boolnum,
// conversions from Boolean should be consistent with one and zero:
BooleanT, num(p => p ? NumberT.one : NumberT.zero),
[], num(() => 0)
)

View file

@ -5,8 +5,4 @@ import {Returns} from '#core/Type.js'
import {onType} from '#core/helpers.js'
export const clone = plain(a => a)
export const isnan = onType(NumberT, math => {
const {BooleanT} = math.types
if (BooleanT) return Returns(BooleanT, a => isNaN(a))
return Returns(NumberT, a => boolnum(isNaN(a)))
})
export const isnan = onType(NumberT, boolnum(isNaN))