fix: fix recurring charging profile translation to current time interval
[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
265 * @returns delay in milliseconds
266 */
267export const exponentialDelay = (retryNumber = 0, maxDelayRatio = 0.2): number => {
268 const delay = Math.pow(2, retryNumber) * 100;
269 const randomSum = delay * maxDelayRatio * secureRandom(); // 0-20% of the delay
270 return delay + randomSum;
271};
272
273const isPromisePending = (promise: Promise<unknown>): boolean => {
274 return inspect(promise).includes('pending');
275};
276
277export const promiseWithTimeout = async <T>(
278 promise: Promise<T>,
279 timeoutMs: number,
280 timeoutError: Error,
281 timeoutCallback: () => void = () => {
282 /* This is intentional */
5edd8ba0 283 },
9bf0ef23
JB
284): Promise<T> => {
285 // Create a timeout promise that rejects in timeout milliseconds
286 const timeoutPromise = new Promise<never>((_, reject) => {
287 setTimeout(() => {
288 if (isPromisePending(promise)) {
289 timeoutCallback();
290 // FIXME: The original promise shall be canceled
5e3cb728 291 }
9bf0ef23
JB
292 reject(timeoutError);
293 }, timeoutMs);
294 });
295
296 // Returns a race between timeout promise and the passed promise
297 return Promise.race<T>([promise, timeoutPromise]);
298};
299
300/**
301 * Generates a cryptographically secure random number in the [0,1[ range
302 *
303 * @returns
304 */
305export const secureRandom = (): number => {
306 return randomBytes(4).readUInt32LE() / 0x100000000;
307};
308
309export const JSONStringifyWithMapSupport = (
310 obj: Record<string, unknown> | Record<string, unknown>[] | Map<unknown, unknown>,
5edd8ba0 311 space?: number,
9bf0ef23
JB
312): string => {
313 return JSON.stringify(
314 obj,
58ddf341 315 (_, value: Record<string, unknown>) => {
9bf0ef23
JB
316 if (value instanceof Map) {
317 return {
318 dataType: 'Map',
319 value: [...value],
320 };
321 }
322 return value;
323 },
5edd8ba0 324 space,
9bf0ef23
JB
325 );
326};
327
328/**
329 * Converts websocket error code to human readable string message
330 *
331 * @param code - websocket error code
332 * @returns human readable string message
333 */
334export const getWebSocketCloseEventStatusString = (code: number): string => {
335 if (code >= 0 && code <= 999) {
336 return '(Unused)';
337 } else if (code >= 1016) {
338 if (code <= 1999) {
339 return '(For WebSocket standard)';
340 } else if (code <= 2999) {
341 return '(For WebSocket extensions)';
342 } else if (code <= 3999) {
343 return '(For libraries and frameworks)';
344 } else if (code <= 4999) {
345 return '(For applications)';
5e3cb728 346 }
5e3cb728 347 }
a37fc6dc
JB
348 if (
349 !isUndefined(
350 WebSocketCloseEventStatusString[code as keyof typeof WebSocketCloseEventStatusString],
351 )
352 ) {
353 return WebSocketCloseEventStatusString[code as keyof typeof WebSocketCloseEventStatusString];
9bf0ef23
JB
354 }
355 return '(Unknown)';
356};