refactor: cleanup log messages
[e-mobility-charging-stations-simulator.git] / src / utils / Utils.ts
CommitLineData
d972af76
JB
1import { randomBytes, randomInt, randomUUID } from 'node:crypto';
2import { inspect } from 'node:util';
8114d10e 3
f0c6601c
JB
4import {
5 formatDuration,
b5c19509 6 isDate,
f0c6601c
JB
7 millisecondsToHours,
8 millisecondsToMinutes,
9 millisecondsToSeconds,
10 secondsToMilliseconds,
11} from 'date-fns';
088ee3c1
JB
12import clone from 'just-clone';
13
878e026c 14import { Constants } from './Constants';
da55bd34 15import { type TimestampedData, WebSocketCloseEventStatusString } from '../types';
5e3cb728 16
9bf0ef23
JB
17export const logPrefix = (prefixString = ''): string => {
18 return `${new Date().toLocaleString()}${prefixString}`;
19};
d5bd1c00 20
9bf0ef23
JB
21export const generateUUID = (): string => {
22 return randomUUID();
23};
147d0e0f 24
9bf0ef23
JB
25export const validateUUID = (uuid: string): boolean => {
26 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(
5edd8ba0 27 uuid,
9bf0ef23
JB
28 );
29};
03eacbe5 30
9bf0ef23 31export const sleep = async (milliSeconds: number): Promise<NodeJS.Timeout> => {
474d4ffc 32 return new Promise<NodeJS.Timeout>((resolve) => setTimeout(resolve as () => void, milliSeconds));
9bf0ef23 33};
7dde0b73 34
9bf0ef23
JB
35export const formatDurationMilliSeconds = (duration: number): string => {
36 duration = convertToInt(duration);
a675e34b 37 const days = Math.floor(duration / (24 * 3600 * 1000));
f0c6601c
JB
38 const hours = Math.floor(millisecondsToHours(duration) - days * 24);
39 const minutes = Math.floor(millisecondsToMinutes(duration) - days * 24 * 60 - hours * 60);
40 const seconds = Math.floor(
41 millisecondsToSeconds(duration) - days * 24 * 3600 - hours * 3600 - minutes * 60,
42 );
a675e34b
JB
43 return formatDuration({
44 days,
45 hours,
46 minutes,
47 seconds,
48 });
9bf0ef23 49};
7dde0b73 50
9bf0ef23 51export const formatDurationSeconds = (duration: number): string => {
a675e34b 52 return formatDurationMilliSeconds(secondsToMilliseconds(duration));
9bf0ef23 53};
7dde0b73 54
b5c19509
JB
55// More efficient date validation function than the one provided by date-fns
56export const isValidDate = (date: unknown): boolean => {
57 if (typeof date === 'number') {
58 return !isNaN(date);
59 } else if (isDate(date)) {
60 return !isNaN((date as Date).getTime());
61 }
62 return false;
63};
64
9bf0ef23 65export const convertToDate = (
5edd8ba0 66 value: Date | string | number | null | undefined,
9bf0ef23
JB
67): Date | null | undefined => {
68 if (isNullOrUndefined(value)) {
69 return value as null | undefined;
7dde0b73 70 }
9bf0ef23
JB
71 if (value instanceof Date) {
72 return value;
a6e68f34 73 }
9bf0ef23 74 if (isString(value) || typeof value === 'number') {
e1d9a0f4 75 return new Date(value!);
560bcf5b 76 }
9bf0ef23
JB
77 return null;
78};
560bcf5b 79
9bf0ef23
JB
80export const convertToInt = (value: unknown): number => {
81 if (!value) {
82 return 0;
560bcf5b 83 }
9bf0ef23
JB
84 let changedValue: number = value as number;
85 if (Number.isSafeInteger(value)) {
86 return value as number;
6d3a11a0 87 }
9bf0ef23
JB
88 if (typeof value === 'number') {
89 return Math.trunc(value);
7dde0b73 90 }
9bf0ef23
JB
91 if (isString(value)) {
92 changedValue = parseInt(value as string);
9ccca265 93 }
9bf0ef23 94 if (isNaN(changedValue)) {
e1d9a0f4 95 // eslint-disable-next-line @typescript-eslint/no-base-to-string
9bf0ef23 96 throw new Error(`Cannot convert to integer: ${value.toString()}`);
dada83ec 97 }
9bf0ef23
JB
98 return changedValue;
99};
dada83ec 100
9bf0ef23
JB
101export const convertToFloat = (value: unknown): number => {
102 if (!value) {
103 return 0;
dada83ec 104 }
9bf0ef23
JB
105 let changedValue: number = value as number;
106 if (isString(value)) {
107 changedValue = parseFloat(value as string);
560bcf5b 108 }
9bf0ef23 109 if (isNaN(changedValue)) {
e1d9a0f4 110 // eslint-disable-next-line @typescript-eslint/no-base-to-string
9bf0ef23 111 throw new Error(`Cannot convert to float: ${value.toString()}`);
5a2a53cf 112 }
9bf0ef23
JB
113 return changedValue;
114};
5a2a53cf 115
9bf0ef23
JB
116export const convertToBoolean = (value: unknown): boolean => {
117 let result = false;
118 if (value) {
119 // Check the type
120 if (typeof value === 'boolean') {
121 return value;
122 } else if (isString(value) && ((value as string).toLowerCase() === 'true' || value === '1')) {
123 result = true;
124 } else if (typeof value === 'number' && value === 1) {
125 result = true;
e7aeea18 126 }
c37528f1 127 }
9bf0ef23
JB
128 return result;
129};
130
131export const getRandomFloat = (max = Number.MAX_VALUE, min = 0): number => {
132 if (max < min) {
133 throw new RangeError('Invalid interval');
134 }
135 if (max - min === Infinity) {
136 throw new RangeError('Invalid interval');
137 }
138 return (randomBytes(4).readUInt32LE() / 0xffffffff) * (max - min) + min;
139};
140
141export const getRandomInteger = (max = Constants.MAX_RANDOM_INTEGER, min = 0): number => {
142 max = Math.floor(max);
143 if (!isNullOrUndefined(min) && min !== 0) {
144 min = Math.ceil(min);
145 return Math.floor(randomInt(min, max + 1));
146 }
147 return Math.floor(randomInt(max + 1));
148};
149
150/**
151 * Rounds the given number to the given scale.
152 * The rounding is done using the "round half away from zero" method.
153 *
154 * @param numberValue - The number to round.
155 * @param scale - The scale to round to.
156 * @returns The rounded number.
157 */
158export const roundTo = (numberValue: number, scale: number): number => {
159 const roundPower = Math.pow(10, scale);
160 return Math.round(numberValue * roundPower * (1 + Number.EPSILON)) / roundPower;
161};
162
163export const getRandomFloatRounded = (max = Number.MAX_VALUE, min = 0, scale = 2): number => {
164 if (min) {
165 return roundTo(getRandomFloat(max, min), scale);
166 }
167 return roundTo(getRandomFloat(max), scale);
168};
169
170export const getRandomFloatFluctuatedRounded = (
171 staticValue: number,
172 fluctuationPercent: number,
5edd8ba0 173 scale = 2,
9bf0ef23
JB
174): number => {
175 if (fluctuationPercent < 0 || fluctuationPercent > 100) {
176 throw new RangeError(
5edd8ba0 177 `Fluctuation percent must be between 0 and 100. Actual value: ${fluctuationPercent}`,
fe791818
JB
178 );
179 }
9bf0ef23
JB
180 if (fluctuationPercent === 0) {
181 return roundTo(staticValue, scale);
182 }
183 const fluctuationRatio = fluctuationPercent / 100;
184 return getRandomFloatRounded(
185 staticValue * (1 + fluctuationRatio),
186 staticValue * (1 - fluctuationRatio),
5edd8ba0 187 scale,
9bf0ef23
JB
188 );
189};
190
da55bd34
JB
191export const extractTimeSeriesValues = (timeSeries: Array<TimestampedData>): number[] => {
192 return timeSeries.map((timeSeriesItem) => timeSeriesItem.value);
193};
194
9bf0ef23
JB
195export const isObject = (item: unknown): boolean => {
196 return (
197 isNullOrUndefined(item) === false && typeof item === 'object' && Array.isArray(item) === false
198 );
199};
200
201export const cloneObject = <T extends object>(object: T): T => {
202 return clone<T>(object);
203};
204
205export const hasOwnProp = (object: unknown, property: PropertyKey): boolean => {
206 return isObject(object) && Object.hasOwn(object as object, property);
207};
208
209export const isCFEnvironment = (): boolean => {
210 return !isNullOrUndefined(process.env.VCAP_APPLICATION);
211};
212
213export const isIterable = <T>(obj: T): boolean => {
a37fc6dc 214 return !isNullOrUndefined(obj) ? typeof obj[Symbol.iterator as keyof T] === 'function' : false;
9bf0ef23
JB
215};
216
217const isString = (value: unknown): boolean => {
218 return typeof value === 'string';
219};
220
221export const isEmptyString = (value: unknown): boolean => {
222 return isNullOrUndefined(value) || (isString(value) && (value as string).trim().length === 0);
223};
224
225export const isNotEmptyString = (value: unknown): boolean => {
226 return isString(value) && (value as string).trim().length > 0;
227};
228
229export const isUndefined = (value: unknown): boolean => {
230 return value === undefined;
231};
232
233export const isNullOrUndefined = (value: unknown): boolean => {
234 // eslint-disable-next-line eqeqeq, no-eq-null
235 return value == null;
236};
237
238export const isEmptyArray = (object: unknown): boolean => {
239 return Array.isArray(object) && object.length === 0;
240};
241
242export const isNotEmptyArray = (object: unknown): boolean => {
243 return Array.isArray(object) && object.length > 0;
244};
245
246export const isEmptyObject = (obj: object): boolean => {
247 if (obj?.constructor !== Object) {
248 return false;
249 }
250 // Iterates over the keys of an object, if
251 // any exist, return false.
252 for (const _ in obj) {
253 return false;
254 }
255 return true;
256};
257
258export const insertAt = (str: string, subStr: string, pos: number): string =>
259 `${str.slice(0, pos)}${subStr}${str.slice(pos)}`;
260
261/**
262 * Computes the retry delay in milliseconds using an exponential backoff algorithm.
263 *
264 * @param retryNumber - the number of retries that have already been attempted
d467756c 265 * @param maxDelayRatio - the maximum ratio of the delay that can be randomized
9bf0ef23
JB
266 * @returns delay in milliseconds
267 */
268export const exponentialDelay = (retryNumber = 0, maxDelayRatio = 0.2): number => {
269 const delay = Math.pow(2, retryNumber) * 100;
d467756c 270 const randomSum = delay * maxDelayRatio * secureRandom(); // 0-(maxDelayRatio*100)% of the delay
9bf0ef23
JB
271 return delay + randomSum;
272};
273
274const isPromisePending = (promise: Promise<unknown>): boolean => {
275 return inspect(promise).includes('pending');
276};
277
278export const promiseWithTimeout = async <T>(
279 promise: Promise<T>,
280 timeoutMs: number,
281 timeoutError: Error,
282 timeoutCallback: () => void = () => {
283 /* This is intentional */
5edd8ba0 284 },
9bf0ef23
JB
285): Promise<T> => {
286 // Create a timeout promise that rejects in timeout milliseconds
287 const timeoutPromise = new Promise<never>((_, reject) => {
288 setTimeout(() => {
289 if (isPromisePending(promise)) {
290 timeoutCallback();
291 // FIXME: The original promise shall be canceled
5e3cb728 292 }
9bf0ef23
JB
293 reject(timeoutError);
294 }, timeoutMs);
295 });
296
297 // Returns a race between timeout promise and the passed promise
298 return Promise.race<T>([promise, timeoutPromise]);
299};
300
301/**
302 * Generates a cryptographically secure random number in the [0,1[ range
303 *
304 * @returns
305 */
306export const secureRandom = (): number => {
307 return randomBytes(4).readUInt32LE() / 0x100000000;
308};
309
310export const JSONStringifyWithMapSupport = (
311 obj: Record<string, unknown> | Record<string, unknown>[] | Map<unknown, unknown>,
5edd8ba0 312 space?: number,
9bf0ef23
JB
313): string => {
314 return JSON.stringify(
315 obj,
58ddf341 316 (_, value: Record<string, unknown>) => {
9bf0ef23
JB
317 if (value instanceof Map) {
318 return {
319 dataType: 'Map',
320 value: [...value],
321 };
322 }
323 return value;
324 },
5edd8ba0 325 space,
9bf0ef23
JB
326 );
327};
328
329/**
330 * Converts websocket error code to human readable string message
331 *
332 * @param code - websocket error code
333 * @returns human readable string message
334 */
335export const getWebSocketCloseEventStatusString = (code: number): string => {
336 if (code >= 0 && code <= 999) {
337 return '(Unused)';
338 } else if (code >= 1016) {
339 if (code <= 1999) {
340 return '(For WebSocket standard)';
341 } else if (code <= 2999) {
342 return '(For WebSocket extensions)';
343 } else if (code <= 3999) {
344 return '(For libraries and frameworks)';
345 } else if (code <= 4999) {
346 return '(For applications)';
5e3cb728 347 }
5e3cb728 348 }
a37fc6dc
JB
349 if (
350 !isUndefined(
351 WebSocketCloseEventStatusString[code as keyof typeof WebSocketCloseEventStatusString],
352 )
353 ) {
354 return WebSocketCloseEventStatusString[code as keyof typeof WebSocketCloseEventStatusString];
9bf0ef23
JB
355 }
356 return '(Unknown)';
357};
80c58041 358
991fb26b
JB
359export const isArraySorted = <T>(array: T[], compareFn: (a: T, b: T) => number): boolean => {
360 for (let index = 0; index < array.length - 1; ++index) {
361 if (compareFn(array[index], array[index + 1]) > 0) {
80c58041
JB
362 return false;
363 }
364 }
365 return true;
366};