X-Git-Url: https://git.ladys.computer/Pisces/blobdiff_plain/58d78d7c0602b17a9599e28232cc8a2ff1d8fc65..07762ac4c632a6436d43d50d58c8d91760e81e44:/collection.js?ds=sidebyside diff --git a/collection.js b/collection.js index 3cfe938..7447685 100644 --- a/collection.js +++ b/collection.js @@ -1,375 +1,1143 @@ -// ♓🌟 Piscēs ∷ collection.js -// ==================================================================== -// -// Copyright © 2020–2023 Lady [@ Lady’s Computer]. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at . - -import { call, makeCallable } from "./function.js"; +// SPDX-FileCopyrightText: 2020, 2021, 2022, 2023, 2025 Lady +// SPDX-License-Identifier: MPL-2.0 +/** + * ⁌ ♓🧩 Piscēs ∷ collection.js + * + * Copyright © 2020–2023, 2025 Lady [@ Ladys Computer]. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at . + */ + +import { + call, + createArrowFunction, + createCallableFunction, + createProxyConstructor, + isCallable, + maybe, +} from "./function.js"; import { - floor, - isIntegralNumber, - isNan, - max, + defineOwnDataProperty, + defineOwnProperty, + getMethod, + hasOwnProperty, + isConcatSpreadableObject, + lengthOfArraylike, + objectCreate, + setPropertyValues, + toObject, + toPropertyDescriptorRecord, +} from "./object.js"; +import { + canonicalNumericIndexString, + ITERATOR, MAXIMUM_SAFE_INTEGRAL_NUMBER, - min, -} from "./numeric.js"; -import { sameValue, type } from "./value.js"; - -const { prototype: arrayPrototype } = Array; + sameValue, + toFunctionName, + toIndex, + toLength, + type, + UNDEFINED, +} from "./value.js"; -export const { - /** Returns an array of the provided values. */ - of: array, +const PISCĒS = "♓🧩 Piscēs"; - /** Returns whether the provided value is an array. */ - isArray, +/** Returns an array of the provided values. */ +export const array = createArrowFunction( + Array.of, + { name: "array" }, +); - /** Returns an array created from the provided arraylike. */ - from: toArray, -} = Array; +export const { + /** + * Returns the result of catenating the provided concat spreadable + * values into a new collection according to the algorithm of + * `Array::concat´. + * + * ※ If no arguments are given, this function returns an empty + * array. This is different behaviour than if an explicit undefined + * first argument is given, in which case the resulting array will + * have undefined as its first item. + * + * ※ Unlike `Array::concat´, this function ignores + * `.constructor[Symbol.species]´ and always returns an array. + */ + concatSpreadableCatenate, +} = (() => { + const { concat } = Array.prototype; + return { + concatSpreadableCatenate: Object.defineProperties( + (...args) => call(concat, [], args), + { + name: { value: "concatSpreadableCatenate" }, + length: { value: 2 }, + }, + ), + }; +})(); /** - * Returns −0 if the provided argument is "-0"; returns a number - * representing the index if the provided argument is a canonical - * numeric index string; otherwise, returns undefined. - * - * There is no clamping of the numeric index, but note that numbers - * above 2^53 − 1 are not safe nor valid integer indices. + * Copies the items in the provided object to a new location according + * to the algorithm of `Array::copyWithin´. */ -export const canonicalNumericIndexString = ($) => { - if (typeof $ !== "string") { - return undefined; - } else if ($ === "-0") { - return -0; - } else { - const n = +$; - return $ === `${n}` ? n : undefined; - } -}; +export const copyWithin = createCallableFunction( + Array.prototype.copyWithin, +); -/** - * Returns the result of catenating the provided arraylikes into a new - * collection according to the algorithm of `Array::concat`. - */ -export const catenate = makeCallable(arrayPrototype.concat); +export const { + /** + * Returns a proxy of the provided arraylike value for which every + * integer index less than its length will appear to be present. + * + * ※ The returned proxy will have the original object as its + * prototype and will update with changes to the original. + * + * ※ The returned proxy only reflects ⹐own⹑ properties on the + * underlying object; if an index is set on the prototype chain, but + * not as an own property on the underlying object, it will appear as + * undefined on the proxy. Like·wise, if the length is not an own + * property, it will appear to be zero. + * + * ※ Both data values and accessors are supported for indices, + * provided they are defined directly on the underlying object. + * + * ※ A proxy can be made non·extensible if the `.length´ of the + * underlying object is read·only (i·e, not defined using a getter) + * and nonconfigurable. + * + * ※ Integer indices on the returned proxy, as well as `.length´, + * are read·only and start out formally (but not manually) + * configurable. They can only be made nonconfigurable if the + * underlying value is guaranteed not to change. As a change in + * `.length´ deletes any integer indices larger than the `.length´, + * the `.length´ of the underlying object must be fixed for any + * integer index to be made nonconfigurable. + * + * ※ When iterating, it is probably faster to just make a copy of + * the original value, for example :— + * + * | `setPropertyValues(´ + * | ` fill(setPropertyValue([], "length", original.length)),´ + * | ` original,´ + * | `);´ + * + * This function is rather intended for the use·case where both the + * proxy and the underlying array are longlived, and the latter may + * change unexpectedly after the formers creation. + */ + denseProxy, -/** - * Copies the items in the provided object to a new location according - * to the algorithm of `Array::copyWithin`. - */ -export const copyWithin = makeCallable(arrayPrototype.copyWithin); + /** + * Returns whether the provided value is a dense proxy (created with + * `denseProxy´). + */ + isDenseProxy, +} = (() => { + const { + deleteProperty: reflectDeleteProperty, + defineProperty: reflectDefineProperty, + getOwnPropertyDescriptor: reflectGetOwnPropertyDescriptor, + getPrototypeOf: reflectGetPrototypeOf, + has: reflectHas, + isExtensible: reflectIsExtensible, + ownKeys: reflectOwnKeys, + preventExtensions: reflectPreventExtensions, + setPrototypeOf: reflectSetPrototypeOf, + } = Reflect; + const isReadOnlyNonconfigurable = (Desc) => + Desc !== UNDEFINED + && !(Desc.configurable || "get" in Desc + || "writable" in Desc && Desc.writable); + + const denseProxyHandler = Object.assign(Object.create(null), { + defineProperty(O, P, Desc) { + const k = P === "length" + ? UNDEFINED + : canonicalNumericIndexString(P); + if ( + P === "length" + || k !== UNDEFINED + && (sameValue(k, 0) || k > 0 && k === toLength(k)) + ) { + // The provided property is either `"length"´ or an integer + // index. + const desc = maybe(Desc, toPropertyDescriptorRecord); + if ( + desc.set !== UNDEFINED + || "writable" in desc && desc.writable + ) { + // The provided descriptor defines a setter or is attempting + // to make the property writable; this is not permitted. + return false; + } else { + // The provided descriptor does not define a setter. + const Q = reflectGetPrototypeOf(O); + const current = maybe( + reflectGetOwnPropertyDescriptor(O, P), + toPropertyDescriptorRecord, + ); + const lenDesc = maybe( + reflectGetOwnPropertyDescriptor(Q, "length"), + toPropertyDescriptorRecord, + ); + const currentlyConfigurable = current?.configurable ?? true; + const willChangeConfigurable = "configurable" in desc + && (desc.configurable !== currentlyConfigurable); + if ( + willChangeConfigurable + && (!currentlyConfigurable + || !isReadOnlyNonconfigurable(lenDesc)) + ) { + // The provided descriptor either aims to make a property + // nonconfigurable when the underlying length is not + // readonly or else aims to make a property configurable + // when it currently isn¦t; neither is permitted. + return false; + } else if (P === "length") { + // The provided descriptor is attempting to modify the + // length. + // + // It is known at this point that either the property + // descriptor is not trying to make the length + // nonconfigurable, or else the underlying length is + // readonly. + const len = !currentlyConfigurable + ? current.value + : lenDesc === UNDEFINED + ? 0 // force zero if not an own property + : !("value" in desc) + ? null // not needed yet + : lengthOfArraylike(Q); + if ( + "get" in desc + || "enumerable" in desc && desc.enumerable + || "value" in desc && desc.value !== len + ) { + // The provided descriptor is attempting to create a + // getter for the length, change the enumerability of the + // length, or change the value of the length; these are + // not permitted. + return false; + } else if (!willChangeConfigurable) { + // The provided descriptor is not attempting to change + // the value of the length or its configurablility; this + // succeeds with no effect. + return true; + } else { + // The provided descriptor is attempting to make a + // read·only, configurable length nonconfigurable. + return reflectDefineProperty( + O, + P, + setPropertyValues(objectCreate(null), { + configurable: false, + enumerable: false, + value: len ?? lengthOfArraylike(Q), + writable: false, + }), + ); + } + } else { + // The provided property is an integral canonical numeric + // index string. + const len = lenDesc === UNDEFINED + ? 0 + : lengthOfArraylike(Q); + if (k < len) { + // The provided property is smaller than the length; this + // property can potentially be modified. + const kDesc = maybe( + reflectGetOwnPropertyDescriptor(Q, P), + toPropertyDescriptorRecord, + ); + const kGet = current?.get; // use current getter + const kValue = // use underlying value + kDesc === UNDEFINED + ? UNDEFINED + : kDesc.value ?? call(kDesc.get, Q, []); + if ( + "get" in desc && desc.get !== kGet + || "enumerable" in desc && !desc.enumerable + || "value" in desc && desc.value !== kValue + ) { + // The provided descriptor is attempting to change the + // value or enumerability of the property away from + // their current values; this is not permitted. + return false; + } else if (!willChangeConfigurable) { + // The provided descriptor is not attempting to change + // the configurability of the property; in this case, + // no actual change is being requested. + return true; + } else { + // The provided descriptor is attempting to make the + // property nonconfigurable, but it is currently + // configurable (and maybe not even present on the + // proxy target); this is only permissible if the value + // of the underlying property is known not to be able + // to change. + // + // Providing the value is okay if the underlying + // property is readonly, but if the underlying property + // is a getter, then the value must not be provided + // (since the resulting property will be defined with a + // brand new getter). + // + // At this point, it is known that the provided + // descriptor does not provide a getter, because + // getters are only supported on index properties which + // are already nonconfigurable. + // + // At this point, we have already confirmed that the + // length of the underlying object is immutable. + const dynamic = kDesc !== UNDEFINED + && !("writable" in kDesc); + const readonly = + kDesc === UNDEFINED && !reflectIsExtensible(Q) + || kDesc !== UNDEFINED && !kDesc.configurable && ( + dynamic || !kDesc.writable + ); + const noChange = !dynamic + || dynamic && !("value" in desc); + return readonly && noChange && reflectDefineProperty( + O, + P, + setPropertyValues( + objectCreate(null), + kDesc !== UNDEFINED && "get" in kDesc + ? { + configurable: false, + enumerable: true, + get: defineOwnProperty( + () => call(kDesc.get, Q, []), + "name", + defineOwnDataProperty( + objectCreate(null), + "value", + toFunctionName(P, "get"), + ), + ), + set: UNDEFINED, + } + : { + configurable: false, + enumerable: true, + value: kValue, + writable: false, + }, + ), + ); + } + } else { + // The provided property is not smaller than the length; + // this is not permitted. + return false; + } + } + } + } else { + // The provided property is not `"length"´ or an integer index. + return reflectDefineProperty(O, P, Desc); + } + }, + deleteProperty(O, P) { + const k = P === "length" + ? UNDEFINED + : canonicalNumericIndexString(P); + if ( + P === "length" + || k !== UNDEFINED + && (sameValue(k, 0) || k > 0 && k === toLength(k)) + ) { + // The property is an integer index or `"length"´. + if (!reflectIsExtensible(O) || P === "length") { + // The proxied object is not extensible or the provided + // property is `"length"´; this is not permitted. + return false; + } else { + // The provided property is an integer index; it can only + // be deleted if it is greater than the length (in which + // case, it is not present in the first place). + const Q = reflectGetPrototypeOf(O); + const len = hasOwnProperty(Q, "length") + ? lengthOfArraylike(Q) + : 0; + return k < len ? false : true; + } + } else { + // The provided property is not `"length"´ or an integer index. + return reflectDeleteProperty(O, P); + } + }, + getOwnPropertyDescriptor(O, P) { + const k = P === "length" + ? UNDEFINED + : canonicalNumericIndexString(P); + if ( + P === "length" + || k !== UNDEFINED + && (sameValue(k, 0) || k > 0 && k === toLength(k)) + ) { + // The property is an integer index or `"length"´. + const Q = reflectGetPrototypeOf(O); + const current = maybe( + reflectGetOwnPropertyDescriptor(O, P), + toPropertyDescriptorRecord, + ); + if (current !== UNDEFINED && !current.configurable) { + // The property is defined and nonconfigurable on the object. + // + // Return its descriptor. + return current; + } else if (P === "length") { + // The property is `"length"´. + // + // Return the length of the underlying object. + return setPropertyValues(objectCreate(null), { + configurable: true, + enumerable: false, + value: hasOwnProperty(Q, "length") + ? lengthOfArraylike(Q) + : 0, + writable: false, + }); + } else { + // The property is an integer index. + // + // Return a data descriptor with its value or undefined as + // appropriate. + const len = hasOwnProperty(Q, "length") + ? lengthOfArraylike(Q) + : 0; + if (k < len) { + // The property is an integer index less than the length. + // + // Provide the current value of the own property. + const kDesc = maybe( + reflectGetOwnPropertyDescriptor(Q, P), + toPropertyDescriptorRecord, + ); + return setPropertyValues(objectCreate(null), { + configurable: true, + enumerable: true, + value: !kDesc + ? UNDEFINED + : "get" in kDesc + ? call(kDesc.get, Q, []) + : kDesc.value, + writable: false, + }); + } else { + // The property is an integer index, but not less than the + // length. + // + // Return undefined. + return UNDEFINED; + } + } + } else { + // The provided property is not `"length"´ or an integer index. + return reflectGetOwnPropertyDescriptor(O, P); + } + }, + has(O, P) { + const k = P === "length" + ? UNDEFINED + : canonicalNumericIndexString(P); + if (P === "length") { + // The provided property is `"length"´; this is always present. + return true; + } else if ( + k !== UNDEFINED + && (sameValue(k, 0) || k > 0 && k === toLength(k)) + ) { + // The provided property is an integer index. + // + // Return whether it is less than the length. + const Q = reflectGetPrototypeOf(O); + const len = hasOwnProperty(Q, "length") + ? lengthOfArraylike(Q) + : 0; + return k < len ? true : reflectHas(O, P); + } else { + // The provided property is not `"length"´ or an integer index. + return reflectHas(O, P); + } + }, + ownKeys(O) { + const keys = reflectOwnKeys(O); + const Q = reflectGetPrototypeOf(O); + const len = hasOwnProperty(Q, "length") + ? lengthOfArraylike(Q) + : 0; + const result = []; + let i; + let hasHitLength = false; + for (i = 0; i < len && i < -1 >>> 0; ++i) { + // Iterate over those array indices which are less than the + // length of the underlying object and collect them in the + // result. + // + // Array indices are handled specially by the Ecmascript + // specification. Other integer indices may also be present + // (if they are too big to be array indices but still smaller + // than the length), but these are added later with all of the + // other keys. + defineOwnDataProperty(result, i, `${i}`); + } + for (let j = 0; j < keys.length; ++j) { + // Iterate over the own keys of the object and collect them in + // the result if necessary. + const P = keys[j]; + const k = P === "length" + ? UNDEFINED + : canonicalNumericIndexString(P); + const isIntegerIndex = k !== UNDEFINED + && (sameValue(k, 0) || k > 0 && k === toLength(k)); + if (!hasHitLength && (!isIntegerIndex || k >= -1 >>> 0)) { + // The current key is the first key which is not an array + // index; add `"length"´ to the result, as well as any + // integer indices which are not array indices. + // + // This may never occur, in which case these properties are + // added after the end of the loop. + // + // `"length"´ is added first as it is conceptually the first + // property on the object. + defineOwnDataProperty(result, result.length, "length"); + for (; i < len; ++i) { + // Iterate over those remaining integer indices which are + // less than the length of the underlying object and + // collect them in the result. + defineOwnDataProperty(result, result.length, `${i}`); + } + hasHitLength = true; + } else { + // The current key is not the first key which is not an array + // index. + /* do nothing */ + } + if (P === "length" || isIntegerIndex && k < len) { + // The current key is either `"length"´ or an integer index + // less than the length; it has already been collected. + /* do nothing */ + } else { + // The current key has not yet been collected into the + // result; add it. + defineOwnDataProperty(result, result.length, P); + } + } + if (!hasHitLength) { + // All of the collected keys were array indices; `"length"´ and + // any outstanding integer indices still need to be collected. + defineOwnDataProperty(result, result.length, "length"); + for (; i < len; ++i) { + // Iterate over those remaining integer indices which are + // less than the length of the underlying object and collect + // them in the result. + defineOwnDataProperty(result, result.length, `${i}`); + } + } else { + // There was at least one key collected which was not an array + // index. + /* do nothing */ + } + return result; + }, + preventExtensions(O) { + if (!reflectIsExtensible(O)) { + // The object is already not extensible; this is an automatic + // success. + return true; + } else { + // The object is currently extensible; see if it can be made + // non·extensible and attempt to do so. + const Q = reflectGetPrototypeOf(O); + const lenDesc = maybe( + reflectGetOwnPropertyDescriptor(Q, "length"), + toPropertyDescriptorRecord, + ); + if (!isReadOnlyNonconfigurable(lenDesc)) { + // The underlying length is not read·only; the object cannot + // be made non·extensible because the indices may change. + return false; + } else { + // The underlying length is read·only; define the needed + // indices on the object and then prevent extensions. + const len = lengthOfArraylike(Q); // definitely exists + for (let k = 0; k < len; ++k) { + // Iterate over each index and define a placeholder for it. + reflectDefineProperty( + O, + k, + setPropertyValues(objectCreate(null), { + configurable: true, + enumerable: true, + value: UNDEFINED, + writable: false, + }), + ); + } + return reflectPreventExtensions(O); + } + } + }, + setPrototypeOf(O, V) { + const Q = reflectGetPrototypeOf(O); + return Q === V ? reflectSetPrototypeOf(O, V) : false; + }, + }); + + const DenseProxy = createProxyConstructor( + denseProxyHandler, + function Dense($) { + return objectCreate(toObject($)); // throws if nullish + }, + ); + + return { + denseProxy: Object.defineProperty( + ($) => new DenseProxy($), + "name", + { value: "denseProxy" }, + ), + isDenseProxy: DenseProxy.isDenseProxy, + }; +})(); /** * Fills the provided object with the provided value according to the - * algorithm of `Array::fill`. + * algorithm of `Array::fill´. */ -export const fill = makeCallable(arrayPrototype.fill); +export const fill = createCallableFunction(Array.prototype.fill); /** * Returns the result of filtering the provided object with the - * provided callback, according to the algorithm of `Array::filter`. + * provided callback, according to the algorithm of `Array::filter´. + * + * ※ Unlike `Array::filter´, this function ignores + * `.constructor[Symbol.species]´ and always returns an array. */ -export const filter = makeCallable(arrayPrototype.filter); +export const filter = ($, callbackFn, thisArg = UNDEFINED) => { + const O = toObject($); + const len = lengthOfArraylike(O); + if (!isCallable(callbackFn)) { + throw new TypeError( + `${PISCĒS}: Filter callback must be callable.`, + ); + } else { + const A = []; + for (let k = 0, to = 0; k < len; ++k) { + if (k in O) { + const kValue = O[k]; + if (call(callbackFn, thisArg, [kValue, k, O])) { + defineOwnDataProperty(A, to++, kValue); + } else { + /* do nothing */ + } + } else { + /* do nothing */ + } + } + return A; + } +}; /** * Returns the first index in the provided object whose value satisfies - * the provided callback according to the algorithm of - * `Array::findIndex`. + * the provided callback. + * + * ※ This function differs from `Array::findIndex´ in that it returns + * undefined, not −1, if no match is found, and indices which aren¦t + * present are skipped, not treated as having values of undefined. */ -export const findIndex = makeCallable(arrayPrototype.findIndex); +export const findFirstIndex = ($, callback, thisArg = UNDEFINED) => + findFirstIndexedEntry($, callback, thisArg)?.[0]; + +export const { + /** + * Returns the first indexed entry in the provided object whose value + * satisfies the provided callback. + * + * If a third argument is supplied, it will be used as the this value + * of the callback. + * + * ※ Unlike the builtin Ecmascript array searching methods, this + * function does not treat indices which are not present on a sparse + * array as though they were undefined. + */ + findFirstIndexedEntry, + + /** + * Returns the last indexed entry in the provided object whose value + * satisfies the provided callback. + * + * If a third argument is supplied, it will be used as the this value + * of the callback. + * + * ※ Unlike the builtin Ecmascript array searching methods, this + * function does not treat indices which are not present on a sparse + * array as though they were undefined. + */ + findLastIndexedEntry, +} = (() => { + const findViaPredicate = ($, direction, predicate, thisArg) => { + const O = toObject($); + const len = lengthOfArraylike(O); + if (!isCallable(predicate)) { + // The provided predicate is not callable; throw an error. + throw new TypeError( + `${PISCĒS}: Find predicate must be callable.`, + ); + } else { + // The provided predicate is callable; do the search. + const ascending = direction === "ascending"; + for ( + let k = ascending ? 0 : len - 1; + ascending ? k < len : k >= 0; + ascending ? ++k : --k + ) { + // Iterate over each possible index between 0 and the length of + // the provided arraylike. + if (!(k in O)) { + // The current index is not present in the provided value. + /* do nothing */ + } else { + // The current index is present in the provided value; test + // to see if it satisfies the predicate. + const kValue = O[k]; + if (call(predicate, thisArg, [kValue, k, O])) { + // The value at the current index satisfies the predicate; + // return the entry. + return [k, kValue]; + } else { + // The value at the current index does not satisfy the + // predicate. + /* do nothing */ + } + } + } + return UNDEFINED; + } + }; + + return { + findFirstIndexedEntry: ($, predicate, thisArg = UNDEFINED) => + findViaPredicate($, "ascending", predicate, thisArg), + findLastIndexedEntry: ($, predicate, thisArg = UNDEFINED) => + findViaPredicate($, "descending", predicate, thisArg), + }; +})(); /** - * Returns the first indexed entry in the provided object whose value + * Returns the first indexed value in the provided object which * satisfies the provided callback. * - * If a third argument is supplied, it will be used as the this value - * of the callback. + * ※ Unlike `Array::find´, this function does not treat indices which + * are not present on a sparse array as though they were undefined. */ -export const findIndexedEntry = ( - $, - callback, - thisArg = undefined, -) => { - let result = undefined; - findItem($, (kValue, k, O) => { - if (call(callback, thisArg, [kValue, k, O])) { - // The callback succeeded. - result = [k, kValue]; - return true; - } else { - // The callback failed. - return false; - } - }); - return result; -}; +export const findFirstItem = ($, callback, thisArg = UNDEFINED) => + findFirstIndexedEntry($, callback, thisArg)?.[1]; /** - * Returns the first indexed value in the provided object which - * satisfies the provided callback, according to the algorithm of - * `Array::find`. + * Returns the last index in the provided object whose value satisfies + * the provided callback. + * + * ※ This function differs from `Array::findLastIndex´ in that it + * returns undefined, not −1, if no match is found, and indices which + * aren¦t present are skipped, not treated as having values of + * undefined. */ -export const findItem = makeCallable(arrayPrototype.find); +export const findLastIndex = ($, callback, thisArg = UNDEFINED) => + findLastIndexedEntry($, callback, thisArg)?.[0]; + +/** + * Returns the last indexed value in the provided object which + * satisfies the provided callback. + * + * ※ Unlike `Array::findLast´, this function does not treat indices + * which are not present on a sparse array as though they were + * undefined. + */ +export const findLastItem = ($, callback, thisArg = UNDEFINED) => + findLastIndexedEntry($, callback, thisArg)?.[1]; /** * Returns the result of flatmapping the provided value with the - * provided callback according to the algorithm of `Array::flatMap`. + * provided callback according to the algorithm of `Array::flatMap´. + * + * ※ Flattening always produces a dense array. */ -export const flatmap = makeCallable(arrayPrototype.flatMap); +export const flatmap = createCallableFunction( + Array.prototype.flatMap, + { name: "flatmap" }, +); /** * Returns the result of flattening the provided object according to - * the algorithm of `Array::flat`. + * the algorithm of `Array::flat´. + * + * ※ Flattening always produces a dense array. */ -export const flatten = makeCallable(arrayPrototype.flat); +export const flatten = createCallableFunction( + Array.prototype.flat, + { name: "flatten" }, +); /** * Returns the first index of the provided object with a value * equivalent to the provided value according to the algorithm of - * `Array::indexOf`. + * `Array::indexOf´. */ -export const getFirstIndex = makeCallable(arrayPrototype.indexOf); +export const getFirstIndex = createCallableFunction( + Array.prototype.indexOf, + { name: "getFirstIndex" }, +); /** * Returns the item on the provided object at the provided index - * according to the algorithm of `Array::at`. + * according to the algorithm of `Array::at´. */ -export const getItem = makeCallable(arrayPrototype.at); +export const getItem = createCallableFunction( + Array.prototype.at, + { name: "getItem" }, +); /** * Returns the last index of the provided object with a value * equivalent to the provided value according to the algorithm of - * `Array::lastIndexOf`. + * `Array::lastIndexOf´. */ -export const getLastIndex = makeCallable(arrayPrototype.lastIndexOf); +export const getLastIndex = createCallableFunction( + Array.prototype.lastIndexOf, + { name: "getLastIndex" }, +); /** * Returns whether every indexed value in the provided object satisfies - * the provided function, according to the algorithm of `Array::every`. + * the provided function, according to the algorithm of `Array::every´. */ -export const hasEvery = makeCallable(arrayPrototype.every); +export const hasEvery = createCallableFunction( + Array.prototype.every, + { name: "hasEvery" }, +); /** * Returns whether the provided object has an indexed value which * satisfies the provided function, according to the algorithm of - * `Array::some`. + * `Array::some´. */ -export const hasSome = makeCallable(arrayPrototype.some); +export const hasSome = createCallableFunction( + Array.prototype.some, + { name: "hasSome" }, +); /** * Returns whether the provided object has an indexed value equivalent * to the provided value according to the algorithm of - * `Array::includes`. + * `Array::includes´. * - * ※ This algorithm treats missing values as `undefined` rather than + * ※ This algorithm treats missing values as undefined rather than * skipping them. */ -export const includes = makeCallable(arrayPrototype.includes); +export const includes = createCallableFunction( + Array.prototype.includes, +); /** * Returns an iterator over the indexed entries in the provided value - * according to the algorithm of `Array::entries`. + * according to the algorithm of `Array::entries´. */ -export const indexedEntries = makeCallable(arrayPrototype.entries); +export const indexedEntries = createCallableFunction( + Array.prototype.entries, + { name: "indexedEntries" }, +); /** * Returns an iterator over the indices in the provided value according - * to the algorithm of `Array::keys`. + * to the algorithm of `Array::keys´. */ -export const indices = makeCallable(arrayPrototype.keys); - -/** Returns whether the provided value is an array index string. */ -export const isArrayIndexString = ($) => { - const value = canonicalNumericIndexString($); - if (value !== undefined) { - // The provided value is a canonical numeric index string. - return sameValue(value, 0) || value > 0 && value < -1 >>> 0 && - value === toLength(value); - } else { - // The provided value is not a canonical numeric index string. - return false; - } -}; +export const indices = createCallableFunction( + Array.prototype.keys, + { name: "indices" }, +); -/** Returns whether the provided value is arraylike. */ -export const isArraylikeObject = ($) => { - if (type($) !== "object") { - return false; - } else { - try { - lengthOfArraylike($); // throws if not arraylike - return true; - } catch { - return false; - } - } -}; +export const isArray = createArrowFunction(Array.isArray); /** * Returns whether the provided object is a collection. * * The definition of “collection” used by Piscēs is similar to - * Ecmascript’s definition of an arraylike object, but it differs in + * Ecmascripts definition of an arraylike object, but it differs in * a few ways :— * * - It requires the provided value to be a proper object. * - * - It requires the `length` property to be an integer index. + * - It requires the `length´ property to be an integer index. * * - It requires the object to be concat‐spreadable, meaning it must - * either be an array or have `.[Symbol.isConcatSpreadable]` be true. + * either be an array or have `.[Symbol.isConcatSpreadable]´ be true. */ export const isCollection = ($) => { if (!(type($) === "object" && "length" in $)) { - // The provided value is not an object or does not have a `length`. + // The provided value is not an object or does not have a `length´. return false; } else { try { - toIndex($.length); // will throw if `length` is not an index - return isConcatSpreadable($); + toIndex($.length); // will throw if `length´ is not an index + return isConcatSpreadableObject($); } catch { return false; } } }; -/** - * Returns whether the provided value is spreadable during array - * concatenation. - * - * This is also used to determine which things should be treated as - * collections. - */ -export const isConcatSpreadable = ($) => { - if (type($) !== "object") { - // The provided value is not an object. - return false; - } else { - // The provided value is an object. - const spreadable = $[Symbol.isConcatSpreadable]; - return spreadable !== undefined ? !!spreadable : isArray($); - } -}; - -/** Returns whether the provided value is an integer index string. */ -export const isIntegerIndexString = ($) => { - const value = canonicalNumericIndexString($); - if (value !== undefined && isIntegralNumber(value)) { - // The provided value is a canonical numeric index string. - return sameValue(value, 0) || - value > 0 && value <= MAXIMUM_SAFE_INTEGRAL_NUMBER && - value === toLength(value); - } else { - // The provided value is not a canonical numeric index string. - return false; - } -}; - /** * Returns an iterator over the items in the provided value according - * to the algorithm of `Array::values`. - */ -export const items = makeCallable(arrayPrototype.values); - -/** - * Returns the length of the provided arraylike object. - * - * Will throw if the provided object is not arraylike. - * - * This can produce larger lengths than can actually be stored in - * arrays, because no such restrictions exist on arraylike methods. + * to the algorithm of `Array::values´. */ -export const lengthOfArraylike = ({ length }) => toLength(length); +export const items = createCallableFunction( + Array.prototype.values, + { name: "items" }, +); /** * Returns the result of mapping the provided value with the provided - * callback according to the algorithm of `Array::map`. + * callback according to the algorithm of `Array::map´. */ -export const map = makeCallable(arrayPrototype.map); +export const map = createCallableFunction(Array.prototype.map); /** * Pops from the provided value according to the algorithm of - * `Array::pop`. + * `Array::pop´. */ -export const pop = makeCallable(arrayPrototype.pop); +export const pop = createCallableFunction(Array.prototype.pop); /** * Pushes onto the provided value according to the algorithm of - * `Array::push`. + * `Array::push´. */ -export const push = makeCallable(arrayPrototype.push); +export const push = createCallableFunction(Array.prototype.push); /** * Returns the result of reducing the provided value with the provided - * callback, according to the algorithm of `Array::reduce`. + * callback, according to the algorithm of `Array::reduce´. */ -export const reduce = makeCallable(arrayPrototype.reduce); +export const reduce = createCallableFunction(Array.prototype.reduce); /** * Reverses the provided value according to the algorithm of - * `Array::reverse`. + * `Array::reverse´. */ -export const reverse = makeCallable(arrayPrototype.reverse); +export const reverse = createCallableFunction(Array.prototype.reverse); /** * Shifts the provided value according to the algorithm of - * `Array::shift`. + * `Array::shift´. */ -export const shift = makeCallable(arrayPrototype.shift); +export const shift = createCallableFunction(Array.prototype.shift); /** * Returns a slice of the provided value according to the algorithm of - * `Array::slice`. + * `Array::slice´. */ -export const slice = makeCallable(arrayPrototype.slice); +export const slice = createCallableFunction(Array.prototype.slice); /** * Sorts the provided value in‐place according to the algorithm of - * `Array::sort`. + * `Array::sort´. */ -export const sort = makeCallable(arrayPrototype.sort); +export const sort = createCallableFunction(Array.prototype.sort); /** * Splices into and out of the provided value according to the - * algorithm of `Array::splice`. + * algorithm of `Array::splice´. */ -export const splice = makeCallable(arrayPrototype.splice); +export const splice = createCallableFunction(Array.prototype.splice); -/** - * Returns the result of converting the provided value to an array - * index, or throws an error if it is out of range. - */ -export const toIndex = ($) => { - const integer = floor($); - if (isNan(integer) || integer == 0) { - // The value is zero·like. - return 0; - } else { - // The value is not zero·like. - const clamped = toLength(integer); - if (clamped !== integer) { - // Clamping the value changes it. - throw new RangeError(`Piscēs: Index out of range: ${$}.`); +export const { + /** + * Returns a potentially‐sparse array created from the provided + * arraylike or iterable. + * + * ※ This function differs from `Array.from´ in that it does not + * support subclassing and, in the case of a provided arraylike + * value, does not set properties on the result which are not present + * in the provided value. This can result in sparse arrays. + * + * ※ An iterator result which lacks a `value´ property also results + * in the corresponding index being missing in the resulting array. + */ + toArray, + + /** + * Returns a dense array created from the provided arraylike or + * iterable. + * + * ※ This function differs from `Array.from´ in that it does not + * support subclassing. + * + * ※ If indices are not present in a provided arraylike value, they + * will be treated exactly as tho they were present and set to + * undefined. + */ + toDenseArray, +} = (() => { + const makeArray = Array; + + const arrayFrom = (items, mapFn, thisArg, allowSparse = false) => { + // This function re·implements the behaviour of `Array.from´, plus + // support for sparse arrays and minus the support for subclassing. + // + // It is actually only needed in the former case, as subclassing + // support can more easily be removed by wrapping it in an arrow + // function or binding it to `Array´ or undefined. + if (mapFn !== UNDEFINED && !isCallable(mapFn)) { + // A mapping function was provided but is not callable; this is + // an error. + throw new TypeError(`${PISCĒS}: Map function not callable.`); } else { - // The value is within appropriate bounds. - return integer; + // Attempt to get an iterator method for the provided items; + // further behaviour depends on whether this is successful. + const iteratorMethod = getMethod(items, ITERATOR); + if (iteratorMethod !== UNDEFINED) { + // An iterator method was found; attempt to create an array + // from its items. + const A = []; + const iterator = call(items, iteratorMethod, []); + if (type(iterator) !== "object") { + // The iterator method did not produce an object; this is an + // error. + throw new TypeError( + `${PISCĒS}: Iterators must be objects, but got: ${iterator}.`, + ); + } else { + // The iterator method produced an iterator object; collect + // its values into the array. + const nextMethod = iterator.next; + for (let k = 0; true; ++k) { + // Loop until the iterator is exhausted. + if (k >= MAXIMUM_SAFE_INTEGRAL_NUMBER) { + // The current index has exceeded the maximum index + // allowable for arrays; close the iterator, then throw + // an error. + try { + // Attempt to close the iterator. + iterator.return(); + } catch { + // Ignore any errors while closing the iterator. + /* do nothing */ + } + throw new TypeError(`${PISCĒS}: Index out of range.`); + } else { + // The current index is a valid index; get the next value + // from the iterator and assign it, if one exists. + const result = call(nextMethod, iterator, []); + if (type(result) !== "object") { + // The next method did not produce an object; this is + // an error. + throw new TypeError( + `${PISCĒS}: Iterator results must be objects, but got: ${result}.`, + ); + } else { + // The next method produced an object; process it. + const { done } = result; + if (done) { + // The iterator has exhausted itself; confirm the + // length of the resulting array and return it. + A.length = k; + return A; + } else { + const present = "value" in result; + // The iterator has not exhausted itself; add its + // value to the array. + if (allowSparse && !present) { + // The iterator has no value and creating sparse + // arrays is allowed. + /* do nothing */ + } else { + // The iterator has a value or sparse arrays are + // disallowed. + const nextValue = present + ? result.value + : UNDEFINED; + try { + // Try to assign the value in the result array, + // mapping if necessary. + defineOwnDataProperty( + A, + k, + mapFn !== UNDEFINED + ? call(mapFn, thisArg, [nextValue, k]) + : nextValue, + ); + } catch (error) { + // There was an error when mapping or assigning + // the value; close the iterator before + // rethrowing the error. + try { + // Attempt to close the iterator. + iterator.return(); + } catch { + // Ignore any errors while closing the + // iterator. + /* do nothing */ + } + throw error; + } + } + } + } + } + } + } + } else { + // No iterator method was found; treat the provided items as an + // arraylike object. + const arraylike = toObject(items); + const len = lengthOfArraylike(arraylike); + const A = makeArray(len); + for (let k = 0; k < len; ++k) { + // Iterate over the values in the arraylike object and assign + // them to the result array as necessary. + const present = k in arraylike; + if (allowSparse && !present) { + // The current index is not present in the arraylike object + // and sparse arrays are allowed. + /* do nothing */ + } else { + // The current index is present in the arraylike object or + // sparse arrays are not allowed; assign the value to the + // appropriate index in the result array, mapping if + // necessary. + const nextValue = present ? arraylike[k] : UNDEFINED; + defineOwnDataProperty( + A, + k, + mapFn !== UNDEFINED + ? call(mapFn, thisArg, [nextValue, k]) + : nextValue, + ); + } + } + A.length = len; + return A; + } } - } -}; + }; -/** Returns the result of converting the provided value to a length. */ -export const toLength = ($) => { - const len = floor($); - return isNan(len) || len == 0 - ? 0 - : max(min(len, MAXIMUM_SAFE_INTEGRAL_NUMBER), 0); -}; + return { + toArray: (items, mapFn = UNDEFINED, thisArg = UNDEFINED) => + arrayFrom(items, mapFn, thisArg, true), + toDenseArray: (items, mapFn = UNDEFINED, thisArg = UNDEFINED) => + arrayFrom(items, mapFn, thisArg, false), + }; +})(); /** * Unshifts the provided value according to the algorithm of - * `Array::unshift`. + * `Array::unshift´. */ -export const unshift = makeCallable(arrayPrototype.unshift); +export const unshift = createCallableFunction(Array.prototype.unshift);