diff --git a/src/Bounded.ts b/src/Bounded.ts index 7eb133c5c..fced2e951 100644 --- a/src/Bounded.ts +++ b/src/Bounded.ts @@ -7,7 +7,10 @@ * * @since 2.0.0 */ -import { Ord, ordNumber } from './Ord' +import * as Ord from './Ord' +import { Option, fromPredicate } from './Option' +import * as n from './number' +import { pipe } from './function' // ------------------------------------------------------------------------------------- // model @@ -17,11 +20,130 @@ import { Ord, ordNumber } from './Ord' * @category type classes * @since 2.0.0 */ -export interface Bounded extends Ord { +export interface Bounded extends Ord.Ord { readonly top: A readonly bottom: A } +// ------------------------------------------------------------------------------------- +// deconstructors +// ------------------------------------------------------------------------------------- + +/** + * @category deconstructors + * @since 2.12.0 + */ +export const top = (B: Bounded) => B.top + +/** + * @category deconstructors + * @since 2.12.0 + */ +export const bottom = (B: Bounded) => B.bottom + +/** + * Returns the tuple [bottom, top]. + * + * @category deconstructors + * @since 2.12.0 + */ +export const toTuple = (B: Bounded): [T, T] => [B.bottom, B.top] + +// ------------------------------------------------------------------------------------- +// guards +// ------------------------------------------------------------------------------------- + +/** + * Test that top >= bottom + * + * @category guards + * @since 2.12.0 + */ +export const isValid = (B: Bounded) => Ord.leq(B)(B.bottom, B.top) + +// ------------------------------------------------------------------------------------- +// constructors +// ------------------------------------------------------------------------------------- + +/** + * Returns an instance of Bounded from a range of values. + * Returns none if bottom > top and some if top >= bottom. + * + * @category constructors + * @since 2.12.0 + */ +export const fromRange = + (O: Ord.Ord) => + (b: T) => + (t: T): Option> => + pipe({ ...O, top: t, bottom: b }, fromPredicate(isValid)) + +/** + * Creates an instance of Bounded from the tuple [bottom, top]. + * Returns none if fst > snd and some if snd >= fst. + * + * @category constructors + * @since 2.12.0 + */ +export const fromTuple = + (O: Ord.Ord) => + ([b, t]: [T, T]) => + fromRange(O)(b)(t) + +/** + * Returns a valid instance of Bounded given two values where top is the greater of + * the two values and bottom is set to the smaller of the values. + * + * @category constructors + * @since 2.12.0 + */ +export const coerceBound = + (O: Ord.Ord) => + (b: T) => + (t: T): Bounded => + Ord.leq(O)(b, t) ? { ...O, bottom: b, top: t } : { ...O, bottom: t, top: b } + +// ------------------------------------------------------------------------------------- +// utils +// ------------------------------------------------------------------------------------- + +/** + * Clamp a value between bottom and top values. + * + * @category utils + * @since 2.12.0 + */ +export const clamp = (B: Bounded) => Ord.clamp(B)(B.bottom, B.top) + +/** + * Tests whether a value lies between the top and bottom values of bound. + * + * @category utils + * @since 2.12.0 + */ +export const isWithin = (B: Bounded) => Ord.between(B)(B.bottom, B.top) + +/** + * Reverses the Ord of a bound and swaps top and bottom values. + * + * @category utils + * @since 2.12.0 + */ +export const reverse = (B: Bounded): Bounded => ({ + ...Ord.reverse(B), + top: B.bottom, + bottom: B.top +}) + +/** + * Tests whether the bounded range only contains a single value. + * I.e. if top == bottom under the bounds instance of equality. + * + * @category utils + * @since 2.12.0 + */ +export const isSingular = (B: Bounded) => B.equals(B.bottom, B.top) + // ------------------------------------------------------------------------------------- // deprecated // ------------------------------------------------------------------------------------- @@ -36,8 +158,8 @@ export interface Bounded extends Ord { * @deprecated */ export const boundedNumber: Bounded = { - equals: ordNumber.equals, - compare: ordNumber.compare, + equals: n.Ord.equals, + compare: n.Ord.compare, top: Infinity, bottom: -Infinity } diff --git a/test/Bounded.ts b/test/Bounded.ts new file mode 100644 index 000000000..936cae331 --- /dev/null +++ b/test/Bounded.ts @@ -0,0 +1,144 @@ +import { + reverse, + isSingular, + coerceBound, + fromRange, + fromTuple, + clamp, + top, + bottom, + isWithin, + toTuple, + isValid +} from '../src/Bounded' +import { BooleanAlgebra as b } from '../src/boolean' +import * as U from './util' +import * as n from '../src/number' +import { pipe } from '../src/function' +import fc from 'fast-check' +import * as Eq from '../src/Eq' +import * as O from 'fp-ts/Option' + +describe('Bounded', () => { + it('top', () => { + fc.assert( + fc.property(fc.integer(), fc.integer(), (b, t) => + pipe({ ...n.Ord, bottom: b, top: t }, top, (val) => n.Eq.equals(t, val)) + ) + ) + }) + + it('bottom', () => { + fc.assert( + fc.property(fc.integer(), fc.integer(), (b, t) => + pipe({ ...n.Ord, bottom: b, top: t }, bottom, (val) => n.Eq.equals(b, val)) + ) + ) + }) + + it('isValid', () => { + fc.assert( + fc.property(fc.integer(), fc.integer(), (bottom, top) => + b.implies(bottom <= top, isValid({ ...n.Ord, bottom, top })) + ) + ) + }) + + it('isSingular', () => { + fc.assert( + fc.property(fc.integer(), fc.integer(), (bottom, top) => + b.implies( + bottom === top, + isSingular({ ...n.Ord, bottom, top }) && b.implies(bottom !== top, !isSingular({ ...n.Ord, bottom, top })) + ) + ) + ) + }) + + it('reverse', () => { + fc.assert( + fc.property(fc.integer(), fc.integer(), (x, y) => { + const bound = { ...n.Ord, bottom: x, top: y } + const reverseBound = reverse(bound) + + return ( + top(bound) === bottom(reverseBound) && + bottom(bound) === top(reverseBound) && + bound.compare(x, y) === reverseBound.compare(y, x) && + b.implies(isValid(bound), isValid(reverseBound)) + ) + }) + ) + }) + + it('coerceBound', () => { + fc.assert( + fc.property(fc.integer(), fc.integer(), (x, y) => { + const bound = coerceBound(n.Ord)(x)(y) + + return ( + isValid(bound) && + b.implies(x <= y, top(bound) === y && bottom(bound) === x) && + b.implies(y <= x, top(bound) === x && bottom(bound) === y) + ) + }) + ) + }) + + it('toTuple', () => { + fc.assert( + fc.property(fc.integer(), (bottom) => { + const top = bottom + 100 + + return pipe({ ...n.Ord, bottom, top }, toTuple, (val) => Eq.tuple(n.Eq, n.Eq).equals([bottom, top], val)) + }) + ) + }) + + it('isWithin', () => { + const inRange = isWithin({ ...n.Ord, bottom: 0, top: 10 }) + + U.deepStrictEqual(inRange(2), true) + U.deepStrictEqual(inRange(10), true) + U.deepStrictEqual(inRange(0), true) + U.deepStrictEqual(inRange(20), false) + U.deepStrictEqual(inRange(-10), false) + }) + + it('fromRange', () => { + const numRange = fromRange(n.Ord) + + U.deepStrictEqual(numRange(0)(10), O.some({ ...n.Ord, bottom: 0, top: 10 })) + U.deepStrictEqual(numRange(0)(0), O.some({ ...n.Ord, bottom: 0, top: 0 })) + U.deepStrictEqual(numRange(-1)(0), O.some({ ...n.Ord, bottom: -1, top: 0 })) + U.deepStrictEqual(numRange(-1)(-2), O.none) + U.deepStrictEqual(numRange(1)(0), O.none) + }) + + it('fromTuple', () => { + const numRange = fromTuple(n.Ord) + + U.deepStrictEqual(numRange([0, 10]), O.some({ ...n.Ord, bottom: 0, top: 10 })) + U.deepStrictEqual(numRange([0, 0]), O.some({ ...n.Ord, bottom: 0, top: 0 })) + U.deepStrictEqual(numRange([-1, 0]), O.some({ ...n.Ord, bottom: -1, top: 0 })) + U.deepStrictEqual(numRange([-1, -2]), O.none) + U.deepStrictEqual(numRange([1, 0]), O.none) + }) + + it('clamp', () => { + fc.assert( + fc.property(fc.integer(), fc.integer(), (bottom, val) => { + const top = bottom + 100 + const bound = { ...n.Ord, bottom, top } + + const clampedValue = clamp(bound)(val) + + return ( + b.implies(isWithin(bound)(val), clampedValue === val) && + b.implies(val < bottom, clampedValue === bottom) && + b.implies(val > top, clampedValue === top) + ) + }) + ) + }) +})