@ethanblaisalarms/utils 1.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/src/index.ts ADDED
@@ -0,0 +1,286 @@
1
+ //=======================================\\
2
+ // EBA UTILITIES 1.0.0 \\
3
+ // index.ts \\
4
+ // Copyright (c) EthanBlaisAlarms 2026 \\
5
+ //=======================================\\
6
+
7
+
8
+ //========================
9
+ // TYPES and INTERFACES
10
+ //========================
11
+ export type TRecord<T = unknown> = Record<string, T>;
12
+ export type TNumRecord<T = unknown> = Record<number, T>;
13
+ export type TCondition = (iteration?:number)=>boolean;
14
+
15
+
16
+ //====================
17
+ // OBJECT UTILITIES
18
+ //====================
19
+ export function merge<T extends object>(
20
+ target:T,
21
+ ...sources:object[]
22
+ ):T {
23
+ // Ignored node names.
24
+ // Prevents accidentally overwriting prototypes and constructors.
25
+ const blocklist = new Set(["__proto__", "prototype", "constructor"]);
26
+ const seen = new WeakSet();
27
+
28
+ const innerMerge = function(
29
+ target:TRecord,
30
+ source:TRecord,
31
+ ):void {
32
+ if (seen.has(source)) return;
33
+ seen.add(source);
34
+
35
+ for (const i of Object.keys(source)) {
36
+ // Skip ignored node names.
37
+ // Skip functions.
38
+ if (blocklist.has(i)) continue;
39
+ if (typeof source[i] === "function") continue;
40
+
41
+ // Merge Arrays
42
+ if (Array.isArray(source[i]) && Array.isArray(target[i])) {
43
+ target[i] = target[i].concat(source[i]);
44
+ continue;
45
+ }
46
+
47
+ // Merge Non-Recursive
48
+ if (!checkPlain(target[i]) || !checkPlain(source[i])) {
49
+ target[i] = source[i];
50
+ continue;
51
+ }
52
+
53
+ // Merge Recursive
54
+ innerMerge(target[i], source[i]);
55
+ }
56
+ seen.delete(source);
57
+ };
58
+
59
+ for (const source of sources) innerMerge(target as TRecord, source as TRecord);
60
+ return target;
61
+ }
62
+ export function decommentJSON(data:string):string {
63
+ const linebreaks:string[] = ["\n", "\r"];
64
+ let inside = false;
65
+ let escape = false;
66
+ let single = false;
67
+ let multi = false;
68
+ let result = "";
69
+
70
+ for (let i = 0; i < data.length; i++) {
71
+ const char:string = data[i];
72
+ const nextChar:string = data[i+1] ?? "";
73
+ const nextChars:string = char + nextChar;
74
+
75
+ if (inside) {
76
+ if (escape) escape = false;
77
+ else if (char == "\\") escape = true;
78
+ else if (char == "\"") inside = false;
79
+ }
80
+ else if (single) {
81
+ if (linebreaks.includes(char)) single = false;
82
+ else continue;
83
+ }
84
+ else if (multi) {
85
+ if (nextChars == "*/") {
86
+ multi = false;
87
+ i++;
88
+ }
89
+ continue;
90
+ }
91
+ else if (nextChars == "//") {
92
+ single = true;
93
+ i++;
94
+ continue;
95
+ }
96
+ else if (nextChars == "/*") {
97
+ multi = true;
98
+ i++;
99
+ continue;
100
+ }
101
+ else if (char == "\"") inside = true;
102
+ result += char;
103
+ }
104
+
105
+ return result;
106
+ }
107
+ export function checkPlain(data:unknown):data is TRecord {
108
+ return Object.prototype.toString.call(data) === "[object Object]";
109
+ }
110
+
111
+
112
+ //===================
113
+ // ARRAY UTILITIES
114
+ //===================
115
+ export function pull<T>(
116
+ target:T[],
117
+ ...elements:T[]
118
+ ):T[] {
119
+ for (const element of elements) {
120
+ const i:number = target.indexOf(element);
121
+ if (i >= 0) target.splice(i, 1);
122
+ }
123
+ return target;
124
+ }
125
+ export function pullAll<T>(
126
+ target:T[],
127
+ ...elements:T[]
128
+ ):T[] {
129
+ for (const element of elements) {
130
+ let i;
131
+ while ((i = target.indexOf(element)) !== -1) target.splice(i, 1);
132
+ }
133
+ return target;
134
+ }
135
+ export function replace<T>(
136
+ target:T[],
137
+ element:T,
138
+ ...replace:T[]
139
+ ):T[] {
140
+ const i:number = target.indexOf(element);
141
+ if (i >= 0) target.splice(i, 1, ...replace);
142
+ return target;
143
+ }
144
+ export function replaceAll<T>(
145
+ target:T[],
146
+ element:T,
147
+ ...replace:T[]
148
+ ):T[] {
149
+ let i;
150
+ while ((i = target.indexOf(element)) !== -1) target.splice(i, 1, ...replace);
151
+ return target;
152
+ }
153
+ export function objectify<T>(source:T[]):TNumRecord<T> {
154
+ const output:TNumRecord<T> = {};
155
+ for (const i of source.keys()) output[i] = source[i];
156
+ return output;
157
+ }
158
+
159
+
160
+ //=============
161
+ // STRINGIFY
162
+ //=============
163
+ export function stringify(data:unknown):string {
164
+ if (typeof data === "bigint") return `${data}n`;
165
+ if (typeof data !== "object") return String(data);
166
+ if (data === null) return "null";
167
+
168
+ if (data instanceof Set) return stringify(Array.from(data));
169
+ if (data instanceof Map) return stringify(Array.from(data.entries()));
170
+
171
+ if (Array.isArray(data)) return stringifyJSON(data);
172
+
173
+ if (
174
+ data.toString !== Object.prototype.toString
175
+ && typeof data.toString === "function"
176
+ ) return data.toString();
177
+
178
+ return stringifyJSON({...data});
179
+ }
180
+ export function stringifyArray(data:unknown[]):string[] {
181
+ return data.map(stringify);
182
+ }
183
+ export function stringifyJSON(
184
+ data:object,
185
+ separator:string = " ",
186
+ ignore:string[] = [],
187
+ ):string {
188
+ const seen:WeakSet<object> = new WeakSet();
189
+ const paths:WeakMap<object, string> = new WeakMap();
190
+ const ignoreSet = new Set(ignore);
191
+
192
+ const replacer = function(this:any, key:string, value:unknown):unknown {
193
+ const parent = paths.get(this) ?? "";
194
+ const current = key
195
+ ? (parent ? parent + "." : "") + key
196
+ : "";
197
+
198
+ if (ignoreSet.has(current)) return undefined;
199
+
200
+ if (typeof value == "bigint") return `${value}n`;
201
+
202
+ if (typeof value == "object" && value) {
203
+ if (seen.has(value)) return `[Circular: ${paths.get(value) || "~"}]`;
204
+ seen.add(value);
205
+ if (current != "") paths.set(value, current);
206
+ }
207
+
208
+ if (value instanceof Set) return Array.from(value);
209
+ if (value instanceof Map) return Array.from(value.entries());
210
+
211
+ return value;
212
+ };
213
+ paths.set(data, "");
214
+ return JSON.stringify(data, replacer, separator);
215
+ }
216
+
217
+
218
+ //=================
219
+ // RANDOMIZATION
220
+ //=================
221
+ export function rand(
222
+ base?:number,
223
+ maxInput?:number,
224
+ ):number;
225
+ export function rand<T>(
226
+ base:T[],
227
+ ):T|null;
228
+ export function rand(
229
+ base:TRecord,
230
+ ):string|null;
231
+ export function rand(
232
+ base?:number|unknown[]|TRecord,
233
+ max?:number,
234
+ ):unknown {
235
+ if (typeof base == "number") {
236
+ const hasMax = typeof max == "number";
237
+ const setBase = hasMax ? Math.min(base, max) : 0;
238
+ const setMax = hasMax ? Math.max(base, max) : base;
239
+ return Math.floor(Math.random() * (setMax - setBase + 1)) + setBase;
240
+ }
241
+
242
+ if (!base) return Math.random();
243
+
244
+ if (Array.isArray(base)) {
245
+ if (base.length < 1) return null;
246
+ return base[rand(base.length - 1)];
247
+ }
248
+
249
+ const keys = Object.keys(base);
250
+ if (keys.length < 1) return null;
251
+ return rand(keys);
252
+ }
253
+ export function randString(
254
+ len=16,
255
+ charset:string|string[]="0123456789ABCDEF",
256
+ ):string {
257
+ if (typeof charset === "string") charset = charset.split("");
258
+ if (charset.length < 1) return "";
259
+ let output = "";
260
+ while (len > 0) {
261
+ output += rand(charset);
262
+ len--;
263
+ }
264
+ return output;
265
+ }
266
+
267
+
268
+ //===================
269
+ // ASYNC UTILITIES
270
+ //===================
271
+ export async function sleep(ms:number):Promise<void> {
272
+ return new Promise((resolve) => {
273
+ setTimeout(resolve, ms);
274
+ });
275
+ }
276
+ export async function until(
277
+ condition:TCondition,
278
+ interval=100,
279
+ attempts:number|null=null,
280
+ ):Promise<number> {
281
+ for (let i = 0; (i < (attempts ?? i+1)); i++) {
282
+ if (condition(i+1)) return i+1;
283
+ await sleep(interval);
284
+ }
285
+ throw new RangeError("Maximum number of attempts reached.");
286
+ }
package/src/test.ts ADDED
@@ -0,0 +1,366 @@
1
+ //=======================================\\
2
+ // EBA UTILITIES 1.0.0 \\
3
+ // test.ts \\
4
+ // Copyright (c) EthanBlaisAlarms 2026 \\
5
+ //=======================================\\
6
+ import * as Assert from "node:assert";
7
+ import {
8
+ checkPlain,
9
+ decommentJSON,
10
+ merge,
11
+ objectify,
12
+ pull,
13
+ pullAll,
14
+ replace,
15
+ replaceAll,
16
+ stringify,
17
+ stringifyArray,
18
+ stringifyJSON,
19
+ TRecord,
20
+ } from "./index.js";
21
+
22
+
23
+ console.log("Starting tests...");
24
+
25
+
26
+ //====================
27
+ // OBJECT UTILITIES
28
+ //====================
29
+ console.log("\n\nCATEGORY: Object Utilities");
30
+
31
+ //-----------------
32
+ // Merge objects
33
+ //-----------------
34
+ console.log("\nFUNCTION: merge()");
35
+
36
+ console.log("TEST 1: General functionality and object mutation");
37
+ const mrg11 = {
38
+ A: 1,
39
+ B: 2,
40
+ C: 3,
41
+ };
42
+ const mrg12 = {
43
+ A: 10,
44
+ D: 40,
45
+ E: 50,
46
+ };
47
+ const mrg13 = {
48
+ A: 100,
49
+ E: 500,
50
+ F: 600,
51
+ };
52
+ merge(mrg11, mrg12, mrg13);
53
+ Assert.deepStrictEqual(mrg11, {
54
+ A: 100,
55
+ B: 2,
56
+ C: 3,
57
+ D: 40,
58
+ E: 500,
59
+ F: 600,
60
+ });
61
+
62
+ console.log("TEST 2: Exclusions and array concatenation");
63
+ const mrg21 = {
64
+ A: 100,
65
+ B: new Date(-1000),
66
+ C: [0, 1, 2],
67
+ prototype: "I'm a prototype!",
68
+ };
69
+ const mrg22 = {
70
+ A: ():true => {
71
+ return true;
72
+ },
73
+ B: new Date(1000),
74
+ C: [3, 4, 5],
75
+ prototype: "I'm overwriting a prototype!",
76
+ [Symbol("Symbol")]: "I'm a symbol!",
77
+ };
78
+ merge(mrg21, mrg22);
79
+ Assert.deepStrictEqual(mrg21, {
80
+ A: 100,
81
+ B: mrg22.B,
82
+ C: [0, 1, 2, 3, 4, 5],
83
+ prototype: "I'm a prototype!",
84
+ });
85
+
86
+
87
+ //-------------------
88
+ // De-Comment JSON
89
+ //-------------------
90
+ console.log("\nFUNCTION: decommentJSON()");
91
+
92
+ console.log("TEST 1: Everything");
93
+ const dcm1 = `{
94
+ // "A" holds the number of apples.
95
+ "A": 10,
96
+ /* "B" holds the number of bananas.
97
+ Must be greater than 0. */
98
+ "B": 20,
99
+ /*
100
+ "C" holds the number of carrots.
101
+ Must be:
102
+ - An even number
103
+ - Divisible by 3
104
+ - Not divisible by 60
105
+ - The digits must no add up to 6
106
+ */
107
+ "C": 30,
108
+ "D": "//This is not a JSON comment"
109
+ }`;
110
+ const dcm2 = JSON.parse(decommentJSON(dcm1));
111
+ Assert.deepStrictEqual(dcm2, {
112
+ A: 10,
113
+ B: 20,
114
+ C: 30,
115
+ D: "//This is not a JSON comment",
116
+ });
117
+
118
+
119
+ //---------------------------
120
+ // Check for plain objects
121
+ //---------------------------
122
+ console.log("\nFUNCTION: checkPlain()");
123
+
124
+ console.log("TEST 1: Everything");
125
+ const chk1 = [
126
+ checkPlain(new Date()),
127
+ checkPlain({}),
128
+ ];
129
+ Assert.deepStrictEqual(chk1, [false, true]);
130
+
131
+
132
+ //===================
133
+ // ARRAY UTILITIES
134
+ //===================
135
+ console.log("\n\nCATEGORY: Array Utilities");
136
+
137
+ //-------------------
138
+ // Pull from array
139
+ //-------------------
140
+ console.log("\nFUNCTION: pull()");
141
+
142
+ console.log("TEST 1: Everything");
143
+ const pul1 = [10, 20, 30, 40, 50, 20];
144
+ pull(pul1, 20, 50);
145
+ Assert.deepStrictEqual(pul1, [10, 30, 40, 20]);
146
+
147
+
148
+ //------------
149
+ // Pull all
150
+ //------------
151
+ console.log("\nFUNCTION: pullAll()");
152
+
153
+ console.log("TEST 1: Everything");
154
+ const pul2 = [10, 20, 30, 40, 50, 20];
155
+ pullAll(pul2, 20, 50);
156
+ Assert.deepStrictEqual(pul2, [10, 30, 40]);
157
+
158
+
159
+ //--------------------
160
+ // Pull and replace
161
+ //--------------------
162
+ console.log("\nFUNCTION: replace()");
163
+
164
+ console.log("TEST 1: Everything");
165
+ const pul3 = [10, 20, 30, 40, 50, 20];
166
+ replace(pul3, 20, 60, 70);
167
+ Assert.deepStrictEqual(pul3, [10, 60, 70, 30, 40, 50, 20]);
168
+
169
+
170
+ //------------------------
171
+ // Pull and replace all
172
+ //------------------------
173
+ console.log("\nFUNCTION: replaceAll()");
174
+
175
+ console.log("TEST 1: Everything");
176
+ const pul4 = [10, 20, 30, 40, 50, 20];
177
+ replaceAll(pul4, 20, 60, 70);
178
+ Assert.deepStrictEqual(pul4, [10, 60, 70, 30, 40, 50, 60, 70]);
179
+
180
+
181
+ //--------------------
182
+ // Objectify arrays
183
+ //--------------------
184
+ console.log("\nFUNCTION: objectify()");
185
+
186
+ console.log("TEST 1: Everything");
187
+ const obj1 = ["zero", "one", "two", "three"];
188
+ const obj2 = objectify(obj1);
189
+ Assert.deepStrictEqual(obj2, {
190
+ 0: "zero",
191
+ 1: "one",
192
+ 2: "two",
193
+ 3: "three",
194
+ });
195
+
196
+
197
+ //====================
198
+ // STRINGIFY VALUES
199
+ //====================
200
+ console.log("\n\nCATEGORY: Stringify Values");
201
+
202
+ //---------------------
203
+ // General stringify
204
+ //---------------------
205
+ console.log("\nFUNCTION: stringify()");
206
+
207
+ console.log("TEST 1: General functionality with primitives");
208
+ const str1 = [
209
+ stringify("hello"),
210
+ stringify(100),
211
+ stringify(500n),
212
+ stringify(false),
213
+ stringify(Symbol("str")),
214
+ stringify(null),
215
+ stringify(undefined),
216
+ ];
217
+ Assert.deepStrictEqual(str1, [
218
+ "hello",
219
+ "100",
220
+ "500n",
221
+ "false",
222
+ "Symbol(str)",
223
+ "null",
224
+ "undefined",
225
+ ]);
226
+
227
+ console.log("TEST 2: Plain objects");
228
+ const str2 = [
229
+ stringify({}),
230
+ stringify({
231
+ A: 100,
232
+ B: {
233
+ C: 500,
234
+ },
235
+ }),
236
+ stringify({
237
+ A: 100,
238
+ toString: ():string => {
239
+ return "A is 100";
240
+ },
241
+ }),
242
+ stringify({
243
+ A: 100,
244
+ toString: "Not callable",
245
+ }),
246
+ stringify([1, 2, 3, 4, 5]),
247
+ ];
248
+ const str3 = `{
249
+ "A": 100,
250
+ "B": {
251
+ "C": 500
252
+ }
253
+ }`;
254
+ const str4 = `{
255
+ "A": 100,
256
+ "toString": "Not callable"
257
+ }`;
258
+ const str5 = `[
259
+ 1,
260
+ 2,
261
+ 3,
262
+ 4,
263
+ 5
264
+ ]`;
265
+ Assert.deepStrictEqual(str2, [
266
+ "{}",
267
+ str3,
268
+ "A is 100",
269
+ str4,
270
+ str5,
271
+ ]);
272
+
273
+ console.log("TEST 3: Object instances");
274
+ const str6 = new Date(0);
275
+ const str7 = [
276
+ stringify(str6),
277
+ stringify(new Error("Something went wrong!")),
278
+ stringify((new Set()).add("One")),
279
+ stringify((new Map()).set("1", "One")),
280
+ ];
281
+ const str8 = `[
282
+ "One"
283
+ ]`;
284
+ const str9 = `[
285
+ [
286
+ "1",
287
+ "One"
288
+ ]
289
+ ]`;
290
+ Assert.deepStrictEqual(str7, [
291
+ str6.toString(),
292
+ "Error: Something went wrong!",
293
+ str8,
294
+ str9,
295
+ ]);
296
+
297
+
298
+ //------------------
299
+ // Stringify JSON
300
+ //------------------
301
+ console.log("\nFUNCTION: stringifyJSON()");
302
+
303
+ console.log("TEST 1: Everything");
304
+ const jsn1 = {
305
+ A: 1,
306
+ B: 2,
307
+ C: (new Set()).add("Ten"),
308
+ };
309
+ const jsn2 = {
310
+ A: 1,
311
+ B: 2,
312
+ C: {
313
+ A: 100n,
314
+ B: 2,
315
+ },
316
+ };
317
+ const jsn3:TRecord = {
318
+ D: jsn1,
319
+ E: jsn1,
320
+ };
321
+ jsn3.F = jsn3;
322
+ const jsn4 = {
323
+ jsn1: stringifyJSON(jsn1),
324
+ jsn2: stringifyJSON(jsn2, ">", ["A", "C.B"]),
325
+ jsn3: stringifyJSON(jsn3),
326
+ };
327
+ const jsn5 = `{
328
+ "A": 1,
329
+ "B": 2,
330
+ "C": [
331
+ "Ten"
332
+ ]
333
+ }`;
334
+ const jsn6 = `{
335
+ >"B": 2,
336
+ >"C": {
337
+ >>"A": "100n"
338
+ >}
339
+ }`;
340
+ const jsn7 = `{
341
+ "D": {
342
+ "A": 1,
343
+ "B": 2,
344
+ "C": [
345
+ "Ten"
346
+ ]
347
+ },
348
+ "E": "[Circular: D]",
349
+ "F": "[Circular: ~]"
350
+ }`;
351
+ Assert.deepStrictEqual(jsn4, {
352
+ jsn1: jsn5,
353
+ jsn2: jsn6,
354
+ jsn3: jsn7,
355
+ });
356
+
357
+
358
+ //----------------------------
359
+ // Stringify array elements
360
+ //----------------------------
361
+ console.log("\nFUNCTION: stringifyArray()");
362
+
363
+ console.log("TEST 1: Everything");
364
+ const arr1 = [null, 20, {}];
365
+ const arr2 = stringifyArray(arr1);
366
+ Assert.deepStrictEqual(arr2, ["null", "20", "{}"]);
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "declaration": true,
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "moduleResolution": "node",
9
+ "types": ["node"],
10
+ "strict": true
11
+ },
12
+ "include": ["src"]
13
+ }