@ezez/utils 1.9.0 → 2.1.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 +14 -1
- package/README.md +4 -0
- package/dist/deserialize.d.ts +13 -0
- package/dist/deserialize.d.ts.map +1 -0
- package/dist/deserialize.js +42 -0
- package/dist/deserialize.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/omit.js +3 -1
- package/dist/omit.js.map +1 -1
- package/dist/replace.d.ts.map +1 -1
- package/dist/replace.js.map +1 -1
- package/dist/replaceDeep.d.ts +3 -0
- package/dist/replaceDeep.d.ts.map +1 -0
- package/dist/replaceDeep.js +26 -0
- package/dist/replaceDeep.js.map +1 -0
- package/dist/round.d.ts +3 -0
- package/dist/round.d.ts.map +1 -0
- package/dist/round.js +9 -0
- package/dist/round.js.map +1 -0
- package/dist/safe.d.ts.map +1 -1
- package/dist/safe.js +1 -1
- package/dist/safe.js.map +1 -1
- package/dist/serialize.d.ts +16 -0
- package/dist/serialize.d.ts.map +1 -0
- package/dist/serialize.js +61 -0
- package/dist/serialize.js.map +1 -0
- package/dist/setImmutable.js +1 -1
- package/dist/setImmutable.js.map +1 -1
- package/dist/sortBy.js +1 -2
- package/dist/sortBy.js.map +1 -1
- package/dist/sortProps.d.ts.map +1 -1
- package/dist/sortProps.js.map +1 -1
- package/dist/throttle.js +5 -7
- package/dist/throttle.js.map +1 -1
- package/docs/assets/search.js +1 -1
- package/docs/functions/cap.html +12 -6
- package/docs/functions/capitalize.html +12 -6
- package/docs/functions/coalesce.html +12 -6
- package/docs/functions/compareArrays.html +12 -6
- package/docs/functions/compareProps.html +12 -6
- package/docs/functions/deserialize.html +149 -0
- package/docs/functions/ensureArray.html +12 -6
- package/docs/functions/ensureDate.html +12 -6
- package/docs/functions/ensureError.html +12 -6
- package/docs/functions/ensurePrefix.html +12 -6
- package/docs/functions/ensureSuffix.html +12 -6
- package/docs/functions/ensureTimestamp.html +12 -6
- package/docs/functions/escapeRegExp.html +12 -6
- package/docs/functions/formatDate.html +12 -6
- package/docs/functions/get.html +12 -6
- package/docs/functions/getMultiple.html +12 -6
- package/docs/functions/insertSeparator.html +12 -6
- package/docs/functions/isEmpty.html +12 -6
- package/docs/functions/isNumericString.html +12 -6
- package/docs/functions/isPlainObject.html +12 -6
- package/docs/functions/last.html +12 -6
- package/docs/functions/later-1.html +12 -6
- package/docs/functions/mapAsync.html +12 -6
- package/docs/functions/mapValues.html +12 -6
- package/docs/functions/match.html +12 -6
- package/docs/functions/merge.html +20 -14
- package/docs/functions/mostFrequent.html +12 -6
- package/docs/functions/noop.html +12 -6
- package/docs/functions/occurrences.html +12 -6
- package/docs/functions/omit.html +12 -6
- package/docs/functions/pick.html +12 -6
- package/docs/functions/pull.html +12 -6
- package/docs/functions/remove.html +12 -6
- package/docs/functions/removeCommonProperties.html +12 -6
- package/docs/functions/replace.html +12 -6
- package/docs/functions/replaceDeep.html +154 -0
- package/docs/functions/rethrow.html +12 -6
- package/docs/functions/round.html +144 -0
- package/docs/functions/safe.html +13 -7
- package/docs/functions/scale.html +12 -6
- package/docs/functions/seq.html +12 -6
- package/docs/functions/seqEarlyBreak.html +12 -6
- package/docs/functions/serialize.html +154 -0
- package/docs/functions/set.html +12 -6
- package/docs/functions/setImmutable.html +12 -6
- package/docs/functions/sortBy.html +12 -6
- package/docs/functions/sortProps.html +15 -7
- package/docs/functions/stripPrefix.html +12 -6
- package/docs/functions/stripSuffix.html +12 -6
- package/docs/functions/throttle.html +12 -6
- package/docs/functions/truthy.html +12 -6
- package/docs/functions/unique.html +12 -6
- package/docs/functions/wait.html +12 -6
- package/docs/functions/waitFor.html +12 -6
- package/docs/functions/waitSync.html +12 -6
- package/docs/index.html +15 -5
- package/docs/interfaces/ComparePropsOptions.html +6 -6
- package/docs/interfaces/GetMultipleSource.html +12 -6
- package/docs/interfaces/GetSource.html +12 -6
- package/docs/interfaces/IsNumericStringOptions.html +9 -9
- package/docs/interfaces/OccurencesOptions.html +6 -6
- package/docs/interfaces/SetImmutableSource.html +12 -6
- package/docs/interfaces/SetSource.html +12 -6
- package/docs/interfaces/ThrottleOptions.html +7 -7
- package/docs/interfaces/ThrottledFunctionExtras.html +7 -7
- package/docs/modules.html +17 -5
- package/docs/pages/CHANGELOG.html +77 -39
- package/docs/pages/Introduction.html +11 -5
- package/docs/types/CustomDeserializers.html +152 -0
- package/docs/types/CustomSerializers.html +152 -0
- package/docs/types/Later.html +12 -6
- package/docs/types/MapValuesFn.html +12 -6
- package/docs/types/MatchCallback.html +12 -6
- package/docs/types/SeqEarlyBreaker.html +12 -6
- package/docs/types/SeqFn.html +12 -6
- package/docs/types/SeqFunctions.html +12 -6
- package/docs/types/SetImmutablePath.html +12 -6
- package/docs/types/ThrottledFunction.html +12 -6
- package/docs/variables/mapValuesUNSET.html +12 -6
- package/docs/variables/mergeUNSET.html +12 -6
- package/esm/deserialize.d.ts +13 -0
- package/esm/deserialize.d.ts.map +1 -0
- package/esm/deserialize.js +39 -0
- package/esm/deserialize.js.map +1 -0
- package/esm/index.d.ts +4 -0
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +4 -0
- package/esm/index.js.map +1 -1
- package/esm/replace.d.ts.map +1 -1
- package/esm/replace.js.map +1 -1
- package/esm/replaceDeep.d.ts +3 -0
- package/esm/replaceDeep.d.ts.map +1 -0
- package/esm/replaceDeep.js +23 -0
- package/esm/replaceDeep.js.map +1 -0
- package/esm/round.d.ts +3 -0
- package/esm/round.d.ts.map +1 -0
- package/esm/round.js +6 -0
- package/esm/round.js.map +1 -0
- package/esm/safe.d.ts.map +1 -1
- package/esm/safe.js.map +1 -1
- package/esm/serialize.d.ts +16 -0
- package/esm/serialize.d.ts.map +1 -0
- package/esm/serialize.js +58 -0
- package/esm/serialize.js.map +1 -0
- package/esm/sortBy.js +1 -2
- package/esm/sortBy.js.map +1 -1
- package/esm/sortProps.d.ts.map +1 -1
- package/esm/sortProps.js.map +1 -1
- package/esm/throttle.js +5 -7
- package/esm/throttle.js.map +1 -1
- package/package.json +26 -25
- package/pnpm-lock.yaml +5223 -0
- package/src/deserialize.spec.ts +69 -0
- package/src/deserialize.ts +74 -0
- package/src/index.ts +4 -0
- package/src/replace.ts +0 -1
- package/src/replaceDeep.spec.ts +71 -0
- package/src/replaceDeep.ts +44 -0
- package/src/round.spec.ts +27 -0
- package/src/round.ts +21 -0
- package/src/safe.ts +0 -1
- package/src/serialize.spec.ts +83 -0
- package/src/serialize.ts +102 -0
- package/src/sortProps.ts +4 -1
- package/babel.config.cjs +0 -6
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { CustomDeserializers } from "./deserialize";
|
|
2
|
+
|
|
3
|
+
import { deserialize } from "./deserialize";
|
|
4
|
+
|
|
5
|
+
class Person {
|
|
6
|
+
public name: string;
|
|
7
|
+
|
|
8
|
+
public constructor(name: string) {
|
|
9
|
+
this.name = name;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("deserialize", () => {
|
|
14
|
+
it("can deserialize primitives", async () => {
|
|
15
|
+
must(deserialize(`"s:test"`)).equal("test");
|
|
16
|
+
must(deserialize(`"n:123"`)).equal(123);
|
|
17
|
+
must(deserialize(`"i:123"`)).equal(123n);
|
|
18
|
+
must(deserialize(`"u:"`)).equal(undefined);
|
|
19
|
+
must(deserialize(`"b:1"`)).equal(true);
|
|
20
|
+
must(deserialize(`"b:"`)).equal(false);
|
|
21
|
+
must(deserialize(`"l:"`)).equal(null);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("can deserialize arrays", () => {
|
|
25
|
+
must(deserialize(`["s:a","s:b","s:c"]`)).eql(["a", "b", "c"]);
|
|
26
|
+
must(deserialize(`["n:1","i:2","u:","b:1","b:","l:"]`)).eql([1, 2n, undefined, true, false, null]);
|
|
27
|
+
|
|
28
|
+
// deep arrays:
|
|
29
|
+
must(deserialize(`["n:1",["n:2",["n:3",["n:4",["n:5"]]]]]`)).eql([1, [2, [3, [4, [5]]]]]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("can deserialize objects", () => {
|
|
33
|
+
must(deserialize(`{"a":"s:a","b":"s:b","c":"s:c"}`)).eql({ a: "a", b: "b", c: "c" });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("supports custom deserializers", () => {
|
|
37
|
+
const customDeserializers: CustomDeserializers = {
|
|
38
|
+
p: (value) => {
|
|
39
|
+
return new Person(value);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const p1 = deserialize<Person>(`"p:John"`, customDeserializers);
|
|
44
|
+
must(p1).be.instanceOf(Person);
|
|
45
|
+
must(p1.name).equal("John");
|
|
46
|
+
|
|
47
|
+
const res = deserialize<{ a: Person }>(`{"a":"p:John"}`, customDeserializers);
|
|
48
|
+
const p2 = res.a;
|
|
49
|
+
must(p2).be.instanceOf(Person);
|
|
50
|
+
must(p2.name).equal("John");
|
|
51
|
+
|
|
52
|
+
must(res).eql({ a: p2 });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("allows custom deserializers to support primitives", () => {
|
|
56
|
+
const customSerializers: CustomDeserializers = {
|
|
57
|
+
sym: (value) => {
|
|
58
|
+
return Symbol.for(value);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// must(serialize(Symbol("x"), customSerializers)).equal(`"sym:x"`);
|
|
63
|
+
must(deserialize(`"sym:x"`, customSerializers)).equal(Symbol.for("x"));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("throws on unknown data type", () => {
|
|
67
|
+
must(() => deserialize(`"v:test"`)).throw("Unsupported data type: v");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { replaceDeep } from "./replaceDeep.js";
|
|
2
|
+
|
|
3
|
+
// if JSON.parse, even via replacer will return `undefined` the value will be eaten out, all other values
|
|
4
|
+
// are fine, so we can use this symbol to simulate `undefined` when parsing, then replace it in the result if needed
|
|
5
|
+
const UNDEFINED = Symbol("undefined");
|
|
6
|
+
|
|
7
|
+
type CustomDeserializers = {
|
|
8
|
+
[key: string]: (data: string) => unknown;
|
|
9
|
+
s?: never;
|
|
10
|
+
n?: never;
|
|
11
|
+
u?: never;
|
|
12
|
+
l?: never;
|
|
13
|
+
b?: never;
|
|
14
|
+
i?: never;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Deserializes a string serialized with `serialize` into a value.
|
|
19
|
+
*
|
|
20
|
+
* You will need to specify deserializers if custom data types are serialized. See `serialize` for more information.
|
|
21
|
+
*
|
|
22
|
+
* @see {@link serialize}.
|
|
23
|
+
*
|
|
24
|
+
* @param serializedString - the serialized string
|
|
25
|
+
* @param customDeserializers - an object with custom deserializers
|
|
26
|
+
*/
|
|
27
|
+
const deserialize = <T>(serializedString: string, customDeserializers?: CustomDeserializers): T => {
|
|
28
|
+
let hasUndefined = false;
|
|
29
|
+
const replacer = (_key: string, value: unknown) => { // eslint-disable-line max-statements
|
|
30
|
+
if (typeof value === "string") {
|
|
31
|
+
if (value.startsWith("s:")) {
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
|
33
|
+
return value.slice(2);
|
|
34
|
+
}
|
|
35
|
+
if (value.startsWith("n:")) {
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
|
37
|
+
return Number(value.slice(2));
|
|
38
|
+
}
|
|
39
|
+
if (value.startsWith("i:")) {
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
|
41
|
+
return BigInt(value.slice(2));
|
|
42
|
+
}
|
|
43
|
+
if (value === "u:") {
|
|
44
|
+
hasUndefined = true;
|
|
45
|
+
return UNDEFINED;
|
|
46
|
+
}
|
|
47
|
+
if (value === "l:") {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (value.startsWith("b:")) {
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
|
52
|
+
return value.slice(2) === "1";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const semiColonIndex = value.indexOf(":");
|
|
56
|
+
const type = value.slice(0, semiColonIndex);
|
|
57
|
+
if (customDeserializers && type in customDeserializers) {
|
|
58
|
+
return customDeserializers[type]!(value.slice(semiColonIndex + 1));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
throw new Error(`Unsupported data type: ${type}`);
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const parsed = JSON.parse(serializedString, replacer) as T;
|
|
67
|
+
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
69
|
+
return hasUndefined ? replaceDeep(parsed, UNDEFINED, undefined) : parsed;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export { deserialize };
|
|
73
|
+
|
|
74
|
+
export type { CustomDeserializers };
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from "./capitalize.js";
|
|
|
6
6
|
export * from "./coalesce.js";
|
|
7
7
|
export * from "./compareArrays.js";
|
|
8
8
|
export * from "./compareProps.js";
|
|
9
|
+
export * from "./deserialize.js";
|
|
9
10
|
export * from "./ensureArray.js";
|
|
10
11
|
export * from "./ensureDate.js";
|
|
11
12
|
export * from "./ensureError.js";
|
|
@@ -33,10 +34,13 @@ export * from "./pull.js";
|
|
|
33
34
|
export * from "./remove.js";
|
|
34
35
|
export * from "./removeCommonProperties.js";
|
|
35
36
|
export * from "./replace.js";
|
|
37
|
+
export * from "./replaceDeep.js";
|
|
36
38
|
export * from "./rethrow.js";
|
|
39
|
+
export * from "./round.js";
|
|
37
40
|
export * from "./safe.js";
|
|
38
41
|
export * from "./scale.js";
|
|
39
42
|
export * from "./seq.js";
|
|
43
|
+
export * from "./serialize.js";
|
|
40
44
|
export * from "./set.js";
|
|
41
45
|
export * from "./setImmutable.js";
|
|
42
46
|
export * from "./sortBy.js";
|
package/src/replace.ts
CHANGED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { replaceDeep } from "./replaceDeep";
|
|
2
|
+
|
|
3
|
+
describe("replaceDeep", () => {
|
|
4
|
+
it("should replace given value in a deep object", () => {
|
|
5
|
+
const source = [
|
|
6
|
+
99,
|
|
7
|
+
100,
|
|
8
|
+
{
|
|
9
|
+
favouriteBook: {
|
|
10
|
+
title: "The Ring of The Lord",
|
|
11
|
+
price: 100,
|
|
12
|
+
},
|
|
13
|
+
otherBooks: [
|
|
14
|
+
{
|
|
15
|
+
title: "Parry Hotter",
|
|
16
|
+
price: 50,
|
|
17
|
+
tag: "100",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
title: "The Hobbyte 100",
|
|
21
|
+
price: [100],
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
must(replaceDeep(source, 100, 200)).eql([
|
|
28
|
+
99,
|
|
29
|
+
200,
|
|
30
|
+
{
|
|
31
|
+
favouriteBook: {
|
|
32
|
+
title: "The Ring of The Lord",
|
|
33
|
+
price: 200,
|
|
34
|
+
},
|
|
35
|
+
otherBooks: [
|
|
36
|
+
{
|
|
37
|
+
title: "Parry Hotter",
|
|
38
|
+
price: 50,
|
|
39
|
+
tag: "100",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
title: "The Hobbyte 100",
|
|
43
|
+
price: [200],
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should leave primitives as-is unless they equal to the search value", () => {
|
|
51
|
+
must(replaceDeep(100, 200, 300)).equal(100);
|
|
52
|
+
must(replaceDeep(200, 200, 300)).equal(300);
|
|
53
|
+
must(replaceDeep("100", 200, 300)).equal("100");
|
|
54
|
+
// ESLINT BUG:
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
|
|
56
|
+
must(replaceDeep(undefined, 200, 300)).equal(undefined);
|
|
57
|
+
must(replaceDeep(null, 200, 300)).equal(null);
|
|
58
|
+
must(replaceDeep(true, 200, 300)).equal(true);
|
|
59
|
+
must(replaceDeep(666n, 200, 300)).equal(666n);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should work with nans", async () => {
|
|
63
|
+
must(replaceDeep({
|
|
64
|
+
a: NaN,
|
|
65
|
+
b: 123,
|
|
66
|
+
}, NaN, 300)).eql({
|
|
67
|
+
a: 300,
|
|
68
|
+
b: 123,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replaces all occurrences of `search` with `value` in `source` object/array. Comparison is done with `Object.is`.
|
|
3
|
+
* If `source` is exactly the `search` a `value` will be returned. It does not do a substring replacements.
|
|
4
|
+
*
|
|
5
|
+
* It mutates the `source` object/array!
|
|
6
|
+
*
|
|
7
|
+
* TypeScript users: This is way too dynamic to type properly, therefore, typing assumes the most basic form of
|
|
8
|
+
* replacement where search and value are of the same type. If that's not the case for you - you'll have to typecast.
|
|
9
|
+
*
|
|
10
|
+
* @param source - source object/array/value
|
|
11
|
+
* @param search - value to search for
|
|
12
|
+
* @param value - value to replace with
|
|
13
|
+
*/
|
|
14
|
+
const replaceDeep = <T>(source: T, search: unknown, value: unknown): T => {
|
|
15
|
+
if (Object.is(source, search)) {
|
|
16
|
+
return value as T;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (source == null) {
|
|
20
|
+
return source;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (typeof source === "object") {
|
|
24
|
+
if (Array.isArray(source)) {
|
|
25
|
+
for (let i = 0; i < source.length; i++) {
|
|
26
|
+
// eslint-disable-next-line no-param-reassign,@typescript-eslint/no-unsafe-assignment
|
|
27
|
+
source[i] = replaceDeep(source[i], search, value);
|
|
28
|
+
}
|
|
29
|
+
return source;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return Object.keys(source).reduce<Record<string, unknown>>((acc, key) => {
|
|
33
|
+
// eslint-disable-next-line no-param-reassign
|
|
34
|
+
acc[key] = replaceDeep((source as Record<string, unknown>)[key], search, value);
|
|
35
|
+
return acc;
|
|
36
|
+
}, {}) as T;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return source;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export {
|
|
43
|
+
replaceDeep,
|
|
44
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { round } from "./round";
|
|
2
|
+
|
|
3
|
+
describe("round", () => {
|
|
4
|
+
it("rounds the values without precision", async () => {
|
|
5
|
+
round(1.23).must.equal(1);
|
|
6
|
+
round(1.55).must.equal(2);
|
|
7
|
+
round(1.5).must.equal(2);
|
|
8
|
+
round(1.49999).must.equal(1);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("rounds the values with precision", async () => {
|
|
12
|
+
round(1.23, 1).must.equal(1.2);
|
|
13
|
+
round(1.55, 1).must.equal(1.6);
|
|
14
|
+
round(1.5, 1).must.equal(1.5);
|
|
15
|
+
round(1.49999, 1).must.equal(1.5);
|
|
16
|
+
|
|
17
|
+
round(1.5, 2).must.equal(1.5);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("rounds the tricky values", async () => {
|
|
21
|
+
round(0.1 + 0.2, 1).must.equal(0.3);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("rounds with higher precision", async () => {
|
|
25
|
+
round(1.2, 6).must.equal(1.2);
|
|
26
|
+
});
|
|
27
|
+
});
|
package/src/round.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rounds a number to a given precision
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* round(1.23) // 1
|
|
6
|
+
* round(1.55) // 2
|
|
7
|
+
* round(1.333, 2) // 1.33
|
|
8
|
+
* round(1.2345, 3) // 1.234
|
|
9
|
+
*
|
|
10
|
+
* @param value - value to round
|
|
11
|
+
* @param precision - precision to round to
|
|
12
|
+
*/
|
|
13
|
+
const round = (value: number, precision?: number) => {
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
|
15
|
+
const multiplier = Math.pow(10, precision || 0);
|
|
16
|
+
return Math.round(value * multiplier) / multiplier;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
round,
|
|
21
|
+
};
|
package/src/safe.ts
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { CustomSerializers } from "./serialize";
|
|
2
|
+
|
|
3
|
+
import { serialize } from "./serialize";
|
|
4
|
+
|
|
5
|
+
interface Test {
|
|
6
|
+
a: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class Person {
|
|
10
|
+
public name: string;
|
|
11
|
+
|
|
12
|
+
public constructor(name: string) {
|
|
13
|
+
this.name = name;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("serialize", () => {
|
|
18
|
+
it("can serialize primitives", async () => {
|
|
19
|
+
must(serialize("test")).equal(`"s:test"`);
|
|
20
|
+
must(serialize(123)).equal(`"n:123"`);
|
|
21
|
+
must(serialize(123n)).equal(`"i:123"`);
|
|
22
|
+
must(serialize(undefined)).equal(`"u:"`);
|
|
23
|
+
must(serialize(true)).equal(`"b:1"`);
|
|
24
|
+
must(serialize(false)).equal(`"b:"`);
|
|
25
|
+
must(serialize(null)).equal(`"l:"`);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("can serialize arrays", async () => {
|
|
29
|
+
must(serialize(["a", "b", "c"])).equal(`["s:a","s:b","s:c"]`);
|
|
30
|
+
must(serialize([1, 2n, undefined, true, false, null])).equal(`["n:1","i:2","u:","b:1","b:","l:"]`);
|
|
31
|
+
|
|
32
|
+
// deep arrays:
|
|
33
|
+
must(serialize([1, [2, [3, [4, [5]]]]])).equal(`["n:1",["n:2",["n:3",["n:4",["n:5"]]]]]`);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("can serialize objects", async () => {
|
|
37
|
+
must(serialize({ a: "a", b: "b", c: "c" })).equal(`{"a":"s:a","b":"s:b","c":"s:c"}`);
|
|
38
|
+
|
|
39
|
+
const a: Test = { a: "a" };
|
|
40
|
+
must(serialize(a)).equal(`{"a":"s:a"}`);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("supports custom serializers", async () => {
|
|
44
|
+
const customSerializers: CustomSerializers = {
|
|
45
|
+
p: (value) => {
|
|
46
|
+
if (value instanceof Person) {
|
|
47
|
+
return value.name;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
must(serialize(new Person("John"), customSerializers)).equal(`"p:John"`);
|
|
54
|
+
must(serialize({ a: new Person("John") }, customSerializers)).equal(`{"a":"p:John"}`);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("throws on unknown data type", () => {
|
|
58
|
+
const x = Symbol("x");
|
|
59
|
+
|
|
60
|
+
must(() => serialize(x)).throw("Unsupported data type: symbol");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("allows custom serializers to support primitives", () => {
|
|
64
|
+
const customSerializers: CustomSerializers = {
|
|
65
|
+
sym: (value) => {
|
|
66
|
+
if (typeof value === "symbol") {
|
|
67
|
+
return String(value);
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
must(serialize(Symbol("x"), customSerializers)).equal(`"sym:Symbol(x)"`);
|
|
74
|
+
must(serialize({ a: Symbol("x") }, customSerializers)).equal(`{"a":"sym:Symbol(x)"}`);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("returns the same for any order of props", () => {
|
|
78
|
+
const a = { a: 1, b: 2 };
|
|
79
|
+
const b = { b: 2, a: 1 };
|
|
80
|
+
|
|
81
|
+
must(serialize(a)).equal(serialize(b));
|
|
82
|
+
});
|
|
83
|
+
});
|
package/src/serialize.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { sortProps } from "./sortProps.js";
|
|
2
|
+
|
|
3
|
+
type CustomSerializers = {
|
|
4
|
+
[key: string]: (data: unknown) => (string | null);
|
|
5
|
+
s?: never;
|
|
6
|
+
n?: never;
|
|
7
|
+
u?: never;
|
|
8
|
+
l?: never;
|
|
9
|
+
b?: never;
|
|
10
|
+
i?: never;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type Options = {
|
|
14
|
+
sortProps?: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Serializes the data into a string. Think of it as a JSON.stringify on steroids.
|
|
19
|
+
* In opposite to JSON.stringify it supports serializing undefined.
|
|
20
|
+
*
|
|
21
|
+
* It also supports custom serializers, which can be used to serialize custom data types. Each value has a prefix which
|
|
22
|
+
* specifies the type of the value, custom serializers is a map of these prefixes to functions which can serialize the
|
|
23
|
+
* value into a string. IMPORTANT: Because this is using JSON.serialize under the hood if a value to serialize includes
|
|
24
|
+
* `toJSON` it won't trigger custom serializer but will be serialized as string. `Date` class defines `toJSON` method!
|
|
25
|
+
*
|
|
26
|
+
* The extra aim of this function is to produce the same output for "the same" data, regardless of the order of the keys
|
|
27
|
+
* (which is not guaranteed by JS spec, but in practice it is guaranteed by current implementations of all JS engines),
|
|
28
|
+
* so it sorts the keys and when serializing data by default, you can opt-out of this behavior by passing
|
|
29
|
+
* `{ sortProps: false }` as the third argument.
|
|
30
|
+
*
|
|
31
|
+
* @param data - the data to serialize
|
|
32
|
+
* @param customSerializers - an object with custom serializers
|
|
33
|
+
* @param options - options
|
|
34
|
+
*/
|
|
35
|
+
const serialize = (data: unknown, customSerializers?: CustomSerializers, options?: Options) => { // eslint-disable-line max-lines-per-function,max-len
|
|
36
|
+
const replacer = (_key: string, value: unknown) => { // eslint-disable-line max-statements
|
|
37
|
+
if (typeof value === "string") {
|
|
38
|
+
return `s:${value}`;
|
|
39
|
+
}
|
|
40
|
+
if (typeof value === "number") {
|
|
41
|
+
return `n:${value}`;
|
|
42
|
+
}
|
|
43
|
+
if (typeof value === "bigint") {
|
|
44
|
+
return `i:${value}`;
|
|
45
|
+
}
|
|
46
|
+
if (typeof value === "undefined") {
|
|
47
|
+
return "u:";
|
|
48
|
+
}
|
|
49
|
+
if (typeof value === "boolean") {
|
|
50
|
+
return "b:" + (value ? "1" : "");
|
|
51
|
+
}
|
|
52
|
+
if (value === null) {
|
|
53
|
+
return "l:";
|
|
54
|
+
}
|
|
55
|
+
const serializerKeys = Object.keys(customSerializers ?? {});
|
|
56
|
+
for (const key of serializerKeys) {
|
|
57
|
+
const serialized = customSerializers![key]!(value);
|
|
58
|
+
if (typeof serialized === "string") {
|
|
59
|
+
return `${key}:${serialized}`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (typeof value === "object") {
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
throw new Error(`Unsupported data type: ${typeof value}`);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
if (
|
|
70
|
+
data == null
|
|
71
|
+
|| typeof data === "string"
|
|
72
|
+
|| typeof data === "number"
|
|
73
|
+
|| typeof data === "bigint"
|
|
74
|
+
|| typeof data === "undefined"
|
|
75
|
+
|| typeof data === "boolean"
|
|
76
|
+
|| Array.isArray(data)
|
|
77
|
+
) {
|
|
78
|
+
return JSON.stringify(data, replacer);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const serializerKeysGlobal = Object.keys(customSerializers ?? {});
|
|
82
|
+
for (const key of serializerKeysGlobal) {
|
|
83
|
+
const serialized = customSerializers![key]!(data);
|
|
84
|
+
if (typeof serialized === "string") {
|
|
85
|
+
return `"${key}:${serialized}"`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (typeof data === "object") {
|
|
90
|
+
return JSON.stringify(
|
|
91
|
+
options?.sortProps === false
|
|
92
|
+
? data
|
|
93
|
+
: sortProps(data as Record<string, unknown>),
|
|
94
|
+
replacer,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
throw new Error(`Unsupported data type: ${typeof data}`);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export type { CustomSerializers };
|
|
102
|
+
export { serialize };
|
package/src/sortProps.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sorts the properties of an object alphabetically, ascending or descending.
|
|
3
|
-
*
|
|
3
|
+
* It does not mutate the original object.
|
|
4
|
+
*
|
|
5
|
+
* REMEMBER: In theory, JS engines do not guarantee the order of object properties.
|
|
6
|
+
* In practice most of the popular engines do.
|
|
4
7
|
* @param object - source object
|
|
5
8
|
* @param asc - sort ascending?
|
|
6
9
|
* @example sortProps({ b: 2, a: 1, z: 26 }) // { a: 1, b: 2, z: 26 }
|