/ lib / util.js
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