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