simple-squiggle

A restricted subset of Squiggle
Log | Files | Refs | README

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 }