Skip to content

Commit

Permalink
feature: Add Comparison Operators and NullishMath.average
Browse files Browse the repository at this point in the history
  • Loading branch information
FlorianWendelborn committed Jul 18, 2024
1 parent 137425c commit c6337a4
Show file tree
Hide file tree
Showing 3 changed files with 233 additions and 10 deletions.
34 changes: 34 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,30 @@ Returns a new instance of `NullishMath` with the sum of the current value and th

Returns a new instance of `NullishMath` with the sum of the current value and the given numbers.

#### `eq(toCompare: NullishMath | number | null | undefined): boolean`

Returns `true` if the result equals `toCompare`, treats null and undefined as equals.

#### `lt(toCompare: NullishMath | number | null | undefined): boolean | null`

Returns `true` if the result is strictly less than `toCompare`, returns `null` if either number is nullish.

#### `lte(toCompare: NullishMath | number | null | undefined): boolean | null`

Returns `true` if the result is less than or equal to `toCompare`, returns `null` if either number is nullish.

#### `gt(toCompare: NullishMath | number | null | undefined): boolean | null`

Returns `true` if the result is strictly greater than `toCompare`, returns `null` if either number is nullish.

#### `gte(toCompare: NullishMath | number | null | undefined): boolean | null`

Returns `true` if the result is greater than or equal to `toCompare`, returns `null` if either number is nullish.

#### `neq(toCompare: NullishMath | number | null | undefined): boolean`

Returns `true` if the result doesn’t equal `toCompare`, treats null and undefined as equals.

#### `subtract(number: NullishMath | number | null | undefined): NullishMath`

Returns a new instance of `NullishMath` with the difference of the current value and the given number.
Expand Down Expand Up @@ -83,6 +107,16 @@ Returns a new instance of `NullishMath` with the quotient of the current value a

Returns the final value of the `NullishMath` instance. If any of the values passed to the math operation methods are `null` or `undefined`, the final value will be `null`.

### `NullishMath.average(Array<NullishMath | number | null | undefined>, options?: { treatNullishAsZero?: boolean }): number | null`

Calculates the average of the provided numbers. By default, `null`s are excluded from the average. This can be changed by setting the `treatNullishAsZero` option. With this flag, nullish numbers get counted as a `0` and thus impact the average.

Returns `null` on division by zero unless `treatNullishAsZero` is set (in which case it returns `0`).

### `NullishMath.unwrap(NullishMath | number | null | undefined): number | null`

Converts the input to either `number | null`. General-purpose equivalent of `nm.end()`

## Development

`nullish-math` uses [`bun`](https://bun.sh)
Expand Down
108 changes: 106 additions & 2 deletions source/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,43 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, it } from 'bun:test'

import { nm } from '.'
import { nm, NullishMath } from '.'

describe('NullishMath static methods', () => {
it('correctly implements NullishMath.unwrap()', () => {
expect(NullishMath.unwrap(null)).toBe(null)
expect(NullishMath.unwrap(undefined)).toBe(null)
// @ts-expect-error not allowed to pass a value that’s always nullish
expect(NullishMath.unwrap(nm(null))).toBe(null)
// @ts-expect-error not allowed to pass a value that’s always nullish
expect(NullishMath.unwrap(nm(undefined))).toBe(null)
expect(NullishMath.unwrap(nm(42))).toBe(42)
expect(NullishMath.unwrap(42)).toBe(42)
})

it('correctly implements NullishMath.average() without options', () => {
expect(NullishMath.average([]).end()).toBe(null)
expect(NullishMath.average([null, undefined]).end()).toBe(null)
expect(NullishMath.average([null, null]).end()).toBe(null)
expect(NullishMath.average([undefined, 42]).end()).toBe(42)
expect(NullishMath.average([null, 42]).end()).toBe(42)
expect(NullishMath.average([undefined, 42, 1337]).end()).toBe(689.5)
expect(NullishMath.average([null, 42, 1337]).end()).toBe(689.5)
expect(NullishMath.average([42, 1337]).end()).toBe(689.5)
})

it('correctly implements NullishMath.average() with treatNullAsZero', () => {
const o = { treatNullishAsZero: true } as const

expect(NullishMath.average([], o).end()).toBe(0)
expect(NullishMath.average([null, undefined], o).end()).toBe(0)
expect(NullishMath.average([null, null], o).end()).toBe(0)
expect(NullishMath.average([undefined, 42], o).end()).toBe(21)
expect(NullishMath.average([null, 42], o).end()).toBe(21)
expect(NullishMath.average([undefined, 10, 20], o).end()).toBe(10)
expect(NullishMath.average([null, 10, 20], o).end()).toBe(10)
expect(NullishMath.average([42, 1337], o).end()).toBe(689.5)
})
})

