util.js
1 /*! 2 * lib/util.js 3 * Copyright © 2019 – Katana Cryptographic Ltd. All Rights Reserved. 4 */ 5 6 7 /** 8 * @description Class providing utility functions as static methods 9 */ 10 const Util = { 11 12 /** 13 * @description Serialize a series of asynchronous calls to a function over a list of objects 14 * @template T 15 * @template R 16 * @param {Array<T>} list 17 * @param {IteratorFn} fn 18 * @returns {Promise<Array<R>>} 19 */ 20 seriesCall: async (list, fn) => { 21 const results = [] 22 23 for (const item of list) { 24 results.push(await fn(item, list)) 25 } 26 27 return results 28 }, 29 30 /** 31 * @description Execute parallel asynchronous calls to a function over a list of objects 32 * @template T 33 * @template R 34 * @param {Array<T>} list 35 * @param {IteratorFn} fn 36 * @returns {Promise<Array<R>>} 37 */ 38 parallelCall: (list, fn) => { 39 const operations = list.map((item) => { 40 return fn(item, list) 41 }) 42 return Promise.all(operations) 43 }, 44 45 /** 46 * @template T 47 * @template R 48 * @callback IteratorFn 49 * @param {T} item - Value of array element 50 * @param {Iterable<T>} iterable - Array of elements 51 * @returns {Promise<R>} 52 */ 53 54 /** 55 * @description Execute asynchronous call in a pool that preserves order of items 56 * @template T 57 * @template R 58 * @param {number} poolLimit - max number of promises running concurrently 59 * @param {Iterable<T>} iterable - array of items 60 * @param {IteratorFn<T, R>} iteratorFn 61 * @returns {Promise<R[]>} 62 */ 63 asyncPool: async (poolLimit, iterable, iteratorFn) => { 64 const ret = [] 65 const executing = new Set() 66 67 for (const item of iterable) { 68 const p = Promise.resolve().then(() => iteratorFn(item, iterable)) 69 ret.push(p) 70 executing.add(p) 71 // eslint-disable-next-line unicorn/consistent-function-scoping 72 const clean = () => executing.delete(p) 73 p.then(clean).catch(clean) 74 75 if (executing.size >= poolLimit) { 76 await Promise.race(executing) 77 } 78 } 79 80 return Promise.all(ret) 81 }, 82 83 /** 84 * @description Delay the call to a function 85 * @param {number} ms 86 * @returns {Promise<void>} 87 */ 88 delay: (ms) => { 89 return new Promise(resolve => { 90 setTimeout(() => resolve(), ms) 91 }) 92 }, 93 94 /** 95 * @description Splits a list into a list of lists each with maximum length LIMIT 96 * @template T 97 * @param {Array<T>} list 98 * @param {number} limit 99 * @returns {Array<Array<T>>} 100 */ 101 splitList: (list, limit) => { 102 const lists = [] 103 for (let i = 0; i < list.length; i += limit) 104 lists.push(list.slice(i, i + limit)) 105 return lists 106 }, 107 108 /** 109 * @description Check if a string is a valid hex value 110 * @param {string} hash 111 * @returns {boolean} 112 */ 113 isHashStr: (hash) => { 114 const hexRegExp = new RegExp(/^[\da-f]*$/, 'i') 115 return (typeof hash === 'string') ? hexRegExp.test(hash): false 116 }, 117 118 /** 119 * @description Check if a string is a well formed 256 bits hash 120 * @param {string} hash 121 * @returns {boolean} 122 */ 123 is256Hash: (hash) => { 124 return Util.isHashStr(hash) && hash.length === 64 125 }, 126 127 128 /** 129 * @description Sum an array of values 130 * @param {Array<number>} arr 131 * @returns {number} 132 */ 133 sum: (arr) => { 134 return arr.reduce((memo, val) => { 135 return memo + val 136 }, 0) 137 }, 138 139 /** 140 * @description Mean of an array of values 141 * @param {Array<number>} arr 142 * @returns {number} 143 */ 144 mean: (arr) => { 145 if (arr.length === 0) 146 return Number.NaN 147 return Util.sum(arr) / arr.length 148 }, 149 150 /** 151 * @description Compare 2 values (asc order) 152 * @param {number} a 153 * @param {number} b 154 * @returns {number} 155 */ 156 cmpAsc: (a, b) => { 157 return a - b 158 }, 159 160 /** 161 * @description Compare 2 values (desc order) 162 * @param {number} a 163 * @param {number} b 164 * @returns {number} 165 */ 166 cmpDesc: (a, b) => { 167 return b - a 168 }, 169 170 /** 171 * @description Median of an array of values 172 * @param {Array<number>} arr 173 * @param {boolean=} sorted 174 * @returns {number} 175 */ 176 median: (arr, sorted) => { 177 if (arr.length === 0) return Number.NaN 178 if (arr.length === 1) return arr[0] 179 180 if (!sorted) 181 arr.sort(Util.cmpAsc) 182 183 const midpoint = Math.floor(arr.length / 2) 184 185 return arr.length % 2 ? arr[midpoint] : (arr[midpoint - 1] + arr[midpoint]) / 2 186 }, 187 188 /** 189 * @description Median Absolute Deviation of an array of values 190 * @param {Array<number>} arr 191 * @param {boolean=} sorted 192 * @returns {number} 193 */ 194 mad: (arr, sorted) => { 195 const med = Util.median(arr, sorted) 196 // Deviations from the median 197 const dev = [] 198 for (let val of arr) 199 dev.push(Math.abs(val - med)) 200 return Util.median(dev) 201 }, 202 203 /** 204 * @description Quartiles of an array of values 205 * @param {Array<number>} arr 206 * @param {boolean=} sorted 207 * @returns {Array<number>} 208 */ 209 quartiles: (arr, sorted) => { 210 const q = [Number.NaN, Number.NaN, Number.NaN] 211 212 if (arr.length < 3) return q 213 214 if (!sorted) 215 arr.sort(Util.cmpAsc) 216 217 // Set median 218 q[1] = Util.median(arr, true) 219 220 const midpoint = Math.floor(arr.length / 2) 221 222 if (arr.length % 2) { 223 // Odd-length array 224 const mod4 = arr.length % 4 225 const n = Math.floor(arr.length / 4) 226 227 if (mod4 === 1) { 228 q[0] = (arr[n - 1] + 3 * arr[n]) / 4 229 q[2] = (3 * arr[3 * n] + arr[3 * n + 1]) / 4 230 } else if (mod4 === 3) { 231 q[0] = (3 * arr[n] + arr[n + 1]) / 4 232 q[2] = (arr[3 * n + 1] + 3 * arr[3 * n + 2]) / 4 233 } 234 235 } else { 236 // Even-length array. Slices are already sorted 237 q[0] = Util.median(arr.slice(0, midpoint), true) 238 q[2] = Util.median(arr.slice(midpoint), true) 239 } 240 241 return q 242 }, 243 244 /** 245 * @description Obtain the value of the PCT-th percentile, where PCT on [0,100] 246 * @param {Array<number>} arr 247 * @param {number} pct 248 * @param {boolean=} sorted 249 * @returns {number} 250 */ 251 percentile: (arr, pct, sorted) => { 252 if (arr.length < 2) return Number.NaN 253 254 if (!sorted) 255 arr.sort(Util.cmpAsc) 256 257 const N = arr.length 258 const p = pct / 100 259 260 let x // target rank 261 262 if (p <= 1 / (N + 1)) { 263 x = 1 264 } else if (p < N / (N + 1)) { 265 x = p * (N + 1) 266 } else { 267 x = N 268 } 269 270 // "Floor-x" 271 const fx = Math.floor(x) - 1 272 273 // "Mod-x" 274 const mx = x % 1 275 276 return fx + 1 >= N ? arr[fx] : arr[fx] + mx * (arr[fx + 1] - arr[fx]) 277 }, 278 279 /** 280 * @description Convert bytes to MB 281 * @param {number} bytes 282 * @returns {number} 283 */ 284 toMb: (bytes) => { 285 return Number((bytes / Util.MB).toFixed(0)) 286 }, 287 288 /** 289 * @description Convert a date to a unix timestamp 290 * @returns {number} 291 */ 292 unix: () => { 293 return Math.trunc(Date.now() / 1000) 294 }, 295 296 /** 297 * @description Convert a value to a padded string (10 chars) 298 * @param {number} v 299 * @returns {string} 300 */ 301 pad10: (v) => { 302 return (v < 10) ? `0${v}` : `${v}` 303 }, 304 305 /** 306 * @description Convert a value to a padded string (100 chars) 307 * @param {number} v 308 * @returns {string} 309 */ 310 pad100: (v) => { 311 if (v < 10) return `00${v}` 312 if (v < 100) return `0${v}` 313 return `${v}` 314 }, 315 316 /** 317 * @description Convert a value to a padded string (1000 chars) 318 * @param {number} v 319 * @returns {string} 320 */ 321 pad1000: (v) => { 322 if (v < 10) return `000${v}` 323 if (v < 100) return `00${v}` 324 if (v < 1000) return `0${v}` 325 return `${v}` 326 }, 327 328 /** 329 * @description Left pad 330 * @param {number} number 331 * @param {number} places 332 * @param {string=} fill 333 * @returns {string} 334 */ 335 leftPad: (number, places, fill) => { 336 number = Math.round(number) 337 places = Math.round(places) 338 fill = fill || ' ' 339 340 if (number < 0) return number 341 342 const mag = (number > 0) ? (Math.floor(Math.log10(number)) + 1) : 1 343 const parts = [] 344 345 for (let i = 0; i < (places - mag); i++) { 346 parts.push(fill) 347 } 348 349 parts.push(number) 350 return parts.join('') 351 }, 352 353 /** 354 * @description Display a time period, in seconds, as DDD:HH:MM:SS[.MS] 355 * @param {number} period 356 * @param {boolean} milliseconds 357 * @returns {string} 358 */ 359 timePeriod: (period, milliseconds) => { 360 const whole = Math.floor(period) 361 const ms = 1000 * (period - whole) 362 const s = whole % 60 363 const m = (whole >= 60) ? Math.floor(whole / 60) % 60 : 0 364 const h = (whole >= 3600) ? Math.floor(whole / 3600) % 24 : 0 365 const d = (whole >= 86400) ? Math.floor(whole / 86400) : 0 366 367 const parts = [Util.pad10(h), Util.pad10(m), Util.pad10(s)] 368 369 if (d > 0) 370 parts.splice(0, 0, Util.pad100(d)) 371 372 const str = parts.join(':') 373 374 return milliseconds ? `${str}.${Util.pad100(ms)}` : str 375 }, 376 377 /** 378 * @description Generate array of sequential numbers from start to stop 379 * @param {number} start 380 * @param {number} stop 381 * @param {number=} step 382 * @returns {number[]} 383 */ 384 range: (start, stop, step = 1) => { 385 return Array.from({ length: Math.ceil((stop - start) / step) }) 386 .fill(start) 387 .map((x, y) => x + y * step) 388 }, 389 /** 390 * @description Get item from an array that contains largest value defined by the callback function 391 * @template T 392 * @param {T[]} arr 393 * @param {(item: T) => number} fn 394 * @returns {T | null} 395 */ 396 maxBy: (arr, fn) => { 397 return arr.reduce((prev, current) => { 398 if (!prev) return current 399 return fn(current) > fn(prev) ? current : prev 400 }, null) 401 }, 402 /** 403 * 1Mb in bytes 404 */ 405 MB: 1024 * 1024, 406 } 407 408 409 export default Util