UNPKG

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