@bitblit/ratchet-common 6.0.145-alpha → 6.0.147-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.
- package/package.json +2 -1
- package/src/2d/line-2d.ts +6 -0
- package/src/2d/matrix-factory.ts +94 -0
- package/src/2d/plane-2d-type.ts +6 -0
- package/src/2d/plane-2d.ts +7 -0
- package/src/2d/point-2d.ts +4 -0
- package/src/2d/poly-line-2d.ts +5 -0
- package/src/2d/ratchet-2d.spec.ts +205 -0
- package/src/2d/ratchet-2d.ts +350 -0
- package/src/2d/transformation-matrix.ts +19 -0
- package/src/build/build-information.ts +8 -0
- package/src/build/ratchet-common-info.ts +19 -0
- package/src/histogram/histogram-entry.ts +4 -0
- package/src/histogram/histogram.spec.ts +25 -0
- package/src/histogram/histogram.ts +61 -0
- package/src/jwt/common-jwt-token.ts +17 -0
- package/src/jwt/expired-jwt-handling.ts +5 -0
- package/src/jwt/jwt-decode-only-ratchet.ts +26 -0
- package/src/jwt/jwt-payload-expiration-ratchet.ts +45 -0
- package/src/jwt/jwt-token-base.ts +14 -0
- package/src/lang/array-ratchet.spec.ts +79 -0
- package/src/lang/array-ratchet.ts +141 -0
- package/src/lang/base64-ratchet.spec.ts +48 -0
- package/src/lang/base64-ratchet.ts +247 -0
- package/src/lang/boolean-ratchet.spec.ts +95 -0
- package/src/lang/boolean-ratchet.ts +52 -0
- package/src/lang/composite-last-success-provider.spec.ts +31 -0
- package/src/lang/composite-last-success-provider.ts +30 -0
- package/src/lang/currency-ratchet.ts +29 -0
- package/src/lang/date-ratchet.spec.ts +27 -0
- package/src/lang/date-ratchet.ts +42 -0
- package/src/lang/duration-ratchet.spec.ts +47 -0
- package/src/lang/duration-ratchet.ts +77 -0
- package/src/lang/enum-ratchet.spec.ts +45 -0
- package/src/lang/enum-ratchet.ts +41 -0
- package/src/lang/error-handling-approach.ts +6 -0
- package/src/lang/error-ratchet.spec.ts +25 -0
- package/src/lang/error-ratchet.ts +70 -0
- package/src/lang/esm-ratchet.ts +81 -0
- package/src/lang/expiring-object.spec.ts +56 -0
- package/src/lang/expiring-object.ts +84 -0
- package/src/lang/geolocation-ratchet.spec.ts +177 -0
- package/src/lang/geolocation-ratchet.ts +341 -0
- package/src/lang/global-ratchet.spec.ts +17 -0
- package/src/lang/global-ratchet.ts +105 -0
- package/src/lang/key-value.ts +8 -0
- package/src/lang/last-success-provider.ts +4 -0
- package/src/lang/map-ratchet.spec.ts +113 -0
- package/src/lang/map-ratchet.ts +220 -0
- package/src/lang/no.spec.ts +9 -0
- package/src/lang/no.ts +7 -0
- package/src/lang/number-ratchet.spec.ts +154 -0
- package/src/lang/number-ratchet.ts +253 -0
- package/src/lang/parsed-url.ts +10 -0
- package/src/lang/promise-ratchet.spec.ts +104 -0
- package/src/lang/promise-ratchet.ts +196 -0
- package/src/lang/range.ts +4 -0
- package/src/lang/require-ratchet.spec.ts +85 -0
- package/src/lang/require-ratchet.ts +68 -0
- package/src/lang/simple-arg-ratchet.spec.ts +13 -0
- package/src/lang/simple-arg-ratchet.ts +47 -0
- package/src/lang/simple-encryption-ratchet.ts +88 -0
- package/src/lang/sort-ratchet.spec.ts +58 -0
- package/src/lang/sort-ratchet.ts +50 -0
- package/src/lang/stop-watch.spec.ts +53 -0
- package/src/lang/stop-watch.ts +202 -0
- package/src/lang/string-ratchet.spec.ts +226 -0
- package/src/lang/string-ratchet.ts +676 -0
- package/src/lang/time-zone-ratchet.spec.ts +51 -0
- package/src/lang/time-zone-ratchet.ts +148 -0
- package/src/lang/timeout-token.spec.ts +12 -0
- package/src/lang/timeout-token.ts +21 -0
- package/src/lang/uint-8-array-ratchet.spec.ts +22 -0
- package/src/lang/uint-8-array-ratchet.ts +48 -0
- package/src/lang/web-stream-ratchet.spec.ts +12 -0
- package/src/lang/web-stream-ratchet.ts +96 -0
- package/src/logger/classic-single-line-log-message-formatter.ts +19 -0
- package/src/logger/log-message-builder.ts +60 -0
- package/src/logger/log-message-format-type.ts +11 -0
- package/src/logger/log-message-formatter.ts +6 -0
- package/src/logger/log-message-processor.ts +6 -0
- package/src/logger/log-message.ts +9 -0
- package/src/logger/log-snapshot.ts +6 -0
- package/src/logger/logger-instance.ts +269 -0
- package/src/logger/logger-level-name.ts +11 -0
- package/src/logger/logger-meta.ts +7 -0
- package/src/logger/logger-options.ts +14 -0
- package/src/logger/logger-output-function.ts +10 -0
- package/src/logger/logger-ring-buffer.ts +89 -0
- package/src/logger/logger-util.spec.ts +11 -0
- package/src/logger/logger-util.ts +68 -0
- package/src/logger/logger.spec.ts +177 -0
- package/src/logger/logger.ts +213 -0
- package/src/logger/none-log-message-formatter.ts +10 -0
- package/src/logger/single-line-no-level-log-message-formatter.ts +18 -0
- package/src/logger/structured-json-log-message-formatter.ts +25 -0
- package/src/mail/archive-email-result.ts +8 -0
- package/src/mail/email-attachment.ts +23 -0
- package/src/mail/mail-sending-provider.ts +21 -0
- package/src/mail/mailer-config.ts +30 -0
- package/src/mail/mailer-like.ts +38 -0
- package/src/mail/mailer-util.ts +65 -0
- package/src/mail/mailer.spec.ts +120 -0
- package/src/mail/mailer.ts +214 -0
- package/src/mail/ready-to-send-email.ts +67 -0
- package/src/mail/resolved-ready-to-send-email.ts +17 -0
- package/src/mail/send-email-result.ts +16 -0
- package/src/mail/test-mail-sending-provider.ts +35 -0
- package/src/network/browser-local-ip-provider.spec.ts +23 -0
- package/src/network/browser-local-ip-provider.ts +26 -0
- package/src/network/fixed-local-ip-provider.ts +9 -0
- package/src/network/local-ip-provider.ts +4 -0
- package/src/network/network-ratchet.spec.ts +17 -0
- package/src/network/network-ratchet.ts +209 -0
- package/src/network/remote-file-tracker/backup-result.ts +6 -0
- package/src/network/remote-file-tracker/file-transfer-result-type.ts +5 -0
- package/src/network/remote-file-tracker/file-transfer-result.ts +9 -0
- package/src/network/remote-file-tracker/remote-file-tracker-options.ts +6 -0
- package/src/network/remote-file-tracker/remote-file-tracker-push-options.ts +4 -0
- package/src/network/remote-file-tracker/remote-file-tracker.ts +117 -0
- package/src/network/remote-file-tracker/remote-file-tracking-provider.ts +19 -0
- package/src/network/remote-file-tracker/remote-status-data-and-content.ts +6 -0
- package/src/network/remote-file-tracker/remote-status-data.ts +7 -0
- package/src/network/restful-api-http-error.spec.ts +13 -0
- package/src/network/restful-api-http-error.ts +173 -0
- package/src/template/ratchet-template-renderer.ts +8 -0
- package/src/third-party/google/google-recaptcha-ratchet.spec.ts +27 -0
- package/src/third-party/google/google-recaptcha-ratchet.ts +36 -0
- package/src/third-party/twilio/twilio-ratchet.ts +92 -0
- package/src/third-party/twilio/twilio-verify-ratchet.ts +83 -0
- package/src/transform/built-in-transforms.ts +214 -0
- package/src/transform/transform-ratchet.spec.ts +134 -0
- package/src/transform/transform-ratchet.ts +88 -0
- package/src/transform/transform-rule.ts +7 -0
- package/src/tx/transaction-configuration.ts +8 -0
- package/src/tx/transaction-final-state.ts +7 -0
- package/src/tx/transaction-ratchet.spec.ts +150 -0
- package/src/tx/transaction-ratchet.ts +98 -0
- package/src/tx/transaction-result.ts +10 -0
- package/src/tx/transaction-step.ts +5 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Functions for working with maps (dictionaries/objects in javascript)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { KeyValue } from './key-value.js';
|
|
6
|
+
import { Logger } from '../logger/logger.js';
|
|
7
|
+
import { ErrorRatchet } from './error-ratchet.js';
|
|
8
|
+
import { StringRatchet } from "./string-ratchet.ts";
|
|
9
|
+
|
|
10
|
+
export class MapRatchet {
|
|
11
|
+
// Takes any map with keys that are nested and expands them
|
|
12
|
+
// eg, x['a.b']=2 becomes x['a']={b:2}
|
|
13
|
+
// Renamed because the pre-v4 version deferred to lodash and handled arrays correctly.
|
|
14
|
+
// This removes the lodash dependency, but isn't fully backward compatible
|
|
15
|
+
public static expandNestedKeysToObjects<T>(src: any, separator = '.'): T {
|
|
16
|
+
if (!separator || separator.length !== 1) {
|
|
17
|
+
throw new Error('Invalid separator (must be single character)');
|
|
18
|
+
}
|
|
19
|
+
const rval: T = {} as T;
|
|
20
|
+
Object.keys(src).forEach((k) => {
|
|
21
|
+
const path: string[] = k.split(separator);
|
|
22
|
+
let target: any = rval;
|
|
23
|
+
while (path.length > 1) {
|
|
24
|
+
target[path[0]] = target[path[0]] || {};
|
|
25
|
+
target = target[path[0]];
|
|
26
|
+
path.splice(0, 1);
|
|
27
|
+
}
|
|
28
|
+
target[path[0]] = src[k];
|
|
29
|
+
});
|
|
30
|
+
return rval;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public static mapByUniqueProperty<T, R>(input: T[], propName: string): Map<R, T> {
|
|
34
|
+
if (!input || !propName) {
|
|
35
|
+
throw new Error('Neither input nor propName can be null');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const rval: Map<R, T> = new Map<R, T>();
|
|
39
|
+
input.forEach((i) => {
|
|
40
|
+
const val: R = i ? i[propName] : null;
|
|
41
|
+
if (val === null || val === undefined) {
|
|
42
|
+
throw new Error('No value for ' + propName + ' found in ' + JSON.stringify(i));
|
|
43
|
+
}
|
|
44
|
+
if (rval.has(val)) {
|
|
45
|
+
throw new Error('Multiple values found for ' + val);
|
|
46
|
+
}
|
|
47
|
+
rval.set(val, i);
|
|
48
|
+
});
|
|
49
|
+
return rval;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public static groupByProperty<T, R>(input: T[], propName: string): Map<R, T[]> {
|
|
53
|
+
if (!input || !propName) {
|
|
54
|
+
throw new Error('Neither input nor propName can be null');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const rval: Map<R, T[]> = new Map<R, T[]>();
|
|
58
|
+
input.forEach((i) => {
|
|
59
|
+
const val: R = i ? i[propName] : null;
|
|
60
|
+
if (val === null || val === undefined) {
|
|
61
|
+
throw ErrorRatchet.fErr('No value for %s found in %j', propName, i);
|
|
62
|
+
}
|
|
63
|
+
if (!rval.has(val)) {
|
|
64
|
+
rval.set(val, []);
|
|
65
|
+
}
|
|
66
|
+
rval.get(val).push(i);
|
|
67
|
+
});
|
|
68
|
+
return rval;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public static findValue(toSearch: any, path: string[]): any {
|
|
72
|
+
if (!path || path.length == 0) {
|
|
73
|
+
return toSearch;
|
|
74
|
+
} else {
|
|
75
|
+
if (toSearch) {
|
|
76
|
+
return MapRatchet.findValue(toSearch[path[0]], path.slice(1));
|
|
77
|
+
} else {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public static findValueDotPath(toSearch: any, dotPath: string): any {
|
|
84
|
+
if (!dotPath || dotPath.length == 0) {
|
|
85
|
+
return toSearch;
|
|
86
|
+
} else {
|
|
87
|
+
if (toSearch) {
|
|
88
|
+
return MapRatchet.findValue(toSearch, dotPath.split('.'));
|
|
89
|
+
} else {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Ok so this does the dumbest possible deep compare, by converting
|
|
96
|
+
// both objects to JSON and comparing strings. Its slow and stupid
|
|
97
|
+
// but its easy.
|
|
98
|
+
|
|
99
|
+
public static simpleDeepCompare(object1: any, object2: any): boolean {
|
|
100
|
+
if (object1 == null && object2 == null) return true;
|
|
101
|
+
if (object1 == null || object2 == null) return false;
|
|
102
|
+
return JSON.stringify(object1) == JSON.stringify(object2);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public static toKeyValueList(value: Record<string, any>): KeyValue<any>[] {
|
|
106
|
+
const returnArray: KeyValue<any>[] = [];
|
|
107
|
+
|
|
108
|
+
for (const k of Object.keys(value)) {
|
|
109
|
+
returnArray.push({
|
|
110
|
+
key: k,
|
|
111
|
+
value: value[k],
|
|
112
|
+
} as KeyValue<any>);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return returnArray;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
public static fromKeyValueList<T>(list: KeyValue<T>[]): Record<string, T> {
|
|
119
|
+
const rval: any = {};
|
|
120
|
+
list.forEach((a) => (rval[a.key] = a.value));
|
|
121
|
+
return rval;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/*
|
|
125
|
+
Mainly here to simplify sending objects to DynamoDB - recursively descend and clean up javascript objects, removing
|
|
126
|
+
any empty strings, nulls, etc.
|
|
127
|
+
|
|
128
|
+
CAW 2024-08-09: Since DynamoClient has its own implementation of this now, better to use the marshaller over there instead
|
|
129
|
+
*/
|
|
130
|
+
public static cleanup<T>(obj: T, stripZero = false, stripNull = true, stripUndefined = true, stripEmptyString = true): T {
|
|
131
|
+
// See : https://stackoverflow.com/questions/286141/remove-blank-attributes-from-an-object-in-javascript
|
|
132
|
+
if (obj === null || obj === undefined || typeof obj !== 'object') {
|
|
133
|
+
return obj;
|
|
134
|
+
}
|
|
135
|
+
const o = JSON.parse(JSON.stringify(obj)); // Clone source oect.
|
|
136
|
+
|
|
137
|
+
Object.keys(o).forEach((key) => {
|
|
138
|
+
if (o[key] && typeof o[key] === 'object') {
|
|
139
|
+
if (Array.isArray(o[key])) {
|
|
140
|
+
for (let i = 0; i < o[key].length; i++) {
|
|
141
|
+
o[key][i] = MapRatchet.cleanup(o[key][i]);
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
o[key] = MapRatchet.cleanup(o[key]); // Recurse.
|
|
145
|
+
}
|
|
146
|
+
} else if (
|
|
147
|
+
(o[key] === undefined && stripUndefined) ||
|
|
148
|
+
(o[key] === null && stripNull) ||
|
|
149
|
+
(o[key] === '' && stripEmptyString) ||
|
|
150
|
+
(o[key] === 0 && stripZero)
|
|
151
|
+
) {
|
|
152
|
+
// This actually IS a bad example of this, but the whole function is deprecated now
|
|
153
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
154
|
+
delete o[key]; // Delete undefined and null.
|
|
155
|
+
} else {
|
|
156
|
+
// Leave it alone
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return o; // Return new object.
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
public static extractValueFromMapIgnoreCase(src: any, key: string): any {
|
|
164
|
+
let rval: any = null;
|
|
165
|
+
if (src && key) {
|
|
166
|
+
const finder: string = key.toLowerCase();
|
|
167
|
+
Object.keys(src).forEach((s) => {
|
|
168
|
+
if (s.toLowerCase() === finder) {
|
|
169
|
+
const newVal: string = src[s];
|
|
170
|
+
if (rval) {
|
|
171
|
+
Logger.warn('Multiple entries found for %s (replacing %s with %s', key, rval, newVal);
|
|
172
|
+
}
|
|
173
|
+
rval = newVal;
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return rval;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
public static safeCallFunction(ob: any, fnName: string): boolean {
|
|
181
|
+
let rval = false;
|
|
182
|
+
if (!!ob && !!ob[fnName] && typeof ob[fnName] === 'function') {
|
|
183
|
+
try {
|
|
184
|
+
ob[fnName]();
|
|
185
|
+
rval = true;
|
|
186
|
+
} catch (err) {
|
|
187
|
+
Logger.warn('Error calling %s on %s : %s', fnName, ob, err, err);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return rval;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
public static caseInsensitiveAccess<T>(ob: any, keyName: string): T {
|
|
194
|
+
let rval: T = null;
|
|
195
|
+
|
|
196
|
+
if (!!ob && !!keyName) {
|
|
197
|
+
rval = ob[keyName]; // Short circuit
|
|
198
|
+
if (!rval) {
|
|
199
|
+
const keyNameCI: string = Object.keys(ob).find((f) => f.toLowerCase() === keyName.toLowerCase());
|
|
200
|
+
if (keyNameCI) {
|
|
201
|
+
rval = ob[keyNameCI];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return rval;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
public static snakeCaseKeysToCamelCase(obj:any):any {
|
|
209
|
+
if (Array.isArray(obj)) {
|
|
210
|
+
return obj.map(v => StringRatchet.snakeCaseToCamelCase(v));
|
|
211
|
+
} else if (obj !== null && obj.constructor === Object) {
|
|
212
|
+
return Object.entries(obj).reduce((acc, [key, value]) => {
|
|
213
|
+
const camelKey = key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
214
|
+
acc[camelKey] = StringRatchet.snakeCaseToCamelCase(value);
|
|
215
|
+
return acc;
|
|
216
|
+
}, {});
|
|
217
|
+
}
|
|
218
|
+
return obj;
|
|
219
|
+
}
|
|
220
|
+
}
|
package/src/lang/no.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { NumberRatchet, SinglesAndRanges } from './number-ratchet.js';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { EsmRatchet } from './esm-ratchet.js';
|
|
5
|
+
import { describe, expect, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
describe('#toFixedDecimalNumber', function () {
|
|
8
|
+
test('should convert "5.1234 to 5.12', function () {
|
|
9
|
+
expect(NumberRatchet.toFixedDecimalNumber(5.1234, 2)).toEqual(5.12);
|
|
10
|
+
expect(NumberRatchet.toFixedDecimalNumber('5.1234', 2)).toEqual(5.12);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('should convert "5.5678 to 5.57', function () {
|
|
14
|
+
expect(NumberRatchet.toFixedDecimalNumber(5.5678, 2)).toEqual(5.57);
|
|
15
|
+
expect(NumberRatchet.toFixedDecimalNumber('5.5678', 2)).toEqual(5.57);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('#leadingZeros', function () {
|
|
20
|
+
test('should convert "5" to 05', function () {
|
|
21
|
+
const result: string = NumberRatchet.leadingZeros('5', 2);
|
|
22
|
+
expect(result).toEqual('05');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('should leave 166 alone', function () {
|
|
26
|
+
const result: string = NumberRatchet.leadingZeros('166', 2);
|
|
27
|
+
expect(result).toEqual('166');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('#safeToNumber', function () {
|
|
32
|
+
test('should convert "55" to 55', function () {
|
|
33
|
+
const result: number = NumberRatchet.safeNumber('55');
|
|
34
|
+
expect(result).toEqual(55);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('should convert "574,528" to 574528', function () {
|
|
38
|
+
const result: number = NumberRatchet.safeNumber('574,528');
|
|
39
|
+
expect(result).toEqual(574528);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('should convert "574.528,88" to 574528.88 like european', function () {
|
|
43
|
+
const result: number = NumberRatchet.safeNumberOpt('574.528,88', { preParseCharacterMapping: { '.': '', ',': '.' } });
|
|
44
|
+
expect(result).toEqual(574528.88);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('should leave 66 alone', function () {
|
|
48
|
+
const result: number = NumberRatchet.safeNumber(66);
|
|
49
|
+
expect(result).toEqual(66);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('should return the default when it cannot parse', function () {
|
|
53
|
+
const result: number = NumberRatchet.safeNumber({ test: 'test' }, 42);
|
|
54
|
+
expect(result).toEqual(42);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('should return the default for the empty string', function () {
|
|
58
|
+
const result: number = NumberRatchet.safeNumber({ test: '' }, 42);
|
|
59
|
+
expect(result).toEqual(42);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('should return the default for null/undefined if set true', function () {
|
|
63
|
+
const result: number = NumberRatchet.safeNumber(null, 42, true);
|
|
64
|
+
expect(result).toEqual(42);
|
|
65
|
+
const result2: number = NumberRatchet.safeNumber(undefined, 46, true);
|
|
66
|
+
expect(result2).toEqual(46);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('should return the passed value for null/undefined if set false', function () {
|
|
70
|
+
const result: number = NumberRatchet.safeNumber(null, 42, false);
|
|
71
|
+
expect(result).toEqual(null);
|
|
72
|
+
const result2: number = NumberRatchet.safeNumber(undefined, 46, false);
|
|
73
|
+
expect(result2).toEqual(undefined);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('should return null for an undefined input by default', function () {
|
|
77
|
+
const result: number | null = NumberRatchet.safeNumber(undefined, 0);
|
|
78
|
+
expect(result).toEqual(null);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('should return null for a null input by default', function () {
|
|
82
|
+
const result: number = NumberRatchet.safeNumber(null, 42);
|
|
83
|
+
expect(result).toEqual(null);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('#parseCSV', function () {
|
|
88
|
+
test('should convert "1,2,3" to [1,2,3]', function () {
|
|
89
|
+
const result: number[] = NumberRatchet.numberCSVToList('1,2,3');
|
|
90
|
+
expect(result.length).toEqual(3);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('should convert " 1, 2,3 " to [1,2,3]', function () {
|
|
94
|
+
const result: number[] = NumberRatchet.numberCSVToList(' 1, 2,3 ');
|
|
95
|
+
expect(result.length).toEqual(3);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('should convert " a1, 2,b " to [2]', function () {
|
|
99
|
+
const result: number[] = NumberRatchet.numberCSVToList(' a1, 2,b ');
|
|
100
|
+
expect(result.length).toEqual(1);
|
|
101
|
+
expect(result[0]).toEqual(2);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('#fitToWindow', function () {
|
|
106
|
+
test('should fit input to the window', function () {
|
|
107
|
+
expect(NumberRatchet.fitToWindow(5, 8, 10)).toEqual(9);
|
|
108
|
+
expect(NumberRatchet.fitToWindow(8, 2, 10)).toEqual(8);
|
|
109
|
+
expect(NumberRatchet.fitToWindow(8, 9, 9)).toEqual(9);
|
|
110
|
+
expect(NumberRatchet.fitToWindow(12, 2, 10)).toEqual(4);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('#groupNumbersIntoContiguousRanges', function () {
|
|
115
|
+
test('should group numbers', function () {
|
|
116
|
+
let input: number[] = JSON.parse(
|
|
117
|
+
fs.readFileSync(path.join(EsmRatchet.fetchDirName(import.meta.url), '../../../../test-data/number_set.json')).toString(),
|
|
118
|
+
);
|
|
119
|
+
input = input.map((i) => NumberRatchet.safeNumber(i));
|
|
120
|
+
const grouped: SinglesAndRanges = NumberRatchet.groupNumbersIntoContiguousRanges(input, 5);
|
|
121
|
+
expect(grouped).toBeTruthy();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('#distributeItemsEvenly', function () {
|
|
126
|
+
test('should distribute evenly', function () {
|
|
127
|
+
const test1: number[] = NumberRatchet.distributeItemsEvenly(4, 6);
|
|
128
|
+
expect(test1).toBeTruthy();
|
|
129
|
+
const test2: number[] = NumberRatchet.distributeItemsEvenly(12, 6);
|
|
130
|
+
expect(test2).toBeTruthy();
|
|
131
|
+
const test3: number[] = NumberRatchet.distributeItemsEvenly(5, 6);
|
|
132
|
+
expect(test3).toBeTruthy();
|
|
133
|
+
const test4: number[] = NumberRatchet.distributeItemsEvenly(192, 11);
|
|
134
|
+
expect(test4).toBeTruthy();
|
|
135
|
+
const test5: number[] = NumberRatchet.distributeItemsEvenly(11, 192);
|
|
136
|
+
expect(test5).toBeTruthy();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('#createRange', function () {
|
|
141
|
+
test('should create ranges', function () {
|
|
142
|
+
const test1: number[] = NumberRatchet.createRange(0, 5, 1);
|
|
143
|
+
expect(test1).toBeTruthy();
|
|
144
|
+
expect(test1.length).toEqual(5);
|
|
145
|
+
|
|
146
|
+
const test2: number[] = NumberRatchet.createRange(1, 5, 1);
|
|
147
|
+
expect(test2).toBeTruthy();
|
|
148
|
+
expect(test2.length).toEqual(4);
|
|
149
|
+
|
|
150
|
+
const test3: number[] = NumberRatchet.createRange(0, 10, 2);
|
|
151
|
+
expect(test3).toBeTruthy();
|
|
152
|
+
expect(test3.length).toEqual(5);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Functions for working with numbers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Logger } from '../logger/logger.js';
|
|
6
|
+
import { RequireRatchet } from './require-ratchet.js';
|
|
7
|
+
import { Range } from './range';
|
|
8
|
+
|
|
9
|
+
export class NumberRatchet {
|
|
10
|
+
private static MAX_LEADING_ZEROS_FORMAT_LENGTH = 1000; // Because really, why?
|
|
11
|
+
public static readonly DEFAULT_SAFE_NUMBER_OPTIONS: SafeNumberOptions = {
|
|
12
|
+
ifNotNumber: null,
|
|
13
|
+
returnValueForNull: null,
|
|
14
|
+
returnValueForUndefined: null,
|
|
15
|
+
preParseCharacterMapping: { ',': '' },
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
public static toFixedDecimalNumber(input: number | string, placesAfterPoint: number): number {
|
|
19
|
+
const v: number = NumberRatchet.safeNumber(input);
|
|
20
|
+
// If v is not defined, let that bubble up...
|
|
21
|
+
return v === null || v === undefined ? v : NumberRatchet.safeNumber(v.toFixed(placesAfterPoint));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public static leadingZeros(val: any, size: number): string {
|
|
25
|
+
const sVal = String(val);
|
|
26
|
+
if (sVal.length < size) {
|
|
27
|
+
let pad = '0000';
|
|
28
|
+
if (size > NumberRatchet.MAX_LEADING_ZEROS_FORMAT_LENGTH) {
|
|
29
|
+
throw 'Cannot format number that large (max length is ' + NumberRatchet.MAX_LEADING_ZEROS_FORMAT_LENGTH + ')';
|
|
30
|
+
}
|
|
31
|
+
while (pad.length < size) {
|
|
32
|
+
pad = pad + pad; // It won't take that long to get there
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (pad + sVal).slice(-1 * size);
|
|
36
|
+
} else {
|
|
37
|
+
return sVal;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public static between(inTest: number, inP1: number, inP2: number): boolean {
|
|
42
|
+
const test: number = NumberRatchet.safeNumber(inTest);
|
|
43
|
+
const p1: number = NumberRatchet.safeNumber(inP1);
|
|
44
|
+
const p2: number = NumberRatchet.safeNumber(inP2);
|
|
45
|
+
|
|
46
|
+
return (test >= p1 && test <= p2) || (test >= p2 && test <= p1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// If it's a number, leave it alone, if it's a string, parse it.
|
|
50
|
+
// If its null or undefined, default to passing it through
|
|
51
|
+
// unmodified to match old behavior, but if the user sets the
|
|
52
|
+
// useDefaultForNullAndUndefined flag, use it instead
|
|
53
|
+
// I support both modes because some people consider "null/undefined" a valid value for
|
|
54
|
+
// numbers and others don't!
|
|
55
|
+
|
|
56
|
+
public static safeNumber(input: any, ifNotNumber: number = null, useDefaultForNullAndUndefined?: boolean): number {
|
|
57
|
+
const opts: Partial<SafeNumberOptions> = {
|
|
58
|
+
ifNotNumber: ifNotNumber,
|
|
59
|
+
returnValueForNull: useDefaultForNullAndUndefined ? ifNotNumber : null,
|
|
60
|
+
returnValueForUndefined: useDefaultForNullAndUndefined ? ifNotNumber : undefined,
|
|
61
|
+
};
|
|
62
|
+
if (useDefaultForNullAndUndefined === undefined) {
|
|
63
|
+
opts.returnValueForUndefined = null; // For backwards compatibility - used to return null for both null and undefined
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return NumberRatchet.safeNumberOpt(input, opts);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public static safeNumberOpt(input: any, optionPart?: Partial<SafeNumberOptions>): number {
|
|
70
|
+
let rval: number = undefined;
|
|
71
|
+
const opts: SafeNumberOptions = Object.assign({}, NumberRatchet.DEFAULT_SAFE_NUMBER_OPTIONS, optionPart || {});
|
|
72
|
+
if (input === null) {
|
|
73
|
+
rval = opts.returnValueForNull;
|
|
74
|
+
} else if (input === undefined) {
|
|
75
|
+
rval = opts.returnValueForUndefined;
|
|
76
|
+
} else {
|
|
77
|
+
const type: string = typeof input;
|
|
78
|
+
if (type == 'number') {
|
|
79
|
+
rval = input;
|
|
80
|
+
} else if (type == 'string') {
|
|
81
|
+
let test: string = input.trim();
|
|
82
|
+
if (test.length === 0) {
|
|
83
|
+
rval = opts.ifNotNumber;
|
|
84
|
+
} else {
|
|
85
|
+
if (opts.preParseCharacterMapping && Object.keys(opts.preParseCharacterMapping).length > 0) {
|
|
86
|
+
let t2 = '';
|
|
87
|
+
for (let i = 0; i < test.length; i++) {
|
|
88
|
+
const cr: string = test.charAt(i);
|
|
89
|
+
t2 += opts.preParseCharacterMapping[cr] === undefined ? cr : opts.preParseCharacterMapping[cr];
|
|
90
|
+
}
|
|
91
|
+
test = t2;
|
|
92
|
+
}
|
|
93
|
+
rval = Number.parseFloat(test);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
Logger.warn('Value is of type %s, returning default', type);
|
|
97
|
+
rval = opts.ifNotNumber;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (isNaN(rval)) {
|
|
101
|
+
Logger.debug('Parsed string to NaN - using NaN value from param');
|
|
102
|
+
rval = opts.ifNotNumber;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return rval;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public static numberCSVToList(inputCSV: string): number[] {
|
|
110
|
+
let rval: number[] = null;
|
|
111
|
+
if (inputCSV) {
|
|
112
|
+
rval = inputCSV.split(',').map((s) => {
|
|
113
|
+
return NumberRatchet.safeNumber(s.trim());
|
|
114
|
+
});
|
|
115
|
+
rval = rval.filter((r) => typeof r === 'number' && !isNaN(r));
|
|
116
|
+
}
|
|
117
|
+
return rval;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
public static fitToWindow(val: number, b1: number, b2: number): number {
|
|
121
|
+
let rval: number = val;
|
|
122
|
+
if (val === null || b1 === null || b2 === null || b1 < 0 || b2 < 0 || val < 0) {
|
|
123
|
+
throw new Error('All values must be non-null and larger than 0');
|
|
124
|
+
}
|
|
125
|
+
const low: number = Math.min(b1, b2);
|
|
126
|
+
const high: number = Math.max(b1, b2);
|
|
127
|
+
const windowSize: number = high - low;
|
|
128
|
+
if (high === low) {
|
|
129
|
+
// If the bounds are the same the answer is always the same
|
|
130
|
+
rval = high;
|
|
131
|
+
} else {
|
|
132
|
+
// Not super efficient (I should use modulo here) but works for the moment
|
|
133
|
+
while (rval < low) {
|
|
134
|
+
rval += windowSize;
|
|
135
|
+
}
|
|
136
|
+
while (rval > high) {
|
|
137
|
+
rval -= windowSize;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return rval;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
public static groupNumbersIntoContiguousRanges(inputData: number[], minRangeSize = 3): SinglesAndRanges {
|
|
145
|
+
RequireRatchet.notNullOrUndefined(inputData);
|
|
146
|
+
const input: number[] = Object.assign([], inputData);
|
|
147
|
+
input.sort((a, b) => a - b);
|
|
148
|
+
|
|
149
|
+
const singles: number[] = [];
|
|
150
|
+
const ranges: NumberRange[] = [];
|
|
151
|
+
let start = 0;
|
|
152
|
+
for (let i = 1; i < input.length; i++) {
|
|
153
|
+
if (input[i] === input[i - 1] + 1) {
|
|
154
|
+
// Just advance
|
|
155
|
+
} else {
|
|
156
|
+
// End of sequence, either single or range
|
|
157
|
+
if (start === i - 1) {
|
|
158
|
+
singles.push(input[i - 1]);
|
|
159
|
+
} else {
|
|
160
|
+
const rangeSize: number = i - start;
|
|
161
|
+
if (rangeSize < minRangeSize) {
|
|
162
|
+
for (let j = start; j < i; j++) {
|
|
163
|
+
singles.push(input[j]);
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
ranges.push({
|
|
167
|
+
min: input[start],
|
|
168
|
+
max: input[i - 1],
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Either way, advance start
|
|
173
|
+
start = i;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
singles: singles,
|
|
178
|
+
ranges: ranges,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Given N items and M buckets, return how many items in each bucket, as even as
|
|
183
|
+
// possible
|
|
184
|
+
public static distributeItemsEvenly(items: number, buckets: number): number[] {
|
|
185
|
+
RequireRatchet.notNullOrUndefined(items, 'items');
|
|
186
|
+
RequireRatchet.notNullOrUndefined(buckets, 'buckets');
|
|
187
|
+
RequireRatchet.true(items > 0 && items === Math.floor(items), 'Items integer larger than 0');
|
|
188
|
+
RequireRatchet.true(buckets > 0 && buckets === Math.floor(buckets), 'Buckets integer larger than 0');
|
|
189
|
+
|
|
190
|
+
const offset: number = buckets / items;
|
|
191
|
+
const rval: number[] = [];
|
|
192
|
+
for (let i = 0; i < buckets; i++) {
|
|
193
|
+
rval.push(0);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let loc = 0;
|
|
197
|
+
let rem: number = items;
|
|
198
|
+
|
|
199
|
+
while (rem > 0) {
|
|
200
|
+
rval[Math.floor(loc) % buckets]++;
|
|
201
|
+
rem--;
|
|
202
|
+
loc += offset;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return rval;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Just creates an array of numbers between 2 bounds
|
|
209
|
+
public static createRange(minInclusive: number, maxExclusive: number, step = 1): number[] {
|
|
210
|
+
const rval: number[] = [];
|
|
211
|
+
let val: number = minInclusive;
|
|
212
|
+
while (val < maxExclusive) {
|
|
213
|
+
rval.push(val);
|
|
214
|
+
val += step;
|
|
215
|
+
}
|
|
216
|
+
return rval;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
public static percentFormatNumberRange(range: Range<number>): string {
|
|
220
|
+
let rval: string = 'N/A';
|
|
221
|
+
if (range) {
|
|
222
|
+
rval = range.low ? NumberRatchet.pctFormatted(range.low) : ' ^ ';
|
|
223
|
+
rval += ' - ';
|
|
224
|
+
rval += range.high ? NumberRatchet.pctFormatted(range.high) : ' ^ ';
|
|
225
|
+
}
|
|
226
|
+
return rval;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
public static pctFormatted(pct: number, fractionDigits: number = 2): string {
|
|
230
|
+
const rval: string =
|
|
231
|
+
pct === null || pct === undefined
|
|
232
|
+
? 'Null'
|
|
233
|
+
: pct.toLocaleString('en-US', { minimumFractionDigits: fractionDigits, maximumFractionDigits: fractionDigits }) + '%';
|
|
234
|
+
return rval;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export interface SinglesAndRanges {
|
|
239
|
+
singles: number[];
|
|
240
|
+
ranges: NumberRange[];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export interface NumberRange {
|
|
244
|
+
min: number;
|
|
245
|
+
max: number;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export interface SafeNumberOptions {
|
|
249
|
+
ifNotNumber: number; // Value to return if the passed value was not a number
|
|
250
|
+
returnValueForNull: number;
|
|
251
|
+
returnValueForUndefined: number;
|
|
252
|
+
preParseCharacterMapping: Record<string, string>; // Replaces any characters on the left side with the right - used to remove the thousand separators and convert for euro currencies
|
|
253
|
+
}
|