Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Add Comparison Operators and NullishMath.average #2

Merged
merged 1 commit into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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