diff --git a/.size-limit.json b/.size-limit.json index c3acaeaa..693b8897 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -37,7 +37,7 @@ { "name": "es6-zip", "path": "lib/dist/es6/mod/ts-utils.js", - "limit": "10.5 Kb", + "limit": "10.75 Kb", "gzip": true, "running": false }, diff --git a/README.md b/README.md index c6953ca8..6226e4ed 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Below is a categorized list of all available utilities with direct links to thei | Number | [getIntValue](https://nevware21.github.io/ts-utils/typedoc/functions/getIntValue.html)(); [isInteger](https://nevware21.github.io/ts-utils/typedoc/functions/isInteger.html)(); [isFiniteNumber](https://nevware21.github.io/ts-utils/typedoc/functions/isFiniteNumber.html)(); [isNumber](https://nevware21.github.io/ts-utils/typedoc/functions/isNumber.html)(); | Math | [mathAbs](https://nevware21.github.io/ts-utils/typedoc/functions/mathAbs.html)(); [mathAcos](https://nevware21.github.io/ts-utils/typedoc/functions/mathAcos.html)(); [mathAsin](https://nevware21.github.io/ts-utils/typedoc/functions/mathAsin.html)(); [mathAtan](https://nevware21.github.io/ts-utils/typedoc/functions/mathAtan.html)(); [mathAtan2](https://nevware21.github.io/ts-utils/typedoc/functions/mathAtan2.html)(); [mathCeil](https://nevware21.github.io/ts-utils/typedoc/functions/mathCeil.html)(); [mathCos](https://nevware21.github.io/ts-utils/typedoc/functions/mathCos.html)(); [mathExp](https://nevware21.github.io/ts-utils/typedoc/functions/mathExp.html)(); [mathFloor](https://nevware21.github.io/ts-utils/typedoc/functions/mathFloor.html)(); [mathLog](https://nevware21.github.io/ts-utils/typedoc/functions/mathLog.html)(); [mathMax](https://nevware21.github.io/ts-utils/typedoc/functions/mathMax.html)(); [mathMin](https://nevware21.github.io/ts-utils/typedoc/functions/mathMin.html)(); [mathPow](https://nevware21.github.io/ts-utils/typedoc/functions/mathPow.html)(); [mathRandom](https://nevware21.github.io/ts-utils/typedoc/functions/mathRandom.html)(); [mathRound](https://nevware21.github.io/ts-utils/typedoc/functions/mathRound.html)(); [mathSin](https://nevware21.github.io/ts-utils/typedoc/functions/mathSin.html)(); [mathSqrt](https://nevware21.github.io/ts-utils/typedoc/functions/mathSqrt.html)(); [mathTan](https://nevware21.github.io/ts-utils/typedoc/functions/mathTan.html)(); [mathToInt](https://nevware21.github.io/ts-utils/typedoc/functions/mathToInt.html)(); [mathTrunc](https://nevware21.github.io/ts-utils/typedoc/functions/mathTrunc.html)(); | Object | [deepExtend](https://nevware21.github.io/ts-utils/typedoc/functions/deepExtend.html)(); [isObject](https://nevware21.github.io/ts-utils/typedoc/functions/isObject.html)(); [objAssign](https://nevware21.github.io/ts-utils/typedoc/functions/objAssign.html)(); [objCopyProps](https://nevware21.github.io/ts-utils/typedoc/functions/objCopyProps.html)(); [objCreate](https://nevware21.github.io/ts-utils/typedoc/functions/objCreate.html)(); [objDeepCopy](https://nevware21.github.io/ts-utils/typedoc/functions/objDeepCopy.html)(); [objDeepFreeze](https://nevware21.github.io/ts-utils/typedoc/functions/objDeepFreeze.html)(); [objDefine](https://nevware21.github.io/ts-utils/typedoc/functions/objDefine.html)(); [objDefineAccessors](https://nevware21.github.io/ts-utils/typedoc/functions/objDefineAccessors.html)(); [objDefineGet](https://nevware21.github.io/ts-utils/typedoc/functions/objDefineGet.html)(); [objDefineProp](https://nevware21.github.io/ts-utils/typedoc/functions/objDefineProp.html)(); [objDefineProps](https://nevware21.github.io/ts-utils/typedoc/functions/objDefineProps.html)(); [objDefineProperties](https://nevware21.github.io/ts-utils/typedoc/functions/objDefineProperties.html)(); [objEntries](https://nevware21.github.io/ts-utils/typedoc/functions/objEntries.html)(); [objExtend](https://nevware21.github.io/ts-utils/typedoc/functions/objExtend.html)(); [objForEachKey](https://nevware21.github.io/ts-utils/typedoc/functions/objForEachKey.html)(); [objFreeze](https://nevware21.github.io/ts-utils/typedoc/functions/objFreeze.html)(); [objFromEntries](https://nevware21.github.io/ts-utils/typedoc/functions/objFromEntries.html)(); [objGetOwnPropertyDescriptor](https://nevware21.github.io/ts-utils/typedoc/functions/objGetOwnPropertyDescriptor.html)(); [objGetOwnPropertyDescriptors](https://nevware21.github.io/ts-utils/typedoc/functions/objGetOwnPropertyDescriptors.html)(); [objGetOwnPropertyNames](https://nevware21.github.io/ts-utils/typedoc/functions/objGetOwnPropertyNames.html)(); [objGetOwnPropertySymbols](https://nevware21.github.io/ts-utils/typedoc/functions/objGetOwnPropertySymbols.html)(); [objHasOwn](https://nevware21.github.io/ts-utils/typedoc/functions/objHasOwn.html)(); [objHasOwnProperty](https://nevware21.github.io/ts-utils/typedoc/functions/objHasOwnProperty.html)(); [objIs](https://nevware21.github.io/ts-utils/typedoc/functions/objIs.html)(); [objIsExtensible](https://nevware21.github.io/ts-utils/typedoc/functions/objIsExtensible.html)(); [objIsFrozen](https://nevware21.github.io/ts-utils/typedoc/functions/objIsFrozen.html)(); [objIsSealed](https://nevware21.github.io/ts-utils/typedoc/functions/objIsSealed.html)(); [objKeys](https://nevware21.github.io/ts-utils/typedoc/functions/objKeys.html)(); [objPreventExtensions](https://nevware21.github.io/ts-utils/typedoc/functions/objPreventExtensions.html)(); [objPropertyIsEnumerable](https://nevware21.github.io/ts-utils/typedoc/functions/objPropertyIsEnumerable.html)(); [objSeal](https://nevware21.github.io/ts-utils/typedoc/functions/objSeal.html)(); [objGetPrototypeOf](https://nevware21.github.io/ts-utils/typedoc/functions/objGetPrototypeOf.html)(); [objSetPrototypeOf](https://nevware21.github.io/ts-utils/typedoc/functions/objSetPrototypeOf.html)(); [objToString](https://nevware21.github.io/ts-utils/typedoc/functions/objToString.html)(); [objValues](https://nevware21.github.io/ts-utils/typedoc/functions/objValues.html)();
[polyObjEntries](https://nevware21.github.io/ts-utils/typedoc/functions/polyObjEntries.html)(); [polyObjIs](https://nevware21.github.io/ts-utils/typedoc/functions/polyObjIs.html)(); [polyObjKeys](https://nevware21.github.io/ts-utils/typedoc/functions/polyObjKeys.html)();
-| String | [asString](https://nevware21.github.io/ts-utils/typedoc/functions/asString.html)(); [getLength](https://nevware21.github.io/ts-utils/typedoc/functions/getLength.html)(); [isString](https://nevware21.github.io/ts-utils/typedoc/functions/isString.html)(); [strEndsWith](https://nevware21.github.io/ts-utils/typedoc/functions/strEndsWith.html)(); [strIndexOf](https://nevware21.github.io/ts-utils/typedoc/functions/strIndexOf.html)(); [strIsNullOrEmpty](https://nevware21.github.io/ts-utils/typedoc/functions/strIsNullOrEmpty.html)(); [strIsNullOrWhiteSpace](https://nevware21.github.io/ts-utils/typedoc/functions/strIsNullOrWhiteSpace.html)(); [strLastIndexOf](https://nevware21.github.io/ts-utils/typedoc/functions/strLastIndexOf.html)(); [strLeft](https://nevware21.github.io/ts-utils/typedoc/functions/strLeft.html)(); [strPadEnd](https://nevware21.github.io/ts-utils/typedoc/functions/strPadEnd.html)(); [strPadStart](https://nevware21.github.io/ts-utils/typedoc/functions/strPadStart.html)(); [strRepeat](https://nevware21.github.io/ts-utils/typedoc/functions/strRepeat.html)(); [strRight](https://nevware21.github.io/ts-utils/typedoc/functions/strRight.html)(); [strSlice](https://nevware21.github.io/ts-utils/typedoc/functions/strSlice.html)(); [strSplit](https://nevware21.github.io/ts-utils/typedoc/functions/strSplit.html)(); [strStartsWith](https://nevware21.github.io/ts-utils/typedoc/functions/strStartsWith.html)(); [strSubstr](https://nevware21.github.io/ts-utils/typedoc/functions/strSubstr.html)(); [strSubstring](https://nevware21.github.io/ts-utils/typedoc/functions/strSubstring.html)(); [strSymSplit](https://nevware21.github.io/ts-utils/typedoc/functions/strSymSplit.html)(); [strTrim](https://nevware21.github.io/ts-utils/typedoc/functions/strTrim.html)(); [strTrimEnd](https://nevware21.github.io/ts-utils/typedoc/functions/strTrimEnd.html)(); [strTrimLeft](https://nevware21.github.io/ts-utils/typedoc/functions/strTrimLeft.html)(); [strTrimRight](https://nevware21.github.io/ts-utils/typedoc/functions/strTrimRight.html)(); [strTrimStart](https://nevware21.github.io/ts-utils/typedoc/functions/strTrimStart.html)(); [strLetterCase](https://nevware21.github.io/ts-utils/typedoc/functions/strLetterCase.html)(); [strCamelCase](https://nevware21.github.io/ts-utils/typedoc/functions/strCamelCase.html)(); [strKebabCase](https://nevware21.github.io/ts-utils/typedoc/functions/strKebabCase.html)(); [strSnakeCase](https://nevware21.github.io/ts-utils/typedoc/functions/strSnakeCase.html)(); [strUpper](https://nevware21.github.io/ts-utils/typedoc/functions/strUpper.html)(); [strLower](https://nevware21.github.io/ts-utils/typedoc/functions/strLower.html)(); [strContains](https://nevware21.github.io/ts-utils/typedoc/functions/strContains.html)(); [strIncludes](https://nevware21.github.io/ts-utils/typedoc/functions/strIncludes.html)();
[polyStrSubstr](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrSubstr.html)(); [polyStrTrim](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrTrim.html)(); [polyStrTrimEnd](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrTrimEnd.html)(); [polyStrTrimStart](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrTrimStart.html)(); [polyStrIncludes](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrIncludes.html)();
+| String | [asString](https://nevware21.github.io/ts-utils/typedoc/functions/asString.html)(); [getLength](https://nevware21.github.io/ts-utils/typedoc/functions/getLength.html)(); [isString](https://nevware21.github.io/ts-utils/typedoc/functions/isString.html)(); [strEndsWith](https://nevware21.github.io/ts-utils/typedoc/functions/strEndsWith.html)(); [strIndexOf](https://nevware21.github.io/ts-utils/typedoc/functions/strIndexOf.html)(); [strIsNullOrEmpty](https://nevware21.github.io/ts-utils/typedoc/functions/strIsNullOrEmpty.html)(); [strIsNullOrWhiteSpace](https://nevware21.github.io/ts-utils/typedoc/functions/strIsNullOrWhiteSpace.html)(); [strLastIndexOf](https://nevware21.github.io/ts-utils/typedoc/functions/strLastIndexOf.html)(); [strLeft](https://nevware21.github.io/ts-utils/typedoc/functions/strLeft.html)(); [strPadEnd](https://nevware21.github.io/ts-utils/typedoc/functions/strPadEnd.html)(); [strPadStart](https://nevware21.github.io/ts-utils/typedoc/functions/strPadStart.html)(); [strRepeat](https://nevware21.github.io/ts-utils/typedoc/functions/strRepeat.html)(); [strReplace](https://nevware21.github.io/ts-utils/typedoc/functions/strReplace.html)(); [strReplaceAll](https://nevware21.github.io/ts-utils/typedoc/functions/strReplaceAll.html)(); [strRight](https://nevware21.github.io/ts-utils/typedoc/functions/strRight.html)(); [strSlice](https://nevware21.github.io/ts-utils/typedoc/functions/strSlice.html)(); [strSplit](https://nevware21.github.io/ts-utils/typedoc/functions/strSplit.html)(); [strStartsWith](https://nevware21.github.io/ts-utils/typedoc/functions/strStartsWith.html)(); [strSubstr](https://nevware21.github.io/ts-utils/typedoc/functions/strSubstr.html)(); [strSubstring](https://nevware21.github.io/ts-utils/typedoc/functions/strSubstring.html)(); [strSymSplit](https://nevware21.github.io/ts-utils/typedoc/functions/strSymSplit.html)(); [strTrim](https://nevware21.github.io/ts-utils/typedoc/functions/strTrim.html)(); [strTrimEnd](https://nevware21.github.io/ts-utils/typedoc/functions/strTrimEnd.html)(); [strTrimLeft](https://nevware21.github.io/ts-utils/typedoc/functions/strTrimLeft.html)(); [strTrimRight](https://nevware21.github.io/ts-utils/typedoc/functions/strTrimRight.html)(); [strTrimStart](https://nevware21.github.io/ts-utils/typedoc/functions/strTrimStart.html)(); [strLetterCase](https://nevware21.github.io/ts-utils/typedoc/functions/strLetterCase.html)(); [strCamelCase](https://nevware21.github.io/ts-utils/typedoc/functions/strCamelCase.html)(); [strKebabCase](https://nevware21.github.io/ts-utils/typedoc/functions/strKebabCase.html)(); [strSnakeCase](https://nevware21.github.io/ts-utils/typedoc/functions/strSnakeCase.html)(); [strUpper](https://nevware21.github.io/ts-utils/typedoc/functions/strUpper.html)(); [strLower](https://nevware21.github.io/ts-utils/typedoc/functions/strLower.html)(); [strContains](https://nevware21.github.io/ts-utils/typedoc/functions/strContains.html)(); [strIncludes](https://nevware21.github.io/ts-utils/typedoc/functions/strIncludes.html)();
[polyStrSubstr](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrSubstr.html)(); [polyStrTrim](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrTrim.html)(); [polyStrTrimEnd](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrTrimEnd.html)(); [polyStrTrimStart](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrTrimStart.html)(); [polyStrIncludes](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrIncludes.html)();
| Symbol | [WellKnownSymbols](https://nevware21.github.io/ts-utils/typedoc/enums/WellKnownSymbols.html) (const enum);
[getKnownSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/getKnownSymbol.html)(); [getSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/getSymbol.html)(); [hasSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/hasSymbol.html)(); [isSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/isSymbol.html)(); [newSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/newSymbol.html)(); [symbolFor](https://nevware21.github.io/ts-utils/typedoc/functions/symbolFor.html)(); [symbolKeyFor](https://nevware21.github.io/ts-utils/typedoc/functions/symbolKeyFor.html)();
[polyGetKnownSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/polyGetKnownSymbol.html)(); [polyNewSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/polyNewSymbol.html)(); [polySymbolFor](https://nevware21.github.io/ts-utils/typedoc/functions/polySymbolFor.html)(); [polySymbolKeyFor](https://nevware21.github.io/ts-utils/typedoc/functions/polySymbolKeyFor.html)();

Polyfills are used to automatically backfill runtimes that do not support `Symbol`, not all of the Symbol functionality is provided. | Timer | [createTimeout](https://nevware21.github.io/ts-utils/typedoc/functions/createTimeout.html)(); [createTimeoutWith](https://nevware21.github.io/ts-utils/typedoc/functions/createTimeoutWith.html)(); [elapsedTime](https://nevware21.github.io/ts-utils/typedoc/functions/elapsedTime.html)(); [perfNow](https://nevware21.github.io/ts-utils/typedoc/functions/perfNow.html)(); [setGlobalTimeoutOverrides](https://nevware21.github.io/ts-utils/typedoc/functions/setGlobalTimeoutOverrides.html)(); [setTimeoutOverrides](https://nevware21.github.io/ts-utils/typedoc/functions/setTimeoutOverrides.html)(); [utcNow](https://nevware21.github.io/ts-utils/typedoc/functions/utcNow.html)(); [scheduleIdleCallback](https://nevware21.github.io/ts-utils/typedoc/functions/scheduleIdleCallback.html)(); [scheduleInterval](https://nevware21.github.io/ts-utils/typedoc/functions/scheduleInterval.html)(); [scheduleTimeout](https://nevware21.github.io/ts-utils/typedoc/functions/scheduleTimeout.html)(); [scheduleTimeoutWith](https://nevware21.github.io/ts-utils/typedoc/functions/scheduleTimeoutWith.html)(); [hasIdleCallback](https://nevware21.github.io/ts-utils/typedoc/functions/hasIdleCallback.html)();
For runtimes that don't support `requestIdleCallback` normal setTimeout() is used with the values from [`setDefaultIdleTimeout`](https://nevware21.github.io/ts-utils/typedoc/functions/setDefaultIdleTimeout.html)() and [`setDefaultMaxExecutionTime`](https://nevware21.github.io/ts-utils/typedoc/functions/setDefaultMaxExecutionTime.html)();
[polyUtcNow](https://nevware21.github.io/ts-utils/typedoc/functions/polyUtcNow.html)(); | Conversion & Encoding | [encodeAsJson](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsJson.html)(); [encodeAsHtml](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsHtml.html)(); [encodeAsBase64](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsBase64.html)(); [decodeBase64](https://nevware21.github.io/ts-utils/typedoc/functions/decodeBase64.html)(); [encodeAsBase64Url](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsBase64Url.html)(); [decodeBase64Url](https://nevware21.github.io/ts-utils/typedoc/functions/decodeBase64Url.html)(); [encodeAsHex](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsHex.html)(); [decodeHex](https://nevware21.github.io/ts-utils/typedoc/functions/decodeHex.html)(); [encodeAsUri](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsUri.html)(); [decodeUri](https://nevware21.github.io/ts-utils/typedoc/functions/decodeUri.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)(); [strLetterCase](https://nevware21.github.io/ts-utils/typedoc/functions/strLetterCase.html)(); [strCamelCase](https://nevware21.github.io/ts-utils/typedoc/functions/strCamelCase.html)(); [strKebabCase](https://nevware21.github.io/ts-utils/typedoc/functions/strKebabCase.html)(); [strSnakeCase](https://nevware21.github.io/ts-utils/typedoc/functions/strSnakeCase.html)(); [strUpper](https://nevware21.github.io/ts-utils/typedoc/functions/strUpper.html)(); [strLower](https://nevware21.github.io/ts-utils/typedoc/functions/strLower.html)(); @@ -225,6 +225,7 @@ import { arrFindLast, arrFindLastIndex, // ES2023 objGetOwnPropertyDescriptors, // ES2017 strPadStart, strPadEnd, // ES2017 + strReplaceAll, // ES2021 isBigInt, // ES2020 isWeakMap, isWeakSet // ES2015+ } from "@nevware21/ts-utils"; @@ -241,6 +242,9 @@ const descriptors = objGetOwnPropertyDescriptors(myObject); const paddedString = strPadStart("123", 5, "0"); // "00123" const paddedEnd = strPadEnd("hello", 10, "."); // "hello....." +// String replaceAll (ES2021) +const replaced = strReplaceAll("a-b-a", "a", "x"); // "x-b-x" + // Safe type checks for modern types if (isBigInt(someValue)) { // Handle BigInt (ES2020) safely diff --git a/docs/feature-backlog.md b/docs/feature-backlog.md new file mode 100644 index 00000000..63671d0b --- /dev/null +++ b/docs/feature-backlog.md @@ -0,0 +1,89 @@ +# Feature Backlog and Request Tracking + +This document tracks suggested additions for `@nevware21/ts-utils`. + +## Request RQ-2026-03-17-ADDITIONS + +- Request: Identify what additions could be added to ts-utils +- Requested by: Maintainer discussion +- Priority: High +- Scope: Array, object, string, iterator, typing, and documentation improvements + +### Objective + +Identify practical, minification-friendly, cross-environment additions that fit the library design principles: + +- zero dependencies +- ES5 compatibility for v0.x / v1.x +- polyfill-backed behavior where needed +- strong tree-shaking and small bundles + +## Suggested Additions (Proposed Only) + +### Language-Native Suggestions (with ECMAScript Version) + +The following suggestions map directly to JavaScript language / standard library features: + +- `strReplaceAll` wrapper/polyfill path + - JavaScript feature: `String.prototype.replaceAll()` + - Added in: **ECMAScript 2021 (ES12)** + +| Suggestion | JavaScript Feature | ECMAScript Version | +| --- | --- | --- | +| `strReplaceAll` wrapper/polyfill path | `String.prototype.replaceAll()` | ECMAScript 2021 (ES12) | + +Notes: + +- Other suggestions below are library-level utilities (not direct language features). +- Iterator helpers are intentionally listed as utility suggestions here rather than standard-language mappings. + +### A. Typing Improvements (High Value) + +- `ReadonlyRecord` helper type alias for API ergonomics +- `DeepPartial` utility type +- `DeepReadonly` utility type +- `Mutable` utility type for controlled writable transformations + +### B. Object Utilities (Medium Value) + +- `objPick` / `objOmit` +- `objMapValues` +- `objMergeIf` +- `objDiff` + +Notes: + +- maintain plain-object safety patterns +- avoid behavior changes to existing deep copy helpers + +### C. String Utilities (Medium Value) + +- `strTruncate` (with optional suffix) +- `strCount` (substring occurrences) +- `strReplaceAll` wrapper/polyfill path **(ECMAScript 2021 / ES12 language feature wrapper)** +- `strCapitalizeWords` + +### D. Iterator and Collection Helpers (Medium Value) + +- `iterMap`, `iterFilter`, `iterTake` +- `arrToMap` helpers with stable key selection +- lightweight set operations for iterables + +### E. Reliability and Tooling (High Value) + +- keep bundle-size thresholds justified with measured report +- require test parity for polyfill vs native behavior +- ensure newly exported functions are reflected in README utility matrix + +## Acceptance Criteria for this Request + +- [x] Identify additions by category +- [x] Prioritize additions by value and fit +- [ ] Create follow-up issue list for proposed items +- [ ] Add ownership and target milestone per item + +## Next Actions + +1. Open GitHub issues for sections A-D candidates. +2. Add milestone tags for upcoming releases. +3. Keep this document focused on proposed additions only. diff --git a/lib/src/array/with.ts b/lib/src/array/with.ts index 7e93a709..2765af64 100644 --- a/lib/src/array/with.ts +++ b/lib/src/array/with.ts @@ -11,6 +11,7 @@ import { _unwrapFunctionWithPoly } from "../internal/unwrapFunction"; import { isArrayLike } from "../helpers/base"; import { getLength } from "../helpers/length"; import { arrSlice } from "./slice"; +import { throwRangeError } from "../helpers/throw"; /** * The arrWith() method is the copying version of using the bracket notation to change the value @@ -60,7 +61,7 @@ export function polyArrWith(theArray: ArrayLike, index: number, value: T): let result: T[]; if (!isArrayLike(theArray)) { - throw new RangeError("Invalid array"); + throwRangeError("Invalid array"); } const len = getLength(theArray); @@ -73,7 +74,7 @@ export function polyArrWith(theArray: ArrayLike, index: number, value: T): // Check bounds if (idx < 0 || idx >= len) { - throw new RangeError("Index out of bounds"); + throwRangeError("Index out of bounds"); } // Create a copy and set value diff --git a/lib/src/helpers/encode.ts b/lib/src/helpers/encode.ts index 5b309336..9173c5f7 100644 --- a/lib/src/helpers/encode.ts +++ b/lib/src/helpers/encode.ts @@ -10,6 +10,7 @@ import { EMPTY, NULL_VALUE, TO_STRING, UNDEF_VALUE } from "../internal/constants import { asString } from "../string/as_string"; import { strCamelCase } from "../string/conversion"; import { strPadStart } from "../string/pad"; +import { strReplace } from "../string/replace"; import { strRepeat } from "../string/repeat"; import { strSubstr } from "../string/substring"; import { strUpper } from "../string/upper_lower"; @@ -79,7 +80,7 @@ let _base64Cache: { [key: string]: number }; */ /*#__NO_SIDE_EFFECTS__*/ export function normalizeJsName(jsName: string, camelCase?: boolean): string { - let result = asString(jsName).replace(INVALID_JS_NAME, "_"); + let result = strReplace(asString(jsName), INVALID_JS_NAME, "_"); return !isUndefined(camelCase) ? strCamelCase(result, !camelCase) : result; } @@ -129,7 +130,7 @@ export function encodeAsJson(value: T, format?: boolean | number): string { if (isString(value)) { // encode if a character is not an alpha, numeric, space or some special characters - result = DBL_QUOTE + value.replace(/[^\w .,\-!@#$%\^&*\(\)_+={}\[\]:;|<>?]/g, (match) => { + result = DBL_QUOTE + strReplace(value, /[^\w .,\-!@#$%\^&*\(\)_+={}\[\]:;|<>?]/g, (match) => { if(match === DBL_QUOTE || match === "\\") { return "\\" + match; } @@ -184,7 +185,7 @@ export function encodeAsHtml(value: string) { "'": "#39" }); - return asString(value).replace(/[&<>"']/g, match => "&" + _htmlEntityCache[match] + ";"); + return strReplace(asString(value), /[&<>"']/g, match => "&" + _htmlEntityCache[match] + ";"); } /** @@ -273,7 +274,7 @@ export function encodeAsBase64Url(value: string): string { let encoded = encodeAsBase64(value); if (encoded) { - encoded = encoded.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); + encoded = strReplace(strReplace(strReplace(encoded, /\+/g, "-"), /\//g, "_"), /=/g, ""); } return encoded || EMPTY; @@ -304,7 +305,7 @@ export function decodeBase64Url(value: string): string { result = result + strRepeat("=", pad); } - result = decodeBase64(result.replace(/-/g, "+").replace(/_/g, "/")) || EMPTY; + result = decodeBase64(strReplace(strReplace(result, /-/g, "+"), /_/g, "/")) || EMPTY; } return result || value || EMPTY; diff --git a/lib/src/helpers/regexp.ts b/lib/src/helpers/regexp.ts index 1db2aca8..e68ba8bc 100644 --- a/lib/src/helpers/regexp.ts +++ b/lib/src/helpers/regexp.ts @@ -8,6 +8,7 @@ import { EMPTY } from "../internal/constants"; import { asString } from "../string/as_string"; +import { strReplace } from "../string/replace"; const MATCH_ANY = "(.*)"; const MATCH_SINGLE = "(.)"; @@ -27,7 +28,7 @@ const MATCH_SINGLE = "(.)"; function _createRegExp(value: string, escapeRgx: RegExp, replaceFn: (value: string) => string, ignoreCase: boolean, fullMatch?: boolean) { // eslint-disable-next-line security/detect-non-literal-regexp return new RegExp( - (fullMatch ? "^" : EMPTY) + replaceFn(value.replace(escapeRgx, "\\$1")) + (fullMatch ? "$" : EMPTY), + (fullMatch ? "^" : EMPTY) + replaceFn(strReplace(value, escapeRgx, "\\$1")) + (fullMatch ? "$" : EMPTY), ignoreCase ? "i" : ""); } @@ -70,7 +71,7 @@ function _createRegExp(value: string, escapeRgx: RegExp, replaceFn: (value: stri /*#__NO_SIDE_EFFECTS__*/ export function createWildcardRegex(value: string, ignoreCase?: boolean, fullMatch?: boolean) { return _createRegExp(asString(value), /([-+|^$#\.\?{}()\[\]\\\/\"\'])/g, (value: string) => { - return value.replace(/\*/g, MATCH_ANY); + return strReplace(value, /\*/g, MATCH_ANY); }, !!ignoreCase, fullMatch); } @@ -120,7 +121,7 @@ export function createWildcardRegex(value: string, ignoreCase?: boolean, fullMat /*#__NO_SIDE_EFFECTS__*/ export function createFilenameRegex(value: string, ignoreCase?: boolean, fullMatch?: boolean) { return _createRegExp(asString(value), /([-+|^$#\.{}()\\\/\[\]\"\'])/g, (value: string) => { - return value.replace(/(\\\\|\\\/|\*|\?)/g, function (_all, g1) { + return strReplace(value, /(\\\\|\\\/|\*|\?)/g, function (_all, g1) { if (g1 == "\\/" || g1 == "\\\\") { return "[\\\\\\/]{1}"; } @@ -195,7 +196,7 @@ export function createFilenameRegex(value: string, ignoreCase?: boolean, fullMat export function makeGlobRegex(value: string, ignoreCase?: boolean, fullMatch?: boolean) { return _createRegExp(asString(value), /([-+|^$#\.{}()\\\/\[\]\"\'])/g, (value: string) => { //"**\/*\.txt" - return value.replace(/(\*\*\\[\\\/]|\\\\|\\\/|\*\*|\*|\?)/g, function (_all, g1) { + return strReplace(value, /(\*\*\\[\\\/]|\\\\|\\\/|\*\*|\*|\?)/g, function (_all, g1) { if (g1 == "**\\/" || g1 == "**\\\\") { return "(.*[\\\\\\/])*"; } diff --git a/lib/src/index.ts b/lib/src/index.ts index 59149f54..98f80c87 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -128,6 +128,8 @@ export { strContains, strIncludes, polyStrIncludes } from "./string/includes"; export { strIndexOf, strLastIndexOf } from "./string/index_of"; export { strIsNullOrWhiteSpace, strIsNullOrEmpty } from "./string/is_null_or"; export { strPadEnd, strPadStart } from "./string/pad"; +export { strReplace } from "./string/replace"; +export { strReplaceAll } from "./string/replace_all"; export { strRepeat } from "./string/repeat"; export { strSlice } from "./string/slice"; export { strStartsWith } from "./string/starts_with"; diff --git a/lib/src/polyfills.ts b/lib/src/polyfills.ts index 512fbeab..a65483a1 100644 --- a/lib/src/polyfills.ts +++ b/lib/src/polyfills.ts @@ -18,6 +18,7 @@ import { polyStrPadEnd, polyStrPadStart } from "./string/pad"; import { makePolyFn } from "./internal/poly_helpers"; import { polyStrSubstr } from "./string/substring"; import { polyStrIncludes } from "./string/includes"; +import { polyStrReplaceAll } from "./string/replace_all"; import { polyObjFromEntries } from "./polyfills/object/objFromEntries"; import { polyObjGetOwnPropertyDescriptors, _polyObjGetOwnPropertySymbols, _polyObjGetOwnPropertyNames } from "./polyfills/object/objGetOwnProperty"; import { polyObjPreventExtensions } from "./polyfills/object/objPreventExtensions"; @@ -58,7 +59,8 @@ import { polyArrWith } from "./array/with"; "trimEnd": polyStrTrimEnd, "trimRight": polyStrTrimEnd, "substr": polyStrSubstr, - "includes": polyStrIncludes + "includes": polyStrIncludes, + "replaceAll": polyStrReplaceAll }; const arrayClsPolyfills = { @@ -104,4 +106,4 @@ import { polyArrWith } from "./array/with"; }); })(); -export { polyArrAt, polyArrFill, polyArrWith }; +export { polyArrAt, polyArrFill, polyArrWith, polyStrReplaceAll }; diff --git a/lib/src/polyfills/trim.ts b/lib/src/polyfills/trim.ts index 8bb9e31e..2420ee7c 100644 --- a/lib/src/polyfills/trim.ts +++ b/lib/src/polyfills/trim.ts @@ -8,14 +8,17 @@ import { EMPTY } from "../internal/constants"; import { _throwIfNullOrUndefined } from "../internal/throwIf"; +import { asString } from "../string/as_string"; +import { strReplace } from "../string/replace"; /*#__NO_SIDE_EFFECTS__*/ function _createTrimFn(exp: RegExp): (value: string) => string { return function _doTrim(value: string): string { _throwIfNullOrUndefined(value); + value = asString(value); - if (value && value.replace) { - value = value.replace(exp, EMPTY); + if (value) { + value = strReplace(value, exp, EMPTY); } return value; diff --git a/lib/src/string/conversion.ts b/lib/src/string/conversion.ts index d9f695e6..3f444332 100644 --- a/lib/src/string/conversion.ts +++ b/lib/src/string/conversion.ts @@ -8,6 +8,7 @@ import { EMPTY } from "../internal/constants"; import { asString } from "./as_string"; +import { strReplace } from "./replace"; import { strTrim } from "./trim"; import { strLower, strUpper } from "./upper_lower"; @@ -22,7 +23,7 @@ import { strLower, strUpper } from "./upper_lower"; */ /*#__NO_SIDE_EFFECTS__*/ function _convertCase(value: T, newPrefix: string, upperWord?: boolean): string { - return strTrim(asString(value)).replace(/((_|\W)+(\w){0,1}|([a-z])([A-Z]))/g, + return strReplace(strTrim(asString(value)), /((_|\W)+(\w){0,1}|([a-z])([A-Z]))/g, (_match, _g1, _g2, wordStart, upperPrefix, upperLetter) => { let convertMatch = wordStart || upperLetter|| EMPTY; if (upperWord) { @@ -53,7 +54,7 @@ function _convertCase(value: T, newPrefix: string, upperWord?: boolean): stri */ /*#__NO_SIDE_EFFECTS__*/ export function strLetterCase(value: T): string { - return asString(value).replace(/(_|\b)\w/g, strUpper); + return strReplace(asString(value), /(_|\b)\w/g, strUpper); } /** @@ -92,7 +93,7 @@ export function strLetterCase(value: T): string { export function strCamelCase(value: T, upperFirst?: boolean): string { let result = _convertCase(value, "", true); - return result.replace(/^\w/, upperFirst ? strUpper : strLower); + return strReplace(result, /^\w/, upperFirst ? strUpper : strLower); } /** diff --git a/lib/src/string/is_null_or.ts b/lib/src/string/is_null_or.ts index 227f3999..6471d52f 100644 --- a/lib/src/string/is_null_or.ts +++ b/lib/src/string/is_null_or.ts @@ -8,6 +8,7 @@ import { isNullOrUndefined, isString } from "../helpers/base"; import { EMPTY } from "../internal/constants"; +import { strReplace } from "./replace"; /** * This method checks if the string `value` is null, undefined, an empty string or only contains @@ -19,7 +20,7 @@ import { EMPTY } from "../internal/constants"; /*#__NO_SIDE_EFFECTS__*/ export function strIsNullOrWhiteSpace(value: string): boolean { if (isString(value)) { - return value.replace(/[\s\t\r\n\f]+/g, EMPTY) === EMPTY; + return strReplace(value, /[\s\t\r\n\f]+/g, EMPTY) === EMPTY; } return isNullOrUndefined(value) diff --git a/lib/src/string/replace.ts b/lib/src/string/replace.ts new file mode 100644 index 00000000..d0f197ec --- /dev/null +++ b/lib/src/string/replace.ts @@ -0,0 +1,29 @@ +/* + * @nevware21/ts-utils + * https://github.com/nevware21/ts-utils + * + * Copyright (c) 2026 NevWare21 Solutions LLC + * Licensed under the MIT license. + */ + +import { StrProto } from "../internal/constants"; +import { _unwrapFunction } from "../internal/unwrapFunction"; + +/** + * The strReplace() method returns a new string with one, some, or all matches of a pattern replaced + * by a replacement. + * @function + * @since 0.14.0 + * @group String + * @param value - The string value to search and replace within. + * @param searchValue - The value to search for. Can be a string or regular expression. + * @param replaceValue - The replacement string or replacer function. + * @returns A new string with one, some, or all matches replaced. + * @example + * ```ts + * strReplace("a-b-a", "a", "x"); // "x-b-a" + * strReplace("a1b2", /\d/g, "#"); // "a#b#" + * strReplace("hello", /[aeiou]/, "*"); // "h*llo" + * ``` + */ +export const strReplace: (value: string, searchValue: string | RegExp, replaceValue: string | ((substring: string, ...args: any[]) => string)) => string = (/*#__PURE__*/_unwrapFunction("replace", StrProto as any)); diff --git a/lib/src/string/replace_all.ts b/lib/src/string/replace_all.ts new file mode 100644 index 00000000..373c31a1 --- /dev/null +++ b/lib/src/string/replace_all.ts @@ -0,0 +1,85 @@ +/* + * @nevware21/ts-utils + * https://github.com/nevware21/ts-utils + * + * Copyright (c) 2026 NevWare21 Solutions LLC + * Licensed under the MIT license. + */ + +import { isFunction, isRegExp, isStrictNullOrUndefined, isString } from "../helpers/base"; +import { throwTypeError } from "../helpers/throw"; +import { StrProto } from "../internal/constants"; +import { _throwIfNullOrUndefined } from "../internal/throwIf"; +import { _unwrapFunctionWithPoly } from "../internal/unwrapFunction"; +import { getKnownSymbol } from "../symbol/symbol"; +import { WellKnownSymbols } from "../symbol/well_known"; +import { asString } from "./as_string"; +import { strReplace } from "./replace"; + +/** + * The strReplaceAll() method returns a new string with all matches of a pattern replaced by a replacement. + * @function + * @since 0.14.0 + * @group String + * @param value - The string value to search and replace within. + * @param searchValue - The value to search for. Can be a string or a global regular expression. + * @param replaceValue - The replacement string or replacer function. + * @returns A new string with every occurrence of searchValue replaced. + * @throws TypeError if searchValue is a regular expression without the global flag. + * @example + * ```ts + * strReplaceAll("a-b-a", "a", "x"); // "x-b-x" + * strReplaceAll("abc123abc", /abc/g, "X"); // "X123X" + * strReplaceAll("abca", "a", (value, index) => value.toUpperCase() + index); + * // "A0bcA3" + * ``` + */ +export const strReplaceAll: (value: string, searchValue: string | RegExp, replaceValue: string | ((substring: string, ...args: any[]) => string)) => string = (/*#__PURE__*/_unwrapFunctionWithPoly("replaceAll", StrProto as any, polyStrReplaceAll)); + +/** + * Polyfill implementation of String.prototype.replaceAll(). + * @since 0.14.0 + * @group String + * @group Polyfill + * @param value - The string value to search and replace within. + * @param searchValue - The value to search for. Can be a string or a global regular expression. + * @param replaceValue - The replacement string or replacer function. + * @returns A new string with every occurrence of searchValue replaced. + * @throws TypeError if searchValue is a regular expression without the global flag. + */ +/*#__NO_SIDE_EFFECTS__*/ +export function polyStrReplaceAll(value: string, searchValue: string | RegExp, replaceValue: string | ((substring: string, ...args: any[]) => string)): string { + _throwIfNullOrUndefined(value); + + let matchSymbol = getKnownSymbol(WellKnownSymbols.match); + let replaceSymbol = getKnownSymbol(WellKnownSymbols.replace); + let replaceFn: (value: string, replaceValue: string | ((substring: string, ...args: any[]) => string)) => string; + let matcher: string | RegExp; + let isRegex = isRegExp(searchValue); + let theValue = isString(value) ? value : asString(value); + let isSearchNotNull = searchValue || !isStrictNullOrUndefined(searchValue); + + if (isRegex && isSearchNotNull) { + isRegex = (searchValue as any)[matchSymbol] !== false; + } + + if (isRegex) { + if (!(searchValue as RegExp).global) { + throwTypeError("searchValue must be a global regular expression"); + } + + matcher = searchValue; + } else { + replaceFn = isSearchNotNull ? (searchValue as any)[replaceSymbol] : null; + if (isFunction(replaceFn)) { + return replaceFn.call(searchValue, theValue, replaceValue); + } + + let search = isString(searchValue) ? searchValue : asString(searchValue); + + // eslint-disable-next-line security/detect-non-literal-regexp + matcher = new RegExp(strReplace(search, /[.*+?^${}()|[\]\\]/g, "\\$&") || "(?:)", "g"); + } + + return strReplace(theValue, matcher, replaceValue as any); +} diff --git a/lib/test/bundle-size-check.js b/lib/test/bundle-size-check.js index 6d5dd6c6..56709fe7 100644 --- a/lib/test/bundle-size-check.js +++ b/lib/test/bundle-size-check.js @@ -7,7 +7,7 @@ const configs = [ { name: "es5-min-full", path: "../bundle/es5/umd/ts-utils.min.js", - limit: 30.5 * 1024, // 30.5 kb in bytes + limit: 30.75 * 1024, // 30.75 kb in bytes compress: false }, { @@ -31,7 +31,7 @@ const configs = [ { name: "es5-min-poly", path: "../bundle/es5/ts-polyfills-utils.min.js", - limit: 9 * 1024, // 9 kb in bytes + limit: 9.5 * 1024, // 9.5 kb in bytes compress: false } ]; diff --git a/lib/test/src/common/array/with.test.ts b/lib/test/src/common/array/with.test.ts index ef5f7eb7..ce58006d 100644 --- a/lib/test/src/common/array/with.test.ts +++ b/lib/test/src/common/array/with.test.ts @@ -65,8 +65,8 @@ describe("arrWith", () => { it("should throw for null and undefined array", () => { // Native .with() throws TypeError for null/undefined - assert.throws(() => arrWith(null, 0, 99)); - assert.throws(() => arrWith(undefined, 0, 99)); + assert.throws(() => arrWith(null as any, 0, 99)); + assert.throws(() => arrWith(undefined as any, 0, 99)); }); it("should work with array-like objects", () => { @@ -84,6 +84,13 @@ describe("arrWith", () => { }); describe("polyArrWith", () => { + it("should throw RangeError for invalid array-like input", () => { + assert.throws(() => polyArrWith(null as any, 0, 99), RangeError); + assert.throws(() => polyArrWith(undefined as any, 0, 99), RangeError); + assert.throws(() => polyArrWith(123 as any, 0, 99), RangeError); + assert.throws(() => polyArrWith({} as any, 0, 99), RangeError); + }); + it("should match native Array.prototype.with for valid indices", () => { const arr: any = [1, 2, 3, 4, 5]; diff --git a/lib/test/src/common/string/replace.test.ts b/lib/test/src/common/string/replace.test.ts new file mode 100644 index 00000000..0f4056bc --- /dev/null +++ b/lib/test/src/common/string/replace.test.ts @@ -0,0 +1,79 @@ +/* + * @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 { dumpObj } from "../../../../src/helpers/diagnostics"; +import { strReplace } from "../../../../src/string/replace"; + +describe("strReplace", () => { + it("basic replacement", () => { + assert.equal(strReplace("a-b-a", "a", "x"), "x-b-a"); + }); + + it("supports regular expressions", () => { + assert.equal(strReplace("abc123def", /\d+/g, "#"), "abc#def"); + }); + + it("supports replacement function", () => { + assert.equal(strReplace("a1b2", /\d/g, (_match, index) => "(" + index + ")"), "a(1)b(3)"); + }); + + it("throws for null and undefined values", () => { + assert.throws(() => strReplace(null as any, "a", "b")); + assert.throws(() => strReplace(undefined as any, "a", "b")); + }); + + it("matches native replace semantics", () => { + let replacer = (match: string, offset: number) => { + return "(" + match + ":" + offset + ")"; + }; + + let testCases: { value: any; searchValue: any; replaceValue: any }[] = [ + { value: "banana", searchValue: "a", replaceValue: "o" }, + { value: "abc123abc", searchValue: /abc/g, replaceValue: "X" }, + { value: "abc123abc", searchValue: /\d+/, replaceValue: replacer }, + { value: "a.b.a.b", searchValue: ".", replaceValue: "!" }, + { value: "aaa", searchValue: "a", replaceValue: "$$" } + ]; + + for (let lp = 0; lp < testCases.length; lp++) { + let testCase = testCases[lp]; + _checkNativeParity(testCase.value, testCase.searchValue, testCase.replaceValue); + } + }); + + function _checkNativeParity(value: any, searchValue: any, replaceValue: any) { + let testResult: any; + let nativeResult: any; + let testThrew: any; + let nativeThrew: any; + + try { + testResult = strReplace(value, searchValue, replaceValue); + } catch (e) { + testThrew = e; + } + + try { + nativeResult = String.prototype.replace.call(value, searchValue, replaceValue); + } catch (e) { + nativeThrew = e; + } + + if (testThrew) { + assert.equal(true, !!nativeThrew, + "Checking whether Native and strReplace both threw [" + dumpObj(testThrew) + "] - [" + dumpObj(nativeThrew || nativeResult) + "] for [" + dumpObj(value) + "] [" + dumpObj(searchValue) + "]"); + } else if (nativeThrew) { + assert.ok(false, + "Native threw but strReplace did not [" + dumpObj(testResult) + "] - [" + dumpObj(nativeThrew) + "] for [" + dumpObj(value) + "] [" + dumpObj(searchValue) + "]"); + } else { + assert.equal(testResult, nativeResult, + "Checking whether Native and strReplace returned the same [" + dumpObj(testResult) + "] - [" + dumpObj(nativeResult) + "] for [" + dumpObj(value) + "] [" + dumpObj(searchValue) + "]"); + } + } +}); diff --git a/lib/test/src/common/string/replace_all.test.ts b/lib/test/src/common/string/replace_all.test.ts new file mode 100644 index 00000000..1714aa98 --- /dev/null +++ b/lib/test/src/common/string/replace_all.test.ts @@ -0,0 +1,195 @@ +/* + * @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 { dumpObj } from "../../../../src/helpers/diagnostics"; +import { polyStrReplaceAll, strReplaceAll } from "../../../../src/string/replace_all"; +import { getKnownSymbol } from "../../../../src/symbol/symbol"; +import { WellKnownSymbols } from "../../../../src/symbol/well_known"; +import { isNode } from "../../../../src/helpers/environment"; + +describe("strReplaceAll helper", () => { + it("basic string replacement", () => { + assert.equal(strReplaceAll("a-b-a-b", "a", "x"), "x-b-x-b"); + assert.equal(polyStrReplaceAll("a-b-a-b", "a", "x"), "x-b-x-b"); + }); + + it("supports empty search string", () => { + assert.equal(strReplaceAll("abc", "", "-"), "-a-b-c-"); + assert.equal(polyStrReplaceAll("abc", "", "-"), "-a-b-c-"); + }); + + it("supports replacement patterns", () => { + assert.equal(strReplaceAll("abc", "b", "[$&]"), "a[b]c"); + assert.equal(polyStrReplaceAll("abc", "b", "[$&]"), "a[b]c"); + }); + + it("supports function replacements", () => { + assert.equal(strReplaceAll("abca", "a", (value, index) => value.toUpperCase() + index), "A0bcA3"); + assert.equal(polyStrReplaceAll("abca", "a", (value, index) => value.toUpperCase() + index), "A0bcA3"); + }); + + it("supports global regular expressions", () => { + assert.equal(strReplaceAll("abc123def456", /\d+/g, "#"), "abc#def#"); + assert.equal(polyStrReplaceAll("abc123def456", /\d+/g, "#"), "abc#def#"); + }); + + it("delegates to @@replace for non-RegExp search objects", () => { + const replaceSym = getKnownSymbol(WellKnownSymbols.replace); + const searchValue = { + [replaceSym]: (value: string, replaceValue: string | ((substring: string, ...args: any[]) => string)) => { + return "custom:" + value + ":" + dumpObj(replaceValue); + } + }; + + const expected = "custom:abcabc:" + dumpObj("X"); + assert.equal(polyStrReplaceAll("abcabc", searchValue as any, "X"), expected); + assert.equal(strReplaceAll("abcabc", searchValue as any, "X"), expected); + }); + + it("treats RegExp with @@match false as non-RegExp for global check", () => { + const matchSym = getKnownSymbol(WellKnownSymbols.match); + const searchValue = /a/; + (searchValue as any)[matchSym] = false; + + assert.equal(strReplaceAll("a_a", searchValue as any, "x"), "x_a"); + assert.equal(polyStrReplaceAll("a_a", searchValue as any, "x"), "x_a"); + }); + + if (isNode()) { + // Browser environment is not "letting" us override built-in prototypes to test delegation for falsey values, + // but currently we can test in Node, this may fail if Node adds a native implementation, but we can address at that time if needed. + it("delegates to @@replace for falsey non-null search values", () => { + const replaceSym = getKnownSymbol(WellKnownSymbols.replace); + const numberReplace = (Number.prototype as any)[replaceSym]; + const booleanReplace = (Boolean.prototype as any)[replaceSym]; + const stringReplace = (String.prototype as any)[replaceSym]; + + (Number.prototype as any)[replaceSym] = function(value: string, replaceValue: any) { + return "number:" + value + ":" + dumpObj(replaceValue); + }; + (Boolean.prototype as any)[replaceSym] = function(value: string, replaceValue: any) { + return "boolean:" + value + ":" + dumpObj(replaceValue); + }; + (String.prototype as any)[replaceSym] = function(value: string, replaceValue: any) { + return "string:" + value + ":" + dumpObj(replaceValue); + }; + + try { + const expectedReplace = dumpObj("X"); + + assert.equal(strReplaceAll("abc", 0 as any, "X"), "number:abc:" + expectedReplace); + assert.equal(polyStrReplaceAll("abc", 0 as any, "X"), "number:abc:" + expectedReplace); + + assert.equal(strReplaceAll("abc", false as any, "X"), "boolean:abc:" + expectedReplace); + assert.equal(polyStrReplaceAll("abc", false as any, "X"), "boolean:abc:" + expectedReplace); + + assert.equal(strReplaceAll("abc", "" as any, "X"), "string:abc:" + expectedReplace); + assert.equal(polyStrReplaceAll("abc", "" as any, "X"), "string:abc:" + expectedReplace); + } finally { + (Number.prototype as any)[replaceSym] = numberReplace; + (Boolean.prototype as any)[replaceSym] = booleanReplace; + (String.prototype as any)[replaceSym] = stringReplace; + } + }); + } + + it("throws for non-global regular expressions", () => { + _expectThrow(() => { + strReplaceAll("abc123", /\d+/, "#"); + }); + + _expectThrow(() => { + polyStrReplaceAll("abc123", /\d+/, "#"); + }); + }); + + it("throws for null and undefined values", () => { + _expectThrow(() => { + strReplaceAll(null as any, "a", "b"); + }); + + _expectThrow(() => { + polyStrReplaceAll(undefined as any, "a", "b"); + }); + }); + + it("matches native replaceAll semantics", () => { + let replacer = (match: string, index: number) => { + return "(" + match + ":" + index + ")"; + }; + + let testCases: { value: any; searchValue: any; replaceValue: any }[] = [ + { value: "banana", searchValue: "a", replaceValue: "o" }, + { value: "banana", searchValue: "", replaceValue: "-" }, + { value: "abc123abc", searchValue: "abc", replaceValue: "X" }, + { value: "abc123abc", searchValue: /abc/g, replaceValue: "X" }, + { value: "abc123abc", searchValue: /\d+/g, replaceValue: replacer }, + { value: "a*b*a", searchValue: "*", replaceValue: "-" }, + { value: "a[b]c[d]", searchValue: "[", replaceValue: "(" }, + { value: "a.b.a.b", searchValue: ".", replaceValue: "!" }, + { value: "null value", searchValue: null, replaceValue: "nil" }, + { value: "undefined value", searchValue: undefined, replaceValue: "undef" }, + { value: 12341234, searchValue: "34", replaceValue: "x" }, + { value: "aaa", searchValue: "a", replaceValue: "$$" } + ]; + + for (let lp = 0; lp < testCases.length; lp++) { + let testCase = testCases[lp]; + _checkNativeParity("strReplaceAll", (value, searchValue, replaceValue) => { + return strReplaceAll(value, searchValue, replaceValue); + }, testCase.value, testCase.searchValue, testCase.replaceValue); + + _checkNativeParity("polyStrReplaceAll", (value, searchValue, replaceValue) => { + return polyStrReplaceAll(value, searchValue, replaceValue); + }, testCase.value, testCase.searchValue, testCase.replaceValue); + } + }); + + function _expectThrow(cb: () => void): Error { + try { + cb(); + } catch (e) { + assert.ok(true, "Expected an exception to be thrown"); + return e as Error; + } + + assert.ok(false, "Expected an exception to be thrown"); + return null as any; + } + + function _checkNativeParity(testName: string, testFn: (value: any, searchValue: any, replaceValue: any) => string, value: any, searchValue: any, replaceValue: any) { + let testResult: any; + let nativeResult: any; + let testThrew: any; + let nativeThrew: any; + + try { + testResult = testFn(value, searchValue, replaceValue); + } catch (e) { + testThrew = e; + } + + try { + nativeResult = (String.prototype as any).replaceAll.call(value, searchValue, replaceValue); + } catch (e) { + nativeThrew = e; + } + + if (testThrew) { + assert.equal(true, !!nativeThrew, + "Checking whether Native and " + testName + " both threw [" + dumpObj(testThrew) + "] - [" + dumpObj(nativeThrew || nativeResult) + "] for [" + dumpObj(value) + "] [" + dumpObj(searchValue) + "]"); + } else if (nativeThrew) { + assert.ok(false, + "Native threw but " + testName + " did not [" + dumpObj(testResult) + "] - [" + dumpObj(nativeThrew) + "] for [" + dumpObj(value) + "] [" + dumpObj(searchValue) + "]"); + } else { + assert.equal(testResult, nativeResult, + "Checking whether Native and " + testName + " returned the same [" + dumpObj(testResult) + "] - [" + dumpObj(nativeResult) + "] for [" + dumpObj(value) + "] [" + dumpObj(searchValue) + "]"); + } + } +}); diff --git a/lib/test/src/common/string/trim.test.ts b/lib/test/src/common/string/trim.test.ts index fae2ac55..4fb4b1b2 100644 --- a/lib/test/src/common/string/trim.test.ts +++ b/lib/test/src/common/string/trim.test.ts @@ -43,6 +43,36 @@ describe("string helpers", () => { _checkPolyTrimEnd(undefined); }); + describe("trim with non-nullish falsy values", () => { + _checkTrim(0); + _checkTrim(false); + }); + + describe("polyTrim with non-nullish falsy values", () => { + _checkPolyTrim(0); + _checkPolyTrim(false); + }); + + describe("trimStart with non-nullish falsy values", () => { + _checkTrimStart(0); + _checkTrimStart(false); + }); + + describe("polyTrimStart with non-nullish falsy values", () => { + _checkPolyTrimStart(0); + _checkPolyTrimStart(false); + }); + + describe("trimEnd with non-nullish falsy values", () => { + _checkTrimEnd(0); + _checkTrimEnd(false); + }); + + describe("polyTrimEnd with non-nullish falsy values", () => { + _checkPolyTrimEnd(0); + _checkPolyTrimEnd(false); + }); + describe("trim With values", () => { _checkTrim("null"); _checkTrim("undefined"); @@ -160,7 +190,7 @@ describe("string helpers", () => { _checkTrimEnd(" abba "); _checkTrimEnd("zyxyvutsrqponmlkjihgfedcba"); _checkTrimEnd(" zyxyvutsrqponmlkjihgfedcba"); - _checkTrimStart(" abba "); + _checkTrimEnd(" abba "); }); describe("polyTrimEnd With values", () => {