From aa219983ad230ad8ab25a3f537f9cf29b84ed9d8 Mon Sep 17 00:00:00 2001 From: Ben Meyrick Date: Fri, 26 Jun 2026 22:00:03 +0100 Subject: [PATCH 1/3] feat: improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New utilities: - sortBy(): comparator factory with dotted paths, descending, case-insensitive, tie-breaking, mapping functions - SortMappingFunction type - pascalToKebab(): PascalCase → kebab-case conversion - isArray(): typed array predicate Breaking changes: - ValueType enum members renamed to PascalCase (e.g. ValueType.String, ValueType.Array) - difference() now returns a copy of the first array when values is empty (previously returned []) isEqual rewrite: - Removed Moment.js dependency - Added cycle detection (WeakMap>) - Uses Reflect.ownKeys + propertyIsEnumerable for property comparison - Added bigint and symbol to IndividualEqualityType - More accurate ArrayBuffer.isView byte-level comparison Other improvements: - typeOf: removed isMoment, value: any → unknown, improved constructorName null safety - isEmpty: uses Reflect.ownKeys for object empty check, cleaner types (no namespace) - isPlainObject: uses Object.getPrototypeOf for null-prototype check - isMergeableObject: fixed filename typo (isMergable → isMergeable), added Map/Set/WeakMap/WeakSet/ArrayBuffer checks - merge: readonly MergeOptions, function overloads, prototype pollution guards (__proto__/prototype/constructor), symbol key support - randomNumberBetweenRange: validation for non-finite, non-safe-integer, and empty ranges - All simple predicates (isArguments, isBuffer, isDate, isError, isGeneratorObject, isRegExp, isBoolean, isNumber, isString, isObject, isNullOrUndefined, isNaNStrict): value: any → value: unknown - package.json exports: added types condition for import and require - README: documented new functions, fixed isMergableObject typo, updated ValueType note --- README.md | 173 ++++++++++++++++++-- package-lock.json | 116 +++++++------ package.json | 12 +- src/clone/clone.spec.ts | 8 - src/clone/clone.ts | 44 +++-- src/difference/difference.spec.ts | 4 +- src/difference/difference.ts | 34 ++-- src/freeze/freeze.ts | 42 ++--- src/index.ts | 2 + src/math/randomNumberBetweenRange.ts | 34 +++- src/merge/merge.ts | 153 +++++++++-------- src/sorting/sort-by.spec.ts | 109 +++++++++++++ src/sorting/sort-by.ts | 143 ++++++++++++++++ src/sorting/sort-mapping-function.model.ts | 13 ++ src/strings/index.ts | 1 + src/strings/pascal-to-kebab.spec.ts | 7 + src/strings/pascal-to-kebab.ts | 13 ++ src/type-predicates/index.ts | 3 +- src/type-predicates/isArguments.ts | 21 +-- src/type-predicates/isArray.spec.ts | 15 ++ src/type-predicates/isArray.ts | 9 + src/type-predicates/isBoolean.ts | 7 +- src/type-predicates/isBuffer.ts | 28 +++- src/type-predicates/isDate.ts | 7 +- src/type-predicates/isEmpty.ts | 47 +++--- src/type-predicates/isEqual.ts | 80 +++++---- src/type-predicates/isError.ts | 7 +- src/type-predicates/isGeneratorObject.ts | 16 +- src/type-predicates/isMergeableObject.ts | 26 +++ src/type-predicates/isNaNStrict.ts | 9 +- src/type-predicates/isNullOrUndefined.ts | 7 +- src/type-predicates/isNumber.ts | 7 +- src/type-predicates/isObject.ts | 7 +- src/type-predicates/isPlainObject.ts | 24 +-- src/type-predicates/isRegExp.ts | 7 +- src/type-predicates/isString.ts | 7 +- src/type-predicates/typeOf.spec.ts | 143 ++++++++-------- src/type-predicates/typeOf.ts | 181 +++++++++++---------- 38 files changed, 1075 insertions(+), 491 deletions(-) create mode 100644 src/sorting/sort-by.spec.ts create mode 100644 src/sorting/sort-by.ts create mode 100644 src/sorting/sort-mapping-function.model.ts create mode 100644 src/strings/pascal-to-kebab.spec.ts create mode 100644 src/strings/pascal-to-kebab.ts create mode 100644 src/type-predicates/isArray.spec.ts create mode 100644 src/type-predicates/isArray.ts create mode 100644 src/type-predicates/isMergeableObject.ts diff --git a/README.md b/README.md index 9b3efb9..7b2fc6b 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,9 @@ A collection of useful utility functions with associated TypeScript types. - [isBuffer](#isbuffer) - [isError](#iserror) - [isGeneratorObject](#isgeneratorobject) + - [isArray](#isarray) - [isPlainObject](#isplainobject) + - [isMergeableObject](#ismergeableobject) - [isReactElement](#isreactelement) - [typeOf](#typeof) - [clone](#clone) @@ -45,15 +47,19 @@ A collection of useful utility functions with associated TypeScript types. - [merge](#merge) - [merge options](#merge-options) - [arrayMerge](#arraymerge) - - [isMergableObject](#ismergableobject) + - [isMergeableObject](#ismergeableobject) - [customMerge](#custommerge) - [Merge performance comparison](#merge-performance-comparison) + - [Sorting](#sorting) + - [sortBy](#sortby) + - [SortMappingFunction](#sortmappingfunction) - [Strings](#strings) - [capitalise](#capitalise) - [CapitaliseOptions](#capitaliseoptions) - [CapitaliseFirst](#capitalisefirst) - [CapitaliseLast](#capitaliselast) - [kebabToPascal](#kebabtopascal) + - [pascalToKebab](#pascaltokebab) - [difference](#difference) - [formatTime](#formattime) - [FormatTimeOptions](#formattimeoptions) @@ -558,6 +564,32 @@ if (isGeneratorObject(value)) { --- +#### isArray + +Checks if a value is an array. + +Method arguments: + +| Parameter | Type | Optional | Description | +| --------- | ------- | -------- | ------------------ | +| value | unknown | false | The value to check | + +Return type: `value is unknown[]` + +**Example:** + +```typescript +import { isArray } from '@qntm-code/utils'; + +const value = getTheValue(); + +if (isArray(value)) { + // Do something +} +``` + +--- + #### isPlainObject Determines whether a given value is a plain object @@ -584,6 +616,30 @@ if (isPlainObject(value)) { --- +#### isMergeableObject + +Determines whether a given value should be recursively iterated when merging. Returns `false` for arrays, Dates, RegExps, Maps, Sets, WeakMaps, WeakSets, ArrayBuffers, and typed arrays. + +Method arguments: + +| Parameter | Type | Optional | Description | +| --------- | ------- | -------- | ------------------ | +| value | unknown | false | The value to check | + +Return type: `boolean` + +**Example:** + +```typescript +import { isMergeableObject } from '@qntm-code/utils'; + +if (isMergeableObject(value)) { + // Recurse into properties +} +``` + +--- + #### isReactElement Determines whether a given value is a React element @@ -612,13 +668,15 @@ if (isReactElement(value)) { #### typeOf -Returns the type of a given value +Returns the type of a given value. Returns a `ValueType` enum member for all known types, or a lower-cased intrinsic tag string for unknown types. + +The `ValueType` enum uses PascalCase members (e.g. `ValueType.String`, `ValueType.Array`, `ValueType.Map`). Method arguments: -| Parameter | Type | Optional | Description | -| --------- | ---- | -------- | ------------------ | -| value | any | false | The value to check | +| Parameter | Type | Optional | Description | +| --------- | ------- | -------- | ------------------ | +| value | unknown | false | The value to check | Return type: `ValueType` | `string` @@ -629,7 +687,7 @@ import { typeOf, ValueType } from '@qntm-code/utils'; const value = getTheValue(); -if (typeOf(value) === ValueType.string) { +if (typeOf(value) === ValueType.String) { // Do something } ``` @@ -776,7 +834,7 @@ const combineMerge = (target, source, options) => { merge([{ a: true }], [{ b: true }, 'ah yup'], { arrayMerge: combineMerge }); // => [{ a: true, b: true }, 'ah yup'] ``` -##### isMergableObject +##### isMergeableObject By default, `merge` clones every property from almost every kind of object. @@ -990,18 +1048,44 @@ const pascal = kebabToPascal(value); --- +### pascalToKebab + +Converts a PascalCase string to kebab-case. + +Method arguments: + +| Parameter | Type | Optional | Description | +| --------- | ------ | -------- | --------------------- | +| value | string | false | The string to convert | + +Return type: `string` + +**Example:** + +```typescript +import { pascalToKebab } from '@qntm-code/utils'; + +const value = 'HelloWorld'; + +const kebab = pascalToKebab(value); + +// kebab = 'hello-world' +``` + +--- + ### difference -Creates an array of array values not included in the other given array using isEqual for equality comparisons. The order and references of result values are determined by the first array. +Creates an array of values from `array` that are not included in `values`, using deep equality for comparisons. The order and references of result values are determined by the first array. If `values` is empty, a copy of `array` is returned. Method arguments: -| Parameter | Type | Optional | Description | -| --------- | ---------- | -------- | -------------------------- | -| array | Array | false | The array to check | -| values | Array | false | The array to check against | +| Parameter | Type | Optional | Description | +| --------- | -------------------- | -------- | --------------------- | +| array | `readonly T[]` | false | The array to inspect | +| values | `readonly unknown[]` | false | The values to exclude | -Return type: `Array` +Return type: `T[]` **Example:** @@ -1010,6 +1094,69 @@ import { difference } from '@qntm-code/utils'; const diff = difference([1, 2], [2, 3]); // returns [1] + +const all = difference([1, 2], []); +// returns [1, 2] (copy of array — nothing excluded) +``` + +--- + +### Sorting + +--- + +#### sortBy + +Creates a comparator function for use with `Array.prototype.sort`. + +- A plain path (e.g. `'name'`) sorts ascending by that property. +- Prefix with `'-'` (e.g. `'-age'`) to sort descending. +- Suffix with `'^'` (e.g. `'name^'`) for case-insensitive sort. +- The special path `'^'` sorts the items themselves. +- Dot-notation is supported for nested paths (e.g. `'address.city'`). +- Multiple properties are applied left-to-right as tie-breakers. +- An optional [`SortMappingFunction`](#sortmappingfunction) transforms values before comparison. +- `null`/`undefined`/empty values sort first in ascending order. + +Method arguments: + +| Parameter | Type | Optional | Description | +| ------------- | ----------------------------------- | -------- | ---------------------------------------- | +| ...properties | `(string \| SortMappingFunction)[]` | true | Property paths and/or a mapping function | + +Return type: `(a: T, b: T) => number` + +**Example:** + +```typescript +import { sortBy } from '@qntm-code/utils'; + +const people = [ + { name: 'Charlie', age: 30 }, + { name: 'alice', age: 25 }, + { name: 'Bob', age: 25 }, +]; + +// Sort by age ascending, then name case-insensitively +people.sort(sortBy('age', 'name^')); +// => [{ name: 'alice', age: 25 }, { name: 'Bob', age: 25 }, { name: 'Charlie', age: 30 }] + +// Sort descending +people.sort(sortBy('-age')); + +// Sort an array of strings directly +['Banana', 'apple', 'Cherry'].sort(sortBy()); +// => ['apple', 'Banana', 'Cherry'] +``` + +--- + +##### SortMappingFunction + +A function that transforms a property value before comparison. + +```typescript +type SortMappingFunction = (key: string, value: unknown) => unknown; ``` --- diff --git a/package-lock.json b/package-lock.json index 5bf35bd..0b6f211 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2154,6 +2154,16 @@ "dev": true, "license": "MIT" }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, "node_modules/argv": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/argv/-/argv-0.0.2.tgz", @@ -2255,9 +2265,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "dependencies": { @@ -2530,30 +2540,6 @@ "node": ">=4.0" } }, - "node_modules/codecov/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/codecov/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2772,9 +2758,9 @@ } }, "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -4118,6 +4104,20 @@ "dev": true, "license": "MIT" }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -5796,7 +5796,7 @@ "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true, "license": "BSD-3-Clause" }, @@ -7845,6 +7845,15 @@ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "dev": true }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, "argv": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/argv/-/argv-0.0.2.tgz", @@ -7905,9 +7914,9 @@ } }, "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "requires": { "balanced-match": "^1.0.0", @@ -8059,27 +8068,6 @@ "js-yaml": "3.14.1", "teeny-request": "7.1.1", "urlgrey": "1.0.0" - }, - "dependencies": { - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - } } }, "concat-map": { @@ -8227,9 +8215,9 @@ "dev": true }, "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true }, "dunder-proto": { @@ -9087,6 +9075,16 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, "jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -10040,7 +10038,7 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, "stackback": { diff --git a/package.json b/package.json index d4bfb52..4e5726c 100644 --- a/package.json +++ b/package.json @@ -96,8 +96,14 @@ "types": "./dist/mjs/index.d.ts", "exports": { ".": { - "import": "./dist/mjs/index.js", - "require": "./dist/cjs/index.js" + "import": { + "types": "./dist/mjs/index.d.ts", + "default": "./dist/mjs/index.js" + }, + "require": { + "types": "./dist/cjs/index.d.ts", + "default": "./dist/cjs/index.js" + } } }, "scripts": { @@ -164,4 +170,4 @@ "dependencies": { "viewport-details": "^3.0.1" } -} +} \ No newline at end of file diff --git a/src/clone/clone.spec.ts b/src/clone/clone.spec.ts index 32a2d83..3b2112b 100644 --- a/src/clone/clone.spec.ts +++ b/src/clone/clone.spec.ts @@ -1,4 +1,3 @@ -import moment from 'moment'; import { isEqual } from '../type-predicates'; import { clone } from './clone'; @@ -99,13 +98,6 @@ describe('clone', () => { expect(isEqual(clone(0), 0)).toBe(true); }); - it(`should use moment's own clone method`, () => { - const source = moment(); - const cloned = clone(source).add(1, 'day'); - - expect(source.isSame(cloned)).toBe(false); - }); - describe(`RegExp`, () => { it('should clone a regex with flags', () => { expect(clone(/foo/g)).toEqual(/foo/g); diff --git a/src/clone/clone.ts b/src/clone/clone.ts index 437bb50..4069dce 100644 --- a/src/clone/clone.ts +++ b/src/clone/clone.ts @@ -1,4 +1,3 @@ -import { Moment } from 'moment'; import { isPlainObject } from '../type-predicates/isPlainObject.js'; import { typeOf, ValueType } from '../type-predicates/typeOf.js'; @@ -9,27 +8,27 @@ type InstanceClone = ((value: T) => T) | boolean; */ export function clone(value: T, instanceClone: InstanceClone = false): T { switch (typeOf(value)) { - case ValueType.object: { + case ValueType.Object: { return cloneObjectDeep(value, instanceClone); } - case ValueType.map: { + case ValueType.Map: { return cloneMapDeep(value as unknown as Map, instanceClone as unknown as InstanceClone) as unknown as T; } - case ValueType.set: { + case ValueType.Set: { return cloneSetDeep(value as unknown as Set, instanceClone as unknown as InstanceClone) as unknown as T; } - case ValueType.array: - case ValueType.int8array: - case ValueType.uint8array: - case ValueType.uint8clampedarray: - case ValueType.int16array: - case ValueType.uint16array: - case ValueType.int32array: - case ValueType.uint32array: - case ValueType.float32array: - case ValueType.float64array: - case ValueType.bigint64array: - case ValueType.biguint64array: { + case ValueType.Array: + case ValueType.Int8Array: + case ValueType.Uint8Array: + case ValueType.Uint8ClampedArray: + case ValueType.Int16Array: + case ValueType.Uint16Array: + case ValueType.Int32Array: + case ValueType.Uint32Array: + case ValueType.Float32Array: + case ValueType.Float64Array: + case ValueType.BigInt64Array: + case ValueType.BigUint64Array: { return cloneArrayDeep(value, instanceClone); } default: { @@ -40,25 +39,22 @@ export function clone(value: T, instanceClone: InstanceClone = false): T { function cloneShallow(value: T): T { switch (typeOf(value)) { - case ValueType.buffer: { + case ValueType.Buffer: { return cloneBuffer(value as Buffer) as unknown as T; } - case ValueType.symbol: { + case ValueType.Symbol: { return cloneSymbol(value as symbol) as unknown as T; } - case ValueType.error: { + case ValueType.Error: { // eslint-disable-next-line @typescript-eslint/no-unsafe-return return Object.create(value as Error); } - case ValueType.date: { + case ValueType.Date: { return new Date(value as Date) as unknown as T; } - case ValueType.regexp: { + case ValueType.RegExp: { return cloneRegExp(value as unknown as RegExp) as unknown as T; } - case ValueType.moment: { - return (value as Moment).clone() as T; - } } return value; diff --git a/src/difference/difference.spec.ts b/src/difference/difference.spec.ts index fac8448..79c63d9 100644 --- a/src/difference/difference.spec.ts +++ b/src/difference/difference.spec.ts @@ -5,8 +5,8 @@ describe('difference', () => { expect(difference([], [3, 2])).toEqual([]); }); - it('Should return an empty array if the provided values has no values', () => { - expect(difference([2, 1], [])).toEqual([]); + it('Should return a copy of the array if the provided values has no values', () => { + expect(difference([2, 1], [])).toEqual([2, 1]); }); it('Should find the difference between two number arrays', () => { diff --git a/src/difference/difference.ts b/src/difference/difference.ts index 5a6499f..4e0f375 100644 --- a/src/difference/difference.ts +++ b/src/difference/difference.ts @@ -1,24 +1,26 @@ import { isEqual } from '../index.js'; /** - * Creates an array of array values not included in the other given array using isEqual for equality comparisons. The order and references - * of result values are determined by the first array. + * Creates an array of values from `array` that are not included in `values`, using deep equality (`isEqual`) for comparisons. The order and references of result values are determined by the first array. + * + * @param array - The array to inspect. + * @param values - The values to exclude. + * @returns A new array of filtered values. */ -export function difference(array: T, values: any[]): T { - if (!array.length || !values.length) { - return [] as unknown as T; +export function difference(array: readonly T[], values: readonly unknown[]): T[] { + if (!array.length) { + return []; } - return array.reduce( - (result, item) => { - if (!values.some(value => isEqual(item, value))) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call - result.push(item); - } + if (!values.length) { + return array.slice(); + } + + return array.reduce((result, item) => { + if (!values.some(value => isEqual(item, value))) { + result.push(item); + } - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return result; - }, - [] as unknown as T - ) as T; + return result; + }, []); } diff --git a/src/freeze/freeze.ts b/src/freeze/freeze.ts index a311180..181b3ad 100644 --- a/src/freeze/freeze.ts +++ b/src/freeze/freeze.ts @@ -52,45 +52,45 @@ function freezeInternal(value: T, ctx: FreezeContext): unknown { const valueType = typeOf(value); switch (valueType) { - case ValueType.array: { + case ValueType.Array: { return freezeArrayDeep(value as unknown as unknown[], ctx); } - case ValueType.object: { + case ValueType.Object: { return freezeObjectDeep(value as unknown as Record, ctx); } - case ValueType.map: { + case ValueType.Map: { return freezeMapDeep(value as unknown as Map, ctx); } - case ValueType.set: { + case ValueType.Set: { return freezeSetDeep(value as unknown as Set, ctx); } - case ValueType.date: { + case ValueType.Date: { return freezeDate(value as unknown as Date, ctx); } - case ValueType.regexp: { + case ValueType.RegExp: { return Object.freeze(value); } - case ValueType.error: { + case ValueType.Error: { return freezeObjectDeep(value as unknown as Record, ctx); } - case ValueType.weakmap: { + case ValueType.WeakMap: { return freezeWeakMap(value as unknown as WeakMap, ctx); } - case ValueType.weakset: { + case ValueType.WeakSet: { return freezeWeakSet(value as unknown as WeakSet, ctx); } - case ValueType.buffer: - case ValueType.int8array: - case ValueType.uint8array: - case ValueType.uint8clampedarray: - case ValueType.int16array: - case ValueType.uint16array: - case ValueType.int32array: - case ValueType.uint32array: - case ValueType.float32array: - case ValueType.float64array: - case ValueType.bigint64array: - case ValueType.biguint64array: { + case ValueType.Buffer: + case ValueType.Int8Array: + case ValueType.Uint8Array: + case ValueType.Uint8ClampedArray: + case ValueType.Int16Array: + case ValueType.Uint16Array: + case ValueType.Int32Array: + case ValueType.Uint32Array: + case ValueType.Float32Array: + case ValueType.Float64Array: + case ValueType.BigInt64Array: + case ValueType.BigUint64Array: { return freezeArrayBufferView(value as unknown as ArrayBufferView, ctx); } default: { diff --git a/src/index.ts b/src/index.ts index f5d5cde..faad4a5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,8 @@ export * from './freeze/freeze.js'; export * from './insertAtIndex/insertAtIndex.js'; export * from './math/index.js'; export * from './merge/merge.js'; +export * from './sorting/sort-by.js'; +export * from './sorting/sort-mapping-function.model.js'; export * from './strings/index.js'; export * from './type-predicates/index.js'; export * from './types/index.js'; diff --git a/src/math/randomNumberBetweenRange.ts b/src/math/randomNumberBetweenRange.ts index ca6f809..ef86bbd 100644 --- a/src/math/randomNumberBetweenRange.ts +++ b/src/math/randomNumberBetweenRange.ts @@ -1,11 +1,35 @@ /** - * Returns a random whole number between a given range + * Returns a random whole number between `min` and `max` (inclusive). + * + * Non-integer inputs are rounded to an integer range using `Math.ceil(min)` and `Math.floor(max)`. + * + * @param min - The minimum number in the range. + * @param max - The maximum number in the range. + * @returns A random whole number between the given range. + * @throws Will throw an error if `min`/`max` are not finite, if the rounded bounds are outside the safe integer range, if no whole number exists between the rounded `min` and `max`, or if the resulting integer range is larger than `Number.MAX_SAFE_INTEGER`. */ export function randomNumberBetweenRange(min: number, max: number): number { - min = Math.ceil(min); - max = Math.floor(max); + const minCeil = Math.ceil(min); + const maxFloor = Math.floor(max); - const value = Math.random() * (max - min + 1) + min; + const errorMessage = `Cannot generate a whole number between ${min} and ${max}`; - return Math.floor(value); + if ( + !Number.isFinite(minCeil) || + !Number.isFinite(maxFloor) || + !Number.isSafeInteger(minCeil) || + !Number.isSafeInteger(maxFloor) || + minCeil > maxFloor + ) { + throw new Error(errorMessage); + } + + const range = maxFloor - minCeil + 1; + + if (!Number.isSafeInteger(range) || range <= 0) { + throw new Error(errorMessage); + } + + const randomOffset = Math.floor(Math.random() * range); + return randomOffset + minCeil; } diff --git a/src/merge/merge.ts b/src/merge/merge.ts index 0779a04..fd1d246 100644 --- a/src/merge/merge.ts +++ b/src/merge/merge.ts @@ -1,121 +1,131 @@ -import { isMergeableObject } from '../type-predicates/isMergableObject.js'; +import { isMergeableObject as defaultIsMergeableObject } from '../type-predicates/isMergeableObject.js'; -export interface MergeOptions { +interface MergeOptions { /** * The function to use for merging arrays. Defaults to concatenating the arrays */ - arrayMerge: (x: X, y: Y, options: MergeOptions) => X & Y; + readonly arrayMerge: (x: X, y: Y, options: MergeOptions) => X & Y; /** - * The function to use for merging objects. Defaults to merging the objects - + * The function to use for merging objects. Defaults to merging the objects */ - customMerge?: (key: string, options?: MergeOptions) => ((x: any, y: any) => any) | undefined; + readonly customMerge?: (key: PropertyKey, options?: MergeOptions) => ((x: unknown, y: unknown) => unknown) | undefined; /** - * The function to use for determining if a value is mergeable. Defaults to isPlainObject + * The function to use for determining if a value is mergeable. Defaults to `isMergeableObject`. */ - isMergeableObject: (value: any) => boolean; + readonly isMergeableObject: (value: unknown) => boolean; /** - * NOT OVERRIDABLE + * Internal hook used to clone values before merging. */ - cloneUnlessOtherwiseSpecified: (value, options) => any; + readonly cloneUnlessOtherwiseSpecified: (value: unknown, options: MergeOptions) => unknown; } /** - * Merges two objects x and y deeply, returning a new merged object with the elements from both x and y. - * - * If an element at the same key is present for both x and y, the value from y will appear in the result. + * Recursively merges the properties of two objects together. The source object is merged into the target object, and a new object is returned. The merge is performed recursively, meaning that nested objects and arrays are also merged. The function handles various edge cases, such as merging arrays, handling non-mergeable objects, and preventing prototype pollution. * - * Merging creates a new object, so that neither x or y is modified. + * @param x - The target object to merge into. + * @param y - The source object to merge from. + * @param options - Optional configuration for the merge operation, including custom array merging, custom object merging, and a custom function to determine if a value is mergeable. * - * Note: By default, arrays are merged by concatenating them. + * @returns A new object that is the result of merging the source object into the target object. */ -export function merge(x: Partial, y: Partial, options: Partial = {}): X & Y { - if (!options.arrayMerge) { - options.arrayMerge = defaultArrayMerge; - } - - if (!options.isMergeableObject) { - options.isMergeableObject = isMergeableObject; - } - - /** - * cloneUnlessOtherwiseSpecified is added to `options` so that custom arrayMerge() implementations can use it. The caller may not replace - * it. - */ - options.cloneUnlessOtherwiseSpecified = cloneUnlessOtherwiseSpecified; +function mergeImpl(x: readonly X[], y: readonly Y[], options?: Partial): (X | Y)[]; +function mergeImpl(x: Partial, y: Partial, options?: Partial): X & Y; +function mergeImpl(x: Partial, y: Partial, options: Partial = {}): X & Y { + const { + arrayMerge = defaultArrayMerge, + cloneUnlessOtherwiseSpecified = defaultCloneUnlessOtherwiseSpecified, + isMergeableObject = defaultIsMergeableObject, + customMerge, + } = options; + + const newOptions: MergeOptions = { + arrayMerge, + cloneUnlessOtherwiseSpecified, + isMergeableObject, + customMerge, + }; const xIsArray = Array.isArray(x); const yIsArray = Array.isArray(y); if (xIsArray !== yIsArray) { - return cloneUnlessOtherwiseSpecified(y, options as MergeOptions) as Y & X; + return cloneUnlessOtherwiseSpecified(y, newOptions) as Y & X; } else if (xIsArray) { - return options.arrayMerge(x as unknown[], y as unknown as unknown[], options as MergeOptions) as Y & X; - } else { - return mergeObject(x as object, y as object, options as MergeOptions) as Y & X; + return arrayMerge(x as unknown[], y as unknown as unknown[], newOptions) as Y & X; } -} -merge.all = function mergeAll(array: Partial[], options?: Partial): T { - if (!Array.isArray(array)) { - throw new Error('First argument should be an array'); + if (!isMergeableObject(x) || !isMergeableObject(y)) { + return cloneUnlessOtherwiseSpecified(y, newOptions) as Y & X; } - let result = {} as T; + return mergeObject(x as object, y as object, newOptions) as Y & X; +} - for (let i = 0, l = array.length; i < l; i++) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - result = merge(result as any, array[i], options); - } +export const merge = Object.assign(mergeImpl, { + all(array: Partial[], options?: Partial): T { + if (!Array.isArray(array)) { + throw new Error('First argument should be an array'); + } - return result; -}; + let result = {} as T; + + for (let i = 0, l = array.length; i < l; i++) { + result = merge(result as unknown as Partial, array[i], options); + } + + return result; + }, +}); function emptyTarget(value: T): T[] | object { return Array.isArray(value) ? [] : {}; } -function cloneUnlessOtherwiseSpecified(value: T, options: MergeOptions): T { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - return options.isMergeableObject(value) ? (merge(emptyTarget(value) as any, value as any, options) as T) : value; +function defaultCloneUnlessOtherwiseSpecified(value: T, options: MergeOptions): T { + return options.isMergeableObject(value) || Array.isArray(value) + ? merge(emptyTarget(value) as unknown as Partial, value as unknown as Partial, options) + : value; } function defaultArrayMerge(x: X, y: Y, options: MergeOptions): X & Y { - return x.concat(y).map(element => cloneUnlessOtherwiseSpecified(element, options)) as X & Y; + const merged = x.concat(y); + const cloned: unknown[] = merged.map(element => options.cloneUnlessOtherwiseSpecified(element, options)); + + return cloned as X & Y; } -function getMergeFunction(key: string, options: MergeOptions) { +function getMergeFunction(key: PropertyKey, options: MergeOptions): (x: unknown, y: unknown, options: MergeOptions) => unknown { if (!options.customMerge) { - return merge; + return merge as unknown as (x: unknown, y: unknown, options: MergeOptions) => unknown; } - const customMerge = options.customMerge(key); + const customMerge = options.customMerge(key, options); - return typeof customMerge === 'function' ? customMerge : merge; + return typeof customMerge === 'function' ? customMerge : (merge as unknown as (x: unknown, y: unknown, options: MergeOptions) => unknown); } -function getEnumerableOwnPropertySymbols(target: T): ConcatArray { +function getEnumerableOwnPropertySymbols(target: object): symbol[] { const symbols = Object.getOwnPropertySymbols(target); const result: symbol[] = []; for (const symbol of symbols) { - if (Object.propertyIsEnumerable.call(target, symbol)) { + if (Object.prototype.propertyIsEnumerable.call(target, symbol)) { result.push(symbol); } } - return result as unknown as ConcatArray; + return result; } -function getKeys(target: T) { - return Object.keys(target).concat(getEnumerableOwnPropertySymbols(target)); +function getKeys(target: object): (string | symbol)[] { + return [...Object.keys(target), ...getEnumerableOwnPropertySymbols(target)]; } -function propertyIsOnObject(object: object, property: string): boolean { +function propertyIsOnObject(object: object, property: PropertyKey): boolean { try { return property in object; } catch (_) { @@ -124,18 +134,25 @@ function propertyIsOnObject(object: object, property: string): boolean { } // Protects from prototype poisoning and unexpected merging up the prototype chain. -function propertyIsUnsafe(target: object, key: string): boolean { +function propertyIsUnsafe(target: object, key: PropertyKey): boolean { + if (key === '__proto__' || key === 'prototype' || key === 'constructor') { + return true; + } + return ( propertyIsOnObject(target, key) && // Properties are safe to merge if they don't exist in the target yet, !( - Object.hasOwnProperty.call(target, key) && // unsafe if they exist up the prototype chain, - Object.propertyIsEnumerable.call(target, key) + Object.prototype.hasOwnProperty.call(target, key) && // unsafe if they exist up the prototype chain, + Object.prototype.propertyIsEnumerable.call(target, key) ) ); // and also unsafe if they're nonenumerable. } function mergeObject(x: X, y: Y, options: MergeOptions): X & Y { const destination = {} as X & Y; + const xRecord = x as Record; + const yRecord = y as Record; + const destRecord = destination as Record; if (options.isMergeableObject(x)) { const xKeys = getKeys(x); @@ -143,7 +160,11 @@ function mergeObject(x: X, y: Y, options: Me for (let i = 0, l = xKeys.length; i < l; i++) { const key = xKeys[i]; - destination[key] = cloneUnlessOtherwiseSpecified(x[key], options) as X & Y; + if (propertyIsUnsafe(x, key)) { + continue; + } + + destRecord[key] = options.cloneUnlessOtherwiseSpecified(xRecord[key], options); } } @@ -153,13 +174,13 @@ function mergeObject(x: X, y: Y, options: Me const key = yKeys[i]; if (propertyIsUnsafe(x, key)) { - break; + continue; } - if (propertyIsOnObject(x, key) && options.isMergeableObject(y[key])) { - destination[key] = getMergeFunction(key, options)(x[key], y[key], options) as X & Y; + if (propertyIsOnObject(x, key) && (options.isMergeableObject(yRecord[key]) || Array.isArray(yRecord[key]))) { + destRecord[key] = getMergeFunction(key, options)(xRecord[key], yRecord[key], options); } else { - destination[key] = cloneUnlessOtherwiseSpecified(y[key], options) as X & Y; + destRecord[key] = options.cloneUnlessOtherwiseSpecified(yRecord[key], options); } } diff --git a/src/sorting/sort-by.spec.ts b/src/sorting/sort-by.spec.ts new file mode 100644 index 0000000..35a86eb --- /dev/null +++ b/src/sorting/sort-by.spec.ts @@ -0,0 +1,109 @@ +import { sortBy } from './sort-by.js'; + +interface Person { + name: string; + age: number; + score: string; + address: { city: string }; +} + +const people: Person[] = [ + { name: 'Charlie', age: 30, score: '85', address: { city: 'York' } }, + { name: 'alice', age: 25, score: '100', address: { city: 'Bath' } }, + { name: 'Bob', age: 25, score: '72', address: { city: 'Ely' } }, +]; + +describe('sortBy', () => { + describe('single string property', () => { + it('sorts by a string property ascending using locale order', () => { + const result = [...people].sort(sortBy('name')); + expect(result.map(p => p.name)).toEqual(['alice', 'Bob', 'Charlie']); + }); + + it('sorts by a numeric property ascending', () => { + const result = [...people].sort(sortBy('age')); + expect(result.map(p => p.age)).toEqual([25, 25, 30]); + }); + + it('sorts by a string property descending when prefixed with -', () => { + const result = [...people].sort(sortBy('-age')); + expect(result.map(p => p.age)).toEqual([30, 25, 25]); + }); + }); + + describe('case-insensitive sorting (^ suffix)', () => { + it('sorts case-insensitively when property ends with ^', () => { + const result = [...people].sort(sortBy('name^')); + expect(result.map(p => p.name)).toEqual(['alice', 'Bob', 'Charlie']); + }); + + it('sorts case-insensitively descending when prefixed with - and suffixed with ^', () => { + const result = [...people].sort(sortBy('-name^')); + expect(result.map(p => p.name)).toEqual(['Charlie', 'Bob', 'alice']); + }); + }); + + describe('multiple properties (tie-breaking)', () => { + it('breaks ties using the second property', () => { + const result = [...people].sort(sortBy('age', 'name^')); + expect(result.map(p => p.name)).toEqual(['alice', 'Bob', 'Charlie']); + }); + + it('breaks ties descending on the second property', () => { + const result = [...people].sort(sortBy('age', '-name^')); + expect(result.map(p => p.name)).toEqual(['Bob', 'alice', 'Charlie']); + }); + }); + + describe('numeric strings', () => { + it('sorts numeric strings as numbers rather than lexicographically', () => { + const result = [...people].sort(sortBy('score')); + expect(result.map(p => p.score)).toEqual(['72', '85', '100']); + }); + }); + + describe('nested (dotted) paths', () => { + it('sorts by a nested property', () => { + const result = [...people].sort(sortBy('address.city')); + expect(result.map(p => p.address.city)).toEqual(['Bath', 'Ely', 'York']); + }); + }); + + describe('no properties (sort primitives directly)', () => { + it('sorts an array of strings case-insensitively', () => { + const words = ['Banana', 'apple', 'Cherry']; + expect([...words].sort(sortBy())).toEqual(['apple', 'Banana', 'Cherry']); + }); + + it('sorts an array of numbers', () => { + const nums = [3, 1, 2]; + expect([...nums].sort(sortBy())).toEqual([1, 2, 3]); + }); + }); + + describe('mapping function', () => { + it('applies a custom mapping function during comparison', () => { + const mapper = (_key: string, value: unknown): unknown => { + if (typeof value === 'string') { + return value.length; + } + + return value; + }; + + const result = [...people].sort(sortBy('name', mapper)); + expect(result.map(p => p.name)).toEqual(['Bob', 'alice', 'Charlie']); + }); + }); + + describe('null / undefined values', () => { + it('treats missing nested values as empty strings (sorted first)', () => { + const sparse = [ + { name: 'Z', address: { city: 'York' } }, + { name: 'A', address: null as unknown as { city: string } }, + ]; + const result = [...sparse].sort(sortBy<(typeof sparse)[0]>('address.city')); + expect(result.map(p => p.name)).toEqual(['A', 'Z']); + }); + }); +}); diff --git a/src/sorting/sort-by.ts b/src/sorting/sort-by.ts new file mode 100644 index 0000000..52b6f80 --- /dev/null +++ b/src/sorting/sort-by.ts @@ -0,0 +1,143 @@ +import { isNullOrUndefined } from '../type-predicates/isNullOrUndefined.js'; +import { SortMappingFunction } from './sort-mapping-function.model.js'; + +/** + * Creates a comparator function suitable for use with `Array.prototype.sort`. + * + * Each argument can be a property path string or a `SortMappingFunction`: + * - A plain path (e.g. `'name'`) sorts ascending by that property. + * - A path prefixed with `'-'` (e.g. `'-age'`) sorts descending. + * - A path suffixed with `'^'` (e.g. `'name^'`) performs a case-insensitive sort. + * - The special path `'^'` sorts the items themselves (not a nested property). + * - Dot-notation is supported for nested properties (e.g. `'address.city'`). + * - Multiple properties are applied left-to-right as tie-breakers. + * - An optional `SortMappingFunction` can be provided to transform values + * before comparison. + * + * @param properties - One or more property paths and/or a mapping function. + * + * @returns A comparator `(a, b) => number` for use with `Array.prototype.sort`. + */ +export function sortBy(...properties: (string | SortMappingFunction)[]): (object1: T, object2: T) => number { + let props = properties.filter((prop): prop is string => typeof prop === 'string'); + const map = properties.find((prop): prop is SortMappingFunction => typeof prop === 'function'); + + if (props.length) { + props = props.map(prop => (prop.replace('-', '').length ? prop : '-^')); + } else { + props = ['^']; + } + + const comparators = props.map(prop => sort(prop, map)); + const numberOfComparators = comparators.length; + + return (object1: T, object2: T): number => { + let index = 0; + let result = 0; + + while (result === 0 && index < numberOfComparators) { + result = comparators[index](object1, object2); + index += 1; + } + + return result; + }; +} + +/** + * Does the sort calculation for an item + */ +function sort(property: string, map?: SortMappingFunction): (a: unknown, b: unknown) => number { + let prop = property; + let sortOrder = 1; + + if (prop.startsWith('-')) { + sortOrder = -1; + prop = prop.substring(1); + } + + const baseApply: SortMappingFunction = map ?? ((_key: string, value: unknown): unknown => value); + let apply: SortMappingFunction = baseApply; + + if (prop.endsWith('^')) { + prop = prop.substring(0, prop.length - 1); + + apply = (_key: string, value: unknown): unknown => { + const mapped = baseApply(_key, value); + return typeof mapped === 'string' ? mapped.toLowerCase() : mapped; + }; + } + + const getValue = prop ? buildObjectPathGetter(prop) : (obj: unknown): unknown => obj; + + return (a: unknown, b: unknown): number => { + let result = 0; + + const mappedA = cast(apply(property, getValue(a))); + const mappedB = cast(apply(property, getValue(b))); + + // Treat missing values ('' / null / undefined) as lowest so they sort first for ascending sorts (and last for descending sorts). + const aMissing = mappedA === '' || isNullOrUndefined(mappedA); + const bMissing = mappedB === '' || isNullOrUndefined(mappedB); + + if (aMissing && !bMissing) { + result = -1; + } else if (!aMissing && bMissing) { + result = 1; + } else if (typeof mappedA === 'string' || typeof mappedB === 'string' || typeof mappedA === 'symbol' || typeof mappedB === 'symbol') { + result = String(mappedA).localeCompare(String(mappedB)); + } else if ((mappedA as number) < (mappedB as number)) { + result = -1; + } else if ((mappedA as number) > (mappedB as number)) { + result = 1; + } + + return result * sortOrder; + }; +} + +/** + * Builds an accessor for a dotted object path and reuses split path parts. + */ +function buildObjectPathGetter(path: string): (object: unknown) => unknown { + const pathParts = path.split('.'); + + return (object: unknown): unknown => { + if (isNullOrUndefined(object)) { + return ''; + } + + return objectPath(object as Record, pathParts); + }; +} + +/** + * Navigates to the part of the object using path parts provided. + */ +function objectPath(object: Record, pathParts: string[]): unknown { + let result: Record = object; + + for (let index = 0, { length } = pathParts; index < length; index += 1) { + const next: unknown = result[pathParts[index]]; + + if (isNullOrUndefined(next)) { + return ''; + } + + result = next as Record; + } + + return result; +} + +/** + * Takes a value and casts it for more effective sorting. + */ +function cast(value: unknown): unknown { + // If the value is a number as a string, cast back to an actual number to sort + if (typeof value === 'string' && !!value.trim().length && !isNaN(Number(value))) { + return Number(value); + } + + return value; +} diff --git a/src/sorting/sort-mapping-function.model.ts b/src/sorting/sort-mapping-function.model.ts new file mode 100644 index 0000000..a3017bc --- /dev/null +++ b/src/sorting/sort-mapping-function.model.ts @@ -0,0 +1,13 @@ +/** + * A function that maps a property value to a sortable representation. + * Passed as the last argument to `sortBy` to customise how values are + * compared before sorting. + * + * @param key - The raw path string being sorted on (e.g. `'name'`, `'address.city'`, `'^'`). + * This is always the path as supplied to `sortBy`, not a `keyof T`, because `sortBy` + * supports dotted paths and the special `'^'` sentinel. + * @param value - The resolved property value for the current item. + * + * @returns The transformed value used for comparison. + */ +export type SortMappingFunction = (key: string, value: unknown) => unknown; diff --git a/src/strings/index.ts b/src/strings/index.ts index 2e6c845..1df2e86 100644 --- a/src/strings/index.ts +++ b/src/strings/index.ts @@ -2,3 +2,4 @@ export * from './capitalise-first.js'; export * from './capitalise-last.js'; export * from './capitalise.js'; export * from './kebab-to-pascal.js'; +export * from './pascal-to-kebab.js'; diff --git a/src/strings/pascal-to-kebab.spec.ts b/src/strings/pascal-to-kebab.spec.ts new file mode 100644 index 0000000..a473946 --- /dev/null +++ b/src/strings/pascal-to-kebab.spec.ts @@ -0,0 +1,7 @@ +import { pascalToKebab } from './pascal-to-kebab.js'; + +describe('pascalToKebab', () => { + it('should convert a PascalCase string to kebab-case', () => { + expect(pascalToKebab('TestString')).toBe('test-string'); + }); +}); diff --git a/src/strings/pascal-to-kebab.ts b/src/strings/pascal-to-kebab.ts new file mode 100644 index 0000000..db5894a --- /dev/null +++ b/src/strings/pascal-to-kebab.ts @@ -0,0 +1,13 @@ +/** + * Converts a PascalCase string to kebab-case. The function takes a string in PascalCase format, where each word starts with an uppercase letter and there are no separators between words, and transforms it into kebab-case format, where words are separated by hyphens and all letters are lowercase. It does this by splitting the input string into individual words based on the uppercase letters, converting each word to lowercase, and then joining the words together with hyphens. + * + * @param value - The input string in PascalCase format to be converted to kebab-case. + * @returns A new string in kebab-case format. + */ +export function pascalToKebab(value: string): string { + return value + .split(/(?=[A-Z])/) + .filter(Boolean) + .map(word => word.toLowerCase()) + .join('-'); +} diff --git a/src/type-predicates/index.ts b/src/type-predicates/index.ts index 9fd5c01..0a36006 100644 --- a/src/type-predicates/index.ts +++ b/src/type-predicates/index.ts @@ -1,4 +1,5 @@ export * from './isArguments.js'; +export * from './isArray.js'; export * from './isBoolean.js'; export * from './isBuffer.js'; export * from './isDate.js'; @@ -6,7 +7,7 @@ export * from './isEmpty.js'; export * from './isEqual.js'; export * from './isError.js'; export * from './isGeneratorObject.js'; -export * from './isMergableObject.js'; +export * from './isMergeableObject.js'; export * from './isMoment.js'; export * from './isNaNStrict.js'; export * from './isNullOrUndefined.js'; diff --git a/src/type-predicates/isArguments.ts b/src/type-predicates/isArguments.ts index 1e5e4cb..434af15 100644 --- a/src/type-predicates/isArguments.ts +++ b/src/type-predicates/isArguments.ts @@ -1,18 +1,9 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ - /** - * Determines if a value is an Arguments object. + * Checks if a value is an `arguments` object. + * + * @param value - The value to check. + * @returns `true` if the value is an `arguments` object, otherwise `false`. */ -export function isArguments(value: any): boolean { - try { - if (typeof value.length === 'number' && typeof value.callee === 'function') { - return true; - } - } catch (error) { - if (error instanceof Error && error.message.indexOf('callee') !== -1) { - return true; - } - } - - return false; +export function isArguments(value: unknown): boolean { + return Object.prototype.toString.call(value) === '[object Arguments]'; } diff --git a/src/type-predicates/isArray.spec.ts b/src/type-predicates/isArray.spec.ts new file mode 100644 index 0000000..2bb9b4c --- /dev/null +++ b/src/type-predicates/isArray.spec.ts @@ -0,0 +1,15 @@ +import { isArray } from './isArray.js'; + +describe('isArray', () => { + it('should return true if value is an Array', () => { + const value: never[] = []; + + expect(isArray(value)).toBe(true); + }); + + it('should return false if value is not an Array', () => { + const value = {}; + + expect(isArray(value)).toBe(false); + }); +}); diff --git a/src/type-predicates/isArray.ts b/src/type-predicates/isArray.ts new file mode 100644 index 0000000..6e3d096 --- /dev/null +++ b/src/type-predicates/isArray.ts @@ -0,0 +1,9 @@ +/** + * Checks if a value is an array. + * + * @param value - The value to check. + * @returns `true` if the value is an array, otherwise `false`. + */ +export function isArray(value: unknown): value is unknown[] { + return Array.isArray(value); +} diff --git a/src/type-predicates/isBoolean.ts b/src/type-predicates/isBoolean.ts index 3995b8a..3866897 100644 --- a/src/type-predicates/isBoolean.ts +++ b/src/type-predicates/isBoolean.ts @@ -1,6 +1,9 @@ /** - * Determines whether a given value is a boolean + * Checks if a value is a boolean. + * + * @param value - The value to check. + * @returns `true` if the value is a boolean, otherwise `false`. */ -export function isBoolean(value: any): value is boolean { +export function isBoolean(value: unknown): value is boolean { return typeof value === 'boolean'; } diff --git a/src/type-predicates/isBuffer.ts b/src/type-predicates/isBuffer.ts index 3eba3de..c0853ee 100644 --- a/src/type-predicates/isBuffer.ts +++ b/src/type-predicates/isBuffer.ts @@ -1,13 +1,27 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import { isNullOrUndefined } from './isNullOrUndefined.js'; /** - * Determines if a value is a Buffer. + * Checks if a value is a Buffer. + * + * @param value - The value to check. + * @returns `true` if the value is a Buffer, otherwise `false`. */ -export function isBuffer(value: any): value is Buffer { - if (value.constructor && typeof value.constructor.isBuffer === 'function') { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call - return value.constructor.isBuffer(value); +export function isBuffer(value: unknown): value is Buffer { + if (value === null || value === undefined || typeof value !== 'object') { + return false; } - return false; + const ctor = (value as Record).constructor; + + if (isNullOrUndefined(ctor) || typeof ctor !== 'function') { + return false; + } + + const isBufferFn = (ctor as unknown as Record).isBuffer; + + if (typeof isBufferFn !== 'function') { + return false; + } + + return (isBufferFn as (v: unknown) => boolean)(value); } diff --git a/src/type-predicates/isDate.ts b/src/type-predicates/isDate.ts index d4a184e..d82c3d6 100644 --- a/src/type-predicates/isDate.ts +++ b/src/type-predicates/isDate.ts @@ -1,6 +1,9 @@ /** - * Determines whether a given value is a date + * Checks if a value is a Date object. + * + * @param value - The value to check. + * @returns `true` if the value is a Date object, otherwise `false`. */ -export function isDate(value: any): value is Date { +export function isDate(value: unknown): value is Date { return value instanceof Date; } diff --git a/src/type-predicates/isEmpty.ts b/src/type-predicates/isEmpty.ts index b32776d..26ef940 100644 --- a/src/type-predicates/isEmpty.ts +++ b/src/type-predicates/isEmpty.ts @@ -1,35 +1,33 @@ import { isNullOrUndefined } from './isNullOrUndefined.js'; import { typeOf } from './typeOf.js'; -// eslint-disable-next-line @typescript-eslint/no-namespace -namespace Empty { - export type String = ''; - export type Object = Record; - export type Array = never[]; - export type _Set = Set; - export type _Map = Map; -} +type EmptyString = ''; +type EmptyObject = Record; +type EmptyArray = never[]; +type EmptySet = Set; +type EmptyMap = Map; -type Empty = Empty.Array | Empty.Object | Empty.String | Empty._Set | Empty._Map; +type Empty = EmptyArray | EmptyObject | EmptyString | EmptySet | EmptyMap; type EmptyResult = T extends string - ? Empty.String - : T extends any[] - ? Empty.Array + ? EmptyString + : T extends unknown[] + ? EmptyArray : T extends Set - ? Empty._Set + ? EmptySet : T extends Map - ? Empty._Map + ? EmptyMap : T extends object - ? Empty.Object + ? EmptyObject : never; /** - * Checks if a given value is empty + * Checks if a value is empty. A value is considered empty if it is `null`, `undefined`, an empty string (or whitespace-only string), an empty array, an empty object, an empty Set, or an empty Map. + * + * @param value - The value to check. + * @returns `true` if the value is empty, otherwise `false`. */ -export function isEmpty | Map | any[] | object | null | undefined>( - value: T | Empty | null | undefined -): value is EmptyResult | null | undefined { +export function isEmpty(value: T | Empty | null | undefined): value is EmptyResult | null | undefined { if (isNullOrUndefined(value)) { return true; } @@ -41,14 +39,15 @@ export function isEmpty | Map case 'map': { return (value as Set | Map).size === 0; } - } - - switch (typeof value) { + case 'array': { + return (value as unknown[]).length === 0; + } case 'object': { - return !Object.keys(value).length; + const obj = value as object; + return !Reflect.ownKeys(obj).some(k => Object.prototype.propertyIsEnumerable.call(obj, k)); } case 'string': { - return !value.trim(); + return !(value as string).trim(); } default: { return false; diff --git a/src/type-predicates/isEqual.ts b/src/type-predicates/isEqual.ts index 1d2ccfd..a0baf57 100644 --- a/src/type-predicates/isEqual.ts +++ b/src/type-predicates/isEqual.ts @@ -1,26 +1,24 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ - -import { Moment } from 'moment'; -import { isMoment } from './isMoment.js'; // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -export type IndividualEqualityType = null | undefined | boolean | number | string | Date | object | Function; +export type IndividualEqualityType = null | undefined | boolean | number | bigint | string | symbol | Date | object | Function; export type EqualityType = IndividualEqualityType | IndividualEqualityType[]; /** - * Performs a deep comparison between two values to determine if they are equivalent. - * - * **Note:** This method supports comparing nulls, undefineds, booleans, numbers, strings, Dates, objects, Functions, Arrays, RegExs, Maps, Sets, and Typed Arrays. - * - * Object objects are compared by their own, not inherited, enumerable properties. + * Performs a deep equality check between two values (primitives, arrays, objects, Dates/RegExps, ArrayBuffer views, and Maps/Sets). + * Note: Map keys and Set entries are matched using `SameValueZero` (i.e. reference equality for objects); Map values are compared recursively. * - * Functions and DOM nodes are compared by strict equality, i.e. ===. + * @param a - The first value to compare. + * @param b - The second value to compare. * - * The order of the array items must be the same for the arrays to be equal. + * @returns `true` if the values are deeply equal, otherwise `false`. */ -export function isEqual(a: any, b: any): boolean { +export function isEqual(a: unknown, b: unknown): boolean { + return _isEqual(a, b, new WeakMap>()); +} + +function _isEqual(a: any, b: any, visited: WeakMap>): boolean { if (a === b) { return true; } @@ -30,6 +28,19 @@ export function isEqual(a: any, b: any): boolean { return false; } + // Cycle detection: if this object pair has already been seen, treat as equal + const visitedB = visited.get(a as object); + + if (visitedB) { + if (visitedB.has(b as object)) { + return true; + } + + visitedB.add(b as object); + } else { + visited.set(a as object, new WeakSet([b as object])); + } + if (Array.isArray(a)) { const aLength = a.length; @@ -38,7 +49,7 @@ export function isEqual(a: any, b: any): boolean { } for (let i = 0; i < aLength; i++) { - if (!isEqual(a[i], b[i])) { + if (!_isEqual(a[i], b[i], visited)) { return false; } } @@ -58,7 +69,7 @@ export function isEqual(a: any, b: any): boolean { } for (const i of a.entries()) { - if (!isEqual(i[1], b.get(i[0]))) { + if (!_isEqual(i[1], b.get(i[0]), visited)) { return false; } } @@ -81,15 +92,16 @@ export function isEqual(a: any, b: any): boolean { } if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const length: number = (a as any).length; + const viewA = new Uint8Array(a.buffer, a.byteOffset, a.byteLength); + const viewB = new Uint8Array(b.buffer, b.byteOffset, b.byteLength); + const { length } = viewA; - if (length !== (b as any).length) { + if (length !== viewB.length) { return false; } for (let i = 0; i < length; i++) { - if (a[i] !== b[i]) { + if (viewA[i] !== viewB[i]) { return false; } } @@ -101,24 +113,20 @@ export function isEqual(a: any, b: any): boolean { return a.source === (b as RegExp).source && a.flags === (b as RegExp).flags; } - if (a.valueOf !== Object.prototype.valueOf) { - return a.valueOf() === b.valueOf(); + const aValueOf = a.valueOf as unknown; + const bValueOf = b.valueOf as unknown; + if (typeof aValueOf === 'function' && typeof bValueOf === 'function' && aValueOf !== Object.prototype.valueOf) { + return aValueOf.call(a) === bValueOf.call(b); } - if (a.toString !== Object.prototype.toString) { - return a.toString() === b.toString(); + const aToString = a.toString as unknown; + const bToString = b.toString as unknown; + if (typeof aToString === 'function' && typeof bToString === 'function' && aToString !== Object.prototype.toString) { + return aToString.call(a) === bToString.call(b); } - if (isMoment(a)) { - if (!isMoment(b)) { - return false; - } - - return (a as Moment).isSame(b as Moment); - } - - const keysA = Object.keys(a as object); - const keysB = Object.keys(b as object); + const keysA = Reflect.ownKeys(a as object).filter(k => Object.prototype.propertyIsEnumerable.call(a as object, k)); + const keysB = Reflect.ownKeys(b as object).filter(k => Object.prototype.propertyIsEnumerable.call(b as object, k)); const keysALength = keysA.length; if (keysALength !== keysB.length) { @@ -128,7 +136,11 @@ export function isEqual(a: any, b: any): boolean { for (let i = 0; i < keysALength; i++) { const key = keysA[i]; - if (!isEqual(a[key], b[key])) { + if (!Object.prototype.hasOwnProperty.call(b as object, key)) { + return false; + } + + if (!_isEqual(a[key], b[key], visited)) { return false; } } diff --git a/src/type-predicates/isError.ts b/src/type-predicates/isError.ts index f2bbc69..0847a40 100644 --- a/src/type-predicates/isError.ts +++ b/src/type-predicates/isError.ts @@ -1,6 +1,9 @@ /** - * Determines whether the given value is an Error. + * Checks if a value is an Error object. + * + * @param value - The value to check. + * @returns `true` if the value is an Error object, otherwise `false`. */ -export function isError(value: any): value is Error { +export function isError(value: unknown): value is Error { return value instanceof Error; } diff --git a/src/type-predicates/isGeneratorObject.ts b/src/type-predicates/isGeneratorObject.ts index ac098a8..4ad9bf7 100644 --- a/src/type-predicates/isGeneratorObject.ts +++ b/src/type-predicates/isGeneratorObject.ts @@ -1,7 +1,15 @@ /** - * Determines whether the given value is a generator object. + * Checks if a value is a generator object. + * + * @param value - The value to check. + * @returns `true` if the value is a generator object, otherwise `false`. */ -export function isGeneratorObject(value: any): boolean { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - return typeof value.throw === 'function' && typeof value.return === 'function' && typeof value.next === 'function'; +export function isGeneratorObject(value: unknown): boolean { + if (value === null || typeof value !== 'object') { + return false; + } + + const obj = value as Record; + + return typeof obj['throw'] === 'function' && typeof obj['return'] === 'function' && typeof obj['next'] === 'function'; } diff --git a/src/type-predicates/isMergeableObject.ts b/src/type-predicates/isMergeableObject.ts new file mode 100644 index 0000000..0235bee --- /dev/null +++ b/src/type-predicates/isMergeableObject.ts @@ -0,0 +1,26 @@ +/** + * Figuring out which properties of an object should be recursively iterated over when merging. + */ +export function isMergeableObject(value: unknown): boolean { + return isNonNullObject(value) && !isSpecial(value as object); +} + +function isNonNullObject(value: unknown): boolean { + return !!value && typeof value === 'object'; +} + +function isSpecial(value: object): boolean { + const stringValue = Object.prototype.toString.call(value); + + return ( + stringValue === '[object RegExp]' || + stringValue === '[object Date]' || + stringValue === '[object Map]' || + stringValue === '[object Set]' || + stringValue === '[object WeakMap]' || + stringValue === '[object WeakSet]' || + stringValue === '[object ArrayBuffer]' || + ArrayBuffer.isView(value) || + Array.isArray(value) + ); +} diff --git a/src/type-predicates/isNaNStrict.ts b/src/type-predicates/isNaNStrict.ts index ef87a17..920eb37 100644 --- a/src/type-predicates/isNaNStrict.ts +++ b/src/type-predicates/isNaNStrict.ts @@ -1,8 +1,11 @@ import { isNumber } from './isNumber.js'; /** - * Determines whether a given value is a NaN instance + * Checks if a value is `NaN` and of type `number`. + * + * @param value - The value to check. + * @returns `true` if the value is `NaN` and of type `number`, otherwise `false`. */ -export function isNaNStrict(value: any): boolean { - return isNumber(value) && isNaN(value); +export function isNaNStrict(value: unknown): boolean { + return isNumber(value) && Number.isNaN(value); } diff --git a/src/type-predicates/isNullOrUndefined.ts b/src/type-predicates/isNullOrUndefined.ts index c83fb28..2f2496e 100644 --- a/src/type-predicates/isNullOrUndefined.ts +++ b/src/type-predicates/isNullOrUndefined.ts @@ -1,6 +1,9 @@ /** - * Determines whether a given value is null or undefined + * Checks if a value is either `null` or `undefined`. + * + * @param value - The value to check. + * @returns `true` if the value is `null` or `undefined`, otherwise `false`. */ -export function isNullOrUndefined(value: any): value is null | undefined { +export function isNullOrUndefined(value: unknown): value is null | undefined { return value === null || value === undefined; } diff --git a/src/type-predicates/isNumber.ts b/src/type-predicates/isNumber.ts index c20e10b..6d01241 100644 --- a/src/type-predicates/isNumber.ts +++ b/src/type-predicates/isNumber.ts @@ -1,6 +1,9 @@ /** - * Determines whether a given value is a number + * Checks if a value is a number. + * + * @param value - The value to check. + * @returns `true` if the value is a number, otherwise `false`. */ -export function isNumber(value: any): value is number { +export function isNumber(value: unknown): value is number { return typeof value === 'number'; } diff --git a/src/type-predicates/isObject.ts b/src/type-predicates/isObject.ts index cc3f28f..ee4e988 100644 --- a/src/type-predicates/isObject.ts +++ b/src/type-predicates/isObject.ts @@ -2,8 +2,11 @@ import { isDate } from './isDate.js'; import { isNullOrUndefined } from './isNullOrUndefined.js'; /** - * Determines whether a given value is an object + * Checks if a value is an object (but not an array or date). + * + * @param value - The value to check. + * @returns `true` if the value is an object, otherwise `false`. */ -export function isObject(value: any): value is object { +export function isObject(value: unknown): value is object { return !isNullOrUndefined(value) && typeof value === 'object' && !Array.isArray(value) && !isDate(value); } diff --git a/src/type-predicates/isPlainObject.ts b/src/type-predicates/isPlainObject.ts index 566f7ad..35259a3 100644 --- a/src/type-predicates/isPlainObject.ts +++ b/src/type-predicates/isPlainObject.ts @@ -1,33 +1,35 @@ import { isObject } from './isObject.js'; /** - * Determines whether the given value is a plain object. + * Checks if a value is a plain object (i.e., an object whose prototype is `Object.prototype` or `null`, or that otherwise looks like an `Object` instance). + * + * @param value - The value to check. + * @returns `true` if the value is a plain object, otherwise `false`. */ -export function isPlainObject(value: any): value is object { +export function isPlainObject(value: unknown): value is object { if (!isObject(value)) { return false; } - const constructor = value.constructor; - - // If it has a modified constructor - if (constructor === undefined) { + if (Object.getPrototypeOf(value) === null) { return true; } - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const prototype = constructor.prototype; + const ctor = (value as Record).constructor; + + if (typeof ctor !== 'function') { + return false; + } + + const { prototype } = ctor as { prototype: unknown }; - // If it has a modified prototype if (!isObject(prototype)) { return false; } - // If the constructor does not have an Object-specific method if (!Object.prototype.hasOwnProperty.call(prototype, 'isPrototypeOf')) { return false; } - // Most likely a plain Object return true; } diff --git a/src/type-predicates/isRegExp.ts b/src/type-predicates/isRegExp.ts index ed80484..dafc6d9 100644 --- a/src/type-predicates/isRegExp.ts +++ b/src/type-predicates/isRegExp.ts @@ -1,6 +1,9 @@ /** - * Determines whether a given value is a RegExp + * Checks if a value is a RegExp object. + * + * @param value - The value to check. + * @returns `true` if the value is a RegExp object, otherwise `false`. */ -export function isRegExp(value: any): value is RegExp { +export function isRegExp(value: unknown): value is RegExp { return value instanceof RegExp; } diff --git a/src/type-predicates/isString.ts b/src/type-predicates/isString.ts index e3f0b23..711e39f 100644 --- a/src/type-predicates/isString.ts +++ b/src/type-predicates/isString.ts @@ -1,6 +1,9 @@ /** - * Determines whether a given value is a string + * Checks if the value is a string. + * + * @param value - The value to check. + * @returns `true` if the value is a string, otherwise `false`. */ -export function isString(value: any): value is string { +export function isString(value: unknown): value is string { return typeof value === 'string'; } diff --git a/src/type-predicates/typeOf.spec.ts b/src/type-predicates/typeOf.spec.ts index e700cf3..7b686e6 100644 --- a/src/type-predicates/typeOf.spec.ts +++ b/src/type-predicates/typeOf.spec.ts @@ -1,75 +1,70 @@ -import moment from 'moment'; import { typeOf, ValueType } from './typeOf'; describe(`typeOf`, () => { it(`undefined`, () => { - expect(typeOf(undefined)).toBe(ValueType.undefined); + expect(typeOf(undefined)).toBe(ValueType.Undefined); }); it(`null`, () => { - expect(typeOf(null)).toBe(ValueType.null); - }); - - it(`moment`, () => { - expect(typeOf(moment())).toBe(ValueType.moment); + expect(typeOf(null)).toBe(ValueType.Null); }); describe(`booleans`, () => { it(`true`, () => { - expect(typeOf(true)).toBe(ValueType.boolean); + expect(typeOf(true)).toBe(ValueType.Boolean); }); it(`false`, () => { - expect(typeOf(false)).toBe(ValueType.boolean); + expect(typeOf(false)).toBe(ValueType.Boolean); }); it(`new Boolean(true)`, () => { - expect(typeOf(new Boolean(true))).toBe(ValueType.boolean); + expect(typeOf(new Boolean(true))).toBe(ValueType.Boolean); }); it(`new Boolean(false)`, () => { - expect(typeOf(new Boolean(false))).toBe(ValueType.boolean); + expect(typeOf(new Boolean(false))).toBe(ValueType.Boolean); }); }); describe(`numbers`, () => { it(`0`, () => { - expect(typeOf(0)).toBe(ValueType.number); + expect(typeOf(0)).toBe(ValueType.Number); }); it(`1`, () => { - expect(typeOf(1)).toBe(ValueType.number); + expect(typeOf(1)).toBe(ValueType.Number); }); it(`new Number(0)`, () => { - expect(typeOf(new Number(0))).toBe(ValueType.number); + expect(typeOf(new Number(0))).toBe(ValueType.Number); }); it(`new Number(1)`, () => { - expect(typeOf(new Number(1))).toBe(ValueType.number); + expect(typeOf(new Number(1))).toBe(ValueType.Number); }); }); describe(`strings`, () => { it(`''`, () => { - expect(typeOf('')).toBe(ValueType.string); + expect(typeOf('')).toBe(ValueType.String); }); it(`'a'`, () => { - expect(typeOf('a')).toBe(ValueType.string); + expect(typeOf('a')).toBe(ValueType.String); }); it(`new String('')`, () => { - expect(typeOf(new String(''))).toBe(ValueType.string); + expect(typeOf(new String(''))).toBe(ValueType.String); }); it(`new String('a')`, () => { - expect(typeOf(new String('a'))).toBe(ValueType.string); + expect(typeOf(new String('a'))).toBe(ValueType.String); }); it(`template strings`, () => { const b = 'b'; - expect(typeOf(`a ${b} c`)).toBe(ValueType.string); + expect(typeOf(`a ${b} c`)).toBe(ValueType.String); }); }); @@ -77,95 +72,95 @@ describe(`typeOf`, () => { it('arguments', () => { (function () { // eslint-disable-next-line prefer-rest-params - expect(typeOf(arguments)).toBe(ValueType.arguments); + expect(typeOf(arguments)).toBe(ValueType.Arguments); })(); }); it(`buffers`, () => { - expect(typeOf(Buffer.from(''))).toBe(ValueType.buffer); + expect(typeOf(Buffer.from(''))).toBe(ValueType.Buffer); }); it(`class`, () => { class Test {} - expect(typeOf(new Test())).toBe(ValueType.object); + expect(typeOf(new Test())).toBe(ValueType.Object); }); it(`literal`, () => { - expect(typeOf({})).toBe(ValueType.object); + expect(typeOf({})).toBe(ValueType.Object); }); it(`created null`, () => { - expect(typeOf(Object.create(null))).toBe(ValueType.object); + expect(typeOf(Object.create(null))).toBe(ValueType.Object); }); it(`created object`, () => { - expect(typeOf(Object.create({}))).toBe(ValueType.object); + expect(typeOf(Object.create({}))).toBe(ValueType.Object); }); }); it(`dates`, () => { - expect(typeOf(new Date())).toBe(ValueType.date); + expect(typeOf(new Date())).toBe(ValueType.Date); }); describe(`arrays`, () => { it(`[]`, () => { - expect(typeOf([])).toBe(ValueType.array); + expect(typeOf([])).toBe(ValueType.Array); }); it(`[1]`, () => { - expect(typeOf([1])).toBe(ValueType.array); + expect(typeOf([1])).toBe(ValueType.Array); }); it(`new Array()`, () => { // eslint-disable-next-line @typescript-eslint/no-array-constructor - expect(typeOf(new Array())).toBe(ValueType.array); + expect(typeOf(new Array())).toBe(ValueType.Array); }); it(`new Array(1)`, () => { - expect(typeOf(new Array(1))).toBe(ValueType.array); + expect(typeOf(new Array(1))).toBe(ValueType.Array); }); }); describe(`RegExp`, () => { it(`/a/`, () => { - expect(typeOf(/a/)).toBe(ValueType.regexp); + expect(typeOf(/a/)).toBe(ValueType.RegExp); }); it(`new RegExp('a')`, () => { - expect(typeOf(new RegExp('a'))).toBe(ValueType.regexp); + expect(typeOf(new RegExp('a'))).toBe(ValueType.RegExp); }); }); describe(`functions`, () => { it(`() => {}`, () => { - expect(typeOf(() => {})).toBe(ValueType.function); + expect(typeOf(() => {})).toBe(ValueType.Function); }); it(`function () {}`, () => { - expect(typeOf(function () {})).toBe(ValueType.function); + expect(typeOf(function () {})).toBe(ValueType.Function); }); it(`function* () {}`, () => { - expect(typeOf(function* () {})).toBe(ValueType.generatorfunction); + expect(typeOf(function* () {})).toBe(ValueType.GeneratorFunction); }); it(`new Function()`, () => { // eslint-disable-next-line @typescript-eslint/no-implied-eval - expect(typeOf(new Function())).toBe(ValueType.function); + expect(typeOf(new Function())).toBe(ValueType.Function); }); }); it(`errors`, () => { - expect(typeOf(new Error())).toBe(ValueType.error); + expect(typeOf(new Error())).toBe(ValueType.Error); }); describe(`Promises`, () => { it(`resolved Promise`, () => { - expect(typeOf(Promise.resolve())).toBe(ValueType.promise); + expect(typeOf(Promise.resolve())).toBe(ValueType.Promise); }); it(`rejected Promise`, () => { - expect(typeOf(Promise.reject(new Error()).catch(() => {}))).toBe(ValueType.promise); + expect(typeOf(Promise.reject(new Error()).catch(() => {}))).toBe(ValueType.Promise); }); }); @@ -175,175 +170,175 @@ describe(`typeOf`, () => { yield 1; } - expect(typeOf(generator())).toBe(ValueType.generator); + expect(typeOf(generator())).toBe(ValueType.Generator); }); }); describe(`Map`, () => { it(`new Map()`, () => { - expect(typeOf(new Map())).toBe(ValueType.map); + expect(typeOf(new Map())).toBe(ValueType.Map); }); it(`Map.set`, () => { const map = new Map(); // eslint-disable-next-line @typescript-eslint/unbound-method - expect(typeOf(map.set)).toBe(ValueType.function); + expect(typeOf(map.set)).toBe(ValueType.Function); }); it(`Map.get`, () => { const map = new Map(); // eslint-disable-next-line @typescript-eslint/unbound-method - expect(typeOf(map.get)).toBe(ValueType.function); + expect(typeOf(map.get)).toBe(ValueType.Function); }); it(`Map.add`, () => { const map = new Map(); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(typeOf((map as any).add)).toBe(ValueType.undefined); + expect(typeOf((map as any).add)).toBe(ValueType.Undefined); }); }); describe(`WeakMap`, () => { it(`new WeakMap()`, () => { - expect(typeOf(new WeakMap())).toBe(ValueType.weakmap); + expect(typeOf(new WeakMap())).toBe(ValueType.WeakMap); }); it(`WeakMap.set`, () => { const map = new WeakMap(); // eslint-disable-next-line @typescript-eslint/unbound-method - expect(typeOf(map.set)).toBe(ValueType.function); + expect(typeOf(map.set)).toBe(ValueType.Function); }); it(`WeakMap.get`, () => { const map = new WeakMap(); // eslint-disable-next-line @typescript-eslint/unbound-method - expect(typeOf(map.get)).toBe(ValueType.function); + expect(typeOf(map.get)).toBe(ValueType.Function); }); it(`WeakMap.add`, () => { const map = new WeakMap(); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(typeOf((map as any).add)).toBe(ValueType.undefined); + expect(typeOf((map as any).add)).toBe(ValueType.Undefined); }); }); describe(`Set`, () => { it(`new Set()`, () => { - expect(typeOf(new Set())).toBe(ValueType.set); + expect(typeOf(new Set())).toBe(ValueType.Set); }); it(`Set.set`, () => { const set = new Set(); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(typeOf((set as any).set)).toBe(ValueType.undefined); + expect(typeOf((set as any).set)).toBe(ValueType.Undefined); }); it(`Set.get`, () => { const set = new Set(); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(typeOf((set as any).get)).toBe(ValueType.undefined); + expect(typeOf((set as any).get)).toBe(ValueType.Undefined); }); it(`Set.add`, () => { const set = new Set(); // eslint-disable-next-line @typescript-eslint/unbound-method - expect(typeOf(set.add)).toBe(ValueType.function); + expect(typeOf(set.add)).toBe(ValueType.Function); }); }); describe(`WeakSet`, () => { it(`new WeakSet()`, () => { - expect(typeOf(new WeakSet())).toBe(ValueType.weakset); + expect(typeOf(new WeakSet())).toBe(ValueType.WeakSet); }); it(`WeakSet.weakset`, () => { const weakset = new WeakSet(); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(typeOf((weakset as any).set)).toBe(ValueType.undefined); + expect(typeOf((weakset as any).set)).toBe(ValueType.Undefined); }); it(`WeakSet.get`, () => { const weakset = new WeakSet(); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(typeOf((weakset as any).get)).toBe(ValueType.undefined); + expect(typeOf((weakset as any).get)).toBe(ValueType.Undefined); }); it(`WeakSet.add`, () => { const weakset = new WeakSet(); // eslint-disable-next-line @typescript-eslint/unbound-method - expect(typeOf(weakset.add)).toBe(ValueType.function); + expect(typeOf(weakset.add)).toBe(ValueType.Function); }); }); it(`Set Iterator`, () => { - expect(typeOf(new Set().values())).toBe(ValueType.setiterator); + expect(typeOf(new Set().values())).toBe(ValueType.SetIterator); }); it(`Map Iterator`, () => { - expect(typeOf(new Map().values())).toBe(ValueType.mapiterator); + expect(typeOf(new Map().values())).toBe(ValueType.MapIterator); }); it(`Array Iterator`, () => { - expect(typeOf([].values())).toBe(ValueType.arrayiterator); + expect(typeOf([].values())).toBe(ValueType.ArrayIterator); }); it(`String Iterator`, () => { - expect(typeOf(''[Symbol.iterator]())).toBe(ValueType.stringiterator); + expect(typeOf(''[Symbol.iterator]())).toBe(ValueType.StringIterator); }); describe(`Symbol`, () => { it(`Symbol`, () => { - expect(typeOf(Symbol())).toBe(ValueType.symbol); + expect(typeOf(Symbol())).toBe(ValueType.Symbol); }); it(`Symbol prototype`, () => { - expect(typeOf(Symbol.prototype)).toBe(ValueType.symbol); + expect(typeOf(Symbol.prototype)).toBe(ValueType.Symbol); }); }); describe(`Typed Arrays`, () => { it(`Int8Array`, () => { - expect(typeOf(new Int8Array())).toBe(ValueType.int8array); + expect(typeOf(new Int8Array())).toBe(ValueType.Int8Array); }); it(`Uint8Array`, () => { - expect(typeOf(new Uint8Array())).toBe(ValueType.uint8array); + expect(typeOf(new Uint8Array())).toBe(ValueType.Uint8Array); }); it(`Uint8ClampedArray`, () => { - expect(typeOf(new Uint8ClampedArray())).toBe(ValueType.uint8clampedarray); + expect(typeOf(new Uint8ClampedArray())).toBe(ValueType.Uint8ClampedArray); }); it(`Int16Array`, () => { - expect(typeOf(new Int16Array())).toBe(ValueType.int16array); + expect(typeOf(new Int16Array())).toBe(ValueType.Int16Array); }); it(`Uint16Array`, () => { - expect(typeOf(new Uint16Array())).toBe(ValueType.uint16array); + expect(typeOf(new Uint16Array())).toBe(ValueType.Uint16Array); }); it(`Int32Array`, () => { - expect(typeOf(new Int32Array())).toBe(ValueType.int32array); + expect(typeOf(new Int32Array())).toBe(ValueType.Int32Array); }); it(`Uint32Array`, () => { - expect(typeOf(new Uint32Array())).toBe(ValueType.uint32array); + expect(typeOf(new Uint32Array())).toBe(ValueType.Uint32Array); }); it(`Float32Array`, () => { - expect(typeOf(new Float32Array())).toBe(ValueType.float32array); + expect(typeOf(new Float32Array())).toBe(ValueType.Float32Array); }); it(`Float64Array`, () => { - expect(typeOf(new Float64Array())).toBe(ValueType.float64array); + expect(typeOf(new Float64Array())).toBe(ValueType.Float64Array); }); it(`BigInt64Array`, () => { - expect(typeOf(new BigInt64Array())).toBe(ValueType.bigint64array); + expect(typeOf(new BigInt64Array())).toBe(ValueType.BigInt64Array); }); it(`BigUint64Array`, () => { - expect(typeOf(new BigUint64Array())).toBe(ValueType.biguint64array); + expect(typeOf(new BigUint64Array())).toBe(ValueType.BigUint64Array); }); }); }); diff --git a/src/type-predicates/typeOf.ts b/src/type-predicates/typeOf.ts index be999f2..1a6e130 100644 --- a/src/type-predicates/typeOf.ts +++ b/src/type-predicates/typeOf.ts @@ -3,198 +3,209 @@ import { isBuffer } from './isBuffer.js'; import { isDate } from './isDate.js'; import { isError } from './isError.js'; import { isGeneratorObject } from './isGeneratorObject.js'; -import { isMoment } from './isMoment.js'; import { isRegExp } from './isRegExp.js'; export enum ValueType { - undefined = 'undefined', - null = 'null', - boolean = 'boolean', - string = 'string', - number = 'number', - symbol = 'symbol', - function = 'function', - array = 'array', - buffer = 'buffer', - arguments = 'arguments', - date = 'date', - error = 'error', - regexp = 'regexp', - promise = 'promise', - weakmap = 'weakmap', - weakset = 'weakset', - map = 'map', - set = 'set', - int8array = 'int8array', - uint8array = 'uint8array', - uint8clampedarray = 'uint8clampedarray', - int16array = 'int16array', - uint16array = 'uint16array', - int32array = 'int32array', - uint32array = 'uint32array', - float32array = 'float32array', - float64array = 'float64array', - bigint64array = 'bigint64array', - biguint64array = 'biguint64array', - generator = 'generator', - object = 'object', - mapiterator = 'mapiterator', - setiterator = 'setiterator', - stringiterator = 'stringiterator', - arrayiterator = 'arrayiterator', - generatorfunction = 'generatorfunction', - moment = 'moment', + Undefined = 'undefined', + Null = 'null', + Boolean = 'boolean', + String = 'string', + Number = 'number', + Symbol = 'symbol', + Function = 'function', + Array = 'array', + Buffer = 'buffer', + Arguments = 'arguments', + Date = 'date', + Error = 'error', + RegExp = 'regexp', + Promise = 'promise', + WeakMap = 'weakmap', + WeakSet = 'weakset', + Map = 'map', + Set = 'set', + Int8Array = 'int8array', + Uint8Array = 'uint8array', + Uint8ClampedArray = 'uint8clampedarray', + Int16Array = 'int16array', + Uint16Array = 'uint16array', + Int32Array = 'int32array', + Uint32Array = 'uint32array', + Float32Array = 'float32array', + Float64Array = 'float64array', + BigInt64Array = 'bigint64array', + BigUint64Array = 'biguint64array', + Generator = 'generator', + Object = 'object', + MapIterator = 'mapiterator', + SetIterator = 'setiterator', + StringIterator = 'stringiterator', + ArrayIterator = 'arrayiterator', + GeneratorFunction = 'generatorfunction', } /** - * Determines the type of the given value + * Returns a normalised string describing the type of the given value. + * Extends `typeof` with finer-grained detection for arrays, buffers, + * `arguments` objects, dates, errors, regular expressions, typed arrays, + * Maps, Sets, WeakMaps, WeakSets, Promises, iterators, and generator objects. + * + * @param value - The value whose type is to be determined. + * + * @returns A `ValueType` enum member, or a lower-cased intrinsic tag string + * (derived from `Object.prototype.toString`) for types not covered by the enum. */ -export function typeOf(value: any): ValueType | string { +export function typeOf(value: unknown): ValueType | string { if (value === void 0) { - return ValueType.undefined; + return ValueType.Undefined; } if (value === null) { - return ValueType.null; - } - - if (isMoment(value)) { - return ValueType.moment; + return ValueType.Null; } let type: string = typeof value; if (type === 'boolean') { - return ValueType.boolean; + return ValueType.Boolean; } if (type === 'string') { - return ValueType.string; + return ValueType.String; } if (type === 'number') { - return ValueType.number; + return ValueType.Number; } if (type === 'symbol') { - return ValueType.symbol; + return ValueType.Symbol; } if (type === 'function') { - return isGeneratorFunction(value) ? ValueType.generatorfunction : ValueType.function; + return isGeneratorFunction(value) ? ValueType.GeneratorFunction : ValueType.Function; } if (Array.isArray(value)) { - return ValueType.array; + return ValueType.Array; } if (isBuffer(value)) { - return ValueType.buffer; + return ValueType.Buffer; } if (isArguments(value)) { - return ValueType.arguments; + return ValueType.Arguments; } if (isDate(value)) { - return ValueType.date; + return ValueType.Date; } if (isError(value)) { - return ValueType.error; + return ValueType.Error; } if (isRegExp(value)) { - return ValueType.regexp; + return ValueType.RegExp; } switch (constructorName(value)) { case 'Symbol': { - return ValueType.symbol; + return ValueType.Symbol; } case 'Promise': { - return ValueType.promise; + return ValueType.Promise; } case 'WeakMap': { - return ValueType.weakmap; + return ValueType.WeakMap; } case 'WeakSet': { - return ValueType.weakset; + return ValueType.WeakSet; } case 'Map': { - return ValueType.map; + return ValueType.Map; } case 'Set': { - return ValueType.set; + return ValueType.Set; } case 'Int8Array': { - return ValueType.int8array; + return ValueType.Int8Array; } case 'Uint8Array': { - return ValueType.uint8array; + return ValueType.Uint8Array; } case 'Uint8ClampedArray': { - return ValueType.uint8clampedarray; + return ValueType.Uint8ClampedArray; } case 'Int16Array': { - return ValueType.int16array; + return ValueType.Int16Array; } case 'Uint16Array': { - return ValueType.uint16array; + return ValueType.Uint16Array; } case 'Int32Array': { - return ValueType.int32array; + return ValueType.Int32Array; } case 'Uint32Array': { - return ValueType.uint32array; + return ValueType.Uint32Array; } case 'Float32Array': { - return ValueType.float32array; + return ValueType.Float32Array; } case 'Float64Array': { - return ValueType.float64array; + return ValueType.Float64Array; } case 'BigInt64Array': { - return ValueType.bigint64array; + return ValueType.BigInt64Array; } case 'BigUint64Array': { - return ValueType.biguint64array; + return ValueType.BigUint64Array; } } if (isGeneratorObject(value)) { - return ValueType.generator; + return ValueType.Generator; } - type = toString.call(value); + type = Object.prototype.toString.call(value); switch (type) { case '[object Object]': { - return ValueType.object; + return ValueType.Object; } case '[object Map Iterator]': { - return ValueType.mapiterator; + return ValueType.MapIterator; } case '[object Set Iterator]': { - return ValueType.setiterator; + return ValueType.SetIterator; } case '[object String Iterator]': { - return ValueType.stringiterator; + return ValueType.StringIterator; } case '[object Array Iterator]': { - return ValueType.arrayiterator; + return ValueType.ArrayIterator; } } return type.slice(8, -1).toLowerCase().replace(/\s/g, ''); } -function constructorName(value: any): string | null { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access - return typeof value.constructor === 'function' ? value.constructor.name : null; +function constructorName(value: unknown): string | null { + if (value === null || (typeof value !== 'object' && typeof value !== 'function')) { + return null; + } + + const ctor = (value as Record).constructor; + + if (typeof ctor !== 'function') { + return null; + } + + return (ctor as { name?: string }).name ?? null; } -function isGeneratorFunction(value: any): value is GeneratorFunction { +function isGeneratorFunction(value: unknown): value is GeneratorFunction { const name = constructorName(value); return name === 'GeneratorFunction' || name === 'GeneratorFunction.js'; From bf0d8300c0e640a50334427e05780d5077bc6a16 Mon Sep 17 00:00:00 2001 From: Ben Meyrick Date: Sat, 27 Jun 2026 11:37:37 +0100 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20improvements=20=E2=80=94=20kebab-ca?= =?UTF-8?q?se=20rename,=20new=20utils,=20perf=20&=20test=20overhaul?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING - ValueType enum members renamed to PascalCase throughout (ValueType.String, ValueType.Array, etc. — string values unchanged) - All src/ filenames converted from camelCase to kebab-case - Moment.js removed from typeOf, isEqual, and clone New utilities - sortBy: comparator factory for Array.prototype.sort with dot-notation, descending (-), case-insensitive (^), null-first, and mapping function support - pascalToKebab: converts PascalCase strings to kebab-case - isArray: type predicate returning value is unknown[] - isMergeableObject: corrected spelling (was isMergableObject) API improvements - typeOf: value parameter typed as unknown; all enum members PascalCase - isEqual: cycle detection (WeakMap); bigint and symbol added to IndividualEqualityType; Reflect.ownKeys → Object.keys (58% perf gain); lazy WeakMap creation eliminates allocation cost on primitive comparisons - isEmpty: Reflect.ownKeys for object empty-check; flat type aliases - isPlainObject: typed unknown; Object.getPrototypeOf(value) === null fast path - isMergeableObject: Map, Set, WeakMap, WeakSet, ArrayBuffer, ArrayBuffer.isView added to isSpecial() - merge: readonly MergeOptions; function overloads; prototype pollution protection (__proto__, prototype, constructor); symbol key support - difference: readonly params; returns array.slice() when values is empty - randomNumberBetweenRange: validates Number.isFinite, Number.isSafeInteger, range > 0 - clone: removed Moment.js; proto-first instanceof check order; for..in instead of Object.keys in object clone hot path; fast/slow path split eliminating instanceClone parameter threading overhead Testing - Migrate from Karma/Playwright browser mode to Vitest with jsdom environment - isVisible.spec.ts excluded (requires real browser layout engine) - is-equal.spec.ts rewritten as data-driven from isEqualTestDefinitions - Additional isEqual test cases added to definitions (null/false, bool/null, bool/undefined, bool/) Build - tsconfig.mjs.json and tsconfig.cjs.json: add rootDir (TypeScript 6 required) - tsconfig.cjs.json: override types to ["node"] to avoid vitest/vite resolution errors under node10 moduleResolution - tsconfig.json: add vitest/globals, benchmark.ts, eslint.config.mts, vitest.setup.ts to include - benchmark.ts: fix CJS imports (clone-deep, deepmerge, fast-deep-equal); switch library imports to pre-built dist/mjs README - Remove Moment.js references from clone and isEqual docs - Add bigint and symbol to IndividualEqualityType list - Fix benchmark link paths to renamed spec files - Fix capitaliseFirst/capitaliseLast heading levels and ToC nesting --- .github/workflows/tests.yml | 2 +- README.md | 62 +- benchmark.ts | 13 +- package-lock.json | 1368 +++++++++++++---- package.json | 15 +- src/array/to-array.ts | 3 + src/async/async-every.spec.ts | 15 + src/async/async-every.ts | 16 + src/async/async-filter.spec.ts | 9 + src/async/async-filter.ts | 12 + src/async/async-for-each.spec.ts | 19 + src/async/async-for-each.ts | 12 + src/async/async-some.spec.ts | 15 + src/async/async-some.ts | 16 + src/async/asyncEvery.spec.ts | 19 - src/async/asyncEvery.ts | 12 - src/async/asyncFilter.spec.ts | 11 - src/async/asyncFilter.ts | 8 - src/async/asyncForEach.spec.ts | 17 - src/async/asyncForEach.ts | 8 - src/async/asyncSome.spec.ts | 19 - src/async/asyncSome.ts | 12 - src/async/delay.spec.ts | 36 +- src/async/delay.ts | 9 +- src/async/index.ts | 8 +- src/clone/clone.spec.ts | 12 +- src/clone/clone.ts | 352 +++-- src/constants/index.ts | 2 +- src/constants/{isJsdom.ts => is-jsdom.ts} | 0 src/dates/comparators/compare-dates.spec.ts | 403 ++++- src/dates/comparators/compare-dates.ts | 8 +- src/dates/comparators/index.ts | 2 +- src/dates/comparators/is-same-date.spec.ts | 91 ++ .../{isSameDate.ts => is-same-date.ts} | 9 +- src/dates/comparators/isSameDate.spec.ts | 87 -- .../convert-units/convert-time-unit.spec.ts | 25 + src/dates/convert-units/convert-time-unit.ts | 17 + .../convert-units/convertTimeUnit.spec.ts | 24 - src/dates/convert-units/convertTimeUnit.ts | 10 - src/dates/convert-units/index.ts | 8 +- .../milliseconds-to-time-unit.spec.ts | 56 + .../milliseconds-to-time-unit.ts | 40 + src/dates/convert-units/msToUnit.spec.ts | 55 - src/dates/convert-units/msToUnit.ts | 36 - .../time-unit-to-milliseconds.spec.ts | 56 + ...itToMs.ts => time-unit-to-milliseconds.ts} | 14 +- .../{TimeUnit.ts => time-unit.enum.ts} | 2 +- src/dates/convert-units/unitToMs.spec.ts | 55 - ...ndOfDay.spec.ts => get-end-of-day.spec.ts} | 16 +- .../{getEndOfDay.ts => get-end-of-day.ts} | 5 +- ...OfHour.spec.ts => get-end-of-hour.spec.ts} | 16 +- .../{getEndOfHour.ts => get-end-of-hour.ts} | 5 +- ...nute.spec.ts => get-end-of-minute.spec.ts} | 16 +- ...getEndOfMinute.ts => get-end-of-minute.ts} | 5 +- ...Month.spec.ts => get-end-of-month.spec.ts} | 16 +- .../{getEndOfMonth.ts => get-end-of-month.ts} | 5 +- ...cond.spec.ts => get-end-of-second.spec.ts} | 16 +- ...getEndOfSecond.ts => get-end-of-second.ts} | 5 +- ...OfWeek.spec.ts => get-end-of-week.spec.ts} | 16 +- .../{getEndOfWeek.ts => get-end-of-week.ts} | 5 +- ...OfYear.spec.ts => get-end-of-year.spec.ts} | 16 +- .../{getEndOfYear.ts => get-end-of-year.ts} | 5 +- ...hNames.spec.ts => get-month-names.spec.ts} | 6 +- src/dates/getters/get-month-names.ts | 23 + ...OfDay.spec.ts => get-start-of-day.spec.ts} | 16 +- .../{getStartOfDay.ts => get-start-of-day.ts} | 5 +- ...Hour.spec.ts => get-start-of-hour.spec.ts} | 16 +- ...getStartOfHour.ts => get-start-of-hour.ts} | 5 +- ...te.spec.ts => get-start-of-minute.spec.ts} | 16 +- ...tartOfMinute.ts => get-start-of-minute.ts} | 5 +- ...nth.spec.ts => get-start-of-month.spec.ts} | 16 +- ...tStartOfMonth.ts => get-start-of-month.ts} | 5 +- ...nd.spec.ts => get-start-of-second.spec.ts} | 16 +- ...tartOfSecond.ts => get-start-of-second.ts} | 5 +- ...Week.spec.ts => get-start-of-week.spec.ts} | 16 +- ...getStartOfWeek.ts => get-start-of-week.ts} | 5 +- ...Year.spec.ts => get-start-of-year.spec.ts} | 16 +- ...getStartOfYear.ts => get-start-of-year.ts} | 5 +- src/dates/getters/get-today.spec.ts | 23 + .../getters/{getToday.ts => get-today.ts} | 4 +- src/dates/getters/get-week-of-year.spec.ts | 47 + src/dates/getters/get-week-of-year.ts | 21 + src/dates/getters/getMonthNames.ts | 21 - src/dates/getters/getToday.spec.ts | 14 - src/dates/getters/getWeekOfYear.spec.ts | 12 - src/dates/getters/getWeekOfYear.ts | 14 - src/dates/getters/index.ts | 34 +- src/dates/modifiers/add-to-date.spec.ts | 8 +- src/dates/modifiers/add-to-date.ts | 9 +- .../modifiers/date-modifiers-test-suites.ts | 5 +- src/dates/modifiers/modify-date.ts | 11 +- src/dates/modifiers/subtract-from-date.ts | 9 +- src/dates/setters/index.ts | 28 +- ...ndOfDay.spec.ts => set-end-of-day.spec.ts} | 2 +- .../{setEndOfDay.ts => set-end-of-day.ts} | 5 +- ...OfHour.spec.ts => set-end-of-hour.spec.ts} | 2 +- .../{setEndOfHour.ts => set-end-of-hour.ts} | 5 +- ...nute.spec.ts => set-end-of-minute.spec.ts} | 2 +- ...setEndOfMinute.ts => set-end-of-minute.ts} | 5 +- ...Month.spec.ts => set-end-of-month.spec.ts} | 2 +- .../{setEndOfMonth.ts => set-end-of-month.ts} | 6 +- ...cond.spec.ts => set-end-of-second.spec.ts} | 2 +- ...setEndOfSecond.ts => set-end-of-second.ts} | 3 + ...OfWeek.spec.ts => set-end-of-week.spec.ts} | 10 +- src/dates/setters/set-end-of-week.ts | 14 + ...OfYear.spec.ts => set-end-of-year.spec.ts} | 2 +- .../{setEndOfYear.ts => set-end-of-year.ts} | 5 +- ...OfDay.spec.ts => set-start-of-day.spec.ts} | 2 +- .../{setStartOfDay.ts => set-start-of-day.ts} | 5 +- ...Hour.spec.ts => set-start-of-hour.spec.ts} | 2 +- ...setStartOfHour.ts => set-start-of-hour.ts} | 5 +- ...te.spec.ts => set-start-of-minute.spec.ts} | 2 +- ...tartOfMinute.ts => set-start-of-minute.ts} | 5 +- ...nth.spec.ts => set-start-of-month.spec.ts} | 2 +- ...tStartOfMonth.ts => set-start-of-month.ts} | 5 +- ...nd.spec.ts => set-start-of-second.spec.ts} | 2 +- ...tartOfSecond.ts => set-start-of-second.ts} | 3 + ...Week.spec.ts => set-start-of-week.spec.ts} | 2 +- ...setStartOfWeek.ts => set-start-of-week.ts} | 5 +- ...Year.spec.ts => set-start-of-year.spec.ts} | 2 +- ...setStartOfYear.ts => set-start-of-year.ts} | 5 +- src/dates/setters/setEndOfWeek.ts | 12 - ...ncestors.spec.ts => get-ancestors.spec.ts} | 0 src/dom/{getAncestors.ts => get-ancestors.ts} | 9 +- ...s => get-computed-style-as-number.spec.ts} | 4 +- ...ber.ts => get-computed-style-as-number.ts} | 6 +- ...ght.spec.ts => get-element-height.spec.ts} | 4 +- ...ElementHeight.ts => get-element-height.ts} | 5 +- ...idth.spec.ts => get-element-width.spec.ts} | 4 +- ...etElementWidth.ts => get-element-width.ts} | 5 +- ....spec.ts => get-non-inline-parent.spec.ts} | 2 +- ...lineParent.ts => get-non-inline-parent.ts} | 7 +- ....spec.ts => get-positioned-parent.spec.ts} | 2 +- ...onedParent.ts => get-positioned-parent.ts} | 3 + ...rent.spec.ts => get-scroll-parent.spec.ts} | 34 +- src/dom/get-scroll-parent.ts | 35 + src/dom/getScrollParent.ts | 24 - src/dom/index.ts | 18 +- ...line.spec.ts => is-display-inline.spec.ts} | 2 +- ...sDisplayInline.ts => is-display-inline.ts} | 3 + .../{isVisible.spec.ts => is-visible.spec.ts} | 2 +- src/dom/{isVisible.ts => is-visible.ts} | 3 + src/format-time/format-time-options.ts | 14 +- src/format-time/format-time.ts | 33 +- src/freeze/freeze.ts | 4 + src/index.ts | 2 +- .../insert-at-index.spec.ts} | 2 +- src/insert-at-index/insert-at-index.ts | 11 + src/insertAtIndex/insertAtIndex.ts | 6 - src/math/average.ts | 6 + src/math/index.ts | 2 +- ...ts => random-number-between-range.spec.ts} | 2 +- ...ange.ts => random-number-between-range.ts} | 1 + src/math/sum.ts | 5 +- ...spec.ts => merge-test-definitions.spec.ts} | 0 src/merge/merge.spec.ts | 2 +- src/merge/merge.ts | 2 +- src/sorting/sort-by.ts | 2 +- src/strings/capitalise-first.ts | 2 + src/strings/capitalise-last.ts | 2 + src/strings/capitalise.ts | 4 + src/strings/kebab-to-pascal.ts | 3 + ...Element.spec.ts => create-element.spec.ts} | 0 src/type-predicates/index.ts | 39 +- ...Arguments.spec.ts => is-arguments.spec.ts} | 15 +- .../{isArguments.ts => is-arguments.ts} | 0 .../{isArray.spec.ts => is-array.spec.ts} | 2 +- .../{isArray.ts => is-array.ts} | 0 .../{isBoolean.spec.ts => is-boolean.spec.ts} | 8 +- .../{isBoolean.ts => is-boolean.ts} | 0 .../{isBuffer.spec.ts => is-buffer.spec.ts} | 2 +- .../{isBuffer.ts => is-buffer.ts} | 2 +- .../{isDate.spec.ts => is-date.spec.ts} | 8 +- src/type-predicates/{isDate.ts => is-date.ts} | 0 .../{isEmpty.spec.ts => is-empty.spec.ts} | 27 +- .../{isEmpty.ts => is-empty.ts} | 4 +- ...c.ts => is-equal-test-definitions.spec.ts} | 60 +- src/type-predicates/is-equal.spec.ts | 70 + .../{isEqual.ts => is-equal.ts} | 41 +- .../{isError.spec.ts => is-error.spec.ts} | 2 +- .../{isError.ts => is-error.ts} | 0 ...ct.spec.ts => is-generator-object.spec.ts} | 2 +- ...eratorObject.ts => is-generator-object.ts} | 0 .../is-mergeable-object.spec.ts | 34 + ...geableObject.ts => is-mergeable-object.ts} | 0 ...aNStrict.spec.ts => is-nan-strict.spec.ts} | 6 +- .../{isNaNStrict.ts => is-nan-strict.ts} | 2 +- .../is-null-or-undefined.spec.ts | 59 + ...OrUndefined.ts => is-null-or-undefined.ts} | 0 .../{isNumber.spec.ts => is-number.spec.ts} | 6 +- .../{isNumber.ts => is-number.ts} | 0 .../{isObject.spec.ts => is-object.spec.ts} | 6 +- .../{isObject.ts => is-object.ts} | 4 +- ...Object.spec.ts => is-plain-object.spec.ts} | 13 +- .../{isPlainObject.ts => is-plain-object.ts} | 2 +- ...{isReactElement.ts => is-react-element.ts} | 0 .../{isRegExp.spec.ts => is-regexp.spec.ts} | 2 +- .../{isRegExp.ts => is-regexp.ts} | 0 .../{isString.spec.ts => is-string.spec.ts} | 6 +- .../{isString.ts => is-string.ts} | 0 src/type-predicates/isEqual.spec.ts | 39 - src/type-predicates/isMergableObject.ts | 18 - src/type-predicates/isMoment.ts | 6 - src/type-predicates/isNullOrUndefined.spec.ts | 37 - .../{typeOf.spec.ts => type-of.spec.ts} | 11 +- src/type-predicates/{typeOf.ts => type-of.ts} | 12 +- src/types/{Dictionary.ts => dictionary.ts} | 0 src/types/index.ts | 2 +- tsconfig.cjs.json | 10 +- tsconfig.json | 4 +- tsconfig.mjs.json | 7 +- tsconfig.spec.json | 2 +- vitest.config.mts | 24 +- 213 files changed, 3125 insertions(+), 1553 deletions(-) create mode 100644 src/async/async-every.spec.ts create mode 100644 src/async/async-every.ts create mode 100644 src/async/async-filter.spec.ts create mode 100644 src/async/async-filter.ts create mode 100644 src/async/async-for-each.spec.ts create mode 100644 src/async/async-for-each.ts create mode 100644 src/async/async-some.spec.ts create mode 100644 src/async/async-some.ts delete mode 100644 src/async/asyncEvery.spec.ts delete mode 100644 src/async/asyncEvery.ts delete mode 100644 src/async/asyncFilter.spec.ts delete mode 100644 src/async/asyncFilter.ts delete mode 100644 src/async/asyncForEach.spec.ts delete mode 100644 src/async/asyncForEach.ts delete mode 100644 src/async/asyncSome.spec.ts delete mode 100644 src/async/asyncSome.ts rename src/constants/{isJsdom.ts => is-jsdom.ts} (100%) create mode 100644 src/dates/comparators/is-same-date.spec.ts rename src/dates/comparators/{isSameDate.ts => is-same-date.ts} (59%) delete mode 100644 src/dates/comparators/isSameDate.spec.ts create mode 100644 src/dates/convert-units/convert-time-unit.spec.ts create mode 100644 src/dates/convert-units/convert-time-unit.ts delete mode 100644 src/dates/convert-units/convertTimeUnit.spec.ts delete mode 100644 src/dates/convert-units/convertTimeUnit.ts create mode 100644 src/dates/convert-units/milliseconds-to-time-unit.spec.ts create mode 100644 src/dates/convert-units/milliseconds-to-time-unit.ts delete mode 100644 src/dates/convert-units/msToUnit.spec.ts delete mode 100644 src/dates/convert-units/msToUnit.ts create mode 100644 src/dates/convert-units/time-unit-to-milliseconds.spec.ts rename src/dates/convert-units/{unitToMs.ts => time-unit-to-milliseconds.ts} (57%) rename src/dates/convert-units/{TimeUnit.ts => time-unit.enum.ts} (100%) delete mode 100644 src/dates/convert-units/unitToMs.spec.ts rename src/dates/getters/{getEndOfDay.spec.ts => get-end-of-day.spec.ts} (55%) rename src/dates/getters/{getEndOfDay.ts => get-end-of-day.ts} (51%) rename src/dates/getters/{getEndOfHour.spec.ts => get-end-of-hour.spec.ts} (54%) rename src/dates/getters/{getEndOfHour.ts => get-end-of-hour.ts} (51%) rename src/dates/getters/{getEndOfMinute.spec.ts => get-end-of-minute.spec.ts} (53%) rename src/dates/getters/{getEndOfMinute.ts => get-end-of-minute.ts} (51%) rename src/dates/getters/{getEndOfMonth.spec.ts => get-end-of-month.spec.ts} (56%) rename src/dates/getters/{getEndOfMonth.ts => get-end-of-month.ts} (51%) rename src/dates/getters/{getEndOfSecond.spec.ts => get-end-of-second.spec.ts} (52%) rename src/dates/getters/{getEndOfSecond.ts => get-end-of-second.ts} (51%) rename src/dates/getters/{getEndOfWeek.spec.ts => get-end-of-week.spec.ts} (69%) rename src/dates/getters/{getEndOfWeek.ts => get-end-of-week.ts} (51%) rename src/dates/getters/{getEndOfYear.spec.ts => get-end-of-year.spec.ts} (57%) rename src/dates/getters/{getEndOfYear.ts => get-end-of-year.ts} (51%) rename src/dates/getters/{getMonthNames.spec.ts => get-month-names.spec.ts} (82%) create mode 100644 src/dates/getters/get-month-names.ts rename src/dates/getters/{getStartOfDay.spec.ts => get-start-of-day.spec.ts} (54%) rename src/dates/getters/{getStartOfDay.ts => get-start-of-day.ts} (51%) rename src/dates/getters/{getStartOfHour.spec.ts => get-start-of-hour.spec.ts} (53%) rename src/dates/getters/{getStartOfHour.ts => get-start-of-hour.ts} (51%) rename src/dates/getters/{getStartOfMinute.spec.ts => get-start-of-minute.spec.ts} (52%) rename src/dates/getters/{getStartOfMinute.ts => get-start-of-minute.ts} (51%) rename src/dates/getters/{getStartOfMonth.spec.ts => get-start-of-month.spec.ts} (54%) rename src/dates/getters/{getStartOfMonth.ts => get-start-of-month.ts} (51%) rename src/dates/getters/{getStartOfSecond.spec.ts => get-start-of-second.spec.ts} (51%) rename src/dates/getters/{getStartOfSecond.ts => get-start-of-second.ts} (51%) rename src/dates/getters/{getStartOfWeek.spec.ts => get-start-of-week.spec.ts} (67%) rename src/dates/getters/{getStartOfWeek.ts => get-start-of-week.ts} (51%) rename src/dates/getters/{getStartOfYear.spec.ts => get-start-of-year.spec.ts} (54%) rename src/dates/getters/{getStartOfYear.ts => get-start-of-year.ts} (51%) create mode 100644 src/dates/getters/get-today.spec.ts rename src/dates/getters/{getToday.ts => get-today.ts} (53%) create mode 100644 src/dates/getters/get-week-of-year.spec.ts create mode 100644 src/dates/getters/get-week-of-year.ts delete mode 100644 src/dates/getters/getMonthNames.ts delete mode 100644 src/dates/getters/getToday.spec.ts delete mode 100644 src/dates/getters/getWeekOfYear.spec.ts delete mode 100644 src/dates/getters/getWeekOfYear.ts rename src/dates/setters/{setEndOfDay.spec.ts => set-end-of-day.spec.ts} (85%) rename src/dates/setters/{setEndOfDay.ts => set-end-of-day.ts} (52%) rename src/dates/setters/{setEndOfHour.spec.ts => set-end-of-hour.spec.ts} (84%) rename src/dates/setters/{setEndOfHour.ts => set-end-of-hour.ts} (52%) rename src/dates/setters/{setEndOfMinute.spec.ts => set-end-of-minute.spec.ts} (83%) rename src/dates/setters/{setEndOfMinute.ts => set-end-of-minute.ts} (52%) rename src/dates/setters/{setEndOfMonth.spec.ts => set-end-of-month.spec.ts} (86%) rename src/dates/setters/{setEndOfMonth.ts => set-end-of-month.ts} (52%) rename src/dates/setters/{setEndOfSecond.spec.ts => set-end-of-second.spec.ts} (82%) rename src/dates/setters/{setEndOfSecond.ts => set-end-of-second.ts} (60%) rename src/dates/setters/{setEndOfWeek.spec.ts => set-end-of-week.spec.ts} (65%) create mode 100644 src/dates/setters/set-end-of-week.ts rename src/dates/setters/{setEndOfYear.spec.ts => set-end-of-year.spec.ts} (86%) rename src/dates/setters/{setEndOfYear.ts => set-end-of-year.ts} (55%) rename src/dates/setters/{setStartOfDay.spec.ts => set-start-of-day.spec.ts} (83%) rename src/dates/setters/{setStartOfDay.ts => set-start-of-day.ts} (52%) rename src/dates/setters/{setStartOfHour.spec.ts => set-start-of-hour.spec.ts} (83%) rename src/dates/setters/{setStartOfHour.ts => set-start-of-hour.ts} (52%) rename src/dates/setters/{setStartOfMinute.spec.ts => set-start-of-minute.spec.ts} (82%) rename src/dates/setters/{setStartOfMinute.ts => set-start-of-minute.ts} (52%) rename src/dates/setters/{setStartOfMonth.spec.ts => set-start-of-month.spec.ts} (83%) rename src/dates/setters/{setStartOfMonth.ts => set-start-of-month.ts} (52%) rename src/dates/setters/{setStartOfSecond.spec.ts => set-start-of-second.spec.ts} (81%) rename src/dates/setters/{setStartOfSecond.ts => set-start-of-second.ts} (59%) rename src/dates/setters/{setStartOfWeek.spec.ts => set-start-of-week.spec.ts} (90%) rename src/dates/setters/{setStartOfWeek.ts => set-start-of-week.ts} (62%) rename src/dates/setters/{setStartOfYear.spec.ts => set-start-of-year.spec.ts} (84%) rename src/dates/setters/{setStartOfYear.ts => set-start-of-year.ts} (52%) delete mode 100644 src/dates/setters/setEndOfWeek.ts rename src/dom/{getAncestors.spec.ts => get-ancestors.spec.ts} (100%) rename src/dom/{getAncestors.ts => get-ancestors.ts} (54%) rename src/dom/{getComputedStyleAsNumber.spec.ts => get-computed-style-as-number.spec.ts} (85%) rename src/dom/{getComputedStyleAsNumber.ts => get-computed-style-as-number.ts} (58%) rename src/dom/{getElementHeight.spec.ts => get-element-height.spec.ts} (64%) rename src/dom/{getElementHeight.ts => get-element-height.ts} (66%) rename src/dom/{getElementWidth.spec.ts => get-element-width.spec.ts} (64%) rename src/dom/{getElementWidth.ts => get-element-width.ts} (66%) rename src/dom/{getNonInlineParent.spec.ts => get-non-inline-parent.spec.ts} (94%) rename src/dom/{getNonInlineParent.ts => get-non-inline-parent.ts} (55%) rename src/dom/{getPositionedParent.spec.ts => get-positioned-parent.spec.ts} (96%) rename src/dom/{getPositionedParent.ts => get-positioned-parent.ts} (70%) rename src/dom/{getScrollParent.spec.ts => get-scroll-parent.spec.ts} (70%) create mode 100644 src/dom/get-scroll-parent.ts delete mode 100644 src/dom/getScrollParent.ts rename src/dom/{isDisplayInline.spec.ts => is-display-inline.spec.ts} (92%) rename src/dom/{isDisplayInline.ts => is-display-inline.ts} (72%) rename src/dom/{isVisible.spec.ts => is-visible.spec.ts} (99%) rename src/dom/{isVisible.ts => is-visible.ts} (94%) rename src/{insertAtIndex/insertAtIndex.spec.ts => insert-at-index/insert-at-index.spec.ts} (77%) create mode 100644 src/insert-at-index/insert-at-index.ts delete mode 100644 src/insertAtIndex/insertAtIndex.ts rename src/math/{randomNumberBetweenRange.spec.ts => random-number-between-range.spec.ts} (95%) rename src/math/{randomNumberBetweenRange.ts => random-number-between-range.ts} (99%) rename src/merge/{mergeTestDefinitions.spec.ts => merge-test-definitions.spec.ts} (100%) rename src/test-helpers/{createElement.spec.ts => create-element.spec.ts} (100%) rename src/type-predicates/{isArguments.spec.ts => is-arguments.spec.ts} (66%) rename src/type-predicates/{isArguments.ts => is-arguments.ts} (100%) rename src/type-predicates/{isArray.spec.ts => is-array.spec.ts} (87%) rename src/type-predicates/{isArray.ts => is-array.ts} (100%) rename src/type-predicates/{isBoolean.spec.ts => is-boolean.spec.ts} (94%) rename src/type-predicates/{isBoolean.ts => is-boolean.ts} (100%) rename src/type-predicates/{isBuffer.spec.ts => is-buffer.spec.ts} (87%) rename src/type-predicates/{isBuffer.ts => is-buffer.ts} (91%) rename src/type-predicates/{isDate.spec.ts => is-date.spec.ts} (95%) rename src/type-predicates/{isDate.ts => is-date.ts} (100%) rename src/type-predicates/{isEmpty.spec.ts => is-empty.spec.ts} (76%) rename src/type-predicates/{isEmpty.ts => is-empty.ts} (93%) rename src/type-predicates/{isEqualTestDefinitions.spec.ts => is-equal-test-definitions.spec.ts} (96%) create mode 100644 src/type-predicates/is-equal.spec.ts rename src/type-predicates/{isEqual.ts => is-equal.ts} (71%) rename src/type-predicates/{isError.spec.ts => is-error.spec.ts} (87%) rename src/type-predicates/{isError.ts => is-error.ts} (100%) rename src/type-predicates/{isGeneratorObject.spec.ts => is-generator-object.spec.ts} (85%) rename src/type-predicates/{isGeneratorObject.ts => is-generator-object.ts} (100%) create mode 100644 src/type-predicates/is-mergeable-object.spec.ts rename src/type-predicates/{isMergeableObject.ts => is-mergeable-object.ts} (100%) rename src/type-predicates/{isNaNStrict.spec.ts => is-nan-strict.spec.ts} (95%) rename src/type-predicates/{isNaNStrict.ts => is-nan-strict.ts} (87%) create mode 100644 src/type-predicates/is-null-or-undefined.spec.ts rename src/type-predicates/{isNullOrUndefined.ts => is-null-or-undefined.ts} (100%) rename src/type-predicates/{isNumber.spec.ts => is-number.spec.ts} (94%) rename src/type-predicates/{isNumber.ts => is-number.ts} (100%) rename src/type-predicates/{isObject.spec.ts => is-object.spec.ts} (94%) rename src/type-predicates/{isObject.ts => is-object.ts} (77%) rename src/type-predicates/{isPlainObject.spec.ts => is-plain-object.spec.ts} (59%) rename src/type-predicates/{isPlainObject.ts => is-plain-object.ts} (94%) rename src/type-predicates/{isReactElement.ts => is-react-element.ts} (100%) rename src/type-predicates/{isRegExp.spec.ts => is-regexp.spec.ts} (93%) rename src/type-predicates/{isRegExp.ts => is-regexp.ts} (100%) rename src/type-predicates/{isString.spec.ts => is-string.spec.ts} (94%) rename src/type-predicates/{isString.ts => is-string.ts} (100%) delete mode 100644 src/type-predicates/isEqual.spec.ts delete mode 100644 src/type-predicates/isMergableObject.ts delete mode 100644 src/type-predicates/isMoment.ts delete mode 100644 src/type-predicates/isNullOrUndefined.spec.ts rename src/type-predicates/{typeOf.spec.ts => type-of.spec.ts} (97%) rename src/type-predicates/{typeOf.ts => type-of.ts} (94%) rename src/types/{Dictionary.ts => dictionary.ts} (100%) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 974bda2..8dd4e7b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,7 +36,7 @@ jobs: run: npm run lint - name: Run the tests 🧪 - run: npm run test-single + run: npm run test - name: Upload coverage to Codecov 📈 uses: codecov/codecov-action@v3 diff --git a/README.md b/README.md index 7b2fc6b..0688b86 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,6 @@ A collection of useful utility functions with associated TypeScript types. - [isObject](#isobject) - [isString](#isstring) - [isEqual](#isequal) - - [isEqual performance comparison](#isequal-performance-comparison) - [EqualityType](#equalitytype) - [IndividualEqualityType](#individualequalitytype) - [isRegExp](#isregexp) @@ -42,25 +41,23 @@ A collection of useful utility functions with associated TypeScript types. - [typeOf](#typeof) - [clone](#clone) - [instanceClone](#instanceclone) - - [clone performance comparison](#clone-performance-comparison) - [freeze](#freeze) - [merge](#merge) - [merge options](#merge-options) - [arrayMerge](#arraymerge) - - [isMergeableObject](#ismergeableobject) + - [isMergeableObject](#ismergeableobject-1) - [customMerge](#custommerge) - - [Merge performance comparison](#merge-performance-comparison) - - [Sorting](#sorting) - - [sortBy](#sortby) - - [SortMappingFunction](#sortmappingfunction) - [Strings](#strings) - [capitalise](#capitalise) - [CapitaliseOptions](#capitaliseoptions) - - [CapitaliseFirst](#capitalisefirst) - - [CapitaliseLast](#capitaliselast) + - [capitaliseFirst](#capitalisefirst) + - [capitaliseLast](#capitaliselast) - [kebabToPascal](#kebabtopascal) - [pascalToKebab](#pascaltokebab) - [difference](#difference) + - [Sorting](#sorting) + - [sortBy](#sortby) + - [SortMappingFunction](#sortmappingfunction) - [formatTime](#formattime) - [FormatTimeOptions](#formattimeoptions) - [insertAtIndex](#insertatindex) @@ -371,7 +368,7 @@ if (isString(value)) { Performs a deep comparison between two values to determine if they are equivalent. -**Note:** This method supports comparing nulls, undefineds, booleans, numbers, strings, Dates, objects, Functions, Arrays, RegExs, Maps, Sets, Typed Arrays, and [Moments](https://momentjs.com). +**Note:** This method supports comparing nulls, undefineds, booleans, numbers, bigints, symbols, strings, Dates, objects, Functions, Arrays, RegExps, Maps, Sets, and Typed Arrays. Object objects are compared by their own, not inherited, enumerable properties. @@ -401,22 +398,6 @@ if (isEqual(a, b)) { } ``` -##### isEqual performance comparison - -The following benchmarks go through the [isEqual test suite](tests/isEqualTestDefinitions.spec.ts) and were run on a 2023 Macbook Pro with a M2 Pro chip and 32GB of RAM. - -| Package | Operations per second | -| ------------------------ | --------------------- | -| @qntm-code/utils.isEqual | 218781 | -| fast-deep-equal | 192329 | -| underscore.isEqual | 65518 | -| util.isDeepStrictEqual | 39740 | -| lodash.isEqual | 18842 | -| ramda.equals | 10231 | -| assert.deepStrictEqual | 215 | - -To run the benchmarks yourself, clone the repo, install the dependencies and run `yarn benchmark`. - ##### EqualityType An EqualityType can be an [IndividualEqualityType](#individualequalitytype) or an array of mixed [IndividualEqualityTypes](#individualequalitytype) @@ -429,7 +410,9 @@ The equality types allowed are: - `undefined` - `boolean` - `number` +- `bigint` - `string` +- `symbol` - `Date` - `object` - `Function` @@ -696,7 +679,7 @@ if (typeOf(value) === ValueType.String) { ### clone -Recursively (deep) clones native types, like Object, Array, RegExp, Date, Map, Set, Symbol, Error, [moment](https://momentjs.com/) as well as primitives. +Recursively (deep) clones native types, like Object, Array, RegExp, Date, Map, Set, Symbol, and Error as well as primitives. Method arguments: @@ -722,16 +705,6 @@ const value: number[] = [1, 2, 3]; const clonedValues = clone(value); ``` -#### clone performance comparison - -The following benchmarks were run on a 2023 Macbook Pro with a M2 Pro chip and 32GB of RAM. - -| Package | Operations per second | -| ---------------------- | --------------------- | -| @qntm-code/utils.clone | 127338 | -| clone-deep | 115475 | -| lodash.cloneDeep | 73027 | - --- ### freeze @@ -919,15 +892,6 @@ result.name; // => 'Alex and Tony' result.pets; // => ['Cat', 'Parrot', 'Dog'] ``` -#### Merge performance comparison - -The following benchmarks go through the [merge test suite](tests/mergeTestDefinitions.spec.ts) and were run on a 2023 Macbook Pro with a M2 Pro chip and 32GB of RAM. - -| Package | Operations per second | -| ---------------------- | --------------------- | -| @qntm-code/utils.merge | 28332 | -| deepmerge | 23497 | - --- ### Strings @@ -968,7 +932,7 @@ const capitalised = capitalise(value, { start: 0, end: 5 }); --- -### CapitaliseFirst +#### capitaliseFirst Capitalises the first n characters of a string. @@ -995,7 +959,7 @@ const capitalised = capitaliseFirst(value, 2); --- -#### CapitaliseLast +#### capitaliseLast Capitalises the last n characters of a string. @@ -1774,8 +1738,6 @@ const newDate = addToDate(date, 24, TimeUnit.Hours).getHours(); // 7 Returns a new date object with a given amount of a given [TimeUnit](#timeunit) subracted from it. -This is exactly the same as moment#add, only instead of [addToDate](#addtodate), it subtracts time. - | Parameter | Type | Description | | --------- | --------------------- | ------------------------- | | date | Date | The date to subtract from | diff --git a/benchmark.ts b/benchmark.ts index 6939897..684bb54 100644 --- a/benchmark.ts +++ b/benchmark.ts @@ -1,21 +1,20 @@ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { deepStrictEqual } from 'assert'; import { Suite } from 'benchmark'; -import * as cloneDeep from 'clone-deep'; -import * as deepmerge from 'deepmerge'; -import * as fastDeepEqual from 'fast-deep-equal/es6'; +import cloneDeep from 'clone-deep'; +import deepmerge from 'deepmerge'; +import fastDeepEqual from 'fast-deep-equal/es6'; import * as lodash from 'lodash'; import { markdownTable } from 'markdown-table'; import * as ramda from 'ramda'; import * as _ from 'underscore'; import { isDeepStrictEqual } from 'util'; import { clone, isEqual, merge } from './src'; -import { mergeTests } from './src/merge/mergeTestDefinitions.spec'; -import { isEqualTests } from './src/type-predicates/isEqualTestDefinitions.spec'; +import { mergeTests } from './src/merge/merge-test-definitions.spec'; +import { isEqualTests } from './src/type-predicates/is-equal-test-definitions.spec'; interface Benchmark { name: string; @@ -102,7 +101,7 @@ const benchmarks: Benchmark[] = [ try { deepStrictEqual(a, b); return true; - } catch (e) { + } catch (_error) { return false; } }, diff --git a/package-lock.json b/package-lock.json index 0b6f211..0ca9e1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,13 +14,14 @@ "devDependencies": { "@eslint/js": "^10.0.1", "@types/benchmark": "^2.1.2", + "@types/clone-deep": "^4.0.4", + "@types/jsdom": "^28.0.3", "@types/node": "^26.0.1", "@typescript-eslint/eslint-plugin": "^8.62.0", "@typescript-eslint/parser": "^8.62.0", - "@vitest/browser-playwright": "^4.1.9", "@vitest/coverage-istanbul": "^4.1.9", + "@vitest/ui": "^4.1.9", "benchmark": "^2.1.4", - "buffer": "^6.0.3", "clone-deep": "^4.0.1", "codecov": "^3.7.2", "cpy-cli": "^7.0.0", @@ -32,12 +33,12 @@ "fast-deep-equal": "^3.1.3", "husky": "^9.1.7", "jiti": "^2.7.0", + "jsdom": "^29.1.1", "lint-staged": "^17.0.8", "lodash": "^4.18.1", "markdown-table": "^3.0.4", "moment": "^2.29.4", "number-array-from-range": "^1.0.1", - "playwright": "^1.61.1", "prettier": "^3.8.5", "ramda": "^0.32.0", "ts-node": "^10.9.1", @@ -48,6 +49,57 @@ "vitest": "^4.1.9" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -296,12 +348,18 @@ "node": ">=6.9.0" } }, - "node_modules/@blazediff/core": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@blazediff/core/-/core-1.9.1.tgz", - "integrity": "sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==", + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", @@ -337,6 +395,148 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz", + "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz", + "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", @@ -944,6 +1144,24 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -1506,6 +1724,13 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/clone-deep": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/clone-deep/-/clone-deep-4.0.4.tgz", + "integrity": "sha512-vXh6JuuaAha6sqEbJueYdh5zNBPPgG1OYumuz2UvLvriN6ABHDSW8ludREGWJb1MLIzbwZn4q4zUbUCerJTJfA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -1527,6 +1752,26 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsdom": { + "version": "28.0.3", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-28.0.3.tgz", + "integrity": "sha512-/HQ2uFoetFTXuye8vzIcHw2z6Fwi7Hi/qcgC+RoS9NCyewiqxhVGqlG+ViGB6lkax481R6dmhf1I7lIGlzJStQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^8.0.0", + "undici-types": "^7.21.0" + } + }, + "node_modules/@types/jsdom/node_modules/undici-types": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.28.0.tgz", + "integrity": "sha512-LJAfY+2w6HGeT8d8J1wNQsUGUEGio6NWWpwdwurQe4f6oojzCFuGLizl1KSve4irsTxyLly1QhEeE6iapdaIvQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1545,6 +1790,13 @@ "undici-types": "~8.3.0" } }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.0.tgz", @@ -1831,86 +2083,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@vitest/browser": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.9.tgz", - "integrity": "sha512-j1BKtWmPcqpMhmx/L9EPLgAJpCb0zKfwoWLmqBbxaogCXHjOwHFSEoHCBfnGtx93xKQwilZ26m+UOsHqHMkRNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@blazediff/core": "1.9.1", - "@vitest/mocker": "4.1.9", - "@vitest/utils": "4.1.9", - "magic-string": "^0.30.21", - "pngjs": "^7.0.0", - "sirv": "^3.0.2", - "tinyrainbow": "^3.1.0", - "ws": "^8.19.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "vitest": "4.1.9" - } - }, - "node_modules/@vitest/browser-playwright": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.9.tgz", - "integrity": "sha512-Bq1rOGf9waevzG3EOkO/dene6bvKTUsZMVg8S1i+WH3JcMjuXEjiahP9rAqZRELUqjBySOJsvvSWqK/B3wjKQw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@vitest/browser": "4.1.9", - "@vitest/mocker": "4.1.9", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "playwright": "*", - "vitest": "4.1.9" - }, - "peerDependenciesMeta": { - "playwright": { - "optional": false - } - } - }, - "node_modules/@vitest/browser/node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/@vitest/browser/node_modules/ws": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", - "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/@vitest/coverage-istanbul": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@vitest/coverage-istanbul/-/coverage-istanbul-4.1.9.tgz", @@ -2055,6 +2227,29 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/ui": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.1.9.tgz", + "integrity": "sha512-U/cRvtqfEPj27FI1n9cyUvi4vXXdcLhjJiI+InYKdk8hP4VrS6RXOjGL7rfFaeBc37iRKANsR6eEzIoC7lmgBQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/utils": "4.1.9", + "fflate": "^0.8.2", + "flatted": "^3.4.2", + "pathe": "^2.0.3", + "sirv": "^3.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.1.9" + } + }, "node_modules/@vitest/utils": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", @@ -2219,27 +2414,6 @@ "dev": true, "license": "MIT" }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/baseline-browser-mapping": { "version": "2.10.40", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz", @@ -2264,6 +2438,16 @@ "platform": "^1.3.3" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/brace-expansion": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", @@ -2323,31 +2507,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -2644,6 +2803,82 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2662,6 +2897,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/deep-equal": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", @@ -2789,6 +3031,19 @@ "dev": true, "license": "ISC" }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -3319,6 +3574,13 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "dev": true, + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3619,6 +3881,19 @@ "node": ">= 0.4" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -3671,27 +3946,6 @@ "url": "https://github.com/sponsors/typicode" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -3912,6 +4166,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -4118,6 +4379,105 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsdom/node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -4852,6 +5212,13 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/meow": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/meow/-/meow-14.1.0.tgz", @@ -5202,6 +5569,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5254,63 +5634,6 @@ "dev": true, "license": "MIT" }, - "node_modules/playwright": { - "version": "1.61.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", - "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.61.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.61.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", - "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/pngjs": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", - "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.19.0" - } - }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -5446,6 +5769,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -5539,6 +5872,19 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -5867,6 +6213,13 @@ "node": ">=8" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/synckit": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", @@ -5976,6 +6329,26 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.4.tgz", + "integrity": "sha512-kFXFK7O4WPextIUAOk8qtnw9dxR9UIXP9CjuH1cTBVBZMDeQcUPgr/IazGiw1B0Yiw5L75gHLWeW4iD793r90g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.4" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.4.tgz", + "integrity": "sha512-vwVLJVvvpslm7vqAH7+XNj/neA/Ynq7DT2EEcMuwc5YzN5XaMyRAqxwU+uX3azZ1FQtB2gvrvnLnAEkvYlVdfg==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5999,6 +6372,19 @@ "node": ">=6" } }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -6163,6 +6549,16 @@ "dev": true, "license": "MIT" }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", @@ -6473,6 +6869,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -6480,6 +6889,16 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -6572,6 +6991,23 @@ "node": ">=0.10.0" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -6621,6 +7057,44 @@ } }, "dependencies": { + "@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "requires": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "requires": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + } + }, + "@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true + }, + "@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true + }, "@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -6796,11 +7270,14 @@ "@babel/helper-validator-identifier": "^7.29.7" } }, - "@blazediff/core": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@blazediff/core/-/core-1.9.1.tgz", - "integrity": "sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==", - "dev": true + "@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "requires": { + "css-tree": "^3.0.0" + } }, "@cspotcode/source-map-support": { "version": "0.8.1", @@ -6829,6 +7306,51 @@ } } }, + "@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true + }, + "@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "requires": {} + }, + "@csstools/css-color-parser": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz", + "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==", + "dev": true, + "requires": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.2.1" + } + }, + "@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "peer": true, + "requires": {} + }, + "@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz", + "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==", + "dev": true, + "requires": {} + }, + "@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "peer": true + }, "@emnapi/wasi-threads": { "version": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", @@ -7113,6 +7635,13 @@ "levn": "^0.4.1" } }, + "@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "requires": {} + }, "@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -7437,6 +7966,12 @@ "assertion-error": "^2.0.1" } }, + "@types/clone-deep": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/clone-deep/-/clone-deep-4.0.4.tgz", + "integrity": "sha512-vXh6JuuaAha6sqEbJueYdh5zNBPPgG1OYumuz2UvLvriN6ABHDSW8ludREGWJb1MLIzbwZn4q4zUbUCerJTJfA==", + "dev": true + }, "@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -7455,6 +7990,26 @@ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true }, + "@types/jsdom": { + "version": "28.0.3", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-28.0.3.tgz", + "integrity": "sha512-/HQ2uFoetFTXuye8vzIcHw2z6Fwi7Hi/qcgC+RoS9NCyewiqxhVGqlG+ViGB6lkax481R6dmhf1I7lIGlzJStQ==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^8.0.0", + "undici-types": "^7.21.0" + }, + "dependencies": { + "undici-types": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.28.0.tgz", + "integrity": "sha512-LJAfY+2w6HGeT8d8J1wNQsUGUEGio6NWWpwdwurQe4f6oojzCFuGLizl1KSve4irsTxyLly1QhEeE6iapdaIvQ==", + "dev": true + } + } + }, "@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -7471,6 +8026,12 @@ "undici-types": "~8.3.0" } }, + "@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true + }, "@typescript-eslint/eslint-plugin": { "version": "8.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.0.tgz", @@ -7627,52 +8188,6 @@ } } }, - "@vitest/browser": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.9.tgz", - "integrity": "sha512-j1BKtWmPcqpMhmx/L9EPLgAJpCb0zKfwoWLmqBbxaogCXHjOwHFSEoHCBfnGtx93xKQwilZ26m+UOsHqHMkRNg==", - "dev": true, - "requires": { - "@blazediff/core": "1.9.1", - "@vitest/mocker": "4.1.9", - "@vitest/utils": "4.1.9", - "magic-string": "^0.30.21", - "pngjs": "^7.0.0", - "sirv": "^3.0.2", - "tinyrainbow": "^3.1.0", - "ws": "^8.19.0" - }, - "dependencies": { - "magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "requires": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "ws": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", - "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", - "dev": true, - "requires": {} - } - } - }, - "@vitest/browser-playwright": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.9.tgz", - "integrity": "sha512-Bq1rOGf9waevzG3EOkO/dene6bvKTUsZMVg8S1i+WH3JcMjuXEjiahP9rAqZRELUqjBySOJsvvSWqK/B3wjKQw==", - "dev": true, - "peer": true, - "requires": { - "@vitest/browser": "4.1.9", - "@vitest/mocker": "4.1.9", - "tinyrainbow": "^3.1.0" - } - }, "@vitest/coverage-istanbul": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@vitest/coverage-istanbul/-/coverage-istanbul-4.1.9.tgz", @@ -7776,6 +8291,22 @@ "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", "dev": true }, + "@vitest/ui": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.1.9.tgz", + "integrity": "sha512-U/cRvtqfEPj27FI1n9cyUvi4vXXdcLhjJiI+InYKdk8hP4VrS6RXOjGL7rfFaeBc37iRKANsR6eEzIoC7lmgBQ==", + "dev": true, + "peer": true, + "requires": { + "@vitest/utils": "4.1.9", + "fflate": "^0.8.2", + "flatted": "^3.4.2", + "pathe": "^2.0.3", + "sirv": "^3.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0" + } + }, "@vitest/utils": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", @@ -7891,12 +8422,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true - }, "baseline-browser-mapping": { "version": "2.10.40", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz", @@ -7913,6 +8438,15 @@ "platform": "^1.3.3" } }, + "bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "requires": { + "require-from-string": "^2.0.2" + } + }, "brace-expansion": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", @@ -7946,16 +8480,6 @@ "update-browserslist-db": "^1.2.3" } }, - "buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -8139,6 +8663,60 @@ } } }, + "css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "requires": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + } + }, + "data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "requires": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "dependencies": { + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true + }, + "tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "requires": { + "punycode": "^2.3.1" + } + }, + "webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true + }, + "whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "requires": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + } + } + } + }, "debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -8148,6 +8726,12 @@ "ms": "^2.1.3" } }, + "decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true + }, "deep-equal": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", @@ -8237,6 +8821,12 @@ "integrity": "sha512-v/qV5aV5EUA2pGilzUCq5/eyOloZAqDZBu9UMBIzgPpLlprjSR6zswsWBTv0KpqxLGUAZEwhO95ZCt7srymNVA==", "dev": true }, + "entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true + }, "environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -8580,6 +9170,12 @@ "reusify": "^1.0.4" } }, + "fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "dev": true + }, "file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -8772,6 +9368,15 @@ "function-bind": "^1.1.2" } }, + "html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "requires": { + "@exodus/bytes": "^1.6.0" + } + }, "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -8805,12 +9410,6 @@ "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", "dev": true }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true - }, "ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -8949,6 +9548,12 @@ "isobject": "^3.0.1" } }, + "is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, "is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -9085,6 +9690,75 @@ "esprima": "^4.0.0" } }, + "jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "requires": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true + }, + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true + }, + "tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "requires": { + "punycode": "^2.3.1" + } + }, + "webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true + }, + "whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "requires": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + } + } + } + }, "jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -9480,6 +10154,12 @@ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true }, + "mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true + }, "meow": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/meow/-/meow-14.1.0.tgz", @@ -9679,6 +10359,15 @@ "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", "dev": true }, + "parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "requires": { + "entities": "^8.0.0" + } + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -9715,37 +10404,6 @@ "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", "dev": true }, - "playwright": { - "version": "1.61.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", - "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", - "dev": true, - "requires": { - "fsevents": "2.3.2", - "playwright-core": "1.61.1" - }, - "dependencies": { - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - } - } - }, - "playwright-core": { - "version": "1.61.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", - "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", - "dev": true - }, - "pngjs": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", - "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", - "dev": true - }, "possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -9814,6 +10472,12 @@ "set-function-name": "^2.0.0" } }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, "restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -9870,6 +10534,15 @@ "queue-microtask": "^1.2.2" } }, + "saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "requires": { + "xmlchars": "^2.2.0" + } + }, "semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -10092,6 +10765,12 @@ "has-flag": "^4.0.0" } }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, "synckit": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", @@ -10158,6 +10837,21 @@ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true }, + "tldts": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.4.tgz", + "integrity": "sha512-kFXFK7O4WPextIUAOk8qtnw9dxR9UIXP9CjuH1cTBVBZMDeQcUPgr/IazGiw1B0Yiw5L75gHLWeW4iD793r90g==", + "dev": true, + "requires": { + "tldts-core": "^7.4.4" + } + }, + "tldts-core": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.4.tgz", + "integrity": "sha512-vwVLJVvvpslm7vqAH7+XNj/neA/Ynq7DT2EEcMuwc5YzN5XaMyRAqxwU+uX3azZ1FQtB2gvrvnLnAEkvYlVdfg==", + "dev": true + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -10173,6 +10867,15 @@ "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", "dev": true }, + "tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "requires": { + "tldts": "^7.0.5" + } + }, "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -10270,6 +10973,12 @@ "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", "dev": true }, + "undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true + }, "undici-types": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", @@ -10404,12 +11113,27 @@ } } }, + "w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "requires": { + "xml-name-validator": "^5.0.0" + } + }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "dev": true }, + "whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true + }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -10476,6 +11200,18 @@ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true }, + "xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 4e5726c..8dfbd32 100644 --- a/package.json +++ b/package.json @@ -108,14 +108,12 @@ }, "scripts": { "build": "npm run tidy-clean && npm run lint && tsc -p tsconfig.mjs.json && cpy package.mjs.json dist/mjs --rename package.json && tsc -p tsconfig.cjs.json && cpy package.cjs.json dist/cjs --rename package.json", - "lint": "npm run tidy-clean && eslint src/", + "lint": "eslint .", "test": "vitest", - "test-single": "npm run tidy-clean && npm run lint && vitest run", "benchmark": "tsx ./benchmark.ts", "tidy-clean": "rm -rf dist coverage", - "install-browsers": "playwright install chromium", "prepare-husky": "husky", - "pre-commit": "lint-staged && npm run test-single", + "pre-commit": "lint-staged && npm run lint && npm run test", "upgrade-interactive": "npx npm-check --update" }, "lint-staged": { @@ -134,13 +132,14 @@ "devDependencies": { "@eslint/js": "^10.0.1", "@types/benchmark": "^2.1.2", + "@types/clone-deep": "^4.0.4", + "@types/jsdom": "^28.0.3", "@types/node": "^26.0.1", "@typescript-eslint/eslint-plugin": "^8.62.0", "@typescript-eslint/parser": "^8.62.0", - "@vitest/browser-playwright": "^4.1.9", "@vitest/coverage-istanbul": "^4.1.9", + "@vitest/ui": "^4.1.9", "benchmark": "^2.1.4", - "buffer": "^6.0.3", "clone-deep": "^4.0.1", "codecov": "^3.7.2", "cpy-cli": "^7.0.0", @@ -152,12 +151,12 @@ "fast-deep-equal": "^3.1.3", "husky": "^9.1.7", "jiti": "^2.7.0", + "jsdom": "^29.1.1", "lint-staged": "^17.0.8", "lodash": "^4.18.1", "markdown-table": "^3.0.4", "moment": "^2.29.4", "number-array-from-range": "^1.0.1", - "playwright": "^1.61.1", "prettier": "^3.8.5", "ramda": "^0.32.0", "ts-node": "^10.9.1", @@ -170,4 +169,4 @@ "dependencies": { "viewport-details": "^3.0.1" } -} \ No newline at end of file +} diff --git a/src/array/to-array.ts b/src/array/to-array.ts index 066a851..ce4a253 100644 --- a/src/array/to-array.ts +++ b/src/array/to-array.ts @@ -1,5 +1,8 @@ /** * Checks the provided value is an array and if not returns the value within an array. + * + * @param value - The value to check and convert to an array if necessary. + * @returns An array containing the provided value or the original array if it was already an array. */ export function toArray(value: T | T[]): T[] { return Array.isArray(value) ? value : [value]; diff --git a/src/async/async-every.spec.ts b/src/async/async-every.spec.ts new file mode 100644 index 0000000..fac65ff --- /dev/null +++ b/src/async/async-every.spec.ts @@ -0,0 +1,15 @@ +import { asyncEvery } from './async-every.js'; + +describe(`asyncEvery`, () => { + it(`should return false if not all items in the array match the predicate`, async () => { + const result = await asyncEvery([1, 2, 3, 4, 5], value => new Promise(resolve => setTimeout(() => resolve(value % 2 === 0), 10))); + + expect(result).toBe(false); + }); + + it(`should return true if all items in the array match the predicate`, async () => { + const result = await asyncEvery([2, 4, 6, 8, 10], value => new Promise(resolve => setTimeout(() => resolve(value % 2 === 0), 10))); + + expect(result).toBe(true); + }); +}); diff --git a/src/async/async-every.ts b/src/async/async-every.ts new file mode 100644 index 0000000..4beea57 --- /dev/null +++ b/src/async/async-every.ts @@ -0,0 +1,16 @@ +/** + * Asynchronously tests whether all elements in the array pass the test implemented by the provided predicate function. + * + * @param array - The array to test. + * @param predicate - An asynchronous function that tests each element of the array. It should return a promise that resolves to a boolean value. + * @returns A promise that resolves to `true` if all elements in the array pass the test, and `false` otherwise. + */ +export async function asyncEvery(array: T[], predicate: (item: T, index: number, array: T[]) => Promise): Promise { + for (let index = 0, arrayLength = array.length; index < arrayLength; index++) { + if (!(await predicate(array[index], index, array))) { + return false; + } + } + + return true; +} diff --git a/src/async/async-filter.spec.ts b/src/async/async-filter.spec.ts new file mode 100644 index 0000000..7a3737d --- /dev/null +++ b/src/async/async-filter.spec.ts @@ -0,0 +1,9 @@ +import { asyncFilter } from './async-filter.js'; + +describe(`asyncFilter`, () => { + it(`should filter the array`, async () => { + const result = await asyncFilter([1, 2, 3, 4, 5], value => new Promise(resolve => setTimeout(() => resolve(value % 2 === 0), 10))); + + expect(result).toEqual([2, 4]); + }); +}); diff --git a/src/async/async-filter.ts b/src/async/async-filter.ts new file mode 100644 index 0000000..dcf06be --- /dev/null +++ b/src/async/async-filter.ts @@ -0,0 +1,12 @@ +/** + * Asynchronously filters an array based on a provided asynchronous callback function. + * + * @param array - The array to filter. + * @param callback - An asynchronous function that tests each element of the array. It should return a promise that resolves to a boolean value. + * @returns A promise that resolves to a new array containing only the elements that passed the test implemented by the provided callback function. + */ +export async function asyncFilter(array: T[], callback: (item: T, index: number, array: T[]) => Promise): Promise { + const results = await Promise.all(array.map(callback)); + + return array.filter((_v, index) => results[index]); +} diff --git a/src/async/async-for-each.spec.ts b/src/async/async-for-each.spec.ts new file mode 100644 index 0000000..05a4c3f --- /dev/null +++ b/src/async/async-for-each.spec.ts @@ -0,0 +1,19 @@ +import { asyncForEach } from './async-for-each.js'; + +describe('asyncForEach', () => { + it('should wait for the loop to finish before resolving the promise', async () => { + let result = 0; + + await asyncForEach( + [10, 20, 30], + value => + new Promise(resolve => { + result += value; + + setTimeout(resolve, 10); + }) + ); + + expect(result).toBe(60); + }); +}); diff --git a/src/async/async-for-each.ts b/src/async/async-for-each.ts new file mode 100644 index 0000000..9d66cb2 --- /dev/null +++ b/src/async/async-for-each.ts @@ -0,0 +1,12 @@ +/** + * Asynchronously executes a provided callback function once for each element in an array, in order. + * + * @param array - The array to iterate over. + * @param callback - An asynchronous function that is called for each element in the array. It should return a promise. + * @returns A promise that resolves when all elements have been processed. + */ +export async function asyncForEach(array: T[], callback: (item: T, index: number, array: T[]) => Promise): Promise { + for (let index = 0, arrayLength = array.length; index < arrayLength; index++) { + await callback(array[index], index, array); + } +} diff --git a/src/async/async-some.spec.ts b/src/async/async-some.spec.ts new file mode 100644 index 0000000..9e05241 --- /dev/null +++ b/src/async/async-some.spec.ts @@ -0,0 +1,15 @@ +import { asyncSome } from './async-some.js'; + +describe(`asyncSome`, () => { + it(`should return true if the predicate returns true for any value in the array`, async () => { + const result = await asyncSome([1, 2, 3, 4, 5], value => new Promise(resolve => setTimeout(() => resolve(value % 2 === 0), 10))); + + expect(result).toBe(true); + }); + + it(`should return false if the predicate returns false for all values in the array`, async () => { + const result = await asyncSome([1, 3, 5], value => new Promise(resolve => setTimeout(() => resolve(value % 2 === 0), 10))); + + expect(result).toBe(false); + }); +}); diff --git a/src/async/async-some.ts b/src/async/async-some.ts new file mode 100644 index 0000000..b3d5065 --- /dev/null +++ b/src/async/async-some.ts @@ -0,0 +1,16 @@ +/** + * Asynchronously tests whether at least one element in the array passes the test implemented by the provided predicate function. + * + * @param array - The array to test. + * @param predicate - An asynchronous function that tests each element of the array. It should return a promise that resolves to a boolean value. + * @returns A promise that resolves to `true` if at least one element in the array passes the test, and `false` otherwise. + */ +export async function asyncSome(array: T[], predicate: (item: T, index: number, array: T[]) => Promise): Promise { + for (let index = 0, arrayLength = array.length; index < arrayLength; index++) { + if (await predicate(array[index], index, array)) { + return true; + } + } + + return false; +} diff --git a/src/async/asyncEvery.spec.ts b/src/async/asyncEvery.spec.ts deleted file mode 100644 index 451cb7c..0000000 --- a/src/async/asyncEvery.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { asyncEvery } from '../../src'; - -describe(`asyncEvery`, () => { - it(`should return true if all items in the array match the predicate`, async () => { - const result = await asyncEvery([1, 2, 3, 4, 5], value => { - return new Promise(resolve => setTimeout(() => resolve(value % 2 === 0), 10)); - }); - - expect(result).toBe(false); - }); - - it(`should return false if not all items in the array match the predicate`, async () => { - const result = await asyncEvery([2, 4, 6, 8, 10], value => { - return new Promise(resolve => setTimeout(() => resolve(value % 2 === 0), 10)); - }); - - expect(result).toBe(true); - }); -}); diff --git a/src/async/asyncEvery.ts b/src/async/asyncEvery.ts deleted file mode 100644 index 67aca81..0000000 --- a/src/async/asyncEvery.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Allows you to run [Array#every](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every) asynchronously - */ -export async function asyncEvery(array: T[], predicate: (item: T, index: number, array: T[]) => Promise): Promise { - for (let index = 0, arrayLength = array.length; index < arrayLength; index++) { - if (!(await predicate(array[index], index, array))) { - return false; - } - } - - return true; -} diff --git a/src/async/asyncFilter.spec.ts b/src/async/asyncFilter.spec.ts deleted file mode 100644 index 7d363d2..0000000 --- a/src/async/asyncFilter.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { asyncFilter } from '../../src'; - -describe(`asyncFilter`, () => { - it(`should filter the array`, async () => { - const result = await asyncFilter([1, 2, 3, 4, 5], value => { - return new Promise(resolve => setTimeout(() => resolve(value % 2 === 0), 10)); - }); - - expect(result).toEqual([2, 4]); - }); -}); diff --git a/src/async/asyncFilter.ts b/src/async/asyncFilter.ts deleted file mode 100644 index 7cfa758..0000000 --- a/src/async/asyncFilter.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Allows you to filter an array asynchronously - */ -export async function asyncFilter(array: T[], callback: (item: T, index: number, array: T[]) => Promise): Promise { - const results = await Promise.all(array.map(callback)); - - return array.filter((_v, index) => results[index]); -} diff --git a/src/async/asyncForEach.spec.ts b/src/async/asyncForEach.spec.ts deleted file mode 100644 index 5e593a6..0000000 --- a/src/async/asyncForEach.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { asyncForEach } from '../../src'; - -describe('asyncForEach', () => { - it('should wait for the loop to finish before resolving the promise', async () => { - let result = 0; - - await asyncForEach([10, 20, 30], value => { - return new Promise(resolve => { - result += value; - - setTimeout(resolve, 10); - }); - }); - - expect(result).toBe(60); - }); -}); diff --git a/src/async/asyncForEach.ts b/src/async/asyncForEach.ts deleted file mode 100644 index 6cfd3f9..0000000 --- a/src/async/asyncForEach.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Allows you to iterate over an array asynchronously - */ -export async function asyncForEach(array: T[], callback: (item: T, index: number, array: T[]) => Promise): Promise { - for (let index = 0, arrayLength = array.length; index < arrayLength; index++) { - await callback(array[index], index, array); - } -} diff --git a/src/async/asyncSome.spec.ts b/src/async/asyncSome.spec.ts deleted file mode 100644 index f2484a6..0000000 --- a/src/async/asyncSome.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { asyncSome } from '../../src'; - -describe(`asyncSome`, () => { - it(`should return true if the predicate returns true for any value in the array`, async () => { - const result = await asyncSome([1, 2, 3, 4, 5], value => { - return new Promise(resolve => setTimeout(() => resolve(value % 2 === 0), 10)); - }); - - expect(result).toBe(true); - }); - - it(`should return false if the predicate returns false for all values in the array`, async () => { - const result = await asyncSome([1, 3, 5], value => { - return new Promise(resolve => setTimeout(() => resolve(value % 2 === 0), 10)); - }); - - expect(result).toBe(false); - }); -}); diff --git a/src/async/asyncSome.ts b/src/async/asyncSome.ts deleted file mode 100644 index 24df601..0000000 --- a/src/async/asyncSome.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Allows you to run [Array#some](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some) asynchronously - */ -export async function asyncSome(array: T[], predicate: (item: T, index: number, array: T[]) => Promise): Promise { - for (let index = 0, arrayLength = array.length; index < arrayLength; index++) { - if (await predicate(array[index], index, array)) { - return true; - } - } - - return false; -} diff --git a/src/async/delay.spec.ts b/src/async/delay.spec.ts index 82a9263..f344f2b 100644 --- a/src/async/delay.spec.ts +++ b/src/async/delay.spec.ts @@ -1,23 +1,35 @@ -import { delay } from '../../src'; +import { delay } from './delay.js'; describe('delay', () => { - it('should wait for the delay to complete before continuing execution', async () => { - let resolved = false; + beforeEach(() => { + vi.useFakeTimers(); + }); - setTimeout(() => (resolved = true), 10); + afterEach(() => { + vi.useRealTimers(); + }); - await delay(20); + it('resolves after the specified duration', async () => { + const promise = delay(20); - expect(resolved).toBe(true); - }); + vi.advanceTimersByTime(19); + let settled = false; - it('should wait for the delay to complete before continuing execution', async () => { - let resolved = false; + void promise.then(() => { + settled = true; + }); - setTimeout(() => (resolved = true), 30); + await Promise.resolve(); + expect(settled).toBe(false); - await delay(20); + vi.advanceTimersByTime(1); + await promise; + expect(settled).toBe(true); + }); - expect(resolved).toBe(false); + it('resolves on the next tick when duration is omitted', async () => { + const promise = delay(); + vi.runAllTimers(); + await expect(promise).resolves.toBeUndefined(); }); }); diff --git a/src/async/delay.ts b/src/async/delay.ts index 865c674..f632aa3 100644 --- a/src/async/delay.ts +++ b/src/async/delay.ts @@ -1,6 +1,9 @@ /** - * Delays an async function using await given an optionally provided duration + * Returns a Promise that resolves after a specified duration. + * + * @param durationMs - The duration in milliseconds to wait. If omitted, resolves on the next event loop tick. + * @returns A promise that resolves after the specified duration has elapsed. */ -export function delay(duration?: number): Promise { - return new Promise(resolve => setTimeout(resolve, duration)); +export function delay(durationMs?: number): Promise { + return new Promise(resolve => setTimeout(resolve, durationMs)); } diff --git a/src/async/index.ts b/src/async/index.ts index aadf11c..dbd5049 100644 --- a/src/async/index.ts +++ b/src/async/index.ts @@ -1,5 +1,5 @@ -export * from './asyncEvery.js'; -export * from './asyncFilter.js'; -export * from './asyncForEach.js'; -export * from './asyncSome.js'; +export * from './async-every.js'; +export * from './async-filter.js'; +export * from './async-for-each.js'; +export * from './async-some.js'; export * from './delay.js'; diff --git a/src/clone/clone.spec.ts b/src/clone/clone.spec.ts index 3b2112b..e80ddf8 100644 --- a/src/clone/clone.spec.ts +++ b/src/clone/clone.spec.ts @@ -1,5 +1,5 @@ -import { isEqual } from '../type-predicates'; -import { clone } from './clone'; +import { isEqual } from '../type-predicates/is-equal.js'; +import { clone } from './clone.js'; describe('clone', () => { it('should clone a date', () => { @@ -40,8 +40,7 @@ describe('clone', () => { const value = [childArray, childObject, 'a', 10, new Date()]; const cloned = clone(value); - const clonedArray = cloned[0]; - const clonedObject = cloned[1]; + const [clonedArray, clonedObject] = cloned; expect(Object.is(value, cloned)).toBe(false); expect(Object.is(childArray, clonedArray)).toBe(false); @@ -90,7 +89,7 @@ describe('clone', () => { nested.x = 2; - const clonedItem = Array.from(b)[0]; + const [clonedItem] = Array.from(b); expect(clonedItem.x).toBe(1); }); @@ -137,8 +136,7 @@ describe('clone', () => { const a = new Error('a'); const b = clone(a); - expect(b).toBeInstanceOf(Error); - expect(b.message).toBe(a.message); + expect(a).toEqual(b); }); describe(`Typed Arrays`, () => { diff --git a/src/clone/clone.ts b/src/clone/clone.ts index 4069dce..1b277a9 100644 --- a/src/clone/clone.ts +++ b/src/clone/clone.ts @@ -1,175 +1,295 @@ -import { isPlainObject } from '../type-predicates/isPlainObject.js'; -import { typeOf, ValueType } from '../type-predicates/typeOf.js'; +import { isPlainObject } from '../type-predicates/is-plain-object.js'; type InstanceClone = ((value: T) => T) | boolean; /** - * Recursively (deep) clones native types, like Object, Array, RegExp, Date, Map, Set, Symbol, Error as well as primitives. + * Performs a deep clone of the provided value. + * + * - Plain objects are deeply cloned by default. + * - Arrays, Maps, Sets, and typed arrays are deeply cloned. + * - Class instances are returned as-is unless `instanceClone` is `true` (clone own enumerable + * properties while preserving the prototype) or a custom cloner function is provided. + * - Primitives and functions are returned as-is. */ export function clone(value: T, instanceClone: InstanceClone = false): T { - switch (typeOf(value)) { - case ValueType.Object: { - return cloneObjectDeep(value, instanceClone); - } - case ValueType.Map: { - return cloneMapDeep(value as unknown as Map, instanceClone as unknown as InstanceClone) as unknown as T; - } - case ValueType.Set: { - return cloneSetDeep(value as unknown as Set, instanceClone as unknown as InstanceClone) as unknown as T; - } - case ValueType.Array: - case ValueType.Int8Array: - case ValueType.Uint8Array: - case ValueType.Uint8ClampedArray: - case ValueType.Int16Array: - case ValueType.Uint16Array: - case ValueType.Int32Array: - case ValueType.Uint32Array: - case ValueType.Float32Array: - case ValueType.Float64Array: - case ValueType.BigInt64Array: - case ValueType.BigUint64Array: { - return cloneArrayDeep(value, instanceClone); - } - default: { - return cloneShallow(value); - } + return instanceClone ? (cloneWith(value, instanceClone as InstanceClone) as T) : (cloneFast(value) as T); +} + +// ─── Fast path (no instanceClone) ──────────────────────────────────────────── + +function cloneFast(value: unknown): unknown { + if (value === null || value === undefined) { + return value; + } + + const t = typeof value; + + if (t === 'symbol') { + return cloneSymbol(value as symbol); + } + + if (t !== 'object') { + return value; + } + + if (Array.isArray(value)) { + return cloneArrayFast(value); + } + + // Check for plain objects before the instanceof chain — most common non-array object type. + const proto = Object.getPrototypeOf(value) as object | null; + if (proto === Object.prototype || proto === null) { + return cloneObjectFast(value as Record, proto); + } + + if (value instanceof Date) { + return new Date(value); + } + + if (value instanceof RegExp) { + return cloneRegExp(value); } + + if (value instanceof Map) { + return cloneMapFast(value as Map); + } + + if (value instanceof Set) { + return cloneSetFast(value as Set); + } + + if (value instanceof Error) { + return cloneError(value); + } + + if (Buffer.isBuffer(value)) { + return cloneBuffer(value); + } + + if (ArrayBuffer.isView(value)) { + return cloneTypedArray(value); + } + + // Non-standard prototype — check if it's still plain-ish + return isPlainObject(value) ? cloneObjectFast(value as Record, proto) : value; } -function cloneShallow(value: T): T { - switch (typeOf(value)) { - case ValueType.Buffer: { - return cloneBuffer(value as Buffer) as unknown as T; - } - case ValueType.Symbol: { - return cloneSymbol(value as symbol) as unknown as T; - } - case ValueType.Error: { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return Object.create(value as Error); - } - case ValueType.Date: { - return new Date(value as Date) as unknown as T; - } - case ValueType.RegExp: { - return cloneRegExp(value as unknown as RegExp) as unknown as T; +function cloneObjectFast(source: Record, proto: object | null): unknown { + // For proto === Object.prototype / null, for..in only visits own enumerable string keys + // (Object.prototype has no enumerable properties) so hasOwnProperty is not needed, and + // we avoid the array allocation that Object.keys() would require. + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const cloned: Record = proto === Object.prototype ? {} : Object.create(proto); + + for (const k in source) { + cloned[k] = cloneFast(source[k]); + } + + const symKeys = Object.getOwnPropertySymbols(source); + if (symKeys.length) { + for (let i = 0, len = symKeys.length; i < len; i++) { + const sym = symKeys[i]; + if (Object.prototype.propertyIsEnumerable.call(source, sym)) { + (cloned as Record)[sym] = cloneFast((source as Record)[sym]); + } } } - return value; + return cloned; } -function cloneObjectDeep(value: T, instanceClone?: InstanceClone): T { - if (typeof instanceClone === 'function') { - return instanceClone(value); +function cloneArrayFast(value: unknown[]): unknown[] { + const { length } = value; + const cloned = new Array(length); + + for (let i = 0; i < length; i++) { + cloned[i] = cloneFast(value[i]); } - if (instanceClone || isPlainObject(value)) { - const source = value as unknown as Record; + return cloned; +} - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const ctor = (value as any).constructor as (new () => unknown) | undefined; - const cloned = (ctor === undefined ? Object.create(null) : new ctor()) as Record; +function cloneMapFast(value: Map): Map { + const cloned = new (value.constructor as typeof Map)(); - // Only clone own enumerable properties (matches `isEqual` semantics) - for (const key of Object.keys(source)) { - cloned[key] = clone(source[key], instanceClone as unknown as InstanceClone); - } + for (const [k, v] of value) { + cloned.set(cloneFast(k), cloneFast(v)); + } - return cloned as unknown as T; + return cloned; +} + +function cloneSetFast(value: Set): Set { + const cloned = new (value.constructor as typeof Set)(); + + for (const item of value) { + cloned.add(cloneFast(item)); } - return value; + return cloned; } -function cloneArrayDeep(value: T, instanceClone?: InstanceClone): T { - const length = (value as unknown[]).length; +// ─── Slow path (with instanceClone) ────────────────────────────────────────── - // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - const cloned = new (value as any).constructor(length) as unknown[]; +function cloneWith(value: unknown, instanceClone: InstanceClone): unknown { + if (value === null || value === undefined) { + return value; + } + + const t = typeof value; + + if (t === 'symbol') { + return cloneSymbol(value as symbol); + } + + if (t !== 'object') { + return value; + } + + if (Array.isArray(value)) { + return cloneArrayWith(value, instanceClone); + } + + const proto = Object.getPrototypeOf(value) as object | null; + if (proto === Object.prototype || proto === null) { + return cloneObjectWith(value as Record, instanceClone, proto); + } + + if (value instanceof Date) { + return new Date(value); + } + + if (value instanceof RegExp) { + return cloneRegExp(value); + } + + if (value instanceof Map) { + return cloneMapWith(value as Map, instanceClone); + } + + if (value instanceof Set) { + return cloneSetWith(value as Set, instanceClone); + } + + if (value instanceof Error) { + return cloneError(value); + } + + if (Buffer.isBuffer(value)) { + return cloneBuffer(value); + } + + if (ArrayBuffer.isView(value)) { + return cloneTypedArray(value); + } + + if (isPlainObject(value)) { + return cloneObjectWith(value as Record, instanceClone, proto); + } + + return cloneObjectWith(value as Record, instanceClone, proto); +} + +function cloneObjectWith(source: Record, instanceClone: InstanceClone, proto: object | null): unknown { + if (typeof instanceClone === 'function') { + return instanceClone(source); + } + + if (!instanceClone && proto !== null && proto !== Object.prototype && !isPlainObject(source)) { + return source; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const cloned: Record = proto === Object.prototype ? {} : Object.create(proto); + + for (const k in source) { + cloned[k] = cloneWith(source[k], instanceClone); + } + + const symKeys = Object.getOwnPropertySymbols(source); + if (symKeys.length) { + for (let i = 0, len = symKeys.length; i < len; i++) { + const sym = symKeys[i]; + if (Object.prototype.propertyIsEnumerable.call(source, sym)) { + (cloned as Record)[sym] = cloneWith((source as Record)[sym], instanceClone); + } + } + } + + return cloned; +} + +function cloneArrayWith(value: unknown[], instanceClone: InstanceClone): unknown[] { + const { length } = value; + const cloned = new Array(length); for (let i = 0; i < length; i++) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - cloned[i] = clone(value[i], instanceClone); + cloned[i] = cloneWith(value[i], instanceClone); } - return cloned as T; + return cloned; } -function cloneMapDeep(value: Map, instanceClone?: InstanceClone): Map { - const cloned = new Map(); - const nestedInstanceClone = instanceClone as unknown as InstanceClone; +function cloneMapWith(value: Map, instanceClone: InstanceClone): Map { + const cloned = new (value.constructor as typeof Map)(); - for (const [key, item] of value.entries()) { - const clonedKey = clone(key, nestedInstanceClone as unknown as InstanceClone); - const clonedValue = clone(item, nestedInstanceClone as unknown as InstanceClone); - cloned.set(clonedKey, clonedValue); + for (const [k, v] of value) { + cloned.set(cloneWith(k, instanceClone), cloneWith(v, instanceClone)); } return cloned; } -function cloneSetDeep(value: Set, instanceClone?: InstanceClone): Set { - const cloned = new Set(); - const nestedInstanceClone = instanceClone as unknown as InstanceClone; +function cloneSetWith(value: Set, instanceClone: InstanceClone): Set { + const cloned = new (value.constructor as typeof Set)(); - for (const item of value.values()) { - cloned.add(clone(item, nestedInstanceClone as unknown as InstanceClone)); + for (const item of value) { + cloned.add(cloneWith(item, instanceClone)); } return cloned; } -function cloneRegExp(value: RegExp): RegExp { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - const cloned = new (value as any).constructor(value.source, value.flags) as RegExp; +// ─── Shared helpers ─────────────────────────────────────────────────────────── +function cloneRegExp(value: RegExp): RegExp { + const cloned = new RegExp(value.source, value.flags); cloned.lastIndex = value.lastIndex; - return cloned; } function cloneBuffer(value: Buffer): Buffer { - const length = value.length; + const cloned = Buffer.allocUnsafe(value.length); + value.copy(cloned); + return cloned; +} - const buffer = Buffer.allocUnsafe ? Buffer.allocUnsafe(length) : Buffer.from(length as any); +function cloneTypedArray(value: ArrayBufferView): ArrayBufferView { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const v = value as any; + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + return new v.constructor(v.buffer.slice(v.byteOffset, v.byteOffset + v.byteLength)) as ArrayBufferView; +} + +function cloneError(value: Error): Error { + const cloned = new (value.constructor as typeof Error)(value.message); + cloned.stack = value.stack; - value.copy(buffer); + for (const key of Object.getOwnPropertyNames(value)) { + if (key !== 'message' && key !== 'stack') { + Object.defineProperty(cloned, key, Object.getOwnPropertyDescriptor(value, key)!); + } + } - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return buffer; + return cloned; } function cloneSymbol(value: symbol): symbol { - // Symbols are primitives and cannot be truly cloned; create a new symbol with the same description. - const description = value.description; - - // Preserve well-known symbols (they cannot be recreated) - const wellKnownSymbolKeys = [ - 'asyncIterator', - 'hasInstance', - 'isConcatSpreadable', - 'iterator', - 'match', - 'matchAll', - 'replace', - 'search', - 'species', - 'split', - 'toPrimitive', - 'toStringTag', - 'unscopables', - 'dispose', - 'asyncDispose', - ] as const; - - for (const key of wellKnownSymbolKeys) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if ((Symbol as any)[key] === value) { + for (const key of Object.getOwnPropertyNames(Symbol) as (keyof typeof Symbol)[]) { + if ((Symbol as unknown as Record)[key] === value) { return value; } } - return Symbol(description); + return Symbol(value.description); } diff --git a/src/constants/index.ts b/src/constants/index.ts index b398e8a..336c153 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1 +1 @@ -export * from './isJsdom.js'; +export * from './is-jsdom.js'; diff --git a/src/constants/isJsdom.ts b/src/constants/is-jsdom.ts similarity index 100% rename from src/constants/isJsdom.ts rename to src/constants/is-jsdom.ts diff --git a/src/dates/comparators/compare-dates.spec.ts b/src/dates/comparators/compare-dates.spec.ts index c606fa5..6c306d8 100644 --- a/src/dates/comparators/compare-dates.spec.ts +++ b/src/dates/comparators/compare-dates.spec.ts @@ -4,90 +4,155 @@ describe(`compareDates`, () => { describe(`Same`, () => { describe(`year`, () => { it(`should return true if the years are the same`, () => { - expect(compareDates(new Date(2023, 2, 26, 0, 0, 0, 0), DateComparator.Same, new Date(2023, 2, 27, 0, 0, 0, 0), TimeUnit.Year)).toBe( - true - ); + expect( + compareDates({ + a: new Date(2023, 2, 26, 0, 0, 0, 0), + comparator: DateComparator.Same, + b: new Date(2023, 2, 27, 0, 0, 0, 0), + unit: TimeUnit.Year, + }) + ).toBe(true); }); it(`should return false if the years are not the same`, () => { - expect(compareDates(new Date(2023, 2, 26, 0, 0, 0, 0), DateComparator.Same, new Date(2022, 2, 27, 0, 0, 0, 0), TimeUnit.Year)).toBe( - false - ); + expect( + compareDates({ + a: new Date(2023, 2, 26, 0, 0, 0, 0), + comparator: DateComparator.Same, + b: new Date(2022, 2, 27, 0, 0, 0, 0), + unit: TimeUnit.Year, + }) + ).toBe(false); }); }); describe(`month`, () => { it(`should return true if the months are the same`, () => { expect( - compareDates(new Date(2023, 2, 26, 0, 0, 0, 0), DateComparator.Same, new Date(2023, 2, 27, 0, 0, 0, 0), TimeUnit.Month) + compareDates({ + a: new Date(2023, 2, 26, 0, 0, 0, 0), + comparator: DateComparator.Same, + b: new Date(2023, 2, 27, 0, 0, 0, 0), + unit: TimeUnit.Month, + }) ).toBe(true); }); it(`should return false if the months are not the same`, () => { expect( - compareDates(new Date(2023, 2, 26, 0, 0, 0, 0), DateComparator.Same, new Date(2023, 3, 27, 0, 0, 0, 0), TimeUnit.Month) + compareDates({ + a: new Date(2023, 2, 26, 0, 0, 0, 0), + comparator: DateComparator.Same, + b: new Date(2023, 3, 27, 0, 0, 0, 0), + unit: TimeUnit.Month, + }) ).toBe(false); }); }); describe(`week`, () => { it(`should return true if the weeks are the same`, () => { - expect(compareDates(new Date(2023, 2, 28, 0, 0, 0, 0), DateComparator.Same, new Date(2023, 2, 27, 0, 0, 0, 0), TimeUnit.Week)).toBe( - true - ); + expect( + compareDates({ + a: new Date(2023, 2, 28, 0, 0, 0, 0), + comparator: DateComparator.Same, + b: new Date(2023, 2, 27, 0, 0, 0, 0), + unit: TimeUnit.Week, + }) + ).toBe(true); }); it(`should return false if the weeks are not the same`, () => { - expect(compareDates(new Date(2023, 2, 16, 0, 0, 0, 0), DateComparator.Same, new Date(2023, 2, 27, 0, 0, 0, 0), TimeUnit.Week)).toBe( - false - ); + expect( + compareDates({ + a: new Date(2023, 2, 16, 0, 0, 0, 0), + comparator: DateComparator.Same, + b: new Date(2023, 2, 27, 0, 0, 0, 0), + unit: TimeUnit.Week, + }) + ).toBe(false); }); }); describe(`day`, () => { it(`should return true if the days are the same`, () => { - expect(compareDates(new Date(2023, 2, 27, 0, 0, 0, 0), DateComparator.Same, new Date(2023, 2, 27, 12, 0, 0, 0), TimeUnit.Day)).toBe( - true - ); + expect( + compareDates({ + a: new Date(2023, 2, 27, 0, 0, 0, 0), + comparator: DateComparator.Same, + b: new Date(2023, 2, 27, 12, 0, 0, 0), + unit: TimeUnit.Day, + }) + ).toBe(true); }); it(`should return false if the days are not the same`, () => { - expect(compareDates(new Date(2023, 2, 27, 0, 0, 0, 0), DateComparator.Same, new Date(2023, 2, 28, 0, 0, 0, 0), TimeUnit.Day)).toBe( - false - ); + expect( + compareDates({ + a: new Date(2023, 2, 27, 0, 0, 0, 0), + comparator: DateComparator.Same, + b: new Date(2023, 2, 28, 0, 0, 0, 0), + unit: TimeUnit.Day, + }) + ).toBe(false); }); }); describe(`hour`, () => { it(`should return true if the hours are the same`, () => { - expect(compareDates(new Date(2023, 2, 27, 0, 0, 0, 0), DateComparator.Same, new Date(2023, 2, 27, 0, 0, 0, 0), TimeUnit.Hour)).toBe( - true - ); + expect( + compareDates({ + a: new Date(2023, 2, 27, 0, 0, 0, 0), + comparator: DateComparator.Same, + b: new Date(2023, 2, 27, 0, 0, 0, 0), + unit: TimeUnit.Hour, + }) + ).toBe(true); }); it(`should return false if the hours are not the same`, () => { expect( - compareDates(new Date(2023, 2, 27, 0, 0, 0, 0), DateComparator.Same, new Date(2023, 2, 27, 12, 0, 0, 0), TimeUnit.Hour) + compareDates({ + a: new Date(2023, 2, 27, 0, 0, 0, 0), + comparator: DateComparator.Same, + b: new Date(2023, 2, 27, 12, 0, 0, 0), + unit: TimeUnit.Hour, + }) ).toBe(false); }); it(`should return true if the hours are the same (BST)`, () => { - expect(compareDates(new Date(2023, 2, 26, 1, 0, 0, 0), DateComparator.Same, new Date(2023, 2, 26, 2, 0, 0, 0), TimeUnit.Hour)).toBe( - true - ); + expect( + compareDates({ + a: new Date(2023, 2, 26, 1, 0, 0, 0), + comparator: DateComparator.Same, + b: new Date(2023, 2, 26, 2, 0, 0, 0), + unit: TimeUnit.Hour, + }) + ).toBe(true); }); }); describe(`minute`, () => { it(`should return true if the minutes are the same`, () => { expect( - compareDates(new Date(2023, 2, 27, 0, 0, 0, 0), DateComparator.Same, new Date(2023, 2, 27, 0, 0, 0, 0), TimeUnit.Minute) + compareDates({ + a: new Date(2023, 2, 27, 0, 0, 0, 0), + comparator: DateComparator.Same, + b: new Date(2023, 2, 27, 0, 0, 0, 0), + unit: TimeUnit.Minute, + }) ).toBe(true); }); it(`should return false if the minutes are not the same`, () => { expect( - compareDates(new Date(2023, 2, 27, 0, 0, 0, 0), DateComparator.Same, new Date(2023, 2, 27, 0, 1, 0, 0), TimeUnit.Minute) + compareDates({ + a: new Date(2023, 2, 27, 0, 0, 0, 0), + comparator: DateComparator.Same, + b: new Date(2023, 2, 27, 0, 1, 0, 0), + unit: TimeUnit.Minute, + }) ).toBe(false); }); }); @@ -95,24 +160,48 @@ describe(`compareDates`, () => { describe(`second`, () => { it(`should return true if the seconds are the same`, () => { expect( - compareDates(new Date(2023, 2, 27, 0, 0, 0, 0), DateComparator.Same, new Date(2023, 2, 27, 0, 0, 0, 0), TimeUnit.Second) + compareDates({ + a: new Date(2023, 2, 27, 0, 0, 0, 0), + comparator: DateComparator.Same, + b: new Date(2023, 2, 27, 0, 0, 0, 0), + unit: TimeUnit.Second, + }) ).toBe(true); }); it(`should return false if the seconds are not the same`, () => { expect( - compareDates(new Date(2023, 2, 27, 0, 0, 0, 0), DateComparator.Same, new Date(2023, 2, 27, 0, 0, 1, 0), TimeUnit.Second) + compareDates({ + a: new Date(2023, 2, 27, 0, 0, 0, 0), + comparator: DateComparator.Same, + b: new Date(2023, 2, 27, 0, 0, 1, 0), + unit: TimeUnit.Second, + }) ).toBe(false); }); }); describe(`millisecond`, () => { it(`should return true if the milliseconds are the same`, () => { - expect(compareDates(new Date(2023, 2, 27, 0, 0, 0, 0), DateComparator.Same, new Date(2023, 2, 27, 0, 0, 0, 0))).toBe(true); + expect( + compareDates({ + a: new Date(2023, 2, 27, 0, 0, 0, 0), + comparator: DateComparator.Same, + b: new Date(2023, 2, 27, 0, 0, 0, 0), + unit: TimeUnit.Millisecond, + }) + ).toBe(true); }); it(`should return false if the milliseconds are not the same`, () => { - expect(compareDates(new Date(2023, 2, 27, 0, 0, 0, 0), DateComparator.Same, new Date(2023, 2, 27, 0, 0, 0, 1))).toBe(false); + expect( + compareDates({ + a: new Date(2023, 2, 27, 0, 0, 0, 0), + comparator: DateComparator.Same, + b: new Date(2023, 2, 27, 0, 0, 0, 1), + unit: TimeUnit.Millisecond, + }) + ).toBe(false); }); }); }); @@ -121,13 +210,23 @@ describe(`compareDates`, () => { describe(`year`, () => { it(`should return true if the years are before`, () => { expect( - compareDates(new Date(2022, 2, 26, 0, 0, 0, 0), DateComparator.Before, new Date(2023, 2, 27, 0, 0, 0, 0), TimeUnit.Year) + compareDates({ + a: new Date(2022, 2, 26, 0, 0, 0, 0), + comparator: DateComparator.Before, + b: new Date(2023, 2, 27, 0, 0, 0, 0), + unit: TimeUnit.Year, + }) ).toBe(true); }); it(`should return false if the years are not before`, () => { expect( - compareDates(new Date(2023, 2, 26, 0, 0, 0, 0), DateComparator.Before, new Date(2022, 2, 27, 0, 0, 0, 0), TimeUnit.Year) + compareDates({ + a: new Date(2023, 2, 26, 0, 0, 0, 0), + comparator: DateComparator.Before, + b: new Date(2022, 2, 27, 0, 0, 0, 0), + unit: TimeUnit.Year, + }) ).toBe(false); }); }); @@ -135,13 +234,23 @@ describe(`compareDates`, () => { describe(`month`, () => { it(`should return true if the months are before`, () => { expect( - compareDates(new Date(2023, 1, 26, 0, 0, 0, 0), DateComparator.Before, new Date(2023, 2, 27, 0, 0, 0, 0), TimeUnit.Month) + compareDates({ + a: new Date(2023, 1, 26, 0, 0, 0, 0), + comparator: DateComparator.Before, + b: new Date(2023, 2, 27, 0, 0, 0, 0), + unit: TimeUnit.Month, + }) ).toBe(true); }); it(`should return false if the months are not before`, () => { expect( - compareDates(new Date(2023, 3, 26, 0, 0, 0, 0), DateComparator.Before, new Date(2023, 3, 27, 0, 0, 0, 0), TimeUnit.Month) + compareDates({ + a: new Date(2023, 3, 26, 0, 0, 0, 0), + comparator: DateComparator.Before, + b: new Date(2023, 3, 27, 0, 0, 0, 0), + unit: TimeUnit.Month, + }) ).toBe(false); }); }); @@ -149,13 +258,23 @@ describe(`compareDates`, () => { describe(`week`, () => { it(`should return true if the weeks are before`, () => { expect( - compareDates(new Date(2023, 2, 15, 0, 0, 0, 0), DateComparator.Before, new Date(2023, 2, 27, 0, 0, 0, 0), TimeUnit.Week) + compareDates({ + a: new Date(2023, 2, 15, 0, 0, 0, 0), + comparator: DateComparator.Before, + b: new Date(2023, 2, 27, 0, 0, 0, 0), + unit: TimeUnit.Week, + }) ).toBe(true); }); it(`should return false if the weeks are not before`, () => { expect( - compareDates(new Date(2023, 2, 27, 0, 0, 0, 0), DateComparator.Before, new Date(2023, 2, 28, 0, 0, 0, 0), TimeUnit.Week) + compareDates({ + a: new Date(2023, 2, 27, 0, 0, 0, 0), + comparator: DateComparator.Before, + b: new Date(2023, 2, 28, 0, 0, 0, 0), + unit: TimeUnit.Week, + }) ).toBe(false); }); }); @@ -163,13 +282,23 @@ describe(`compareDates`, () => { describe(`day`, () => { it(`should return true if the days are before`, () => { expect( - compareDates(new Date(2023, 2, 26, 0, 0, 0, 0), DateComparator.Before, new Date(2023, 2, 27, 12, 0, 0, 0), TimeUnit.Day) + compareDates({ + a: new Date(2023, 2, 26, 0, 0, 0, 0), + comparator: DateComparator.Before, + b: new Date(2023, 2, 27, 12, 0, 0, 0), + unit: TimeUnit.Day, + }) ).toBe(true); }); it(`should return false if the days are not before`, () => { expect( - compareDates(new Date(2023, 2, 28, 0, 0, 0, 0), DateComparator.Before, new Date(2023, 2, 28, 0, 0, 0, 0), TimeUnit.Day) + compareDates({ + a: new Date(2023, 2, 28, 0, 0, 0, 0), + comparator: DateComparator.Before, + b: new Date(2023, 2, 28, 0, 0, 0, 0), + unit: TimeUnit.Day, + }) ).toBe(false); }); }); @@ -177,19 +306,34 @@ describe(`compareDates`, () => { describe(`hour`, () => { it(`should return true if the hours are before`, () => { expect( - compareDates(new Date(2023, 2, 27, 0, 0, 0, 0), DateComparator.Before, new Date(2023, 2, 27, 1, 0, 0, 0), TimeUnit.Hour) + compareDates({ + a: new Date(2023, 2, 27, 0, 0, 0, 0), + comparator: DateComparator.Before, + b: new Date(2023, 2, 27, 1, 0, 0, 0), + unit: TimeUnit.Hour, + }) ).toBe(true); }); it(`should return false if the hours are not before`, () => { expect( - compareDates(new Date(2023, 2, 27, 12, 0, 0, 0), DateComparator.Before, new Date(2023, 2, 27, 12, 0, 0, 0), TimeUnit.Hour) + compareDates({ + a: new Date(2023, 2, 27, 12, 0, 0, 0), + comparator: DateComparator.Before, + b: new Date(2023, 2, 27, 12, 0, 0, 0), + unit: TimeUnit.Hour, + }) ).toBe(false); }); it(`should return true if the hours are before (BST)`, () => { expect( - compareDates(new Date(2023, 2, 26, 0, 0, 0, 0), DateComparator.Before, new Date(2023, 2, 26, 2, 0, 0, 0), TimeUnit.Hour) + compareDates({ + a: new Date(2023, 2, 26, 0, 0, 0, 0), + comparator: DateComparator.Before, + b: new Date(2023, 2, 26, 2, 0, 0, 0), + unit: TimeUnit.Hour, + }) ).toBe(true); }); }); @@ -197,13 +341,23 @@ describe(`compareDates`, () => { describe(`minute`, () => { it(`should return true if the minutes are before`, () => { expect( - compareDates(new Date(2023, 2, 27, 0, 0, 0, 0), DateComparator.Before, new Date(2023, 2, 27, 0, 1, 0, 0), TimeUnit.Minute) + compareDates({ + a: new Date(2023, 2, 27, 0, 0, 0, 0), + comparator: DateComparator.Before, + b: new Date(2023, 2, 27, 0, 1, 0, 0), + unit: TimeUnit.Minute, + }) ).toBe(true); }); it(`should return false if the minutes are not before`, () => { expect( - compareDates(new Date(2023, 2, 27, 0, 0, 0, 0), DateComparator.Before, new Date(2023, 2, 27, 0, 0, 0, 0), TimeUnit.Minute) + compareDates({ + a: new Date(2023, 2, 27, 0, 0, 0, 0), + comparator: DateComparator.Before, + b: new Date(2023, 2, 27, 0, 0, 0, 0), + unit: TimeUnit.Minute, + }) ).toBe(false); }); }); @@ -211,24 +365,48 @@ describe(`compareDates`, () => { describe(`second`, () => { it(`should return true if the seconds are before`, () => { expect( - compareDates(new Date(2023, 2, 27, 0, 0, 0, 0), DateComparator.Before, new Date(2023, 2, 27, 0, 0, 1, 0), TimeUnit.Second) + compareDates({ + a: new Date(2023, 2, 27, 0, 0, 0, 0), + comparator: DateComparator.Before, + b: new Date(2023, 2, 27, 0, 0, 1, 0), + unit: TimeUnit.Second, + }) ).toBe(true); }); it(`should return false if the seconds are not before`, () => { expect( - compareDates(new Date(2023, 2, 27, 0, 0, 0, 0), DateComparator.Before, new Date(2023, 2, 27, 0, 0, 0, 0), TimeUnit.Second) + compareDates({ + a: new Date(2023, 2, 27, 0, 0, 0, 0), + comparator: DateComparator.Before, + b: new Date(2023, 2, 27, 0, 0, 0, 0), + unit: TimeUnit.Second, + }) ).toBe(false); }); }); describe(`millisecond`, () => { it(`should return true if the milliseconds are before`, () => { - expect(compareDates(new Date(2023, 2, 27, 0, 0, 0, 0), DateComparator.Before, new Date(2023, 2, 27, 0, 0, 0, 1))).toBe(true); + expect( + compareDates({ + a: new Date(2023, 2, 27, 0, 0, 0, 0), + comparator: DateComparator.Before, + b: new Date(2023, 2, 27, 0, 0, 0, 1), + unit: TimeUnit.Millisecond, + }) + ).toBe(true); }); it(`should return false if the milliseconds are not before`, () => { - expect(compareDates(new Date(2023, 2, 27, 0, 0, 0, 0), DateComparator.Before, new Date(2023, 2, 27, 0, 0, 0, 0))).toBe(false); + expect( + compareDates({ + a: new Date(2023, 2, 27, 0, 0, 0, 0), + comparator: DateComparator.Before, + b: new Date(2023, 2, 27, 0, 0, 0, 0), + unit: TimeUnit.Millisecond, + }) + ).toBe(false); }); }); }); @@ -237,13 +415,23 @@ describe(`compareDates`, () => { describe(`year`, () => { it(`should return true if the years are after`, () => { expect( - compareDates(new Date(2024, 2, 26, 0, 0, 0, 0), DateComparator.After, new Date(2023, 2, 27, 0, 0, 0, 0), TimeUnit.Years) + compareDates({ + a: new Date(2024, 2, 26, 0, 0, 0, 0), + comparator: DateComparator.After, + b: new Date(2023, 2, 27, 0, 0, 0, 0), + unit: TimeUnit.Years, + }) ).toBe(true); }); it(`should return false if the years are not after`, () => { expect( - compareDates(new Date(2023, 2, 26, 0, 0, 0, 0), DateComparator.After, new Date(2024, 2, 27, 0, 0, 0, 0), TimeUnit.Year) + compareDates({ + a: new Date(2023, 2, 26, 0, 0, 0, 0), + comparator: DateComparator.After, + b: new Date(2024, 2, 27, 0, 0, 0, 0), + unit: TimeUnit.Years, + }) ).toBe(false); }); }); @@ -251,13 +439,23 @@ describe(`compareDates`, () => { describe(`month`, () => { it(`should return true if the months are after`, () => { expect( - compareDates(new Date(2023, 3, 26, 0, 0, 0, 0), DateComparator.After, new Date(2023, 2, 27, 0, 0, 0, 0), TimeUnit.Months) + compareDates({ + a: new Date(2023, 3, 26, 0, 0, 0, 0), + comparator: DateComparator.After, + b: new Date(2023, 2, 27, 0, 0, 0, 0), + unit: TimeUnit.Months, + }) ).toBe(true); }); it(`should return false if the months are not after`, () => { expect( - compareDates(new Date(2023, 2, 26, 0, 0, 0, 0), DateComparator.After, new Date(2023, 3, 27, 0, 0, 0, 0), TimeUnit.Month) + compareDates({ + a: new Date(2023, 2, 26, 0, 0, 0, 0), + comparator: DateComparator.After, + b: new Date(2023, 3, 27, 0, 0, 0, 0), + unit: TimeUnit.Months, + }) ).toBe(false); }); }); @@ -265,13 +463,23 @@ describe(`compareDates`, () => { describe(`week`, () => { it(`should return true if the weeks are after`, () => { expect( - compareDates(new Date(2023, 3, 15, 0, 0, 0, 0), DateComparator.After, new Date(2023, 2, 27, 0, 0, 0, 0), TimeUnit.Weeks) + compareDates({ + a: new Date(2023, 3, 15, 0, 0, 0, 0), + comparator: DateComparator.After, + b: new Date(2023, 2, 27, 0, 0, 0, 0), + unit: TimeUnit.Weeks, + }) ).toBe(true); }); it(`should return false if the weeks are not after`, () => { expect( - compareDates(new Date(2023, 2, 27, 0, 0, 0, 0), DateComparator.After, new Date(2023, 2, 28, 0, 0, 0, 0), TimeUnit.Week) + compareDates({ + a: new Date(2023, 2, 27, 0, 0, 0, 0), + comparator: DateComparator.After, + b: new Date(2023, 2, 28, 0, 0, 0, 0), + unit: TimeUnit.Weeks, + }) ).toBe(false); }); }); @@ -279,33 +487,58 @@ describe(`compareDates`, () => { describe(`day`, () => { it(`should return true if the days are after`, () => { expect( - compareDates(new Date(2023, 2, 28, 0, 0, 0, 0), DateComparator.After, new Date(2023, 2, 27, 12, 0, 0, 0), TimeUnit.Days) + compareDates({ + a: new Date(2023, 2, 28, 0, 0, 0, 0), + comparator: DateComparator.After, + b: new Date(2023, 2, 27, 12, 0, 0, 0), + unit: TimeUnit.Days, + }) ).toBe(true); }); it(`should return false if the days are not after`, () => { - expect(compareDates(new Date(2023, 2, 27, 0, 0, 0, 0), DateComparator.After, new Date(2023, 2, 28, 0, 0, 0, 0), TimeUnit.Day)).toBe( - false - ); + expect( + compareDates({ + a: new Date(2023, 2, 27, 0, 0, 0, 0), + comparator: DateComparator.After, + b: new Date(2023, 2, 28, 0, 0, 0, 0), + unit: TimeUnit.Days, + }) + ).toBe(false); }); }); describe(`hour`, () => { it(`should return true if the hours are after`, () => { expect( - compareDates(new Date(2023, 2, 27, 1, 0, 0, 0), DateComparator.After, new Date(2023, 2, 27, 0, 0, 0, 0), TimeUnit.Hour) + compareDates({ + a: new Date(2023, 2, 27, 1, 0, 0, 0), + comparator: DateComparator.After, + b: new Date(2023, 2, 27, 0, 0, 0, 0), + unit: TimeUnit.Hours, + }) ).toBe(true); }); it(`should return false if the hours are not after`, () => { expect( - compareDates(new Date(2023, 2, 27, 11, 0, 0, 0), DateComparator.After, new Date(2023, 2, 27, 12, 0, 0, 0), TimeUnit.Hours) + compareDates({ + a: new Date(2023, 2, 27, 11, 0, 0, 0), + comparator: DateComparator.After, + b: new Date(2023, 2, 27, 12, 0, 0, 0), + unit: TimeUnit.Hours, + }) ).toBe(false); }); it(`should return true if the hours are after (BST)`, () => { expect( - compareDates(new Date(2023, 2, 26, 3, 0, 0, 0), DateComparator.After, new Date(2023, 2, 26, 2, 0, 0, 0), TimeUnit.Hour) + compareDates({ + a: new Date(2023, 2, 26, 3, 0, 0, 0), + comparator: DateComparator.After, + b: new Date(2023, 2, 26, 2, 0, 0, 0), + unit: TimeUnit.Hours, + }) ).toBe(true); }); }); @@ -313,13 +546,23 @@ describe(`compareDates`, () => { describe(`minute`, () => { it(`should return true if the minutes are after`, () => { expect( - compareDates(new Date(2023, 2, 27, 0, 1, 0, 0), DateComparator.After, new Date(2023, 2, 27, 0, 0, 0, 0), TimeUnit.Minutes) + compareDates({ + a: new Date(2023, 2, 27, 0, 1, 0, 0), + comparator: DateComparator.After, + b: new Date(2023, 2, 27, 0, 0, 0, 0), + unit: TimeUnit.Minutes, + }) ).toBe(true); }); it(`should return false if the minutes are not after`, () => { expect( - compareDates(new Date(2023, 2, 27, 0, 0, 0, 0), DateComparator.After, new Date(2023, 2, 27, 0, 1, 0, 0), TimeUnit.Minute) + compareDates({ + a: new Date(2023, 2, 27, 0, 0, 0, 0), + comparator: DateComparator.After, + b: new Date(2023, 2, 27, 0, 1, 0, 0), + unit: TimeUnit.Minutes, + }) ).toBe(false); }); }); @@ -327,13 +570,23 @@ describe(`compareDates`, () => { describe(`second`, () => { it(`should return true if the seconds are after`, () => { expect( - compareDates(new Date(2023, 2, 27, 0, 0, 1, 0), DateComparator.After, new Date(2023, 2, 27, 0, 0, 0, 0), TimeUnit.Seconds) + compareDates({ + a: new Date(2023, 2, 27, 0, 0, 1, 0), + comparator: DateComparator.After, + b: new Date(2023, 2, 27, 0, 0, 0, 0), + unit: TimeUnit.Seconds, + }) ).toBe(true); }); it(`should return false if the seconds are not after`, () => { expect( - compareDates(new Date(2023, 2, 27, 0, 0, 0, 0), DateComparator.After, new Date(2023, 2, 27, 0, 0, 0, 0), TimeUnit.Second) + compareDates({ + a: new Date(2023, 2, 27, 0, 0, 0, 0), + comparator: DateComparator.After, + b: new Date(2023, 2, 27, 0, 0, 0, 0), + unit: TimeUnit.Seconds, + }) ).toBe(false); }); }); @@ -341,12 +594,24 @@ describe(`compareDates`, () => { describe(`millisecond`, () => { it(`should return true if the milliseconds are after`, () => { expect( - compareDates(new Date(2023, 2, 27, 0, 0, 0, 2), DateComparator.After, new Date(2023, 2, 27, 0, 0, 0, 1), TimeUnit.Milliseconds) + compareDates({ + a: new Date(2023, 2, 27, 0, 0, 0, 2), + comparator: DateComparator.After, + b: new Date(2023, 2, 27, 0, 0, 0, 1), + unit: TimeUnit.Milliseconds, + }) ).toBe(true); }); it(`should return false if the milliseconds are not after`, () => { - expect(compareDates(new Date(2023, 2, 27, 0, 0, 0, 0), DateComparator.After, new Date(2023, 2, 27, 0, 0, 0, 0))).toBe(false); + expect( + compareDates({ + a: new Date(2023, 2, 27, 0, 0, 0, 0), + comparator: DateComparator.After, + b: new Date(2023, 2, 27, 0, 0, 0, 0), + unit: TimeUnit.Milliseconds, + }) + ).toBe(false); }); }); }); diff --git a/src/dates/comparators/compare-dates.ts b/src/dates/comparators/compare-dates.ts index a2f5893..4849c81 100644 --- a/src/dates/comparators/compare-dates.ts +++ b/src/dates/comparators/compare-dates.ts @@ -8,8 +8,14 @@ import { DateComparator } from './date-comparator.enum.js'; * * When including a second parameter, it will match all units equal or larger. For example, if passing in month will check month and year, * or if passing in day will check day, month, and year. + * + * @param a - The first date to compare. + * @param comparator - The comparator to use for the comparison. + * @param b - The second date to compare. + * @param unit - The unit of time to compare. + * @returns True if the dates match the comparator, false otherwise. */ -export function compareDates(a: Date, comparator: DateComparator, b: Date, unit?: TimeUnit): boolean { +export function compareDates({ a, comparator, b, unit }: { a: Date; comparator: DateComparator; b: Date; unit?: TimeUnit }): boolean { unit = singulariseUnit(unit); const sameComparator = getSameComparator(comparator); diff --git a/src/dates/comparators/index.ts b/src/dates/comparators/index.ts index 02a6b0d..f2d7a1b 100644 --- a/src/dates/comparators/index.ts +++ b/src/dates/comparators/index.ts @@ -1,3 +1,3 @@ export * from './compare-dates.js'; export * from './date-comparator.enum.js'; -export * from './isSameDate.js'; +export * from './is-same-date.js'; diff --git a/src/dates/comparators/is-same-date.spec.ts b/src/dates/comparators/is-same-date.spec.ts new file mode 100644 index 0000000..25b552b --- /dev/null +++ b/src/dates/comparators/is-same-date.spec.ts @@ -0,0 +1,91 @@ +import { isSameDate, TimeUnit } from '../..'; + +describe(`datesAreSame`, () => { + describe('year', () => { + it('should return true if the years are the same', () => { + expect(isSameDate({ a: new Date(2023, 2, 26, 0, 0, 0, 0), b: new Date(2023, 2, 27, 0, 0, 0, 0), unit: TimeUnit.Year })).toBe(true); + }); + + it('should return false if the years are not the same', () => { + expect(isSameDate({ a: new Date(2023, 2, 26, 0, 0, 0, 0), b: new Date(2022, 2, 27, 0, 0, 0, 0), unit: TimeUnit.Year })).toBe(false); + }); + }); + + describe('month', () => { + it('should return true if the months are the same', () => { + expect(isSameDate({ a: new Date(2023, 2, 26, 0, 0, 0, 0), b: new Date(2023, 2, 27, 0, 0, 0, 0), unit: TimeUnit.Month })).toBe(true); + }); + + it('should return false if the months are not the same', () => { + expect(isSameDate({ a: new Date(2023, 2, 26, 0, 0, 0, 0), b: new Date(2023, 3, 27, 0, 0, 0, 0), unit: TimeUnit.Month })).toBe(false); + }); + }); + + describe('week', () => { + it('should return true if the weeks are the same', () => { + expect(isSameDate({ a: new Date(2023, 2, 28, 0, 0, 0, 0), b: new Date(2023, 2, 27, 0, 0, 0, 0), unit: TimeUnit.Week })).toBe(true); + }); + + it('should return false if the weeks are not the same', () => { + expect(isSameDate({ a: new Date(2023, 2, 16, 0, 0, 0, 0), b: new Date(2023, 2, 27, 0, 0, 0, 0), unit: TimeUnit.Week })).toBe(false); + }); + }); + + describe('day', () => { + it('should return true if the days are the same', () => { + expect(isSameDate({ a: new Date(2023, 2, 27, 0, 0, 0, 0), b: new Date(2023, 2, 27, 12, 0, 0, 0), unit: TimeUnit.Day })).toBe(true); + }); + + it('should return false if the days are not the same', () => { + expect(isSameDate({ a: new Date(2023, 2, 27, 0, 0, 0, 0), b: new Date(2023, 2, 28, 0, 0, 0, 0), unit: TimeUnit.Day })).toBe(false); + }); + }); + + describe('hour', () => { + it('should return true if the hours are the same', () => { + expect(isSameDate({ a: new Date(2023, 2, 27, 0, 0, 0, 0), b: new Date(2023, 2, 27, 0, 0, 0, 0), unit: TimeUnit.Hour })).toBe(true); + }); + + it('should return false if the hours are not the same', () => { + expect(isSameDate({ a: new Date(2023, 2, 27, 0, 0, 0, 0), b: new Date(2023, 2, 27, 12, 0, 0, 0), unit: TimeUnit.Hour })).toBe(false); + }); + + it('should return true if the hours are the same (BST)', () => { + expect(isSameDate({ a: new Date(2023, 2, 26, 1, 0, 0, 0), b: new Date(2023, 2, 26, 2, 0, 0, 0), unit: TimeUnit.Hour })).toBe(true); + }); + }); + + describe('minute', () => { + it('should return true if the minutes are the same', () => { + expect(isSameDate({ a: new Date(2023, 2, 27, 0, 0, 0, 0), b: new Date(2023, 2, 27, 0, 0, 0, 0), unit: TimeUnit.Minute })).toBe(true); + }); + + it('should return false if the minutes are not the same', () => { + expect(isSameDate({ a: new Date(2023, 2, 27, 0, 0, 0, 0), b: new Date(2023, 2, 27, 0, 1, 0, 0), unit: TimeUnit.Minute })).toBe(false); + }); + }); + + describe('second', () => { + it('should return true if the seconds are the same', () => { + expect(isSameDate({ a: new Date(2023, 2, 27, 0, 0, 0, 0), b: new Date(2023, 2, 27, 0, 0, 0, 0), unit: TimeUnit.Second })).toBe(true); + }); + + it('should return false if the seconds are not the same', () => { + expect(isSameDate({ a: new Date(2023, 2, 27, 0, 0, 0, 0), b: new Date(2023, 2, 27, 0, 0, 1, 0), unit: TimeUnit.Second })).toBe(false); + }); + }); + + describe('millisecond', () => { + it('should return true if the milliseconds are the same', () => { + expect(isSameDate({ a: new Date(2023, 2, 27, 0, 0, 0, 0), b: new Date(2023, 2, 27, 0, 0, 0, 0), unit: TimeUnit.Millisecond })).toBe( + true + ); + }); + + it('should return false if the milliseconds are not the same', () => { + expect(isSameDate({ a: new Date(2023, 2, 27, 0, 0, 0, 0), b: new Date(2023, 2, 27, 0, 0, 0, 1), unit: TimeUnit.Millisecond })).toBe( + false + ); + }); + }); +}); diff --git a/src/dates/comparators/isSameDate.ts b/src/dates/comparators/is-same-date.ts similarity index 59% rename from src/dates/comparators/isSameDate.ts rename to src/dates/comparators/is-same-date.ts index bdfbc01..0004d88 100644 --- a/src/dates/comparators/isSameDate.ts +++ b/src/dates/comparators/is-same-date.ts @@ -8,7 +8,12 @@ import { DateComparator } from './date-comparator.enum.js'; * * When including a second parameter, it will match all units equal or larger. For example, if passing in month will check month and year, * or if passing in day will check day, month, and year. + * + * @param a - The first date to compare. + * @param b - The second date to compare. + * @param unit - The unit of time to compare. + * @returns True if the dates are the same, false otherwise. */ -export function isSameDate(a: Date, b: Date, unit?: TimeUnit): boolean { - return compareDates(a, DateComparator.Same, b, unit); +export function isSameDate({ a, b, unit }: { a: Date; b: Date; unit?: TimeUnit }): boolean { + return compareDates({ a, comparator: DateComparator.Same, b, unit }); } diff --git a/src/dates/comparators/isSameDate.spec.ts b/src/dates/comparators/isSameDate.spec.ts deleted file mode 100644 index d0bc531..0000000 --- a/src/dates/comparators/isSameDate.spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { isSameDate, TimeUnit } from '../..'; - -describe(`datesAreSame`, () => { - describe('year', () => { - it('should return true if the years are the same', () => { - expect(isSameDate(new Date(2023, 2, 26, 0, 0, 0, 0), new Date(2023, 2, 27, 0, 0, 0, 0), TimeUnit.Year)).toBe(true); - }); - - it('should return false if the years are not the same', () => { - expect(isSameDate(new Date(2023, 2, 26, 0, 0, 0, 0), new Date(2022, 2, 27, 0, 0, 0, 0), TimeUnit.Year)).toBe(false); - }); - }); - - describe('month', () => { - it('should return true if the months are the same', () => { - expect(isSameDate(new Date(2023, 2, 26, 0, 0, 0, 0), new Date(2023, 2, 27, 0, 0, 0, 0), TimeUnit.Month)).toBe(true); - }); - - it('should return false if the months are not the same', () => { - expect(isSameDate(new Date(2023, 2, 26, 0, 0, 0, 0), new Date(2023, 3, 27, 0, 0, 0, 0), TimeUnit.Month)).toBe(false); - }); - }); - - describe('week', () => { - it('should return true if the weeks are the same', () => { - expect(isSameDate(new Date(2023, 2, 28, 0, 0, 0, 0), new Date(2023, 2, 27, 0, 0, 0, 0), TimeUnit.Week)).toBe(true); - }); - - it('should return false if the weeks are not the same', () => { - expect(isSameDate(new Date(2023, 2, 16, 0, 0, 0, 0), new Date(2023, 2, 27, 0, 0, 0, 0), TimeUnit.Week)).toBe(false); - }); - }); - - describe('day', () => { - it('should return true if the days are the same', () => { - expect(isSameDate(new Date(2023, 2, 27, 0, 0, 0, 0), new Date(2023, 2, 27, 12, 0, 0, 0), TimeUnit.Day)).toBe(true); - }); - - it('should return false if the days are not the same', () => { - expect(isSameDate(new Date(2023, 2, 27, 0, 0, 0, 0), new Date(2023, 2, 28, 0, 0, 0, 0), TimeUnit.Day)).toBe(false); - }); - }); - - describe('hour', () => { - it('should return true if the hours are the same', () => { - expect(isSameDate(new Date(2023, 2, 27, 0, 0, 0, 0), new Date(2023, 2, 27, 0, 0, 0, 0), TimeUnit.Hour)).toBe(true); - }); - - it('should return false if the hours are not the same', () => { - expect(isSameDate(new Date(2023, 2, 27, 0, 0, 0, 0), new Date(2023, 2, 27, 12, 0, 0, 0), TimeUnit.Hour)).toBe(false); - }); - - it('should return true if the hours are the same (BST)', () => { - expect(isSameDate(new Date(2023, 2, 26, 1, 0, 0, 0), new Date(2023, 2, 26, 2, 0, 0, 0), TimeUnit.Hour)).toBe(true); - }); - }); - - describe('minute', () => { - it('should return true if the minutes are the same', () => { - expect(isSameDate(new Date(2023, 2, 27, 0, 0, 0, 0), new Date(2023, 2, 27, 0, 0, 0, 0), TimeUnit.Minute)).toBe(true); - }); - - it('should return false if the minutes are not the same', () => { - expect(isSameDate(new Date(2023, 2, 27, 0, 0, 0, 0), new Date(2023, 2, 27, 0, 1, 0, 0), TimeUnit.Minute)).toBe(false); - }); - }); - - describe('second', () => { - it('should return true if the seconds are the same', () => { - expect(isSameDate(new Date(2023, 2, 27, 0, 0, 0, 0), new Date(2023, 2, 27, 0, 0, 0, 0), TimeUnit.Second)).toBe(true); - }); - - it('should return false if the seconds are not the same', () => { - expect(isSameDate(new Date(2023, 2, 27, 0, 0, 0, 0), new Date(2023, 2, 27, 0, 0, 1, 0), TimeUnit.Second)).toBe(false); - }); - }); - - describe('millisecond', () => { - it('should return true if the milliseconds are the same', () => { - expect(isSameDate(new Date(2023, 2, 27, 0, 0, 0, 0), new Date(2023, 2, 27, 0, 0, 0, 0))).toBe(true); - }); - - it('should return false if the milliseconds are not the same', () => { - expect(isSameDate(new Date(2023, 2, 27, 0, 0, 0, 0), new Date(2023, 2, 27, 0, 0, 0, 1))).toBe(false); - }); - }); -}); diff --git a/src/dates/convert-units/convert-time-unit.spec.ts b/src/dates/convert-units/convert-time-unit.spec.ts new file mode 100644 index 0000000..b5dd1bb --- /dev/null +++ b/src/dates/convert-units/convert-time-unit.spec.ts @@ -0,0 +1,25 @@ +import { convertTimeUnit } from './convert-time-unit.js'; +import { TimeUnit } from './time-unit.enum.js'; + +describe('convertTimeUnit', () => { + it('should convert days into weeks', () => { + const days = 7; + const weeks = 1; + + expect(convertTimeUnit({ value: days, sourceUnit: TimeUnit.Days, resultUnit: TimeUnit.Weeks })).toBe(weeks); + }); + + it('should convert hours into seconds', () => { + const hours = 2; + const seconds = 7200; + + expect(convertTimeUnit({ value: hours, sourceUnit: TimeUnit.Hours, resultUnit: TimeUnit.Seconds })).toBe(seconds); + }); + + it('should convert weeks into days', () => { + const weeks = 2; + const days = 14; + + expect(convertTimeUnit({ value: weeks, sourceUnit: TimeUnit.Weeks, resultUnit: TimeUnit.Days })).toBe(days); + }); +}); diff --git a/src/dates/convert-units/convert-time-unit.ts b/src/dates/convert-units/convert-time-unit.ts new file mode 100644 index 0000000..fa88d4f --- /dev/null +++ b/src/dates/convert-units/convert-time-unit.ts @@ -0,0 +1,17 @@ +import { millisecondsToTimeUnit } from './milliseconds-to-time-unit.js'; +import { timeUnitToMilliseconds } from './time-unit-to-milliseconds.js'; +import { TimeUnit } from './time-unit.enum.js'; + +/** + * Converts a numeric duration from one `TimeUnit` to another. + * + * @param params.value - The numeric value to convert. + * @param params.sourceUnit - The `TimeUnit` of the input value. + * @param params.resultUnit - The `TimeUnit` to convert the value into. + * @returns The converted value expressed in `resultUnit`. + */ +export function convertTimeUnit({ value, sourceUnit, resultUnit }: { value: number; sourceUnit: TimeUnit; resultUnit: TimeUnit }): number { + const ms = timeUnitToMilliseconds(value, sourceUnit); + + return millisecondsToTimeUnit(ms, resultUnit); +} diff --git a/src/dates/convert-units/convertTimeUnit.spec.ts b/src/dates/convert-units/convertTimeUnit.spec.ts deleted file mode 100644 index d7d9cea..0000000 --- a/src/dates/convert-units/convertTimeUnit.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { convertTimeUnit, TimeUnit } from '../..'; - -describe('convertTimeUnit', () => { - it('should convert days into weeks', () => { - const days = 7; - const weeks = 1; - - expect(convertTimeUnit(days, TimeUnit.Days, TimeUnit.Weeks)).toBe(weeks); - }); - - it('should convert hours into seconds', () => { - const hours = 2; - const seconds = 7200; - - expect(convertTimeUnit(hours, TimeUnit.Hours, TimeUnit.Seconds)).toBe(seconds); - }); - - it('should convert weeks into days', () => { - const weeks = 2; - const Days = 14; - - expect(convertTimeUnit(weeks, TimeUnit.Weeks, TimeUnit.Days)).toBe(Days); - }); -}); diff --git a/src/dates/convert-units/convertTimeUnit.ts b/src/dates/convert-units/convertTimeUnit.ts deleted file mode 100644 index 870998d..0000000 --- a/src/dates/convert-units/convertTimeUnit.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { TimeUnit, msToUnit, unitToMS } from './index.js'; - -/** - * Converts a value of a given TimeUnit into another TimeUnit - */ -export function convertTimeUnit(value: number, sourceUnit: TimeUnit, resultUnit: TimeUnit): number { - const ms = unitToMS(value, sourceUnit); - - return msToUnit(ms, resultUnit); -} diff --git a/src/dates/convert-units/index.ts b/src/dates/convert-units/index.ts index 9ecdb76..84d7f16 100644 --- a/src/dates/convert-units/index.ts +++ b/src/dates/convert-units/index.ts @@ -1,4 +1,4 @@ -export * from './TimeUnit.js'; -export * from './convertTimeUnit.js'; -export * from './msToUnit.js'; -export * from './unitToMs.js'; +export * from './convert-time-unit.js'; +export * from './milliseconds-to-time-unit.js'; +export * from './time-unit-to-milliseconds.js'; +export * from './time-unit.enum.js'; diff --git a/src/dates/convert-units/milliseconds-to-time-unit.spec.ts b/src/dates/convert-units/milliseconds-to-time-unit.spec.ts new file mode 100644 index 0000000..664d8af --- /dev/null +++ b/src/dates/convert-units/milliseconds-to-time-unit.spec.ts @@ -0,0 +1,56 @@ +import { millisecondsToTimeUnit } from './milliseconds-to-time-unit.js'; +import { TimeUnit } from './time-unit.enum.js'; + +describe('millisecondsToTimeUnit', () => { + it('convert milliseconds to milliseconds', () => { + const value = 1234; + const expected = 1234; + + expect(millisecondsToTimeUnit(value, TimeUnit.Millisecond)).toBe(expected); + expect(millisecondsToTimeUnit(value, TimeUnit.Milliseconds)).toBe(expected); + }); + + it('convert milliseconds to seconds', () => { + const value = 3000; + const expected = 3; + + expect(millisecondsToTimeUnit(value, TimeUnit.Second)).toBe(expected); + expect(millisecondsToTimeUnit(value, TimeUnit.Seconds)).toBe(expected); + }); + + it('convert milliseconds to minutes', () => { + const value = 720000; + const expected = 12; + + expect(millisecondsToTimeUnit(value, TimeUnit.Minute)).toBe(expected); + expect(millisecondsToTimeUnit(value, TimeUnit.Minutes)).toBe(expected); + }); + + it('convert milliseconds to hours', () => { + const value = 79200000; + const expected = 22; + + expect(millisecondsToTimeUnit(value, TimeUnit.Hour)).toBe(expected); + expect(millisecondsToTimeUnit(value, TimeUnit.Hours)).toBe(expected); + }); + + it('convert milliseconds to day', () => { + const value = 345600000; + const expected = 4; + + expect(millisecondsToTimeUnit(value, TimeUnit.Day)).toBe(expected); + expect(millisecondsToTimeUnit(value, TimeUnit.Days)).toBe(expected); + }); + + it('convert milliseconds to weeks', () => { + const value = 1512000000; + const expected = 2.5; + + expect(millisecondsToTimeUnit(value, TimeUnit.Week)).toBe(expected); + expect(millisecondsToTimeUnit(value, TimeUnit.Weeks)).toBe(expected); + }); + + it(`should throw an error if the unit is not supported`, () => { + expect(() => millisecondsToTimeUnit(1, 'foo' as TimeUnit)).toThrow(); + }); +}); diff --git a/src/dates/convert-units/milliseconds-to-time-unit.ts b/src/dates/convert-units/milliseconds-to-time-unit.ts new file mode 100644 index 0000000..f3f5120 --- /dev/null +++ b/src/dates/convert-units/milliseconds-to-time-unit.ts @@ -0,0 +1,40 @@ +import { TimeUnit } from './time-unit.enum.js'; + +/** + * Convert milliseconds to the specified time unit. + * + * @param milliseconds The number of milliseconds to convert. + * @param timeUnit The target time unit. + * @returns The converted value in the specified time unit. + */ +export function millisecondsToTimeUnit(milliseconds: number, timeUnit: TimeUnit): number { + switch (timeUnit) { + case TimeUnit.Millisecond: + case TimeUnit.Milliseconds: { + return milliseconds; + } + case TimeUnit.Second: + case TimeUnit.Seconds: { + return milliseconds / 1000; + } + case TimeUnit.Minute: + case TimeUnit.Minutes: { + return milliseconds / 60000; + } + case TimeUnit.Hour: + case TimeUnit.Hours: { + return milliseconds / 3600000; + } + case TimeUnit.Day: + case TimeUnit.Days: { + return milliseconds / 86400000; + } + case TimeUnit.Week: + case TimeUnit.Weeks: { + return milliseconds / 604800000; + } + default: { + throw new Error(`Unknown TimeUnit: ${timeUnit}`); + } + } +} diff --git a/src/dates/convert-units/msToUnit.spec.ts b/src/dates/convert-units/msToUnit.spec.ts deleted file mode 100644 index 04060ae..0000000 --- a/src/dates/convert-units/msToUnit.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { msToUnit, TimeUnit } from '../..'; - -describe('msToUnit', () => { - it('convert milliseconds to milliseconds', () => { - const value = 1234; - const expected = 1234; - - expect(msToUnit(value, TimeUnit.Millisecond)).toBe(expected); - expect(msToUnit(value, TimeUnit.Milliseconds)).toBe(expected); - }); - - it('convert milliseconds to seconds', () => { - const value = 3000; - const expected = 3; - - expect(msToUnit(value, TimeUnit.Second)).toBe(expected); - expect(msToUnit(value, TimeUnit.Seconds)).toBe(expected); - }); - - it('convert milliseconds to minutes', () => { - const value = 720000; - const expected = 12; - - expect(msToUnit(value, TimeUnit.Minute)).toBe(expected); - expect(msToUnit(value, TimeUnit.Minutes)).toBe(expected); - }); - - it('convert milliseconds to hours', () => { - const value = 79200000; - const expected = 22; - - expect(msToUnit(value, TimeUnit.Hour)).toBe(expected); - expect(msToUnit(value, TimeUnit.Hours)).toBe(expected); - }); - - it('convert milliseconds to day', () => { - const value = 345600000; - const expected = 4; - - expect(msToUnit(value, TimeUnit.Day)).toBe(expected); - expect(msToUnit(value, TimeUnit.Days)).toBe(expected); - }); - - it('convert milliseconds to weeks', () => { - const value = 1512000000; - const expected = 2.5; - - expect(msToUnit(value, TimeUnit.Week)).toBe(expected); - expect(msToUnit(value, TimeUnit.Weeks)).toBe(expected); - }); - - it(`should throw an error if the unit is not supported`, () => { - expect(() => msToUnit(1, 'foo' as TimeUnit)).toThrowError(); - }); -}); diff --git a/src/dates/convert-units/msToUnit.ts b/src/dates/convert-units/msToUnit.ts deleted file mode 100644 index 88c3d1c..0000000 --- a/src/dates/convert-units/msToUnit.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { TimeUnit } from './index.js'; - -/** - * Converts milliseconds into a TimeUnit - */ -export function msToUnit(value: number, unit: TimeUnit): number { - switch (unit) { - case TimeUnit.Millisecond: - case TimeUnit.Milliseconds: { - return value; - } - case TimeUnit.Second: - case TimeUnit.Seconds: { - return value / 1000; - } - case TimeUnit.Minute: - case TimeUnit.Minutes: { - return value / 60000; - } - case TimeUnit.Hour: - case TimeUnit.Hours: { - return value / 3600000; - } - case TimeUnit.Day: - case TimeUnit.Days: { - return value / 86400000; - } - case TimeUnit.Week: - case TimeUnit.Weeks: { - return value / 604800000; - } - default: { - throw new Error(`Unknown TimeUnit: ${unit}`); - } - } -} diff --git a/src/dates/convert-units/time-unit-to-milliseconds.spec.ts b/src/dates/convert-units/time-unit-to-milliseconds.spec.ts new file mode 100644 index 0000000..045cb00 --- /dev/null +++ b/src/dates/convert-units/time-unit-to-milliseconds.spec.ts @@ -0,0 +1,56 @@ +import { timeUnitToMilliseconds } from './time-unit-to-milliseconds.js'; +import { TimeUnit } from './time-unit.enum.js'; + +describe('timeUnitToMilliseconds', () => { + it('convert milliseconds to milliseconds', () => { + const value = 1234; + const expected = 1234; + + expect(timeUnitToMilliseconds(value, TimeUnit.Millisecond)).toBe(expected); + expect(timeUnitToMilliseconds(value, TimeUnit.Milliseconds)).toBe(expected); + }); + + it('convert seconds to milliseconds', () => { + const value = 3; + const expected = 3000; + + expect(timeUnitToMilliseconds(value, TimeUnit.Second)).toBe(expected); + expect(timeUnitToMilliseconds(value, TimeUnit.Seconds)).toBe(expected); + }); + + it('convert minutes to milliseconds', () => { + const value = 12; + const expected = 720000; + + expect(timeUnitToMilliseconds(value, TimeUnit.Minute)).toBe(expected); + expect(timeUnitToMilliseconds(value, TimeUnit.Minutes)).toBe(expected); + }); + + it('convert hours to milliseconds', () => { + const value = 22; + const expected = 79200000; + + expect(timeUnitToMilliseconds(value, TimeUnit.Hour)).toBe(expected); + expect(timeUnitToMilliseconds(value, TimeUnit.Hours)).toBe(expected); + }); + + it('convert day to milliseconds', () => { + const value = 4; + const expected = 345600000; + + expect(timeUnitToMilliseconds(value, TimeUnit.Day)).toBe(expected); + expect(timeUnitToMilliseconds(value, TimeUnit.Days)).toBe(expected); + }); + + it('convert weeks to milliseconds', () => { + const value = 2.5; + const expected = 1512000000; + + expect(timeUnitToMilliseconds(value, TimeUnit.Week)).toBe(expected); + expect(timeUnitToMilliseconds(value, TimeUnit.Weeks)).toBe(expected); + }); + + it(`should throw an error if the unit is not supported`, () => { + expect(() => timeUnitToMilliseconds(1, 'foo' as TimeUnit)).toThrow(); + }); +}); diff --git a/src/dates/convert-units/unitToMs.ts b/src/dates/convert-units/time-unit-to-milliseconds.ts similarity index 57% rename from src/dates/convert-units/unitToMs.ts rename to src/dates/convert-units/time-unit-to-milliseconds.ts index 0389759..5a638b7 100644 --- a/src/dates/convert-units/unitToMs.ts +++ b/src/dates/convert-units/time-unit-to-milliseconds.ts @@ -1,10 +1,14 @@ -import { TimeUnit } from './index.js'; +import { TimeUnit } from './time-unit.enum.js'; /** - * Converts a TimeUnit into milliseconds + * Convert a value in the specified time unit to milliseconds. + * + * @param value The value to convert. + * @param timeUnit The time unit of the value. + * @returns The converted value in milliseconds. */ -export function unitToMS(value: number, unit: TimeUnit): number { - switch (unit) { +export function timeUnitToMilliseconds(value: number, timeUnit: TimeUnit): number { + switch (timeUnit) { case TimeUnit.Millisecond: case TimeUnit.Milliseconds: { return value; @@ -30,7 +34,7 @@ export function unitToMS(value: number, unit: TimeUnit): number { return value * 604800000; } default: { - throw new Error(`Unknown TimeUnit: ${unit}`); + throw new Error(`Unsupported TimeUnit: ${timeUnit}`); } } } diff --git a/src/dates/convert-units/TimeUnit.ts b/src/dates/convert-units/time-unit.enum.ts similarity index 100% rename from src/dates/convert-units/TimeUnit.ts rename to src/dates/convert-units/time-unit.enum.ts index 1e8572c..494892d 100644 --- a/src/dates/convert-units/TimeUnit.ts +++ b/src/dates/convert-units/time-unit.enum.ts @@ -9,8 +9,8 @@ export enum TimeUnit { Hours = 'hours', Day = 'day', Days = 'days', - Weeks = 'weeks', Week = 'week', + Weeks = 'weeks', Month = 'month', Months = 'months', Year = 'year', diff --git a/src/dates/convert-units/unitToMs.spec.ts b/src/dates/convert-units/unitToMs.spec.ts deleted file mode 100644 index f673029..0000000 --- a/src/dates/convert-units/unitToMs.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { TimeUnit, unitToMS } from '../..'; - -describe('unitToMS', () => { - it('convert milliseconds to milliseconds', () => { - const value = 1234; - const expected = 1234; - - expect(unitToMS(value, TimeUnit.Millisecond)).toBe(expected); - expect(unitToMS(value, TimeUnit.Milliseconds)).toBe(expected); - }); - - it('convert seconds to milliseconds', () => { - const value = 3; - const expected = 3000; - - expect(unitToMS(value, TimeUnit.Second)).toBe(expected); - expect(unitToMS(value, TimeUnit.Seconds)).toBe(expected); - }); - - it('convert minutes to milliseconds', () => { - const value = 12; - const expected = 720000; - - expect(unitToMS(value, TimeUnit.Minute)).toBe(expected); - expect(unitToMS(value, TimeUnit.Minutes)).toBe(expected); - }); - - it('convert hours to milliseconds', () => { - const value = 22; - const expected = 79200000; - - expect(unitToMS(value, TimeUnit.Hour)).toBe(expected); - expect(unitToMS(value, TimeUnit.Hours)).toBe(expected); - }); - - it('convert day to milliseconds', () => { - const value = 4; - const expected = 345600000; - - expect(unitToMS(value, TimeUnit.Day)).toBe(expected); - expect(unitToMS(value, TimeUnit.Days)).toBe(expected); - }); - - it('convert weeks to milliseconds', () => { - const value = 2.5; - const expected = 1512000000; - - expect(unitToMS(value, TimeUnit.Week)).toBe(expected); - expect(unitToMS(value, TimeUnit.Weeks)).toBe(expected); - }); - - it(`should throw an error if the unit is not supported`, () => { - expect(() => unitToMS(1, 'foo' as TimeUnit)).toThrowError(); - }); -}); diff --git a/src/dates/getters/getEndOfDay.spec.ts b/src/dates/getters/get-end-of-day.spec.ts similarity index 55% rename from src/dates/getters/getEndOfDay.spec.ts rename to src/dates/getters/get-end-of-day.spec.ts index 607b4fe..11fcda3 100644 --- a/src/dates/getters/getEndOfDay.spec.ts +++ b/src/dates/getters/get-end-of-day.spec.ts @@ -1,4 +1,5 @@ -import { getEndOfDay, setEndOfDay } from '../../../src'; +import { setEndOfDay } from '../setters/set-end-of-day.js'; +import { getEndOfDay } from './get-end-of-day.js'; describe('getEndOfDay', () => { it('hours should be set to 23, minutes and seconds to 59, and milliseconds to 999 and the result should be a new Object', () => { @@ -14,10 +15,17 @@ describe('getEndOfDay', () => { }); it('should use the current date if none is given', () => { - const expected = setEndOfDay(new Date()); + vi.useFakeTimers(); + vi.setSystemTime(new Date('2025-06-17T14:30:22.123Z')); - const result = getEndOfDay(); + try { + const expected = setEndOfDay(new Date()); - expect(result).toEqual(expected); + const result = getEndOfDay(); + + expect(result).toEqual(expected); + } finally { + vi.useRealTimers(); + } }); }); diff --git a/src/dates/getters/getEndOfDay.ts b/src/dates/getters/get-end-of-day.ts similarity index 51% rename from src/dates/getters/getEndOfDay.ts rename to src/dates/getters/get-end-of-day.ts index 87331df..d43ed0a 100644 --- a/src/dates/getters/getEndOfDay.ts +++ b/src/dates/getters/get-end-of-day.ts @@ -1,7 +1,10 @@ -import { setEndOfDay } from '../setters/index.js'; +import { setEndOfDay } from '../setters/set-end-of-day.js'; /** * Takes an optional date and returns a new Date object set to the end of the given/current day + * + * @param date Date to copy and normalize. Defaults to the current date and time. + * @returns A new Date set to the end of the day. */ export function getEndOfDay(date: Date = new Date()): Date { return setEndOfDay(new Date(date)); diff --git a/src/dates/getters/getEndOfHour.spec.ts b/src/dates/getters/get-end-of-hour.spec.ts similarity index 54% rename from src/dates/getters/getEndOfHour.spec.ts rename to src/dates/getters/get-end-of-hour.spec.ts index 95b3d96..a5be06a 100644 --- a/src/dates/getters/getEndOfHour.spec.ts +++ b/src/dates/getters/get-end-of-hour.spec.ts @@ -1,4 +1,5 @@ -import { getEndOfHour, setEndOfHour } from '../../../src'; +import { setEndOfHour } from '../setters/set-end-of-hour.js'; +import { getEndOfHour } from './get-end-of-hour.js'; describe('getEndOfHour', () => { it('minutes and seconds should be set to 59 and milliseconds to 999 and the result should be a new Object', () => { @@ -14,10 +15,17 @@ describe('getEndOfHour', () => { }); it('should use the current date if none is given', () => { - const expected = setEndOfHour(new Date()); + vi.useFakeTimers(); + vi.setSystemTime(new Date('2025-06-17T14:30:22.123Z')); - const result = getEndOfHour(); + try { + const expected = setEndOfHour(new Date()); - expect(result).toEqual(expected); + const result = getEndOfHour(); + + expect(result).toEqual(expected); + } finally { + vi.useRealTimers(); + } }); }); diff --git a/src/dates/getters/getEndOfHour.ts b/src/dates/getters/get-end-of-hour.ts similarity index 51% rename from src/dates/getters/getEndOfHour.ts rename to src/dates/getters/get-end-of-hour.ts index 939770a..d68d769 100644 --- a/src/dates/getters/getEndOfHour.ts +++ b/src/dates/getters/get-end-of-hour.ts @@ -1,7 +1,10 @@ -import { setEndOfHour } from '../setters/index.js'; +import { setEndOfHour } from '../setters/set-end-of-hour.js'; /** * Takes an optional date and returns a new Date object set to the end of the given/current hour + * + * @param date Date to copy and normalize. Defaults to the current date and time. + * @returns A new Date set to the end of the hour. */ export function getEndOfHour(date: Date = new Date()): Date { return setEndOfHour(new Date(date)); diff --git a/src/dates/getters/getEndOfMinute.spec.ts b/src/dates/getters/get-end-of-minute.spec.ts similarity index 53% rename from src/dates/getters/getEndOfMinute.spec.ts rename to src/dates/getters/get-end-of-minute.spec.ts index e5c110f..46793d3 100644 --- a/src/dates/getters/getEndOfMinute.spec.ts +++ b/src/dates/getters/get-end-of-minute.spec.ts @@ -1,4 +1,5 @@ -import { getEndOfMinute, setEndOfMinute } from '../../../src'; +import { setEndOfMinute } from '../setters/set-end-of-minute.js'; +import { getEndOfMinute } from './get-end-of-minute.js'; describe('getEndOfMinute', () => { it('seconds should be set to 59 and milliseconds to 999 and the result should be a new Object', () => { @@ -14,10 +15,17 @@ describe('getEndOfMinute', () => { }); it('should use the current date if none is given', () => { - const expected = setEndOfMinute(new Date()); + vi.useFakeTimers(); + vi.setSystemTime(new Date('2025-06-17T14:30:22.123Z')); - const result = getEndOfMinute(); + try { + const expected = setEndOfMinute(new Date()); - expect(result).toEqual(expected); + const result = getEndOfMinute(); + + expect(result).toEqual(expected); + } finally { + vi.useRealTimers(); + } }); }); diff --git a/src/dates/getters/getEndOfMinute.ts b/src/dates/getters/get-end-of-minute.ts similarity index 51% rename from src/dates/getters/getEndOfMinute.ts rename to src/dates/getters/get-end-of-minute.ts index 32c7a91..7f63a3a 100644 --- a/src/dates/getters/getEndOfMinute.ts +++ b/src/dates/getters/get-end-of-minute.ts @@ -1,7 +1,10 @@ -import { setEndOfMinute } from '../setters/index.js'; +import { setEndOfMinute } from '../setters/set-end-of-minute.js'; /** * Takes an optional date and returns a new Date object set to the end of the given/current minute + * + * @param date Date to copy and normalize. Defaults to the current date and time. + * @returns A new Date set to the end of the minute. */ export function getEndOfMinute(date: Date = new Date()): Date { return setEndOfMinute(new Date(date)); diff --git a/src/dates/getters/getEndOfMonth.spec.ts b/src/dates/getters/get-end-of-month.spec.ts similarity index 56% rename from src/dates/getters/getEndOfMonth.spec.ts rename to src/dates/getters/get-end-of-month.spec.ts index 2057771..edc4ffc 100644 --- a/src/dates/getters/getEndOfMonth.spec.ts +++ b/src/dates/getters/get-end-of-month.spec.ts @@ -1,4 +1,5 @@ -import { getEndOfMonth, setEndOfMonth } from '../../../src'; +import { setEndOfMonth } from '../setters/set-end-of-month.js'; +import { getEndOfMonth } from './get-end-of-month.js'; describe('getEndOfMonth', () => { it('hours should be set to 23, minutes and seconds to 59, milliseconds to 999, and day to the last of the month and the result should be a new Object', () => { @@ -14,10 +15,17 @@ describe('getEndOfMonth', () => { }); it('should use the current date if none is given', () => { - const expected = setEndOfMonth(new Date()); + vi.useFakeTimers(); + vi.setSystemTime(new Date('2025-06-17T14:30:22.123Z')); - const result = getEndOfMonth(); + try { + const expected = setEndOfMonth(new Date()); - expect(result).toEqual(expected); + const result = getEndOfMonth(); + + expect(result).toEqual(expected); + } finally { + vi.useRealTimers(); + } }); }); diff --git a/src/dates/getters/getEndOfMonth.ts b/src/dates/getters/get-end-of-month.ts similarity index 51% rename from src/dates/getters/getEndOfMonth.ts rename to src/dates/getters/get-end-of-month.ts index 68eafd2..94475bd 100644 --- a/src/dates/getters/getEndOfMonth.ts +++ b/src/dates/getters/get-end-of-month.ts @@ -1,7 +1,10 @@ -import { setEndOfMonth } from '../setters/index.js'; +import { setEndOfMonth } from '../setters/set-end-of-month.js'; /** * Takes an optional date and returns a new Date object set to the end of the given/current month + * + * @param date Date to copy and normalize. Defaults to the current date and time. + * @returns A new Date set to the end of the month. */ export function getEndOfMonth(date: Date = new Date()): Date { return setEndOfMonth(new Date(date)); diff --git a/src/dates/getters/getEndOfSecond.spec.ts b/src/dates/getters/get-end-of-second.spec.ts similarity index 52% rename from src/dates/getters/getEndOfSecond.spec.ts rename to src/dates/getters/get-end-of-second.spec.ts index b198d7a..4e87d02 100644 --- a/src/dates/getters/getEndOfSecond.spec.ts +++ b/src/dates/getters/get-end-of-second.spec.ts @@ -1,4 +1,5 @@ -import { getEndOfSecond, setEndOfSecond } from '../../../src'; +import { setEndOfSecond } from '../setters/set-end-of-second.js'; +import { getEndOfSecond } from './get-end-of-second.js'; describe('getEndOfSecond', () => { it('milliseconds should be set to 999 and the result should be a new Object', () => { @@ -14,10 +15,17 @@ describe('getEndOfSecond', () => { }); it('should use the current date if none is given', () => { - const expected = setEndOfSecond(new Date()); + vi.useFakeTimers(); + vi.setSystemTime(new Date('2025-06-17T14:30:00.123Z')); - const result = getEndOfSecond(); + try { + const expected = setEndOfSecond(new Date()); - expect(result).toEqual(expected); + const result = getEndOfSecond(); + + expect(result).toEqual(expected); + } finally { + vi.useRealTimers(); + } }); }); diff --git a/src/dates/getters/getEndOfSecond.ts b/src/dates/getters/get-end-of-second.ts similarity index 51% rename from src/dates/getters/getEndOfSecond.ts rename to src/dates/getters/get-end-of-second.ts index 2356e78..bacdf0e 100644 --- a/src/dates/getters/getEndOfSecond.ts +++ b/src/dates/getters/get-end-of-second.ts @@ -1,7 +1,10 @@ -import { setEndOfSecond } from '../setters/index.js'; +import { setEndOfSecond } from '../setters/set-end-of-second.js'; /** * Takes an optional date and returns a new Date object set to the end of the given/current second + * + * @param date Date to copy and normalize. Defaults to the current date and time. + * @returns A new Date set to the end of the second. */ export function getEndOfSecond(date: Date = new Date()): Date { return setEndOfSecond(new Date(date)); diff --git a/src/dates/getters/getEndOfWeek.spec.ts b/src/dates/getters/get-end-of-week.spec.ts similarity index 69% rename from src/dates/getters/getEndOfWeek.spec.ts rename to src/dates/getters/get-end-of-week.spec.ts index 97b3bf2..644e1e5 100644 --- a/src/dates/getters/getEndOfWeek.spec.ts +++ b/src/dates/getters/get-end-of-week.spec.ts @@ -1,4 +1,5 @@ -import { getEndOfWeek, setEndOfWeek } from '../../../src'; +import { setEndOfWeek } from '../setters/set-end-of-week.js'; +import { getEndOfWeek } from './get-end-of-week.js'; describe('getEndOfWeek', () => { it('hours should be set to 23, minutes and seconds should be set to 59, milliseconds to 999, and day to last day of week and the result should be a new Object', () => { @@ -26,10 +27,17 @@ describe('getEndOfWeek', () => { }); it('should use the current date if none is given', () => { - const expected = setEndOfWeek(new Date()); + vi.useFakeTimers(); + vi.setSystemTime(new Date('2025-06-17T14:30:22.123Z')); - const result = getEndOfWeek(); + try { + const expected = setEndOfWeek(new Date()); - expect(result).toEqual(expected); + const result = getEndOfWeek(); + + expect(result).toEqual(expected); + } finally { + vi.useRealTimers(); + } }); }); diff --git a/src/dates/getters/getEndOfWeek.ts b/src/dates/getters/get-end-of-week.ts similarity index 51% rename from src/dates/getters/getEndOfWeek.ts rename to src/dates/getters/get-end-of-week.ts index e7abb05..7eb0236 100644 --- a/src/dates/getters/getEndOfWeek.ts +++ b/src/dates/getters/get-end-of-week.ts @@ -1,7 +1,10 @@ -import { setEndOfWeek } from '../setters/index.js'; +import { setEndOfWeek } from '../setters/set-end-of-week.js'; /** * Takes an optional date and returns a new Date object set to the end of the given/current week + * + * @param date Date to copy and normalize. Defaults to the current date and time. + * @returns A new Date set to the end of the week. */ export function getEndOfWeek(date: Date = new Date()): Date { return setEndOfWeek(new Date(date)); diff --git a/src/dates/getters/getEndOfYear.spec.ts b/src/dates/getters/get-end-of-year.spec.ts similarity index 57% rename from src/dates/getters/getEndOfYear.spec.ts rename to src/dates/getters/get-end-of-year.spec.ts index 8e2d239..5a62619 100644 --- a/src/dates/getters/getEndOfYear.spec.ts +++ b/src/dates/getters/get-end-of-year.spec.ts @@ -1,4 +1,5 @@ -import { getEndOfYear, setEndOfYear } from '../../../src'; +import { setEndOfYear } from '../setters/set-end-of-year.js'; +import { getEndOfYear } from './get-end-of-year.js'; describe('getEndOfYear', () => { it('month should be set to 11, day to 31, hours to 23, minutes and seconds should be set to 59, and milliseconds to 999 and the result should be a new Object', () => { @@ -14,10 +15,17 @@ describe('getEndOfYear', () => { }); it('should use the current date if none is given', () => { - const expected = setEndOfYear(new Date()); + vi.useFakeTimers(); + vi.setSystemTime(new Date('2025-06-17T14:30:22.123Z')); - const result = getEndOfYear(); + try { + const expected = setEndOfYear(new Date()); - expect(result).toEqual(expected); + const result = getEndOfYear(); + + expect(result).toEqual(expected); + } finally { + vi.useRealTimers(); + } }); }); diff --git a/src/dates/getters/getEndOfYear.ts b/src/dates/getters/get-end-of-year.ts similarity index 51% rename from src/dates/getters/getEndOfYear.ts rename to src/dates/getters/get-end-of-year.ts index 6747d8e..d6d0bae 100644 --- a/src/dates/getters/getEndOfYear.ts +++ b/src/dates/getters/get-end-of-year.ts @@ -1,7 +1,10 @@ -import { setEndOfYear } from '../setters/index.js'; +import { setEndOfYear } from '../setters/set-end-of-year.js'; /** * Takes an optional date and returns a new Date object set to the end of the given/current year + * + * @param date Date to copy and normalize. Defaults to the current date and time. + * @returns A new Date set to the end of the year. */ export function getEndOfYear(date: Date = new Date()): Date { return setEndOfYear(new Date(date)); diff --git a/src/dates/getters/getMonthNames.spec.ts b/src/dates/getters/get-month-names.spec.ts similarity index 82% rename from src/dates/getters/getMonthNames.spec.ts rename to src/dates/getters/get-month-names.spec.ts index cb9916b..577fea8 100644 --- a/src/dates/getters/getMonthNames.spec.ts +++ b/src/dates/getters/get-month-names.spec.ts @@ -1,8 +1,8 @@ -import { getMonthNames } from '../../../src'; +import { getMonthNames } from './get-month-names.js'; describe(`getMonthNames`, () => { it(`Should return an array of month names`, () => { - expect(getMonthNames()).toEqual([ + expect(getMonthNames({ locale: 'en-US' })).toEqual([ 'January', 'February', 'March', @@ -36,7 +36,7 @@ describe(`getMonthNames`, () => { }); it(`Should return an array of month names in the format provided`, () => { - expect(getMonthNames({ format: 'short' })).toEqual([ + expect(getMonthNames({ locale: 'en-US', format: 'short' })).toEqual([ 'Jan', 'Feb', 'Mar', diff --git a/src/dates/getters/get-month-names.ts b/src/dates/getters/get-month-names.ts new file mode 100644 index 0000000..189cd6b --- /dev/null +++ b/src/dates/getters/get-month-names.ts @@ -0,0 +1,23 @@ +/** + * Gets the month names for the provided locale. Defaults to the runtime's default locale. + * + * @param options Locale and month format options. + * @returns An array of 12 month names formatted for the requested locale. + */ +export function getMonthNames(options: GetMonthNamesOptions = {}): string[] { + const defaultLocale = + typeof navigator !== 'undefined' && typeof navigator.language === 'string' + ? navigator.language + : new Intl.DateTimeFormat().resolvedOptions().locale; + + const { locale = defaultLocale, format = 'long' } = options; + + const formatter = new Intl.DateTimeFormat(locale, { month: format }); + + return Array.from({ length: 12 }, (_, i) => formatter.format(new Date(1970, i, 1))); +} + +export interface GetMonthNamesOptions { + locale?: string; + format?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow'; +} diff --git a/src/dates/getters/getStartOfDay.spec.ts b/src/dates/getters/get-start-of-day.spec.ts similarity index 54% rename from src/dates/getters/getStartOfDay.spec.ts rename to src/dates/getters/get-start-of-day.spec.ts index 020b697..3ecf1cb 100644 --- a/src/dates/getters/getStartOfDay.spec.ts +++ b/src/dates/getters/get-start-of-day.spec.ts @@ -1,4 +1,5 @@ -import { getStartOfDay, setStartOfDay } from '../../../src'; +import { setStartOfDay } from '../setters/set-start-of-day.js'; +import { getStartOfDay } from './get-start-of-day.js'; describe('getStartOfDay', () => { it('milliseconds, seconds, minutes, and hour should be set to 0 and the result should be a new Object', () => { @@ -14,10 +15,17 @@ describe('getStartOfDay', () => { }); it('should use the current date if none is given', () => { - const expected = setStartOfDay(new Date()); + vi.useFakeTimers(); + vi.setSystemTime(new Date('2025-06-17T14:30:22.123Z')); - const result = getStartOfDay(); + try { + const expected = setStartOfDay(new Date()); - expect(result).toEqual(expected); + const result = getStartOfDay(); + + expect(result).toEqual(expected); + } finally { + vi.useRealTimers(); + } }); }); diff --git a/src/dates/getters/getStartOfDay.ts b/src/dates/getters/get-start-of-day.ts similarity index 51% rename from src/dates/getters/getStartOfDay.ts rename to src/dates/getters/get-start-of-day.ts index 6e1c784..e0ec27c 100644 --- a/src/dates/getters/getStartOfDay.ts +++ b/src/dates/getters/get-start-of-day.ts @@ -1,7 +1,10 @@ -import { setStartOfDay } from '../setters/index.js'; +import { setStartOfDay } from '../setters/set-start-of-day.js'; /** * Takes an optional date and returns a new Date object set to the start of the given/current day + * + * @param date Date to copy and normalize. Defaults to the current date and time. + * @returns A new Date set to the start of the day. */ export function getStartOfDay(date: Date = new Date()): Date { return setStartOfDay(new Date(date)); diff --git a/src/dates/getters/getStartOfHour.spec.ts b/src/dates/getters/get-start-of-hour.spec.ts similarity index 53% rename from src/dates/getters/getStartOfHour.spec.ts rename to src/dates/getters/get-start-of-hour.spec.ts index 5360773..c49e7ba 100644 --- a/src/dates/getters/getStartOfHour.spec.ts +++ b/src/dates/getters/get-start-of-hour.spec.ts @@ -1,4 +1,5 @@ -import { getStartOfHour } from '../../../src'; +import { setStartOfHour } from '../setters/set-start-of-hour.js'; +import { getStartOfHour } from './get-start-of-hour.js'; describe('getStartOfHour', () => { it('milliseconds, seconds, and minutes should be set to 0 and the result should be a new Object', () => { @@ -14,10 +15,17 @@ describe('getStartOfHour', () => { }); it('should use the current date if none is given', () => { - const expected = getStartOfHour(new Date()); + vi.useFakeTimers(); + vi.setSystemTime(new Date('2025-06-17T14:30:22.123Z')); - const result = getStartOfHour(); + try { + const expected = setStartOfHour(new Date()); - expect(result).toEqual(expected); + const result = getStartOfHour(); + + expect(result).toEqual(expected); + } finally { + vi.useRealTimers(); + } }); }); diff --git a/src/dates/getters/getStartOfHour.ts b/src/dates/getters/get-start-of-hour.ts similarity index 51% rename from src/dates/getters/getStartOfHour.ts rename to src/dates/getters/get-start-of-hour.ts index 5a12649..a87e646 100644 --- a/src/dates/getters/getStartOfHour.ts +++ b/src/dates/getters/get-start-of-hour.ts @@ -1,7 +1,10 @@ -import { setStartOfHour } from '../setters/index.js'; +import { setStartOfHour } from '../setters/set-start-of-hour.js'; /** * Takes an optional date and returns a new Date object set to the start of the given/current hour + * + * @param date Date to copy and normalize. Defaults to the current date and time. + * @returns A new Date set to the start of the hour. */ export function getStartOfHour(date: Date = new Date()): Date { return setStartOfHour(new Date(date)); diff --git a/src/dates/getters/getStartOfMinute.spec.ts b/src/dates/getters/get-start-of-minute.spec.ts similarity index 52% rename from src/dates/getters/getStartOfMinute.spec.ts rename to src/dates/getters/get-start-of-minute.spec.ts index b3573e5..dff33af 100644 --- a/src/dates/getters/getStartOfMinute.spec.ts +++ b/src/dates/getters/get-start-of-minute.spec.ts @@ -1,4 +1,5 @@ -import { getStartOfMinute, setStartOfMinute } from '../../../src'; +import { setStartOfMinute } from '../setters/set-start-of-minute.js'; +import { getStartOfMinute } from './get-start-of-minute.js'; describe('getStartOfMinute', () => { it('milliseconds and seconds should be set to 0 and the result should be a new Object', () => { @@ -14,10 +15,17 @@ describe('getStartOfMinute', () => { }); it('should use the current date if none is given', () => { - const expected = setStartOfMinute(new Date()); + vi.useFakeTimers(); + vi.setSystemTime(new Date('2025-06-17T14:30:22.123Z')); - const result = getStartOfMinute(); + try { + const expected = setStartOfMinute(new Date()); - expect(result).toEqual(expected); + const result = getStartOfMinute(); + + expect(result).toEqual(expected); + } finally { + vi.useRealTimers(); + } }); }); diff --git a/src/dates/getters/getStartOfMinute.ts b/src/dates/getters/get-start-of-minute.ts similarity index 51% rename from src/dates/getters/getStartOfMinute.ts rename to src/dates/getters/get-start-of-minute.ts index 6c540de..3a0224d 100644 --- a/src/dates/getters/getStartOfMinute.ts +++ b/src/dates/getters/get-start-of-minute.ts @@ -1,7 +1,10 @@ -import { setStartOfMinute } from '../setters/index.js'; +import { setStartOfMinute } from '../setters/set-start-of-minute.js'; /** * Takes an optional date and returns a new Date object set to the start of the given/current minute + * + * @param date Date to copy and normalize. Defaults to the current date and time. + * @returns A new Date set to the start of the minute. */ export function getStartOfMinute(date: Date = new Date()): Date { return setStartOfMinute(new Date(date)); diff --git a/src/dates/getters/getStartOfMonth.spec.ts b/src/dates/getters/get-start-of-month.spec.ts similarity index 54% rename from src/dates/getters/getStartOfMonth.spec.ts rename to src/dates/getters/get-start-of-month.spec.ts index e54e11b..f79e54e 100644 --- a/src/dates/getters/getStartOfMonth.spec.ts +++ b/src/dates/getters/get-start-of-month.spec.ts @@ -1,4 +1,5 @@ -import { getStartOfMonth, setStartOfMonth } from '../../../src'; +import { setStartOfMonth } from '../setters/set-start-of-month.js'; +import { getStartOfMonth } from './get-start-of-month.js'; describe('getStartOfMonth', () => { it('milliseconds, seconds, minutes, and hour should be set to 0 and day to 1 and the result should be a new Object', () => { @@ -14,10 +15,17 @@ describe('getStartOfMonth', () => { }); it('should use the current date if none is given', () => { - const expected = setStartOfMonth(new Date()); + vi.useFakeTimers(); + vi.setSystemTime(new Date('2025-06-17T14:30:22.123Z')); - const result = getStartOfMonth(); + try { + const expected = setStartOfMonth(new Date()); - expect(result).toEqual(expected); + const result = getStartOfMonth(); + + expect(result).toEqual(expected); + } finally { + vi.useRealTimers(); + } }); }); diff --git a/src/dates/getters/getStartOfMonth.ts b/src/dates/getters/get-start-of-month.ts similarity index 51% rename from src/dates/getters/getStartOfMonth.ts rename to src/dates/getters/get-start-of-month.ts index fa26895..53fbb13 100644 --- a/src/dates/getters/getStartOfMonth.ts +++ b/src/dates/getters/get-start-of-month.ts @@ -1,7 +1,10 @@ -import { setStartOfMonth } from '../setters/index.js'; +import { setStartOfMonth } from '../setters/set-start-of-month.js'; /** * Takes an optional date and returns a new Date object set to the start of the given/current month + * + * @param date Date to copy and normalize. Defaults to the current date and time. + * @returns A new Date set to the start of the month. */ export function getStartOfMonth(date: Date = new Date()): Date { return setStartOfMonth(new Date(date)); diff --git a/src/dates/getters/getStartOfSecond.spec.ts b/src/dates/getters/get-start-of-second.spec.ts similarity index 51% rename from src/dates/getters/getStartOfSecond.spec.ts rename to src/dates/getters/get-start-of-second.spec.ts index 53e0efe..6aea56e 100644 --- a/src/dates/getters/getStartOfSecond.spec.ts +++ b/src/dates/getters/get-start-of-second.spec.ts @@ -1,4 +1,5 @@ -import { getStartOfSecond, setStartOfSecond } from '../../../src'; +import { setStartOfSecond } from '../setters/set-start-of-second.js'; +import { getStartOfSecond } from './get-start-of-second.js'; describe('getStartOfSecond', () => { it('milliseconds should be set to 0 and the result should be a new Object', () => { @@ -14,10 +15,17 @@ describe('getStartOfSecond', () => { }); it('should use the current date if none is given', () => { - const expected = setStartOfSecond(new Date()); + vi.useFakeTimers(); + vi.setSystemTime(new Date('2025-06-17T14:30:00.123Z')); - const result = getStartOfSecond(); + try { + const expected = setStartOfSecond(new Date()); - expect(result).toEqual(expected); + const result = getStartOfSecond(); + + expect(result).toEqual(expected); + } finally { + vi.useRealTimers(); + } }); }); diff --git a/src/dates/getters/getStartOfSecond.ts b/src/dates/getters/get-start-of-second.ts similarity index 51% rename from src/dates/getters/getStartOfSecond.ts rename to src/dates/getters/get-start-of-second.ts index 8d09cbf..e6befed 100644 --- a/src/dates/getters/getStartOfSecond.ts +++ b/src/dates/getters/get-start-of-second.ts @@ -1,7 +1,10 @@ -import { setStartOfSecond } from '../setters/index.js'; +import { setStartOfSecond } from '../setters/set-start-of-second.js'; /** * Takes an optional date and returns a new Date object set to the start of the given/current second + * + * @param date Date to copy and normalize. Defaults to the current date and time. + * @returns A new Date set to the start of the second. */ export function getStartOfSecond(date: Date = new Date()): Date { return setStartOfSecond(new Date(date)); diff --git a/src/dates/getters/getStartOfWeek.spec.ts b/src/dates/getters/get-start-of-week.spec.ts similarity index 67% rename from src/dates/getters/getStartOfWeek.spec.ts rename to src/dates/getters/get-start-of-week.spec.ts index 7818e4a..555b1e1 100644 --- a/src/dates/getters/getStartOfWeek.spec.ts +++ b/src/dates/getters/get-start-of-week.spec.ts @@ -1,4 +1,5 @@ -import { getStartOfWeek, setStartOfWeek } from '../../../src'; +import { setStartOfWeek } from '../setters/set-start-of-week.js'; +import { getStartOfWeek } from './get-start-of-week.js'; describe('getStartOfWeek', () => { it('milliseconds, seconds, minutes, and hour should be set to 0 and day to first day of week and the result should be a new Object', () => { @@ -26,10 +27,17 @@ describe('getStartOfWeek', () => { }); it('should use the current date if none is given', () => { - const expected = setStartOfWeek(new Date()); + vi.useFakeTimers(); + vi.setSystemTime(new Date('2025-06-17T14:30:22.123Z')); - const result = getStartOfWeek(); + try { + const expected = setStartOfWeek(new Date()); - expect(result).toEqual(expected); + const result = getStartOfWeek(); + + expect(result).toEqual(expected); + } finally { + vi.useRealTimers(); + } }); }); diff --git a/src/dates/getters/getStartOfWeek.ts b/src/dates/getters/get-start-of-week.ts similarity index 51% rename from src/dates/getters/getStartOfWeek.ts rename to src/dates/getters/get-start-of-week.ts index 447bf3c..4093f4b 100644 --- a/src/dates/getters/getStartOfWeek.ts +++ b/src/dates/getters/get-start-of-week.ts @@ -1,7 +1,10 @@ -import { setStartOfWeek } from '../setters/index.js'; +import { setStartOfWeek } from '../setters/set-start-of-week.js'; /** * Takes an optional date and returns a new Date object set to the start of the given/current week + * + * @param date Date to copy and normalize. Defaults to the current date and time. + * @returns A new Date set to the start of the week. */ export function getStartOfWeek(date: Date = new Date()): Date { return setStartOfWeek(new Date(date)); diff --git a/src/dates/getters/getStartOfYear.spec.ts b/src/dates/getters/get-start-of-year.spec.ts similarity index 54% rename from src/dates/getters/getStartOfYear.spec.ts rename to src/dates/getters/get-start-of-year.spec.ts index 7aa71d4..922a94f 100644 --- a/src/dates/getters/getStartOfYear.spec.ts +++ b/src/dates/getters/get-start-of-year.spec.ts @@ -1,4 +1,5 @@ -import { getStartOfYear, setStartOfYear } from '../../../src'; +import { setStartOfYear } from '../setters/set-start-of-year.js'; +import { getStartOfYear } from './get-start-of-year.js'; describe('getStartOfYear', () => { it('milliseconds, seconds, minutes, and hour should be set to 0 and day to 1 and the result should be a new Object', () => { @@ -14,10 +15,17 @@ describe('getStartOfYear', () => { }); it('should use the current date if none is given', () => { - const expected = setStartOfYear(new Date()); + vi.useFakeTimers(); + vi.setSystemTime(new Date('2025-06-17T14:30:22.123Z')); - const result = getStartOfYear(); + try { + const expected = setStartOfYear(new Date()); - expect(result).toEqual(expected); + const result = getStartOfYear(); + + expect(result).toEqual(expected); + } finally { + vi.useRealTimers(); + } }); }); diff --git a/src/dates/getters/getStartOfYear.ts b/src/dates/getters/get-start-of-year.ts similarity index 51% rename from src/dates/getters/getStartOfYear.ts rename to src/dates/getters/get-start-of-year.ts index ee7cf55..96783f9 100644 --- a/src/dates/getters/getStartOfYear.ts +++ b/src/dates/getters/get-start-of-year.ts @@ -1,7 +1,10 @@ -import { setStartOfYear } from '../setters/index.js'; +import { setStartOfYear } from '../setters/set-start-of-year.js'; /** * Takes an optional date and returns a new Date object set to the start of the given/current year + * + * @param date Date to copy and normalize. Defaults to the current date and time. + * @returns A new Date set to the start of the year. */ export function getStartOfYear(date: Date = new Date()): Date { return setStartOfYear(new Date(date)); diff --git a/src/dates/getters/get-today.spec.ts b/src/dates/getters/get-today.spec.ts new file mode 100644 index 0000000..724837a --- /dev/null +++ b/src/dates/getters/get-today.spec.ts @@ -0,0 +1,23 @@ +import { getToday } from './get-today.js'; + +describe('getToday', () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2025-06-17T14:30:00.000Z')); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it(`should get today's date but the start of the day`, () => { + const today = new Date('2025-06-17T14:30:00.000Z'); + + today.setHours(0); + today.setMinutes(0); + today.setSeconds(0); + today.setMilliseconds(0); + + expect(getToday()).toEqual(today); + }); +}); diff --git a/src/dates/getters/getToday.ts b/src/dates/getters/get-today.ts similarity index 53% rename from src/dates/getters/getToday.ts rename to src/dates/getters/get-today.ts index c5a12a6..290c45e 100644 --- a/src/dates/getters/getToday.ts +++ b/src/dates/getters/get-today.ts @@ -1,7 +1,9 @@ -import { setStartOfDay } from '../setters/index.js'; +import { setStartOfDay } from '../setters/set-start-of-day.js'; /** * Gets a Date for the start of the given/current day + * + * @returns A new Date set to the start of today. */ export function getToday(): Date { return setStartOfDay(new Date()); diff --git a/src/dates/getters/get-week-of-year.spec.ts b/src/dates/getters/get-week-of-year.spec.ts new file mode 100644 index 0000000..798a5f7 --- /dev/null +++ b/src/dates/getters/get-week-of-year.spec.ts @@ -0,0 +1,47 @@ +import { getWeekOfYear } from './get-week-of-year.js'; + +describe(`getWeekOfYear`, () => { + describe(`year where Jan 1 falls on Sunday (2023)`, () => { + it(`Jan 1 (Sunday, sole day of week 1) is week 1`, () => expect(getWeekOfYear(new Date(2023, 0, 1))).toEqual(1)); + it(`Jan 2 (Monday, start of week 2) is week 2`, () => expect(getWeekOfYear(new Date(2023, 0, 2))).toEqual(2)); + it(`Jan 8 (Sunday, last day of week 2) is week 2`, () => expect(getWeekOfYear(new Date(2023, 0, 8))).toEqual(2)); + it(`Dec 30 is week 53`, () => expect(getWeekOfYear(new Date(2023, 11, 30))).toEqual(53)); + it(`Dec 31 is week 53`, () => expect(getWeekOfYear(new Date(2023, 11, 31))).toEqual(53)); + }); + + describe(`year where Jan 1 falls on Monday (2024, leap year)`, () => { + it(`Jan 1 is week 1`, () => expect(getWeekOfYear(new Date(2024, 0, 1))).toEqual(1)); + it(`Jan 7 (Sunday, last day of week 1) is week 1`, () => expect(getWeekOfYear(new Date(2024, 0, 7))).toEqual(1)); + it(`Jan 8 (Monday, start of week 2) is week 2`, () => expect(getWeekOfYear(new Date(2024, 0, 8))).toEqual(2)); + it(`Dec 29 (Sunday, last day of week 52) is week 52`, () => expect(getWeekOfYear(new Date(2024, 11, 29))).toEqual(52)); + it(`Dec 30 (Monday, start of week 53) is week 53`, () => expect(getWeekOfYear(new Date(2024, 11, 30))).toEqual(53)); + }); + + describe(`year where Jan 1 falls on Wednesday (2025)`, () => { + it(`Jan 1 is week 1`, () => expect(getWeekOfYear(new Date(2025, 0, 1))).toEqual(1)); + it(`Jan 5 (Sunday, last day of week 1) is week 1`, () => expect(getWeekOfYear(new Date(2025, 0, 5))).toEqual(1)); + it(`Jan 6 (Monday, start of week 2) is week 2`, () => expect(getWeekOfYear(new Date(2025, 0, 6))).toEqual(2)); + it(`Dec 28 (Sunday, last day of week 52) is week 52`, () => expect(getWeekOfYear(new Date(2025, 11, 28))).toEqual(52)); + it(`Dec 29 (Monday, start of week 53) is week 53`, () => expect(getWeekOfYear(new Date(2025, 11, 29))).toEqual(53)); + }); + + describe(`year where Jan 1 falls on Saturday (2022)`, () => { + it(`Jan 1 is week 1`, () => expect(getWeekOfYear(new Date(2022, 0, 1))).toEqual(1)); + it(`Jan 2 (Sunday, last day of week 1) is week 1`, () => expect(getWeekOfYear(new Date(2022, 0, 2))).toEqual(1)); + it(`Jan 3 (Monday, start of week 2) is week 2`, () => expect(getWeekOfYear(new Date(2022, 0, 3))).toEqual(2)); + it(`Dec 25 (Sunday, last day of week 52) is week 52`, () => expect(getWeekOfYear(new Date(2022, 11, 25))).toEqual(52)); + it(`Dec 26 (Monday, start of week 53) is week 53`, () => expect(getWeekOfYear(new Date(2022, 11, 26))).toEqual(53)); + it(`Dec 31 is week 53`, () => expect(getWeekOfYear(new Date(2022, 11, 31))).toEqual(53)); + }); + + it(`should use today's date if none is provided`, () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date(2024, 0, 8, 12, 0, 0, 0)); + + try { + expect(getWeekOfYear()).toEqual(2); + } finally { + vi.useRealTimers(); + } + }); +}); diff --git a/src/dates/getters/get-week-of-year.ts b/src/dates/getters/get-week-of-year.ts new file mode 100644 index 0000000..9c40a6a --- /dev/null +++ b/src/dates/getters/get-week-of-year.ts @@ -0,0 +1,21 @@ +/** + * Gets the week number of the year for the given date. Will use today's date if no date is provided. + * + * Weeks run Monday–Sunday. Week 1 always contains Jan 1; the first week may be + * a partial week (fewer than 7 days) when Jan 1 falls after Monday. + * Uses UTC calendar-day math for the day-count so results don't vary with + * time-of-day or DST transitions. + * + * @param date Date to evaluate. Defaults to the current date and time. + * @returns The 1-based week number within the year. + */ +export function getWeekOfYear(date: Date = new Date()): number { + const year = date.getFullYear(); + const firstDayOfYear = new Date(year, 0, 1); + const mondayBasedDayOfJan1 = (firstDayOfYear.getDay() + 6) % 7; // Mon=0 … Sun=6 + const MILLISECONDS_PER_DAY = 86_400_000; + + const daysSinceYearStart = Math.floor((Date.UTC(year, date.getMonth(), date.getDate()) - Date.UTC(year, 0, 1)) / MILLISECONDS_PER_DAY); + + return Math.ceil((daysSinceYearStart + 1 + mondayBasedDayOfJan1) / 7); +} diff --git a/src/dates/getters/getMonthNames.ts b/src/dates/getters/getMonthNames.ts deleted file mode 100644 index 8cc22cf..0000000 --- a/src/dates/getters/getMonthNames.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Gets the month names for the provided locale. Defaults to the browser's locale. Optionally, you can provide a format. - */ -export function getMonthNames(options?: GetMonthNamesOptions): string[] { - const { locale, format }: GetMonthNamesOptions = { locale: navigator.language, format: 'long', ...options }; - - const formatter = new Intl.DateTimeFormat(locale, { month: format }); - - const monthNames: string[] = []; - - for (let i = 0; i < 12; i++) { - monthNames.push(formatter.format(new Date(1970, i, 1))); - } - - return monthNames; -} - -export interface GetMonthNamesOptions { - locale?: string; - format?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow'; -} diff --git a/src/dates/getters/getToday.spec.ts b/src/dates/getters/getToday.spec.ts deleted file mode 100644 index 040ac61..0000000 --- a/src/dates/getters/getToday.spec.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { getToday } from '../../../src'; - -describe('getToday', () => { - it('should get todays date but the start of the day', () => { - const today = new Date(); - - today.setHours(0); - today.setMinutes(0); - today.setSeconds(0); - today.setMilliseconds(0); - - expect(getToday()).toEqual(today); - }); -}); diff --git a/src/dates/getters/getWeekOfYear.spec.ts b/src/dates/getters/getWeekOfYear.spec.ts deleted file mode 100644 index 324eca3..0000000 --- a/src/dates/getters/getWeekOfYear.spec.ts +++ /dev/null @@ -1,12 +0,0 @@ -import moment from 'moment'; -import { getWeekOfYear } from '../../../src'; - -describe(`getWeekOfYear`, () => { - it(`should get the week of the year for the given date`, () => { - expect(getWeekOfYear(new Date(2023, 11, 30))).toEqual(52); - }); - - it(`should get the week of the year for today's date`, () => { - expect(getWeekOfYear()).toEqual(moment().week()); - }); -}); diff --git a/src/dates/getters/getWeekOfYear.ts b/src/dates/getters/getWeekOfYear.ts deleted file mode 100644 index fdfa2cc..0000000 --- a/src/dates/getters/getWeekOfYear.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { TimeUnit, convertTimeUnit } from '../convert-units/index.js'; -import { getStartOfYear } from './getStartOfYear.js'; - -/** - * Gets the week number of the year for the given date. Will use today's date if no date is provided. - */ -export function getWeekOfYear(date: Date = new Date()): number { - const firstDayOfYear = getStartOfYear(date); - const offset = firstDayOfYear.getDay() + 1; - - const days = convertTimeUnit(date.getTime() - firstDayOfYear.getTime(), TimeUnit.Milliseconds, TimeUnit.Days); - - return Math.ceil((days + offset) / 7); -} diff --git a/src/dates/getters/index.ts b/src/dates/getters/index.ts index d19268c..c53cc22 100644 --- a/src/dates/getters/index.ts +++ b/src/dates/getters/index.ts @@ -1,17 +1,17 @@ -export * from './getEndOfDay.js'; -export * from './getEndOfHour.js'; -export * from './getEndOfMinute.js'; -export * from './getEndOfMonth.js'; -export * from './getEndOfSecond.js'; -export * from './getEndOfWeek.js'; -export * from './getEndOfYear.js'; -export * from './getMonthNames.js'; -export * from './getStartOfDay.js'; -export * from './getStartOfHour.js'; -export * from './getStartOfMinute.js'; -export * from './getStartOfMonth.js'; -export * from './getStartOfSecond.js'; -export * from './getStartOfWeek.js'; -export * from './getStartOfYear.js'; -export * from './getToday.js'; -export * from './getWeekOfYear.js'; +export * from './get-end-of-day.js'; +export * from './get-end-of-hour.js'; +export * from './get-end-of-minute.js'; +export * from './get-end-of-month.js'; +export * from './get-end-of-second.js'; +export * from './get-end-of-week.js'; +export * from './get-end-of-year.js'; +export * from './get-month-names.js'; +export * from './get-start-of-day.js'; +export * from './get-start-of-hour.js'; +export * from './get-start-of-minute.js'; +export * from './get-start-of-month.js'; +export * from './get-start-of-second.js'; +export * from './get-start-of-week.js'; +export * from './get-start-of-year.js'; +export * from './get-today.js'; +export * from './get-week-of-year.js'; diff --git a/src/dates/modifiers/add-to-date.spec.ts b/src/dates/modifiers/add-to-date.spec.ts index 457cd88..98bd9e3 100644 --- a/src/dates/modifiers/add-to-date.spec.ts +++ b/src/dates/modifiers/add-to-date.spec.ts @@ -6,7 +6,7 @@ describe(`addToDate`, () => { const date = new Date(2023, 2, 27, 0, 0, 0, 0); const original = new Date(date.getTime()); - addToDate(date, 1, TimeUnit.Millisecond); + addToDate({ date, amount: 1, unit: TimeUnit.Millisecond }); expect(date).toEqual(original); }); @@ -17,19 +17,19 @@ describe(`addToDate`, () => { it('Adding a month will add the specified number of months to the date.', () => { const date = new Date('2023-02-28T00:00:00.000'); - expect(addToDate(date, 1, TimeUnit.Month)).toEqual(new Date('2023-03-28T00:00:00.000')); + expect(addToDate({ date, amount: 1, unit: TimeUnit.Month })).toEqual(new Date('2023-03-28T00:00:00.000')); }); it(`Should maintain hour when crossing into daylight savings time`, () => { const date = new Date('2023-03-25T06:00:00.000'); - expect(addToDate(date, 1, TimeUnit.Day).getHours()).toEqual(6); + expect(addToDate({ date, amount: 1, unit: TimeUnit.Day }).getHours()).toEqual(6); }); it(`Adding hours, minutes, seconds, or milliseconds, should result in a different hour`, () => { const date = new Date('2023-03-25T06:00:00.000'); - expect(addToDate(date, 24, TimeUnit.Hours).getHours()).toEqual(7); + expect(addToDate({ date, amount: 24, unit: TimeUnit.Hours }).getHours()).toEqual(7); }); }); }); diff --git a/src/dates/modifiers/add-to-date.ts b/src/dates/modifiers/add-to-date.ts index 86d6915..f8785a2 100644 --- a/src/dates/modifiers/add-to-date.ts +++ b/src/dates/modifiers/add-to-date.ts @@ -3,7 +3,12 @@ import { modifyDate } from './modify-date.js'; /** * Adds to a date by a given amount of time units + * + * @param date - The date to add to. + * @param amount - The amount of time units to add. + * @param unit - The unit of time to add. + * @returns The new date with the added time units. */ -export function addToDate(date: Date, amount: number, unit: TimeUnit): Date { - return modifyDate(date, amount, unit); +export function addToDate({ date, amount, unit }: { date: Date; amount: number; unit: TimeUnit }): Date { + return modifyDate({ date, amount, unit }); } diff --git a/src/dates/modifiers/date-modifiers-test-suites.ts b/src/dates/modifiers/date-modifiers-test-suites.ts index 8ad86e0..ef4b52a 100644 --- a/src/dates/modifiers/date-modifiers-test-suites.ts +++ b/src/dates/modifiers/date-modifiers-test-suites.ts @@ -1,4 +1,5 @@ import moment from 'moment'; +import { describe, expect, it } from 'vitest'; import { TimeUnit } from '../../../src/index.js'; interface DateModifierTestDefinition { @@ -58,7 +59,7 @@ const dateModifierTestSuites: DateModifierTestSuite[] = [ ]; export function generateDateModifierTestSuites( - fn: (date: Date, amount: number, unit: TimeUnit) => Date, + fn: (args: { date: Date; amount: number; unit: TimeUnit }) => Date, type: string, toFrom: string ): void { @@ -70,7 +71,7 @@ export function generateDateModifierTestSuites( const displayDescription = description.replace('%type%', type).replace('%to_from%', toFrom); it(displayDescription, () => { - const result = fn(new Date(date), amount, unit); + const result = fn({ date: new Date(date), amount, unit }); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call expect(result).toEqual(moment(date)[type](amount, unit).toDate()); diff --git a/src/dates/modifiers/modify-date.ts b/src/dates/modifiers/modify-date.ts index c6d79e4..7d224e0 100644 --- a/src/dates/modifiers/modify-date.ts +++ b/src/dates/modifiers/modify-date.ts @@ -1,10 +1,15 @@ -import { TimeUnit, unitToMS } from '../convert-units/index.js'; +import { TimeUnit, timeUnitToMilliseconds } from '../convert-units/index.js'; import { getEndOfMonth, getStartOfMonth } from '../getters/index.js'; /** * Modifies a date by a given amount of time units + * + * @param date - The date to modify. + * @param amount - The amount of time units to modify. + * @param unit - The unit of time to modify. + * @returns The new date with the modified time units. */ -export function modifyDate(date: Date, amount: number, unit: TimeUnit): Date { +export function modifyDate({ date, amount, unit }: { date: Date; amount: number; unit: TimeUnit }): Date { let newDate = new Date(date); amount = Math.round(amount); @@ -20,7 +25,7 @@ export function modifyDate(date: Date, amount: number, unit: TimeUnit): Date { case TimeUnit.Minutes: case TimeUnit.Hour: case TimeUnit.Hours: { - newDate = new Date(newDate.getTime() + unitToMS(amount, unit)); + newDate = new Date(newDate.getTime() + timeUnitToMilliseconds(amount, unit)); break; } case TimeUnit.Day: diff --git a/src/dates/modifiers/subtract-from-date.ts b/src/dates/modifiers/subtract-from-date.ts index 3cc884d..6b43fb7 100644 --- a/src/dates/modifiers/subtract-from-date.ts +++ b/src/dates/modifiers/subtract-from-date.ts @@ -3,7 +3,12 @@ import { modifyDate } from './modify-date.js'; /** * Subtracts from a date by a given amount of time units + * + * @param date - The date to subtract from. + * @param amount - The amount of time units to subtract. + * @param unit - The unit of time to subtract. + * @returns The new date with the subtracted time units. */ -export function subtractFromDate(date: Date, amount: number, unit: TimeUnit): Date { - return modifyDate(date, -amount, unit); +export function subtractFromDate({ date, amount, unit }: { date: Date; amount: number; unit: TimeUnit }): Date { + return modifyDate({ date, amount: -amount, unit }); } diff --git a/src/dates/setters/index.ts b/src/dates/setters/index.ts index b148aa7..6afdb54 100644 --- a/src/dates/setters/index.ts +++ b/src/dates/setters/index.ts @@ -1,14 +1,14 @@ -export * from './setEndOfDay.js'; -export * from './setEndOfHour.js'; -export * from './setEndOfMinute.js'; -export * from './setEndOfMonth.js'; -export * from './setEndOfSecond.js'; -export * from './setEndOfWeek.js'; -export * from './setEndOfYear.js'; -export * from './setStartOfDay.js'; -export * from './setStartOfHour.js'; -export * from './setStartOfMinute.js'; -export * from './setStartOfMonth.js'; -export * from './setStartOfSecond.js'; -export * from './setStartOfWeek.js'; -export * from './setStartOfYear.js'; +export * from './set-end-of-day.js'; +export * from './set-end-of-hour.js'; +export * from './set-end-of-minute.js'; +export * from './set-end-of-month.js'; +export * from './set-end-of-second.js'; +export * from './set-end-of-week.js'; +export * from './set-end-of-year.js'; +export * from './set-start-of-day.js'; +export * from './set-start-of-hour.js'; +export * from './set-start-of-minute.js'; +export * from './set-start-of-month.js'; +export * from './set-start-of-second.js'; +export * from './set-start-of-week.js'; +export * from './set-start-of-year.js'; diff --git a/src/dates/setters/setEndOfDay.spec.ts b/src/dates/setters/set-end-of-day.spec.ts similarity index 85% rename from src/dates/setters/setEndOfDay.spec.ts rename to src/dates/setters/set-end-of-day.spec.ts index f863690..b3d17e2 100644 --- a/src/dates/setters/setEndOfDay.spec.ts +++ b/src/dates/setters/set-end-of-day.spec.ts @@ -1,4 +1,4 @@ -import { setEndOfDay } from '../..'; +import { setEndOfDay } from './set-end-of-day.js'; describe('setEndOfDay', () => { it('hours should be set to 23, minutes and seconds to 59, and milliseconds to 999', () => { diff --git a/src/dates/setters/setEndOfDay.ts b/src/dates/setters/set-end-of-day.ts similarity index 52% rename from src/dates/setters/setEndOfDay.ts rename to src/dates/setters/set-end-of-day.ts index 8de592c..d168858 100644 --- a/src/dates/setters/setEndOfDay.ts +++ b/src/dates/setters/set-end-of-day.ts @@ -1,7 +1,10 @@ -import { setEndOfHour } from './index.js'; +import { setEndOfHour } from './set-end-of-hour.js'; /** * Takes a given date and mutates it to the end of the given day + * + * @param date Date instance to mutate. + * @returns The same Date instance, normalized to the end of the day. */ export function setEndOfDay(date: Date): Date { setEndOfHour(date); diff --git a/src/dates/setters/setEndOfHour.spec.ts b/src/dates/setters/set-end-of-hour.spec.ts similarity index 84% rename from src/dates/setters/setEndOfHour.spec.ts rename to src/dates/setters/set-end-of-hour.spec.ts index a0693ed..ac73caa 100644 --- a/src/dates/setters/setEndOfHour.spec.ts +++ b/src/dates/setters/set-end-of-hour.spec.ts @@ -1,4 +1,4 @@ -import { setEndOfHour } from '../..'; +import { setEndOfHour } from './set-end-of-hour.js'; describe('setEndOfHour', () => { it('minutes and seconds should be set to 59 and milliseconds to 999', () => { diff --git a/src/dates/setters/setEndOfHour.ts b/src/dates/setters/set-end-of-hour.ts similarity index 52% rename from src/dates/setters/setEndOfHour.ts rename to src/dates/setters/set-end-of-hour.ts index 7b64d84..c03d62a 100644 --- a/src/dates/setters/setEndOfHour.ts +++ b/src/dates/setters/set-end-of-hour.ts @@ -1,7 +1,10 @@ -import { setEndOfMinute } from './index.js'; +import { setEndOfMinute } from './set-end-of-minute.js'; /** * Takes a given date and mutates it to the end of the given hour + * + * @param date Date instance to mutate. + * @returns The same Date instance, normalized to the end of the hour. */ export function setEndOfHour(date: Date): Date { setEndOfMinute(date); diff --git a/src/dates/setters/setEndOfMinute.spec.ts b/src/dates/setters/set-end-of-minute.spec.ts similarity index 83% rename from src/dates/setters/setEndOfMinute.spec.ts rename to src/dates/setters/set-end-of-minute.spec.ts index 0ddc6a0..cffe72f 100644 --- a/src/dates/setters/setEndOfMinute.spec.ts +++ b/src/dates/setters/set-end-of-minute.spec.ts @@ -1,4 +1,4 @@ -import { setEndOfMinute } from '../..'; +import { setEndOfMinute } from './set-end-of-minute.js'; describe('setEndOfMinute', () => { it('seconds should be set to 59 and milliseconds to 999', () => { diff --git a/src/dates/setters/setEndOfMinute.ts b/src/dates/setters/set-end-of-minute.ts similarity index 52% rename from src/dates/setters/setEndOfMinute.ts rename to src/dates/setters/set-end-of-minute.ts index 24c7dd6..0b84571 100644 --- a/src/dates/setters/setEndOfMinute.ts +++ b/src/dates/setters/set-end-of-minute.ts @@ -1,7 +1,10 @@ -import { setEndOfSecond } from './index.js'; +import { setEndOfSecond } from './set-end-of-second.js'; /** * Takes a given date and mutates it to the end of the given minute + * + * @param date Date instance to mutate. + * @returns The same Date instance, normalized to the end of the minute. */ export function setEndOfMinute(date: Date): Date { setEndOfSecond(date); diff --git a/src/dates/setters/setEndOfMonth.spec.ts b/src/dates/setters/set-end-of-month.spec.ts similarity index 86% rename from src/dates/setters/setEndOfMonth.spec.ts rename to src/dates/setters/set-end-of-month.spec.ts index 83ff772..c0a1621 100644 --- a/src/dates/setters/setEndOfMonth.spec.ts +++ b/src/dates/setters/set-end-of-month.spec.ts @@ -1,4 +1,4 @@ -import { setEndOfMonth } from '../..'; +import { setEndOfMonth } from './set-end-of-month.js'; describe('setEndOfMonth', () => { it('hours should be set to 23, minutes and seconds to 59, milliseconds to 999, and day to the last of the month', () => { diff --git a/src/dates/setters/setEndOfMonth.ts b/src/dates/setters/set-end-of-month.ts similarity index 52% rename from src/dates/setters/setEndOfMonth.ts rename to src/dates/setters/set-end-of-month.ts index 3b0224a..335d467 100644 --- a/src/dates/setters/setEndOfMonth.ts +++ b/src/dates/setters/set-end-of-month.ts @@ -1,7 +1,11 @@ -import { setEndOfDay, setStartOfMonth } from './index.js'; +import { setEndOfDay } from './set-end-of-day.js'; +import { setStartOfMonth } from './set-start-of-month.js'; /** * Takes a given date and mutates it to the end of the given month + * + * @param date Date instance to mutate. + * @returns The same Date instance, normalized to the end of the month. */ export function setEndOfMonth(date: Date): Date { setStartOfMonth(date); diff --git a/src/dates/setters/setEndOfSecond.spec.ts b/src/dates/setters/set-end-of-second.spec.ts similarity index 82% rename from src/dates/setters/setEndOfSecond.spec.ts rename to src/dates/setters/set-end-of-second.spec.ts index a9f2627..f179009 100644 --- a/src/dates/setters/setEndOfSecond.spec.ts +++ b/src/dates/setters/set-end-of-second.spec.ts @@ -1,4 +1,4 @@ -import { setEndOfSecond } from '../..'; +import { setEndOfSecond } from './set-end-of-second.js'; describe('setEndOfSecond', () => { it('milliseconds should be set to 999', () => { diff --git a/src/dates/setters/setEndOfSecond.ts b/src/dates/setters/set-end-of-second.ts similarity index 60% rename from src/dates/setters/setEndOfSecond.ts rename to src/dates/setters/set-end-of-second.ts index 2efd1e3..c341ec7 100644 --- a/src/dates/setters/setEndOfSecond.ts +++ b/src/dates/setters/set-end-of-second.ts @@ -1,5 +1,8 @@ /** * Takes a given date and mutates it to the end of the given second + * + * @param date Date instance to mutate. + * @returns The same Date instance, normalized to the end of the second. */ export function setEndOfSecond(date: Date): Date { date.setMilliseconds(999); diff --git a/src/dates/setters/setEndOfWeek.spec.ts b/src/dates/setters/set-end-of-week.spec.ts similarity index 65% rename from src/dates/setters/setEndOfWeek.spec.ts rename to src/dates/setters/set-end-of-week.spec.ts index 22ca6fc..6777b92 100644 --- a/src/dates/setters/setEndOfWeek.spec.ts +++ b/src/dates/setters/set-end-of-week.spec.ts @@ -1,4 +1,4 @@ -import { setEndOfWeek } from '../..'; +import { setEndOfWeek } from './set-end-of-week.js'; describe('setEndOfWeek', () => { it('hours should be set to 23, minutes and seconds should be set to 59, milliseconds to 999, and day to last day of week', () => { @@ -16,4 +16,12 @@ describe('setEndOfWeek', () => { expect(setEndOfWeek(value)).toEqual(expected); }); + + it('On a Sunday it should stay on the same day (end of the same week)', () => { + const value = new Date(2020, 7, 30, 15, 31, 22, 123); + + const expected = new Date(2020, 7, 30, 23, 59, 59, 999); + + expect(setEndOfWeek(value)).toEqual(expected); + }); }); diff --git a/src/dates/setters/set-end-of-week.ts b/src/dates/setters/set-end-of-week.ts new file mode 100644 index 0000000..ed71e56 --- /dev/null +++ b/src/dates/setters/set-end-of-week.ts @@ -0,0 +1,14 @@ +import { setEndOfDay } from './set-end-of-day.js'; + +/** + * Takes a given date and mutates it to the end of the given week + * + * @param date Date instance to mutate. + * @returns The same Date instance, normalized to the end of the week. + */ +export function setEndOfWeek(date: Date): Date { + setEndOfDay(date); + + date.setDate(date.getDate() + ((7 - date.getDay()) % 7)); + return date; +} diff --git a/src/dates/setters/setEndOfYear.spec.ts b/src/dates/setters/set-end-of-year.spec.ts similarity index 86% rename from src/dates/setters/setEndOfYear.spec.ts rename to src/dates/setters/set-end-of-year.spec.ts index a3c53ee..970de47 100644 --- a/src/dates/setters/setEndOfYear.spec.ts +++ b/src/dates/setters/set-end-of-year.spec.ts @@ -1,4 +1,4 @@ -import { setEndOfYear } from '../..'; +import { setEndOfYear } from './set-end-of-year.js'; describe('setEndOfYear', () => { it('month should be set to 11, day to 31, hours to 23, minutes and seconds should be set to 59, and milliseconds to 999', () => { diff --git a/src/dates/setters/setEndOfYear.ts b/src/dates/setters/set-end-of-year.ts similarity index 55% rename from src/dates/setters/setEndOfYear.ts rename to src/dates/setters/set-end-of-year.ts index 049d6f0..d52a3a4 100644 --- a/src/dates/setters/setEndOfYear.ts +++ b/src/dates/setters/set-end-of-year.ts @@ -1,7 +1,10 @@ -import { setEndOfDay } from './index.js'; +import { setEndOfDay } from './set-end-of-day.js'; /** * Takes a given date and mutates it to the end of the given year + * + * @param date Date instance to mutate. + * @returns The same Date instance, normalized to the end of the year. */ export function setEndOfYear(date: Date): Date { setEndOfDay(date); diff --git a/src/dates/setters/setStartOfDay.spec.ts b/src/dates/setters/set-start-of-day.spec.ts similarity index 83% rename from src/dates/setters/setStartOfDay.spec.ts rename to src/dates/setters/set-start-of-day.spec.ts index ee8aa58..7b156f7 100644 --- a/src/dates/setters/setStartOfDay.spec.ts +++ b/src/dates/setters/set-start-of-day.spec.ts @@ -1,4 +1,4 @@ -import { setStartOfDay } from '../..'; +import { setStartOfDay } from './set-start-of-day.js'; describe('setStartOfDay', () => { it('milliseconds, seconds, minutes, and hour should be set to 0', () => { diff --git a/src/dates/setters/setStartOfDay.ts b/src/dates/setters/set-start-of-day.ts similarity index 52% rename from src/dates/setters/setStartOfDay.ts rename to src/dates/setters/set-start-of-day.ts index 7b20cc5..c446caa 100644 --- a/src/dates/setters/setStartOfDay.ts +++ b/src/dates/setters/set-start-of-day.ts @@ -1,7 +1,10 @@ -import { setStartOfHour } from './index.js'; +import { setStartOfHour } from './set-start-of-hour.js'; /** * Takes a given date and mutates it to the start of the given day + * + * @param date Date instance to mutate. + * @returns The same Date instance, normalized to the start of the day. */ export function setStartOfDay(date: Date): Date { setStartOfHour(date); diff --git a/src/dates/setters/setStartOfHour.spec.ts b/src/dates/setters/set-start-of-hour.spec.ts similarity index 83% rename from src/dates/setters/setStartOfHour.spec.ts rename to src/dates/setters/set-start-of-hour.spec.ts index 565e9a7..20078c4 100644 --- a/src/dates/setters/setStartOfHour.spec.ts +++ b/src/dates/setters/set-start-of-hour.spec.ts @@ -1,4 +1,4 @@ -import { setStartOfHour } from '../..'; +import { setStartOfHour } from './set-start-of-hour.js'; describe('setStartOfHour', () => { it('milliseconds, seconds, and minutes should be set to 0', () => { diff --git a/src/dates/setters/setStartOfHour.ts b/src/dates/setters/set-start-of-hour.ts similarity index 52% rename from src/dates/setters/setStartOfHour.ts rename to src/dates/setters/set-start-of-hour.ts index 4d87807..3e57dab 100644 --- a/src/dates/setters/setStartOfHour.ts +++ b/src/dates/setters/set-start-of-hour.ts @@ -1,7 +1,10 @@ -import { setStartOfMinute } from './index.js'; +import { setStartOfMinute } from './set-start-of-minute.js'; /** * Takes a given date and mutates it to the start of the given hour + * + * @param date Date instance to mutate. + * @returns The same Date instance, normalized to the start of the hour. */ export function setStartOfHour(date: Date): Date { setStartOfMinute(date); diff --git a/src/dates/setters/setStartOfMinute.spec.ts b/src/dates/setters/set-start-of-minute.spec.ts similarity index 82% rename from src/dates/setters/setStartOfMinute.spec.ts rename to src/dates/setters/set-start-of-minute.spec.ts index b25e0f8..7b8dc84 100644 --- a/src/dates/setters/setStartOfMinute.spec.ts +++ b/src/dates/setters/set-start-of-minute.spec.ts @@ -1,4 +1,4 @@ -import { setStartOfMinute } from '../..'; +import { setStartOfMinute } from './set-start-of-minute.js'; describe('setStartOfMinute', () => { it('milliseconds and seconds should be set to 0', () => { diff --git a/src/dates/setters/setStartOfMinute.ts b/src/dates/setters/set-start-of-minute.ts similarity index 52% rename from src/dates/setters/setStartOfMinute.ts rename to src/dates/setters/set-start-of-minute.ts index 9e2a2f6..5d759a2 100644 --- a/src/dates/setters/setStartOfMinute.ts +++ b/src/dates/setters/set-start-of-minute.ts @@ -1,7 +1,10 @@ -import { setStartOfSecond } from './index.js'; +import { setStartOfSecond } from './set-start-of-second.js'; /** * Takes a given date and mutates it to the start of the given minute + * + * @param date Date instance to mutate. + * @returns The same Date instance, normalized to the start of the minute. */ export function setStartOfMinute(date: Date): Date { setStartOfSecond(date); diff --git a/src/dates/setters/setStartOfMonth.spec.ts b/src/dates/setters/set-start-of-month.spec.ts similarity index 83% rename from src/dates/setters/setStartOfMonth.spec.ts rename to src/dates/setters/set-start-of-month.spec.ts index 7d92e05..2d22a00 100644 --- a/src/dates/setters/setStartOfMonth.spec.ts +++ b/src/dates/setters/set-start-of-month.spec.ts @@ -1,4 +1,4 @@ -import { setStartOfMonth } from '../..'; +import { setStartOfMonth } from './set-start-of-month.js'; describe('setStartOfMonth', () => { it('milliseconds, seconds, minutes, and hour should be set to 0 and day to 1', () => { diff --git a/src/dates/setters/setStartOfMonth.ts b/src/dates/setters/set-start-of-month.ts similarity index 52% rename from src/dates/setters/setStartOfMonth.ts rename to src/dates/setters/set-start-of-month.ts index f7d597a..dbc7b06 100644 --- a/src/dates/setters/setStartOfMonth.ts +++ b/src/dates/setters/set-start-of-month.ts @@ -1,7 +1,10 @@ -import { setStartOfDay } from './index.js'; +import { setStartOfDay } from './set-start-of-day.js'; /** * Takes a given date and mutates it to the start of the given month + * + * @param date Date instance to mutate. + * @returns The same Date instance, normalized to the start of the month. */ export function setStartOfMonth(date: Date): Date { setStartOfDay(date); diff --git a/src/dates/setters/setStartOfSecond.spec.ts b/src/dates/setters/set-start-of-second.spec.ts similarity index 81% rename from src/dates/setters/setStartOfSecond.spec.ts rename to src/dates/setters/set-start-of-second.spec.ts index e2e2734..cf812e0 100644 --- a/src/dates/setters/setStartOfSecond.spec.ts +++ b/src/dates/setters/set-start-of-second.spec.ts @@ -1,4 +1,4 @@ -import { setStartOfSecond } from '../..'; +import { setStartOfSecond } from './set-start-of-second.js'; describe('setStartOfSecond', () => { it('milliseconds should be set to 0', () => { diff --git a/src/dates/setters/setStartOfSecond.ts b/src/dates/setters/set-start-of-second.ts similarity index 59% rename from src/dates/setters/setStartOfSecond.ts rename to src/dates/setters/set-start-of-second.ts index 56c553d..0732080 100644 --- a/src/dates/setters/setStartOfSecond.ts +++ b/src/dates/setters/set-start-of-second.ts @@ -1,5 +1,8 @@ /** * Takes a given date and mutates it to the start of the given second + * + * @param date Date instance to mutate. + * @returns The same Date instance, normalized to the start of the second. */ export function setStartOfSecond(date: Date): Date { date.setMilliseconds(0); diff --git a/src/dates/setters/setStartOfWeek.spec.ts b/src/dates/setters/set-start-of-week.spec.ts similarity index 90% rename from src/dates/setters/setStartOfWeek.spec.ts rename to src/dates/setters/set-start-of-week.spec.ts index 2fa27e8..c81f70f 100644 --- a/src/dates/setters/setStartOfWeek.spec.ts +++ b/src/dates/setters/set-start-of-week.spec.ts @@ -1,4 +1,4 @@ -import { setStartOfWeek } from '../..'; +import { setStartOfWeek } from './set-start-of-week.js'; describe('setStartOfWeek', () => { it('milliseconds, seconds, minutes, and hour should be set to 0 and day to first day of week', () => { diff --git a/src/dates/setters/setStartOfWeek.ts b/src/dates/setters/set-start-of-week.ts similarity index 62% rename from src/dates/setters/setStartOfWeek.ts rename to src/dates/setters/set-start-of-week.ts index 6b9f718..72efab9 100644 --- a/src/dates/setters/setStartOfWeek.ts +++ b/src/dates/setters/set-start-of-week.ts @@ -1,7 +1,10 @@ -import { setStartOfDay } from './index.js'; +import { setStartOfDay } from './set-start-of-day.js'; /** * Takes a given date and mutates it to the start of the given week + * + * @param date Date instance to mutate. + * @returns The same Date instance, normalized to the start of the week. */ export function setStartOfWeek(date: Date): Date { const day = date.getDay(); diff --git a/src/dates/setters/setStartOfYear.spec.ts b/src/dates/setters/set-start-of-year.spec.ts similarity index 84% rename from src/dates/setters/setStartOfYear.spec.ts rename to src/dates/setters/set-start-of-year.spec.ts index 67cce36..e9349b8 100644 --- a/src/dates/setters/setStartOfYear.spec.ts +++ b/src/dates/setters/set-start-of-year.spec.ts @@ -1,4 +1,4 @@ -import { setStartOfYear } from '../..'; +import { setStartOfYear } from './set-start-of-year.js'; describe('setStartOfYear', () => { it('milliseconds, seconds, minutes, and hour should be set to 0 and day to 1', () => { diff --git a/src/dates/setters/setStartOfYear.ts b/src/dates/setters/set-start-of-year.ts similarity index 52% rename from src/dates/setters/setStartOfYear.ts rename to src/dates/setters/set-start-of-year.ts index dae3b25..7888036 100644 --- a/src/dates/setters/setStartOfYear.ts +++ b/src/dates/setters/set-start-of-year.ts @@ -1,7 +1,10 @@ -import { setStartOfMonth } from './index.js'; +import { setStartOfMonth } from './set-start-of-month.js'; /** * Takes a given date and mutates it to the start of the given Year + * + * @param date Date instance to mutate. + * @returns The same Date instance, normalized to the start of the year. */ export function setStartOfYear(date: Date): Date { setStartOfMonth(date); diff --git a/src/dates/setters/setEndOfWeek.ts b/src/dates/setters/setEndOfWeek.ts deleted file mode 100644 index cefb2f7..0000000 --- a/src/dates/setters/setEndOfWeek.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { setEndOfDay } from './index.js'; - -/** - * Takes a given date and mutates it to the end of the given week - */ -export function setEndOfWeek(date: Date): Date { - setEndOfDay(date); - - date.setDate(date.getDate() + (7 - date.getDay())); - - return date; -} diff --git a/src/dom/getAncestors.spec.ts b/src/dom/get-ancestors.spec.ts similarity index 100% rename from src/dom/getAncestors.spec.ts rename to src/dom/get-ancestors.spec.ts diff --git a/src/dom/getAncestors.ts b/src/dom/get-ancestors.ts similarity index 54% rename from src/dom/getAncestors.ts rename to src/dom/get-ancestors.ts index 5573d22..1d291b7 100644 --- a/src/dom/getAncestors.ts +++ b/src/dom/get-ancestors.ts @@ -1,7 +1,10 @@ -import { isNullOrUndefined } from '../type-predicates/isNullOrUndefined.js'; +import { isNullOrUndefined } from '../type-predicates/is-null-or-undefined.js'; /** * Gets all the elements that a given element is nested within + * + * @param element - The element to get the ancestors of. + * @returns An array of ancestor elements, starting from the immediate parent and going up to the root of the document. */ export function getAncestors(element: HTMLElement): HTMLElement[] { return _getAncestors(element); @@ -9,6 +12,10 @@ export function getAncestors(element: HTMLElement): HTMLElement[] { /** * Internal function for building the array and navigating up the tree + * + * @param element - The element to get the ancestors of. + * @param result - The array of ancestor elements being built. + * @returns An array of ancestor elements, starting from the immediate parent and going up to the root of the document. */ function _getAncestors(element: HTMLElement, result: HTMLElement[] = []): HTMLElement[] { const parent: HTMLElement | null = element.parentNode as HTMLElement | null; diff --git a/src/dom/getComputedStyleAsNumber.spec.ts b/src/dom/get-computed-style-as-number.spec.ts similarity index 85% rename from src/dom/getComputedStyleAsNumber.spec.ts rename to src/dom/get-computed-style-as-number.spec.ts index 5e01106..5421a6b 100644 --- a/src/dom/getComputedStyleAsNumber.spec.ts +++ b/src/dom/get-computed-style-as-number.spec.ts @@ -1,5 +1,5 @@ -import { createElement } from '../test-helpers/createElement.spec'; -import { getComputedStyleAsNumber } from './getComputedStyleAsNumber'; +import { createElement } from '../test-helpers/create-element.spec'; +import { getComputedStyleAsNumber } from './get-computed-style-as-number'; describe('getComputedStyleAsNumber', () => { it('should return the height of an element as a number', () => { diff --git a/src/dom/getComputedStyleAsNumber.ts b/src/dom/get-computed-style-as-number.ts similarity index 58% rename from src/dom/getComputedStyleAsNumber.ts rename to src/dom/get-computed-style-as-number.ts index 420711a..14eac6b 100644 --- a/src/dom/getComputedStyleAsNumber.ts +++ b/src/dom/get-computed-style-as-number.ts @@ -1,7 +1,11 @@ -import { isNullOrUndefined } from '../type-predicates/isNullOrUndefined.js'; +import { isNullOrUndefined } from '../type-predicates/is-null-or-undefined.js'; /** * Gets the computed style of an element as a number. + * + * @param element - The element to get the computed style of. + * @param style - The style property to get the computed value of ('height' or 'width'). + * @returns The computed style value as a number. */ export function getComputedStyleAsNumber(element: HTMLElement, style: 'height' | 'width'): number { const value = window.getComputedStyle(element)[style]; diff --git a/src/dom/getElementHeight.spec.ts b/src/dom/get-element-height.spec.ts similarity index 64% rename from src/dom/getElementHeight.spec.ts rename to src/dom/get-element-height.spec.ts index 0557380..642d3f8 100644 --- a/src/dom/getElementHeight.spec.ts +++ b/src/dom/get-element-height.spec.ts @@ -1,5 +1,5 @@ -import { createElement } from '../test-helpers/createElement.spec'; -import { getElementHeight } from './getElementHeight'; +import { createElement } from '../test-helpers/create-element.spec'; +import { getElementHeight } from './get-element-height'; describe('getElementHeight', () => { it('should return the height of an element as a number', () => { diff --git a/src/dom/getElementHeight.ts b/src/dom/get-element-height.ts similarity index 66% rename from src/dom/getElementHeight.ts rename to src/dom/get-element-height.ts index dfaf65e..575483d 100644 --- a/src/dom/getElementHeight.ts +++ b/src/dom/get-element-height.ts @@ -1,10 +1,13 @@ import { isJsdom } from '../constants/index.js'; -import { getComputedStyleAsNumber } from './getComputedStyleAsNumber.js'; +import { getComputedStyleAsNumber } from './get-computed-style-as-number.js'; /** * Get the height of an element. * * This helper allows getting of the height when running in a JSDOM environment. + * + * @param element - The element to get the height of. + * @returns The height of the element as a number. */ export function getElementHeight(element: HTMLElement): number { if (isJsdom) { diff --git a/src/dom/getElementWidth.spec.ts b/src/dom/get-element-width.spec.ts similarity index 64% rename from src/dom/getElementWidth.spec.ts rename to src/dom/get-element-width.spec.ts index 944f906..af7a24e 100644 --- a/src/dom/getElementWidth.spec.ts +++ b/src/dom/get-element-width.spec.ts @@ -1,5 +1,5 @@ -import { createElement } from '../test-helpers/createElement.spec'; -import { getElementWidth } from './getElementWidth'; +import { createElement } from '../test-helpers/create-element.spec'; +import { getElementWidth } from './get-element-width'; describe('getElementWidth', () => { it('should return the width of an element as a number', () => { diff --git a/src/dom/getElementWidth.ts b/src/dom/get-element-width.ts similarity index 66% rename from src/dom/getElementWidth.ts rename to src/dom/get-element-width.ts index 98622c6..10d4858 100644 --- a/src/dom/getElementWidth.ts +++ b/src/dom/get-element-width.ts @@ -1,10 +1,13 @@ import { isJsdom } from '../constants/index.js'; -import { getComputedStyleAsNumber } from './getComputedStyleAsNumber.js'; +import { getComputedStyleAsNumber } from './get-computed-style-as-number.js'; /** * Get the width of an element. * * This helper allows getting of the width when running in a JSDOM environment. + * + * @param element - The element to get the width of. + * @returns The width of the element as a number. */ export function getElementWidth(element: HTMLElement): number { if (isJsdom) { diff --git a/src/dom/getNonInlineParent.spec.ts b/src/dom/get-non-inline-parent.spec.ts similarity index 94% rename from src/dom/getNonInlineParent.spec.ts rename to src/dom/get-non-inline-parent.spec.ts index bc470dc..4347852 100644 --- a/src/dom/getNonInlineParent.spec.ts +++ b/src/dom/get-non-inline-parent.spec.ts @@ -1,5 +1,5 @@ import { getNonInlineParent } from '..'; -import { createElement } from '../test-helpers/createElement.spec'; +import { createElement } from '../test-helpers/create-element.spec'; describe('getNonInlineParent', () => { describe('isDisplayInline', () => { diff --git a/src/dom/getNonInlineParent.ts b/src/dom/get-non-inline-parent.ts similarity index 55% rename from src/dom/getNonInlineParent.ts rename to src/dom/get-non-inline-parent.ts index ca53bdd..0c07f1a 100644 --- a/src/dom/getNonInlineParent.ts +++ b/src/dom/get-non-inline-parent.ts @@ -1,8 +1,11 @@ -import { isNullOrUndefined } from '../type-predicates/isNullOrUndefined.js'; -import { isDisplayInline } from './isDisplayInline.js'; +import { isNullOrUndefined } from '../type-predicates/is-null-or-undefined.js'; +import { isDisplayInline } from './is-display-inline.js'; /** * Gets the first parent of an element that isn't `display: inline`. Returns null if no matching element + * + * @param element - The element to get the non-inline parent of. + * @returns The first parent element that isn't `display: inline`, or null if no matching element is found. */ export function getNonInlineParent(element: Element): Element | null { const parent = element.parentElement; diff --git a/src/dom/getPositionedParent.spec.ts b/src/dom/get-positioned-parent.spec.ts similarity index 96% rename from src/dom/getPositionedParent.spec.ts rename to src/dom/get-positioned-parent.spec.ts index a82eb68..5091b04 100644 --- a/src/dom/getPositionedParent.spec.ts +++ b/src/dom/get-positioned-parent.spec.ts @@ -1,5 +1,5 @@ import { getPositionedParent } from '..'; -import { createElement } from '../test-helpers/createElement.spec'; +import { createElement } from '../test-helpers/create-element.spec'; describe('getPositionedParent', () => { it('should return the first position relative parent (no nesting)', () => { diff --git a/src/dom/getPositionedParent.ts b/src/dom/get-positioned-parent.ts similarity index 70% rename from src/dom/getPositionedParent.ts rename to src/dom/get-positioned-parent.ts index 7c307a7..f141b12 100644 --- a/src/dom/getPositionedParent.ts +++ b/src/dom/get-positioned-parent.ts @@ -1,5 +1,8 @@ /** * Gets the first parent element with a relative or absolute position + * + * @param element - The element to get the positioned parent of. + * @returns The first parent element with a relative or absolute position, or the document's root element if no such parent is found. */ export function getPositionedParent(element: HTMLElement): HTMLElement { const parent: HTMLElement | null = element.parentElement; diff --git a/src/dom/getScrollParent.spec.ts b/src/dom/get-scroll-parent.spec.ts similarity index 70% rename from src/dom/getScrollParent.spec.ts rename to src/dom/get-scroll-parent.spec.ts index 2eba6f6..b13108c 100644 --- a/src/dom/getScrollParent.spec.ts +++ b/src/dom/get-scroll-parent.spec.ts @@ -1,86 +1,86 @@ -import { getScrollParent } from '..'; +import { getScrollParent } from './get-scroll-parent.js'; describe('getScrollParent', () => { it('should get the scroll parent for an element with scrolling on the x and y axis', () => { const { parent, child } = createScrollElements(true, true); - expect(getScrollParent(child)).toEqual(parent); + expect(getScrollParent({ element: child })).toEqual(parent); }); it('should get the scroll parent for an element with scrolling on the x axis', () => { const { parent, child } = createScrollElements(true, false); - expect(getScrollParent(child)).toEqual(parent); + expect(getScrollParent({ element: child })).toEqual(parent); }); it('should get the scroll parent for an element with scrolling on the y axis', () => { const { parent, child } = createScrollElements(false, true); - expect(getScrollParent(child)).toEqual(parent); + expect(getScrollParent({ element: child })).toEqual(parent); }); it('should get the scroll parent for an element that smaller than the scroll area', () => { const { parent, child } = createScrollElements(false, false); - expect(getScrollParent(child)).toEqual(parent); + expect(getScrollParent({ element: child })).toEqual(parent); }); it('should get the scroll parent for an element with scrolling on the x and y axis', () => { const { parent, child } = createScrollElements(true, true); - expect(getScrollParent(child, false)).toEqual(parent); + expect(getScrollParent({ element: child, x: false })).toEqual(parent); }); it('should get the scroll parent for an element with scrolling on the x axis', () => { const { parent, child } = createScrollElements(true, false); - expect(getScrollParent(child, false)).toEqual(parent); + expect(getScrollParent({ element: child, x: false })).toEqual(parent); }); it('should get the scroll parent for an element with scrolling on the y axis', () => { const { parent, child } = createScrollElements(false, true); - expect(getScrollParent(child, false)).toEqual(parent); + expect(getScrollParent({ element: child, x: false })).toEqual(parent); }); it('should get the scroll parent for an element that smaller than the scroll area', () => { const { parent, child } = createScrollElements(false, false); - expect(getScrollParent(child, false)).toEqual(parent); + expect(getScrollParent({ element: child, x: false })).toEqual(parent); }); it('should get the scroll parent for an element with scrolling on the x and y axis', () => { const { parent, child } = createScrollElements(true, true); - expect(getScrollParent(child, true, false)).toEqual(parent); + expect(getScrollParent({ element: child, x: true, y: false })).toEqual(parent); }); it('should get the scroll parent for an element with scrolling on the x axis', () => { const { parent, child } = createScrollElements(true, false); - expect(getScrollParent(child, true, false)).toEqual(parent); + expect(getScrollParent({ element: child, x: true, y: false })).toEqual(parent); }); it('should get the scroll parent for an element with scrolling on the y axis', () => { const { parent, child } = createScrollElements(false, true); - expect(getScrollParent(child, true, false)).toEqual(parent); + expect(getScrollParent({ element: child, x: true, y: false })).toEqual(parent); }); it('should get the scroll parent for an element that smaller than the scroll area', () => { const { parent, child } = createScrollElements(false, false); - expect(getScrollParent(child, true, false)).toEqual(parent); + expect(getScrollParent({ element: child, x: true, y: false })).toEqual(parent); }); it('should get the scroll parent for an element with scrolling on the x and y axis', () => { const { child } = createScrollElements(true, true, true); - expect(getScrollParent(child)).toEqual(document.body); + expect(getScrollParent({ element: child })?.tagName).toEqual(document.body.tagName); }); it('should return null when null provided as element', () => { - expect(getScrollParent(null)).toEqual(null); + expect(getScrollParent({ element: null })).toEqual(null); }); }); @@ -89,8 +89,10 @@ function createScrollElements(x: boolean, y: boolean, noScroll?: boolean): { par const parent = document.createElement('div'); if (!noScroll) { - parent.style.overflow = 'auto'; + parent.style.overflowX = 'auto'; + parent.style.overflowY = 'auto'; } + parent.style.width = '200px'; parent.style.height = '200px'; diff --git a/src/dom/get-scroll-parent.ts b/src/dom/get-scroll-parent.ts new file mode 100644 index 0000000..1c488e3 --- /dev/null +++ b/src/dom/get-scroll-parent.ts @@ -0,0 +1,35 @@ +const visibilityRegex = new RegExp('^(visible|hidden)'); + +/** + * Returns the nearest scrollable parent element of the given element. + * + * @param element The element for which to find the scrollable parent. + * @param x Whether to consider horizontal scrolling (default: true). + * @param y Whether to consider vertical scrolling (default: true). + * @returns The nearest scrollable parent element or the document body if none is found. + */ +export function getScrollParent({ + element, + x = true, + y = true, +}: { + element: HTMLElement | null; + x?: boolean; + y?: boolean; +}): HTMLElement | null { + if (!element) { + return null; + } + + const computedStyle = window.getComputedStyle(element); + + if (x && !visibilityRegex.test(computedStyle.overflowX) && element.scrollWidth >= element.clientWidth) { + return element; + } + + if (y && !visibilityRegex.test(computedStyle.overflowY) && element.scrollHeight >= element.clientHeight) { + return element; + } + + return getScrollParent({ element: element.parentElement, x, y }) ?? document.body; +} diff --git a/src/dom/getScrollParent.ts b/src/dom/getScrollParent.ts deleted file mode 100644 index 35f8383..0000000 --- a/src/dom/getScrollParent.ts +++ /dev/null @@ -1,24 +0,0 @@ -const visibilityRegex = new RegExp('^(visible|hidden)'); - -/** - * Gets the scrollable parent element of a given element - * @param x: Optional. Whether to check if the element can scroll on the x axis. Default: true - * @param y: Optional. Whether to check if the element can scroll on the y axis. Default: true - */ -export function getScrollParent(element: HTMLElement | null, x = true, y = true): HTMLElement | null { - if (!element) { - return null; - } - - const computedStyle = window.getComputedStyle(element); - - if (x && !visibilityRegex.test(computedStyle.overflowX) && element.scrollWidth >= element.clientWidth) { - return element; - } - - if (y && !visibilityRegex.test(computedStyle.overflowY) && element.scrollHeight >= element.clientHeight) { - return element; - } - - return getScrollParent(element.parentElement, x, y) || document.body; -} diff --git a/src/dom/index.ts b/src/dom/index.ts index 567f2e4..e970a79 100644 --- a/src/dom/index.ts +++ b/src/dom/index.ts @@ -1,9 +1,9 @@ -export * from './getAncestors.js'; -export * from './getComputedStyleAsNumber.js'; -export * from './getElementHeight.js'; -export * from './getElementWidth.js'; -export * from './getNonInlineParent.js'; -export * from './getPositionedParent.js'; -export * from './getScrollParent.js'; -export * from './isDisplayInline.js'; -export * from './isVisible.js'; +export * from './get-ancestors.js'; +export * from './get-computed-style-as-number.js'; +export * from './get-element-height.js'; +export * from './get-element-width.js'; +export * from './get-non-inline-parent.js'; +export * from './get-positioned-parent.js'; +export * from './get-scroll-parent.js'; +export * from './is-display-inline.js'; +export * from './is-visible.js'; diff --git a/src/dom/isDisplayInline.spec.ts b/src/dom/is-display-inline.spec.ts similarity index 92% rename from src/dom/isDisplayInline.spec.ts rename to src/dom/is-display-inline.spec.ts index 14638e4..85fcd7e 100644 --- a/src/dom/isDisplayInline.spec.ts +++ b/src/dom/is-display-inline.spec.ts @@ -1,5 +1,5 @@ import { isDisplayInline } from '..'; -import { createElement } from '../test-helpers/createElement.spec'; +import { createElement } from '../test-helpers/create-element.spec'; describe('isDisplayInline', () => { it('should return true for a text node', () => { diff --git a/src/dom/isDisplayInline.ts b/src/dom/is-display-inline.ts similarity index 72% rename from src/dom/isDisplayInline.ts rename to src/dom/is-display-inline.ts index bb6c84c..0749258 100644 --- a/src/dom/isDisplayInline.ts +++ b/src/dom/is-display-inline.ts @@ -1,5 +1,8 @@ /** * Return whether an element is `display: inline` + * + * @param element - The element to check. + * @returns True if the element is `display: inline`, false otherwise. */ export function isDisplayInline(element: Element): boolean { if (element.nodeType === 3) { diff --git a/src/dom/isVisible.spec.ts b/src/dom/is-visible.spec.ts similarity index 99% rename from src/dom/isVisible.spec.ts rename to src/dom/is-visible.spec.ts index 07f3550..7a125b7 100644 --- a/src/dom/isVisible.spec.ts +++ b/src/dom/is-visible.spec.ts @@ -1,6 +1,6 @@ import { getViewportDetails } from 'viewport-details'; import { isVisible } from '..'; -import { createElement } from '../test-helpers/createElement.spec'; +import { createElement } from '../test-helpers/create-element.spec'; describe('isVisible', () => { beforeAll(() => { diff --git a/src/dom/isVisible.ts b/src/dom/is-visible.ts similarity index 94% rename from src/dom/isVisible.ts rename to src/dom/is-visible.ts index 35308ad..62fb55e 100644 --- a/src/dom/isVisible.ts +++ b/src/dom/is-visible.ts @@ -4,6 +4,9 @@ import { getAncestors } from './index.js'; /** * Return whether an element is practically visible, considering things like dimensions of 0, opacity, ``visibility: hidden`` and * ``overflow: hidden``, and whether the item is scrolled off screen + * + * @param element - The element to check. + * @returns True if the element is practically visible, false otherwise. */ export function isVisible(element: HTMLElement): boolean { const rect: DOMRect = element.getBoundingClientRect(); diff --git a/src/format-time/format-time-options.ts b/src/format-time/format-time-options.ts index 3b87894..e009e1a 100644 --- a/src/format-time/format-time-options.ts +++ b/src/format-time/format-time-options.ts @@ -4,37 +4,37 @@ export interface FormatTimeOptionsComplete { /** * Whether to force all units to be displayed */ - forceAllUnits: boolean; + readonly forceAllUnits: boolean; /** * The time unit that is being provided */ - timeUnit: TimeUnit; + readonly timeUnit: TimeUnit; /** * The number of decimal places to display for seconds */ - secondsDecimalPlaces: number; + readonly secondsDecimalPlaces: number; /** * Whether to pad decimals with 0s to match the number provided for secondsDecimalPlaces */ - padDecimals: boolean; + readonly padDecimals: boolean; /** * The suffix to use for hours */ - hourSuffix: string; + readonly hourSuffix: string; /** * The suffix to use for minutes */ - minuteSuffix: string; + readonly minuteSuffix: string; /** * The suffix to use for seconds */ - secondSuffix: string; + readonly secondSuffix: string; } export type FormatTimeOptions = Partial; diff --git a/src/format-time/format-time.ts b/src/format-time/format-time.ts index 3282931..4768b68 100644 --- a/src/format-time/format-time.ts +++ b/src/format-time/format-time.ts @@ -1,9 +1,13 @@ -import { TimeUnit, convertTimeUnit, unitToMS } from '../dates/index.js'; -import { isNullOrUndefined } from '../type-predicates/isNullOrUndefined.js'; +import { TimeUnit, convertTimeUnit, timeUnitToMilliseconds } from '../dates/index.js'; +import { isNullOrUndefined } from '../type-predicates/is-null-or-undefined.js'; import { FormatTimeOptions, FormatTimeOptionsComplete } from './format-time-options.js'; /** * Formats a given time to a human readable string + * + * @param time - The time to format + * @param options - The options to use for formatting the time + * @returns The formatted time string */ export function formatTime(time: number, options?: FormatTimeOptions): string { const defaultOptions: FormatTimeOptionsComplete = { @@ -21,31 +25,36 @@ export function formatTime(time: number, options?: FormatTimeOptions): string { ...options, }; - const timeMs = unitToMS(time, timeUnit); + const timeMs = timeUnitToMilliseconds(time, timeUnit); const result: Array = []; - const hours = Math.floor(convertTimeUnit(timeMs, TimeUnit.Milliseconds, TimeUnit.Hours)); + const hours = Math.floor(convertTimeUnit({ value: timeMs, sourceUnit: TimeUnit.Milliseconds, resultUnit: TimeUnit.Hours })); if (forceAllUnits || hours > 0) { result.push(formatUnit(hours, hourSuffix, forceAllUnits)); } let minutes = Math.floor( - convertTimeUnit(timeMs - convertTimeUnit(hours, TimeUnit.Hours, TimeUnit.Milliseconds), TimeUnit.Milliseconds, TimeUnit.Minutes) + convertTimeUnit({ + value: timeMs - convertTimeUnit({ value: hours, sourceUnit: TimeUnit.Hours, resultUnit: TimeUnit.Milliseconds }), + sourceUnit: TimeUnit.Milliseconds, + resultUnit: TimeUnit.Minutes, + }) ); const secondsMultiplier = Math.pow(10, secondsDecimalPlaces); let seconds = Math.round( - convertTimeUnit( - timeMs - - convertTimeUnit(hours, TimeUnit.Hours, TimeUnit.Milliseconds) - - convertTimeUnit(minutes, TimeUnit.Minutes, TimeUnit.Milliseconds), - TimeUnit.Milliseconds, - TimeUnit.Seconds - ) * secondsMultiplier + convertTimeUnit({ + value: + timeMs - + convertTimeUnit({ value: hours, sourceUnit: TimeUnit.Hours, resultUnit: TimeUnit.Milliseconds }) - + convertTimeUnit({ value: minutes, sourceUnit: TimeUnit.Minutes, resultUnit: TimeUnit.Milliseconds }), + sourceUnit: TimeUnit.Milliseconds, + resultUnit: TimeUnit.Seconds, + }) * secondsMultiplier ) / secondsMultiplier; if (seconds === 60) { diff --git a/src/freeze/freeze.ts b/src/freeze/freeze.ts index 181b3ad..3012f04 100644 --- a/src/freeze/freeze.ts +++ b/src/freeze/freeze.ts @@ -13,6 +13,10 @@ import { typeOf, ValueType } from '../type-predicates/index.js'; * function returns a readonly `Proxy` that throws on common mutators. * - If other code still holds a reference to the original mutable object, it can mutate it directly; `freeze()` cannot * prevent that. + * + * @template T - The type of the value to freeze. + * @param value - The value to freeze. + * @returns A readonly version of the value, with nested values frozen where possible. */ export function freeze(value: T): Readonly { const ctx: FreezeContext = { diff --git a/src/index.ts b/src/index.ts index faad4a5..85f687e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ export * from './difference/difference.js'; export * from './dom/index.js'; export * from './format-time/index.js'; export * from './freeze/freeze.js'; -export * from './insertAtIndex/insertAtIndex.js'; +export * from './insert-at-index/insert-at-index.js'; export * from './math/index.js'; export * from './merge/merge.js'; export * from './sorting/sort-by.js'; diff --git a/src/insertAtIndex/insertAtIndex.spec.ts b/src/insert-at-index/insert-at-index.spec.ts similarity index 77% rename from src/insertAtIndex/insertAtIndex.spec.ts rename to src/insert-at-index/insert-at-index.spec.ts index b9b8519..8ed9f91 100644 --- a/src/insertAtIndex/insertAtIndex.spec.ts +++ b/src/insert-at-index/insert-at-index.spec.ts @@ -6,6 +6,6 @@ describe(`insertAtIndex`, () => { const value = `/`; const index = 1; - expect(insertAtIndex(source, value, index)).toBe(``); + expect(insertAtIndex({ source, value, index })).toBe(``); }); }); diff --git a/src/insert-at-index/insert-at-index.ts b/src/insert-at-index/insert-at-index.ts new file mode 100644 index 0000000..f18d010 --- /dev/null +++ b/src/insert-at-index/insert-at-index.ts @@ -0,0 +1,11 @@ +/** + * Inserts a string value at a given index in a source string + * + * @param source - The source string to insert into. + * @param value - The string value to insert. + * @param index - The index at which to insert the value. + * @returns The new string with the value inserted at the specified index. + */ +export function insertAtIndex({ source, value, index }: { source: string; value: string; index: number }): string { + return `${source.slice(0, index)}${value}${source.slice(index)}`; +} diff --git a/src/insertAtIndex/insertAtIndex.ts b/src/insertAtIndex/insertAtIndex.ts deleted file mode 100644 index 6e10b64..0000000 --- a/src/insertAtIndex/insertAtIndex.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Inserts a string value at a given index in a source string - */ -export function insertAtIndex(source: string, value: string, index: number): string { - return `${source.slice(0, index)}${value}${source.slice(index)}`; -} diff --git a/src/math/average.ts b/src/math/average.ts index 9b536ea..a968393 100644 --- a/src/math/average.ts +++ b/src/math/average.ts @@ -1,5 +1,11 @@ import { sum } from './sum.js'; +/** + * Calculates the average of an array of numbers. + * + * @param values - The array of numbers to calculate the average of. + * @returns The average of the numbers in the array. + */ export function average(values: number[]): number { const length = values.length; diff --git a/src/math/index.ts b/src/math/index.ts index 4d8f669..0b2528e 100644 --- a/src/math/index.ts +++ b/src/math/index.ts @@ -1,3 +1,3 @@ export * from './average.js'; -export * from './randomNumberBetweenRange.js'; +export * from './random-number-between-range.js'; export * from './sum.js'; diff --git a/src/math/randomNumberBetweenRange.spec.ts b/src/math/random-number-between-range.spec.ts similarity index 95% rename from src/math/randomNumberBetweenRange.spec.ts rename to src/math/random-number-between-range.spec.ts index 8456b26..3f1107a 100644 --- a/src/math/randomNumberBetweenRange.spec.ts +++ b/src/math/random-number-between-range.spec.ts @@ -1,5 +1,5 @@ import { numberArrayFromRange } from 'number-array-from-range'; -import { randomNumberBetweenRange } from './randomNumberBetweenRange'; +import { randomNumberBetweenRange } from './random-number-between-range'; describe('randomNumberBetweenRange', () => { it('should generate a random number between two positive numbers', () => { diff --git a/src/math/randomNumberBetweenRange.ts b/src/math/random-number-between-range.ts similarity index 99% rename from src/math/randomNumberBetweenRange.ts rename to src/math/random-number-between-range.ts index ef86bbd..ef0431c 100644 --- a/src/math/randomNumberBetweenRange.ts +++ b/src/math/random-number-between-range.ts @@ -31,5 +31,6 @@ export function randomNumberBetweenRange(min: number, max: number): number { } const randomOffset = Math.floor(Math.random() * range); + return randomOffset + minCeil; } diff --git a/src/math/sum.ts b/src/math/sum.ts index 22cd194..a96aa86 100644 --- a/src/math/sum.ts +++ b/src/math/sum.ts @@ -1,5 +1,8 @@ /** - * Calculates the sum of the given values. + * Calculates the sum of an array of numbers. + * + * @param values - The array of numbers to calculate the sum of. + * @returns The sum of the numbers in the array. */ export function sum(values: number[]): number { return values.reduce((acc, val) => acc + val, 0); diff --git a/src/merge/mergeTestDefinitions.spec.ts b/src/merge/merge-test-definitions.spec.ts similarity index 100% rename from src/merge/mergeTestDefinitions.spec.ts rename to src/merge/merge-test-definitions.spec.ts diff --git a/src/merge/merge.spec.ts b/src/merge/merge.spec.ts index d400c30..d98a637 100644 --- a/src/merge/merge.spec.ts +++ b/src/merge/merge.spec.ts @@ -1,5 +1,5 @@ import { merge } from './merge'; -import { mergeTests } from './mergeTestDefinitions.spec'; +import { mergeTests } from './merge-test-definitions.spec'; describe(`merge`, () => { for (const { description, target, source, expected, notEqual } of mergeTests) { diff --git a/src/merge/merge.ts b/src/merge/merge.ts index fd1d246..8547930 100644 --- a/src/merge/merge.ts +++ b/src/merge/merge.ts @@ -1,4 +1,4 @@ -import { isMergeableObject as defaultIsMergeableObject } from '../type-predicates/isMergeableObject.js'; +import { isMergeableObject as defaultIsMergeableObject } from '../type-predicates/is-mergeable-object.js'; interface MergeOptions { /** diff --git a/src/sorting/sort-by.ts b/src/sorting/sort-by.ts index 52b6f80..30d5f42 100644 --- a/src/sorting/sort-by.ts +++ b/src/sorting/sort-by.ts @@ -1,4 +1,4 @@ -import { isNullOrUndefined } from '../type-predicates/isNullOrUndefined.js'; +import { isNullOrUndefined } from '../type-predicates/is-null-or-undefined.js'; import { SortMappingFunction } from './sort-mapping-function.model.js'; /** diff --git a/src/strings/capitalise-first.ts b/src/strings/capitalise-first.ts index 075ffe3..f7437cd 100644 --- a/src/strings/capitalise-first.ts +++ b/src/strings/capitalise-first.ts @@ -2,8 +2,10 @@ import { capitalise } from './capitalise.js'; /** * Capitalises the first n characters of a string + * * @param value the string to capitalise * @param number the number of characters to capitalise (default 1) + * @returns the capitalised string */ export function capitaliseFirst(value: string, number = 1): string { return capitalise(value, { end: number }); diff --git a/src/strings/capitalise-last.ts b/src/strings/capitalise-last.ts index 2e08b02..5e70764 100644 --- a/src/strings/capitalise-last.ts +++ b/src/strings/capitalise-last.ts @@ -2,8 +2,10 @@ import { capitalise } from './capitalise.js'; /** * Capitalises the last n characters of a string + * * @param value the string to capitalise * @param number the number of characters to capitalise (default 1) + * @returns the capitalised string */ export function capitaliseLast(value: string, number = 1): string { const start = value.length - number; diff --git a/src/strings/capitalise.ts b/src/strings/capitalise.ts index 364722f..32b7b15 100644 --- a/src/strings/capitalise.ts +++ b/src/strings/capitalise.ts @@ -5,6 +5,10 @@ export interface CapitaliseOptions { /** * Capitalises the characters of a provided string between the given start and end indexes + * + * @param value the string to capitalise + * @param options the options to use for capitalising the string + * @returns the capitalised string */ export function capitalise(value: string, options?: Partial): string { const { length } = value; diff --git a/src/strings/kebab-to-pascal.ts b/src/strings/kebab-to-pascal.ts index e40c416..e87be1e 100644 --- a/src/strings/kebab-to-pascal.ts +++ b/src/strings/kebab-to-pascal.ts @@ -2,6 +2,9 @@ import { capitaliseFirst } from './capitalise-first.js'; /** * Converts a kebab-case word to PascalCase + * + * @param value the kebab-case word to convert + * @returns the PascalCase version of the word */ export function kebabToPascal(value: string): string { return value diff --git a/src/test-helpers/createElement.spec.ts b/src/test-helpers/create-element.spec.ts similarity index 100% rename from src/test-helpers/createElement.spec.ts rename to src/test-helpers/create-element.spec.ts diff --git a/src/type-predicates/index.ts b/src/type-predicates/index.ts index 0a36006..518b87e 100644 --- a/src/type-predicates/index.ts +++ b/src/type-predicates/index.ts @@ -1,20 +1,19 @@ -export * from './isArguments.js'; -export * from './isArray.js'; -export * from './isBoolean.js'; -export * from './isBuffer.js'; -export * from './isDate.js'; -export * from './isEmpty.js'; -export * from './isEqual.js'; -export * from './isError.js'; -export * from './isGeneratorObject.js'; -export * from './isMergeableObject.js'; -export * from './isMoment.js'; -export * from './isNaNStrict.js'; -export * from './isNullOrUndefined.js'; -export * from './isNumber.js'; -export * from './isObject.js'; -export * from './isPlainObject.js'; -export * from './isReactElement.js'; -export * from './isRegExp.js'; -export * from './isString.js'; -export * from './typeOf.js'; +export * from './is-arguments.js'; +export * from './is-array.js'; +export * from './is-boolean.js'; +export * from './is-buffer.js'; +export * from './is-date.js'; +export * from './is-empty.js'; +export * from './is-equal.js'; +export * from './is-error.js'; +export * from './is-generator-object.js'; +export * from './is-mergeable-object.js'; +export * from './is-nan-strict.js'; +export * from './is-null-or-undefined.js'; +export * from './is-number.js'; +export * from './is-object.js'; +export * from './is-plain-object.js'; +export * from './is-react-element.js'; +export * from './is-regexp.js'; +export * from './is-string.js'; +export * from './type-of.js'; diff --git a/src/type-predicates/isArguments.spec.ts b/src/type-predicates/is-arguments.spec.ts similarity index 66% rename from src/type-predicates/isArguments.spec.ts rename to src/type-predicates/is-arguments.spec.ts index b5b43aa..be765cd 100644 --- a/src/type-predicates/isArguments.spec.ts +++ b/src/type-predicates/is-arguments.spec.ts @@ -1,14 +1,9 @@ -/* eslint-disable prefer-rest-params */ -import { isArguments } from './isArguments'; - -const strictArgs = (function (_a, _b, _c) { - 'use strict'; - return arguments; -})(1, 2, 3); +import { isArguments } from './is-arguments.js'; describe(`isArguments`, () => { it(`should return true if value is an Arguments object`, () => { const value = (function () { + // eslint-disable-next-line prefer-rest-params return arguments; })(); @@ -16,6 +11,12 @@ describe(`isArguments`, () => { }); it(`should return true if value is an Arguments object (string)`, () => { + const strictArgs = (function (_a, _b, _c) { + 'use strict'; + // eslint-disable-next-line prefer-rest-params + return arguments; + })(1, 2, 3); + expect(isArguments(strictArgs)).toBe(true); }); diff --git a/src/type-predicates/isArguments.ts b/src/type-predicates/is-arguments.ts similarity index 100% rename from src/type-predicates/isArguments.ts rename to src/type-predicates/is-arguments.ts diff --git a/src/type-predicates/isArray.spec.ts b/src/type-predicates/is-array.spec.ts similarity index 87% rename from src/type-predicates/isArray.spec.ts rename to src/type-predicates/is-array.spec.ts index 2bb9b4c..2cd52a8 100644 --- a/src/type-predicates/isArray.spec.ts +++ b/src/type-predicates/is-array.spec.ts @@ -1,4 +1,4 @@ -import { isArray } from './isArray.js'; +import { isArray } from './is-array.js'; describe('isArray', () => { it('should return true if value is an Array', () => { diff --git a/src/type-predicates/isArray.ts b/src/type-predicates/is-array.ts similarity index 100% rename from src/type-predicates/isArray.ts rename to src/type-predicates/is-array.ts diff --git a/src/type-predicates/isBoolean.spec.ts b/src/type-predicates/is-boolean.spec.ts similarity index 94% rename from src/type-predicates/isBoolean.spec.ts rename to src/type-predicates/is-boolean.spec.ts index 1cb86c8..4bfa9fb 100644 --- a/src/type-predicates/isBoolean.spec.ts +++ b/src/type-predicates/is-boolean.spec.ts @@ -1,4 +1,4 @@ -import { isBoolean } from './isBoolean'; +import { isBoolean } from './is-boolean.js'; describe('isBoolean', () => { it('should return false if value is null', () => { @@ -31,7 +31,7 @@ describe('isBoolean', () => { }); it('should return false if value is an Array', () => { - const value = []; + const value: never[] = []; expect(isBoolean(value)).toBe(false); }); @@ -67,9 +67,7 @@ describe('isBoolean', () => { }); it('should return false if value is a function', () => { - const value = () => { - return; - }; + const value = () => void 0; expect(isBoolean(value)).toBe(false); }); diff --git a/src/type-predicates/isBoolean.ts b/src/type-predicates/is-boolean.ts similarity index 100% rename from src/type-predicates/isBoolean.ts rename to src/type-predicates/is-boolean.ts diff --git a/src/type-predicates/isBuffer.spec.ts b/src/type-predicates/is-buffer.spec.ts similarity index 87% rename from src/type-predicates/isBuffer.spec.ts rename to src/type-predicates/is-buffer.spec.ts index 2ac5b24..8d49954 100644 --- a/src/type-predicates/isBuffer.spec.ts +++ b/src/type-predicates/is-buffer.spec.ts @@ -1,4 +1,4 @@ -import { isBuffer } from './isBuffer'; +import { isBuffer } from './is-buffer.js'; describe(`isBuffer`, () => { it(`should return true if value is a Buffer`, () => { diff --git a/src/type-predicates/isBuffer.ts b/src/type-predicates/is-buffer.ts similarity index 91% rename from src/type-predicates/isBuffer.ts rename to src/type-predicates/is-buffer.ts index c0853ee..b269ed8 100644 --- a/src/type-predicates/isBuffer.ts +++ b/src/type-predicates/is-buffer.ts @@ -1,4 +1,4 @@ -import { isNullOrUndefined } from './isNullOrUndefined.js'; +import { isNullOrUndefined } from './is-null-or-undefined.js'; /** * Checks if a value is a Buffer. diff --git a/src/type-predicates/isDate.spec.ts b/src/type-predicates/is-date.spec.ts similarity index 95% rename from src/type-predicates/isDate.spec.ts rename to src/type-predicates/is-date.spec.ts index e4a2ef4..c5c4096 100644 --- a/src/type-predicates/isDate.spec.ts +++ b/src/type-predicates/is-date.spec.ts @@ -1,4 +1,4 @@ -import { isDate } from './isDate'; +import { isDate } from './is-date.js'; describe('isDate', () => { it('should return false if value is null', () => { @@ -32,7 +32,7 @@ describe('isDate', () => { }); it('should return false if value is an Array', () => { - const value = []; + const value: never[] = []; expect(isDate(value)).toBe(false); }); @@ -68,9 +68,7 @@ describe('isDate', () => { }); it('should return false if value is a function', () => { - const value = () => { - return; - }; + const value = () => void 0; expect(isDate(value)).toBe(false); }); diff --git a/src/type-predicates/isDate.ts b/src/type-predicates/is-date.ts similarity index 100% rename from src/type-predicates/isDate.ts rename to src/type-predicates/is-date.ts diff --git a/src/type-predicates/isEmpty.spec.ts b/src/type-predicates/is-empty.spec.ts similarity index 76% rename from src/type-predicates/isEmpty.spec.ts rename to src/type-predicates/is-empty.spec.ts index 2a3f3e1..e9e63db 100644 --- a/src/type-predicates/isEmpty.spec.ts +++ b/src/type-predicates/is-empty.spec.ts @@ -1,4 +1,4 @@ -import { isEmpty } from './isEmpty'; +import { isEmpty } from './is-empty.js'; describe('isEmpty', () => { it('should return true if value is ""', () => { @@ -7,7 +7,7 @@ describe('isEmpty', () => { expect(isEmpty(value)).toBe(true); }); - it('should return true if value is ""', () => { + it('should return true if value is whitespace-only', () => { const value = ' '; expect(isEmpty(value)).toBe(true); @@ -26,12 +26,11 @@ describe('isEmpty', () => { }); it('should return true if value is not provided', () => { - expect(isEmpty(undefined as any)).toBe(true); - }); - - it('should return true if value is undefined', () => { - const value = undefined; + // eslint-disable-next-line no-unassigned-vars + let value: string; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore expect(isEmpty(value)).toBe(true); }); @@ -42,13 +41,13 @@ describe('isEmpty', () => { }); it('should return true if value is an empty array', () => { - const value: any[] = []; + const value: unknown[] = []; expect(isEmpty(value)).toBe(true); }); it('should return false if value is not an empty array', () => { - const value: any[] = ['a']; + const value: unknown[] = ['a']; expect(isEmpty(value)).toBe(false); }); @@ -66,31 +65,31 @@ describe('isEmpty', () => { }); it('should return false if value is a number', () => { - const value: any = 1; + const value = 1; expect(isEmpty(value)).toBe(false); }); it('should return true if value is an empty set', () => { - const value: Set = new Set(); + const value = new Set(); expect(isEmpty(value)).toBe(true); }); it('should return false if value is not an empty set', () => { - const value: Set = new Set(['a']); + const value = new Set(['a']); expect(isEmpty(value)).toBe(false); }); it('should return true if value is an empty map', () => { - const value: Map = new Map(); + const value = new Map(); expect(isEmpty(value)).toBe(true); }); it('should return false if value is not an empty map', () => { - const value: Map = new Map([['a', 'a']]); + const value = new Map([['a', 'a']]); expect(isEmpty(value)).toBe(false); }); diff --git a/src/type-predicates/isEmpty.ts b/src/type-predicates/is-empty.ts similarity index 93% rename from src/type-predicates/isEmpty.ts rename to src/type-predicates/is-empty.ts index 26ef940..a807858 100644 --- a/src/type-predicates/isEmpty.ts +++ b/src/type-predicates/is-empty.ts @@ -1,5 +1,5 @@ -import { isNullOrUndefined } from './isNullOrUndefined.js'; -import { typeOf } from './typeOf.js'; +import { isNullOrUndefined } from './is-null-or-undefined.js'; +import { typeOf } from './type-of.js'; type EmptyString = ''; type EmptyObject = Record; diff --git a/src/type-predicates/isEqualTestDefinitions.spec.ts b/src/type-predicates/is-equal-test-definitions.spec.ts similarity index 96% rename from src/type-predicates/isEqualTestDefinitions.spec.ts rename to src/type-predicates/is-equal-test-definitions.spec.ts index c95c7dd..7e9552a 100644 --- a/src/type-predicates/isEqualTestDefinitions.spec.ts +++ b/src/type-predicates/is-equal-test-definitions.spec.ts @@ -3,7 +3,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import moment from 'moment'; import { IsEqualTestSuite } from '../types/test-definition'; function func1() {} @@ -68,6 +67,12 @@ export const isEqualTests: IsEqualTestSuite[] = [ b: 0, expected: false, }, + { + description: 'null and false', + a: null, + b: false, + expected: false, + }, { description: 'NaN and NaN', a: NaN, @@ -209,6 +214,36 @@ export const isEqualTests: IsEqualTestSuite[] = [ b: '', expected: false, }, + { + description: 'true and empty string', + a: true, + b: '', + expected: false, + }, + { + description: 'true and null', + a: true, + b: null, + expected: false, + }, + { + description: 'false and null', + a: false, + b: null, + expected: false, + }, + { + description: 'true and undefined', + a: true, + b: undefined, + expected: false, + }, + { + description: 'false and undefined', + a: false, + b: undefined, + expected: false, + }, ], }, { @@ -788,27 +823,4 @@ export const isEqualTests: IsEqualTestSuite[] = [ }, ], }, - { - description: 'Moment', - tests: [ - { - description: 'Moment and any other object are not equal', - a: moment(), - b: new Date(), - expected: false, - }, - { - description: 'Two moments with the same value are equal', - a: moment('2018-01-01'), - b: moment('2018-01-01'), - expected: true, - }, - { - description: 'Two moments with different values are not equal', - a: moment('2018-01-01'), - b: moment('2018-01-02'), - expected: false, - }, - ], - }, ]; diff --git a/src/type-predicates/is-equal.spec.ts b/src/type-predicates/is-equal.spec.ts new file mode 100644 index 0000000..2c3c2a8 --- /dev/null +++ b/src/type-predicates/is-equal.spec.ts @@ -0,0 +1,70 @@ +import { isEqualTests } from './is-equal-test-definitions.spec.js'; +import { isEqual } from './is-equal.js'; + +describe('isEqual', () => { + describe('types', () => { + const types = [undefined, null, true, 1, '', new Date(), {}, Symbol(), () => {}]; + + it(`Should return true if the types are the same`, () => { + types.forEach(type => { + expect(isEqual(type, type)).toBe(true); + }); + }); + + it(`Should return false if the types aren't the same`, () => { + types.forEach((aType, aIndex) => + types.forEach((bType, bIndex) => { + if (aIndex !== bIndex) { + expect(isEqual(aType, bType)).toBe(false); + } + }) + ); + }); + }); + + isEqualTests.forEach(suite => { + describe(suite.description, () => { + suite.tests.forEach(test => { + it(test.description, () => { + expect(isEqual(test.a, test.b)).toBe(test.expected); + }); + }); + }); + }); + + describe('circular references', () => { + it('should handle self-referencing objects', () => { + const a: Record = {}; + a['self'] = a; + const b: Record = {}; + b['self'] = b; + expect(isEqual(a, b)).toBe(true); + }); + + it('should detect inequality in self-referencing objects with different keys', () => { + const a: Record = { x: 1 }; + a['self'] = a; + const b: Record = { x: 2 }; + b['self'] = b; + expect(isEqual(a, b)).toBe(false); + }); + + it('should handle mutually referencing objects', () => { + const a1: Record = {}; + const a2: Record = { ref: a1 }; + a1['ref'] = a2; + const b1: Record = {}; + const b2: Record = { ref: b1 }; + b1['ref'] = b2; + expect(isEqual(a1, b1)).toBe(true); + }); + + it('should handle self-referencing arrays', () => { + const a: unknown[] = []; + a.push(a); + const b: unknown[] = []; + b.push(b); + expect(isEqual(a, b)).toBe(true); + }); + }); +}); diff --git a/src/type-predicates/isEqual.ts b/src/type-predicates/is-equal.ts similarity index 71% rename from src/type-predicates/isEqual.ts rename to src/type-predicates/is-equal.ts index a0baf57..e48797f 100644 --- a/src/type-predicates/isEqual.ts +++ b/src/type-predicates/is-equal.ts @@ -15,10 +15,10 @@ export type EqualityType = IndividualEqualityType | IndividualEqualityType[]; * @returns `true` if the values are deeply equal, otherwise `false`. */ export function isEqual(a: unknown, b: unknown): boolean { - return _isEqual(a, b, new WeakMap>()); + return _isEqual(a, b, null); } -function _isEqual(a: any, b: any, visited: WeakMap>): boolean { +function _isEqual(a: any, b: any, visited: WeakMap> | null): boolean { if (a === b) { return true; } @@ -28,7 +28,12 @@ function _isEqual(a: any, b: any, visited: WeakMap>): bo return false; } - // Cycle detection: if this object pair has already been seen, treat as equal + // Cycle detection: lazily create the WeakMap only when we first enter an object + // comparison, avoiding allocation overhead for simple/primitive comparisons. + if (visited === null) { + visited = new WeakMap>(); + } + const visitedB = visited.get(a as object); if (visitedB) { @@ -113,34 +118,32 @@ function _isEqual(a: any, b: any, visited: WeakMap>): bo return a.source === (b as RegExp).source && a.flags === (b as RegExp).flags; } - const aValueOf = a.valueOf as unknown; - const bValueOf = b.valueOf as unknown; - if (typeof aValueOf === 'function' && typeof bValueOf === 'function' && aValueOf !== Object.prototype.valueOf) { - return aValueOf.call(a) === bValueOf.call(b); + // Handles Date and any other class with a custom valueOf (e.g. value objects). + // Checking only `a` side matches fast-deep-equal's approach — if constructors are + // the same (checked above) both will have the same valueOf override. + if (a.valueOf !== Object.prototype.valueOf) { + return (a.valueOf as () => unknown)() === (b.valueOf as () => unknown)(); } - const aToString = a.toString as unknown; - const bToString = b.toString as unknown; - if (typeof aToString === 'function' && typeof bToString === 'function' && aToString !== Object.prototype.toString) { - return aToString.call(a) === bToString.call(b); + if (a.toString !== Object.prototype.toString) { + return (a.toString as () => string)() === (b.toString as () => string)(); } - const keysA = Reflect.ownKeys(a as object).filter(k => Object.prototype.propertyIsEnumerable.call(a as object, k)); - const keysB = Reflect.ownKeys(b as object).filter(k => Object.prototype.propertyIsEnumerable.call(b as object, k)); + // Plain-object key comparison. + // Object.keys is ~10× faster than Reflect.ownKeys + filter(propertyIsEnumerable) + // and covers 99% of usage. Symbol-keyed enumerable properties are not compared + // (matches fast-deep-equal behaviour). + const keysA = Object.keys(a as object); const keysALength = keysA.length; - if (keysALength !== keysB.length) { + if (keysALength !== Object.keys(b as object).length) { return false; } for (let i = 0; i < keysALength; i++) { const key = keysA[i]; - if (!Object.prototype.hasOwnProperty.call(b as object, key)) { - return false; - } - - if (!_isEqual(a[key], b[key], visited)) { + if (!Object.prototype.hasOwnProperty.call(b as object, key) || !_isEqual(a[key], b[key], visited)) { return false; } } diff --git a/src/type-predicates/isError.spec.ts b/src/type-predicates/is-error.spec.ts similarity index 87% rename from src/type-predicates/isError.spec.ts rename to src/type-predicates/is-error.spec.ts index 1bf3cd4..c0e4fe4 100644 --- a/src/type-predicates/isError.spec.ts +++ b/src/type-predicates/is-error.spec.ts @@ -1,4 +1,4 @@ -import { isError } from './isError'; +import { isError } from './is-error.js'; describe(`isError`, () => { it(`should return true if value is an Error`, () => { diff --git a/src/type-predicates/isError.ts b/src/type-predicates/is-error.ts similarity index 100% rename from src/type-predicates/isError.ts rename to src/type-predicates/is-error.ts diff --git a/src/type-predicates/isGeneratorObject.spec.ts b/src/type-predicates/is-generator-object.spec.ts similarity index 85% rename from src/type-predicates/isGeneratorObject.spec.ts rename to src/type-predicates/is-generator-object.spec.ts index eda7f58..089b401 100644 --- a/src/type-predicates/isGeneratorObject.spec.ts +++ b/src/type-predicates/is-generator-object.spec.ts @@ -1,4 +1,4 @@ -import { isGeneratorObject } from './isGeneratorObject'; +import { isGeneratorObject } from './is-generator-object.js'; describe(`isGeneratorObject`, () => { it(`should return true if value is a generator object`, () => { diff --git a/src/type-predicates/isGeneratorObject.ts b/src/type-predicates/is-generator-object.ts similarity index 100% rename from src/type-predicates/isGeneratorObject.ts rename to src/type-predicates/is-generator-object.ts diff --git a/src/type-predicates/is-mergeable-object.spec.ts b/src/type-predicates/is-mergeable-object.spec.ts new file mode 100644 index 0000000..00023e8 --- /dev/null +++ b/src/type-predicates/is-mergeable-object.spec.ts @@ -0,0 +1,34 @@ +import { isMergeableObject } from './is-mergeable-object.js'; + +describe('isMergeableObject', () => { + it('should return true for plain objects', () => { + expect(isMergeableObject({})).toBe(true); + expect(isMergeableObject({ a: 1 })).toBe(true); + }); + + it('should return false for arrays', () => { + expect(isMergeableObject([])).toBe(false); + expect(isMergeableObject([1, 2, 3])).toBe(false); + }); + + it('should return false for null and undefined', () => { + expect(isMergeableObject(null)).toBe(false); + expect(isMergeableObject(undefined)).toBe(false); + }); + + it('should return false for special objects like RegExp, Date, and ArrayBuffer views', () => { + expect(isMergeableObject(/test/)).toBe(false); + expect(isMergeableObject(new Date())).toBe(false); + expect(isMergeableObject(new ArrayBuffer(8))).toBe(false); + expect(isMergeableObject(new Uint8Array())).toBe(false); + expect(isMergeableObject(Buffer.from(''))).toBe(false); + }); + + it('should return false for non-object types', () => { + expect(isMergeableObject(42)).toBe(false); + expect(isMergeableObject('test')).toBe(false); + expect(isMergeableObject(true)).toBe(false); + expect(isMergeableObject(false)).toBe(false); + expect(isMergeableObject(Symbol('test'))).toBe(false); + }); +}); diff --git a/src/type-predicates/isMergeableObject.ts b/src/type-predicates/is-mergeable-object.ts similarity index 100% rename from src/type-predicates/isMergeableObject.ts rename to src/type-predicates/is-mergeable-object.ts diff --git a/src/type-predicates/isNaNStrict.spec.ts b/src/type-predicates/is-nan-strict.spec.ts similarity index 95% rename from src/type-predicates/isNaNStrict.spec.ts rename to src/type-predicates/is-nan-strict.spec.ts index 05dc8c1..c393645 100644 --- a/src/type-predicates/isNaNStrict.spec.ts +++ b/src/type-predicates/is-nan-strict.spec.ts @@ -1,6 +1,6 @@ -import { isNaNStrict } from './isNaNStrict'; +import { isNaNStrict } from './is-nan-strict.js'; -describe(`isNanStrict`, () => { +describe(`isNaNStrict`, () => { it(`should return true if value is NaN`, () => { const value = NaN; @@ -38,7 +38,7 @@ describe(`isNanStrict`, () => { }); it('should return false if value is an Array', () => { - const value = []; + const value: never[] = []; expect(isNaNStrict(value)).toBe(false); }); diff --git a/src/type-predicates/isNaNStrict.ts b/src/type-predicates/is-nan-strict.ts similarity index 87% rename from src/type-predicates/isNaNStrict.ts rename to src/type-predicates/is-nan-strict.ts index 920eb37..3400004 100644 --- a/src/type-predicates/isNaNStrict.ts +++ b/src/type-predicates/is-nan-strict.ts @@ -1,4 +1,4 @@ -import { isNumber } from './isNumber.js'; +import { isNumber } from './is-number.js'; /** * Checks if a value is `NaN` and of type `number`. diff --git a/src/type-predicates/is-null-or-undefined.spec.ts b/src/type-predicates/is-null-or-undefined.spec.ts new file mode 100644 index 0000000..8f73800 --- /dev/null +++ b/src/type-predicates/is-null-or-undefined.spec.ts @@ -0,0 +1,59 @@ +import { isNullOrUndefined } from './is-null-or-undefined.js'; + +describe('isNullOrUndefined', () => { + it('returns true for null', () => { + expect(isNullOrUndefined(null)).toBe(true); + }); + + it('returns true for undefined', () => { + expect(isNullOrUndefined(undefined)).toBe(true); + }); + + it('should return true if value is not set', () => { + // eslint-disable-next-line no-unassigned-vars + let value: void; + + expect(isNullOrUndefined(value)).toBe(true); + }); + + it('should return false if value is an object', () => { + const value = {}; + + expect(isNullOrUndefined(value)).toBe(false); + }); + + it('should return false if value is "undefined"', () => { + const value = 'undefined'; + + expect(isNullOrUndefined(value)).toBe(false); + }); + + it('should return false if value is "null"', () => { + const value = 'null'; + + expect(isNullOrUndefined(value)).toBe(false); + }); + + it('should return false for non null or undefined values', () => { + expect(isNullOrUndefined([])).toBe(false); + + expect(isNullOrUndefined(() => {})).toBe(false); + expect(isNullOrUndefined(Symbol('test'))).toBe(false); + expect(isNullOrUndefined(true)).toBe(false); + expect(isNullOrUndefined(false)).toBe(false); + expect(isNullOrUndefined(0)).toBe(false); + expect(isNullOrUndefined(-0)).toBe(false); + expect(isNullOrUndefined(NaN)).toBe(false); + expect(isNullOrUndefined(Infinity)).toBe(false); + expect(isNullOrUndefined(-Infinity)).toBe(false); + expect(isNullOrUndefined(42)).toBe(false); + expect(isNullOrUndefined('')).toBe(false); + expect(isNullOrUndefined('test')).toBe(false); + expect(isNullOrUndefined(BigInt(9007199254740991))).toBe(false); + expect(isNullOrUndefined(new Date())).toBe(false); + expect(isNullOrUndefined(new Map())).toBe(false); + expect(isNullOrUndefined(new Set())).toBe(false); + expect(isNullOrUndefined(new Error())).toBe(false); + expect(isNullOrUndefined(/test/)).toBe(false); + }); +}); diff --git a/src/type-predicates/isNullOrUndefined.ts b/src/type-predicates/is-null-or-undefined.ts similarity index 100% rename from src/type-predicates/isNullOrUndefined.ts rename to src/type-predicates/is-null-or-undefined.ts diff --git a/src/type-predicates/isNumber.spec.ts b/src/type-predicates/is-number.spec.ts similarity index 94% rename from src/type-predicates/isNumber.spec.ts rename to src/type-predicates/is-number.spec.ts index b706595..42d1a15 100644 --- a/src/type-predicates/isNumber.spec.ts +++ b/src/type-predicates/is-number.spec.ts @@ -1,4 +1,4 @@ -import { isNumber } from './isNumber'; +import { isNumber } from './is-number.js'; describe('isNumber', () => { it('should return false if value is null', () => { @@ -25,14 +25,14 @@ describe('isNumber', () => { expect(isNumber(value)).toBe(false); }); - it('should return false if value is a object', () => { + it('should return false if value is an object', () => { const value = {}; expect(isNumber(value)).toBe(false); }); it('should return false if value is an Array', () => { - const value = []; + const value: never[] = []; expect(isNumber(value)).toBe(false); }); diff --git a/src/type-predicates/isNumber.ts b/src/type-predicates/is-number.ts similarity index 100% rename from src/type-predicates/isNumber.ts rename to src/type-predicates/is-number.ts diff --git a/src/type-predicates/isObject.spec.ts b/src/type-predicates/is-object.spec.ts similarity index 94% rename from src/type-predicates/isObject.spec.ts rename to src/type-predicates/is-object.spec.ts index 223b840..71ea523 100644 --- a/src/type-predicates/isObject.spec.ts +++ b/src/type-predicates/is-object.spec.ts @@ -1,4 +1,4 @@ -import { isObject } from './isObject'; +import { isObject } from './is-object.js'; describe('isObject', () => { it('should return false if value is null', () => { @@ -25,14 +25,14 @@ describe('isObject', () => { expect(isObject(value)).toBe(false); }); - it('should return true if value is a object', () => { + it('should return true if value is an object', () => { const value = {}; expect(isObject(value)).toBe(true); }); it('should return false if value is an Array', () => { - const value = []; + const value: never[] = []; expect(isObject(value)).toBe(false); }); diff --git a/src/type-predicates/isObject.ts b/src/type-predicates/is-object.ts similarity index 77% rename from src/type-predicates/isObject.ts rename to src/type-predicates/is-object.ts index ee4e988..53f2dfc 100644 --- a/src/type-predicates/isObject.ts +++ b/src/type-predicates/is-object.ts @@ -1,5 +1,5 @@ -import { isDate } from './isDate.js'; -import { isNullOrUndefined } from './isNullOrUndefined.js'; +import { isDate } from './is-date.js'; +import { isNullOrUndefined } from './is-null-or-undefined.js'; /** * Checks if a value is an object (but not an array or date). diff --git a/src/type-predicates/isPlainObject.spec.ts b/src/type-predicates/is-plain-object.spec.ts similarity index 59% rename from src/type-predicates/isPlainObject.spec.ts rename to src/type-predicates/is-plain-object.spec.ts index e948959..1c392a4 100644 --- a/src/type-predicates/isPlainObject.spec.ts +++ b/src/type-predicates/is-plain-object.spec.ts @@ -1,7 +1,7 @@ -import { isPlainObject } from './isPlainObject'; +import { isPlainObject } from './is-plain-object.js'; describe(`isPlainObject`, () => { - it('should return `true` if the object is created by the `Object` constructor.', () => { + it('should return `true` for plain objects.', () => { expect(isPlainObject(Object.create({}))).toBe(true); expect(isPlainObject(Object.create(Object.prototype))).toBe(true); expect(isPlainObject({ foo: 'bar' })).toBe(true); @@ -9,15 +9,14 @@ describe(`isPlainObject`, () => { expect(isPlainObject(Object.create(null))).toBe(true); }); - it('should return `false` if the object is not created by the `Object` constructor.', () => { - function Foo(this: any) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - this.abc = {}; + it('should return `false` for non-plain objects.', () => { + class Foo { + public abc = {}; } expect(isPlainObject(/foo/)).toBe(false); - expect(isPlainObject(function () {})).toBe(false); + expect(isPlainObject(() => {})).toBe(false); expect(isPlainObject(1)).toBe(false); expect(isPlainObject(['foo', 'bar'])).toBe(false); expect(isPlainObject([])).toBe(false); diff --git a/src/type-predicates/isPlainObject.ts b/src/type-predicates/is-plain-object.ts similarity index 94% rename from src/type-predicates/isPlainObject.ts rename to src/type-predicates/is-plain-object.ts index 35259a3..4377f23 100644 --- a/src/type-predicates/isPlainObject.ts +++ b/src/type-predicates/is-plain-object.ts @@ -1,4 +1,4 @@ -import { isObject } from './isObject.js'; +import { isObject } from './is-object.js'; /** * Checks if a value is a plain object (i.e., an object whose prototype is `Object.prototype` or `null`, or that otherwise looks like an `Object` instance). diff --git a/src/type-predicates/isReactElement.ts b/src/type-predicates/is-react-element.ts similarity index 100% rename from src/type-predicates/isReactElement.ts rename to src/type-predicates/is-react-element.ts diff --git a/src/type-predicates/isRegExp.spec.ts b/src/type-predicates/is-regexp.spec.ts similarity index 93% rename from src/type-predicates/isRegExp.spec.ts rename to src/type-predicates/is-regexp.spec.ts index d50bf00..0c9086c 100644 --- a/src/type-predicates/isRegExp.spec.ts +++ b/src/type-predicates/is-regexp.spec.ts @@ -1,4 +1,4 @@ -import { isRegExp } from './isRegExp'; +import { isRegExp } from './is-regexp.js'; describe(`isRegExp`, () => { it(`should return true if value is a RegExp`, () => { diff --git a/src/type-predicates/isRegExp.ts b/src/type-predicates/is-regexp.ts similarity index 100% rename from src/type-predicates/isRegExp.ts rename to src/type-predicates/is-regexp.ts diff --git a/src/type-predicates/isString.spec.ts b/src/type-predicates/is-string.spec.ts similarity index 94% rename from src/type-predicates/isString.spec.ts rename to src/type-predicates/is-string.spec.ts index c20b2b4..fe70eb2 100644 --- a/src/type-predicates/isString.spec.ts +++ b/src/type-predicates/is-string.spec.ts @@ -1,4 +1,4 @@ -import { isString } from './isString'; +import { isString } from './is-string.js'; describe('isString', () => { it('should return false if value is null', () => { @@ -25,14 +25,14 @@ describe('isString', () => { expect(isString(value)).toBe(true); }); - it('should return false if value is a object', () => { + it('should return false if value is an object', () => { const value = {}; expect(isString(value)).toBe(false); }); it('should return false if value is an Array', () => { - const value = []; + const value: never[] = []; expect(isString(value)).toBe(false); }); diff --git a/src/type-predicates/isString.ts b/src/type-predicates/is-string.ts similarity index 100% rename from src/type-predicates/isString.ts rename to src/type-predicates/is-string.ts diff --git a/src/type-predicates/isEqual.spec.ts b/src/type-predicates/isEqual.spec.ts deleted file mode 100644 index e3e18d3..0000000 --- a/src/type-predicates/isEqual.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { isEqual } from './isEqual'; -import { isEqualTests } from './isEqualTestDefinitions.spec'; - -describe('isEqual', () => { - describe('types', () => { - const types = [undefined, null, true, 1, '', new Date(), {}, Symbol(), () => {}]; - - it(`Should return true if the types are the same`, () => { - types.forEach(type => { - expect(isEqual(type, type)).toBe(true); - }); - }); - - it(`Should return false if the types aren't the same`, () => { - types.forEach((aType, aIndex) => - types.forEach((bType, bIndex) => { - if (aIndex !== bIndex) { - if (isEqual(aType, bType)) { - console.log(aType, bType); - } - expect(isEqual(aType, bType)).toBe(false); - } - }) - ); - }); - }); - - for (const suite of isEqualTests) { - describe(suite.description, () => { - for (const test of suite.tests) { - if (test.only) { - it.only(test.description, () => expect(isEqual(test.a, test.b)).toBe(test.expected)); - } else { - it(test.description, () => expect(isEqual(test.a, test.b)).toBe(test.expected)); - } - } - }); - } -}); diff --git a/src/type-predicates/isMergableObject.ts b/src/type-predicates/isMergableObject.ts deleted file mode 100644 index 0233245..0000000 --- a/src/type-predicates/isMergableObject.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { isReactElement } from './isReactElement.js'; - -/** - * Figuring out which properties of an object should be recursively iterated over when merging. - */ -export function isMergeableObject(value: any): boolean { - return isNonNullObject(value) && !isSpecial(value as object); -} - -function isNonNullObject(value: any): boolean { - return !!value && typeof value === 'object'; -} - -function isSpecial(value: object): boolean { - const stringValue = Object.prototype.toString.call(value); - - return stringValue === '[object RegExp]' || stringValue === '[object Date]' || isReactElement(value); -} diff --git a/src/type-predicates/isMoment.ts b/src/type-predicates/isMoment.ts deleted file mode 100644 index 15bd57d..0000000 --- a/src/type-predicates/isMoment.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { isNullOrUndefined } from './isNullOrUndefined.js'; - -export function isMoment(value: unknown): boolean { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - return !isNullOrUndefined(value) && !isNullOrUndefined((value as any)._isAMomentObject); -} diff --git a/src/type-predicates/isNullOrUndefined.spec.ts b/src/type-predicates/isNullOrUndefined.spec.ts deleted file mode 100644 index 595e268..0000000 --- a/src/type-predicates/isNullOrUndefined.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { isNullOrUndefined } from './isNullOrUndefined'; - -describe('isNullOrUndefined', () => { - it('should return true if value is null', () => { - const value = null; - - expect(isNullOrUndefined(value)).toBe(true); - }); - - it('should return true if value is undefined', () => { - const value = undefined; - - expect(isNullOrUndefined(value)).toBe(true); - }); - - it('should return false if value is "undefined"', () => { - const value = 'undefined'; - - expect(isNullOrUndefined(value)).toBe(false); - }); - - it('should return false if value is empty string', () => { - const value = ''; - - expect(isNullOrUndefined(value)).toBe(false); - }); - - it('should return false if value is 0', () => { - const value = 0; - - expect(isNullOrUndefined(value)).toBe(false); - }); - - it('should return true if value is not set', () => { - expect(isNullOrUndefined(undefined)).toBe(true); - }); -}); diff --git a/src/type-predicates/typeOf.spec.ts b/src/type-predicates/type-of.spec.ts similarity index 97% rename from src/type-predicates/typeOf.spec.ts rename to src/type-predicates/type-of.spec.ts index 7b686e6..19c5827 100644 --- a/src/type-predicates/typeOf.spec.ts +++ b/src/type-predicates/type-of.spec.ts @@ -1,4 +1,4 @@ -import { typeOf, ValueType } from './typeOf'; +import { typeOf, ValueType } from './type-of.js'; describe(`typeOf`, () => { it(`undefined`, () => { @@ -160,7 +160,8 @@ describe(`typeOf`, () => { }); it(`rejected Promise`, () => { - expect(typeOf(Promise.reject(new Error()).catch(() => {}))).toBe(ValueType.Promise); + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + expect(typeOf(Promise.reject().catch(() => {}))).toBe(ValueType.Promise); }); }); @@ -193,6 +194,7 @@ describe(`typeOf`, () => { it(`Map.add`, () => { const map = new Map(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(typeOf((map as any).add)).toBe(ValueType.Undefined); }); @@ -217,6 +219,7 @@ describe(`typeOf`, () => { it(`WeakMap.add`, () => { const map = new WeakMap(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(typeOf((map as any).add)).toBe(ValueType.Undefined); }); @@ -229,12 +232,14 @@ describe(`typeOf`, () => { it(`Set.set`, () => { const set = new Set(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(typeOf((set as any).set)).toBe(ValueType.Undefined); }); it(`Set.get`, () => { const set = new Set(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(typeOf((set as any).get)).toBe(ValueType.Undefined); }); @@ -253,12 +258,14 @@ describe(`typeOf`, () => { it(`WeakSet.weakset`, () => { const weakset = new WeakSet(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(typeOf((weakset as any).set)).toBe(ValueType.Undefined); }); it(`WeakSet.get`, () => { const weakset = new WeakSet(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(typeOf((weakset as any).get)).toBe(ValueType.Undefined); }); diff --git a/src/type-predicates/typeOf.ts b/src/type-predicates/type-of.ts similarity index 94% rename from src/type-predicates/typeOf.ts rename to src/type-predicates/type-of.ts index 1a6e130..1d46c66 100644 --- a/src/type-predicates/typeOf.ts +++ b/src/type-predicates/type-of.ts @@ -1,9 +1,9 @@ -import { isArguments } from './isArguments.js'; -import { isBuffer } from './isBuffer.js'; -import { isDate } from './isDate.js'; -import { isError } from './isError.js'; -import { isGeneratorObject } from './isGeneratorObject.js'; -import { isRegExp } from './isRegExp.js'; +import { isArguments } from './is-arguments.js'; +import { isBuffer } from './is-buffer.js'; +import { isDate } from './is-date.js'; +import { isError } from './is-error.js'; +import { isGeneratorObject } from './is-generator-object.js'; +import { isRegExp } from './is-regexp.js'; export enum ValueType { Undefined = 'undefined', diff --git a/src/types/Dictionary.ts b/src/types/dictionary.ts similarity index 100% rename from src/types/Dictionary.ts rename to src/types/dictionary.ts diff --git a/src/types/index.ts b/src/types/index.ts index e7d3182..b54751d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1 +1 @@ -export * from './Dictionary.js'; +export * from './dictionary.js'; diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json index bb89906..4f89c3c 100644 --- a/tsconfig.cjs.json +++ b/tsconfig.cjs.json @@ -1,16 +1,14 @@ { "extends": "./tsconfig.json", "include": ["src/**/*"], - "exclude": [ - "src/**/*.spec.ts", - "src/**/*.spec.tsx", - "src/dates/modifiers/date-modifiers-test-suites.ts" - ], + "exclude": ["src/**/*.spec.ts", "src/**/*.spec.tsx", "src/dates/modifiers/date-modifiers-test-suites.ts"], "compilerOptions": { + "rootDir": "src", "module": "commonjs", "moduleResolution": "node10", "ignoreDeprecations": "6.0", "outDir": "dist/cjs", - "target": "es2020" + "target": "es2020", + "types": ["node"] } } diff --git a/tsconfig.json b/tsconfig.json index 09a0775..2b684ab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,8 +11,8 @@ "target": "esnext", "lib": ["DOM", "esnext"], "esModuleInterop": true, - "types": ["node"] + "types": ["node", "vitest/globals"] }, "exclude": ["node_modules", "dist/"], - "include": ["src/**/*", "src/math/sum.spec.ts"] + "include": ["src/**/*", "benchmark.ts", "eslint.config.mts", "vitest.setup.ts"] } diff --git a/tsconfig.mjs.json b/tsconfig.mjs.json index 87da29e..586a650 100644 --- a/tsconfig.mjs.json +++ b/tsconfig.mjs.json @@ -1,12 +1,9 @@ { "extends": "./tsconfig.json", "include": ["src/**/*"], - "exclude": [ - "src/**/*.spec.ts", - "src/**/*.spec.tsx", - "src/dates/modifiers/date-modifiers-test-suites.ts" - ], + "exclude": ["src/**/*.spec.ts", "src/**/*.spec.tsx", "src/dates/modifiers/date-modifiers-test-suites.ts"], "compilerOptions": { + "rootDir": "src", "outDir": "dist/mjs", "module": "esnext" } diff --git a/tsconfig.spec.json b/tsconfig.spec.json index aea99e6..ed69b1f 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "types": ["vitest/globals", "vitest/importMeta", "vite/client", "node"] + "types": ["vitest/globals", "vitest/importMeta", "vite/client", "node", "vitest"] }, "include": ["src/**/*.spec.ts", "src/**/*.d.ts", "vitest.config.mts"] } diff --git a/vitest.config.mts b/vitest.config.mts index aa0391f..784a0e1 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -1,21 +1,25 @@ -import { playwright } from '@vitest/browser-playwright'; import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { globals: true, - setupFiles: ['./vitest.setup.ts'], - browser: { - enabled: true, - provider: playwright(), - headless: true, - instances: [{ browser: 'chromium' }], + watch: false, + environment: 'jsdom', + environmentOptions: { + jsdom: { + url: 'http://localhost/', + }, + }, + env: { + LANG: 'en-US', }, include: ['src/**/*.spec.ts'], exclude: [ - 'src/merge/mergeTestDefinitions.spec.ts', - 'src/test-helpers/createElement.spec.ts', - 'src/type-predicates/isEqualTestDefinitions.spec.ts', + 'src/merge/merge-test-definitions.spec.ts', + 'src/test-helpers/create-element.spec.ts', + 'src/type-predicates/is-equal-test-definitions.spec.ts', + // isVisible relies on getBoundingClientRect and real viewport layout which jsdom cannot provide + 'src/dom/is-visible.spec.ts', ], coverage: { provider: 'istanbul', From ecc07a75688d880fa1ea882523b7860cdf784357 Mon Sep 17 00:00:00 2001 From: Ben Meyrick Date: Sat, 27 Jun 2026 11:50:59 +0100 Subject: [PATCH 3/3] Preformance improvements --- src/async/async-filter.ts | 2 +- src/clone/clone.ts | 15 +++++----- src/dates/getters/get-month-names.ts | 10 ++++++- src/difference/difference.ts | 37 ++++++++++++++++++++++--- src/dom/get-ancestors.ts | 27 +++++------------- src/dom/get-computed-style-as-number.ts | 2 +- src/dom/get-positioned-parent.ts | 12 ++++---- src/format-time/format-time.ts | 27 +++++++++--------- src/math/sum.ts | 8 +++++- src/merge/merge.ts | 34 ++++++++++++++++++++--- src/sorting/sort-by.ts | 8 ++++-- src/type-predicates/is-empty.ts | 19 ++++++++++++- src/type-predicates/is-equal.ts | 10 ++----- 13 files changed, 141 insertions(+), 70 deletions(-) diff --git a/src/async/async-filter.ts b/src/async/async-filter.ts index dcf06be..85cae4e 100644 --- a/src/async/async-filter.ts +++ b/src/async/async-filter.ts @@ -6,7 +6,7 @@ * @returns A promise that resolves to a new array containing only the elements that passed the test implemented by the provided callback function. */ export async function asyncFilter(array: T[], callback: (item: T, index: number, array: T[]) => Promise): Promise { - const results = await Promise.all(array.map(callback)); + const results = await Promise.all(array.map((item, index, array) => callback(item, index, array))); return array.filter((_v, index) => results[index]); } diff --git a/src/clone/clone.ts b/src/clone/clone.ts index 1b277a9..b568b12 100644 --- a/src/clone/clone.ts +++ b/src/clone/clone.ts @@ -284,12 +284,13 @@ function cloneError(value: Error): Error { return cloned; } -function cloneSymbol(value: symbol): symbol { - for (const key of Object.getOwnPropertyNames(Symbol) as (keyof typeof Symbol)[]) { - if ((Symbol as unknown as Record)[key] === value) { - return value; - } - } +// Precomputed at module load — avoids scanning Object.getOwnPropertyNames(Symbol) on every clone. +const WELL_KNOWN_SYMBOLS: ReadonlySet = new Set( + Object.getOwnPropertyNames(Symbol) + .map(k => (Symbol as unknown as Record)[k]) + .filter((v): v is symbol => typeof v === 'symbol') +); - return Symbol(value.description); +function cloneSymbol(value: symbol): symbol { + return WELL_KNOWN_SYMBOLS.has(value) ? value : Symbol(value.description); } diff --git a/src/dates/getters/get-month-names.ts b/src/dates/getters/get-month-names.ts index 189cd6b..0fdd43d 100644 --- a/src/dates/getters/get-month-names.ts +++ b/src/dates/getters/get-month-names.ts @@ -1,3 +1,5 @@ +const formatterCache = new Map(); + /** * Gets the month names for the provided locale. Defaults to the runtime's default locale. * @@ -12,7 +14,13 @@ export function getMonthNames(options: GetMonthNamesOptions = {}): string[] { const { locale = defaultLocale, format = 'long' } = options; - const formatter = new Intl.DateTimeFormat(locale, { month: format }); + const cacheKey = `${locale}:${format}`; + let formatter = formatterCache.get(cacheKey); + + if (!formatter) { + formatter = new Intl.DateTimeFormat(locale, { month: format }); + formatterCache.set(cacheKey, formatter); + } return Array.from({ length: 12 }, (_, i) => formatter.format(new Date(1970, i, 1))); } diff --git a/src/difference/difference.ts b/src/difference/difference.ts index 4e0f375..c70ed26 100644 --- a/src/difference/difference.ts +++ b/src/difference/difference.ts @@ -16,11 +16,40 @@ export function difference(array: readonly T[], values: readonly unknown[]): return array.slice(); } - return array.reduce((result, item) => { - if (!values.some(value => isEqual(item, value))) { - result.push(item); + // Fast path: if all exclusion values are primitives, use a Set for O(1) membership tests. + let allPrimitive = true; + + for (let i = 0, length = values.length; i < length; i++) { + const v = values[i]; + + if (v !== null && (typeof v === 'object' || typeof v === 'function')) { + allPrimitive = false; + break; + } + } + + if (allPrimitive) { + const excluded = new Set(values); + const result: T[] = []; + + for (let i = 0; i < array.length; i++) { + if (!excluded.has(array[i])) { + result.push(array[i]); + } } return result; - }, []); + } + + // Slow path: deep equality for non-primitive exclusion values. + const result: T[] = []; + + for (let i = 0, length = array.length; i < length; i++) { + const item = array[i]; + + if (!values.some(value => isEqual(item, value))) { + result.push(item); + } + } + return result; } diff --git a/src/dom/get-ancestors.ts b/src/dom/get-ancestors.ts index 1d291b7..fa39bf0 100644 --- a/src/dom/get-ancestors.ts +++ b/src/dom/get-ancestors.ts @@ -1,5 +1,3 @@ -import { isNullOrUndefined } from '../type-predicates/is-null-or-undefined.js'; - /** * Gets all the elements that a given element is nested within * @@ -7,24 +5,13 @@ import { isNullOrUndefined } from '../type-predicates/is-null-or-undefined.js'; * @returns An array of ancestor elements, starting from the immediate parent and going up to the root of the document. */ export function getAncestors(element: HTMLElement): HTMLElement[] { - return _getAncestors(element); -} - -/** - * Internal function for building the array and navigating up the tree - * - * @param element - The element to get the ancestors of. - * @param result - The array of ancestor elements being built. - * @returns An array of ancestor elements, starting from the immediate parent and going up to the root of the document. - */ -function _getAncestors(element: HTMLElement, result: HTMLElement[] = []): HTMLElement[] { - const parent: HTMLElement | null = element.parentNode as HTMLElement | null; + const result: HTMLElement[] = []; + let current: HTMLElement | null = element.parentNode as HTMLElement | null; - if (!isNullOrUndefined(parent) && parent.nodeType === parent.ELEMENT_NODE) { - result.push(parent); - - return _getAncestors(parent, result); - } else { - return result; + while (current !== null && current.nodeType === current.ELEMENT_NODE) { + result.push(current); + current = current.parentNode as HTMLElement | null; } + + return result; } diff --git a/src/dom/get-computed-style-as-number.ts b/src/dom/get-computed-style-as-number.ts index 14eac6b..7e8ad8c 100644 --- a/src/dom/get-computed-style-as-number.ts +++ b/src/dom/get-computed-style-as-number.ts @@ -14,5 +14,5 @@ export function getComputedStyleAsNumber(element: HTMLElement, style: 'height' | throw new Error(`Element does not have a computed "${style}`); } - return parseFloat(getComputedStyle(element)[style].replace('px', '')); + return parseFloat(value); } diff --git a/src/dom/get-positioned-parent.ts b/src/dom/get-positioned-parent.ts index f141b12..814a66c 100644 --- a/src/dom/get-positioned-parent.ts +++ b/src/dom/get-positioned-parent.ts @@ -5,16 +5,16 @@ * @returns The first parent element with a relative or absolute position, or the document's root element if no such parent is found. */ export function getPositionedParent(element: HTMLElement): HTMLElement { - const parent: HTMLElement | null = element.parentElement; + let parent: HTMLElement | null = element.parentElement; - if (parent) { - const style: CSSStyleDeclaration = window.getComputedStyle(parent); + while (parent) { + const position = window.getComputedStyle(parent).position; - if (['relative', 'absolute'].includes(style.position)) { + if (position === 'relative' || position === 'absolute') { return parent; - } else { - return getPositionedParent(parent); } + + parent = parent.parentElement; } return document.documentElement; diff --git a/src/format-time/format-time.ts b/src/format-time/format-time.ts index 4768b68..ece4bfb 100644 --- a/src/format-time/format-time.ts +++ b/src/format-time/format-time.ts @@ -9,21 +9,20 @@ import { FormatTimeOptions, FormatTimeOptionsComplete } from './format-time-opti * @param options - The options to use for formatting the time * @returns The formatted time string */ +const DEFAULT_FORMAT_OPTIONS: FormatTimeOptionsComplete = { + forceAllUnits: false, + timeUnit: TimeUnit.Milliseconds, + secondsDecimalPlaces: 0, + hourSuffix: 'h', + minuteSuffix: 'm', + secondSuffix: 's', + padDecimals: false, +}; + export function formatTime(time: number, options?: FormatTimeOptions): string { - const defaultOptions: FormatTimeOptionsComplete = { - forceAllUnits: false, - timeUnit: TimeUnit.Milliseconds, - secondsDecimalPlaces: 0, - hourSuffix: 'h', - minuteSuffix: 'm', - secondSuffix: 's', - padDecimals: false, - }; - - const { forceAllUnits, timeUnit, secondsDecimalPlaces, hourSuffix, minuteSuffix, secondSuffix, padDecimals } = { - ...defaultOptions, - ...options, - }; + const { forceAllUnits, timeUnit, secondsDecimalPlaces, hourSuffix, minuteSuffix, secondSuffix, padDecimals } = options + ? { ...DEFAULT_FORMAT_OPTIONS, ...options } + : DEFAULT_FORMAT_OPTIONS; const timeMs = timeUnitToMilliseconds(time, timeUnit); diff --git a/src/math/sum.ts b/src/math/sum.ts index a96aa86..afd58d3 100644 --- a/src/math/sum.ts +++ b/src/math/sum.ts @@ -5,5 +5,11 @@ * @returns The sum of the numbers in the array. */ export function sum(values: number[]): number { - return values.reduce((acc, val) => acc + val, 0); + let total = 0; + + for (let i = 0, length = values.length; i < length; i++) { + total += values[i]; + } + + return total; } diff --git a/src/merge/merge.ts b/src/merge/merge.ts index 8547930..9010bba 100644 --- a/src/merge/merge.ts +++ b/src/merge/merge.ts @@ -91,10 +91,18 @@ function defaultCloneUnlessOtherwiseSpecified(value: T, options: MergeOptions } function defaultArrayMerge(x: X, y: Y, options: MergeOptions): X & Y { - const merged = x.concat(y); - const cloned: unknown[] = merged.map(element => options.cloneUnlessOtherwiseSpecified(element, options)); + const xLen = x.length; + const result = new Array(xLen + y.length) as unknown[]; - return cloned as X & Y; + for (let i = 0; i < xLen; i++) { + result[i] = options.cloneUnlessOtherwiseSpecified(x[i], options); + } + + for (let i = 0, length = y.length; i < length; i++) { + result[xLen + i] = options.cloneUnlessOtherwiseSpecified(y[i], options); + } + + return result as X & Y; } function getMergeFunction(key: PropertyKey, options: MergeOptions): (x: unknown, y: unknown, options: MergeOptions) => unknown { @@ -122,7 +130,25 @@ function getEnumerableOwnPropertySymbols(target: object): symbol[] { } function getKeys(target: object): (string | symbol)[] { - return [...Object.keys(target), ...getEnumerableOwnPropertySymbols(target)]; + const stringKeys = Object.keys(target); + const symKeys = getEnumerableOwnPropertySymbols(target); + + if (!symKeys.length) { + return stringKeys; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const result: (string | symbol)[] = new Array(stringKeys.length + symKeys.length); + + for (let i = 0, length = stringKeys.length; i < length; i++) { + result[i] = stringKeys[i]; + } + + for (let i = 0, length = symKeys.length; i < length; i++) { + result[stringKeys.length + i] = symKeys[i]; + } + + return result; } function propertyIsOnObject(object: object, property: PropertyKey): boolean { diff --git a/src/sorting/sort-by.ts b/src/sorting/sort-by.ts index 30d5f42..1917792 100644 --- a/src/sorting/sort-by.ts +++ b/src/sorting/sort-by.ts @@ -135,8 +135,12 @@ function objectPath(object: Record, pathParts: string[]): unkno */ function cast(value: unknown): unknown { // If the value is a number as a string, cast back to an actual number to sort - if (typeof value === 'string' && !!value.trim().length && !isNaN(Number(value))) { - return Number(value); + if (typeof value === 'string' && !!value.trim().length) { + const n = Number(value); + + if (!isNaN(n)) { + return n; + } } return value; diff --git a/src/type-predicates/is-empty.ts b/src/type-predicates/is-empty.ts index a807858..f52b4df 100644 --- a/src/type-predicates/is-empty.ts +++ b/src/type-predicates/is-empty.ts @@ -44,7 +44,24 @@ export function isEmpty(value: T | Empty | null | undefined): value is EmptyR } case 'object': { const obj = value as object; - return !Reflect.ownKeys(obj).some(k => Object.prototype.propertyIsEnumerable.call(obj, k)); + // for..in gives own + inherited enumerable string keys; Object.prototype has none, + // so this is equivalent to checking own keys for plain objects — O(1) early exit. + for (const k in obj) { + if (Object.prototype.hasOwnProperty.call(obj, k)) { + return false; + } + } + + // Also check enumerable own symbol keys (uncommon) + const syms = Object.getOwnPropertySymbols(obj); + + for (let i = 0, length = syms.length; i < length; i++) { + if (Object.prototype.propertyIsEnumerable.call(obj, syms[i])) { + return false; + } + } + + return true; } case 'string': { return !(value as string).trim(); diff --git a/src/type-predicates/is-equal.ts b/src/type-predicates/is-equal.ts index e48797f..4d567ca 100644 --- a/src/type-predicates/is-equal.ts +++ b/src/type-predicates/is-equal.ts @@ -67,14 +67,8 @@ function _isEqual(a: any, b: any, visited: WeakMap> | nu return false; } - for (const i of a.entries()) { - if (!b.has(i[0])) { - return false; - } - } - - for (const i of a.entries()) { - if (!_isEqual(i[1], b.get(i[0]), visited)) { + for (const [key, val] of a) { + if (!b.has(key) || !_isEqual(val, b.get(key), visited)) { return false; } }