X-Git-Url: https://git.ladys.computer/Pisces/blobdiff_plain/8c8953d378b20e194535e22214750367961b6963..07762ac4c632a6436d43d50d58c8d91760e81e44:/collection.js
diff --git a/collection.js b/collection.js
index 6920661..7447685 100644
--- a/collection.js
+++ b/collection.js
@@ -1,67 +1,893 @@
-// ♓🌟 Piscēs ∷ collection.js
-// ====================================================================
-//
-// Copyright © 2020–2022 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 { MAX_SAFE_INTEGER } from "./numeric.js";
-import { isObject } from "./object.js";
-
-/**
- * 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.
+// SPDX-FileCopyrightText: 2020, 2021, 2022, 2023, 2025 Lady
+// SPDX-License-Identifier: MPL-2.0
+/**
+ * ⁌ ♓🧩 Piscēs ∷ collection.js
*
- * There is no clamping of the numeric index, but note that numbers
- * above 2^53 − 1 are not safe nor valid integer indices.
- */
-export const canonicalNumericIndexString = ($) => {
- if (typeof $ != "string") {
- return undefined;
- } else if ($ === "-0") {
- return -0;
- } else {
- const n = +$;
- return $ === `${n}` ? n : undefined;
- }
-};
+ * 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 {
+ 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";
+
+const PISCĒS = "♓🧩 Piscēs";
+
+/** 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´.
+ */
+export const copyWithin = createCallableFunction(
+ 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;
+ },
+ });
-/** Returns whether the provided value is an array index. */
-export const isArrayIndex = ($) => {
- const value = canonicalNumericIndexString($);
- if (value !== undefined) {
- return Object.is(value, 0) || value > 0 && value < -1 >>> 0;
+ 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´.
+ */
+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´.
+ *
+ * ※ Unlike `Array::filter´, this function ignores
+ * `.constructor[Symbol.species]´ and always returns an array.
+ */
+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 {
- return false;
+ 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.
+ *
+ * ※ 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 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 value in the provided object which
+ * satisfies the provided 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 findFirstItem = ($, callback, thisArg = UNDEFINED) =>
+ findFirstIndexedEntry($, callback, thisArg)?.[1];
+
+/**
+ * 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 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´.
+ *
+ * ※ Flattening always produces a dense array.
+ */
+export const flatmap = createCallableFunction(
+ Array.prototype.flatMap,
+ { name: "flatmap" },
+);
+
+/**
+ * Returns the result of flattening the provided object according to
+ * the algorithm of `Array::flat´.
+ *
+ * ※ Flattening always produces a dense array.
+ */
+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´.
+ */
+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´.
+ */
+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´.
+ */
+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´.
+ */
+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´.
+ */
+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´.
+ *
+ * ※ This algorithm treats missing values as undefined rather than
+ * skipping them.
+ */
+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´.
+ */
+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´.
+ */
+export const indices = createCallableFunction(
+ 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
- * a few ways :—
+ * 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 corresponding
- * to 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 (!(isObject($) && "length" in $)) {
- // The provided value is not an object or does not have a `length`.
+ if (!(type($) === "object" && "length" in $)) {
+ // 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;
}
@@ -69,72 +895,249 @@ export const isCollection = ($) => {
};
/**
- * Returns whether the provided value is spreadable during array
- * concatenation.
- *
- * This is also used to determine which things should be treated as
- * collections.
+ * Returns an iterator over the items in the provided value according
+ * to the algorithm of `Array::values´.
*/
-export const isConcatSpreadable = ($) => {
- if (!isObject($)) {
- // The provided value is not an object.
- return false;
- } else {
- // The provided value is an object.
- return !!($[Symbol.isConcatSpreadable] ?? Array.isArray($));
- }
-};
+export const items = createCallableFunction(
+ Array.prototype.values,
+ { name: "items" },
+);
-/** Returns whether the provided value is an integer index. */
-export const isIntegerIndex = ($) => {
- const value = canonicalNumericIndexString($);
- if (value !== undefined) {
- return Object.is(value, 0) ||
- value > 0 && value <= MAX_SAFE_INTEGER;
- } else {
- return false;
- }
-};
+/**
+ * Returns the result of mapping the provided value with the provided
+ * callback according to the algorithm of `Array::map´.
+ */
+export const map = createCallableFunction(Array.prototype.map);
/**
- * Returns the length of the provided arraylike object.
- *
- * Will throw if the provided object is not arraylike.
- *
- * This produces larger lengths than can actually be stored in arrays,
- * because no such restrictions exist on arraylike methods. Use
- * `isIndex` to determine if a value is an actual array index.
+ * Pops from the provided value according to the algorithm of
+ * `Array::pop´.
*/
-export const lengthOfArrayLike = ({ length }) => {
- return toLength(length);
-};
+export const pop = createCallableFunction(Array.prototype.pop);
/**
- * Converts the provided value to an array index, or throws an error if
- * it is out of range.
+ * Pushes onto the provided value according to the algorithm of
+ * `Array::push´.
*/
-export const toIndex = ($) => {
- const integer = Math.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 push = createCallableFunction(Array.prototype.push);
+
+/**
+ * Returns the result of reducing the provided value with the provided
+ * callback, according to the algorithm of `Array::reduce´.
+ */
+export const reduce = createCallableFunction(Array.prototype.reduce);
+
+/**
+ * Reverses the provided value according to the algorithm of
+ * `Array::reverse´.
+ */
+export const reverse = createCallableFunction(Array.prototype.reverse);
+
+/**
+ * Shifts the provided value according to the algorithm of
+ * `Array::shift´.
+ */
+export const shift = createCallableFunction(Array.prototype.shift);
+
+/**
+ * Returns a slice of the provided value according to the algorithm of
+ * `Array::slice´.
+ */
+export const slice = createCallableFunction(Array.prototype.slice);
+
+/**
+ * Sorts the provided value in‐place according to the algorithm of
+ * `Array::sort´.
+ */
+export const sort = createCallableFunction(Array.prototype.sort);
+
+/**
+ * Splices into and out of the provided value according to the
+ * algorithm of `Array::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 {
- // 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;
+ }
}
- }
-};
+ };
-/** Converts the provided value to a length. */
-export const toLength = ($) => {
- const len = Math.floor($);
- return isNaN(len) || len == 0
- ? 0
- : Math.max(Math.min(len, MAX_SAFE_INTEGER), 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´.
+ */
+export const unshift = createCallableFunction(Array.prototype.unshift);