@bitblit/ratchet-common 6.0.146-alpha → 6.0.148-alpha

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.
Files changed (140) hide show
  1. package/package.json +2 -1
  2. package/src/2d/line-2d.ts +6 -0
  3. package/src/2d/matrix-factory.ts +94 -0
  4. package/src/2d/plane-2d-type.ts +6 -0
  5. package/src/2d/plane-2d.ts +7 -0
  6. package/src/2d/point-2d.ts +4 -0
  7. package/src/2d/poly-line-2d.ts +5 -0
  8. package/src/2d/ratchet-2d.spec.ts +205 -0
  9. package/src/2d/ratchet-2d.ts +350 -0
  10. package/src/2d/transformation-matrix.ts +19 -0
  11. package/src/build/build-information.ts +8 -0
  12. package/src/build/ratchet-common-info.ts +19 -0
  13. package/src/histogram/histogram-entry.ts +4 -0
  14. package/src/histogram/histogram.spec.ts +25 -0
  15. package/src/histogram/histogram.ts +61 -0
  16. package/src/jwt/common-jwt-token.ts +17 -0
  17. package/src/jwt/expired-jwt-handling.ts +5 -0
  18. package/src/jwt/jwt-decode-only-ratchet.ts +26 -0
  19. package/src/jwt/jwt-payload-expiration-ratchet.ts +45 -0
  20. package/src/jwt/jwt-token-base.ts +14 -0
  21. package/src/lang/array-ratchet.spec.ts +79 -0
  22. package/src/lang/array-ratchet.ts +141 -0
  23. package/src/lang/base64-ratchet.spec.ts +48 -0
  24. package/src/lang/base64-ratchet.ts +247 -0
  25. package/src/lang/boolean-ratchet.spec.ts +95 -0
  26. package/src/lang/boolean-ratchet.ts +52 -0
  27. package/src/lang/composite-last-success-provider.spec.ts +31 -0
  28. package/src/lang/composite-last-success-provider.ts +30 -0
  29. package/src/lang/currency-ratchet.ts +29 -0
  30. package/src/lang/date-ratchet.spec.ts +27 -0
  31. package/src/lang/date-ratchet.ts +42 -0
  32. package/src/lang/duration-ratchet.spec.ts +47 -0
  33. package/src/lang/duration-ratchet.ts +77 -0
  34. package/src/lang/enum-ratchet.spec.ts +45 -0
  35. package/src/lang/enum-ratchet.ts +41 -0
  36. package/src/lang/error-handling-approach.ts +6 -0
  37. package/src/lang/error-ratchet.spec.ts +25 -0
  38. package/src/lang/error-ratchet.ts +70 -0
  39. package/src/lang/esm-ratchet.ts +81 -0
  40. package/src/lang/expiring-object.spec.ts +56 -0
  41. package/src/lang/expiring-object.ts +84 -0
  42. package/src/lang/geolocation-ratchet.spec.ts +177 -0
  43. package/src/lang/geolocation-ratchet.ts +341 -0
  44. package/src/lang/global-ratchet.spec.ts +17 -0
  45. package/src/lang/global-ratchet.ts +105 -0
  46. package/src/lang/key-value.ts +8 -0
  47. package/src/lang/last-success-provider.ts +4 -0
  48. package/src/lang/map-ratchet.spec.ts +113 -0
  49. package/src/lang/map-ratchet.ts +220 -0
  50. package/src/lang/no.spec.ts +9 -0
  51. package/src/lang/no.ts +7 -0
  52. package/src/lang/number-ratchet.spec.ts +154 -0
  53. package/src/lang/number-ratchet.ts +253 -0
  54. package/src/lang/parsed-url.ts +10 -0
  55. package/src/lang/promise-ratchet.spec.ts +104 -0
  56. package/src/lang/promise-ratchet.ts +196 -0
  57. package/src/lang/range.ts +4 -0
  58. package/src/lang/require-ratchet.spec.ts +85 -0
  59. package/src/lang/require-ratchet.ts +68 -0
  60. package/src/lang/simple-arg-ratchet.spec.ts +13 -0
  61. package/src/lang/simple-arg-ratchet.ts +47 -0
  62. package/src/lang/simple-encryption-ratchet.ts +88 -0
  63. package/src/lang/sort-ratchet.spec.ts +58 -0
  64. package/src/lang/sort-ratchet.ts +50 -0
  65. package/src/lang/stop-watch.spec.ts +53 -0
  66. package/src/lang/stop-watch.ts +202 -0
  67. package/src/lang/string-ratchet.spec.ts +226 -0
  68. package/src/lang/string-ratchet.ts +676 -0
  69. package/src/lang/time-zone-ratchet.spec.ts +51 -0
  70. package/src/lang/time-zone-ratchet.ts +148 -0
  71. package/src/lang/timeout-token.spec.ts +12 -0
  72. package/src/lang/timeout-token.ts +21 -0
  73. package/src/lang/uint-8-array-ratchet.spec.ts +22 -0
  74. package/src/lang/uint-8-array-ratchet.ts +48 -0
  75. package/src/lang/web-stream-ratchet.spec.ts +12 -0
  76. package/src/lang/web-stream-ratchet.ts +96 -0
  77. package/src/logger/classic-single-line-log-message-formatter.ts +19 -0
  78. package/src/logger/log-message-builder.ts +60 -0
  79. package/src/logger/log-message-format-type.ts +11 -0
  80. package/src/logger/log-message-formatter.ts +6 -0
  81. package/src/logger/log-message-processor.ts +6 -0
  82. package/src/logger/log-message.ts +9 -0
  83. package/src/logger/log-snapshot.ts +6 -0
  84. package/src/logger/logger-instance.ts +269 -0
  85. package/src/logger/logger-level-name.ts +11 -0
  86. package/src/logger/logger-meta.ts +7 -0
  87. package/src/logger/logger-options.ts +14 -0
  88. package/src/logger/logger-output-function.ts +10 -0
  89. package/src/logger/logger-ring-buffer.ts +89 -0
  90. package/src/logger/logger-util.spec.ts +11 -0
  91. package/src/logger/logger-util.ts +68 -0
  92. package/src/logger/logger.spec.ts +177 -0
  93. package/src/logger/logger.ts +213 -0
  94. package/src/logger/none-log-message-formatter.ts +10 -0
  95. package/src/logger/single-line-no-level-log-message-formatter.ts +18 -0
  96. package/src/logger/structured-json-log-message-formatter.ts +25 -0
  97. package/src/mail/archive-email-result.ts +8 -0
  98. package/src/mail/email-attachment.ts +23 -0
  99. package/src/mail/mail-sending-provider.ts +21 -0
  100. package/src/mail/mailer-config.ts +30 -0
  101. package/src/mail/mailer-like.ts +38 -0
  102. package/src/mail/mailer-util.ts +65 -0
  103. package/src/mail/mailer.spec.ts +120 -0
  104. package/src/mail/mailer.ts +214 -0
  105. package/src/mail/ready-to-send-email.ts +67 -0
  106. package/src/mail/resolved-ready-to-send-email.ts +17 -0
  107. package/src/mail/send-email-result.ts +16 -0
  108. package/src/mail/test-mail-sending-provider.ts +35 -0
  109. package/src/network/browser-local-ip-provider.spec.ts +23 -0
  110. package/src/network/browser-local-ip-provider.ts +26 -0
  111. package/src/network/fixed-local-ip-provider.ts +9 -0
  112. package/src/network/local-ip-provider.ts +4 -0
  113. package/src/network/network-ratchet.spec.ts +17 -0
  114. package/src/network/network-ratchet.ts +209 -0
  115. package/src/network/remote-file-tracker/backup-result.ts +6 -0
  116. package/src/network/remote-file-tracker/file-transfer-result-type.ts +5 -0
  117. package/src/network/remote-file-tracker/file-transfer-result.ts +9 -0
  118. package/src/network/remote-file-tracker/remote-file-tracker-options.ts +6 -0
  119. package/src/network/remote-file-tracker/remote-file-tracker-push-options.ts +4 -0
  120. package/src/network/remote-file-tracker/remote-file-tracker.ts +117 -0
  121. package/src/network/remote-file-tracker/remote-file-tracking-provider.ts +19 -0
  122. package/src/network/remote-file-tracker/remote-status-data-and-content.ts +6 -0
  123. package/src/network/remote-file-tracker/remote-status-data.ts +7 -0
  124. package/src/network/restful-api-http-error.spec.ts +13 -0
  125. package/src/network/restful-api-http-error.ts +173 -0
  126. package/src/template/ratchet-template-renderer.ts +8 -0
  127. package/src/third-party/google/google-recaptcha-ratchet.spec.ts +27 -0
  128. package/src/third-party/google/google-recaptcha-ratchet.ts +36 -0
  129. package/src/third-party/twilio/twilio-ratchet.ts +92 -0
  130. package/src/third-party/twilio/twilio-verify-ratchet.ts +83 -0
  131. package/src/transform/built-in-transforms.ts +214 -0
  132. package/src/transform/transform-ratchet.spec.ts +134 -0
  133. package/src/transform/transform-ratchet.ts +88 -0
  134. package/src/transform/transform-rule.ts +7 -0
  135. package/src/tx/transaction-configuration.ts +8 -0
  136. package/src/tx/transaction-final-state.ts +7 -0
  137. package/src/tx/transaction-ratchet.spec.ts +150 -0
  138. package/src/tx/transaction-ratchet.ts +98 -0
  139. package/src/tx/transaction-result.ts +10 -0
  140. package/src/tx/transaction-step.ts +5 -0
