Skip to content

Commit

Permalink
feat(utils): equal
Browse files Browse the repository at this point in the history
Signed-off-by: Lexus Drumgold <[email protected]>
  • Loading branch information
unicornware committed May 18, 2023
1 parent d697c35 commit e0b3571
Show file tree
Hide file tree
Showing 28 changed files with 156 additions and 40 deletions.
1 change: 1 addition & 0 deletions .dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ cefc
codecov
commitlintrc
dedupe
dequal
dessant
dohm
fbca
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@
"typecheck": "vitest typecheck --run",
"typecheck:watch": "vitest typecheck"
},
"dependencies": {
"dequal": "2.0.3"
},
"devDependencies": {
"@commitlint/cli": "17.6.3",
"@faker-js/faker": "8.0.1",
Expand Down
10 changes: 6 additions & 4 deletions src/types/__tests__/fn.spec-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
import type TestSubject from '../fn'

describe('unit-d:types/Fn', () => {
type A = [string, RegExp | string | undefined, (number | undefined)?]
type R = string[]
type Subject = TestSubject<A, R>

it('should be callable with A', () => {
expectTypeOf<TestSubject>().parameters.toEqualTypeOf<any[]>()
expectTypeOf<TestSubject<[string]>>().parameters.toEqualTypeOf<[string]>()
expectTypeOf<Subject>().parameters.toEqualTypeOf<A>()
})

it('should return R', () => {
expectTypeOf<TestSubject>().returns.toBeAny()
expectTypeOf<TestSubject<any, string>>().returns.toBeString()
expectTypeOf<Subject>().returns.toEqualTypeOf<R>()
})
})
4 changes: 2 additions & 2 deletions src/types/fn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
/**
* A function.
*
* @template A - Arguments type
* @template R - Return type
* @template A - Function arguments type
* @template R - Function return type
*/
type Fn<A extends any[] = any[], R = any> = Function & ((...args: A) => R)

Expand Down
2 changes: 1 addition & 1 deletion src/types/nilable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import type NIL from './nil'

/**
* Type representing a value that also be `NIL`.
* Constructs a union type of {@linkcode NIL} and `T`.
*
* @template T - Value type
*/
Expand Down
2 changes: 1 addition & 1 deletion src/types/nullable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

/**
* Constructs a type where the value can be any type, or `null`.
* Constructs a union type of `T` and `null`.
*
* @template T - Value type
*/
Expand Down
38 changes: 38 additions & 0 deletions src/utils/__tests__/equal.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* @file Unit Tests - equal
* @module tutils/utils/tests/unit/equal
*/

import INTEGER from '#fixtures/integer'
import testSubject from '../equal'

describe('unit:utils/equal', () => {
it('should return false if a and b are not deeply equal', () => {
// Arrange
const cases: Parameters<typeof testSubject>[] = [
[{}, { value: faker.number.int() }],
[[], [faker.number.int(), faker.number.int(), faker.number.int()]],
[INTEGER, INTEGER * -1],
[faker.date.future(), faker.date.past()]
]

// Act + Expect
cases.forEach(([a, b]) => expect(testSubject(a, b)).to.be.false)
})

it('should return true if a and b are deeply equal', () => {
// Arrange
const cases: Parameters<typeof testSubject>[] = [
[{ value: INTEGER }, { value: INTEGER }],
[[INTEGER], [INTEGER]],
[INTEGER, INTEGER],
[new Date(), new Date()],
[new Date(), new Date(), value => (value as Date).getTime()]
]

// Act + Expect
cases.forEach(([a, b, identity]) => {
expect(testSubject(a, b, identity)).to.be.true
})
})
})
9 changes: 7 additions & 2 deletions src/utils/__tests__/is-function.spec-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import type { Fn } from '#src/types'
import type testSubject from '../is-function'

describe('unit-d:utils/isFunction', () => {
it('should guard Fn', () => {
expectTypeOf<typeof testSubject>().guards.toEqualTypeOf<Fn>()
it('should guard Fn<A, R>', () => {
// Arrange
type A = any[]
type R = any

// Expect
expectTypeOf<typeof testSubject<A, R>>().guards.toEqualTypeOf<Fn>()
})
})
2 changes: 1 addition & 1 deletion src/utils/__tests__/is-null.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import testSubject from '../is-null'

describe('unit:utils/isNull', () => {
it('should return false if value is not null', () => {
expect(testSubject(faker.datatype.datetime())).to.be.false
expect(testSubject(faker.date.anytime())).to.be.false
})

it('should return true if value is null', () => {
Expand Down
6 changes: 3 additions & 3 deletions src/utils/__tests__/is-object-plain.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ describe('unit:utils/isObjectPlain', () => {
it('should return false if value is not plain object', () => {
// Arrange
const cases: Parameters<typeof testSubject>[] = [
[faker.datatype.array()],
[new Date()],
[[]],
[faker.date.anytime()],
[null]
]

Expand All @@ -21,7 +21,7 @@ describe('unit:utils/isObjectPlain', () => {
it('should return true if value is plain object', () => {
// Arrange
const cases: Parameters<typeof testSubject>[] = [
[{ x: 0, y: 0 }],
[{ zodiac: faker.person.zodiacSign() }],
[Object.create(null)]
]

Expand Down
2 changes: 1 addition & 1 deletion src/utils/__tests__/is-undefined.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import testSubject from '../is-undefined'

describe('unit:utils/isUndefined', () => {
it('should return false if value is not undefined', () => {
expect(testSubject(faker.datatype.array())).to.be.false
expect(testSubject(faker.date.anytime())).to.be.false
})

it('should return true if value is undefined', () => {
Expand Down
35 changes: 35 additions & 0 deletions src/utils/equal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* @file Utilities - equal
* @module tutils/utils/equal
*/

import type { Fn, IndexSignature, Nilable, NumberString } from '#src/types'
import { dequal } from 'dequal'
import isFunction from './is-function'

/**
* Returns a boolean indicating if values `a` and `b` are deeply equal.
*
* An `identity` function can be used to convert either value to a unique key.
* If provided, items with the same identity key will be considered equal.
*
* @see https://github.com/lukeed/dequal
*
* @template A - First comparison value type
* @template B - Second comparison value type
* @template K - Identity key type
*
* @param {unknown} a - First comparison value
* @param {unknown} b - Second comparison value
* @param {Nilable<Fn<[A | B], K>>} [identity] - Identity key function
* @return {boolean} `true` if `a` and `b` are deeply equal
*/
function equal<A, B, K extends IndexSignature = NumberString>(
a: A,
b: B,
identity?: Nilable<Fn<[A | B], K>>
): boolean {
return isFunction(identity) ? dequal(identity(a), identity(b)) : dequal(a, b)
}

export default equal
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

export { default as capitalize } from './capitalize'
export { default as equal } from './equal'
export { default as isAppEnv } from './is-app-env'
export { default as isArray } from './is-array'
export { default as isBigInt } from './is-big-int'
Expand Down
3 changes: 2 additions & 1 deletion src/utils/is-array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* @module tutils/utils/isArray
*/

import equal from './equal'
import isObject from './is-object'

/**
Expand All @@ -14,7 +15,7 @@ import isObject from './is-object'
* @return {value is ReadonlyArray<T> | T[]} `true` if `value` is an array
*/
function isArray<T>(value: unknown): value is T[] | readonly T[] {
return isObject(value) && Reflect.get(value, 'constructor') === Array
return isObject(value) && equal(Reflect.get(value, 'constructor'), Array)
}

export default isArray
3 changes: 2 additions & 1 deletion src/utils/is-date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* @module tutils/utils/isDate
*/

import equal from './equal'
import isObject from './is-object'

/**
Expand All @@ -12,7 +13,7 @@ import isObject from './is-object'
* @return {value is Date} `true` if `value` is {@linkcode Date} instance
*/
function isDate(value: unknown): value is Date {
return isObject(value) && Reflect.get(value, 'constructor') === Date
return isObject(value) && equal(Reflect.get(value, 'constructor'), Date)
}

export default isDate
8 changes: 5 additions & 3 deletions src/utils/is-empty-string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@
*/

import type { EmptyString } from '#src/types'
import equal from './equal'
import isString from './is-string'
import trim from './trim'

/**
* Checks if the given `value` is an empty string.
* Checks if `value` is an empty string.
*
* @param {unknown} value - Value to evaluate
* @param {unknown} value - Value to check
* @return {value is EmptyString} `true` if `value` is empty string
*/
const isEmptyString = (value: unknown): value is EmptyString => {
return isString(value) ? value.trim() === '' : false
return isString(value) && equal(trim(value), '')
}

export default isEmptyString
3 changes: 2 additions & 1 deletion src/utils/is-float.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import type { Float } from '#src/types'
import equal from './equal'
import isNumber from './is-number'

/**
Expand All @@ -15,7 +16,7 @@ import isNumber from './is-number'
* @return {value is Float} `true` if `value` is a float
*/
function isFloat(value: unknown): value is Float {
return isNumber(value) && value % 1 !== 0
return isNumber(value) && !equal(value % 1, 0)
}

export default isFloat
13 changes: 9 additions & 4 deletions src/utils/is-function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@
import type { Fn } from '#src/types'

/**
* Checks if the given `value` is a function.
* Checks if `value` is a function.
*
* @param {unknown} value - Value to evaluate
* @return {value is Fn} `true` if `value` is a function
* @template A - Function arguments type
* @template R - Function return type
*
* @param {unknown} value - Value to check
* @return {value is Fn<A, R>} `true` if `value` is a function
*/
const isFunction = (value: unknown): value is Fn => typeof value === 'function'
function isFunction<A extends any[], R>(value: unknown): value is Fn<A, R> {
return typeof value === 'function'
}

export default isFunction
3 changes: 2 additions & 1 deletion src/utils/is-integer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import type { Integer } from '#src/types'
import equal from './equal'
import isNumber from './is-number'

/**
Expand All @@ -15,7 +16,7 @@ import isNumber from './is-number'
* @return {value is Integer} `true` if `value` is an integer
*/
function isInteger(value: unknown): value is Integer {
return isNumber(value) && value % 1 === 0
return isNumber(value) && equal(value % 1, 0)
}

export default isInteger
3 changes: 2 additions & 1 deletion src/utils/is-lowercase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* @module tutils/utils/isLowercase
*/

import equal from './equal'
import isString from './is-string'
import lowercase from './lowercase'

Expand All @@ -15,7 +16,7 @@ import lowercase from './lowercase'
* @return {value is Lowercase<T>} `true` if `value` is lowercase string
*/
function isLowercase<T extends string>(value: unknown): value is Lowercase<T> {
return isString(value) && value === lowercase(value)
return isString(value) && equal(value, lowercase(value))
}

export default isLowercase
3 changes: 2 additions & 1 deletion src/utils/is-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* @module tutils/utils/isMap
*/

import equal from './equal'
import isObject from './is-object'

/**
Expand All @@ -15,7 +16,7 @@ import isObject from './is-object'
* @return {value is Map<K, V>} `true` if `value` is {@linkcode Map} instance
*/
function isMap<K, V>(value: unknown): value is Map<K, V> {
return isObject(value) && Reflect.get(value, 'constructor') === Map
return isObject(value) && equal(Reflect.get(value, 'constructor'), Map)
}

export default isMap
8 changes: 5 additions & 3 deletions src/utils/is-null.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
* @module tutils/utils/isNull
*/

import equal from './equal'

/**
* Checks if the given `value` is `null`.
* Checks if `value` is `null`.
*
* @param {unknown} value - Value to evaluate
* @param {unknown} value - Value to check
* @return {value is null} `true` if `value` is `null`
*/
const isNull = (value: unknown): value is null => value === null
const isNull = (value: unknown): value is null => equal(value, null)

export default isNull
7 changes: 4 additions & 3 deletions src/utils/is-object-plain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
*/

import type { ObjectPlain } from '#src/types'
import equal from './equal'
import isNull from './is-null'

/**
* Checks if the given `value` is a plain object (i.e. [POJO][1]).
* Checks if `value` is a plain object (i.e. [POJO][1]).
*
* A plain object is an object created by the [`Object`][2] constructor or an
* object with a `[[Prototype]]` of `null`.
Expand All @@ -17,7 +18,7 @@ import isNull from './is-null'
*
* @see {@linkcode ObjectPlain}
*
* @param {unknown} value - Value to evaluate
* @param {unknown} value - Value to check
* @return {value is ObjectPlain} `true` if `value` is plain object
*/
const isObjectPlain = (value: unknown): value is ObjectPlain => {
Expand Down Expand Up @@ -51,7 +52,7 @@ const isObjectPlain = (value: unknown): value is ObjectPlain => {
}

// prototypes are equal => value is plain object
plain = Object.getPrototypeOf(value) === proto
plain = equal(Object.getPrototypeOf(value), proto)

break
}
Expand Down
Loading

0 comments on commit e0b3571

Please sign in to comment.