From: Lady Date: Wed, 13 Aug 2025 02:25:03 +0000 (-0400) Subject: Add denseProxy, various collection.js improvements X-Git-Url: https://git.ladys.computer/Pisces/commitdiff_plain/HEAD?ds=inline;hp=e1cb83c479df2a3e4a5e918867a135ff9dde8121 Add denseProxy, various collection.js improvements This commit is a bit messy, but splitting it out seems difficult. More to come with regards to testing the remainder of `collection.js`. Originally, there were plans for both `denseProxy` and a `revocableDenseProxy`, but the latter is of unclear utility (this is hardly the use·case that revocable proxies are designed for), so it was removed. `concat`, `filter`, and `toArray` now always return an array instead of respecting `.constructor[Symbol.species]`. `toArray` now supports sparse arrays (if the argument is a sparse arraylike object, or if an iterator does not produce a value when it yields); there is now also a `toDenseArray` with behaviour more similar to `Array.from`. Awkwardly, `toArray` won¦t preserve sparseness when called with an existing sparse array, since the `.[Symbol.iterator]` method of arrays yields a value of `undefined` for missing keys. And supporting a semantic distinction between iterator results with a `.value` of `undefined` and those with no `.value` present is definitely a grey area of the Ecmascript specification. So the following changes may be called for :— - Disallowing sparseness when calling `toArray` with an iterable, because “sparse iterables” is not a weldefined concept, and - When calling `toArray` with a “collection” (as ♓🌟 Piscēs has them), prioritizing iterating over its indices rather than treating it as an iterable. It is an open question whether objects which are neither iterables nor collections (i·e, that don¦t respond to `.[Symbol.iterator]`, are not concat‐spreadable, or which do not have a valid length) should be supported in `toArray` and kin at all. (It is probably worth renaming “collection” to “indexed collection” at some point.) The “finding” methods are now `findFirstIndex`, `findFirstEntry`, and `findFirstItem` (and there are corresponding `findLast—` versions). These differ from the built·in methods in that they return `undefined`, not `-1`, if a match is not found. --- 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); diff --git a/collection.test.js b/collection.test.js index c066932..079ef4b 100644 --- a/collection.test.js +++ b/collection.test.js @@ -1,54 +1,1201 @@ -// ♓🌟 Piscēs ∷ collection.test.js -// ==================================================================== -// -// Copyright © 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 . +// SPDX-FileCopyrightText: 2022, 2023, 2025 Lady +// SPDX-License-Identifier: MPL-2.0 +/** + * ⁌ ♓🧩 Piscēs ∷ collection.js + * + * Copyright © 2022–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 { assertEquals, assertSpyCall, assertSpyCalls, assertStrictEquals, + assertThrows, describe, it, spy, } from "./dev-deps.js"; -import { findIndexedEntry, isCollection } from "./collection.js"; +import { + array, + concatSpreadableCatenate, + copyWithin, + denseProxy, + fill, + filter, + findFirstIndex, + findFirstIndexedEntry, + findFirstItem, + findLastIndex, + findLastIndexedEntry, + findLastItem, + flatmap, + flatten as _flatten /* TK */, + getFirstIndex as _getFirstIndex /* TK */, + getItem as _getItem /* TK */, + getLastIndex as _getLastIndex /* TK */, + hasEvery as _hasEvery /* TK */, + hasSome as _hasSome /* TK */, + includes as _includes /* TK */, + indexedEntries as _indexedEntries /* TK */, + indices as _indices /* TK */, + isArray as _isArray /* TK */, + isCollection, + isDenseProxy, + items as _items /* TK */, + map as _map /* TK */, + pop as _pop /* TK */, + push as _push /* TK */, + reduce as _reduce /* TK */, + reverse as _reverse /* TK */, + shift as _shift /* TK */, + slice as _slice /* TK */, + sort as _sort /* TK */, + splice as _splice /* TK */, + toArray, + toDenseArray, + unshift as _unshift /* TK */, +} from "./collection.js"; + +describe("array", () => { + it("[[Call]] returns an array of the provided values", () => { + assertEquals(array("etaoin", [], true), ["etaoin", [], true]); + }); + + it("[[Call]] returns an empty array with no arguments", () => { + assertEquals(array(), []); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new array()); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(array.length, 0); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(array.name, "array"); + }); + }); +}); + +describe("concatSpreadableCatenate", () => { + it("[[Call]] catenates concat spreadable values", () => { + assertEquals( + concatSpreadableCatenate([1, 2], [2, [3]], { + length: 1, + [Symbol.isConcatSpreadable]: true, + }), + [1, 2, 2, [3], ,], + ); + }); + + it("[[Call]] does not catenate other values", () => { + assertEquals(concatSpreadableCatenate({}, "etaoin"), [ + {}, + "etaoin", + ]); + }); + + it("[[Call]] allows a nullish first argument", () => { + assertEquals(concatSpreadableCatenate(null), [null]); + assertEquals(concatSpreadableCatenate(undefined), [undefined]); + }); + + it("[[Call]] returns an empty array with no arguments", () => { + assertEquals(concatSpreadableCatenate(), []); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new concatSpreadableCatenate()); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(concatSpreadableCatenate.length, 2); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + concatSpreadableCatenate.name, + "concatSpreadableCatenate", + ); + }); + }); +}); + +describe("copyWithin", () => { + it("[[Call]] copies within", () => { + assertEquals( + copyWithin(["a", "b", "c", , "e"], 0, 3, 4), + [, "b", "c", , "e"], + ); + assertEquals( + copyWithin(["a", "b", "c", , "e"], 1, 3), + ["a", , "e", , "e"], + ); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new copyWithin([], 0, 0)); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(copyWithin.length, 3); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(copyWithin.name, "copyWithin"); + }); + }); +}); + +describe("denseProxy", () => { + const makeUnderlying = () => + Object.create({ 2: "inherited" }, { + // 0 is not present + 1: { value: "static", configurable: true, writable: true }, + // 2 is not an own property, but is present on the prototype + 3: { configurable: true, get: () => "dynamic" }, + length: { value: "4", configurable: true, writable: true }, + }); + + it("[[Call]] returns an object which inherits from the provided object", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Object.getPrototypeOf(proxy), + underlying, + ); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new denseProxy([])); + }); + + it("[[OwnPropertyKeys]] lists integer indices, then the length, then other keys", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + const sym = Symbol(); + proxy[sym] = "shrdlu"; + proxy["1.2.3"] = "etaion"; + assertEquals( + Reflect.ownKeys(proxy), + ["0", "1", "2", "3", "length", "1.2.3", sym], + ); + }); + + it("[[PreventExtensions]] fails if the underlying object is extensible", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.preventExtensions(proxy), + false, + ); + }); + + it("[[PreventExtensions]] fails if the underlying object has a nonconstant length", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + Object.defineProperty(underlying, "length", { get: () => 4 }); + Object.freeze(underlying); + assertStrictEquals( + Reflect.preventExtensions(proxy), + false, + ); + }); + + it("[[PreventExtensions]] succeeds if the underlying object is non·extensible and has a constant length", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + Object.defineProperty(underlying, "length", { + configurable: false, + writable: false, + }); + Object.preventExtensions(underlying); + assertStrictEquals( + Reflect.preventExtensions(proxy), + true, + ); + }); + + it("[[SetPrototypeOf]] fails to change the prototype", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.setPrototypeOf(proxy, {}), + false, + ); + }); + + it("[[SetPrototypeOf]] succeeds keeping the prototype the same", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.setPrototypeOf(proxy, underlying), + true, + ); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(denseProxy.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(denseProxy.name, "denseProxy"); + }); + }); + + describe("~[]", () => { + it("[[DefineProperty]] allows changes when the property is not an index property or length", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + const newValue = Symbol(); + proxy.etaoin = newValue; + assertStrictEquals( + proxy.etaoin, + newValue, + ); + }); + + it("[[DefineProperty]] allows changing nothing when the property is not an own property", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.defineProperty( + proxy, + "0", + { + configurable: true, + enumerable: true, + writable: false, + value: undefined, + }, + ), + true, + ); + assertStrictEquals( + Reflect.defineProperty( + proxy, + "2", + { + configurable: true, + enumerable: true, + writable: false, + value: undefined, + }, + ), + true, + ); + /* test nonconfigurable versions too */ + Object.freeze(underlying); + Object.defineProperty(proxy, "0", { configurable: false }); + Object.defineProperty(proxy, "2", { configurable: false }); + assertStrictEquals( + Reflect.defineProperty( + proxy, + "0", + Object.getOwnPropertyDescriptor(proxy, "0"), + ), + true, + ); + assertStrictEquals( + Reflect.defineProperty( + proxy, + "2", + Object.getOwnPropertyDescriptor(proxy, "2"), + ), + true, + ); + }); + + it("[[DefineProperty]] allows changing nothing when the property is a data index property", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.defineProperty( + proxy, + "1", + { + configurable: true, + enumerable: true, + writable: false, + value: underlying[1], + }, + ), + true, + ); + /* test nonconfigurable versions too */ + Object.freeze(underlying); + Object.defineProperty(proxy, "1", { configurable: false }); + assertStrictEquals( + Reflect.defineProperty( + proxy, + "1", + Object.getOwnPropertyDescriptor(proxy, "1"), + ), + true, + ); + }); + + it("[[DefineProperty]] allows changing nothing when the property is an accessor index property", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.defineProperty( + proxy, + "3", + { + configurable: true, + enumerable: true, + writable: false, + value: underlying[3], + }, + ), + true, + ); + /* test nonconfigurable versions too */ + Object.freeze(underlying); + Object.defineProperty(proxy, "1", { configurable: false }); + assertStrictEquals( + Reflect.defineProperty( + proxy, + "1", + Object.getOwnPropertyDescriptor(proxy, "1"), + ), + true, + ); + }); + + it("[[DefineProperty]] does not allow a change in enumerablility on index properties", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + for (let i = 0; i < 4; ++i) { + assertStrictEquals( + Reflect.defineProperty(proxy, i, { enumerable: false }), + false, + ); + } + }); + + it("[[DefineProperty]] does not allow a change in value on index properties", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + for (let i = 0; i < 4; ++i) { + assertStrictEquals( + Reflect.defineProperty(proxy, i, { value: "new value" }) + && (() => { + throw i; + })(), + false, + ); + } + }); + + it("[[DefineProperty]] does not allow a change in getter on index properties", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + for (let i = 0; i < 4; ++i) { + assertStrictEquals( + Reflect.defineProperty(proxy, i, { + get: () => underlying[i], + }) && (() => { + throw i; + })(), + false, + ); + } + }); + + it("[[DefineProperty]] does not allow a change in setter on index properties", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + for (let i = 0; i < 4; ++i) { + assertStrictEquals( + Reflect.defineProperty(proxy, i, { set: () => undefined }), + false, + ); + } + }); + + it("[[DefineProperty]] does not allow a change in configurability on index properties if the property in the underlying object may change", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + Object.defineProperty(underlying, "length", { + configurable: false, + writable: false, + }); + for (let i = 0; i < 4; ++i) { + assertStrictEquals( + Reflect.defineProperty(proxy, i, { configurable: false }), + false, + ); + } + }); + + it("[[DefineProperty]] does not allow a change in configurability on index properties if the length of the underlying object may change", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + Object.seal(underlying); + for (let i = 0; i < 4; ++i) { + assertStrictEquals( + Reflect.defineProperty(proxy, i, { configurable: false }), + false, + ); + } + }); + + it("[[DefineProperty]] allows a change in configurability on index properties when it is safe to do so", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + Object.defineProperty(underlying, "length", { + configurable: false, + writable: false, + }); + Object.defineProperty(underlying, "1", { + configurable: false, + writable: false, + }); + Object.defineProperty(underlying, "3", { configurable: false }); + Object.preventExtensions(underlying); + for (let i = 0; i < 4; ++i) { + assertStrictEquals( + Reflect.defineProperty(proxy, i, { configurable: false }), + true, + ); + } + }); + + it("[[Delete]] is allowed when the property is not an index property or length", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + proxy.a = "etaoin"; + assertStrictEquals( + Reflect.deleteProperty(proxy, "a"), + true, + ); + assertStrictEquals( + Reflect.has(proxy, "a"), + false, + ); + }); + + it("[[Delete]] is forbidden for index properties less than the length", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + for (let i = 0; i < 4; ++i) { + assertStrictEquals( + Reflect.deleteProperty(proxy, i), + false, + ); + } + }); + + it("[[Delete]] is allowed for index properties greater than or equal to the length", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.deleteProperty(proxy, "4"), + true, + ); + assertStrictEquals( + Reflect.deleteProperty(proxy, "5"), + true, + ); + }); + + it("[[GetOwnProperty]] gives the value of an index property as a data property by default", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertEquals( + Object.getOwnPropertyDescriptor(proxy, 1), + { + configurable: true, + enumerable: true, + value: underlying[1], + writable: false, + }, + ); + assertEquals( + Object.getOwnPropertyDescriptor(proxy, 3), + { + configurable: true, + enumerable: true, + value: underlying[3], + writable: false, + }, + ); + /* test nonconfigurable data properties too */ + Object.freeze(underlying); + Object.defineProperty(proxy, "1", { configurable: false }); + assertEquals( + Object.getOwnPropertyDescriptor(proxy, 1), + { + configurable: false, + enumerable: true, + value: underlying[1], + writable: false, + }, + ); + }); + + it("[[GetOwnProperty]] gives a value of undefined if the underlying object does not have an index property as an own property", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertEquals( + Object.getOwnPropertyDescriptor(proxy, 0), + { + configurable: true, + enumerable: true, + value: undefined, + writable: false, + }, + ); + assertEquals( + Object.getOwnPropertyDescriptor(proxy, 2), + { + configurable: true, + enumerable: true, + value: undefined, + writable: false, + }, + ); + }); + + it("[[GetOwnProperty]] gives a value of undefined for index properties less than the length", () => { + const underlying = makeUnderlying(); + underlying.length = 0; + const proxy = denseProxy(underlying); + for (let i = 0; i < 4; ++i) { + assertStrictEquals( + Object.getOwnPropertyDescriptor(proxy, i), + undefined, + ); + } + }); + + it("[[GetOwnProperty]] gives a getter when the underlying object has a getter and an index property is not configurable", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + Object.freeze(underlying); + Object.defineProperty(proxy, "3", { configurable: false }); + const descriptor = Object.getOwnPropertyDescriptor(proxy, 3); + assertEquals( + descriptor, + { + configurable: false, + enumerable: true, + get: descriptor.get, + set: undefined, + }, + ); + }); + + describe("[[GetOwnProperty]].get.length", () => { + it("[[Get]] returns the correct length", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + Object.freeze(underlying); + Object.defineProperty(proxy, "3", { configurable: false }); + assertStrictEquals( + Object.getOwnPropertyDescriptor( + proxy, + "3", + ).get.length, + 0, + ); + }); + }); + + describe("[[GetOwnProperty]].get.name", () => { + it("[[Get]] returns the correct name", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + Object.freeze(underlying); + Object.defineProperty(proxy, "3", { configurable: false }); + assertStrictEquals( + Object.getOwnPropertyDescriptor( + proxy, + "3", + ).get.name, + "get 3", + ); + }); + }); + + it("[[HasProperty]] works when the property is not an index property or length", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + proxy.a = "etaoin"; + Object.getPrototypeOf(underlying).b = "shrdlu"; + assertStrictEquals( + Reflect.has(proxy, "a"), + true, + ); + assertStrictEquals( + Reflect.has(proxy, "b"), + true, + ); + assertStrictEquals( + Reflect.has(proxy, "z"), + false, + ); + }); + + it("[[HasProperty]] works for index properties less than the length", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + for (let i = 0; i < 4; ++i) { + assertStrictEquals( + Reflect.has(proxy, i), + true, + ); + } + delete underlying.length; + for (let i = 0; i < 4; ++i) { + assertStrictEquals( + Reflect.has(proxy, i), + Reflect.has(underlying, i), + ); + } + }); + + it("[[HasProperty]] works for index properties greater than or equal to the length", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.has(proxy, "4"), + false, + ); + assertStrictEquals( + Reflect.has(proxy, "5"), + false, + ); + underlying[4] = "inherited now"; + assertStrictEquals( + Reflect.has(proxy, "4"), + true, + ); + }); + }); + + describe("~length", () => { + it("[[DefineProperty]] allows changing nothing", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.defineProperty( + proxy, + "length", + { + configurable: true, + enumerable: false, + writable: false, + value: underlying.length >>> 0, + }, + ), + true, + ); + /* test nonconfigurable versions too */ + Object.freeze(underlying); + Object.defineProperty(proxy, "length", { configurable: false }); + assertStrictEquals( + Reflect.defineProperty( + proxy, + "length", + Object.getOwnPropertyDescriptor(proxy, "length"), + ), + true, + ); + }); + + it("[[DefineProperty]] does not allow a change in enumerablility", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.defineProperty(proxy, "length", { enumerable: true }), + false, + ); + }); + + it("[[DefineProperty]] does not allow a change in value", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.defineProperty(proxy, "length", { value: 0 }), + false, + ); + }); + + it("[[DefineProperty]] does not allow a change in getter", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.defineProperty(proxy, "length", { + get: () => underlying.length >>> 0, + }), + false, + ); + }); + + it("[[DefineProperty]] does not allow a change in setter", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.defineProperty(proxy, "length", { set: () => {} }), + false, + ); + }); + + it("[[DefineProperty]] does not allow a change in configurability if the length of the underlying object may change", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.defineProperty(proxy, "length", { + configurable: false, + }), + false, + ); + Object.defineProperty(underlying, "length", { + configurable: false, + get: () => 0, + }); + assertStrictEquals( + Reflect.defineProperty(proxy, "length", { + configurable: false, + }), + false, + ); + }); + + it("[[DefineProperty]] allows a change in configurability when it is safe to do so", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + Object.defineProperty(underlying, "length", { + configurable: false, + writable: false, + }); + assertStrictEquals( + Reflect.defineProperty(proxy, "length", { + configurable: false, + }), + true, + ); + }); + + it("[[Delete]] is forbidden", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.deleteProperty( + proxy, + "length", + ), + false, + ); + }); + + it("[[GetOwnProperty]] gives the value as a data property", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertEquals( + Object.getOwnPropertyDescriptor(proxy, "length"), + { + configurable: true, + enumerable: false, + value: underlying.length >>> 0, + writable: false, + }, + ); + /* test nonconfigurable data properties too */ + Object.freeze(underlying); + Object.defineProperty(proxy, "length", { configurable: false }); + assertEquals( + Object.getOwnPropertyDescriptor(proxy, "length"), + { + configurable: false, + enumerable: false, + value: underlying.length >>> 0, + writable: false, + }, + ); + }); + + it("[[GetOwnProperty]] gives 0 if the underlying object does not have the property as an own property", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + delete underlying.length; + Object.getPrototypeOf(underlying).length = 3; + assertEquals( + Object.getOwnPropertyDescriptor(proxy, "length"), + { + configurable: true, + enumerable: false, + value: 0, + writable: false, + }, + ); + }); + + it("[[HasProperty]] is always true", () => { + const underlying = makeUnderlying(); + const proxy = denseProxy(underlying); + assertStrictEquals( + Reflect.has(proxy, "length"), + true, + ); + delete underlying.length; + assertStrictEquals( + Reflect.has(proxy, "length"), + true, + ); + }); + }); +}); + +describe("fill", () => { + it("[[Call]] fills", () => { + assertEquals( + fill({ 1: "failure", length: 3 }, "success"), + { 0: "success", 1: "success", 2: "success", length: 3 }, + ); + }); + + it("[[Call]] can fill a range", () => { + assertEquals( + fill({ 1: "failure", length: 4 }, "success", 1, 3), + { 1: "success", 2: "success", length: 4 }, + ); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new fill([], null)); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(fill.length, 2); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(fill.name, "fill"); + }); + }); +}); + +describe("filter", () => { + it("[[Call]] filters", () => { + assertEquals( + filter(["failure", "success", ,], function ($) { + return !$ || $ == this; + }, "success"), + ["success"], + ); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new filter([], () => {})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(filter.length, 2); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(filter.name, "filter"); + }); + }); +}); + +describe("findFirstIndex", () => { + it("[[Call]] returns undefined if no matching entry exists", () => { + assertStrictEquals(findFirstIndex([], () => true), undefined); + assertStrictEquals(findFirstIndex([1], () => false), undefined); + }); + + it("[[Call]] returns an index for the first match", () => { + assertStrictEquals( + findFirstIndex([, true, false], ($) => $ ?? true), + 1, + ); + assertStrictEquals( + findFirstIndex( + ["failure", "success", "success"], + ($) => $ == "success", + ), + 1, + ); + }); + + it("[[Call]] works on arraylike objects", () => { + assertStrictEquals( + findFirstIndex({ 1: "success", length: 2 }, ($) => $), + 1, + ); + assertStrictEquals( + findFirstIndex({ 1: "failure", length: 1 }, ($) => $), + undefined, + ); + }); + + it("[[Call]] only gets the value once", () => { + const get1 = spy(() => true); + findFirstIndex({ + get 1() { + return get1(); + }, + length: 2, + }, ($) => $); + assertSpyCalls(get1, 1); + }); + + it("[[Call]] passes the value, index, and this value to the callback", () => { + const arr = [, "failure", "success", "success"]; + const callback = spy(($) => $ === "success"); + const thisArg = {}; + findFirstIndex(arr, callback, thisArg); + assertSpyCalls(callback, 2); + assertSpyCall(callback, 0, { + args: ["failure", 1, arr], + self: thisArg, + }); + assertSpyCall(callback, 1, { + args: ["success", 2, arr], + self: thisArg, + }); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new findFirstIndex([], () => {})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(findFirstIndex.length, 2); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(findFirstIndex.name, "findFirstIndex"); + }); + }); +}); -describe("findIndexedEntry", () => { +describe("findFirstIndexedEntry", () => { it("[[Call]] returns undefined if no matching entry exists", () => { - assertStrictEquals(findIndexedEntry([], () => true), void {}); - assertStrictEquals(findIndexedEntry([1], () => false), void {}); + assertStrictEquals( + findFirstIndexedEntry([], () => true), + undefined, + ); + assertStrictEquals( + findFirstIndexedEntry([1], () => false), + undefined, + ); }); it("[[Call]] returns an entry for the first match", () => { assertEquals( - findIndexedEntry([, true, false], ($) => $ ?? true), - [0, void {}], + findFirstIndexedEntry([, true, false], ($) => $ ?? true), + [1, true], ); assertEquals( - findIndexedEntry(["failure", "success"], ($) => $ == "success"), + findFirstIndexedEntry( + ["failure", "success", "success"], + ($) => $ == "success", + ), [1, "success"], ); }); it("[[Call]] works on arraylike objects", () => { assertEquals( - findIndexedEntry({ 1: "success", length: 2 }, ($) => $), + findFirstIndexedEntry({ 1: "success", length: 2 }, ($) => $), [1, "success"], ); assertEquals( - findIndexedEntry({ 1: "failure", length: 1 }, ($) => $), - void {}, + findFirstIndexedEntry({ 1: "failure", length: 1 }, ($) => $), + undefined, + ); + }); + + it("[[Call]] only gets the value once", () => { + const get1 = spy(() => true); + findFirstIndexedEntry({ + get 1() { + return get1(); + }, + length: 2, + }, ($) => $); + assertSpyCalls(get1, 1); + }); + + it("[[Call]] passes the value, index, and this value to the callback", () => { + const arr = [, "failure", "success", "success"]; + const callback = spy(($) => $ === "success"); + const thisArg = {}; + findFirstIndexedEntry(arr, callback, thisArg); + assertSpyCalls(callback, 2); + assertSpyCall(callback, 0, { + args: ["failure", 1, arr], + self: thisArg, + }); + assertSpyCall(callback, 1, { + args: ["success", 2, arr], + self: thisArg, + }); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new findFirstIndexedEntry([], () => {})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(findFirstIndexedEntry.length, 2); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + findFirstIndexedEntry.name, + "findFirstIndexedEntry", + ); + }); + }); +}); + +describe("findFirstItem", () => { + it("[[Call]] returns undefined if no matching item exists", () => { + assertStrictEquals(findFirstItem([], () => true), undefined); + assertStrictEquals(findFirstItem([1], () => false), undefined); + }); + + it("[[Call]] returns the first match", () => { + assertStrictEquals( + findFirstItem([, true, false], ($) => $ ?? true), + true, + ); + assertStrictEquals( + findFirstItem( + ["failure", "success", "success"], + ($) => $ == "success", + ), + "success", + ); + }); + + it("[[Call]] works on arraylike objects", () => { + assertStrictEquals( + findFirstItem({ 1: "success", length: 2 }, ($) => $), + "success", + ); + assertStrictEquals( + findFirstItem({ 1: "failure", length: 1 }, ($) => $), + undefined, + ); + }); + + it("[[Call]] only gets the value once", () => { + const get1 = spy(() => true); + findFirstItem({ + get 1() { + return get1(); + }, + length: 2, + }, ($) => $); + assertSpyCalls(get1, 1); + }); + + it("[[Call]] passes the value, index, and this value to the callback", () => { + const arr = [, "failure", "success", "success"]; + const callback = spy(($) => $ === "success"); + const thisArg = {}; + findFirstItem(arr, callback, thisArg); + assertSpyCalls(callback, 2); + assertSpyCall(callback, 0, { + args: ["failure", 1, arr], + self: thisArg, + }); + assertSpyCall(callback, 1, { + args: ["success", 2, arr], + self: thisArg, + }); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new findFirstItem([], () => {})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(findFirstItem.length, 2); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(findFirstItem.name, "findFirstItem"); + }); + }); +}); + +describe("findLastIndex", () => { + it("[[Call]] returns undefined if no matching entry exists", () => { + assertStrictEquals(findLastIndex([], () => true), undefined); + assertStrictEquals(findLastIndex([1], () => false), undefined); + }); + + it("[[Call]] returns an index for the first match", () => { + assertStrictEquals( + findLastIndex([true, false, ,], ($) => $ ?? true), + 0, + ); + assertStrictEquals( + findLastIndex( + ["success", "success", "failure"], + ($) => $ == "success", + ), + 1, + ); + }); + + it("[[Call]] works on arraylike objects", () => { + assertStrictEquals( + findLastIndex({ 1: "success", length: 2 }, ($) => $), + 1, + ); + assertStrictEquals( + findLastIndex({ 1: "failure", length: 1 }, ($) => $), + undefined, ); }); it("[[Call]] only gets the value once", () => { const get1 = spy(() => true); - findIndexedEntry({ + findLastIndex({ get 1() { return get1(); }, @@ -58,13 +1205,13 @@ describe("findIndexedEntry", () => { }); it("[[Call]] passes the value, index, and this value to the callback", () => { - const arr = ["failure", "success", "success"]; + const arr = ["success", "success", "failure", ,]; const callback = spy(($) => $ === "success"); const thisArg = {}; - findIndexedEntry(arr, callback, thisArg); + findLastIndex(arr, callback, thisArg); assertSpyCalls(callback, 2); assertSpyCall(callback, 0, { - args: ["failure", 0, arr], + args: ["failure", 2, arr], self: thisArg, }); assertSpyCall(callback, 1, { @@ -72,6 +1219,206 @@ describe("findIndexedEntry", () => { self: thisArg, }); }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new findLastIndex([], () => {})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(findLastIndex.length, 2); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(findLastIndex.name, "findLastIndex"); + }); + }); +}); + +describe("findLastIndexedEntry", () => { + it("[[Call]] returns undefined if no matching entry exists", () => { + assertStrictEquals( + findLastIndexedEntry([], () => true), + undefined, + ); + assertStrictEquals( + findLastIndexedEntry([1], () => false), + undefined, + ); + }); + + it("[[Call]] returns an index for the first match", () => { + assertEquals( + findLastIndexedEntry([true, false, ,], ($) => $ ?? true), + [0, true], + ); + assertEquals( + findLastIndexedEntry( + ["success", "success", "failure"], + ($) => $ == "success", + ), + [1, "success"], + ); + }); + + it("[[Call]] works on arraylike objects", () => { + assertEquals( + findLastIndexedEntry({ 1: "success", length: 2 }, ($) => $), + [1, "success"], + ); + assertEquals( + findLastIndexedEntry({ 1: "failure", length: 1 }, ($) => $), + undefined, + ); + }); + + it("[[Call]] only gets the value once", () => { + const get1 = spy(() => true); + findLastIndexedEntry({ + get 1() { + return get1(); + }, + length: 2, + }, ($) => $); + assertSpyCalls(get1, 1); + }); + + it("[[Call]] passes the value, index, and this value to the callback", () => { + const arr = ["success", "success", "failure", ,]; + const callback = spy(($) => $ === "success"); + const thisArg = {}; + findLastIndexedEntry(arr, callback, thisArg); + assertSpyCalls(callback, 2); + assertSpyCall(callback, 0, { + args: ["failure", 2, arr], + self: thisArg, + }); + assertSpyCall(callback, 1, { + args: ["success", 1, arr], + self: thisArg, + }); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new findLastIndexedEntry([], () => {})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(findLastIndexedEntry.length, 2); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + findLastIndexedEntry.name, + "findLastIndexedEntry", + ); + }); + }); +}); + +describe("findLastItem", () => { + it("[[Call]] returns undefined if no matching entry exists", () => { + assertStrictEquals(findLastItem([], () => true), undefined); + assertStrictEquals(findLastItem([1], () => false), undefined); + }); + + it("[[Call]] returns an index for the first match", () => { + assertStrictEquals( + findLastItem([true, false, ,], ($) => $ ?? true), + true, + ); + assertStrictEquals( + findLastItem( + ["success", "success", "failure"], + ($) => $ == "success", + ), + "success", + ); + }); + + it("[[Call]] works on arraylike objects", () => { + assertStrictEquals( + findLastItem({ 1: "success", length: 2 }, ($) => $), + "success", + ); + assertStrictEquals( + findLastItem({ 1: "failure", length: 1 }, ($) => $), + undefined, + ); + }); + + it("[[Call]] only gets the value once", () => { + const get1 = spy(() => true); + findLastItem({ + get 1() { + return get1(); + }, + length: 2, + }, ($) => $); + assertSpyCalls(get1, 1); + }); + + it("[[Call]] passes the value, index, and this value to the callback", () => { + const arr = ["success", "success", "failure", ,]; + const callback = spy(($) => $ === "success"); + const thisArg = {}; + findLastItem(arr, callback, thisArg); + assertSpyCalls(callback, 2); + assertSpyCall(callback, 0, { + args: ["failure", 2, arr], + self: thisArg, + }); + assertSpyCall(callback, 1, { + args: ["success", 1, arr], + self: thisArg, + }); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new findLastItem([], () => {})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(findLastItem.length, 2); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(findLastItem.name, "findLastItem"); + }); + }); +}); + +describe("flatmap", () => { + it("[[Call]] flatmaps", () => { + assertEquals( + flatmap([, "a", ["b"], [["c"]]], ($) => $), + ["a", "b", ["c"]], + ); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new flatmap([], () => {})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(flatmap.length, 2); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(flatmap.name, "flatmap"); + }); + }); }); describe("isCollection", () => { @@ -140,3 +1487,55 @@ describe("isCollection", () => { ); }); }); + +describe("isDenseProxy", () => { + it("[[Call]] returns true for dense proxies", () => { + const underlying = []; + const proxy = denseProxy(underlying); + assertStrictEquals( + isDenseProxy(proxy), + true, + ); + }); + + it("[[Call]] returns false for others", () => { + assertStrictEquals( + isDenseProxy([]), + false, + ); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new isDenseProxy([])); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(isDenseProxy.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(isDenseProxy.name, "isDenseProxy"); + }); + }); +}); + +describe("toArray", () => { + it("[[Call]] returns an array of the values in a provided arraylike", () => { + assertEquals( + toArray({ 1: "success", length: 3 }), + [, "success", ,], + ); + }); +}); + +describe("toDenseArray", () => { + it("[[Call]] returns a dense array of the values in a provided arraylike", () => { + assertEquals( + toDenseArray({ 1: "success", length: 3 }), + [undefined, "success", undefined], + ); + }); +}); diff --git a/function.test.js b/function.test.js index a5c8311..1b9ea74 100644 --- a/function.test.js +++ b/function.test.js @@ -668,7 +668,7 @@ describe("createProxyConstructor", () => { }); }); - describe("~", () => { + describe("()", () => { it("[[Call]] throws an error", () => { assertThrows(() => { createProxyConstructor({})(); @@ -713,7 +713,7 @@ describe("createProxyConstructor", () => { }); }); - describe("~is[[.name]]", () => { + describe("().is[[.name]]", () => { it("[[GetOwnProperty]] defines the appropriate method", () => { assertNotStrictEquals( Object.getOwnPropertyDescriptor( @@ -802,7 +802,7 @@ describe("createProxyConstructor", () => { }); }); - describe("~length", () => { + describe("().length", () => { it("[[GetOwnProperty]] has the correct descriptor", () => { assertEquals( Object.getOwnPropertyDescriptor( @@ -819,7 +819,7 @@ describe("createProxyConstructor", () => { }); }); - describe("~name", () => { + describe("().name", () => { it("[[GetOwnProperty]] has the correct descriptor", () => { assertEquals( Object.getOwnPropertyDescriptor( @@ -836,7 +836,7 @@ describe("createProxyConstructor", () => { }); }); - describe("~prototype", () => { + describe("().prototype", () => { it("[[GetOwnProperty]] has the correct descriptor", () => { assertEquals( Object.getOwnPropertyDescriptor( @@ -853,7 +853,7 @@ describe("createProxyConstructor", () => { }); }); - describe("~revocable", () => { + describe("().revocable", () => { it("[[Call]] produces a revocable proxy", () => { const obj = {}; const proxyConstructor = createProxyConstructor({ diff --git a/numeric.js b/numeric.js index 079e56e..c4d07f8 100644 --- a/numeric.js +++ b/numeric.js @@ -1,11 +1,14 @@ -// ♓🌟 Piscēs ∷ numeric.js -// ==================================================================== -// -// Copyright © 2022–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 . +// SPDX-FileCopyrightText: 2022, 2023, 2025 Lady +// SPDX-License-Identifier: MPL-2.0 +/** + * ⁌ ♓🧩 Piscēs ∷ numeric.js + * + * Copyright © 2022–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 } from "./function.js"; import { defineOwnDataProperty } from "./object.js"; @@ -25,10 +28,12 @@ import { UNDEFINED, } from "./value.js"; +const PISCĒS = "♓🧩 Piscēs"; + /** * Returns the magnitude (absolute value) of the provided value. * - * ※ Unlike `Math.abs`, this function can take big·int arguments. + * ※ Unlike `Math.abs´, this function can take big·int arguments. */ export const abs = ($) => { const n = toNumeric($); @@ -48,7 +53,7 @@ export const abs = ($) => { /** * Returns the arccos of the provided value. * - * ※ This function is effectively an alias for `Math.acos`. + * ※ This function is effectively an alias for `Math.acos´. * * ☡ This function does not allow big·int arguments. */ @@ -60,7 +65,7 @@ export const arccos = createArrowFunction( /** * Returns the arccosh of the provided value. * - * ※ This function is effectively an alias for `Math.acosh`. + * ※ This function is effectively an alias for `Math.acosh´. * * ☡ This function does not allow big·int arguments. */ @@ -72,7 +77,7 @@ export const arccosh = createArrowFunction( /** * Returns the arcsin of the provided value. * - * ※ This function is effectively an alias for `Math.asin`. + * ※ This function is effectively an alias for `Math.asin´. * * ☡ This function does not allow big·int arguments. */ @@ -84,7 +89,7 @@ export const arcsin = createArrowFunction( /** * Returns the arcsinh of the provided value. * - * ※ This function is effectively an alias for `Math.asinh`. + * ※ This function is effectively an alias for `Math.asinh´. * * ☡ This function does not allow big·int arguments. */ @@ -96,7 +101,7 @@ export const arcsinh = createArrowFunction( /** * Returns the arctan of the provided value. * - * ※ This function is effectively an alias for `Math.atan`. + * ※ This function is effectively an alias for `Math.atan´. * * ☡ This function does not allow big·int arguments. */ @@ -109,7 +114,7 @@ export const { /** * Returns the arctangent of the dividend of the provided values. * - * ※ Unlike `Math.atan2`, this function can take big·int arguments. + * ※ Unlike `Math.atan2´, this function can take big·int arguments. * However, the result will always be a number. */ arctan2, @@ -118,7 +123,7 @@ export const { * Returns the number of leading zeroes in the 32‐bit representation * of the provided value. * - * ※ Unlike `Math.clz32`, this function accepts either number or + * ※ Unlike `Math.clz32´, this function accepts either number or * big·int values. */ clz32, @@ -127,7 +132,7 @@ export const { * Returns the 32‐bit float which best approximate the provided * value. * - * ※ Unlike `Math.fround`, this function can take big·int arguments. + * ※ Unlike `Math.fround´, this function can take big·int arguments. * However, the result will always be a number. */ toFloat32, @@ -150,7 +155,7 @@ export const { /** * Returns the arctanh of the provided value. * - * ※ This function is effectively an alias for `Math.atanh`. + * ※ This function is effectively an alias for `Math.atanh´. * * ☡ This function does not allow big·int arguments. */ @@ -162,7 +167,7 @@ export const arctanh = createArrowFunction( /** * Returns the cube root of the provided value. * - * ※ This function is effectively an alias for `Math.cbrt`. + * ※ This function is effectively an alias for `Math.cbrt´. * * ☡ This function does not allow big·int arguments. */ @@ -171,7 +176,7 @@ export const cbrt = createArrowFunction(Math.cbrt); /** * Returns the ceiling of the provided value. * - * ※ This function is effectively an alias for `Math.ceil`. + * ※ This function is effectively an alias for `Math.ceil´. * * ☡ This function does not allow big·int arguments. */ @@ -180,7 +185,7 @@ export const ceil = createArrowFunction(Math.ceil); /** * Returns the cos of the provided value. * - * ※ This function is effectively an alias for `Math.cos`. + * ※ This function is effectively an alias for `Math.cos´. * * ☡ This function does not allow big·int arguments. */ @@ -189,7 +194,7 @@ export const cos = createArrowFunction(Math.cos); /** * Returns the cosh of the provided value. * - * ※ This function is effectively an alias for `Math.cosh`. + * ※ This function is effectively an alias for `Math.cosh´. * * ☡ This function does not allow big·int arguments. */ @@ -198,7 +203,7 @@ export const cosh = createArrowFunction(Math.cosh); /** * Returns the Euler number raised to the provided value. * - * ※ This function is effectively an alias for `Math.exp`. + * ※ This function is effectively an alias for `Math.exp´. * * ☡ This function does not allow big·int arguments. */ @@ -207,7 +212,7 @@ export const exp = createArrowFunction(Math.exp); /** * Returns the Euler number raised to the provided value, minus one. * - * ※ This function is effectively an alias for `Math.expm1`. + * ※ This function is effectively an alias for `Math.expm1´. * * ☡ This function does not allow big·int arguments. */ @@ -216,7 +221,7 @@ export const expm1 = createArrowFunction(Math.expm1); /** * Returns the floor of the provided value. * - * ※ This function is effectively an alias for `Math.floor`. + * ※ This function is effectively an alias for `Math.floor´. * * ☡ This function does not allow big·int arguments. */ @@ -226,7 +231,7 @@ export const floor = createArrowFunction(Math.floor); * Returns the square root of the sum of the squares of the provided * arguments. * - * ※ This function is effectively an alias for `Math.hypot`. + * ※ This function is effectively an alias for `Math.hypot´. * * ☡ This function does not allow big·int arguments. */ @@ -235,7 +240,7 @@ export const hypot = createArrowFunction(Math.hypot); /** * Returns whether the provided value is a finite number. * - * ※ This function is effectively an alias for `Number.isFinite`. + * ※ This function is effectively an alias for `Number.isFinite´. */ export const isFiniteNumber = createArrowFunction( Number.isFinite, @@ -245,7 +250,7 @@ export const isFiniteNumber = createArrowFunction( /** * Returns whether the provided value is an integral number. * - * ※ This function is effectively an alias for `Number.isInteger`. + * ※ This function is effectively an alias for `Number.isInteger´. */ export const isIntegralNumber = createArrowFunction( Number.isInteger, @@ -255,7 +260,7 @@ export const isIntegralNumber = createArrowFunction( /** * Returns whether the provided value is nan. * - * ※ This function is effectively an alias for `Number.isNaN`. + * ※ This function is effectively an alias for `Number.isNaN´. */ export const isNan = createArrowFunction( Number.isNaN, @@ -265,7 +270,7 @@ export const isNan = createArrowFunction( /** * Returns whether the provided value is a safe integral number. * - * ※ This function is effectively an alias for `Number.isSafeInteger`. + * ※ This function is effectively an alias for `Number.isSafeInteger´. */ export const isSafeIntegralNumber = createArrowFunction( Number.isSafeInteger, @@ -275,7 +280,7 @@ export const isSafeIntegralNumber = createArrowFunction( /** * Returns the ln of the provided value. * - * ※ This function is effectively an alias for `Math.log`. + * ※ This function is effectively an alias for `Math.log´. * * ☡ This function does not allow big·int arguments. */ @@ -284,7 +289,7 @@ export const ln = createArrowFunction(Math.log, { name: "ln" }); /** * Returns the ln of one plus the provided value. * - * ※ This function is effectively an alias for `Math.log1p`. + * ※ This function is effectively an alias for `Math.log1p´. * * ☡ This function does not allow big·int arguments. */ @@ -293,7 +298,7 @@ export const ln1p = createArrowFunction(Math.log1p, { name: "ln1p" }); /** * Returns the log10 of the provided value. * - * ※ This function is effectively an alias for `Math.log10`. + * ※ This function is effectively an alias for `Math.log10´. * * ☡ This function does not allow big·int arguments. */ @@ -302,7 +307,7 @@ export const log10 = createArrowFunction(Math.log10); /** * Returns the log2 of the provided value. * - * ※ This function is effectively an alias for `Math.log2`. + * ※ This function is effectively an alias for `Math.log2´. * * ☡ This function does not allow big·int arguments. */ @@ -312,7 +317,7 @@ export const log2 = createArrowFunction(Math.log2); * Returns the highest value of the provided arguments, or negative * infinity if no argument is provided. * - * ※ Unlike `Math.max`, this function accepts either number or big·int + * ※ Unlike `Math.max´, this function accepts either number or big·int * values. All values must be of the same type, or this function will * throw an error. * @@ -335,9 +340,9 @@ export const max = Object.defineProperties((...$s) => { } } else { if (typeof highest !== typeof number) { - // The type of the current number and the lowest number don’t + // The type of the current number and the lowest number don¦t // match. - throw new TypeError("Piscēs: Type mismatch."); + throw new TypeError(`${PISCĒS}: Type mismatch.`); } else if (isNan(number)) { // The current number is nan. return NAN; @@ -364,7 +369,7 @@ export const max = Object.defineProperties((...$s) => { * Returns the lowest value of the provided arguments, or positive * infinity if no argument is provided. * - * ※ Unlike `Math.min`, this function accepts either number or big·int + * ※ Unlike `Math.min´, this function accepts either number or big·int * values. All values must be of the same type, or this function will * throw an error. * @@ -388,9 +393,9 @@ export const min = Object.defineProperties((...$s) => { } else { // The current number is not the first one. if (typeof lowest !== typeof number) { - // The type of the current number and the lowest number don’t + // The type of the current number and the lowest number don¦t // match. - throw new TypeError("Piscēs: Type mismatch."); + throw new TypeError(`${PISCĒS}: Type mismatch.`); } else if (isNan(number)) { // The current number is nan. return NAN; @@ -416,7 +421,7 @@ export const min = Object.defineProperties((...$s) => { /** * Returns a pseudo·random value in the range [0, 1). * - * ※ This function is effectively an alias for `Math.random`. + * ※ This function is effectively an alias for `Math.random´. */ export const rand = createArrowFunction( Math.random, @@ -426,7 +431,7 @@ export const rand = createArrowFunction( /** * Returns the round of the provided value. * - * ※ This function is effectively an alias for `Math.round`. + * ※ This function is effectively an alias for `Math.round´. * * ☡ This function does not allow big·int arguments. */ @@ -447,7 +452,7 @@ export const round = createArrowFunction(Math.round); * that positive and negative infinity will return +1 and −1 * respectively. * - * ※ Unlike `Math.sign`, this function accepts either number or + * ※ Unlike `Math.sign´, this function accepts either number or * big·int values. */ export const sgn = ($) => { @@ -465,7 +470,7 @@ export const sgn = ($) => { /** * Returns the sin of the provided value. * - * ※ This function is effectively an alias for `Math.sin`. + * ※ This function is effectively an alias for `Math.sin´. * * ☡ This function does not allow big·int arguments. */ @@ -474,7 +479,7 @@ export const sin = createArrowFunction(Math.sin); /** * Returns the sinh of the provided value. * - * ※ This function is effectively an alias for `Math.sinh`. + * ※ This function is effectively an alias for `Math.sinh´. * * ☡ This function does not allow big·int arguments. */ @@ -483,7 +488,7 @@ export const sinh = createArrowFunction(Math.sinh); /** * Returns the square root of the provided value. * - * ※ This function is effectively an alias for `Math.sqrt`. + * ※ This function is effectively an alias for `Math.sqrt´. * * ☡ This function does not allow big·int arguments. */ @@ -492,7 +497,7 @@ export const sqrt = createArrowFunction(Math.sqrt); /** * Returns the tan of the provided value. * - * ※ This function is effectively an alias for `Math.tan`. + * ※ This function is effectively an alias for `Math.tan´. * * ☡ This function does not allow big·int arguments. */ @@ -501,7 +506,7 @@ export const tan = createArrowFunction(Math.tan); /** * Returns the tanh of the provided value. * - * ※ This function is effectively an alias for `Math.tanh`. + * ※ This function is effectively an alias for `Math.tanh´. * * ☡ This function does not allow big·int arguments. */ @@ -512,7 +517,7 @@ export const tanh = createArrowFunction(Math.tanh); * * ※ This method is safe to use with numbers. * - * ※ This is effectively an alias for `BigInt`. + * ※ This is effectively an alias for `BigInt´. */ export const { toBigInt } = (() => { const makeBigInt = BigInt; @@ -552,7 +557,7 @@ export const { const f = toIntegralNumberOrInfinity(fractionDigits); if (!isFiniteNumber(f) || f < 0 || f > 100) { throw new RangeError( - `Piscēs: The number of fractional digits must be a finite number between 0 and 100 inclusive; got: ${f}.`, + `${PISCĒS}: The number of fractional digits must be a finite number between 0 and 100 inclusive; got: ${f}.`, ); } else { if (typeof n === "number") { @@ -589,7 +594,7 @@ export const { const f = toIntegralNumberOrInfinity(fractionDigits); if (!isFiniteNumber(f) || f < 0 || f > 100) { throw new RangeError( - `Piscēs: The number of fractional digits must be a finite number between 0 and 100 inclusive; got: ${f}.`, + `${PISCĒS}: The number of fractional digits must be a finite number between 0 and 100 inclusive; got: ${f}.`, ); } else { const n = toNumeric($); @@ -649,13 +654,13 @@ export const toIntegralNumberOrInfinity = ($) => { * * ※ This function is safe to use with big·ints. * - * ※ This is effectively a nonconstructible version of the `Number` + * ※ This is effectively a nonconstructible version of the `Number´ * constructor. */ -export const { toNumber } = (() => { - const makeNumber = Number; - return { toNumber: ($) => makeNumber($) }; -})(); +export const toNumber = createArrowFunction( + Number, + { name: "toNumber" }, +); /** * Returns the result of converting the provided value to a number or @@ -674,7 +679,7 @@ export const { * Returns the result of converting the provided value to fit within * the provided number of bits as a signed integer. * - * ※ Unlike `BigInt.asIntN`, this function accepts both big·int and + * ※ Unlike `BigInt.asIntN´, this function accepts both big·int and * number values. * * ☡ The first argument, the number of bits, must be a number. @@ -685,7 +690,7 @@ export const { * Returns the result of converting the provided value to fit within * the provided number of bits as an unsigned integer. * - * ※ Unlike `BigInt.asUintN`, this function accepts both big·int and + * ※ Unlike `BigInt.asUintN´, this function accepts both big·int and * number values. * * ☡ The first argument, the number of bits, must be a number. @@ -734,7 +739,7 @@ export const { /** * Returns the trunc of the provided value. * - * ※ This function is effectively an alias for `Math.trunc`. + * ※ This function is effectively an alias for `Math.trunc´. * * ☡ This function does not allow big·int arguments. */ diff --git a/numeric.test.js b/numeric.test.js index 0129ae3..b6dbf0d 100644 --- a/numeric.test.js +++ b/numeric.test.js @@ -1,11 +1,14 @@ -// ♓🌟 Piscēs ∷ numeric.test.js -// ==================================================================== -// -// Copyright © 2022–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 . +// SPDX-FileCopyrightText: 2022, 2023, 2025 Lady +// SPDX-License-Identifier: MPL-2.0 +/** + * ⁌ ♓🧩 Piscēs ∷ numeric.test.js + * + * Copyright © 2022–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 { assert, @@ -1351,6 +1354,22 @@ describe("toNumber", () => { it("[[Call]] converts to a number", () => { assertStrictEquals(toNumber(2n), 2); }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new toNumber(1)); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(toNumber.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(toNumber.name, "toNumber"); + }); + }); }); describe("toNumeric", () => { diff --git a/string.js b/string.js index ddc05d7..99528a8 100644 --- a/string.js +++ b/string.js @@ -345,25 +345,6 @@ export const { }; })(); -/** - * 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. - */ -export const canonicalNumericIndexString = ($) => { - if (typeof $ !== "string") { - return UNDEFINED; - } else if ($ === "-0") { - return -0; - } else { - const n = +$; - return $ === `${n}` ? n : UNDEFINED; - } -}; - export const { /** * Returns an iterator over the codepoints in the string representation @@ -548,36 +529,6 @@ export const getLastSubstringIndex = createCallableFunction( { name: "getLastSubstringIndex" }, ); -/** Returns whether the provided value is an array index. */ -export const isArrayIndexString = ($) => { - const value = canonicalNumericIndexString($); - if (value !== UNDEFINED) { - // The provided value is a canonical numeric index string. - // - // Return whether it is in range for array indices. - return sameValue(value, 0) - || value === toLength(value) && value > 0 && value < -1 >>> 0; - } else { - // The provided value is not a canonical numeric index string. - return false; - } -}; - -/** Returns whether the provided value is an integer index string. */ -export const isIntegerIndexString = ($) => { - const value = canonicalNumericIndexString($); - if (value !== UNDEFINED) { - // The provided value is a canonical numeric index string. - // - // Return whether it is in range for integer indices. - return sameValue(value, 0) - || value === toLength(value) && value > 0; - } else { - // The provided value is not a canonical numeric index string. - return false; - } -}; - /** * Returns the result of joining the provided iterable. * diff --git a/string.test.js b/string.test.js index 147e109..e567060 100644 --- a/string.test.js +++ b/string.test.js @@ -23,7 +23,6 @@ import { import { asciiLowercase, asciiUppercase, - canonicalNumericIndexString, characters, codepoints, codeUnits, @@ -32,8 +31,6 @@ import { getCodeUnit, getFirstSubstringIndex, getLastSubstringIndex, - isArrayIndexString, - isIntegerIndexString, join, Matcher, rawString, @@ -635,59 +632,6 @@ describe("asciiUppercase", () => { }); }); -describe("canonicalNumericIndexString", () => { - it("[[Call]] returns undefined for nonstrings", () => { - assertStrictEquals(canonicalNumericIndexString(1), void {}); - }); - - it("[[Call]] returns undefined for noncanonical strings", () => { - assertStrictEquals(canonicalNumericIndexString(""), void {}); - assertStrictEquals(canonicalNumericIndexString("01"), void {}); - assertStrictEquals( - canonicalNumericIndexString("9007199254740993"), - void {}, - ); - }); - - it('[[Call]] returns -0 for "-0"', () => { - assertStrictEquals(canonicalNumericIndexString("-0"), -0); - }); - - it("[[Call]] returns the corresponding number for canonical strings", () => { - assertStrictEquals(canonicalNumericIndexString("0"), 0); - assertStrictEquals(canonicalNumericIndexString("-0.25"), -0.25); - assertStrictEquals( - canonicalNumericIndexString("9007199254740992"), - 9007199254740992, - ); - assertStrictEquals(canonicalNumericIndexString("NaN"), 0 / 0); - assertStrictEquals(canonicalNumericIndexString("Infinity"), 1 / 0); - assertStrictEquals( - canonicalNumericIndexString("-Infinity"), - -1 / 0, - ); - }); - - it("[[Construct]] throws an error", () => { - assertThrows(() => new canonicalNumericIndexString("")); - }); - - describe(".length", () => { - it("[[Get]] returns the correct length", () => { - assertStrictEquals(canonicalNumericIndexString.length, 1); - }); - }); - - describe(".name", () => { - it("[[Get]] returns the correct name", () => { - assertStrictEquals( - canonicalNumericIndexString.name, - "canonicalNumericIndexString", - ); - }); - }); -}); - describe("characters", () => { it("[[Call]] returns an iterable", () => { assertStrictEquals( @@ -1008,131 +952,6 @@ describe("getLastSubstringIndex", () => { }); }); -describe("isArrayIndexString", () => { - it("[[Call]] returns false for nonstrings", () => { - assertStrictEquals(isArrayIndexString(1), false); - }); - - it("[[Call]] returns false for noncanonical strings", () => { - assertStrictEquals(isArrayIndexString(""), false); - assertStrictEquals(isArrayIndexString("01"), false); - assertStrictEquals(isArrayIndexString("9007199254740993"), false); - }); - - it("[[Call]] returns false for nonfinite numbers", () => { - assertStrictEquals(isArrayIndexString("NaN"), false); - assertStrictEquals(isArrayIndexString("Infinity"), false); - assertStrictEquals(isArrayIndexString("-Infinity"), false); - }); - - it("[[Call]] returns false for negative numbers", () => { - assertStrictEquals(isArrayIndexString("-0"), false); - assertStrictEquals(isArrayIndexString("-1"), false); - }); - - it("[[Call]] returns false for nonintegers", () => { - assertStrictEquals(isArrayIndexString("0.25"), false); - assertStrictEquals(isArrayIndexString("1.1"), false); - }); - - it("[[Call]] returns false for numbers greater than or equal to -1 >>> 0", () => { - assertStrictEquals(isArrayIndexString(String(-1 >>> 0)), false); - assertStrictEquals( - isArrayIndexString(String((-1 >>> 0) + 1)), - false, - ); - }); - - it("[[Call]] returns true for array lengths less than -1 >>> 0", () => { - assertStrictEquals(isArrayIndexString("0"), true); - assertStrictEquals( - isArrayIndexString(String((-1 >>> 0) - 1)), - true, - ); - }); - - it("[[Construct]] throws an error", () => { - assertThrows(() => new isArrayIndexString("0")); - }); - - describe(".length", () => { - it("[[Get]] returns the correct length", () => { - assertStrictEquals(isArrayIndexString.length, 1); - }); - }); - - describe(".name", () => { - it("[[Get]] returns the correct name", () => { - assertStrictEquals( - isArrayIndexString.name, - "isArrayIndexString", - ); - }); - }); -}); - -describe("isIntegerIndexString", () => { - it("[[Call]] returns false for nonstrings", () => { - assertStrictEquals(isIntegerIndexString(1), false); - }); - - it("[[Call]] returns false for noncanonical strings", () => { - assertStrictEquals(isIntegerIndexString(""), false); - assertStrictEquals(isIntegerIndexString("01"), false); - assertStrictEquals( - isIntegerIndexString("9007199254740993"), - false, - ); - }); - - it("[[Call]] returns false for nonfinite numbers", () => { - assertStrictEquals(isIntegerIndexString("NaN"), false); - assertStrictEquals(isIntegerIndexString("Infinity"), false); - assertStrictEquals(isIntegerIndexString("-Infinity"), false); - }); - - it("[[Call]] returns false for negative numbers", () => { - assertStrictEquals(isIntegerIndexString("-0"), false); - assertStrictEquals(isIntegerIndexString("-1"), false); - }); - - it("[[Call]] returns false for nonintegers", () => { - assertStrictEquals(isIntegerIndexString("0.25"), false); - assertStrictEquals(isIntegerIndexString("1.1"), false); - }); - - it("[[Call]] returns false for numbers greater than or equal to 2 ** 53", () => { - assertStrictEquals( - isIntegerIndexString("9007199254740992"), - false, - ); - }); - - it("[[Call]] returns true for safe canonical integer strings", () => { - assertStrictEquals(isIntegerIndexString("0"), true); - assertStrictEquals(isIntegerIndexString("9007199254740991"), true); - }); - - it("[[Construct]] throws an error", () => { - assertThrows(() => new isIntegerIndexString("0")); - }); - - describe(".length", () => { - it("[[Get]] returns the correct length", () => { - assertStrictEquals(isIntegerIndexString.length, 1); - }); - }); - - describe(".name", () => { - it("[[Get]] returns the correct name", () => { - assertStrictEquals( - isIntegerIndexString.name, - "isIntegerIndexString", - ); - }); - }); -}); - describe("join", () => { it("[[Call]] joins the provided iterator with the provided separartor", () => { assertStrictEquals(join([1, 2, 3, 4].values(), "☂"), "1☂2☂3☂4"); diff --git a/value.js b/value.js index 6956b6f..341ecdf 100644 --- a/value.js +++ b/value.js @@ -178,6 +178,25 @@ export const POSITIVE_ZERO = 0; /** The undefined primitive. */ export const UNDEFINED = undefined; +/** + * 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. + */ +export const canonicalNumericIndexString = ($) => { + if (typeof $ !== "string") { + return UNDEFINED; + } else if ($ === "-0") { + return -0; + } else { + const n = +$; + return $ === `${n}` ? n : UNDEFINED; + } +}; + /** * Completes the provided property descriptor by setting missing values * to their defaults. @@ -243,6 +262,21 @@ export const completePropertyDescriptor = (Desc) => { export const isAccessorDescriptor = (Desc) => Desc !== UNDEFINED && ("get" in Desc || "set" in Desc); +/** Returns whether the provided value is an array index. */ +export const isArrayIndexString = ($) => { + const value = canonicalNumericIndexString($); + if (value !== UNDEFINED) { + // The provided value is a canonical numeric index string. + // + // Return whether it is in range for array indices. + return sameValue(value, 0) + || value === toLength(value) && value > 0 && value < -1 >>> 0; + } else { + // The provided value is not a canonical numeric index string. + return false; + } +}; + /** Gets whether the provided value is a data descrtiptor. */ export const isDataDescriptor = (Desc) => Desc !== UNDEFINED && ("value" in Desc || "writable" in Desc); @@ -266,6 +300,21 @@ export const isGenericDescriptor = (Desc) => && !("get" in Desc || "set" in Desc || "value" in Desc || "writable" in Desc); +/** Returns whether the provided value is an integer index string. */ +export const isIntegerIndexString = ($) => { + const value = canonicalNumericIndexString($); + if (value !== UNDEFINED) { + // The provided value is a canonical numeric index string. + // + // Return whether it is in range for integer indices. + return sameValue(value, 0) + || value === toLength(value) && value > 0; + } else { + // The provided value is not a canonical numeric index string. + return false; + } +}; + export const { /** * Returns the primitive value of the provided object per its @@ -420,7 +469,7 @@ export const { const { isNaN: isNan } = Number; const { is } = Object; return { - sameValue: (a, b) => is(a, b), + sameValue: ($1, $2) => is($1, $2), sameValueZero: ($1, $2) => { const type1 = type($1); const type2 = type($2); diff --git a/value.test.js b/value.test.js index b7dcd69..c55b591 100644 --- a/value.test.js +++ b/value.test.js @@ -19,13 +19,16 @@ import { } from "./dev-deps.js"; import { ASYNC_ITERATOR, + canonicalNumericIndexSting, completePropertyDescriptor, HAS_INSTANCE, IS_CONCAT_SPREADABLE, isAccessorDescriptor, + isArrayIndexString, isDataDescriptor, isFullyPopulatedDescriptor, isGenericDescriptor, + isIntegerIndexString, ITERATOR, LN_10, LN_2, @@ -249,6 +252,59 @@ describe("UNSCOPABLES", () => { }); }); +describe("canonicalNumericIndexString", () => { + it("[[Call]] returns undefined for nonstrings", () => { + assertStrictEquals(canonicalNumericIndexString(1), void {}); + }); + + it("[[Call]] returns undefined for noncanonical strings", () => { + assertStrictEquals(canonicalNumericIndexString(""), void {}); + assertStrictEquals(canonicalNumericIndexString("01"), void {}); + assertStrictEquals( + canonicalNumericIndexString("9007199254740993"), + void {}, + ); + }); + + it('[[Call]] returns -0 for "-0"', () => { + assertStrictEquals(canonicalNumericIndexString("-0"), -0); + }); + + it("[[Call]] returns the corresponding number for canonical strings", () => { + assertStrictEquals(canonicalNumericIndexString("0"), 0); + assertStrictEquals(canonicalNumericIndexString("-0.25"), -0.25); + assertStrictEquals( + canonicalNumericIndexString("9007199254740992"), + 9007199254740992, + ); + assertStrictEquals(canonicalNumericIndexString("NaN"), 0 / 0); + assertStrictEquals(canonicalNumericIndexString("Infinity"), 1 / 0); + assertStrictEquals( + canonicalNumericIndexString("-Infinity"), + -1 / 0, + ); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new canonicalNumericIndexString("")); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(canonicalNumericIndexString.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + canonicalNumericIndexString.name, + "canonicalNumericIndexString", + ); + }); + }); +}); + describe("completePropertyDescriptor", () => { it("[[Call]] completes a generic descriptor", () => { const desc = {}; @@ -352,6 +408,69 @@ describe("isAccessorDescriptor", () => { }); }); +describe("isArrayIndexString", () => { + it("[[Call]] returns false for nonstrings", () => { + assertStrictEquals(isArrayIndexString(1), false); + }); + + it("[[Call]] returns false for noncanonical strings", () => { + assertStrictEquals(isArrayIndexString(""), false); + assertStrictEquals(isArrayIndexString("01"), false); + assertStrictEquals(isArrayIndexString("9007199254740993"), false); + }); + + it("[[Call]] returns false for nonfinite numbers", () => { + assertStrictEquals(isArrayIndexString("NaN"), false); + assertStrictEquals(isArrayIndexString("Infinity"), false); + assertStrictEquals(isArrayIndexString("-Infinity"), false); + }); + + it("[[Call]] returns false for negative numbers", () => { + assertStrictEquals(isArrayIndexString("-0"), false); + assertStrictEquals(isArrayIndexString("-1"), false); + }); + + it("[[Call]] returns false for nonintegers", () => { + assertStrictEquals(isArrayIndexString("0.25"), false); + assertStrictEquals(isArrayIndexString("1.1"), false); + }); + + it("[[Call]] returns false for numbers greater than or equal to -1 >>> 0", () => { + assertStrictEquals(isArrayIndexString(String(-1 >>> 0)), false); + assertStrictEquals( + isArrayIndexString(String((-1 >>> 0) + 1)), + false, + ); + }); + + it("[[Call]] returns true for array lengths less than -1 >>> 0", () => { + assertStrictEquals(isArrayIndexString("0"), true); + assertStrictEquals( + isArrayIndexString(String((-1 >>> 0) - 1)), + true, + ); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new isArrayIndexString("0")); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(isArrayIndexString.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + isArrayIndexString.name, + "isArrayIndexString", + ); + }); + }); +}); + describe("isDataDescriptor", () => { it("[[Call]] returns false for a generic descriptor", () => { assertStrictEquals(isDataDescriptor({}), false); @@ -505,6 +624,68 @@ describe("isGenericDescriptor", () => { }); }); +describe("isIntegerIndexString", () => { + it("[[Call]] returns false for nonstrings", () => { + assertStrictEquals(isIntegerIndexString(1), false); + }); + + it("[[Call]] returns false for noncanonical strings", () => { + assertStrictEquals(isIntegerIndexString(""), false); + assertStrictEquals(isIntegerIndexString("01"), false); + assertStrictEquals( + isIntegerIndexString("9007199254740993"), + false, + ); + }); + + it("[[Call]] returns false for nonfinite numbers", () => { + assertStrictEquals(isIntegerIndexString("NaN"), false); + assertStrictEquals(isIntegerIndexString("Infinity"), false); + assertStrictEquals(isIntegerIndexString("-Infinity"), false); + }); + + it("[[Call]] returns false for negative numbers", () => { + assertStrictEquals(isIntegerIndexString("-0"), false); + assertStrictEquals(isIntegerIndexString("-1"), false); + }); + + it("[[Call]] returns false for nonintegers", () => { + assertStrictEquals(isIntegerIndexString("0.25"), false); + assertStrictEquals(isIntegerIndexString("1.1"), false); + }); + + it("[[Call]] returns false for numbers greater than or equal to 2 ** 53", () => { + assertStrictEquals( + isIntegerIndexString("9007199254740992"), + false, + ); + }); + + it("[[Call]] returns true for safe canonical integer strings", () => { + assertStrictEquals(isIntegerIndexString("0"), true); + assertStrictEquals(isIntegerIndexString("9007199254740991"), true); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new isIntegerIndexString("0")); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(isIntegerIndexString.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + isIntegerIndexString.name, + "isIntegerIndexString", + ); + }); + }); +}); + describe("ordinaryToPrimitive", () => { it("[[Call]] prefers `valueOf` by default", () => { const obj = {