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