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