X-Git-Url: https://git.ladys.computer/Pisces/blobdiff_plain/ea0c5228a40540f4e3263f303b22f94df29abeaa..07762ac4c632a6436d43d50d58c8d91760e81e44:/collection.js diff --git a/collection.js b/collection.js index d340709..7447685 100644 --- a/collection.js +++ b/collection.js @@ -1,220 +1,892 @@ -// ♓🌟 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, createCallableFunction } from "./function.js"; -import { isConcatSpreadableObject } from "./object.js"; -import { toIndex, type } from "./value.js"; - -const { prototype: arrayPrototype } = Array; - -export const { - /** Returns an array of the provided values. */ - of: array, +// 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 . + */ - /** Returns whether the provided value is an array. */ - isArray, +import { + call, + createArrowFunction, + createCallableFunction, + createProxyConstructor, + isCallable, + maybe, +} from "./function.js"; +import { + defineOwnDataProperty, + defineOwnProperty, + getMethod, + hasOwnProperty, + isConcatSpreadableObject, + lengthOfArraylike, + objectCreate, + setPropertyValues, + toObject, + toPropertyDescriptorRecord, +} from "./object.js"; +import { + canonicalNumericIndexString, + ITERATOR, + MAXIMUM_SAFE_INTEGRAL_NUMBER, + sameValue, + toFunctionName, + toIndex, + toLength, + type, + UNDEFINED, +} from "./value.js"; - /** Returns an array created from the provided arraylike. */ - from: toArray, -} = Array; +const PISCĒS = "♓🧩 Piscēs"; -/** - * Returns the result of catenating the provided arraylikes into a new - * collection according to the algorithm of `Array::concat`. - */ -export const catenate = createCallableFunction( - arrayPrototype.concat, - "catenate", +/** Returns an array of the provided values. */ +export const array = createArrowFunction( + Array.of, + { name: "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 }, + }, + ), + }; +})(); + /** * Copies the items in the provided object to a new location according - * to the algorithm of `Array::copyWithin`. + * to the algorithm of `Array::copyWithin´. */ export const copyWithin = createCallableFunction( - arrayPrototype.copyWithin, + Array.prototype.copyWithin, ); +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, + + /** + * 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 = createCallableFunction(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 = createCallableFunction(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 = createCallableFunction( - 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 = createCallableFunction( - arrayPrototype.find, - "findItem", -); +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 = createCallableFunction( - arrayPrototype.flatMap, - "flatmap", + 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 = createCallableFunction( - arrayPrototype.flat, - "flatten", + 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 = createCallableFunction( - arrayPrototype.indexOf, - "getFirstIndex", + 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 = createCallableFunction( - arrayPrototype.at, - "getItem", + 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 = createCallableFunction( - arrayPrototype.lastIndexOf, - "getLastIndex", + 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 = createCallableFunction( - arrayPrototype.every, - "hasEvery", + 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 = createCallableFunction( - arrayPrototype.some, - "hasSome", + 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 = createCallableFunction( - arrayPrototype.includes, + 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 = createCallableFunction( - arrayPrototype.entries, - "indexedEntries", + 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 = createCallableFunction( - arrayPrototype.keys, - "indices", + Array.prototype.keys, + { name: "indices" }, ); +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 + toIndex($.length); // will throw if `length´ is not an index return isConcatSpreadableObject($); } catch { return false; @@ -224,69 +896,248 @@ export const isCollection = ($) => { /** * Returns an iterator over the items in the provided value according - * to the algorithm of `Array::values`. + * to the algorithm of `Array::values´. */ export const items = createCallableFunction( - arrayPrototype.values, - "items", + 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 = createCallableFunction(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 = createCallableFunction(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 = createCallableFunction(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 = createCallableFunction(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 = createCallableFunction(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 = createCallableFunction(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 = createCallableFunction(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 = createCallableFunction(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 = createCallableFunction(arrayPrototype.splice); +export const splice = createCallableFunction(Array.prototype.splice); + +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 { + // 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; + } + } + }; + + 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 = createCallableFunction(arrayPrototype.unshift); +export const unshift = createCallableFunction(Array.prototype.unshift);