feat: add support charging profile validity 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,
6 millisecondsToHours,
7 millisecondsToMinutes,
8 millisecondsToSeconds,
9 secondsToMilliseconds,
10} from 'date-fns';
088ee3c1
JB
11import clone from 'just-clone';
12
878e026c 13import { Constants } from './Constants';
da55bd34 14import { type TimestampedData, WebSocketCloseEventStatusString } from '../types';
5e3cb728 15
9bf0ef23
JB
16export const logPrefix = (prefixString = ''): string => {
17 return `${new Date().toLocaleString()}${prefixString}`;
18};
d5bd1c00 19
9bf0ef23
JB
20export const generateUUID = (): string => {
21 return randomUUID();
22};
147d0e0f 23
9bf0ef23
JB
24export 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(
5edd8ba0 26 uuid,
9bf0ef23
JB
27 );
28};
03eacbe5 29
9bf0ef23 30export const sleep = async (milliSeconds: number): Promise<NodeJS.Timeout> => {
474d4ffc 31 return new Promise<NodeJS.Timeout>((resolve) => setTimeout(resolve as () => void, milliSeconds));
9bf0ef23 32};
7dde0b73 33
9bf0ef23
JB
34export const formatDurationMilliSeconds = (duration: number): string => {
35 duration = convertToInt(duration);
a675e34b 36 const days = Math.floor(duration / (24 * 3600 * 1000));
f0c6601c
JB
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 );
a675e34b
JB
42 return formatDuration({
43 days,
44 hours,
45 minutes,
46 seconds,
47 });
9bf0ef23 48};
7dde0b73 49
9bf0ef23 50export const formatDurationSeconds = (duration: number): string => {
a675e34b 51 return formatDurationMilliSeconds(secondsToMilliseconds(duration));
9bf0ef23 52};
7dde0b73 53
9bf0ef23 54export const convertToDate = (
5edd8ba0 55 value: Date | string | number | null | undefined,
9bf0ef23
JB
56): Date | null | undefined => {
57 if (isNullOrUndefined(value)) {
58 return value as null | undefined;
7dde0b73 59 }
9bf0ef23
JB
60 if (value instanceof Date) {
61 return value;
a6e68f34 62 }
9bf0ef23 63 if (isString(value) || typeof value === 'number') {
e1d9a0f4 64 return new Date(value!);
560bcf5b 65 }
9bf0ef23
JB
66 return null;
67};
560bcf5b 68
9bf0ef23
JB
69export const convertToInt = (value: unknown): number => {
70 if (!value) {
71 return 0;
560bcf5b 72 }
9bf0ef23
JB
73 let changedValue: number = value as number;
74 if (Number.isSafeInteger(value)) {
75 return value as number;
6d3a11a0 76 }
9bf0ef23
JB
77 if (typeof value === 'number') {
78 return Math.trunc(value);
7dde0b73 79 }
9bf0ef23
JB
80 if (isString(value)) {
81 changedValue = parseInt(value as string);
9ccca265 82 }
9bf0ef23 83 if (isNaN(changedValue)) {
e1d9a0f4 84 // eslint-disable-next-line @typescript-eslint/no-base-to-string
9bf0ef23 85 throw new Error(`Cannot convert to integer: ${value.toString()}`);
dada83ec 86 }
9bf0ef23
JB
87 return changedValue;
88};
dada83ec 89
9bf0ef23
JB
90export const convertToFloat = (value: unknown): number => {
91 if (!value) {
92 return 0;
dada83ec 93 }
9bf0ef23
JB
94 let changedValue: number = value as number;
95 if (isString(value)) {
96 changedValue = parseFloat(value as string);
560bcf5b 97 }
9bf0ef23 98 if (isNaN(changedValue)) {
e1d9a0f4 99 // eslint-disable-next-line @typescript-eslint/no-base-to-string
9bf0ef23 100 throw new Error(`Cannot convert to float: ${value.toString()}`);
5a2a53cf 101 }
9bf0ef23
JB
102 return changedValue;
103};
5a2a53cf 104
9bf0ef23
JB
105export 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;
e7aeea18 115 }
c37528f1 116 }
9bf0ef23
JB
117 return result;
118};
119
120export 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
130export 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 */
147export 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
152export 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
159export const getRandomFloatFluctuatedRounded = (
160 staticValue: number,
161 fluctuationPercent: number,
5edd8ba0 162 scale = 2,
9bf0ef23
JB
163): number => {
164 if (fluctuationPercent < 0 || fluctuationPercent > 100) {
165 throw new RangeError(
5edd8ba0 166 `Fluctuation percent must be between 0 and 100. Actual value: ${fluctuationPercent}`,
fe791818
JB
167 );
168 }
9bf0ef23
JB
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),
5edd8ba0 176 scale,
9bf0ef23
JB
177 );
178};
179
da55bd34
JB
180export const extractTimeSeriesValues = (timeSeries: Array<TimestampedData>): number[] => {
181 return timeSeries.map((timeSeriesItem) => timeSeriesItem.value);
182};
183
9bf0ef23
JB
184export const isObject = (item: unknown): boolean => {
185 return (
186 isNullOrUndefined(item) === false && typeof item === 'object' && Array.isArray(item) === false
187 );
188};
189
190export const cloneObject = <T extends object>(object: T): T => {
191 return clone<T>(object);
192};
193
194export const hasOwnProp = (object: unknown, property: PropertyKey): boolean => {
195 return isObject(object) && Object.hasOwn(object as object, property);
196};
197
198export const isCFEnvironment = (): boolean => {
199 return !isNullOrUndefined(process.env.VCAP_APPLICATION);
200};
201
202export const isIterable = <T>(obj: T): boolean => {
a37fc6dc 203 return !isNullOrUndefined(obj) ? typeof obj[Symbol.iterator as keyof T] === 'function' : false;
9bf0ef23
JB
204};
205
206const isString = (value: unknown): boolean => {
207 return typeof value === 'string';
208};
209
210export const isEmptyString = (value: unknown): boolean => {
211 return isNullOrUndefined(value) || (isString(value) && (value as string).trim().length === 0);
212};
213
214export const isNotEmptyString = (value: unknown): boolean => {
215 return isString(value) && (value as string).trim().length > 0;
216};
217
218export const isUndefined = (value: unknown): boolean => {
219 return value === undefined;
220};
221
222export const isNullOrUndefined = (value: unknown): boolean => {
223 // eslint-disable-next-line eqeqeq, no-eq-null
224 return value == null;
225};
226
227export const isEmptyArray = (object: unknown): boolean => {
228 return Array.isArray(object) && object.length === 0;
229};
230
231export const isNotEmptyArray = (object: unknown): boolean => {
232 return Array.isArray(object) && object.length > 0;
233};
234
235export 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
247export 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 */
256export 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
262const isPromisePending = (promise: Promise<unknown>): boolean => {
263 return inspect(promise).includes('pending');
264};
265
266export const promiseWithTimeout = async <T>(
267 promise: Promise<T>,
268 timeoutMs: number,
269 timeoutError: Error,
270 timeoutCallback: () => void = () => {
271 /* This is intentional */
5edd8ba0 272 },
9bf0ef23
JB
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
5e3cb728 280 }
9bf0ef23
JB
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 */
294export const secureRandom = (): number => {
295 return randomBytes(4).readUInt32LE() / 0x100000000;
296};
297
298export const JSONStringifyWithMapSupport = (
299 obj: Record<string, unknown> | Record<string, unknown>[] | Map<unknown, unknown>,
5edd8ba0 300 space?: number,
9bf0ef23
JB
301): string => {
302 return JSON.stringify(
303 obj,
58ddf341 304 (_, value: Record<string, unknown>) => {
9bf0ef23
JB
305 if (value instanceof Map) {
306 return {
307 dataType: 'Map',
308 value: [...value],
309 };
310 }
311 return value;
312 },
5edd8ba0 313 space,
9bf0ef23
JB
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 */
323export 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)';
5e3cb728 335 }
5e3cb728 336 }
a37fc6dc
JB
337 if (
338 !isUndefined(
339 WebSocketCloseEventStatusString[code as keyof typeof WebSocketCloseEventStatusString],
340 )
341 ) {
342 return WebSocketCloseEventStatusString[code as keyof typeof WebSocketCloseEventStatusString];
9bf0ef23
JB
343 }
344 return '(Unknown)';
345};