describe('nm.add()', () => {
it('supports null #value', () => {
Expand Down Expand Up @@ -66,6 +103,73 @@ describe('nm.addMany()', () => {
})
})

describe('comparison operators', () => {
it('nm.eq()', () => {
// @ts-expect-error not allowed to pass a value that’s always nullish
expect(nm(null).eq(null)).toBe(true)
// @ts-expect-error not allowed to pass a value that’s always nullish
expect(nm(null).eq(undefined)).toBe(true)
// @ts-expect-error not allowed to pass a value that’s always nullish
expect(nm(undefined).eq(null)).toBe(true)

expect(nm(1).eq(0)).toBe(false)
expect(nm(1).eq(1)).toBe(true)
expect(nm(1).eq(2)).toBe(false)
})

it('nm.lt()', () => {
// @ts-expect-error not allowed to pass a value that’s always nullish
expect(nm(null).lt(null)).toBe(null)
// @ts-expect-error not allowed to pass a value that’s always nullish
expect(nm(null).lt(undefined)).toBe(null)
// @ts-expect-error not allowed to pass a value that’s always nullish
expect(nm(undefined).lt(null)).toBe(null)

expect(nm(1).lt(0)).toBe(false)
expect(nm(1).lt(1)).toBe(false)
expect(nm(1).lt(2)).toBe(true)
})

it('nm.lte()', () => {
// @ts-expect-error not allowed to pass a value that’s always nullish
expect(nm(null).lte(null)).toBe(null)
// @ts-expect-error not allowed to pass a value that’s always nullish
expect(nm(null).lte(undefined)).toBe(null)
// @ts-expect-error not allowed to pass a value that’s always nullish
expect(nm(undefined).lte(null)).toBe(null)

expect(nm(1).lte(0)).toBe(false)
expect(nm(1).lte(1)).toBe(true)
expect(nm(1).lte(2)).toBe(true)
})

it('nm.gt()', () => {
// @ts-expect-error not allowed to pass a value that’s always nullish
expect(nm(null).gt(null)).toBe(null)
// @ts-expect-error not allowed to pass a value that’s always nullish
expect(nm(null).gt(undefined)).toBe(null)
// @ts-expect-error not allowed to pass a value that’s always nullish
expect(nm(undefined).gt(null)).toBe(null)

expect(nm(1).gt(0)).toBe(true)
expect(nm(1).gt(1)).toBe(false)
expect(nm(1).gt(2)).toBe(false)
})

it('nm.gte()', () => {
// @ts-expect-error not allowed to pass a value that’s always nullish
expect(nm(null).gte(null)).toBe(null)
// @ts-expect-error not allowed to pass a value that’s always nullish
expect(nm(null).gte(undefined)).toBe(null)
// @ts-expect-error not allowed to pass a value that’s always nullish
expect(nm(undefined).gte(null)).toBe(null)

expect(nm(1).gte(0)).toBe(true)
expect(nm(1).gte(1)).toBe(true)
expect(nm(1).gte(2)).toBe(false)
})
})

describe('nm.subtract()', () => {
it('supports null #value', () => {
// @ts-expect-error not allowed to pass a value that’s always nullish
Expand Down
101 changes: 93 additions & 8 deletions source/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@ export type NullishNumber =
type NotOnlyNullish<T extends NullishNumber> = [T] extends [null]
? 'Number cannot always be null'
: [T] extends [undefined]
? 'Number cannot always be undefined'
: [T] extends [null | undefined]
? 'Number cannot always be nullish'
: T
? 'Number cannot always be undefined'
: [T] extends [null | undefined]
? 'Number cannot always be nullish'
: T

type NotOnlyNullishArray<T extends NullishNumber[]> = [T] extends [null[]]
? 'Number cannot always be null'[]
: [T] extends [undefined[]]
? 'Number cannot always be undefined'[]
: [T] extends [Array<null | undefined>]
? 'Number cannot always be nullish'[]
: T
? 'Number cannot always be undefined'[]
: [T] extends [Array<null | undefined>]
? 'Number cannot always be nullish'[]
: T

export class NullishMath<T extends NullishNumber> {
readonly #value: number | null
Expand All @@ -27,6 +27,35 @@ export class NullishMath<T extends NullishNumber> {
this.#value = NullishMath.unwrap(value as NullishNumber)
}

static average = (
numbers: NullishNumber[],
options: {
treatNullishAsZero?: boolean
} = {
treatNullishAsZero: false,
},
): NullishMath<NullishNumber> => {
let countValid = 0
let sumValid = 0

for (const rawNumber of numbers) {
const number = NullishMath.unwrap(rawNumber)

if (number === null) {
if (options.treatNullishAsZero) countValid += 1
continue
}

countValid += 1
sumValid += number
}

// division by zero
if (countValid === 0) return nm(options.treatNullishAsZero ? 0 : null)

return nm(sumValid).divide(countValid)
}

static unwrap(value: NullishNumber) {
if (value === null) return null
if (value === undefined) return null
Expand Down Expand Up @@ -61,6 +90,62 @@ export class NullishMath<T extends NullishNumber> {
return new NullishMath(result)
}

/**
* Returns true if the two numbers are equal, including the case where both are null.
*/
eq(toCompare: NullishNumber): boolean {
const n = NullishMath.unwrap(toCompare)
return this.#value === n
}

/**
* Returns true if toCompare is strictly greater than the current number. Returns null if either number is null
*/
gt(toCompare: NullishNumber): boolean | null {
const n = NullishMath.unwrap(toCompare)
if (n === null) return null
if (this.#value === null) return null
return this.#value > n
}

/**
* Returns true if toCompare is greater than or equal to the current number. Returns null if either number is null
*/
gte(toCompare: NullishNumber): boolean | null {
const n = NullishMath.unwrap(toCompare)
if (n === null) return null
if (this.#value === null) return null
return this.#value >= n
}

/**
* Returns true if toCompare is strictly less than the current number. Returns null if either number is null
*/
lt(toCompare: NullishNumber): boolean | null {
const n = NullishMath.unwrap(toCompare)
if (n === null) return null
if (this.#value === null) return null
return this.#value < n
}

/**
* Returns true if toCompare is less than or equal to the current number. Returns null if either number is null
*/
lte(toCompare: NullishNumber): boolean | null {
const n = NullishMath.unwrap(toCompare)
if (n === null) return null
if (this.#value === null) return null
return this.#value <= n
}

/**
* Returns true if the two numbers are not equal, also returns false when both numbers are null
*/
neq(toCompare: NullishNumber): boolean {
const n = NullishMath.unwrap(toCompare)
return this.#value !== n
}

subtract<T extends NullishNumber>(
number: NotOnlyNullish<T>,
): NullishMath<NullishNumber> {
Expand Down

0 comments on commit c6337a4

Please sign in to comment.