array.js 16 KB

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