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 9b3efb9..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) @@ -35,26 +34,30 @@ 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) - [instanceClone](#instanceclone) - - [clone performance comparison](#clone-performance-comparison) - [freeze](#freeze) - [merge](#merge) - [merge options](#merge-options) - [arrayMerge](#arraymerge) - - [isMergableObject](#ismergableobject) + - [isMergeableObject](#ismergeableobject-1) - [customMerge](#custommerge) - - [Merge performance comparison](#merge-performance-comparison) - [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) @@ -365,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. @@ -395,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) @@ -423,7 +410,9 @@ The equality types allowed are: - `undefined` - `boolean` - `number` +- `bigint` - `string` +- `symbol` - `Date` - `object` - `Function` @@ -558,6 +547,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 +599,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 +651,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 +670,7 @@ import { typeOf, ValueType } from '@qntm-code/utils'; const value = getTheValue(); -if (typeOf(value) === ValueType.string) { +if (typeOf(value) === ValueType.String) { // Do something } ``` @@ -638,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: @@ -664,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 @@ -776,7 +807,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. @@ -861,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 @@ -910,7 +932,7 @@ const capitalised = capitalise(value, { start: 0, end: 5 }); --- -### CapitaliseFirst +#### capitaliseFirst Capitalises the first n characters of a string. @@ -937,7 +959,7 @@ const capitalised = capitaliseFirst(value, 2); --- -#### CapitaliseLast +#### capitaliseLast Capitalises the last n characters of a string. @@ -990,18 +1012,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 +1058,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; ``` --- @@ -1627,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 5bf35bd..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", @@ -2154,6 +2349,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", @@ -2209,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", @@ -2254,10 +2438,20 @@ "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.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": { @@ -2313,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", @@ -2530,30 +2699,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", @@ -2639,23 +2784,99 @@ "which": "^2.0.1" }, "engines": { - "node": ">= 8" + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "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/cross-spawn/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "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": "ISC", + "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": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" }, "engines": { - "node": ">= 8" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/debug": { @@ -2676,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", @@ -2772,9 +3000,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": { @@ -2803,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", @@ -3333,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", @@ -3633,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", @@ -3685,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", @@ -3926,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 +4365,119 @@ "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/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", @@ -5796,7 +6142,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" }, @@ -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", @@ -7845,6 +8376,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", @@ -7882,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", @@ -7904,10 +8438,19 @@ "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.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", @@ -7937,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", @@ -8059,27 +8592,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": { @@ -8151,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", @@ -8160,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", @@ -8227,9 +8799,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": { @@ -8249,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", @@ -8592,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", @@ -8784,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", @@ -8817,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", @@ -8961,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", @@ -9087,6 +9680,85 @@ "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" + } + }, + "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", @@ -9482,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", @@ -9681,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", @@ -9717,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", @@ -9816,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", @@ -9872,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", @@ -10040,7 +10711,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": { @@ -10094,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", @@ -10160,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", @@ -10175,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", @@ -10272,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", @@ -10406,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", @@ -10478,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 d4bfb52..8dfbd32 100644 --- a/package.json +++ b/package.json @@ -96,20 +96,24 @@ "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": { "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": { @@ -128,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", @@ -146,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", 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..85cae4e --- /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((item, index, array) => callback(item, index, array))); + + 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 32a2d83..e80ddf8 100644 --- a/src/clone/clone.spec.ts +++ b/src/clone/clone.spec.ts @@ -1,6 +1,5 @@ -import moment from 'moment'; -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', () => { @@ -41,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); @@ -91,7 +89,7 @@ describe('clone', () => { nested.x = 2; - const clonedItem = Array.from(b)[0]; + const [clonedItem] = Array.from(b); expect(clonedItem.x).toBe(1); }); @@ -99,13 +97,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); @@ -145,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 437bb50..b568b12 100644 --- a/src/clone/clone.ts +++ b/src/clone/clone.ts @@ -1,179 +1,296 @@ -import { Moment } from 'moment'; -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; - } - case ValueType.moment: { - return (value as Moment).clone() 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; +} + +function cloneSetFast(value: Set): Set { + const cloned = new (value.constructor as typeof Set)(); - return cloned as unknown as T; + 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; + } - for (let i = 0; i < length; i++) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - cloned[i] = clone(value[i], instanceClone); + const t = typeof value; + + if (t === 'symbol') { + return cloneSymbol(value as symbol); } - return cloned as T; + 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 cloneMapDeep(value: Map, instanceClone?: InstanceClone): Map { - const cloned = new Map(); - const nestedInstanceClone = instanceClone as unknown as InstanceClone; +function cloneObjectWith(source: Record, instanceClone: InstanceClone, proto: object | null): unknown { + if (typeof instanceClone === 'function') { + return instanceClone(source); + } - 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); + 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 cloneSetDeep(value: Set, instanceClone?: InstanceClone): Set { - const cloned = new Set(); - const nestedInstanceClone = instanceClone as unknown as InstanceClone; +function cloneArrayWith(value: unknown[], instanceClone: InstanceClone): unknown[] { + const { length } = value; + const cloned = new Array(length); - for (const item of value.values()) { - cloned.add(clone(item, nestedInstanceClone as unknown as InstanceClone)); + for (let i = 0; i < length; i++) { + cloned[i] = cloneWith(value[i], 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; +function cloneMapWith(value: Map, instanceClone: InstanceClone): Map { + const cloned = new (value.constructor as typeof Map)(); - cloned.lastIndex = value.lastIndex; + for (const [k, v] of value) { + cloned.set(cloneWith(k, instanceClone), cloneWith(v, instanceClone)); + } return cloned; } -function cloneBuffer(value: Buffer): Buffer { - const length = value.length; +function cloneSetWith(value: Set, instanceClone: InstanceClone): Set { + const cloned = new (value.constructor as typeof Set)(); - const buffer = Buffer.allocUnsafe ? Buffer.allocUnsafe(length) : Buffer.from(length as any); + for (const item of value) { + cloned.add(cloneWith(item, instanceClone)); + } + + return cloned; +} + +// โ”€โ”€โ”€ Shared helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +function cloneRegExp(value: RegExp): RegExp { + const cloned = new RegExp(value.source, value.flags); + cloned.lastIndex = value.lastIndex; + return cloned; +} - value.copy(buffer); +function cloneBuffer(value: Buffer): Buffer { + const cloned = Buffer.allocUnsafe(value.length); + value.copy(cloned); + return cloned; +} - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return buffer; +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 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) { - return value; +function cloneError(value: Error): Error { + const cloned = new (value.constructor as typeof Error)(value.message); + cloned.stack = value.stack; + + for (const key of Object.getOwnPropertyNames(value)) { + if (key !== 'message' && key !== 'stack') { + Object.defineProperty(cloned, key, Object.getOwnPropertyDescriptor(value, key)!); } } - return Symbol(description); + return cloned; +} + +// 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') +); + +function cloneSymbol(value: symbol): symbol { + return WELL_KNOWN_SYMBOLS.has(value) ? value : 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..0fdd43d --- /dev/null +++ b/src/dates/getters/get-month-names.ts @@ -0,0 +1,31 @@ +const formatterCache = new Map(); + +/** + * 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 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))); +} + +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/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..c70ed26 100644 --- a/src/difference/difference.ts +++ b/src/difference/difference.ts @@ -1,24 +1,55 @@ 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(); + } + + // 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; + } - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return result; - }, - [] as unknown as T - ) as T; + // 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/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/get-ancestors.ts b/src/dom/get-ancestors.ts new file mode 100644 index 0000000..fa39bf0 --- /dev/null +++ b/src/dom/get-ancestors.ts @@ -0,0 +1,17 @@ +/** + * 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[] { + const result: HTMLElement[] = []; + let current: HTMLElement | null = element.parentNode as HTMLElement | null; + + while (current !== null && current.nodeType === current.ELEMENT_NODE) { + result.push(current); + current = current.parentNode as HTMLElement | null; + } + + return result; +} 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 51% rename from src/dom/getComputedStyleAsNumber.ts rename to src/dom/get-computed-style-as-number.ts index 420711a..7e8ad8c 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]; @@ -10,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/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/get-positioned-parent.ts b/src/dom/get-positioned-parent.ts new file mode 100644 index 0000000..814a66c --- /dev/null +++ b/src/dom/get-positioned-parent.ts @@ -0,0 +1,21 @@ +/** + * 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 { + let parent: HTMLElement | null = element.parentElement; + + while (parent) { + const position = window.getComputedStyle(parent).position; + + if (position === 'relative' || position === 'absolute') { + return parent; + } + + parent = parent.parentElement; + } + + return document.documentElement; +} 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/getAncestors.ts b/src/dom/getAncestors.ts deleted file mode 100644 index 5573d22..0000000 --- a/src/dom/getAncestors.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { isNullOrUndefined } from '../type-predicates/isNullOrUndefined.js'; - -/** - * Gets all the elements that a given element is nested within - */ -export function getAncestors(element: HTMLElement): HTMLElement[] { - return _getAncestors(element); -} - -/** - * Internal function for building the array and navigating up the tree - */ -function _getAncestors(element: HTMLElement, result: HTMLElement[] = []): HTMLElement[] { - const parent: 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; - } -} diff --git a/src/dom/getPositionedParent.ts b/src/dom/getPositionedParent.ts deleted file mode 100644 index 7c307a7..0000000 --- a/src/dom/getPositionedParent.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Gets the first parent element with a relative or absolute position - */ -export function getPositionedParent(element: HTMLElement): HTMLElement { - const parent: HTMLElement | null = element.parentElement; - - if (parent) { - const style: CSSStyleDeclaration = window.getComputedStyle(parent); - - if (['relative', 'absolute'].includes(style.position)) { - return parent; - } else { - return getPositionedParent(parent); - } - } - - return document.documentElement; -} 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..ece4bfb 100644 --- a/src/format-time/format-time.ts +++ b/src/format-time/format-time.ts @@ -1,51 +1,59 @@ -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 */ +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 timeMs = unitToMS(time, timeUnit); + const { forceAllUnits, timeUnit, secondsDecimalPlaces, hourSuffix, minuteSuffix, secondSuffix, padDecimals } = options + ? { ...DEFAULT_FORMAT_OPTIONS, ...options } + : DEFAULT_FORMAT_OPTIONS; + + 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 a311180..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 = { @@ -52,45 +56,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..85f687e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,9 +7,11 @@ 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'; +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/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/random-number-between-range.ts b/src/math/random-number-between-range.ts new file mode 100644 index 0000000..ef0431c --- /dev/null +++ b/src/math/random-number-between-range.ts @@ -0,0 +1,36 @@ +/** + * 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 { + const minCeil = Math.ceil(min); + const maxFloor = Math.floor(max); + + const errorMessage = `Cannot generate a whole number between ${min} and ${max}`; + + 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/math/randomNumberBetweenRange.ts b/src/math/randomNumberBetweenRange.ts deleted file mode 100644 index ca6f809..0000000 --- a/src/math/randomNumberBetweenRange.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Returns a random whole number between a given range - */ -export function randomNumberBetweenRange(min: number, max: number): number { - min = Math.ceil(min); - max = Math.floor(max); - - const value = Math.random() * (max - min + 1) + min; - - return Math.floor(value); -} diff --git a/src/math/sum.ts b/src/math/sum.ts index 22cd194..afd58d3 100644 --- a/src/math/sum.ts +++ b/src/math/sum.ts @@ -1,6 +1,15 @@ /** - * 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); + let total = 0; + + for (let i = 0, length = values.length; i < length; i++) { + total += values[i]; + } + + return total; } 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 0779a04..9010bba 100644 --- a/src/merge/merge.ts +++ b/src/merge/merge.ts @@ -1,121 +1,157 @@ -import { isMergeableObject } from '../type-predicates/isMergableObject.js'; +import { isMergeableObject as defaultIsMergeableObject } from '../type-predicates/is-mergeable-object.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 xLen = x.length; + const result = new Array(xLen + y.length) as unknown[]; + + 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: 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)[] { + 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: string): boolean { +function propertyIsOnObject(object: object, property: PropertyKey): boolean { try { return property in object; } catch (_) { @@ -124,18 +160,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 +186,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 +200,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..1917792 --- /dev/null +++ b/src/sorting/sort-by.ts @@ -0,0 +1,147 @@ +import { isNullOrUndefined } from '../type-predicates/is-null-or-undefined.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) { + const n = Number(value); + + if (!isNaN(n)) { + return n; + } + } + + 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/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/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/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/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/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 9fd5c01..518b87e 100644 --- a/src/type-predicates/index.ts +++ b/src/type-predicates/index.ts @@ -1,19 +1,19 @@ -export * from './isArguments.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 './isMergableObject.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/is-arguments.ts b/src/type-predicates/is-arguments.ts new file mode 100644 index 0000000..434af15 --- /dev/null +++ b/src/type-predicates/is-arguments.ts @@ -0,0 +1,9 @@ +/** + * 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: unknown): boolean { + return Object.prototype.toString.call(value) === '[object Arguments]'; +} diff --git a/src/type-predicates/is-array.spec.ts b/src/type-predicates/is-array.spec.ts new file mode 100644 index 0000000..2cd52a8 --- /dev/null +++ b/src/type-predicates/is-array.spec.ts @@ -0,0 +1,15 @@ +import { isArray } from './is-array.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/is-array.ts b/src/type-predicates/is-array.ts new file mode 100644 index 0000000..6e3d096 --- /dev/null +++ b/src/type-predicates/is-array.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.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/is-boolean.ts b/src/type-predicates/is-boolean.ts new file mode 100644 index 0000000..3866897 --- /dev/null +++ b/src/type-predicates/is-boolean.ts @@ -0,0 +1,9 @@ +/** + * 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: unknown): value is boolean { + return typeof value === 'boolean'; +} 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/is-buffer.ts b/src/type-predicates/is-buffer.ts new file mode 100644 index 0000000..b269ed8 --- /dev/null +++ b/src/type-predicates/is-buffer.ts @@ -0,0 +1,27 @@ +import { isNullOrUndefined } from './is-null-or-undefined.js'; + +/** + * 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: unknown): value is Buffer { + if (value === null || value === undefined || typeof value !== 'object') { + 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.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/is-date.ts b/src/type-predicates/is-date.ts new file mode 100644 index 0000000..d82c3d6 --- /dev/null +++ b/src/type-predicates/is-date.ts @@ -0,0 +1,9 @@ +/** + * 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: unknown): value is Date { + return value instanceof Date; +} 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/is-empty.ts b/src/type-predicates/is-empty.ts new file mode 100644 index 0000000..f52b4df --- /dev/null +++ b/src/type-predicates/is-empty.ts @@ -0,0 +1,73 @@ +import { isNullOrUndefined } from './is-null-or-undefined.js'; +import { typeOf } from './type-of.js'; + +type EmptyString = ''; +type EmptyObject = Record; +type EmptyArray = never[]; +type EmptySet = Set; +type EmptyMap = Map; + +type Empty = EmptyArray | EmptyObject | EmptyString | EmptySet | EmptyMap; + +type EmptyResult = T extends string + ? EmptyString + : T extends unknown[] + ? EmptyArray + : T extends Set + ? EmptySet + : T extends Map + ? EmptyMap + : T extends object + ? EmptyObject + : never; + +/** + * 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(value: T | Empty | null | undefined): value is EmptyResult | null | undefined { + if (isNullOrUndefined(value)) { + return true; + } + + const type = typeOf(value); + + switch (type) { + case 'set': + case 'map': { + return (value as Set | Map).size === 0; + } + case 'array': { + return (value as unknown[]).length === 0; + } + case 'object': { + const obj = value as object; + // 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(); + } + default: { + return false; + } + } +} 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/is-equal.ts b/src/type-predicates/is-equal.ts new file mode 100644 index 0000000..4d567ca --- /dev/null +++ b/src/type-predicates/is-equal.ts @@ -0,0 +1,149 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ + +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type +export type IndividualEqualityType = null | undefined | boolean | number | bigint | string | symbol | Date | object | Function; + +export type EqualityType = IndividualEqualityType | IndividualEqualityType[]; + +/** + * 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. + * + * @param a - The first value to compare. + * @param b - The second value to compare. + * + * @returns `true` if the values are deeply equal, otherwise `false`. + */ +export function isEqual(a: unknown, b: unknown): boolean { + return _isEqual(a, b, null); +} + +function _isEqual(a: any, b: any, visited: WeakMap> | null): boolean { + if (a === b) { + return true; + } + + if (a && b && typeof a === 'object' && typeof b === 'object') { + if (a.constructor !== b.constructor) { + return false; + } + + // 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) { + 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; + + if (aLength !== b.length) { + return false; + } + + for (let i = 0; i < aLength; i++) { + if (!_isEqual(a[i], b[i], visited)) { + return false; + } + } + + return true; + } + + if (a instanceof Map && b instanceof Map) { + if (a.size !== b.size) { + return false; + } + + for (const [key, val] of a) { + if (!b.has(key) || !_isEqual(val, b.get(key), visited)) { + return false; + } + } + + return true; + } + + if (a instanceof Set && b instanceof Set) { + if (a.size !== b.size) { + return false; + } + + for (const i of a.entries()) { + if (!b.has(i[0])) { + return false; + } + } + + return true; + } + + if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) { + 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 !== viewB.length) { + return false; + } + + for (let i = 0; i < length; i++) { + if (viewA[i] !== viewB[i]) { + return false; + } + } + + return true; + } + + if (a.constructor === RegExp) { + return a.source === (b as RegExp).source && a.flags === (b as RegExp).flags; + } + + // 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)(); + } + + if (a.toString !== Object.prototype.toString) { + return (a.toString as () => string)() === (b.toString as () => string)(); + } + + // 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 !== 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) || !_isEqual(a[key], b[key], visited)) { + return false; + } + } + + return true; + } + + return a !== a && b !== b; +} 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/is-error.ts b/src/type-predicates/is-error.ts new file mode 100644 index 0000000..0847a40 --- /dev/null +++ b/src/type-predicates/is-error.ts @@ -0,0 +1,9 @@ +/** + * 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: unknown): value is Error { + return value instanceof Error; +} 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/is-generator-object.ts b/src/type-predicates/is-generator-object.ts new file mode 100644 index 0000000..4ad9bf7 --- /dev/null +++ b/src/type-predicates/is-generator-object.ts @@ -0,0 +1,15 @@ +/** + * 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: 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/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/is-mergeable-object.ts b/src/type-predicates/is-mergeable-object.ts new file mode 100644 index 0000000..0235bee --- /dev/null +++ b/src/type-predicates/is-mergeable-object.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.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/is-nan-strict.ts b/src/type-predicates/is-nan-strict.ts new file mode 100644 index 0000000..3400004 --- /dev/null +++ b/src/type-predicates/is-nan-strict.ts @@ -0,0 +1,11 @@ +import { isNumber } from './is-number.js'; + +/** + * 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: unknown): boolean { + return isNumber(value) && Number.isNaN(value); +} 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/is-null-or-undefined.ts b/src/type-predicates/is-null-or-undefined.ts new file mode 100644 index 0000000..2f2496e --- /dev/null +++ b/src/type-predicates/is-null-or-undefined.ts @@ -0,0 +1,9 @@ +/** + * 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: unknown): value is null | undefined { + return value === null || value === undefined; +} 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/is-number.ts b/src/type-predicates/is-number.ts new file mode 100644 index 0000000..6d01241 --- /dev/null +++ b/src/type-predicates/is-number.ts @@ -0,0 +1,9 @@ +/** + * 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: unknown): value is number { + return typeof value === 'number'; +} 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/is-object.ts b/src/type-predicates/is-object.ts new file mode 100644 index 0000000..53f2dfc --- /dev/null +++ b/src/type-predicates/is-object.ts @@ -0,0 +1,12 @@ +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). + * + * @param value - The value to check. + * @returns `true` if the value is an object, otherwise `false`. + */ +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.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/is-plain-object.ts b/src/type-predicates/is-plain-object.ts new file mode 100644 index 0000000..4377f23 --- /dev/null +++ b/src/type-predicates/is-plain-object.ts @@ -0,0 +1,35 @@ +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). + * + * @param value - The value to check. + * @returns `true` if the value is a plain object, otherwise `false`. + */ +export function isPlainObject(value: unknown): value is object { + if (!isObject(value)) { + return false; + } + + if (Object.getPrototypeOf(value) === null) { + return true; + } + + const ctor = (value as Record).constructor; + + if (typeof ctor !== 'function') { + return false; + } + + const { prototype } = ctor as { prototype: unknown }; + + if (!isObject(prototype)) { + return false; + } + + if (!Object.prototype.hasOwnProperty.call(prototype, 'isPrototypeOf')) { + return false; + } + + return true; +} 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/is-regexp.ts b/src/type-predicates/is-regexp.ts new file mode 100644 index 0000000..dafc6d9 --- /dev/null +++ b/src/type-predicates/is-regexp.ts @@ -0,0 +1,9 @@ +/** + * 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: unknown): value is RegExp { + return value instanceof RegExp; +} 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/is-string.ts b/src/type-predicates/is-string.ts new file mode 100644 index 0000000..711e39f --- /dev/null +++ b/src/type-predicates/is-string.ts @@ -0,0 +1,9 @@ +/** + * 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: unknown): value is string { + return typeof value === 'string'; +} diff --git a/src/type-predicates/isArguments.ts b/src/type-predicates/isArguments.ts deleted file mode 100644 index 1e5e4cb..0000000 --- a/src/type-predicates/isArguments.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ - -/** - * Determines if a value is an Arguments object. - */ -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; -} diff --git a/src/type-predicates/isBoolean.ts b/src/type-predicates/isBoolean.ts deleted file mode 100644 index 3995b8a..0000000 --- a/src/type-predicates/isBoolean.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Determines whether a given value is a boolean - */ -export function isBoolean(value: any): value is boolean { - return typeof value === 'boolean'; -} diff --git a/src/type-predicates/isBuffer.ts b/src/type-predicates/isBuffer.ts deleted file mode 100644 index 3eba3de..0000000 --- a/src/type-predicates/isBuffer.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ - -/** - * Determines if a value is a Buffer. - */ -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); - } - - return false; -} diff --git a/src/type-predicates/isDate.ts b/src/type-predicates/isDate.ts deleted file mode 100644 index d4a184e..0000000 --- a/src/type-predicates/isDate.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Determines whether a given value is a date - */ -export function isDate(value: any): value is Date { - return value instanceof Date; -} diff --git a/src/type-predicates/isEmpty.ts b/src/type-predicates/isEmpty.ts deleted file mode 100644 index b32776d..0000000 --- a/src/type-predicates/isEmpty.ts +++ /dev/null @@ -1,57 +0,0 @@ -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 Empty = Empty.Array | Empty.Object | Empty.String | Empty._Set | Empty._Map; - -type EmptyResult = T extends string - ? Empty.String - : T extends any[] - ? Empty.Array - : T extends Set - ? Empty._Set - : T extends Map - ? Empty._Map - : T extends object - ? Empty.Object - : never; - -/** - * Checks if a given value is empty - */ -export function isEmpty | Map | any[] | object | null | undefined>( - value: T | Empty | null | undefined -): value is EmptyResult | null | undefined { - if (isNullOrUndefined(value)) { - return true; - } - - const type = typeOf(value); - - switch (type) { - case 'set': - case 'map': { - return (value as Set | Map).size === 0; - } - } - - switch (typeof value) { - case 'object': { - return !Object.keys(value).length; - } - case 'string': { - return !value.trim(); - } - default: { - return false; - } - } -} 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/isEqual.ts b/src/type-predicates/isEqual.ts deleted file mode 100644 index 1d2ccfd..0000000 --- a/src/type-predicates/isEqual.ts +++ /dev/null @@ -1,140 +0,0 @@ -/* 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 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. - * - * Functions and DOM nodes are compared by strict equality, i.e. ===. - * - * The order of the array items must be the same for the arrays to be equal. - */ -export function isEqual(a: any, b: any): boolean { - if (a === b) { - return true; - } - - if (a && b && typeof a === 'object' && typeof b === 'object') { - if (a.constructor !== b.constructor) { - return false; - } - - if (Array.isArray(a)) { - const aLength = a.length; - - if (aLength !== b.length) { - return false; - } - - for (let i = 0; i < aLength; i++) { - if (!isEqual(a[i], b[i])) { - return false; - } - } - - return true; - } - - if (a instanceof Map && b instanceof Map) { - if (a.size !== b.size) { - 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]))) { - return false; - } - } - - return true; - } - - if (a instanceof Set && b instanceof Set) { - if (a.size !== b.size) { - return false; - } - - for (const i of a.entries()) { - if (!b.has(i[0])) { - return false; - } - } - - return true; - } - - if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const length: number = (a as any).length; - - if (length !== (b as any).length) { - return false; - } - - for (let i = 0; i < length; i++) { - if (a[i] !== b[i]) { - return false; - } - } - - return true; - } - - if (a.constructor === RegExp) { - return a.source === (b as RegExp).source && a.flags === (b as RegExp).flags; - } - - if (a.valueOf !== Object.prototype.valueOf) { - return a.valueOf() === b.valueOf(); - } - - if (a.toString !== Object.prototype.toString) { - return a.toString() === b.toString(); - } - - 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 keysALength = keysA.length; - - if (keysALength !== keysB.length) { - return false; - } - - for (let i = 0; i < keysALength; i++) { - const key = keysA[i]; - - if (!isEqual(a[key], b[key])) { - return false; - } - } - - return true; - } - - return a !== a && b !== b; -} diff --git a/src/type-predicates/isError.ts b/src/type-predicates/isError.ts deleted file mode 100644 index f2bbc69..0000000 --- a/src/type-predicates/isError.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Determines whether the given value is an Error. - */ -export function isError(value: any): value is Error { - return value instanceof Error; -} diff --git a/src/type-predicates/isGeneratorObject.ts b/src/type-predicates/isGeneratorObject.ts deleted file mode 100644 index ac098a8..0000000 --- a/src/type-predicates/isGeneratorObject.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Determines whether the given value is a generator object. - */ -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'; -} 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/isNaNStrict.ts b/src/type-predicates/isNaNStrict.ts deleted file mode 100644 index ef87a17..0000000 --- a/src/type-predicates/isNaNStrict.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { isNumber } from './isNumber.js'; - -/** - * Determines whether a given value is a NaN instance - */ -export function isNaNStrict(value: any): boolean { - return isNumber(value) && isNaN(value); -} 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/isNullOrUndefined.ts b/src/type-predicates/isNullOrUndefined.ts deleted file mode 100644 index c83fb28..0000000 --- a/src/type-predicates/isNullOrUndefined.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Determines whether a given value is null or undefined - */ -export function isNullOrUndefined(value: any): value is null | undefined { - return value === null || value === undefined; -} diff --git a/src/type-predicates/isNumber.ts b/src/type-predicates/isNumber.ts deleted file mode 100644 index c20e10b..0000000 --- a/src/type-predicates/isNumber.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Determines whether a given value is a number - */ -export function isNumber(value: any): value is number { - return typeof value === 'number'; -} diff --git a/src/type-predicates/isObject.ts b/src/type-predicates/isObject.ts deleted file mode 100644 index cc3f28f..0000000 --- a/src/type-predicates/isObject.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { isDate } from './isDate.js'; -import { isNullOrUndefined } from './isNullOrUndefined.js'; - -/** - * Determines whether a given value is an object - */ -export function isObject(value: any): 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 deleted file mode 100644 index 566f7ad..0000000 --- a/src/type-predicates/isPlainObject.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { isObject } from './isObject.js'; - -/** - * Determines whether the given value is a plain object. - */ -export function isPlainObject(value: any): value is object { - if (!isObject(value)) { - return false; - } - - const constructor = value.constructor; - - // If it has a modified constructor - if (constructor === undefined) { - return true; - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const prototype = constructor.prototype; - - // 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 deleted file mode 100644 index ed80484..0000000 --- a/src/type-predicates/isRegExp.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Determines whether a given value is a RegExp - */ -export function isRegExp(value: any): value is RegExp { - return value instanceof RegExp; -} diff --git a/src/type-predicates/isString.ts b/src/type-predicates/isString.ts deleted file mode 100644 index e3f0b23..0000000 --- a/src/type-predicates/isString.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Determines whether a given value is a string - */ -export function isString(value: any): value is string { - return typeof value === 'string'; -} diff --git a/src/type-predicates/typeOf.spec.ts b/src/type-predicates/type-of.spec.ts similarity index 53% rename from src/type-predicates/typeOf.spec.ts rename to src/type-predicates/type-of.spec.ts index e700cf3..19c5827 100644 --- a/src/type-predicates/typeOf.spec.ts +++ b/src/type-predicates/type-of.spec.ts @@ -1,75 +1,70 @@ -import moment from 'moment'; -import { typeOf, ValueType } from './typeOf'; +import { typeOf, ValueType } from './type-of.js'; 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,96 @@ 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); + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + expect(typeOf(Promise.reject().catch(() => {}))).toBe(ValueType.Promise); }); }); @@ -175,175 +171,181 @@ 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/type-of.ts b/src/type-predicates/type-of.ts new file mode 100644 index 0000000..1d46c66 --- /dev/null +++ b/src/type-predicates/type-of.ts @@ -0,0 +1,212 @@ +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', + 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', +} + +/** + * 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: unknown): ValueType | string { + if (value === void 0) { + return ValueType.Undefined; + } + + if (value === null) { + return ValueType.Null; + } + + let type: string = typeof value; + + if (type === 'boolean') { + return ValueType.Boolean; + } + + if (type === 'string') { + return ValueType.String; + } + + if (type === 'number') { + return ValueType.Number; + } + + if (type === 'symbol') { + return ValueType.Symbol; + } + + if (type === 'function') { + return isGeneratorFunction(value) ? ValueType.GeneratorFunction : ValueType.Function; + } + + if (Array.isArray(value)) { + return ValueType.Array; + } + + if (isBuffer(value)) { + return ValueType.Buffer; + } + + if (isArguments(value)) { + return ValueType.Arguments; + } + + if (isDate(value)) { + return ValueType.Date; + } + + if (isError(value)) { + return ValueType.Error; + } + + if (isRegExp(value)) { + return ValueType.RegExp; + } + + switch (constructorName(value)) { + case 'Symbol': { + return ValueType.Symbol; + } + case 'Promise': { + return ValueType.Promise; + } + case 'WeakMap': { + return ValueType.WeakMap; + } + case 'WeakSet': { + return ValueType.WeakSet; + } + case 'Map': { + return ValueType.Map; + } + case 'Set': { + return ValueType.Set; + } + case 'Int8Array': { + return ValueType.Int8Array; + } + case 'Uint8Array': { + return ValueType.Uint8Array; + } + case 'Uint8ClampedArray': { + return ValueType.Uint8ClampedArray; + } + case 'Int16Array': { + return ValueType.Int16Array; + } + case 'Uint16Array': { + return ValueType.Uint16Array; + } + case 'Int32Array': { + return ValueType.Int32Array; + } + case 'Uint32Array': { + return ValueType.Uint32Array; + } + case 'Float32Array': { + return ValueType.Float32Array; + } + case 'Float64Array': { + return ValueType.Float64Array; + } + case 'BigInt64Array': { + return ValueType.BigInt64Array; + } + case 'BigUint64Array': { + return ValueType.BigUint64Array; + } + } + + if (isGeneratorObject(value)) { + return ValueType.Generator; + } + + type = Object.prototype.toString.call(value); + + switch (type) { + case '[object Object]': { + return ValueType.Object; + } + case '[object Map Iterator]': { + return ValueType.MapIterator; + } + case '[object Set Iterator]': { + return ValueType.SetIterator; + } + case '[object String Iterator]': { + return ValueType.StringIterator; + } + case '[object Array Iterator]': { + return ValueType.ArrayIterator; + } + } + + return type.slice(8, -1).toLowerCase().replace(/\s/g, ''); +} + +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: unknown): value is GeneratorFunction { + const name = constructorName(value); + + return name === 'GeneratorFunction' || name === 'GeneratorFunction.js'; +} diff --git a/src/type-predicates/typeOf.ts b/src/type-predicates/typeOf.ts deleted file mode 100644 index be999f2..0000000 --- a/src/type-predicates/typeOf.ts +++ /dev/null @@ -1,201 +0,0 @@ -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 { 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', -} - -/** - * Determines the type of the given value - */ -export function typeOf(value: any): ValueType | string { - if (value === void 0) { - return ValueType.undefined; - } - - if (value === null) { - return ValueType.null; - } - - if (isMoment(value)) { - return ValueType.moment; - } - - let type: string = typeof value; - - if (type === 'boolean') { - return ValueType.boolean; - } - - if (type === 'string') { - return ValueType.string; - } - - if (type === 'number') { - return ValueType.number; - } - - if (type === 'symbol') { - return ValueType.symbol; - } - - if (type === 'function') { - return isGeneratorFunction(value) ? ValueType.generatorfunction : ValueType.function; - } - - if (Array.isArray(value)) { - return ValueType.array; - } - - if (isBuffer(value)) { - return ValueType.buffer; - } - - if (isArguments(value)) { - return ValueType.arguments; - } - - if (isDate(value)) { - return ValueType.date; - } - - if (isError(value)) { - return ValueType.error; - } - - if (isRegExp(value)) { - return ValueType.regexp; - } - - switch (constructorName(value)) { - case 'Symbol': { - return ValueType.symbol; - } - case 'Promise': { - return ValueType.promise; - } - case 'WeakMap': { - return ValueType.weakmap; - } - case 'WeakSet': { - return ValueType.weakset; - } - case 'Map': { - return ValueType.map; - } - case 'Set': { - return ValueType.set; - } - case 'Int8Array': { - return ValueType.int8array; - } - case 'Uint8Array': { - return ValueType.uint8array; - } - case 'Uint8ClampedArray': { - return ValueType.uint8clampedarray; - } - case 'Int16Array': { - return ValueType.int16array; - } - case 'Uint16Array': { - return ValueType.uint16array; - } - case 'Int32Array': { - return ValueType.int32array; - } - case 'Uint32Array': { - return ValueType.uint32array; - } - case 'Float32Array': { - return ValueType.float32array; - } - case 'Float64Array': { - return ValueType.float64array; - } - case 'BigInt64Array': { - return ValueType.bigint64array; - } - case 'BigUint64Array': { - return ValueType.biguint64array; - } - } - - if (isGeneratorObject(value)) { - return ValueType.generator; - } - - type = toString.call(value); - - switch (type) { - case '[object Object]': { - return ValueType.object; - } - case '[object Map Iterator]': { - return ValueType.mapiterator; - } - case '[object Set Iterator]': { - return ValueType.setiterator; - } - case '[object String Iterator]': { - return ValueType.stringiterator; - } - case '[object Array Iterator]': { - 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 isGeneratorFunction(value: any): value is GeneratorFunction { - const name = constructorName(value); - - return name === 'GeneratorFunction' || name === 'GeneratorFunction.js'; -} 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',