@@ -0,0 +1,676 @@
1
+ /*
2
+ Functions for working with strings
3
+ */
4
+
5
+ import { RequireRatchet } from './require-ratchet.js';
6
+
7
+ export class StringRatchet {
8
+ // % isn't technically reserved, but it is still a pain in the butt
9
+ public static readonly RFC_3986_RESERVED = [
10
+ '!',
11
+ '*',
12
+ "'",
13
+ '(',
14
+ ')',
15
+ ';',
16
+ ':',
17
+ '@',
18
+ '&',
19
+ '=',
20
+ '+',
21
+ '$',
22
+ ',',
23
+ '/',
24
+ '?',
25
+ '#',
26
+ '[',
27
+ ']',
28
+ '%',
29
+ ];
30
+
31
+ public static readonly WHITESPACE: string = ' \n\t';
32
+ public static readonly DIGITS: string = '0123456789';
33
+ public static readonly HEXITS: string = StringRatchet.DIGITS + 'ABCDEF';
34
+ public static readonly UPPER_CASE_LATIN: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
35
+ public static readonly LOWER_CASE_LATIN: string = 'abcdefghijklmnopqrstuvwxyz';
36
+ public static readonly CASE_INSENSITIVE_LATIN: string = StringRatchet.UPPER_CASE_LATIN + StringRatchet.LOWER_CASE_LATIN;
37
+
38
+ /**
39
+ * Checks if the given input string contains only characters present in the provided alphabet.
40
+ * @param input - The string to check.
41
+ * @param alphabet - The set of allowed characters.
42
+ * @returns True if the string contains only characters from the alphabet, otherwise false.
43
+ */
44
+ public static stringIsInGivenAlphabet(input: string, alphabet: string): boolean {
45
+ let rval = false;
46
+ if (input && alphabet) {
47
+ for (let i = 0; i < input.length && !rval; i++) {
48
+ rval = alphabet.includes(input.charAt(i));
49
+ }
50
+ }
51
+ return rval;
52
+ }
53
+
54
+ /**
55
+ * Converts a string to a Uint8Array using TextEncoder.
56
+ * @param val - The input string.
57
+ * @returns Uint8Array representing the input string, or null if input is falsy.
58
+ */
59
+ public static stringToUint8Array(val: string): Uint8Array<ArrayBuffer> {
60
+ return val ? new TextEncoder().encode(val) : null;
61
+ }
62
+
63
+ /**
64
+ * Decodes a Uint8Array into a string using TextDecoder.
65
+ * @param val - The input Uint8Array.
66
+ * @returns The decoded string, or null if input is falsy.
67
+ */
68
+ public static uint8ArrayToString(val: Uint8Array): string {
69
+ return val ? new TextDecoder().decode(val) : null;
70
+ }
71
+
72
+ /**
73
+ * Attempts to parse a string as JSON.
74
+ *
75
+ * Really only useful if you wanna swallow the exception when something is
76
+ * not valid JSON (or at least not parseable as JSON - the spec says 'true'
77
+ * or '2' are not technically valid JSON strings)
78
+ *
79
+ * @param val - The input string.
80
+ * @returns The parsed JSON object, or null if parsing fails.
81
+ */
82
+ public static attemptJsonParse(val: string): any {
83
+ let rval: any = null;
84
+ if (StringRatchet.trimToNull(val)) {
85
+ try {
86
+ rval = JSON.parse(val);
87
+ } catch {
88
+ //(err) {
89
+ rval = null;
90
+ }
91
+ }
92
+ return rval;
93
+ }
94
+
95
+ /**
96
+ * Checks if a string can be parsed as JSON.
97
+ * @param val - The input string.
98
+ * @returns True if parsing succeeds, otherwise false.
99
+ */
100
+ public static canParseAsJson(val: string): boolean {
101
+ return !!StringRatchet.attemptJsonParse(val);
102
+ }
103
+
104
+ /**
105
+ * Determines if all characters in the string are unique.
106
+ * @param input - The input string.
107
+ * @returns True if all characters are unique, false otherwise.
108
+ */
109
+ public static allUnique(input: string): boolean {
110
+ let rval = true;
111
+ if (input) {
112
+ const check: Set<string> = new Set<string>();
113
+ for (let i = 0; i < input.length && rval; i++) {
114
+ const test: string = input.charAt(i);
115
+ rval = !check.has(test);
116
+ check.add(test);
117
+ }
118
+ }
119
+ return rval;
120
+ }
121
+
122
+ /**
123
+ * Generates all permutations of a given length using the provided alphabet.
124
+ * @param len - The length of each permutation.
125
+ * @param alphabet - A string containing unique characters to use.
126
+ * @returns Array of permutation strings.
127
+ */
128
+ public static allPermutationsOfLength(len: number, alphabet: string): string[] {
129
+ const rval: string[] = [];
130
+ if (len > 0 && alphabet && alphabet.length > 0) {
131
+ RequireRatchet.true(StringRatchet.allUnique(alphabet), 'Alphabet must be unique');
132
+ const step: string[] = len === 1 ? [''] : StringRatchet.allPermutationsOfLength(len - 1, alphabet);
133
+ for (let i = 0; i < alphabet.length; i++) {
134
+ step.forEach((s) => rval.push(alphabet.charAt(i) + s));
135
+ }
136
+ }
137
+ return rval;
138
+ }
139
+
140
+ /**
141
+ * Breaks a string into blocks of specified size separated by the given separator.
142
+ * @param input - The input string.
143
+ * @param blockSize - Size of each block.
144
+ * @param separator - The separator to use between blocks.
145
+ * @returns The formatted string.
146
+ */
147
+ public static breakIntoBlocks(input: string, blockSize: number, separator: string): string {
148
+ let out = '';
149
+ while (input.length > blockSize) {
150
+ out = separator + input.substring(input.length - blockSize) + out;
151
+ input = input.substring(0, input.length - blockSize);
152
+ }
153
+
154
+ if (input.length > 0) {
155
+ out = input + out;
156
+ } else {
157
+ out = out.substring(1); // strip the leading separator
158
+ }
159
+ return out;
160
+ }
161
+
162
+ /**
163
+ * Creates a short unique identifier based on the current time and randomness.
164
+ * @param blockSize - Optional block size to format the UID.
165
+ * @param uniquesPerSecond - Unique numbers per second.
166
+ * @param radix - The radix to convert the number.
167
+ * @returns The generated unique identifier.
168
+ */
169
+ public static createShortUid(blockSize = 0, uniquesPerSecond = 1000, radix = 36): string {
170
+ const currentEpoch: number = Math.floor(Date.now() / 1000);
171
+ const asDecimal: number = parseInt(String(Math.floor(Math.random() * uniquesPerSecond)) + String(currentEpoch));
172
+ const asHex: string = asDecimal.toString(radix);
173
+ const out: string = blockSize > 0 ? StringRatchet.breakIntoBlocks(asHex, blockSize, '-') : asHex;
174
+ return out;
175
+ }
176
+
177
+ /**
178
+ * Generates a version 4 GUID.
179
+ * @returns A GUID string.
180
+ */
181
+ public static createType4Guid(): string {
182
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
183
+ const r = (Math.random() * 16) | 0,
184
+ v = c == 'x' ? r : (r & 0x3) | 0x8;
185
+ return v.toString(16);
186
+ });
187
+ }
188
+
189
+ /**
190
+ * Generates a random string of a specified length from the given alphabet.
191
+ * @param alphabet - The set of characters to use.
192
+ * @param len - Desired length of the random string.
193
+ * @returns The generated random string.
194
+ */
195
+ public static createRandomStringFromAlphabet(alphabet: string, len:number = 10): string {
196
+ RequireRatchet.notNullUndefinedOrOnlyWhitespaceString(alphabet, 'Alphabet may not be empty');
197
+ let rval: string = '';
198
+
199
+ while (rval.length < len) {
200
+ rval += alphabet.charAt(Math.floor(Math.random() * alphabet.length));
201
+ }
202
+ return rval;
203
+
204
+ }
205
+
206
+ /**
207
+ * Generates a random hexadecimal string of specified length.
208
+ * @param len - Desired length of the hex string.
209
+ * @returns The generated hex string.
210
+ */
211
+ public static createRandomHexString(len = 10): string {
212
+ let r = '';
213
+ for (let i = 0; i < len; i++) {
214
+ r += Math.floor(Math.random() * 16).toString(16);
215
+ }
216
+ return r;
217
+ }
218
+
219
+ /**
220
+ * Canonicalizes a string by lowering case, replacing spaces with dashes, and removing reserved characters.
221
+ * @param value - The input string.
222
+ * @returns The canonicalized string.
223
+ */
224
+ public static canonicalize(value: string): string {
225
+ let rval = value ? value.toLowerCase() : '';
226
+
227
+ rval = rval.replace(' ', '-');
228
+ StringRatchet.RFC_3986_RESERVED.forEach((s) => {
229
+ rval = rval.replace(s, '');
230
+ });
231
+
232
+ return rval;
233
+ }
234
+
235
+ /**
236
+ * Formats a number of bytes into a human-readable string.
237
+ * Taken from https://stackoverflow.com/questions/15900485/correct-way-to-convert-size-in-bytes-to-kb-mb-gb-in-javascript
238
+ * @param bytes - The number of bytes.
239
+ * @param decimals - Number of decimal places.
240
+ * @returns A formatted string representing the size.
241
+ */
242
+ public static formatBytes(bytes: number, decimals = 2): string {
243
+ if (bytes == 0) return '0 Bytes';
244
+ const k = 1024,
245
+ dm = decimals || 2,
246
+ sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
247
+ i = Math.floor(Math.log(bytes) / Math.log(k));
248
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
249
+ }
250
+
251
+ /**
252
+ * Converts any input to a string representation regardless of type.
253
+ * @param input - The input value.
254
+ * @returns The string representation of the input.
255
+ */
256
+ public static safeString(input: any): string {
257
+ let rval: string = null;
258
+ if (input != null) {
259
+ const type: string = typeof input;
260
+ if (type == 'string') {
261
+ rval = input;
262
+ } else {
263
+ rval = String(input);
264
+ }
265
+ }
266
+ return rval;
267
+ }
268
+
269
+ /**
270
+ * Determines if a string contains only whitespace characters.
271
+ * @param input - The input string.
272
+ * @returns True if the string contains only whitespace, false otherwise.
273
+ */
274
+ public static stringContainsOnlyWhitespace(input: string): boolean {
275
+ return StringRatchet.stringContainsOnly(input, StringRatchet.WHITESPACE);
276
+ }
277
+
278
+ /**
279
+ * Checks if the string contains only numeric characters.
280
+ * @param input - The input string.
281
+ * @returns True if the string consists solely of digits, otherwise false.
282
+ */
283
+ public static stringContainsOnlyNumbers(input: string): boolean {
284
+ const rval: boolean = /^[0-9]+$/.test(input);
285
+ return rval;
286
+ }
287
+
288
+ /**
289
+ * Checks if the string contains only alphanumeric characters.
290
+ * @param input - The input string.
291
+ * @returns True if the string is alphanumeric, otherwise false.
292
+ */
293
+ public static stringContainsOnlyAlphanumeric(input: string): boolean {
294
+ const rval: boolean = /^[0-9a-zA-Z]+$/.test(input);
295
+ return rval;
296
+ }
297
+ /**
298
+ * Checks if the string contains only hexadecimal characters.
299
+ * @param input - The input string.
300
+ * @returns True if the string is hexadecimal, otherwise false.
301
+ */
302
+ public static stringContainsOnlyHex(input: string): boolean {
303
+ const rval: boolean = /^[0-9a-fA-F]+$/.test(input);
304
+ return rval;
305
+ }
306
+ /**
307
+ * Checks if the string contains only characters found in the provided validChars.
308
+ * @param inVal - The input string.
309
+ * @param validCharsIn - A string of valid characters.
310
+ * @returns True if all characters in inVal are within validCharsIn, otherwise false.
311
+ */
312
+ public static stringContainsOnly(inVal: string, validCharsIn: string): boolean {
313
+ const input: string = !inVal ? '' : inVal;
314
+ const validChars: string = !validCharsIn ? '' : validCharsIn;
315
+ let rval = true;
316
+
317
+ for (let i = 0; i < input.length && rval; i++) {
318
+ rval = validChars.indexOf(input.charAt(i)) >= 0;
319
+ }
320
+ return rval;
321
+ }
322
+
323
+ /**
324
+ * Obscures a string by replacing its middle characters with asterisks.
325
+ * @param input - The string to obscure.
326
+ * @param prefixLength - Number of characters to leave unmasked at the start.
327
+ * @param suffixLength - Number of characters to leave unmasked at the end.
328
+ * @returns The obscured string.
329
+ */
330
+ public static obscure(input: string, prefixLength = 2, suffixLength = 2): string {
331
+ if (!input) {
332
+ return input;
333
+ }
334
+ const len: number = input.length;
335
+ let pl: number = prefixLength;
336
+ let sl: number = suffixLength;
337
+
338
+ while (len > 0 && len < pl + sl + 1) {
339
+ pl = Math.max(0, pl - 1);
340
+ sl = Math.max(0, sl - 1);
341
+ }
342
+ const rem = len - (pl + sl);
343
+
344
+ let rval = '';
345
+ rval += input.substring(0, pl);
346
+ // Yeah, I know. I'm in a rush here
347
+ for (let i = 0; i < rem; i++) {
348
+ rval += '*';
349
+ }
350
+ rval += input.substring(len - sl);
351
+ return rval;
352
+ }
353
+
354
+ /**
355
+ * Pads a number with leading zeros to ensure a fixed length.
356
+ * @param inVal - The number or value to pad.
357
+ * @param size - The desired total length of the resulting string.
358
+ * @returns The padded string.
359
+ */
360
+ public static leadingZeros(inVal: any, size: number): string {
361
+ const pad = '00000000000000000000000000000000000000000000000000';
362
+ let negative = false;
363
+ let sVal = String(inVal);
364
+ if (sVal.startsWith('-')) {
365
+ negative = true;
366
+ sVal = sVal.substring(1);
367
+ }
368
+
369
+ if (size > pad.length) {
370
+ throw new Error('Cannot format number that large');
371
+ }
372
+
373
+ let rval: string = (pad + sVal).slice(-1 * size);
374
+ if (negative) {
375
+ rval = '-' + rval;
376
+ }
377
+ return rval;
378
+ }
379
+
380
+ /**
381
+ * Trims whitespace from a string and returns an empty string if input is null.
382
+ * @param input - The input string.
383
+ * @returns A trimmed string or empty string.
384
+ */
385
+ public static trimToEmpty(input: string): string {
386
+ const t: string = input ? StringRatchet.safeString(input) : '';
387
+ return t.trim();
388
+ }
389
+
390
+ /**
391
+ * Trims whitespace from a string and returns null if the result is empty.
392
+ * @param input - The input string.
393
+ * @returns A trimmed string or null if empty.
394
+ */
395
+ public static trimToNull(input: string): string {
396
+ const x: string = StringRatchet.trimToEmpty(input);
397
+ return x.length > 0 ? x : null;
398
+ }
399
+
400
+ /**
401
+ * Trims all string properties in the provided object, setting properties that contain only whitespace to null.
402
+ * @param input - The object whose string properties should be trimmed.
403
+ * @returns The modified object with trimmed string properties.
404
+ */
405
+ public static trimAllStringPropertiesToNullInPlace<T>(input: T): T {
406
+ return StringRatchet.trimAllStringPropertiesInPlace(input, false);
407
+ }
408
+
409
+ /**
410
+ * Trims all string properties in the provided object, setting properties that contain only whitespace to an empty string.
411
+ * @param input - The object whose string properties should be trimmed.
412
+ * @returns The modified object with trimmed string properties.
413
+ */
414
+ public static trimAllStringPropertiesToEmptyInPlace<T>(input: T): T {
415
+ return StringRatchet.trimAllStringPropertiesInPlace(input, true);
416
+ }
417
+
418
+ private static trimAllStringPropertiesInPlace<T>(input: T, toEmpty: boolean): T {
419
+ const dealKeys = Object.keys(input);
420
+ dealKeys.forEach((key) => {
421
+ const val = input[key];
422
+ if (val != null && typeof val === 'string') {
423
+ input[key] = toEmpty ? StringRatchet.trimToEmpty(input[key]) : StringRatchet.trimToNull(input[key]);
424
+ }
425
+ });
426
+ return input;
427
+ }
428
+
429
+ /**
430
+ * Removes all non-numeric characters from the input string.
431
+ * @param input - The input string.
432
+ * @returns A string consisting only of numeric characters (and an optional leading '-' sign).
433
+ */
434
+ public static stripNonNumeric(input: string): string {
435
+ let rval: string = input;
436
+ if (input != null && !StringRatchet.stringContainsOnlyNumbers(input)) {
437
+ // Im sure there is a better way
438
+ rval = '';
439
+ for (let i = 0; i < input.length; i++) {
440
+ const c: string = input.charAt(i);
441
+ if (StringRatchet.stringContainsOnlyNumbers(c) || (i === 0 && c === '-')) {
442
+ rval += c;
443
+ }
444
+ }
445
+ }
446
+
447
+ return rval;
448
+ }
449
+
450
+ /**
451
+ * Formats a value into a CSV-safe string by quoting if necessary and escaping quotes.
452
+ * @param input - The value to format.
453
+ * @returns A CSV-safe string.
454
+ */
455
+ public static csvSafe(input: any): string {
456
+ let rval: string = StringRatchet.trimToEmpty(StringRatchet.safeString(input));
457
+ rval.split('"').join('\\"');
458
+ if (rval.indexOf(',') !== -1 || rval.indexOf('"') !== -1 || rval.indexOf("'") !== -1) {
459
+ rval = '"' + rval + '"';
460
+ }
461
+ return rval;
462
+ }
463
+
464
+ // Zach
465
+ /**
466
+ * Removes non-ASCII characters from a string.
467
+ * @param value - The input string.
468
+ * @returns A string containing only ASCII characters.
469
+ */
470
+ public static stripNonAscii(value: string): string {
471
+ const reduced = [...value].reduce((previousValue: string, currentValue: string) => {
472
+ const charCode: number = currentValue.charCodeAt(0);
473
+ if (charCode > 127) {
474
+ return previousValue;
475
+ }
476
+ return previousValue + currentValue;
477
+ });
478
+ return reduced;
479
+ }
480
+
481
+ /**
482
+ * Replaces all occurrences of a substring with another substring.
483
+ * @param value - The original string.
484
+ * @param src - The substring to replace.
485
+ * @param dst - The replacement substring.
486
+ * @returns The modified string.
487
+ */
488
+ public static replaceAll(value: string, src: string, dst: string): string {
489
+ let rval: string = value;
490
+ if (rval?.length && src?.length && dst?.length) {
491
+ rval = rval.split(src).join(dst);
492
+ }
493
+ return rval;
494
+ }
495
+
496
+ /**
497
+ * Performs a simple template fill on the provided string.
498
+ * Replaces placeholders in the form ${key} with corresponding values from the filler's object.
499
+ * e.g., "This is ${value} output", where value=An, becomes "This is An output"
500
+ * Very similar to what handlebars will do, or for that matter, what
501
+ * JavaScript will do natively with backticks, but typesafe and can
502
+ * be passed around. Default template style is ${value} to match JS backticks
503
+ * @param template - The template string containing placeholders.
504
+ * @param fillers - An object with keys corresponding to placeholders in the template.
505
+ * @param errorOnMissingFiller - If true, throws an error if any placeholder is not provided a value.
506
+ * @param opener - The opening delimiter for a placeholder.
507
+ * @param closer - The closing delimiter for a placeholder.
508
+ * @returns The template string with placeholders replaced by their corresponding values.
509
+ */
510
+ public static simpleTemplateFill(
511
+ template: string,
512
+ fillers: Record<string, any>,
513
+ errorOnMissingFiller = false,
514
+ opener = '${',
515
+ closer = '}',
516
+ ): string {
517
+ let rval: string = template;
518
+ if (rval && fillers) {
519
+ Object.keys(fillers).forEach((key) => {
520
+ rval = rval.split(opener + key + closer).join(fillers[key]);
521
+ });
522
+ }
523
+ if (errorOnMissingFiller && rval?.indexOf(opener) >= 0) {
524
+ throw new Error('Template has unfilled variables:' + rval);
525
+ }
526
+ return rval;
527
+ }
528
+
529
+ /**
530
+ * Safely JSON stringifies an input, handling circular references gracefully.
531
+ * @param input - The input value to stringify.
532
+ * @returns A JSON string representation of the input, or an error message if circular references are found.
533
+ */
534
+ public static circSafeJsonStringify(input: any): string {
535
+ let rval: string = null;
536
+ try {
537
+ rval = JSON.stringify(input);
538
+ } catch (err) {
539
+ if (err instanceof TypeError) {
540
+ let lines: string[] = err.message
541
+ .split('\n')
542
+ .map((s) => StringRatchet.trimToNull(s))
543
+ .filter((s) => !!s);
544
+ lines = lines.filter((s) => s.startsWith('-->') || s.startsWith('---'));
545
+
546
+ rval = 'Cannot stringify - object contains circular reference : ' + lines.join(', ');
547
+ } else {
548
+ throw err;
549
+ }
550
+ }
551
+ return rval;
552
+ }
553
+
554
+ /**
555
+ * Formats a string using placeholders similar to "util.format" in Node.js.
556
+ * Supported placeholders: %o for objects (must be arrays), %s for strings,
557
+ * %d for numbers, %j for JSON.
558
+ *
559
+ * Here so that I don't need to bring in a polyfill. It is lifted DIRECTLY
560
+ * from https://github.com/tmpfs/format-util License is MIT, see
561
+ * ReferencedLicences for details.
562
+ * All credit is due to that author for actually writing this thing.
563
+ * @param fmt - The format string.
564
+ * @param args - Values to replace placeholders.
565
+ * @returns The formatted string.
566
+ */
567
+ public static format(fmt: string, ...args: any[]): string {
568
+ const re: RegExp = /(%?)(%([ojds]))/g;
569
+ if (args.length) {
570
+ fmt = fmt.replace(re, function (match, escaped, ptn, flag) {
571
+ let arg = args.shift();
572
+ switch (flag) {
573
+ case 'o':
574
+ if (Array.isArray(arg)) {
575
+ arg = StringRatchet.circSafeJsonStringify(arg);
576
+ break;
577
+ } else {
578
+ throw new Error('Cannot use o placeholder for argument of type ' + typeof arg);
579
+ }
580
+ case 's':
581
+ arg = '' + arg;
582
+ break;
583
+ case 'd':
584
+ arg = Number(arg);
585
+ break;
586
+ case 'j':
587
+ arg = StringRatchet.circSafeJsonStringify(arg);
588
+ break;
589
+ }
590
+ if (!escaped) {
591
+ return arg;
592
+ }
593
+ args.unshift(arg);
594
+ return match;
595
+ });
596
+ }
597
+
598
+ // arguments remain after formatting
599
+ if (args.length) {
600
+ fmt += ' ' + args.join(' ');
601
+ }
602
+
603
+ // update escaped %% values
604
+ fmt = fmt.replace(/%{2,2}/g, '%');
605
+
606
+ return '' + fmt;
607
+ }
608
+
609
+ /**
610
+ * Helper function to compute the length of the suffix that repeats without overlap.
611
+ * Detail: Finds the longest repeating and non-overlapping substring using memoization
612
+ * Ripped from https://www.geeksforgeeks.org/longest-repeating-and-non-overlapping-substring/
613
+ * With some updates added to show types
614
+ * @param i - Starting index for first substring.
615
+ * @param j - Starting index for second substring.
616
+ * @param s - The input string.
617
+ * @param memo - 2D memoization array to cache results.
618
+ * @returns The length of the common suffix that does not overlap.
619
+ */
620
+ public static findSuffix(i: number, j: number, s: string, memo: number[][]) {
621
+ // base case
622
+ if (j === s.length) return 0;
623
+
624
+ // return memoized value
625
+ if (memo[i][j] !== -1) return memo[i][j];
626
+
627
+ // if characters match
628
+ if (s[i] === s[j]) {
629
+ memo[i][j] = 1 + Math.min(StringRatchet.findSuffix(i + 1, j + 1, s, memo), j - i - 1);
630
+ } else {
631
+ memo[i][j] = 0;
632
+ }
633
+
634
+ return memo[i][j];
635
+ }
636
+
637
+ /**
638
+ * Finds the longest non-overlapping repeating substring in the given string.
639
+ * @param s - The input string.
640
+ * @returns The longest repeating substring if found, otherwise null.
641
+ */
642
+ public static longestNonOverlappingRepeatingSubstring(s: string): string | null {
643
+ const n: number = s.length;
644
+
645
+ const memo: number[][] = Array.from({ length: n }, () => Array(n).fill(-1));
646
+
647
+ // find length of non-overlapping
648
+ // substrings for all pairs (i, j)
649
+ for (let i = 0; i < n; i++) {
650
+ for (let j = i + 1; j < n; j++) {
651
+ StringRatchet.findSuffix(i, j, s, memo);
652
+ }
653
+ }
654
+
655
+ let ans = '';
656
+ let ansLen = 0;
657
+
658
+ // If length of suffix is greater
659
+ // than ansLen, update ans and ansLen
660
+ for (let i = 0; i < n; i++) {
661
+ for (let j = i + 1; j < n; j++) {
662
+ if (memo[i][j] > ansLen) {
663
+ ansLen = memo[i][j];
664
+ ans = s.substring(i, i + ansLen);
665
+ }
666
+ }
667
+ }
668
+
669
+ return ansLen > 0 ? ans : null;
670
+ }
671
+
672
+ public static snakeCaseToCamelCase(str) {
673
+ return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
674
+ }
675
+
676
+ }