UNPKG

14.9 kBJavaScriptView Raw
1import { isInteger } from './number'
2import { isNumber } from './is'
3import { format } from './string'
4import { DimensionError } from '../error/DimensionError'
5import { IndexError } from '../error/IndexError'
6
7/**
8 * Calculate the size of a multi dimensional array.
9 * This function checks the size of the first entry, it does not validate
10 * whether all dimensions match. (use function `validate` for that)
11 * @param {Array} x
12 * @Return {Number[]} size
13 */
14export function arraySize (x) {
15 const s = []
16
17 while (Array.isArray(x)) {
18 s.push(x.length)
19 x = x[0]
20 }
21
22 return s
23}
24
25/**
26 * Recursively validate whether each element in a multi dimensional array
27 * has a size corresponding to the provided size array.
28 * @param {Array} array Array to be validated
29 * @param {number[]} size Array with the size of each dimension
30 * @param {number} dim Current dimension
31 * @throws DimensionError
32 * @private
33 */
34function _validate (array, size, dim) {
35 let i
36 const len = array.length
37
38 if (len !== size[dim]) {
39 throw new DimensionError(len, size[dim])
40 }
41
42 if (dim < size.length - 1) {
43 // recursively validate each child array
44 const dimNext = dim + 1
45 for (i = 0; i < len; i++) {
46 const child = array[i]
47 if (!Array.isArray(child)) {
48 throw new DimensionError(size.length - 1, size.length, '<')
49 }
50 _validate(array[i], size, dimNext)
51 }
52 } else {
53 // last dimension. none of the childs may be an array
54 for (i = 0; i < len; i++) {
55 if (Array.isArray(array[i])) {
56 throw new DimensionError(size.length + 1, size.length, '>')
57 }
58 }
59 }
60}
61
62/**
63 * Validate whether each element in a multi dimensional array has
64 * a size corresponding to the provided size array.
65 * @param {Array} array Array to be validated
66 * @param {number[]} size Array with the size of each dimension
67 * @throws DimensionError
68 */
69export function validate (array, size) {
70 const isScalar = (size.length === 0)
71 if (isScalar) {
72 // scalar
73 if (Array.isArray(array)) {
74 throw new DimensionError(array.length, 0)
75 }
76 } else {
77 // array
78 _validate(array, size, 0)
79 }
80}
81
82/**
83 * Test whether index is an integer number with index >= 0 and index < length
84 * when length is provided
85 * @param {number} index Zero-based index
86 * @param {number} [length] Length of the array
87 */
88export function validateIndex (index, length) {
89 if (!isNumber(index) || !isInteger(index)) {
90 throw new TypeError('Index must be an integer (value: ' + index + ')')
91 }
92 if (index < 0 || (typeof length === 'number' && index >= length)) {
93 throw new IndexError(index, length)
94 }
95}
96
97/**
98 * Resize a multi dimensional array. The resized array is returned.
99 * @param {Array} array Array to be resized
100 * @param {Array.<number>} size Array with the size of each dimension
101 * @param {*} [defaultValue=0] Value to be filled in in new entries,
102 * zero by default. Specify for example `null`,
103 * to clearly see entries that are not explicitly
104 * set.
105 * @return {Array} array The resized array
106 */
107export function resize (array, size, defaultValue) {
108 // TODO: add support for scalars, having size=[] ?
109
110 // check the type of the arguments
111 if (!Array.isArray(array) || !Array.isArray(size)) {
112 throw new TypeError('Array expected')
113 }
114 if (size.length === 0) {
115 throw new Error('Resizing to scalar is not supported')
116 }
117
118 // check whether size contains positive integers
119 size.forEach(function (value) {
120 if (!isNumber(value) || !isInteger(value) || value < 0) {
121 throw new TypeError('Invalid size, must contain positive integers ' +
122 '(size: ' + format(size) + ')')
123 }
124 })
125
126 // recursively resize the array
127 const _defaultValue = (defaultValue !== undefined) ? defaultValue : 0
128 _resize(array, size, 0, _defaultValue)
129
130 return array
131}
132
133/**
134 * Recursively resize a multi dimensional array
135 * @param {Array} array Array to be resized
136 * @param {number[]} size Array with the size of each dimension
137 * @param {number} dim Current dimension
138 * @param {*} [defaultValue] Value to be filled in in new entries,
139 * undefined by default.
140 * @private
141 */
142function _resize (array, size, dim, defaultValue) {
143 let i
144 let elem
145 const oldLen = array.length
146 const newLen = size[dim]
147 const minLen = Math.min(oldLen, newLen)
148
149 // apply new length
150 array.length = newLen
151
152 if (dim < size.length - 1) {
153 // non-last dimension
154 const dimNext = dim + 1
155
156 // resize existing child arrays
157 for (i = 0; i < minLen; i++) {
158 // resize child array
159 elem = array[i]
160 if (!Array.isArray(elem)) {
161 elem = [elem] // add a dimension
162 array[i] = elem
163 }
164 _resize(elem, size, dimNext, defaultValue)
165 }
166
167 // create new child arrays
168 for (i = minLen; i < newLen; i++) {
169 // get child array
170 elem = []
171 array[i] = elem
172
173 // resize new child array
174 _resize(elem, size, dimNext, defaultValue)
175 }
176 } else {
177 // last dimension
178
179 // remove dimensions of existing values
180 for (i = 0; i < minLen; i++) {
181 while (Array.isArray(array[i])) {
182 array[i] = array[i][0]
183 }
184 }
185
186 // fill new elements with the default value
187 for (i = minLen; i < newLen; i++) {
188 array[i] = defaultValue
189 }
190 }
191}
192
193/**
194 * Re-shape a multi dimensional array to fit the specified dimensions
195 * @param {Array} array Array to be reshaped
196 * @param {Array.<number>} sizes List of sizes for each dimension
197 * @returns {Array} Array whose data has been formatted to fit the
198 * specified dimensions
199 *
200 * @throws {DimensionError} If the product of the new dimension sizes does
201 * not equal that of the old ones
202 */
203export function reshape (array, sizes) {
204 const flatArray = flatten(array)
205 let newArray
206
207 function product (arr) {
208 return arr.reduce((prev, curr) => prev * curr)
209 }
210
211 if (!Array.isArray(array) || !Array.isArray(sizes)) {
212 throw new TypeError('Array expected')
213 }
214
215 if (sizes.length === 0) {
216 throw new DimensionError(0, product(arraySize(array)), '!=')
217 }
218
219 let totalSize = 1
220 for (let sizeIndex = 0; sizeIndex < sizes.length; sizeIndex++) {
221 totalSize *= sizes[sizeIndex]
222 }
223
224 if (flatArray.length !== totalSize) {
225 throw new DimensionError(
226 product(sizes),
227 product(arraySize(array)),
228 '!='
229 )
230 }
231
232 try {
233 newArray = _reshape(flatArray, sizes)
234 } catch (e) {
235 if (e instanceof DimensionError) {
236 throw new DimensionError(
237 product(sizes),
238 product(arraySize(array)),
239 '!='
240 )
241 }
242 throw e
243 }
244
245 return newArray
246}
247
248/**
249 * Iteratively re-shape a multi dimensional array to fit the specified dimensions
250 * @param {Array} array Array to be reshaped
251 * @param {Array.<number>} sizes List of sizes for each dimension
252 * @returns {Array} Array whose data has been formatted to fit the
253 * specified dimensions
254 */
255
256function _reshape (array, sizes) {
257 // testing if there are enough elements for the requested shape
258 let tmpArray = array
259 let tmpArray2
260 // for each dimensions starting by the last one and ignoring the first one
261 for (let sizeIndex = sizes.length - 1; sizeIndex > 0; sizeIndex--) {
262 const size = sizes[sizeIndex]
263 tmpArray2 = []
264
265 // aggregate the elements of the current tmpArray in elements of the requested size
266 const length = tmpArray.length / size
267 for (let i = 0; i < length; i++) {
268 tmpArray2.push(tmpArray.slice(i * size, (i + 1) * size))
269 }
270 // set it as the new tmpArray for the next loop turn or for return
271 tmpArray = tmpArray2
272 }
273
274 return tmpArray
275}
276
277/**
278 * Squeeze a multi dimensional array
279 * @param {Array} array
280 * @param {Array} [size]
281 * @returns {Array} returns the array itself
282 */
283export function squeeze (array, size) {
284 const s = size || arraySize(array)
285
286 // squeeze outer dimensions
287 while (Array.isArray(array) && array.length === 1) {
288 array = array[0]
289 s.shift()
290 }
291
292 // find the first dimension to be squeezed
293 let dims = s.length
294 while (s[dims - 1] === 1) {
295 dims--
296 }
297
298 // squeeze inner dimensions
299 if (dims < s.length) {
300 array = _squeeze(array, dims, 0)
301 s.length = dims
302 }
303
304 return array
305}
306
307/**
308 * Recursively squeeze a multi dimensional array
309 * @param {Array} array
310 * @param {number} dims Required number of dimensions
311 * @param {number} dim Current dimension
312 * @returns {Array | *} Returns the squeezed array
313 * @private
314 */
315function _squeeze (array, dims, dim) {
316 let i, ii
317
318 if (dim < dims) {
319 const next = dim + 1
320 for (i = 0, ii = array.length; i < ii; i++) {
321 array[i] = _squeeze(array[i], dims, next)
322 }
323 } else {
324 while (Array.isArray(array)) {
325 array = array[0]
326 }
327 }
328
329 return array
330}
331
332/**
333 * Unsqueeze a multi dimensional array: add dimensions when missing
334 *
335 * Paramter `size` will be mutated to match the new, unqueezed matrix size.
336 *
337 * @param {Array} array
338 * @param {number} dims Desired number of dimensions of the array
339 * @param {number} [outer] Number of outer dimensions to be added
340 * @param {Array} [size] Current size of array.
341 * @returns {Array} returns the array itself
342 * @private
343 */
344export function unsqueeze (array, dims, outer, size) {
345 const s = size || arraySize(array)
346
347 // unsqueeze outer dimensions
348 if (outer) {
349 for (let i = 0; i < outer; i++) {
350 array = [array]
351 s.unshift(1)
352 }
353 }
354
355 // unsqueeze inner dimensions
356 array = _unsqueeze(array, dims, 0)
357 while (s.length < dims) {
358 s.push(1)
359 }
360
361 return array
362}
363
364/**
365 * Recursively unsqueeze a multi dimensional array
366 * @param {Array} array
367 * @param {number} dims Required number of dimensions
368 * @param {number} dim Current dimension
369 * @returns {Array | *} Returns the squeezed array
370 * @private
371 */
372function _unsqueeze (array, dims, dim) {
373 let i, ii
374
375 if (Array.isArray(array)) {
376 const next = dim + 1
377 for (i = 0, ii = array.length; i < ii; i++) {
378 array[i] = _unsqueeze(array[i], dims, next)
379 }
380 } else {
381 for (let d = dim; d < dims; d++) {
382 array = [array]
383 }
384 }
385
386 return array
387}
388/**
389 * Flatten a multi dimensional array, put all elements in a one dimensional
390 * array
391 * @param {Array} array A multi dimensional array
392 * @return {Array} The flattened array (1 dimensional)
393 */
394export function flatten (array) {
395 if (!Array.isArray(array)) {
396 // if not an array, return as is
397 return array
398 }
399 const flat = []
400
401 array.forEach(function callback (value) {
402 if (Array.isArray(value)) {
403 value.forEach(callback) // traverse through sub-arrays recursively
404 } else {
405 flat.push(value)
406 }
407 })
408
409 return flat
410}
411
412/**
413 * A safe map
414 * @param {Array} array
415 * @param {function} callback
416 */
417export function map (array, callback) {
418 return Array.prototype.map.call(array, callback)
419}
420
421/**
422 * A safe forEach
423 * @param {Array} array
424 * @param {function} callback
425 */
426export function forEach (array, callback) {
427 Array.prototype.forEach.call(array, callback)
428}
429
430/**
431 * A safe filter
432 * @param {Array} array
433 * @param {function} callback
434 */
435export function filter (array, callback) {
436 if (arraySize(array).length !== 1) {
437 throw new Error('Only one dimensional matrices supported')
438 }
439
440 return Array.prototype.filter.call(array, callback)
441}
442
443/**
444 * Filter values in a callback given a regular expression
445 * @param {Array} array
446 * @param {RegExp} regexp
447 * @return {Array} Returns the filtered array
448 * @private
449 */
450export function filterRegExp (array, regexp) {
451 if (arraySize(array).length !== 1) {
452 throw new Error('Only one dimensional matrices supported')
453 }
454
455 return Array.prototype.filter.call(array, (entry) => regexp.test(entry))
456}
457
458/**
459 * A safe join
460 * @param {Array} array
461 * @param {string} separator
462 */
463export function join (array, separator) {
464 return Array.prototype.join.call(array, separator)
465}
466
467/**
468 * Assign a numeric identifier to every element of a sorted array
469 * @param {Array} a An array
470 * @return {Array} An array of objects containing the original value and its identifier
471 */
472export function identify (a) {
473 if (!Array.isArray(a)) {
474 throw new TypeError('Array input expected')
475 }
476
477 if (a.length === 0) {
478 return a
479 }
480
481 const b = []
482 let count = 0
483 b[0] = { value: a[0], identifier: 0 }
484 for (let i = 1; i < a.length; i++) {
485 if (a[i] === a[i - 1]) {
486 count++
487 } else {
488 count = 0
489 }
490 b.push({ value: a[i], identifier: count })
491 }
492 return b
493}
494
495/**
496 * Remove the numeric identifier from the elements
497 * @param {array} a An array
498 * @return {array} An array of values without identifiers
499 */
500export function generalize (a) {
501 if (!Array.isArray(a)) {
502 throw new TypeError('Array input expected')
503 }
504
505 if (a.length === 0) {
506 return a
507 }
508
509 const b = []
510 for (let i = 0; i < a.length; i++) {
511 b.push(a[i].value)
512 }
513 return b
514}
515
516/**
517 * Check the datatype of a given object
518 * This is a low level implementation that should only be used by
519 * parent Matrix classes such as SparseMatrix or DenseMatrix
520 * This method does not validate Array Matrix shape
521 * @param {Array} array
522 * @param {function} typeOf Callback function to use to determine the type of a value
523 * @return string
524 */
525export function getArrayDataType (array, typeOf) {
526 let type // to hold type info
527 let length = 0 // to hold length value to ensure it has consistent sizes
528
529 for (let i = 0; i < array.length; i++) {
530 const item = array[i]
531 const isArray = Array.isArray(item)
532
533 // Saving the target matrix row size
534 if (i === 0 && isArray) {
535 length = item.length
536 }
537
538 // If the current item is an array but the length does not equal the targetVectorSize
539 if (isArray && item.length !== length) {
540 return undefined
541 }
542
543 const itemType = isArray
544 ? getArrayDataType(item, typeOf) // recurse into a nested array
545 : typeOf(item)
546
547 if (type === undefined) {
548 type = itemType // first item
549 } else if (type !== itemType) {
550 return 'mixed'
551 } else {
552 // we're good, everything has the same type so far
553 }
554 }
555
556 return type
557}
558
559/**
560 * Return the last item from an array
561 * @param array
562 * @returns {*}
563 */
564export function last (array) {
565 return array[array.length - 1]
566}
567
568/**
569 * Get all but the last element of array.
570 */
571export function initial (array) {
572 return array.slice(0, array.length - 1)
573}
574
575/**
576 * Test whether an array or string contains an item
577 * @param {Array | string} array
578 * @param {*} item
579 * @return {boolean}
580 */
581export function contains (array, item) {
582 return array.indexOf(item) !== -1
583}