@ezez/utils 3.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/omit.d.ts +1 -1
- package/dist/omit.d.ts.map +1 -1
- package/dist/omit.js.map +1 -1
- package/dist/replaceDeep.d.ts +5 -1
- package/dist/replaceDeep.d.ts.map +1 -1
- package/dist/replaceDeep.js +3 -20
- package/dist/replaceDeep.js.map +1 -1
- package/dist/replaceDeepByFn.d.ts +7 -0
- package/dist/replaceDeepByFn.d.ts.map +1 -0
- package/dist/replaceDeepByFn.js +47 -0
- package/dist/replaceDeepByFn.js.map +1 -0
- package/dist/serialize.d.ts.map +1 -1
- package/dist/serialize.js +56 -18
- package/dist/serialize.js.map +1 -1
- package/dist/trim.d.ts +3 -0
- package/dist/trim.d.ts.map +1 -0
- package/dist/trim.js +10 -0
- package/dist/trim.js.map +1 -0
- package/dist/trimEnd.d.ts +3 -0
- package/dist/trimEnd.d.ts.map +1 -0
- package/dist/trimEnd.js +12 -0
- package/dist/trimEnd.js.map +1 -0
- package/dist/trimStart.d.ts +3 -0
- package/dist/trimStart.d.ts.map +1 -0
- package/dist/trimStart.js +12 -0
- package/dist/trimStart.js.map +1 -0
- package/dist/utils/utils.d.ts +6 -0
- package/dist/utils/utils.d.ts.map +1 -0
- package/dist/utils/utils.js +10 -0
- package/dist/utils/utils.js.map +1 -0
- package/docs/assets/search.js +1 -1
- package/docs/functions/cap.html +8 -5
- package/docs/functions/capitalize.html +8 -5
- package/docs/functions/coalesce.html +8 -5
- package/docs/functions/compareArrays.html +8 -5
- package/docs/functions/compareProps.html +8 -5
- package/docs/functions/deserialize.html +8 -5
- package/docs/functions/ensureArray.html +8 -5
- package/docs/functions/ensureDate.html +8 -5
- package/docs/functions/ensureError.html +8 -5
- package/docs/functions/ensurePrefix.html +8 -5
- package/docs/functions/ensureSuffix.html +8 -5
- package/docs/functions/ensureTimestamp.html +8 -5
- package/docs/functions/escapeRegExp.html +8 -5
- package/docs/functions/formatDate.html +8 -5
- package/docs/functions/get.html +8 -5
- package/docs/functions/getMultiple.html +8 -5
- package/docs/functions/insertSeparator.html +8 -5
- package/docs/functions/isEmpty.html +8 -5
- package/docs/functions/isNumericString.html +8 -5
- package/docs/functions/isPlainObject.html +8 -5
- package/docs/functions/last.html +8 -5
- package/docs/functions/later-1.html +8 -5
- package/docs/functions/mapAsync.html +8 -5
- package/docs/functions/mapValues.html +8 -5
- package/docs/functions/match.html +8 -5
- package/docs/functions/merge.html +16 -13
- package/docs/functions/mostFrequent.html +8 -5
- package/docs/functions/noop.html +8 -5
- package/docs/functions/occurrences.html +8 -5
- package/docs/functions/omit.html +14 -7
- package/docs/functions/pick.html +9 -6
- package/docs/functions/pull.html +8 -5
- package/docs/functions/remove.html +8 -5
- package/docs/functions/removeCommonProperties.html +8 -5
- package/docs/functions/replace.html +8 -5
- package/docs/functions/replaceDeep.html +19 -7
- package/docs/functions/rethrow.html +8 -5
- package/docs/functions/round.html +8 -5
- package/docs/functions/safe.html +9 -6
- package/docs/functions/sample.html +8 -5
- package/docs/functions/samples.html +8 -5
- package/docs/functions/scale.html +8 -5
- package/docs/functions/seq.html +8 -5
- package/docs/functions/seqEarlyBreak.html +8 -5
- package/docs/functions/serialize.html +8 -5
- package/docs/functions/set.html +8 -5
- package/docs/functions/setImmutable.html +8 -5
- package/docs/functions/shuffle.html +8 -5
- package/docs/functions/sortBy.html +8 -5
- package/docs/functions/sortProps.html +8 -5
- package/docs/functions/stripPrefix.html +8 -5
- package/docs/functions/stripSuffix.html +8 -5
- package/docs/functions/throttle.html +8 -5
- package/docs/functions/toggle.html +8 -5
- package/docs/functions/trim.html +152 -0
- package/docs/functions/trimEnd.html +152 -0
- package/docs/functions/trimStart.html +152 -0
- package/docs/functions/truthy.html +8 -5
- package/docs/functions/unique.html +8 -5
- package/docs/functions/wait.html +8 -5
- package/docs/functions/waitFor.html +8 -5
- package/docs/functions/waitSync.html +8 -5
- package/docs/index.html +7 -4
- package/docs/interfaces/ComparePropsOptions.html +6 -6
- package/docs/interfaces/GetMultipleSource.html +8 -5
- package/docs/interfaces/GetSource.html +8 -5
- package/docs/interfaces/IsNumericStringOptions.html +9 -9
- package/docs/interfaces/OccurencesOptions.html +6 -6
- package/docs/interfaces/SetImmutableSource.html +8 -5
- package/docs/interfaces/SetSource.html +8 -5
- package/docs/interfaces/ThrottleOptions.html +7 -7
- package/docs/interfaces/ThrottledFunctionExtras.html +7 -7
- package/docs/modules.html +10 -4
- package/docs/pages/CHANGELOG.html +113 -64
- package/docs/pages/Introduction.html +7 -4
- package/docs/types/CustomDeserializers.html +8 -5
- package/docs/types/CustomSerializers.html +8 -5
- package/docs/types/Later.html +8 -5
- package/docs/types/MapValuesFn.html +8 -5
- package/docs/types/MatchCallback.html +8 -5
- package/docs/types/SeqEarlyBreaker.html +8 -5
- package/docs/types/SeqFn.html +8 -5
- package/docs/types/SeqFunctions.html +8 -5
- package/docs/types/SetImmutablePath.html +8 -5
- package/docs/types/ThrottledFunction.html +8 -5
- package/docs/variables/mapValuesUNSET.html +8 -5
- package/docs/variables/mergeUNSET.html +8 -5
- package/esm/index.d.ts +3 -0
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +3 -0
- package/esm/index.js.map +1 -1
- package/esm/omit.d.ts +1 -1
- package/esm/omit.d.ts.map +1 -1
- package/esm/omit.js.map +1 -1
- package/esm/replaceDeep.d.ts +5 -1
- package/esm/replaceDeep.d.ts.map +1 -1
- package/esm/replaceDeep.js +3 -20
- package/esm/replaceDeep.js.map +1 -1
- package/esm/replaceDeepByFn.d.ts +7 -0
- package/esm/replaceDeepByFn.d.ts.map +1 -0
- package/esm/replaceDeepByFn.js +44 -0
- package/esm/replaceDeepByFn.js.map +1 -0
- package/esm/serialize.d.ts.map +1 -1
- package/esm/serialize.js +56 -18
- package/esm/serialize.js.map +1 -1
- package/esm/trim.d.ts +3 -0
- package/esm/trim.d.ts.map +1 -0
- package/esm/trim.js +7 -0
- package/esm/trim.js.map +1 -0
- package/esm/trimEnd.d.ts +3 -0
- package/esm/trimEnd.d.ts.map +1 -0
- package/esm/trimEnd.js +9 -0
- package/esm/trimEnd.js.map +1 -0
- package/esm/trimStart.d.ts +3 -0
- package/esm/trimStart.d.ts.map +1 -0
- package/esm/trimStart.js +9 -0
- package/esm/trimStart.js.map +1 -0
- package/esm/utils/utils.d.ts +6 -0
- package/esm/utils/utils.d.ts.map +1 -0
- package/esm/utils/utils.js +7 -0
- package/esm/utils/utils.js.map +1 -0
- package/package.json +1 -1
- package/src/deserialize.spec.ts +12 -0
- package/src/index.ts +3 -0
- package/src/omit.ts +8 -2
- package/src/pick.ts +1 -1
- package/src/replaceDeep.spec.ts +91 -0
- package/src/replaceDeep.ts +22 -27
- package/src/replaceDeepByFn.spec.ts +162 -0
- package/src/replaceDeepByFn.ts +93 -0
- package/src/serialize.spec.ts +42 -0
- package/src/serialize.ts +65 -18
- package/src/trim.spec.ts +22 -0
- package/src/trim.ts +23 -0
- package/src/trimEnd.spec.ts +20 -0
- package/src/trimEnd.ts +22 -0
- package/src/trimStart.spec.ts +20 -0
- package/src/trimStart.ts +22 -0
- package/src/utils/utils.ts +11 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { isPlainObject } from "./isPlainObject";
|
|
2
|
+
|
|
3
|
+
type Options = {
|
|
4
|
+
/**
|
|
5
|
+
* If true, the source objects and arrays will be mutated. Default is false.
|
|
6
|
+
*/
|
|
7
|
+
mutate?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* If true, the function will go into instances for replacement. Otherwise, it will only replace properties of plain
|
|
10
|
+
* objects. Default is false.
|
|
11
|
+
* Warning: This option requires `mutate` to be enabled, because we can't clone instances.
|
|
12
|
+
*/
|
|
13
|
+
replaceInstancesProps?: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Replaces all occurrences of `search` with `value` in `source` object/array. You do comparison by yourself by providing a callback function.
|
|
18
|
+
*
|
|
19
|
+
* Warnings:
|
|
20
|
+
* - By default, it does not mutate the `source`/deep objects/arrays, but it can be enabled with `mutate` option.
|
|
21
|
+
* - By default, it does not go into instances for replacement, only plain objects.
|
|
22
|
+
* - If your instances are cross-referenced, you may end up in an infinite loop.
|
|
23
|
+
*
|
|
24
|
+
* TypeScript users: This is way too dynamic to type properly, therefore, typing assumes the most basic form of
|
|
25
|
+
* replacement where search and value are of the same type. If that's not the case for you - you'll have to typecast.
|
|
26
|
+
*
|
|
27
|
+
* @param source - source object/array/value
|
|
28
|
+
* @param search - value to search for
|
|
29
|
+
* @param replaceWith - value to replace with
|
|
30
|
+
* @param options - optional options
|
|
31
|
+
*/
|
|
32
|
+
const replaceDeepByFn = <T>( // eslint-disable-line max-statements
|
|
33
|
+
source: T, search: (value: unknown) => boolean, replaceWith: (value: unknown) => unknown, options?: Options,
|
|
34
|
+
): T => {
|
|
35
|
+
if (options?.replaceInstancesProps && !options.mutate) {
|
|
36
|
+
throw new Error("`replaceInstancesProps` option requires `mutate` to be enabled");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const searchResult = search(source);
|
|
40
|
+
|
|
41
|
+
if (typeof searchResult !== "boolean") {
|
|
42
|
+
throw new Error("search function must return a boolean");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (searchResult) {
|
|
46
|
+
return replaceWith(source) as T;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (source == null) {
|
|
50
|
+
return source;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (typeof source === "object") {
|
|
54
|
+
if (Array.isArray(source)) {
|
|
55
|
+
if (options?.mutate) {
|
|
56
|
+
for (let i = 0; i < source.length; i++) {
|
|
57
|
+
// eslint-disable-next-line no-param-reassign,@typescript-eslint/no-unsafe-assignment
|
|
58
|
+
source[i] = replaceDeepByFn(source[i], search, replaceWith, options);
|
|
59
|
+
}
|
|
60
|
+
return source;
|
|
61
|
+
}
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
63
|
+
return source.map((item) => replaceDeepByFn(item, search, replaceWith, options)) as unknown as T;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (options?.mutate) {
|
|
67
|
+
if (isPlainObject(source) || options.replaceInstancesProps) {
|
|
68
|
+
for (const key of Object.keys(source)) {
|
|
69
|
+
// eslint-disable-next-line no-param-reassign
|
|
70
|
+
(source as Record<string, unknown>)[key] = replaceDeepByFn(
|
|
71
|
+
(source as Record<string, unknown>)[key], search, replaceWith, options,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return source;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (isPlainObject(source)) {
|
|
80
|
+
return Object.keys(source).reduce<Record<string, unknown>>((acc, key) => {
|
|
81
|
+
// eslint-disable-next-line no-param-reassign
|
|
82
|
+
acc[key] = replaceDeepByFn((source as Record<string, unknown>)[key], search, replaceWith, options);
|
|
83
|
+
return acc;
|
|
84
|
+
}, {}) as T;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return source;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export {
|
|
92
|
+
replaceDeepByFn,
|
|
93
|
+
};
|
package/src/serialize.spec.ts
CHANGED
|
@@ -80,4 +80,46 @@ describe("serialize", () => {
|
|
|
80
80
|
|
|
81
81
|
must(serialize(a)).equal(serialize(b));
|
|
82
82
|
});
|
|
83
|
+
|
|
84
|
+
it("allows serializing dates and other objects containing .toJSON", async () => {
|
|
85
|
+
const date = new Date(1714390008941);
|
|
86
|
+
must(serialize(date, {
|
|
87
|
+
D: (value) => {
|
|
88
|
+
if (value instanceof Date) {
|
|
89
|
+
return String(value.getTime());
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
},
|
|
93
|
+
})).equal(`"D:1714390008941"`);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should avoid excessive calls to custom serializers", async () => {
|
|
97
|
+
const customSerializers: CustomSerializers = {
|
|
98
|
+
p: (value) => {
|
|
99
|
+
// console.log("Called with", typeof value, value);
|
|
100
|
+
if (value instanceof Person) {
|
|
101
|
+
return value.name;
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const p1 = new Person("John");
|
|
108
|
+
const spy = jest.spyOn(customSerializers, "p");
|
|
109
|
+
|
|
110
|
+
serialize({
|
|
111
|
+
a: [p1],
|
|
112
|
+
b: 1,
|
|
113
|
+
c: {
|
|
114
|
+
d: true,
|
|
115
|
+
e: p1,
|
|
116
|
+
},
|
|
117
|
+
}, customSerializers);
|
|
118
|
+
expect(spy).toHaveBeenCalledTimes(4);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("should not crash on unknown instances but serialize as plain object", async () => {
|
|
122
|
+
const p1 = new Person("John");
|
|
123
|
+
must(serialize(p1)).equal(`{"name":"s:John"}`);
|
|
124
|
+
});
|
|
83
125
|
});
|
package/src/serialize.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { sortProps } from "./sortProps.js";
|
|
2
|
+
import { replaceDeepByFn } from "./replaceDeepByFn.js";
|
|
3
|
+
import { isPlainObject } from "./isPlainObject.js";
|
|
4
|
+
import { DataWrapper } from "./utils/utils.js";
|
|
2
5
|
|
|
3
6
|
type CustomSerializers = {
|
|
4
7
|
[key: string]: (data: unknown) => (string | null);
|
|
@@ -33,6 +36,32 @@ type Options = {
|
|
|
33
36
|
* @param options - options
|
|
34
37
|
*/
|
|
35
38
|
const serialize = (data: unknown, customSerializers?: CustomSerializers, options?: Options) => { // eslint-disable-line max-lines-per-function,max-len
|
|
39
|
+
const sourceData = Object.keys(customSerializers ?? {}).length
|
|
40
|
+
? replaceDeepByFn(
|
|
41
|
+
data,
|
|
42
|
+
value => {
|
|
43
|
+
if (["string", "number", "bigint", "undefined", "boolean"].includes(typeof value)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (value === null || Array.isArray(value)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (isPlainObject(value)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const serializerKeys = Object.keys(customSerializers ?? {});
|
|
54
|
+
for (const key of serializerKeys) {
|
|
55
|
+
const serialized = customSerializers![key]!(value);
|
|
56
|
+
if (typeof serialized === "string") {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
},
|
|
62
|
+
value => new DataWrapper(value),
|
|
63
|
+
)
|
|
64
|
+
: data;
|
|
36
65
|
const replacer = (_key: string, value: unknown) => { // eslint-disable-line max-statements
|
|
37
66
|
if (typeof value === "string") {
|
|
38
67
|
return `s:${value}`;
|
|
@@ -52,12 +81,22 @@ const serialize = (data: unknown, customSerializers?: CustomSerializers, options
|
|
|
52
81
|
if (value === null) {
|
|
53
82
|
return "l:";
|
|
54
83
|
}
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
return value as unknown[];
|
|
86
|
+
}
|
|
87
|
+
if (isPlainObject(value)) {
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
55
90
|
const serializerKeys = Object.keys(customSerializers ?? {});
|
|
56
91
|
for (const key of serializerKeys) {
|
|
57
|
-
const serialized = customSerializers![key]!(value);
|
|
92
|
+
const serialized = customSerializers![key]!(value instanceof DataWrapper ? value.data : value);
|
|
58
93
|
if (typeof serialized === "string") {
|
|
59
94
|
return `${key}:${serialized}`;
|
|
60
95
|
}
|
|
96
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
97
|
+
if (serialized !== null) {
|
|
98
|
+
throw new Error(`Custom serializer for key ${key} returned a non-string value: ${String(serialized)}`);
|
|
99
|
+
}
|
|
61
100
|
}
|
|
62
101
|
if (typeof value === "object") {
|
|
63
102
|
return value;
|
|
@@ -67,35 +106,43 @@ const serialize = (data: unknown, customSerializers?: CustomSerializers, options
|
|
|
67
106
|
};
|
|
68
107
|
|
|
69
108
|
if (
|
|
70
|
-
|
|
71
|
-
|| typeof
|
|
72
|
-
|| typeof
|
|
73
|
-
|| typeof
|
|
74
|
-
|| typeof
|
|
75
|
-
|| typeof
|
|
76
|
-
|| Array.isArray(
|
|
109
|
+
sourceData == null
|
|
110
|
+
|| typeof sourceData === "string"
|
|
111
|
+
|| typeof sourceData === "number"
|
|
112
|
+
|| typeof sourceData === "bigint"
|
|
113
|
+
|| typeof sourceData === "undefined"
|
|
114
|
+
|| typeof sourceData === "boolean"
|
|
115
|
+
|| Array.isArray(sourceData)
|
|
77
116
|
) {
|
|
78
|
-
return JSON.stringify(
|
|
117
|
+
return JSON.stringify(sourceData, replacer);
|
|
79
118
|
}
|
|
80
119
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
120
|
+
if (!isPlainObject(sourceData)) {
|
|
121
|
+
const serializerKeysGlobal = Object.keys(customSerializers ?? {});
|
|
122
|
+
for (const key of serializerKeysGlobal) {
|
|
123
|
+
const serialized = customSerializers![key]!(
|
|
124
|
+
sourceData instanceof DataWrapper ? sourceData.data : sourceData,
|
|
125
|
+
);
|
|
126
|
+
if (typeof serialized === "string") {
|
|
127
|
+
return `"${key}:${serialized}"`;
|
|
128
|
+
}
|
|
129
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
130
|
+
if (serialized !== null) {
|
|
131
|
+
throw new Error(`Custom serializer for key ${key} returned a non-string value: ${String(serialized)}`);
|
|
132
|
+
}
|
|
86
133
|
}
|
|
87
134
|
}
|
|
88
135
|
|
|
89
|
-
if (typeof
|
|
136
|
+
if (typeof sourceData === "object") {
|
|
90
137
|
return JSON.stringify(
|
|
91
138
|
options?.sortProps === false
|
|
92
|
-
?
|
|
93
|
-
: sortProps(
|
|
139
|
+
? sourceData
|
|
140
|
+
: sortProps(sourceData as Record<string, unknown>),
|
|
94
141
|
replacer,
|
|
95
142
|
);
|
|
96
143
|
}
|
|
97
144
|
|
|
98
|
-
throw new Error(`Unsupported data type: ${typeof
|
|
145
|
+
throw new Error(`Unsupported data type: ${typeof sourceData}`);
|
|
99
146
|
};
|
|
100
147
|
|
|
101
148
|
export type { CustomSerializers };
|
package/src/trim.spec.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { trim } from "./trim";
|
|
2
|
+
|
|
3
|
+
describe("trim", () => {
|
|
4
|
+
it("should trim single character from both sides of the string", async () => {
|
|
5
|
+
must(trim("aaacataaa", "a")).equal("cat");
|
|
6
|
+
must(trim("abacteria", "a")).equal("bacteri");
|
|
7
|
+
must(trim("abc", "a")).equal("bc");
|
|
8
|
+
must(trim("abc", "c")).equal("ab");
|
|
9
|
+
must(trim("aajjja", "a")).equal("jjj");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("should trim multiple characters as a whole", async () => {
|
|
13
|
+
must(trim("abopopab", "ab")).equal("opop");
|
|
14
|
+
must(trim("atitleb", "ab")).equal("atitleb");
|
|
15
|
+
must(trim("abtitleab", "ab")).equal("title");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("can trim into empty string", async () => {
|
|
19
|
+
must(trim("abc", "abc")).equal("");
|
|
20
|
+
must(trim("a", "a")).equal("");
|
|
21
|
+
});
|
|
22
|
+
});
|
package/src/trim.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { trimStart } from "./trimStart.js";
|
|
2
|
+
import { trimEnd } from "./trimEnd.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Removes given characters from both sides of the string.
|
|
6
|
+
* If you want to remove from one side of the string see {@link trimStart} and {@link trimEnd}.
|
|
7
|
+
*
|
|
8
|
+
* @param source - Source string.
|
|
9
|
+
* @param characters - Characters to remove, taken as a whole.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* trim("abcb", "ab"); // "cb"
|
|
13
|
+
* trim("!aaa!", "!"); // "aaa"
|
|
14
|
+
* trim("!aaa!!!", "!"); // "aaa"
|
|
15
|
+
* trim("!aaa!!!", "!!!"); // "!aaa"
|
|
16
|
+
*/
|
|
17
|
+
const trim = (source: string, characters: string) => {
|
|
18
|
+
return trimStart(trimEnd(source, characters), characters);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
trim,
|
|
23
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { trimEnd } from "./trimEnd";
|
|
2
|
+
|
|
3
|
+
describe("trimEnd", () => {
|
|
4
|
+
it("should trim single character from the end of the string", async () => {
|
|
5
|
+
must(trimEnd("abc?", "?")).equal("abc");
|
|
6
|
+
must(trimEnd("a? b?c???", "?")).equal("a? b?c");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("should trim multiple characters as a whole", async () => {
|
|
10
|
+
must(trimEnd("abc", "bc")).equal("a");
|
|
11
|
+
|
|
12
|
+
must(trimEnd("abcc", "bc")).equal("abcc");
|
|
13
|
+
must(trimEnd("abcb", "bc")).equal("abcb");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("can trim into empty string", async () => {
|
|
17
|
+
must(trimEnd("abc", "abc")).equal("");
|
|
18
|
+
must(trimEnd("a", "a")).equal("");
|
|
19
|
+
});
|
|
20
|
+
});
|
package/src/trimEnd.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removes given characters from the end of the string.
|
|
3
|
+
* See also: {@link trim} and {@link trimStart}.
|
|
4
|
+
*
|
|
5
|
+
* @param source - Source string.
|
|
6
|
+
* @param characters - Characters to remove, taken as a whole.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* trimEnd("abcxzyz", "yz"); // "abcxz"
|
|
10
|
+
* trimEnd("!aaa!!", "!"); // "!aaa"
|
|
11
|
+
*/
|
|
12
|
+
const trimEnd = (source: string, characters: string) => {
|
|
13
|
+
let s = source;
|
|
14
|
+
while (s.endsWith(characters)) {
|
|
15
|
+
s = s.slice(0, -characters.length);
|
|
16
|
+
}
|
|
17
|
+
return s;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
trimEnd,
|
|
22
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { trimStart } from "./trimStart";
|
|
2
|
+
|
|
3
|
+
describe("trimStart", () => {
|
|
4
|
+
it("should trim single character from the start of the string", async () => {
|
|
5
|
+
must(trimStart("?abc", "?")).equal("abc");
|
|
6
|
+
must(trimStart("aaabc", "a")).equal("bc");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("should trim multiple characters as a whole", async () => {
|
|
10
|
+
must(trimStart("abc", "ab")).equal("c");
|
|
11
|
+
|
|
12
|
+
must(trimStart("aabcc", "ab")).equal("aabcc");
|
|
13
|
+
must(trimStart("babcb", "ab")).equal("babcb");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should trim into empty string if needed", async () => {
|
|
17
|
+
must(trimStart("abc", "abc")).equal("");
|
|
18
|
+
must(trimStart("aaa", "a")).equal("");
|
|
19
|
+
});
|
|
20
|
+
});
|
package/src/trimStart.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removes given characters from the start of the string.
|
|
3
|
+
* See also: {@link trim} and {@link trimEnd}.
|
|
4
|
+
*
|
|
5
|
+
* @param source - Source string.
|
|
6
|
+
* @param characters - Characters to remove, taken as a whole.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* trimStart("abbcb", "ab"); // "bcb"
|
|
10
|
+
* trimStart("!!aaa!", "!"); // "aaa!"
|
|
11
|
+
*/
|
|
12
|
+
const trimStart = (source: string, characters: string) => {
|
|
13
|
+
let s = source;
|
|
14
|
+
while (s.startsWith(characters)) {
|
|
15
|
+
s = s.slice(characters.length);
|
|
16
|
+
}
|
|
17
|
+
return s;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
trimStart,
|
|
22
|
+
};
|