8f63eb681940925a075c0ae6f29fc66f7781e4a3
[e-mobility-charging-stations-simulator.git] / src / utils / Utils.ts
1 import crypto from 'node:crypto';
2 import util from 'node:util';
3
4 import clone from 'just-clone';
5
6 import { WebSocketCloseEventStatusString } from '../types/WebSocket';
7
8 export default class Utils {
9 private constructor() {
10 // This is intentional
11 }
12
13 public static logPrefix(prefixString = ''): string {
14 return new Date().toLocaleString() + prefixString;
15 }
16
17 public static generateUUID(): string {
18 return crypto.randomUUID();
19 }
20
21 public static validateUUID(uuid: string): boolean {
22 return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
23 uuid
24 );
25 }
26
27 public static async sleep(milliSeconds: number): Promise<NodeJS.Timeout> {
28 return new Promise((resolve) => setTimeout(resolve as () => void, milliSeconds));
29 }
30
31 public static formatDurationMilliSeconds(duration: number): string {
32 duration = Utils.convertToInt(duration);
33 const hours = Math.floor(duration / (3600 * 1000));
34 const minutes = Math.floor((duration / 1000 - hours * 3600) / 60);
35 const seconds = duration / 1000 - hours * 3600 - minutes * 60;
36 let hoursStr = hours.toString();
37 let minutesStr = minutes.toString();
38 let secondsStr = seconds.toString();
39
40 if (hours < 10) {
41 hoursStr = '0' + hours.toString();
42 }
43 if (minutes < 10) {
44 minutesStr = '0' + minutes.toString();
45 }
46 if (seconds < 10) {
47 secondsStr = '0' + seconds.toString();
48 }
49 return hoursStr + ':' + minutesStr + ':' + secondsStr.substring(0, 6);
50 }
51
52 public static formatDurationSeconds(duration: number): string {
53 return Utils.formatDurationMilliSeconds(duration * 1000);
54 }
55
56 public static convertToDate(
57 value: Date | string | number | null | undefined
58 ): Date | null | undefined {
59 if (Utils.isNullOrUndefined(value)) {
60 return value as null | undefined;
61 }
62 if (value instanceof Date) {
63 return value;
64 }
65 if (Utils.isString(value) || typeof value === 'number') {
66 return new Date(value);
67 }
68 return null;
69 }
70
71 public static convertToInt(value: unknown): number {
72 if (!value) {
73 return 0;
74 }
75 let changedValue: number = value as number;
76 if (Number.isSafeInteger(value)) {
77 return value as number;
78 }
79 if (typeof value === 'number') {
80 return Math.trunc(value);
81 }
82 if (Utils.isString(value)) {
83 changedValue = parseInt(value as string);
84 }
85 if (isNaN(changedValue)) {
86 throw new Error(`Cannot convert to integer: ${value.toString()}`);
87 }
88 return changedValue;
89 }
90
91 public static convertToFloat(value: unknown): number {
92 if (!value) {
93 return 0;
94 }
95 let changedValue: number = value as number;
96 if (Utils.isString(value)) {
97 changedValue = parseFloat(value as string);
98 }
99 if (isNaN(changedValue)) {
100 throw new Error(`Cannot convert to float: ${value.toString()}`);
101 }
102 return changedValue;
103 }
104
105 public static convertToBoolean(value: unknown): boolean {
106 let result = false;
107 if (value) {
108 // Check the type
109 if (typeof value === 'boolean') {
110 return value;
111 } else if (
112 Utils.isString(value) &&
113 ((value as string).toLowerCase() === 'true' || value === '1')
114 ) {
115 result = true;
116 } else if (typeof value === 'number' && value === 1) {
117 result = true;
118 }
119 }
120 return result;
121 }
122
123 public static getRandomFloat(max = Number.MAX_VALUE, min = 0, negative = false): number {
124 if (max < min || max < 0 || min < 0) {
125 throw new RangeError('Invalid interval');
126 }
127 const randomPositiveFloat = crypto.randomBytes(4).readUInt32LE() / 0xffffffff;
128 const sign = negative && randomPositiveFloat < 0.5 ? -1 : 1;
129 return sign * (randomPositiveFloat * (max - min) + min);
130 }
131
132 public static getRandomInteger(max = Number.MAX_SAFE_INTEGER, min = 0): number {
133 if (max < min || max < 0 || min < 0) {
134 throw new RangeError('Invalid interval');
135 }
136 max = Math.floor(max);
137 if (!Utils.isNullOrUndefined(min) && min !== 0) {
138 min = Math.ceil(min);
139 return Math.floor(Utils.secureRandom() * (max - min + 1)) + min;
140 }
141 return Math.floor(Utils.secureRandom() * (max + 1));
142 }
143
144 public static roundTo(numberValue: number, scale: number): number {
145 const roundPower = Math.pow(10, scale);
146 return Math.round(numberValue * roundPower) / roundPower;
147 }
148
149 public static truncTo(numberValue: number, scale: number): number {
150 const truncPower = Math.pow(10, scale);
151 return Math.trunc(numberValue * truncPower) / truncPower;
152 }
153
154 public static getRandomFloatRounded(max = Number.MAX_VALUE, min = 0, scale = 2): number {
155 if (min) {
156 return Utils.roundTo(Utils.getRandomFloat(max, min), scale);
157 }
158 return Utils.roundTo(Utils.getRandomFloat(max), scale);
159 }
160
161 public static getRandomFloatFluctuatedRounded(
162 staticValue: number,
163 fluctuationPercent: number,
164 scale = 2
165 ): number {
166 if (fluctuationPercent === 0) {
167 return Utils.roundTo(staticValue, scale);
168 }
169 const fluctuationRatio = fluctuationPercent / 100;
170 return Utils.getRandomFloatRounded(
171 staticValue * (1 + fluctuationRatio),
172 staticValue * (1 - fluctuationRatio),
173 scale
174 );
175 }
176
177 public static isObject(item: unknown): boolean {
178 return (
179 Utils.isNullOrUndefined(item) === false &&
180 typeof item === 'object' &&
181 Array.isArray(item) === false
182 );
183 }
184
185 public static cloneObject<T extends object>(object: T): T {
186 return clone<T>(object);
187 }
188
189 public static isIterable<T>(obj: T): boolean {
190 return obj ? typeof obj[Symbol.iterator] === 'function' : false;
191 }
192
193 public static isString(value: unknown): boolean {
194 return typeof value === 'string';
195 }
196
197 public static isEmptyString(value: unknown): boolean {
198 return Utils.isString(value) && (value as string).trim().length === 0;
199 }
200
201 public static isUndefined(value: unknown): boolean {
202 return typeof value === 'undefined';
203 }
204
205 public static isNullOrUndefined(value: unknown): boolean {
206 // eslint-disable-next-line eqeqeq, no-eq-null
207 return value == null ? true : false;
208 }
209
210 public static isEmptyArray(object: unknown | unknown[]): boolean {
211 if (!Array.isArray(object)) {
212 return true;
213 }
214 if (object.length > 0) {
215 return false;
216 }
217 return true;
218 }
219
220 public static isEmptyObject(obj: object): boolean {
221 if (obj?.constructor !== Object) {
222 return false;
223 }
224 // Iterates over the keys of an object, if
225 // any exist, return false.
226 for (const _ in obj) {
227 return false;
228 }
229 return true;
230 }
231
232 public static insertAt = (str: string, subStr: string, pos: number): string =>
233 `${str.slice(0, pos)}${subStr}${str.slice(pos)}`;
234
235 /**
236 * @param retryNumber - the number of retries that have already been attempted
237 * @returns delay in milliseconds
238 */
239 public static exponentialDelay(retryNumber = 0): number {
240 const delay = Math.pow(2, retryNumber) * 100;
241 const randomSum = delay * 0.2 * Utils.secureRandom(); // 0-20% of the delay
242 return delay + randomSum;
243 }
244
245 public static isPromisePending(promise: Promise<unknown>): boolean {
246 return util.inspect(promise).includes('pending');
247 }
248
249 public static async promiseWithTimeout<T>(
250 promise: Promise<T>,
251 timeoutMs: number,
252 timeoutError: Error,
253 timeoutCallback: () => void = () => {
254 /* This is intentional */
255 }
256 ): Promise<T> {
257 // Create a timeout promise that rejects in timeout milliseconds
258 const timeoutPromise = new Promise<never>((_, reject) => {
259 setTimeout(() => {
260 if (Utils.isPromisePending(promise)) {
261 timeoutCallback();
262 }
263 reject(timeoutError);
264 }, timeoutMs);
265 });
266
267 // Returns a race between timeout promise and the passed promise
268 return Promise.race<T>([promise, timeoutPromise]);
269 }
270
271 /**
272 * Generate a cryptographically secure random number in the [0,1[ range
273 *
274 * @returns
275 */
276 public static secureRandom(): number {
277 return crypto.randomBytes(4).readUInt32LE() / 0x100000000;
278 }
279
280 public static JSONStringifyWithMapSupport(
281 obj: Record<string, unknown> | Record<string, unknown>[] | Map<unknown, unknown>,
282 space?: number
283 ): string {
284 return JSON.stringify(
285 obj,
286 (key, value: Record<string, unknown>) => {
287 if (value instanceof Map) {
288 return {
289 dataType: 'Map',
290 value: [...value],
291 };
292 }
293 return value;
294 },
295 space
296 );
297 }
298
299 /**
300 * Convert websocket error code to human readable string message
301 *
302 * @param code - websocket error code
303 * @returns human readable string message
304 */
305 public static getWebSocketCloseEventStatusString(code: number): string {
306 if (code >= 0 && code <= 999) {
307 return '(Unused)';
308 } else if (code >= 1016) {
309 if (code <= 1999) {
310 return '(For WebSocket standard)';
311 } else if (code <= 2999) {
312 return '(For WebSocket extensions)';
313 } else if (code <= 3999) {
314 return '(For libraries and frameworks)';
315 } else if (code <= 4999) {
316 return '(For applications)';
317 }
318 }
319 if (!Utils.isUndefined(WebSocketCloseEventStatusString[code])) {
320 return WebSocketCloseEventStatusString[code] as string;
321 }
322 return '(Unknown)';
323 }
324 }