]> Lady’s Gitweb - Pisces/blobdiff - collection.js
Add denseProxy, various collection.js improvements
[Pisces] / collection.js
index 38b048cc063b61475366442a7e6488cca18fe4e4..74476857c6c5b5886c18fb291912e826d07b381f 100644 (file)
-// ♓🌟 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 <https://mozilla.org/MPL/2.0/>.
-
-import { call, makeCallable } from "./function.js";
+// SPDX-FileCopyrightText: 2020, 2021, 2022, 2023, 2025 Lady <https://www.ladys.computer/about/#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 <https://mozilla.org/MPL/2.0/>.
+ */
+
+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";
+  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, returning
- * a new collection according to the algorithm of Array::concat.
- */
-export const catenate = makeCallable(Array.prototype.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(Array.prototype.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 using the
- * algorithm of Array::fill.
+ * Fills the provided object with the provided value according to the
+ * algorithm of `Array::fill´.
  */
-export const fill = makeCallable(Array.prototype.fill);
+export const fill = createCallableFunction(Array.prototype.fill);
 
 /**
  * Returns the result of filtering the provided object with the
- * provided callback, using 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(Array.prototype.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 using 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(Array.prototype.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, using 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 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 findItem = makeCallable(Array.prototype.find);
+export const findLastItem = ($, callback, thisArg = UNDEFINED) =>
+  findLastIndexedEntry($, callback, thisArg)?.[1];
 
 /**
  * Returns the result of flatmapping the provided value with the
- * provided callback using the algorithm of Array::flatMap.
+ * provided callback according to the algorithm of `Array::flatMap´.
+ *
+ * ※ Flattening always produces a dense array.
  */
-export const flatmap = makeCallable(Array.prototype.flatMap);
+export const flatmap = createCallableFunction(
+  Array.prototype.flatMap,
+  { name: "flatmap" },
+);
 
 /**
- * Returns the result of flattening the provided object using the
- * algorithm of Array::flat.
+ * Returns the result of flattening the provided object according to
+ * the algorithm of `Array::flat´.
+ *
+ * ※ Flattening always produces a dense array.
  */
-export const flatten = makeCallable(Array.prototype.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(Array.prototype.indexOf);
+export const getFirstIndex = createCallableFunction(
+  Array.prototype.indexOf,
+  { name: "getFirstIndex" },
+);
 
 /**
- * Returns the item on the provided object at the provided index using
- * the algorithm of Array::at.
+ * Returns the item on the provided object at the provided index
+ * according to the algorithm of `Array::at´.
  */
-export const getItem = makeCallable(Array.prototype.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(Array.prototype.lastIndexOf);
+export const getLastIndex = createCallableFunction(
+  Array.prototype.lastIndexOf,
+  { name: "getLastIndex" },
+);
 
 /**
  * Returns whether every indexed value in the provided object satisfies
- * the provided function, using the algorithm of Array::every.
+ * the provided function, according to the algorithm of `Array::every´.
  */
-export const hasEvery = makeCallable(Array.prototype.every);
+export const hasEvery = createCallableFunction(
+  Array.prototype.every,
+  { name: "hasEvery" },
+);
 
 /**
  * Returns whether the provided object has an indexed value which
- * satisfies the provided function, using the algorithm of Array::some.
+ * satisfies the provided function, according to the algorithm of
+ * `Array::some´.
  */
-export const hasSome = makeCallable(Array.prototype.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.
+ * to the provided value according to the algorithm of
+ * `Array::includes´.
  *
- * > ☡ This algorithm treats missing values as `undefined` rather than
- * skipping them.
+ * ※ This algorithm treats missing values as undefined rather than
+ * skipping them.
  */
-export const includes = makeCallable(Array.prototype.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(Array.prototype.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(Array.prototype.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
- * Ecmascripts 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 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.
+ * to the algorithm of `Array::values´.
  */
-export const items = makeCallable(Array.prototype.values);
+export const items = createCallableFunction(
+  Array.prototype.values,
+  { name: "items" },
+);
 
 /**
- * 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.
+ * Returns the result of mapping the provided value with the provided
+ * callback according to the algorithm of `Array::map´.
  */
-export const lengthOfArraylike = ({ length }) => toLength(length);
+export const map = createCallableFunction(Array.prototype.map);
 
 /**
- * Returns the result of mapping the provided value with the provided
- * callback using the algorithm of Array::map.
+ * Pops from the provided value according to the algorithm of
+ * `Array::pop´.
  */
-export const map = makeCallable(Array.prototype.map);
-
-/** Pops from the provided value using the algorithm of Array::pop. */
-export const pop = makeCallable(Array.prototype.pop);
+export const pop = createCallableFunction(Array.prototype.pop);
 
 /**
- * Pushes onto the provided value using the algorithm of Array::push.
+ * Pushes onto the provided value according to the algorithm of
+ * `Array::push´.
  */
-export const push = makeCallable(Array.prototype.push);
+export const push = createCallableFunction(Array.prototype.push);
 
 /**
  * Returns the result of reducing the provided value with the provided
- * callback, using the algorithm of Array::reduce.
+ * callback, according to the algorithm of `Array::reduce´.
  */
-export const reduce = makeCallable(Array.prototype.reduce);
+export const reduce = createCallableFunction(Array.prototype.reduce);
 
 /**
- * Reverses the provided value using the algorithm of Array::reverse.
+ * Reverses the provided value according to the algorithm of
+ * `Array::reverse´.
  */
-export const reverse = makeCallable(Array.prototype.reverse);
-
-/** Shifts the provided value using the algorithm of Array::shift. */
-export const shift = makeCallable(Array.prototype.shift);
+export const reverse = createCallableFunction(Array.prototype.reverse);
 
 /**
- * Returns a slice of the provided value using the algorithm of
- * Array::slice.
+ * Shifts the provided value according to the algorithm of
+ * `Array::shift´.
  */
-export const slice = makeCallable(Array.prototype.slice);
+export const shift = createCallableFunction(Array.prototype.shift);
 
 /**
- * Sorts the provided value in‐place using the algorithm of
- * Array::sort.
+ * Returns a slice of the provided value according to the algorithm of
+ * `Array::slice´.
  */
-export const sort = makeCallable(Array.prototype.sort);
+export const slice = createCallableFunction(Array.prototype.slice);
 
 /**
- * Splices into and out of the provided value using the algorithm of
- * Array::splice.
+ * Sorts the provided value in‐place according to the algorithm of
+ * `Array::sort´.
  */
-export const splice = makeCallable(Array.prototype.splice);
+export const sort = createCallableFunction(Array.prototype.sort);
 
 /**
- * Returns the result of converting the provided value to an array
- * index, or throws an error if it is out of range.
+ * Splices into and out of the provided value according to the
+ * algorithm of `Array::splice´.
  */
-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 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;
+      }
     }
-  }
-};
+  };
 
-/** 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 using the algorithm of Array::unshift.
+ * Unshifts the provided value according to the algorithm of
+ * `Array::unshift´.
  */
-export const unshift = makeCallable(Array.prototype.unshift);
+export const unshift = createCallableFunction(Array.prototype.unshift);
This page took 0.440958 seconds and 4 git commands to generate.