From 944e9355716a4640f718c0cff7acf7806425b105 Mon Sep 17 00:00:00 2001 From: Nev Date: Sun, 19 Jul 2026 13:17:41 -0700 Subject: [PATCH 1/3] feat(array): add arrIndexOfSubset() and arrLastIndexOfSubset() Locates the first/last index at which an array of values matches contiguously within another, re-searching later/earlier occurrences of the leading value when it repeats (e.g. matching one stack trace against another) instead of stopping at the first, wrong occurrence. A subset that runs off the end of theArray still matches against its available prefix, so a shorter (e.g. truncated) array can be matched against a longer one, or vice-versa. --- .size-limit.json | 8 +- CHANGELOG.md | 4 +- README.md | 4 +- lib/src/array/arrIndexOfSubset.ts | 197 ++++++++++++++++++ lib/src/index.ts | 1 + lib/test/bundle-size-check.js | 4 +- .../src/common/array/arrIndexOfSubset.test.ts | 174 ++++++++++++++++ 7 files changed, 383 insertions(+), 9 deletions(-) create mode 100644 lib/src/array/arrIndexOfSubset.ts create mode 100644 lib/test/src/common/array/arrIndexOfSubset.test.ts diff --git a/.size-limit.json b/.size-limit.json index 4c18c11c..bcc1ea5a 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -2,21 +2,21 @@ { "name": "es5-full", "path": "lib/dist/es5/mod/ts-utils.js", - "limit": "37.5 kb", + "limit": "37.75 kb", "brotli": false, "running": false }, { "name": "es6-full", "path": "lib/dist/es6/mod/ts-utils.js", - "limit": "36 kb", + "limit": "36.5 kb", "brotli": false, "running": false }, { "name": "es5-full-brotli", "path": "lib/dist/es5/mod/ts-utils.js", - "limit": "12.75 kb", + "limit": "13 kb", "brotli": true, "running": false }, @@ -30,7 +30,7 @@ { "name": "es5-zip", "path": "lib/dist/es5/mod/ts-utils.js", - "limit": "14 Kb", + "limit": "14.5 Kb", "gzip": true, "running": false }, diff --git a/CHANGELOG.md b/CHANGELOG.md index a6e722e0..15ceac9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,11 @@ -# Unreleased +# Unreleased (Proposed 0.17.0) ## Changelog ### Features +- feat(array): add `arrIndexOfSubset()` and `arrLastIndexOfSubset()` to locate the first/last index at which an array of values matches contiguously within another, re-searching later/earlier occurrences of the leading value when it repeats (eg. matching one stack trace against another) instead of stopping at the first, wrong occurrence + ### Bug Fixes # v0.16.0 July 18th, 2026 diff --git a/README.md b/README.md index 9fa567b3..4a424057 100644 --- a/README.md +++ b/README.md @@ -120,8 +120,8 @@ Below is a categorized list of all available utilities with direct links to thei | Value Check | [hasValue](https://nevware21.github.io/ts-utils/typedoc/functions/hasValue.html)(); [isDefined](https://nevware21.github.io/ts-utils/typedoc/functions/isDefined.html)(); [isEmpty](https://nevware21.github.io/ts-utils/typedoc/functions/isEmpty.html)(); [isNotTruthy](https://nevware21.github.io/ts-utils/typedoc/functions/isNotTruthy.html)(); [isNullOrUndefined](https://nevware21.github.io/ts-utils/typedoc/functions/isNullOrUndefined.html)(); [isStrictNullOrUndefined](https://nevware21.github.io/ts-utils/typedoc/functions/isStrictNullOrUndefined.html)(); [isStrictUndefined](https://nevware21.github.io/ts-utils/typedoc/functions/isStrictUndefined.html)(); [isTruthy](https://nevware21.github.io/ts-utils/typedoc/functions/isTruthy.html)(); [isUndefined](https://nevware21.github.io/ts-utils/typedoc/functions/isUndefined.html)(); | Value | [getValueByKey](https://nevware21.github.io/ts-utils/typedoc/functions/getValueByKey.html)(); [setValueByKey](https://nevware21.github.io/ts-utils/typedoc/functions/setValueByKey.html)(); [getValueByIter](https://nevware21.github.io/ts-utils/typedoc/functions/getValueByIter.html)(); [setValueByIter](https://nevware21.github.io/ts-utils/typedoc/functions/setValueByIter.html)(); [encodeAsJson](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsJson.html)(); [encodeAsHtml](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsHtml.html)(); [asString](https://nevware21.github.io/ts-utils/typedoc/functions/asString.html)(); [getIntValue](https://nevware21.github.io/ts-utils/typedoc/functions/getIntValue.html)(); [normalizeJsName](https://nevware21.github.io/ts-utils/typedoc/functions/normalizeJsName.html)(); |   |   -| Array | [arrAppend](https://nevware21.github.io/ts-utils/typedoc/functions/arrAppend.html)(); [arrConcat](https://nevware21.github.io/ts-utils/typedoc/functions/arrConcat.html)(); [arrAt](https://nevware21.github.io/ts-utils/typedoc/functions/arrAt.html)(); [arrChunk](https://nevware21.github.io/ts-utils/typedoc/functions/arrChunk.html)(); [arrCompact](https://nevware21.github.io/ts-utils/typedoc/functions/arrCompact.html)(); [arrContains](https://nevware21.github.io/ts-utils/typedoc/functions/arrContains.html)(); [arrDifference](https://nevware21.github.io/ts-utils/typedoc/functions/arrDifference.html)(); [arrDrop](https://nevware21.github.io/ts-utils/typedoc/functions/arrDrop.html)(); [arrDropWhile](https://nevware21.github.io/ts-utils/typedoc/functions/arrDropWhile.html)(); [arrEvery](https://nevware21.github.io/ts-utils/typedoc/functions/arrEvery.html)(); [arrFilter](https://nevware21.github.io/ts-utils/typedoc/functions/arrFilter.html)(); [arrFill](https://nevware21.github.io/ts-utils/typedoc/functions/arrFill.html)(); [arrFind](https://nevware21.github.io/ts-utils/typedoc/functions/arrFind.html)(); [arrFindIndex](https://nevware21.github.io/ts-utils/typedoc/functions/arrFindIndex.html)(); [arrFindLast](https://nevware21.github.io/ts-utils/typedoc/functions/arrFindLast.html)(); [arrFindLastIndex](https://nevware21.github.io/ts-utils/typedoc/functions/arrFindLastIndex.html)(); [arrFlatten](https://nevware21.github.io/ts-utils/typedoc/functions/arrFlatten.html)(); [arrForEach](https://nevware21.github.io/ts-utils/typedoc/functions/arrForEach.html)(); [arrFrom](https://nevware21.github.io/ts-utils/typedoc/functions/arrFrom.html)(); [arrGroupBy](https://nevware21.github.io/ts-utils/typedoc/functions/arrGroupBy.html)(); [arrIncludes](https://nevware21.github.io/ts-utils/typedoc/functions/arrIncludes.html)(); [arrIndexKeys](https://nevware21.github.io/ts-utils/typedoc/functions/arrIndexKeys.html)(); [arrIndexOf](https://nevware21.github.io/ts-utils/typedoc/functions/arrIndexOf.html)(); [arrIntersection](https://nevware21.github.io/ts-utils/typedoc/functions/arrIntersection.html)(); [arrKeys](https://nevware21.github.io/ts-utils/typedoc/functions/arrKeys.html)(); [arrLastIndexOf](https://nevware21.github.io/ts-utils/typedoc/functions/arrLastIndexOf.html)(); [arrMap](https://nevware21.github.io/ts-utils/typedoc/functions/arrMap.html)(); [arrPartition](https://nevware21.github.io/ts-utils/typedoc/functions/arrPartition.html)(); [arrReduce](https://nevware21.github.io/ts-utils/typedoc/functions/arrReduce.html)(); [arrReverse](https://nevware21.github.io/ts-utils/typedoc/functions/arrReverse.html)(); [arrRotate](https://nevware21.github.io/ts-utils/typedoc/functions/arrRotate.html)(); [arrSample](https://nevware21.github.io/ts-utils/typedoc/functions/arrSample.html)(); [arrShuffle](https://nevware21.github.io/ts-utils/typedoc/functions/arrShuffle.html)(); [arrSlice](https://nevware21.github.io/ts-utils/typedoc/functions/arrSlice.html)(); [arrSome](https://nevware21.github.io/ts-utils/typedoc/functions/arrSome.html)(); [arrTake](https://nevware21.github.io/ts-utils/typedoc/functions/arrTake.html)(); [arrTakeWhile](https://nevware21.github.io/ts-utils/typedoc/functions/arrTakeWhile.html)(); [arrUnion](https://nevware21.github.io/ts-utils/typedoc/functions/arrUnion.html)(); [arrUnique](https://nevware21.github.io/ts-utils/typedoc/functions/arrUnique.html)(); [arrUnzip](https://nevware21.github.io/ts-utils/typedoc/functions/arrUnzip.html)(); [arrWith](https://nevware21.github.io/ts-utils/typedoc/functions/arrWith.html)(); [arrZip](https://nevware21.github.io/ts-utils/typedoc/functions/arrZip.html)(); [getLength](https://nevware21.github.io/ts-utils/typedoc/functions/getLength.html)(); [isArray](https://nevware21.github.io/ts-utils/typedoc/functions/isArray.html)();
[polyIsArray](https://nevware21.github.io/ts-utils/typedoc/functions/polyIsArray.html)(); [polyArrIncludes](https://nevware21.github.io/ts-utils/typedoc/functions/polyArrIncludes.html)(); [polyArrFind](https://nevware21.github.io/ts-utils/typedoc/functions/polyArrFind.html)(); [polyArrFindIndex](https://nevware21.github.io/ts-utils/typedoc/functions/polyArrFindIndex.html)(); [polyArrFindLast](https://nevware21.github.io/ts-utils/typedoc/functions/polyArrFindLast.html)(); [polyArrFindLastIndex](https://nevware21.github.io/ts-utils/typedoc/functions/polyArrFindLastIndex.html)(); [polyArrFrom](https://nevware21.github.io/ts-utils/typedoc/functions/polyArrFrom.html)();
-| ArrayLike | [arrAt](https://nevware21.github.io/ts-utils/typedoc/functions/arrAt.html)(); [arrChunk](https://nevware21.github.io/ts-utils/typedoc/functions/arrChunk.html)(); [arrCompact](https://nevware21.github.io/ts-utils/typedoc/functions/arrCompact.html)(); [arrContains](https://nevware21.github.io/ts-utils/typedoc/functions/arrContains.html)(); [arrDifference](https://nevware21.github.io/ts-utils/typedoc/functions/arrDifference.html)(); [arrDrop](https://nevware21.github.io/ts-utils/typedoc/functions/arrDrop.html)(); [arrDropWhile](https://nevware21.github.io/ts-utils/typedoc/functions/arrDropWhile.html)(); [arrEvery](https://nevware21.github.io/ts-utils/typedoc/functions/arrEvery.html)(); [arrFilter](https://nevware21.github.io/ts-utils/typedoc/functions/arrFilter.html)(); [arrFill](https://nevware21.github.io/ts-utils/typedoc/functions/arrFill.html)(); [arrFind](https://nevware21.github.io/ts-utils/typedoc/functions/arrFind.html)(); [arrFindIndex](https://nevware21.github.io/ts-utils/typedoc/functions/arrFindIndex.html)(); [arrFindLast](https://nevware21.github.io/ts-utils/typedoc/functions/arrFindLast.html)(); [arrFindLastIndex](https://nevware21.github.io/ts-utils/typedoc/functions/arrFindLastIndex.html)(); [arrFlatten](https://nevware21.github.io/ts-utils/typedoc/functions/arrFlatten.html)(); [arrForEach](https://nevware21.github.io/ts-utils/typedoc/functions/arrForEach.html)(); [arrFrom](https://nevware21.github.io/ts-utils/typedoc/functions/arrFrom.html)(); [arrGroupBy](https://nevware21.github.io/ts-utils/typedoc/functions/arrGroupBy.html)(); [arrIncludes](https://nevware21.github.io/ts-utils/typedoc/functions/arrIncludes.html)(); [arrIndexKeys](https://nevware21.github.io/ts-utils/typedoc/functions/arrIndexKeys.html)(); [arrIndexOf](https://nevware21.github.io/ts-utils/typedoc/functions/arrIndexOf.html)(); [arrIntersection](https://nevware21.github.io/ts-utils/typedoc/functions/arrIntersection.html)(); [arrKeys](https://nevware21.github.io/ts-utils/typedoc/functions/arrKeys.html)(); [arrLastIndexOf](https://nevware21.github.io/ts-utils/typedoc/functions/arrLastIndexOf.html)(); [arrMap](https://nevware21.github.io/ts-utils/typedoc/functions/arrMap.html)(); [arrPartition](https://nevware21.github.io/ts-utils/typedoc/functions/arrPartition.html)(); [arrReduce](https://nevware21.github.io/ts-utils/typedoc/functions/arrReduce.html)(); [arrReverse](https://nevware21.github.io/ts-utils/typedoc/functions/arrReverse.html)(); [arrRotate](https://nevware21.github.io/ts-utils/typedoc/functions/arrRotate.html)(); [arrSample](https://nevware21.github.io/ts-utils/typedoc/functions/arrSample.html)(); [arrShuffle](https://nevware21.github.io/ts-utils/typedoc/functions/arrShuffle.html)(); [arrSlice](https://nevware21.github.io/ts-utils/typedoc/functions/arrSlice.html)(); [arrSome](https://nevware21.github.io/ts-utils/typedoc/functions/arrSome.html)(); [arrTake](https://nevware21.github.io/ts-utils/typedoc/functions/arrTake.html)(); [arrTakeWhile](https://nevware21.github.io/ts-utils/typedoc/functions/arrTakeWhile.html)(); [arrUnion](https://nevware21.github.io/ts-utils/typedoc/functions/arrUnion.html)(); [arrUnique](https://nevware21.github.io/ts-utils/typedoc/functions/arrUnique.html)(); [arrUnzip](https://nevware21.github.io/ts-utils/typedoc/functions/arrUnzip.html)(); [arrWith](https://nevware21.github.io/ts-utils/typedoc/functions/arrWith.html)(); [arrZip](https://nevware21.github.io/ts-utils/typedoc/functions/arrZip.html)(); [getLength](https://nevware21.github.io/ts-utils/typedoc/functions/getLength.html)(); [objEntries](https://nevware21.github.io/ts-utils/typedoc/functions/objEntries.html)(); [objValues](https://nevware21.github.io/ts-utils/typedoc/functions/objValues.html)(); +| Array | [arrAppend](https://nevware21.github.io/ts-utils/typedoc/functions/arrAppend.html)(); [arrConcat](https://nevware21.github.io/ts-utils/typedoc/functions/arrConcat.html)(); [arrAt](https://nevware21.github.io/ts-utils/typedoc/functions/arrAt.html)(); [arrChunk](https://nevware21.github.io/ts-utils/typedoc/functions/arrChunk.html)(); [arrCompact](https://nevware21.github.io/ts-utils/typedoc/functions/arrCompact.html)(); [arrContains](https://nevware21.github.io/ts-utils/typedoc/functions/arrContains.html)(); [arrDifference](https://nevware21.github.io/ts-utils/typedoc/functions/arrDifference.html)(); [arrDrop](https://nevware21.github.io/ts-utils/typedoc/functions/arrDrop.html)(); [arrDropWhile](https://nevware21.github.io/ts-utils/typedoc/functions/arrDropWhile.html)(); [arrEvery](https://nevware21.github.io/ts-utils/typedoc/functions/arrEvery.html)(); [arrFilter](https://nevware21.github.io/ts-utils/typedoc/functions/arrFilter.html)(); [arrFill](https://nevware21.github.io/ts-utils/typedoc/functions/arrFill.html)(); [arrFind](https://nevware21.github.io/ts-utils/typedoc/functions/arrFind.html)(); [arrFindIndex](https://nevware21.github.io/ts-utils/typedoc/functions/arrFindIndex.html)(); [arrFindLast](https://nevware21.github.io/ts-utils/typedoc/functions/arrFindLast.html)(); [arrFindLastIndex](https://nevware21.github.io/ts-utils/typedoc/functions/arrFindLastIndex.html)(); [arrFlatten](https://nevware21.github.io/ts-utils/typedoc/functions/arrFlatten.html)(); [arrForEach](https://nevware21.github.io/ts-utils/typedoc/functions/arrForEach.html)(); [arrFrom](https://nevware21.github.io/ts-utils/typedoc/functions/arrFrom.html)(); [arrGroupBy](https://nevware21.github.io/ts-utils/typedoc/functions/arrGroupBy.html)(); [arrIncludes](https://nevware21.github.io/ts-utils/typedoc/functions/arrIncludes.html)(); [arrIndexKeys](https://nevware21.github.io/ts-utils/typedoc/functions/arrIndexKeys.html)(); [arrIndexOf](https://nevware21.github.io/ts-utils/typedoc/functions/arrIndexOf.html)(); [arrIndexOfSubset](https://nevware21.github.io/ts-utils/typedoc/functions/arrIndexOfSubset.html)(); [arrIntersection](https://nevware21.github.io/ts-utils/typedoc/functions/arrIntersection.html)(); [arrKeys](https://nevware21.github.io/ts-utils/typedoc/functions/arrKeys.html)(); [arrLastIndexOf](https://nevware21.github.io/ts-utils/typedoc/functions/arrLastIndexOf.html)(); [arrLastIndexOfSubset](https://nevware21.github.io/ts-utils/typedoc/functions/arrLastIndexOfSubset.html)(); [arrMap](https://nevware21.github.io/ts-utils/typedoc/functions/arrMap.html)(); [arrPartition](https://nevware21.github.io/ts-utils/typedoc/functions/arrPartition.html)(); [arrReduce](https://nevware21.github.io/ts-utils/typedoc/functions/arrReduce.html)(); [arrReverse](https://nevware21.github.io/ts-utils/typedoc/functions/arrReverse.html)(); [arrRotate](https://nevware21.github.io/ts-utils/typedoc/functions/arrRotate.html)(); [arrSample](https://nevware21.github.io/ts-utils/typedoc/functions/arrSample.html)(); [arrShuffle](https://nevware21.github.io/ts-utils/typedoc/functions/arrShuffle.html)(); [arrSlice](https://nevware21.github.io/ts-utils/typedoc/functions/arrSlice.html)(); [arrSome](https://nevware21.github.io/ts-utils/typedoc/functions/arrSome.html)(); [arrTake](https://nevware21.github.io/ts-utils/typedoc/functions/arrTake.html)(); [arrTakeWhile](https://nevware21.github.io/ts-utils/typedoc/functions/arrTakeWhile.html)(); [arrUnion](https://nevware21.github.io/ts-utils/typedoc/functions/arrUnion.html)(); [arrUnique](https://nevware21.github.io/ts-utils/typedoc/functions/arrUnique.html)(); [arrUnzip](https://nevware21.github.io/ts-utils/typedoc/functions/arrUnzip.html)(); [arrWith](https://nevware21.github.io/ts-utils/typedoc/functions/arrWith.html)(); [arrZip](https://nevware21.github.io/ts-utils/typedoc/functions/arrZip.html)(); [getLength](https://nevware21.github.io/ts-utils/typedoc/functions/getLength.html)(); [isArray](https://nevware21.github.io/ts-utils/typedoc/functions/isArray.html)();
[polyIsArray](https://nevware21.github.io/ts-utils/typedoc/functions/polyIsArray.html)(); [polyArrIncludes](https://nevware21.github.io/ts-utils/typedoc/functions/polyArrIncludes.html)(); [polyArrFind](https://nevware21.github.io/ts-utils/typedoc/functions/polyArrFind.html)(); [polyArrFindIndex](https://nevware21.github.io/ts-utils/typedoc/functions/polyArrFindIndex.html)(); [polyArrFindLast](https://nevware21.github.io/ts-utils/typedoc/functions/polyArrFindLast.html)(); [polyArrFindLastIndex](https://nevware21.github.io/ts-utils/typedoc/functions/polyArrFindLastIndex.html)(); [polyArrFrom](https://nevware21.github.io/ts-utils/typedoc/functions/polyArrFrom.html)();
+| ArrayLike | [arrAt](https://nevware21.github.io/ts-utils/typedoc/functions/arrAt.html)(); [arrChunk](https://nevware21.github.io/ts-utils/typedoc/functions/arrChunk.html)(); [arrCompact](https://nevware21.github.io/ts-utils/typedoc/functions/arrCompact.html)(); [arrContains](https://nevware21.github.io/ts-utils/typedoc/functions/arrContains.html)(); [arrDifference](https://nevware21.github.io/ts-utils/typedoc/functions/arrDifference.html)(); [arrDrop](https://nevware21.github.io/ts-utils/typedoc/functions/arrDrop.html)(); [arrDropWhile](https://nevware21.github.io/ts-utils/typedoc/functions/arrDropWhile.html)(); [arrEvery](https://nevware21.github.io/ts-utils/typedoc/functions/arrEvery.html)(); [arrFilter](https://nevware21.github.io/ts-utils/typedoc/functions/arrFilter.html)(); [arrFill](https://nevware21.github.io/ts-utils/typedoc/functions/arrFill.html)(); [arrFind](https://nevware21.github.io/ts-utils/typedoc/functions/arrFind.html)(); [arrFindIndex](https://nevware21.github.io/ts-utils/typedoc/functions/arrFindIndex.html)(); [arrFindLast](https://nevware21.github.io/ts-utils/typedoc/functions/arrFindLast.html)(); [arrFindLastIndex](https://nevware21.github.io/ts-utils/typedoc/functions/arrFindLastIndex.html)(); [arrFlatten](https://nevware21.github.io/ts-utils/typedoc/functions/arrFlatten.html)(); [arrForEach](https://nevware21.github.io/ts-utils/typedoc/functions/arrForEach.html)(); [arrFrom](https://nevware21.github.io/ts-utils/typedoc/functions/arrFrom.html)(); [arrGroupBy](https://nevware21.github.io/ts-utils/typedoc/functions/arrGroupBy.html)(); [arrIncludes](https://nevware21.github.io/ts-utils/typedoc/functions/arrIncludes.html)(); [arrIndexKeys](https://nevware21.github.io/ts-utils/typedoc/functions/arrIndexKeys.html)(); [arrIndexOf](https://nevware21.github.io/ts-utils/typedoc/functions/arrIndexOf.html)(); [arrIndexOfSubset](https://nevware21.github.io/ts-utils/typedoc/functions/arrIndexOfSubset.html)(); [arrIntersection](https://nevware21.github.io/ts-utils/typedoc/functions/arrIntersection.html)(); [arrKeys](https://nevware21.github.io/ts-utils/typedoc/functions/arrKeys.html)(); [arrLastIndexOf](https://nevware21.github.io/ts-utils/typedoc/functions/arrLastIndexOf.html)(); [arrLastIndexOfSubset](https://nevware21.github.io/ts-utils/typedoc/functions/arrLastIndexOfSubset.html)(); [arrMap](https://nevware21.github.io/ts-utils/typedoc/functions/arrMap.html)(); [arrPartition](https://nevware21.github.io/ts-utils/typedoc/functions/arrPartition.html)(); [arrReduce](https://nevware21.github.io/ts-utils/typedoc/functions/arrReduce.html)(); [arrReverse](https://nevware21.github.io/ts-utils/typedoc/functions/arrReverse.html)(); [arrRotate](https://nevware21.github.io/ts-utils/typedoc/functions/arrRotate.html)(); [arrSample](https://nevware21.github.io/ts-utils/typedoc/functions/arrSample.html)(); [arrShuffle](https://nevware21.github.io/ts-utils/typedoc/functions/arrShuffle.html)(); [arrSlice](https://nevware21.github.io/ts-utils/typedoc/functions/arrSlice.html)(); [arrSome](https://nevware21.github.io/ts-utils/typedoc/functions/arrSome.html)(); [arrTake](https://nevware21.github.io/ts-utils/typedoc/functions/arrTake.html)(); [arrTakeWhile](https://nevware21.github.io/ts-utils/typedoc/functions/arrTakeWhile.html)(); [arrUnion](https://nevware21.github.io/ts-utils/typedoc/functions/arrUnion.html)(); [arrUnique](https://nevware21.github.io/ts-utils/typedoc/functions/arrUnique.html)(); [arrUnzip](https://nevware21.github.io/ts-utils/typedoc/functions/arrUnzip.html)(); [arrWith](https://nevware21.github.io/ts-utils/typedoc/functions/arrWith.html)(); [arrZip](https://nevware21.github.io/ts-utils/typedoc/functions/arrZip.html)(); [getLength](https://nevware21.github.io/ts-utils/typedoc/functions/getLength.html)(); [objEntries](https://nevware21.github.io/ts-utils/typedoc/functions/objEntries.html)(); [objValues](https://nevware21.github.io/ts-utils/typedoc/functions/objValues.html)(); | DOM | [isElement](https://nevware21.github.io/ts-utils/typedoc/functions/isElement.html)(); [isElementLike](https://nevware21.github.io/ts-utils/typedoc/functions/isElementLike.html)(); | Enum | [createEnum](https://nevware21.github.io/ts-utils/typedoc/functions/createEnum.html)(); [createEnumKeyMap](https://nevware21.github.io/ts-utils/typedoc/functions/createEnumKeyMap.html)(); [createEnumValueMap](https://nevware21.github.io/ts-utils/typedoc/functions/createEnumValueMap.html)(); [createSimpleMap](https://nevware21.github.io/ts-utils/typedoc/functions/createSimpleMap.html)(); [createTypeMap](https://nevware21.github.io/ts-utils/typedoc/functions/createTypeMap.html)(); | Error | [createCustomError](https://nevware21.github.io/ts-utils/typedoc/functions/createCustomError.html)(); [isError](https://nevware21.github.io/ts-utils/typedoc/functions/isError.html)(); [throwError](https://nevware21.github.io/ts-utils/typedoc/functions/throwError.html)(); [throwRangeError](https://nevware21.github.io/ts-utils/typedoc/functions/throwRangeError.html)(); [throwTypeError](https://nevware21.github.io/ts-utils/typedoc/functions/throwTypeError.html)(); [throwUnsupported](https://nevware21.github.io/ts-utils/typedoc/functions/throwUnsupported.html)(); diff --git a/lib/src/array/arrIndexOfSubset.ts b/lib/src/array/arrIndexOfSubset.ts new file mode 100644 index 00000000..0bf74bab --- /dev/null +++ b/lib/src/array/arrIndexOfSubset.ts @@ -0,0 +1,197 @@ +/* + * @nevware21/ts-utils + * https://github.com/nevware21/ts-utils + * + * Copyright (c) 2026 NevWare21 Solutions LLC + * Licensed under the MIT license. + */ + +import { isArrayLike } from "../helpers/base"; +import { mathTrunc } from "../math/trunc"; +import { arrIndexOf, arrLastIndexOf } from "./indexOf"; + +/** + * Normalizes a fromIndex argument the same way the native `indexOf()` / `lastIndexOf()` do -- an + * omitted (`undefined`) value falls back to defValue, while `null` and `NaN` both coerce to `0` + * (matching `ToInteger(null) === 0`), and any other finite value is truncated towards zero. + */ +function _normalizeFromIndex(value: number | null | undefined, defValue: number): number { + if (value === undefined) { + return defValue; + } + + if (value === null || isNaN(value)) { + return 0; + } + + return mathTrunc(value); +} + +/** + * The arrIndexOfSubset() method returns the index within {@link theArray} at which {@link subset} + * first matches contiguously, comparing elements using strict equality (the same method used by the + * === or triple-equals operator). + * + * Unlike a simple `arrIndexOf()` of the first element of {@link subset}, this continues searching + * later occurrences of that first element until it finds one where the whole sequence lines up (or + * exhausts {@link theArray}), which avoids landing on the wrong occurrence when values repeat -- for + * example matching one stack trace against another when recursive calls repeat identical lines. + * + * If {@link theArray} has fewer remaining elements than {@link subset} from a candidate position, the + * comparison is limited to just those available elements -- so a {@link subset} that "runs off the end" + * of {@link theArray} is still considered a match against its available prefix. This makes it possible + * to match a shorter (eg. truncated) array against a longer one, or vice-versa. + * @function + * @since 0.17.0 + * @group Array + * @group ArrayLike + * @typeParam T - Identifies the type of array elements + * @param theArray - The array or array-like object of elements to be searched. + * @param subset - The (ordered) array or array-like object of elements to locate within theArray. + * @param fromIndex - The index to start the search at. If the provided index value is a negative + * number, it is taken as the offset from the end of theArray. Default: 0 (search from the start). + * @returns The index within theArray at which subset fully matches (or matches theArray's available + * elements from that position), or -1 if no such position exists. Returns fromIndex (normalized to a + * valid index, or the length of theArray) if subset is empty. + * @example + * ```ts + * arrIndexOfSubset([1, 2, 3, 4, 5], [3, 4]); // 2 + * arrIndexOfSubset([1, 2, 3, 4, 5], [4, 3]); // -1 + * arrIndexOfSubset(["a", "b", "a", "b", "c"], ["b", "c"]); // 3 + * arrIndexOfSubset([1, 2, 3], [3, 4, 5]); // 2 (matches the available "3") + * arrIndexOfSubset([1, 2, 3], []); // 0 + * + * // Repeated values -- a plain indexOf() of the first element would land on index 0 + * let haystack = ["at foo", "at bar", "at foo", "at bar", "at baz"]; + * arrIndexOfSubset(haystack, ["at foo", "at bar", "at baz"]); // 2 + * ``` + */ +/*#__NO_SIDE_EFFECTS__*/ +export function arrIndexOfSubset(theArray: ArrayLike, subset: ArrayLike, fromIndex?: number): number { + if (!isArrayLike(theArray) || !isArrayLike(subset)) { + return -1; + } + + let haystackLen = theArray.length; + let subsetLen = subset.length; + + let searchFrom = _normalizeFromIndex(fromIndex, 0); + if (searchFrom < 0) { + searchFrom = haystackLen + searchFrom; + } + + if (searchFrom < 0) { + searchFrom = 0; + } + + if (subsetLen === 0) { + return searchFrom < haystackLen ? searchFrom : haystackLen; + } + + while (searchFrom < haystackLen) { + let pos = arrIndexOf(theArray, subset[0], searchFrom); + if (pos === -1) { + return -1; + } + + // Only compare as many elements as are actually available in theArray from pos + let matchLen = subsetLen < (haystackLen - pos) ? subsetLen : (haystackLen - pos); + let isMatch = true; + for (let lp = 1; isMatch && lp < matchLen; lp++) { + isMatch = theArray[pos + lp] === subset[lp]; + } + + if (isMatch) { + return pos; + } + + searchFrom = pos + 1; + } + + return -1; +} + +/** + * The arrLastIndexOfSubset() method returns the last index within {@link theArray} at which + * {@link subset} matches contiguously, comparing elements using strict equality (the same method + * used by the === or triple-equals operator). It is the mirror of {@link arrIndexOfSubset}, searching + * backwards from {@link fromIndex} (or the end of theArray) towards the start. + * + * As with {@link arrIndexOfSubset}, if {@link theArray} has fewer remaining elements than + * {@link subset} from a candidate position, the comparison is limited to just those available + * elements -- so a {@link subset} that "runs off the end" of {@link theArray} is still considered a + * match against its available prefix. + * @function + * @since 0.17.0 + * @group Array + * @group ArrayLike + * @typeParam T - Identifies the type of array elements + * @param theArray - The array or array-like object of elements to be searched. + * @param subset - The (ordered) array or array-like object of elements to locate within theArray. + * @param fromIndex - The index to start searching backwards from (the position at which subset's + * first element may occur). If the provided index value is a negative number, it is taken as the + * offset from the end of theArray. Default: theArray's length (search from the end). + * @returns The last index within theArray at which subset fully matches (or matches theArray's + * available elements from that position), or -1 if no such position exists. Returns fromIndex + * (normalized to a valid index, or theArray's length) if subset is empty. + * @example + * ```ts + * arrLastIndexOfSubset([1, 2, 3, 4, 5], [3, 4]); // 2 + * arrLastIndexOfSubset([1, 2, 3, 4, 5], [4, 3]); // -1 + * arrLastIndexOfSubset(["a", "b", "a", "b", "c"], ["a", "b"]); // 2 (the later occurrence) + * arrLastIndexOfSubset([1, 2, 3], [3, 4, 5]); // 2 (matches the available "3") + * arrLastIndexOfSubset([1, 2, 3], []); // 3 + * + * // Repeated values -- returns the later occurrence, unlike arrIndexOfSubset() + * let haystack = ["at foo", "at bar", "at foo", "at bar", "at baz"]; + * arrLastIndexOfSubset(haystack, ["at foo", "at bar"]); // 2 + * ``` + */ +/*#__NO_SIDE_EFFECTS__*/ +export function arrLastIndexOfSubset(theArray: ArrayLike, subset: ArrayLike, fromIndex?: number): number { + if (!isArrayLike(theArray) || !isArrayLike(subset)) { + return -1; + } + + let haystackLen = theArray.length; + let subsetLen = subset.length; + + let searchFrom = _normalizeFromIndex(fromIndex, haystackLen); + if (searchFrom < 0) { + searchFrom = haystackLen + searchFrom; + } + + if (searchFrom > haystackLen) { + searchFrom = haystackLen; + } + + if (subsetLen === 0) { + return searchFrom < 0 ? 0 : searchFrom; + } + + if (searchFrom > haystackLen - 1) { + searchFrom = haystackLen - 1; + } + + while (searchFrom >= 0) { + let pos = arrLastIndexOf(theArray, subset[0], searchFrom); + if (pos === -1) { + return -1; + } + + // Only compare as many elements as are actually available in theArray from pos + let matchLen = subsetLen < (haystackLen - pos) ? subsetLen : (haystackLen - pos); + let isMatch = true; + for (let lp = 1; isMatch && lp < matchLen; lp++) { + isMatch = theArray[pos + lp] === subset[lp]; + } + + if (isMatch) { + return pos; + } + + searchFrom = pos - 1; + } + + return -1; +} diff --git a/lib/src/index.ts b/lib/src/index.ts index d7d3a00d..f9917d0a 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -28,6 +28,7 @@ export { arrFrom } from "./array/from"; export { ArrGroupByCallbackFn, arrGroupBy } from "./array/groupBy"; export { arrContains, arrIncludes } from "./array/includes"; export { arrIndexOf, arrLastIndexOf } from "./array/indexOf"; +export { arrIndexOfSubset, arrLastIndexOfSubset } from "./array/arrIndexOfSubset"; export { arrIntersection } from "./array/intersection"; export { arrIndexKeys } from "./array/arrIndexKeys"; export { arrMap } from "./array/map"; diff --git a/lib/test/bundle-size-check.js b/lib/test/bundle-size-check.js index 9289185a..f479393e 100644 --- a/lib/test/bundle-size-check.js +++ b/lib/test/bundle-size-check.js @@ -7,13 +7,13 @@ const configs = [ { name: "es5-min-full", path: "../bundle/es5/umd/ts-utils.min.js", - limit: 40.5 * 1024, // 40.5 kb in bytes + limit: 41.5 * 1024, // 41.5 kb in bytes compress: false }, { name: "es6-min-full", path: "../bundle/es6/umd/ts-utils.min.js", - limit: 39.5 * 1024, // 39.5 kb in bytes + limit: 40.5 * 1024, // 40.5 kb in bytes compress: false }, { diff --git a/lib/test/src/common/array/arrIndexOfSubset.test.ts b/lib/test/src/common/array/arrIndexOfSubset.test.ts new file mode 100644 index 00000000..ba46ee72 --- /dev/null +++ b/lib/test/src/common/array/arrIndexOfSubset.test.ts @@ -0,0 +1,174 @@ +/* + * @nevware21/ts-utils + * https://github.com/nevware21/ts-utils + * + * Copyright (c) 2026 NevWare21 Solutions LLC + * Licensed under the MIT license. + */ + +import { assert } from "@nevware21/tripwire-chai"; +import { arrIndexOfSubset, arrLastIndexOfSubset } from "../../../../src/array/arrIndexOfSubset"; + +describe("arrIndexOfSubset", () => { + + it("null / undefined", () => { + assert.equal(arrIndexOfSubset(null as any, [1]), -1); + assert.equal(arrIndexOfSubset(undefined as any, [1]), -1); + assert.equal(arrIndexOfSubset([1, 2], null as any), -1); + assert.equal(arrIndexOfSubset([1, 2], undefined as any), -1); + }); + + it("empty subset", () => { + assert.equal(arrIndexOfSubset([1, 2, 3], []), 0); + assert.equal(arrIndexOfSubset([1, 2, 3], [], 2), 2); + assert.equal(arrIndexOfSubset([1, 2, 3], [], 10), 3); + assert.equal(arrIndexOfSubset([], []), 0); + }); + + it("simple example", () => { + assert.equal(arrIndexOfSubset([1, 2, 3, 4, 5], [3, 4]), 2); + assert.equal(arrIndexOfSubset([1, 2, 3, 4, 5], [4, 3]), -1); + assert.equal(arrIndexOfSubset([1, 2, 3, 4, 5], [1, 2, 3, 4, 5]), 0); + assert.equal(arrIndexOfSubset([1, 2, 3, 4, 5], [5]), 4); + assert.equal(arrIndexOfSubset([1, 2, 3, 4, 5], [6]), -1); + // subset overruns theArray's end, so it matches against the available prefix + assert.equal(arrIndexOfSubset([1, 2, 3, 4, 5], [1, 2, 3, 4, 5, 6]), 0); + }); + + it("truncated subset matches available prefix", () => { + assert.equal(arrIndexOfSubset([1, 2, 3], [3, 4, 5]), 2); + assert.equal(arrIndexOfSubset([1, 2, 3], [2, 3, 99, 100]), 1); + }); + + it("fromIndex", () => { + const array = ["a", "b", "a", "b", "c"]; + assert.equal(arrIndexOfSubset(array, ["a", "b"]), 0); + assert.equal(arrIndexOfSubset(array, ["a", "b"], 1), 2); + assert.equal(arrIndexOfSubset(array, ["a", "b"], -2), -1); + assert.equal(arrIndexOfSubset(array, ["b", "c"], -2), 3); + }); + + it("repeated leading value requires re-searching later occurrences", () => { + const haystack = ["at foo", "at bar", "at foo", "at bar", "at baz"]; + assert.equal(arrIndexOfSubset(haystack, ["at foo", "at bar", "at baz"]), 2); + assert.equal(arrIndexOfSubset(haystack, ["at foo", "at qux"]), -1); + }); + + it("NaN / non-integer fromIndex", () => { + const array = [1, 2, 3, 4, 5]; + assert.equal(arrIndexOfSubset(array, [3, 4], NaN), 2); + assert.equal(arrIndexOfSubset(array, [1, 2], NaN), 0); + assert.equal(arrIndexOfSubset(array, [3, 4], 1.9), 2); + assert.equal(arrIndexOfSubset(array, [3, 4], -1.9), -1); + assert.equal(arrIndexOfSubset(array, [], NaN), 0); + }); + + it("null fromIndex coerces to 0, unlike omitted (undefined) fromIndex", () => { + const array = [1, 2, 3, 4, 5]; + assert.equal(arrIndexOfSubset(array, [1, 2], null as any), 0); + assert.equal(arrIndexOfSubset(array, [3, 4], null as any), 2); + assert.equal(arrIndexOfSubset(array, [], null as any), 0); + }); + + it("array like", () => { + let arrayLike = { + length: 4, + 0: "potato", + 1: "tomato", + 2: "chillies", + 3: "green-pepper" + }; + + assert.equal(arrIndexOfSubset(arrayLike, ["tomato", "chillies"]), 1); + assert.equal(arrIndexOfSubset(arrayLike, ["chillies", "tomato"]), -1); + }); +}); + +describe("arrLastIndexOfSubset", () => { + + it("null / undefined", () => { + assert.equal(arrLastIndexOfSubset(null as any, [1]), -1); + assert.equal(arrLastIndexOfSubset(undefined as any, [1]), -1); + assert.equal(arrLastIndexOfSubset([1, 2], null as any), -1); + assert.equal(arrLastIndexOfSubset([1, 2], undefined as any), -1); + }); + + it("empty subset", () => { + assert.equal(arrLastIndexOfSubset([1, 2, 3], []), 3); + assert.equal(arrLastIndexOfSubset([1, 2, 3], [], 2), 2); + assert.equal(arrLastIndexOfSubset([1, 2, 3], [], 10), 3); + assert.equal(arrLastIndexOfSubset([], []), 0); + }); + + it("simple example", () => { + assert.equal(arrLastIndexOfSubset([1, 2, 3, 4, 5], [3, 4]), 2); + assert.equal(arrLastIndexOfSubset([1, 2, 3, 4, 5], [4, 3]), -1); + assert.equal(arrLastIndexOfSubset([1, 2, 3, 4, 5], [1, 2, 3, 4, 5]), 0); + assert.equal(arrLastIndexOfSubset([1, 2, 3, 4, 5], [5]), 4); + assert.equal(arrLastIndexOfSubset([1, 2, 3, 4, 5], [6]), -1); + // subset overruns theArray's end, so it matches against the available prefix + assert.equal(arrLastIndexOfSubset([1, 2, 3, 4, 5], [1, 2, 3, 4, 5, 6]), 0); + }); + + it("truncated subset matches available prefix", () => { + assert.equal(arrLastIndexOfSubset([1, 2, 3], [3, 4, 5]), 2); + assert.equal(arrLastIndexOfSubset([1, 2, 3], [2, 3, 99, 100]), 1); + }); + + it("returns the later occurrence, unlike arrIndexOfSubset", () => { + const array = ["a", "b", "a", "b", "c"]; + assert.equal(arrLastIndexOfSubset(array, ["a", "b"]), 2); + assert.equal(arrLastIndexOfSubset(array, ["b", "c"]), 3); + + const haystack = ["at foo", "at bar", "at foo", "at bar", "at baz"]; + assert.equal(arrLastIndexOfSubset(haystack, ["at foo", "at bar"]), 2); + assert.equal(arrLastIndexOfSubset(haystack, ["at foo", "at qux"]), -1); + }); + + it("fromIndex", () => { + const array = ["a", "b", "a", "b", "c"]; + assert.equal(arrLastIndexOfSubset(array, ["a", "b"], 1), 0); + assert.equal(arrLastIndexOfSubset(array, ["a", "b"], 4), 2); + assert.equal(arrLastIndexOfSubset(array, ["a", "b"], -2), 2); + assert.equal(arrLastIndexOfSubset(array, ["a", "b"], -4), 0); + }); + + it("array like", () => { + let arrayLike = { + length: 4, + 0: "potato", + 1: "tomato", + 2: "chillies", + 3: "green-pepper" + }; + + assert.equal(arrLastIndexOfSubset(arrayLike, ["tomato", "chillies"]), 1); + assert.equal(arrLastIndexOfSubset(arrayLike, ["chillies", "tomato"]), -1); + }); + + it("NaN / non-integer fromIndex", () => { + const array = [1, 2, 3, 4, 5]; + // NaN fromIndex normalizes to 0 (matching native lastIndexOf), not the default (array length) + assert.equal(arrLastIndexOfSubset(array, [1, 2], NaN), 0); + assert.equal(arrLastIndexOfSubset(array, [3, 4], NaN), -1); + assert.equal(arrLastIndexOfSubset(array, [3, 4], 2.9), 2); + assert.equal(arrLastIndexOfSubset(array, [3, 4], -1.9), 2); + assert.equal(arrLastIndexOfSubset(array, [], NaN), 0); + }); + + it("null fromIndex coerces to 0, unlike omitted (undefined) fromIndex", () => { + const array = [1, 2, 3, 4, 5]; + assert.equal(arrLastIndexOfSubset(array, [1, 2], null as any), 0); + assert.equal(arrLastIndexOfSubset(array, [3, 4], null as any), -1); + assert.equal(arrLastIndexOfSubset(array, [], null as any), 0); + }); + + it("null fromIndex searches from index 0, not from the end (regression)", () => { + // [3, 4] genuinely exists (at index 2), so this only returns -1 if null caused the + // search to start from index 0 rather than defaulting to theArray's length like an + // omitted fromIndex would. + const array = [1, 2, 3, 4, 5]; + assert.equal(arrLastIndexOfSubset(array, [3, 4]), 2, "omitted fromIndex finds it from the end"); + assert.equal(arrLastIndexOfSubset(array, [3, 4], null as any), -1, "null fromIndex only searches index 0"); + }); +}); From f992d577eedf1f69e80ba66ceb7534e66904a86e Mon Sep 17 00:00:00 2001 From: nev21 <82737406+nev21@users.noreply.github.com> Date: Sun, 19 Jul 2026 15:48:05 -0700 Subject: [PATCH 2/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/test/src/common/array/arrIndexOfSubset.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/test/src/common/array/arrIndexOfSubset.test.ts b/lib/test/src/common/array/arrIndexOfSubset.test.ts index ba46ee72..3b9ea7ac 100644 --- a/lib/test/src/common/array/arrIndexOfSubset.test.ts +++ b/lib/test/src/common/array/arrIndexOfSubset.test.ts @@ -127,6 +127,7 @@ describe("arrLastIndexOfSubset", () => { it("fromIndex", () => { const array = ["a", "b", "a", "b", "c"]; + assert.equal(arrLastIndexOfSubset(array, ["a", "b"], null as any), 0); assert.equal(arrLastIndexOfSubset(array, ["a", "b"], 1), 0); assert.equal(arrLastIndexOfSubset(array, ["a", "b"], 4), 2); assert.equal(arrLastIndexOfSubset(array, ["a", "b"], -2), 2); From 2e30ee5454961cc756aee0b442dc0b96a93072b8 Mon Sep 17 00:00:00 2001 From: nev21 <82737406+nev21@users.noreply.github.com> Date: Sun, 19 Jul 2026 15:55:44 -0700 Subject: [PATCH 3/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/src/array/arrIndexOfSubset.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/array/arrIndexOfSubset.ts b/lib/src/array/arrIndexOfSubset.ts index 0bf74bab..c0982c23 100644 --- a/lib/src/array/arrIndexOfSubset.ts +++ b/lib/src/array/arrIndexOfSubset.ts @@ -130,7 +130,7 @@ export function arrIndexOfSubset(theArray: ArrayLike, subset: ArrayLike * @param subset - The (ordered) array or array-like object of elements to locate within theArray. * @param fromIndex - The index to start searching backwards from (the position at which subset's * first element may occur). If the provided index value is a negative number, it is taken as the - * offset from the end of theArray. Default: theArray's length (search from the end). + * offset from the end of theArray. Default: theArray's length - 1 (search from the end). * @returns The last index within theArray at which subset fully matches (or matches theArray's * available elements from that position), or -1 if no such position exists. Returns fromIndex * (normalized to a valid index, or theArray's length) if subset is empty.