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=54b756dbfe3e86b528253082215a9354e14313f0 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/LICENSES/CC0-1.0.txt b/LICENSES/CC0-1.0.txt new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/LICENSES/CC0-1.0.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/LICENSE b/LICENSES/MPL-2.0.txt similarity index 100% rename from LICENSE rename to LICENSES/MPL-2.0.txt 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/deno.json b/deno.json index 6bdb9a3..7cceeff 100644 --- a/deno.json +++ b/deno.json @@ -1,5 +1,11 @@ -{ - "fmt": { "lineWidth": 71 }, - "lint": { "rules": { "exclude": ["constructor-super"] } }, - "lock": false -} +{ "fmt": + { "exclude": + [ "deno.json" ] + , "lineWidth": 71 + , "operatorPosition": "nextLine" + , "useBraces": "always" } +, "lint": + { "rules": + { "exclude": + [ "constructor-super" ] } } +, "lock": false } diff --git a/deno.json.license b/deno.json.license new file mode 100644 index 0000000..399e2f2 --- /dev/null +++ b/deno.json.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2023, 2025 Lady +SPDX-License-Identifier: CC0-1.0 diff --git a/dev-deps.js b/dev-deps.js index 3c4981e..99ca0fb 100644 --- a/dev-deps.js +++ b/dev-deps.js @@ -1,25 +1,24 @@ -// ♓🌟 Piscēs ∷ dev-deps.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 ∷ dev-deps.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 . + */ export { assert, assertEquals, assertNotStrictEquals, assertStrictEquals, assertThrows, -} from "https://deno.land/std@0.208.0/testing/asserts.ts"; -export { - describe, - it, -} from "https://deno.land/std@0.208.0/testing/bdd.ts"; +} from "jsr:@std/assert@1.0.13"; +export { describe, it } from "jsr:@std/testing@1.0.15/bdd"; export { assertSpyCall, assertSpyCalls, spy, -} from "https://deno.land/std@0.208.0/testing/mock.ts"; +} from "jsr:@std/testing@1.0.15/mock"; diff --git a/function.js b/function.js index 2f242fe..5f9c2b5 100644 --- a/function.js +++ b/function.js @@ -1,11 +1,14 @@ -// ♓🌟 Piscēs ∷ function.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 ∷ function.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 { ITERATOR, @@ -20,78 +23,81 @@ import { defineOwnProperty, getOwnPropertyDescriptor, getPrototype, + hasOwnProperty, objectCreate, setPropertyValues, setPrototype, } from "./object.js"; +const PISCĒS = "♓🧩 Piscēs"; + export const { /** * Creates a bound function from the provided function using the * provided this value and arguments list. * - * ☡ As with `call` and `construct`, the arguments must be passed as + * ☡ As with `call´ and `construct´, the arguments must be passed as * an array. */ bind, /** - * Returns a new function which calls the provided function with its - * first argument as the `this` value and the remaining arguments - * passed through. + * Returns a new arrow function function which wraps the provided + * function and passes its arguments thru. * - * The `length`, `name`, and prototype of the provided function will + * The `length´, `name´, and prototype of the provided function will * be preserved in the new one. A second argument may be used to - * override `length` and `name`. + * override `length´ and `name´. */ createArrowFunction, /** - * Returns a new function which calls the provided function with its - * first argument as the `this` value and the remaining arguments - * passed through. + * Returns a new arrow function which wraps the provided function, + * using its first argument as the this value when calling the + * provided function and passing the remainder thru. * - * The `length`, `name`, and prototype of the provided function will + * The `length´, `name´, and prototype of the provided function will * be preserved in the new one. A second argument may be used to - * override `length` and `name`. + * override `length´ and `name´. * - * ※ This is effectively an alias for `Function::call.bind`. + * ※ This is effectively an alias for `Function::call.bind´. */ createCallableFunction, /** * Returns a constructor which throws whenever it is called but has - * the same `.name` and `.prototype` as the provided value. + * the same `.length´, `.name´, and `.prototype´ as the provided + * value. * - * The `length`, `name`, `prototype`, and prototype of the provided + * The `length´, `name´, `prototype´, and prototype of the provided * function will be preserved in the new one. A second argument may - * be used to override `length`, `name`, and `prototype`. + * be used to override `length´, `name´, and `prototype´. */ createIllegalConstructor, /** - * Returns a constructor which produces a new constructor which wraps + * Returns a constructor which produces a new constructor that wraps * the provided constructor, but returns a proxy of the result using * the provided handler. * * The resulting constructor inherits from, and has the same basic - * shape as, `Proxy`. + * shape as, `Proxy´. * - * If a base constructor is not provided, `Object` will be used. + * If a base constructor is not provided, `Object´ will be used. * * If a third argument is provided, it is used as the target for the * provided constructor when it is constructed. This can be used to * prevent leakage of the provided constructor to superclasses - * through `new.target`. + * through `new.target´. * - * The `length` of the provided function will be preserved in the new - * one. A fourth argument may be used to override `length` and - * `name`. + * The `length´ of the provided function will be preserved in the new + * one. A fourth argument may be used to override `length´ and + * `name´. * - * ※ `.prototype` will be present, but undefined, on the resulting - * constructor. This differs from the behaviour of `Proxy`, for which - * `.prototype` is not present at all. It is not presently possible - * to create a constructor with no `.prototype` property in + * ※ `.prototype´ will be present, but undefined, on the resulting + * constructor. This differs from the behaviour of `Proxy´, for which + * `.prototype´ is not present at all. It is not presently possible + * to create a constructor with no `.prototype´ property in * Ecmascript code. */ createProxyConstructor, @@ -123,7 +129,7 @@ export const { }, next: callBind( arrayIteratorNext, - call(arrayIterator, this.args, []), + reflectApply(arrayIterator, this.args, []), ), }; }, @@ -138,8 +144,13 @@ export const { constructor, ]); if (existing) { + // There is an existing values set for this constructor. + // + // It is returned. return existing; } else { + // There is no existing values set for this constructor so one + // must be created. const result = new wsConstructor(); reflectApply(wmSet, proxyConstructorValuesMap, [ constructor, @@ -156,18 +167,41 @@ export const { // No base function was provided to apply. return $; } else { - // A base function was provided; apply it. - const { length, name, prototype } = base; + // A base function was provided. + // + // Apply it. + const overrides = objectCreate(null); + overrides.length = +base.length + lengthDelta; + overrides.name = base.name; if (getPrototype($) === functionPrototype) { + // The provided function has the function prototype. + // + // Change it to match the base function. setPrototype($, getPrototype(base)); } else { + // The provided function already does not have the function + // prototype, so its prototype is not changed. /* do nothing */ } - return applyProperties($, { - length: +length + lengthDelta, - name, - prototype, - }); + if (hasOwnProperty($, "prototype") && "prototype" in base) { + // The provided function has a `.prototype´ property, and one + // was provided in the base function as well. + try { + // If this does not throw, the provided function is a + // constructor. Its `.prototype´ property is set alongside + // the others. + reflectConstruct(function () {}, [], $); + overrides.prototype = base.prototype; + } catch { + // The provided function is not a constructor. + /* do nothing */ + } + } else { + // The provided function does not have a `.prototype´ property, + // or the base function did not have one. + /* do nothing */ + } + return applyProperties($, overrides); } }; const applyProperties = ($, override) => { @@ -175,21 +209,23 @@ export const { // No properties were provided to apply. return $; } else { - // Properties were provided; apply them. - const { length, name, prototype } = override; + // Properties were provided. + // + // Apply them. + const { length, name } = override; if ( - prototype === UNDEFINED || - !getOwnPropertyDescriptor($, "prototype")?.writable + !("prototype" in override) + || !getOwnPropertyDescriptor($, "prototype")?.writable ) { - // The provided function has no `.prototype`, its prototype is + // The provided function has no `.prototype´, its prototype is // not writable, or no prototype value was provided. // // Do not modify the prototype property of the provided // function. /* do nothing */ } else { - // The provided function is a constructor and a prototype value - // was provided. + // The provided function has a writable `.prototype´ and a + // prototype value was provided. // // Change the prototype property of the provided function to // match. @@ -199,7 +235,7 @@ export const { defineOwnDataProperty( objectCreate(null), "value", - prototype, + override.prototype, ), ); } @@ -284,19 +320,19 @@ export const { if (!(type(handler) === "object")) { // The provided handler is not an object; this is an error. throw new TypeError( - `Piscēs: Proxy handler must be an object, but got: ${handler}.`, + `${PISCĒS}: Proxy handler must be an object, but got: ${handler}.`, ); } else if (!isConstructor(constructor)) { // The provided constructor is not a constructor; this is an // error. throw new TypeError( - "Piscēs: Cannot create proxy constructor from nonconstructible value.", + `${PISCĒS}: Cannot create proxy constructor from nonconstructible value.`, ); } else if (!isConstructor(target)) { // The provided new target is not a constructor; this is an // error. throw new TypeError( - "Piscēs: New target must be a constructor.", + `${PISCĒS}: New target must be a constructor.`, ); } else { // The arguments are acceptable. @@ -305,16 +341,17 @@ export const { setPrototype( function (...$s) { if (new.target === UNDEFINED) { - // The constructor was not called with new; this is an - // error. + // The constructor was not called with new; this is + // an error. throw new TypeError( - `Piscēs: ${ + `${PISCĒS}: ${ C.name ?? "Proxy" } must be called with new.`, ); } else { - // The constructor was called with new; return the - // appropriate proxy. + // The constructor was called with new. + // + // Return the appropriate proxy. const O = reflectConstruct( constructor, $s, @@ -393,8 +430,9 @@ export const { return false; } else { // One or more values has been registered for the - // current constructor; return whether the provided - // argument is one. + // current constructor. + // + // Return whether the provided argument is one. return reflectApply(wsHas, values, [$]); } }, @@ -417,7 +455,7 @@ export const { * Calls the provided function with the provided this value and * arguments list. * - * ☡ This is effectively an alias for `Reflect.apply`—the arguments + * ☡ This is effectively an alias for `Reflect.apply´—the arguments * must be passed as an arraylike. */ export const call = createArrowFunction(Reflect.apply, { @@ -425,7 +463,7 @@ export const call = createArrowFunction(Reflect.apply, { }); /** - * Returns whether calling the provided function with no `this` value + * Returns whether calling the provided function with no this value * or arguments completes normally; that is, does not throw an error. * * ☡ This function will throw an error if the provided argument is not @@ -435,16 +473,20 @@ export const completesNormally = ($) => { if (!isCallable($)) { // The provided value is not callable; this is an error. throw new TypeError( - `Piscēs: Cannot determine completion of noncallable value: ${$}`, + `${PISCĒS}: Cannot determine completion of noncallable value: ${$}.`, ); } else { // The provided value is callable. try { - // Attempt to call the function and return true if this succeeds. + // This will throw if calling the function throws. + // + // Otherwise, return true. $(); return true; } catch { - // Calling the function did not succeed; return false. + // Calling the function did not succeed. + // + // Return false. return false; } } @@ -454,7 +496,7 @@ export const completesNormally = ($) => { * Constructs the provided function with the provided arguments list * and new target. * - * ☡ This is effectively an alias for `Reflect.construct`—the + * ☡ This is effectively an alias for `Reflect.construct´—the * arguments must be passed as an arraylike. */ export const construct = createArrowFunction(Reflect.construct); @@ -463,8 +505,8 @@ export const construct = createArrowFunction(Reflect.construct); * Returns the provided value. * * ※ This function can be called as a constructor. When used in an - * `extends` clause and called via `super`, it will set the value of - * `this` to the provided value, enabling it to be extended with + * `extends´ clause and called via `super´, it will set the value of + * `this´ to the provided value, enabling it to be extended with * private class features. */ export const identity = function ($) { @@ -477,9 +519,10 @@ export const isCallable = ($) => typeof $ === "function"; /** Returns whether the provided value is a constructor. */ export const isConstructor = ($) => completesNormally(() => - // Try constructing a new object with the provided value as its - // `new.target`. This will throw if the provided value is not a - // constructor. + // Try constructing a new object using the provided value as + // `new.target´. + // + // ☡ This will throw if the provided value is not a constructor. construct(function () {}, [], $) ); diff --git a/function.test.js b/function.test.js index bf0e907..1b9ea74 100644 --- a/function.test.js +++ b/function.test.js @@ -1,11 +1,14 @@ -// ♓🌟 Piscēs ∷ function.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 ∷ function.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, @@ -338,8 +341,8 @@ describe("createArrowFunction", () => { it("[[Call]] returns a function with no prototype property", () => { assert( - !("prototype" in - createArrowFunction(function () {}, { prototype: {} })), + !("prototype" + in createArrowFunction(function () {}, { prototype: {} })), ); }); @@ -430,8 +433,8 @@ describe("createCallableFunction", () => { it("[[Call]] returns a function with no prototype property", () => { assert( - !("prototype" in - createCallableFunction(function () {}, { prototype: {} })), + !("prototype" + in createCallableFunction(function () {}, { prototype: {} })), ); }); @@ -665,7 +668,7 @@ describe("createProxyConstructor", () => { }); }); - describe("~", () => { + describe("()", () => { it("[[Call]] throws an error", () => { assertThrows(() => { createProxyConstructor({})(); @@ -710,7 +713,7 @@ describe("createProxyConstructor", () => { }); }); - describe("~is[[.name]]", () => { + describe("().is[[.name]]", () => { it("[[GetOwnProperty]] defines the appropriate method", () => { assertNotStrictEquals( Object.getOwnPropertyDescriptor( @@ -799,7 +802,7 @@ describe("createProxyConstructor", () => { }); }); - describe("~length", () => { + describe("().length", () => { it("[[GetOwnProperty]] has the correct descriptor", () => { assertEquals( Object.getOwnPropertyDescriptor( @@ -816,7 +819,7 @@ describe("createProxyConstructor", () => { }); }); - describe("~name", () => { + describe("().name", () => { it("[[GetOwnProperty]] has the correct descriptor", () => { assertEquals( Object.getOwnPropertyDescriptor( @@ -833,7 +836,7 @@ describe("createProxyConstructor", () => { }); }); - describe("~prototype", () => { + describe("().prototype", () => { it("[[GetOwnProperty]] has the correct descriptor", () => { assertEquals( Object.getOwnPropertyDescriptor( @@ -850,7 +853,7 @@ describe("createProxyConstructor", () => { }); }); - describe("~revocable", () => { + describe("().revocable", () => { it("[[Call]] produces a revocable proxy", () => { const obj = {}; const proxyConstructor = createProxyConstructor({ diff --git a/iterable.js b/iterable.js index 7149a28..6b45000 100644 --- a/iterable.js +++ b/iterable.js @@ -1,11 +1,14 @@ -// ♓🌟 Piscēs ∷ iterable.js -// ==================================================================== -// -// Copyright © 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: 2023, 2025 Lady +// SPDX-License-Identifier: MPL-2.0 +/** + * ⁌ ♓🧩 Piscēs ∷ iterable.js + * + * Copyright © 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 { bind, call, identity } from "./function.js"; import { @@ -30,7 +33,7 @@ export const { * the resulting iterator. * * The resulting function also takes a second argument, which will be - * used as the `this` value when calling the provided generator + * used as the this value when calling the provided generator * function, if provided. * * ※ The returned function is an ordinary nonconstructible (arrow) @@ -53,7 +56,7 @@ export const { * the resulting iterator. * * The resulting function also takes a second argument, which will be - * used as the `this` value when calling the provided generator + * used as the this value when calling the provided generator * function, if provided. * * ※ The returned function is an ordinary nonconstructible (arrow) @@ -82,7 +85,7 @@ export const { * the resulting iterator. * * The resulting function also takes a second argument, which will be - * used as the `this` value when calling the provided generator + * used as the this value when calling the provided generator * function, if provided. * * ※ The returned function is an ordinary nonconstructible (arrow) @@ -105,7 +108,7 @@ export const { * the resulting iterator. * * The resulting function also takes a second argument, which will be - * used as the `this` value when calling the provided generator + * used as the this value when calling the provided generator * function, if provided. * * ※ The returned function is an ordinary nonconstructible (arrow) @@ -128,11 +131,11 @@ export const { * the resulting iterator. * * The resulting function also takes a second argument, which will be - * used as the `this` value when calling the provided generator + * used as the this value when calling the provided generator * function, if provided. * * ※ This iterator function iterates over characters; use - * `arrayIteratorFunction` to iterate over code units. + * arrayIteratorFunction to iterate over code units. * * ※ The returned function is an ordinary nonconstructible (arrow) * function which, when called with a string, returns an iterator. @@ -174,7 +177,7 @@ export const { * An iterator generated by an iterator function. * * This class provides the internal data structure of all the - * iterator functions as well as the `::next` behaviour they all use. + * iterator functions as well as the `::next´ behaviour they all use. * * ※ This class extends the identity function to allow for arbitrary * construction of its superclass instance based on the provided @@ -197,7 +200,7 @@ export const { * * ☡ It is not possible to type·check the provided next method or * generator function to ensure that they actually are correct and - * appropriately callable; if they aren’t, an error will be thrown + * appropriately callable; if they aren¦t, an error will be thrown * when attempting to yield the first value. */ constructor( @@ -220,8 +223,8 @@ export const { const baseIteratorNext = this.#baseIteratorNext; const generateNext = this.#generateNext; while (true) { - // This function sometimes needs to repeat its processing steps - // in the case that a generator function was provided. + // This function some·times needs to repeat its processing + // steps in the case that a generator function was provided. // // To avoid potentially large amounts of recursive calls, it is // defined in a loop which will exit the first time a suitable @@ -233,7 +236,7 @@ export const { return { value: UNDEFINED, done: true }; } else if (nextIterator === null) { // This iterator is not currently yielding values from the - // provided generator function, either because it doesn’t + // provided generator function, either because it doesn¦t // exist or because its values have been exhausted. // // Get the next value in the base iterator and either provide @@ -283,7 +286,9 @@ export const { continue; } else { // The current iterator of values has yielded another - // value; reyield it. + // value. + // + // Reyield it. return { value, done: false }; } } @@ -328,17 +333,28 @@ export const { const iteratorFunction = ( makeBaseIterator, baseIteratorNext, + iteratorFunctionName, generateNext, stringTag = "Iterator", ) => { const prototype = makePrototype(stringTag); // intentionally cached - return ($, thisArg = UNDEFINED) => - new Iterator( - prototype, - call(makeBaseIterator, $, []), - baseIteratorNext, - generateNext == null ? null : bind(generateNext, thisArg, []), - ); + return defineOwnProperty( + ($, thisArg = UNDEFINED) => + new Iterator( + prototype, + call(makeBaseIterator, $, []), + baseIteratorNext, + generateNext == null + ? null + : bind(generateNext, thisArg, []), + ), + "name", + defineOwnDataProperty( + objectCreate(null), + "value", + iteratorFunctionName, + ), + ); }; return { @@ -346,7 +362,7 @@ export const { bind( iteratorFunction, UNDEFINED, - [arrayIterator, arrayIteratorNext], + [arrayIterator, arrayIteratorNext, "values"], ), "name", defineOwnDataProperty( @@ -364,6 +380,7 @@ export const { return this(); }, generatorIteratorNext, + "yields", ], ), "name", @@ -377,7 +394,7 @@ export const { bind( iteratorFunction, UNDEFINED, - [mapIterator, mapIteratorNext], + [mapIterator, mapIteratorNext, "entries"], ), "name", defineOwnDataProperty( @@ -390,7 +407,7 @@ export const { bind( iteratorFunction, UNDEFINED, - [setIterator, setIteratorNext], + [setIterator, setIteratorNext, "values"], ), "name", defineOwnDataProperty( @@ -403,7 +420,7 @@ export const { bind( iteratorFunction, UNDEFINED, - [stringIterator, stringIteratorNext], + [stringIterator, stringIteratorNext, "characters"], ), "name", defineOwnDataProperty( diff --git a/iterable.test.js b/iterable.test.js index 0f7c9fc..3651c6c 100644 --- a/iterable.test.js +++ b/iterable.test.js @@ -1,11 +1,14 @@ -// ♓🌟 Piscēs ∷ iterable.test.js -// ==================================================================== -// -// Copyright © 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: 2023, 2025 Lady +// SPDX-License-Identifier: MPL-2.0 +/** + * ⁌ ♓🧩 Piscēs ∷ iterable.test.js + * + * Copyright © 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, @@ -126,6 +129,26 @@ describe("arrayIteratorFunction", () => { }); }); + it("[[Construct]] throws an error", () => { + const iterator = arrayIteratorFunction(); + assertThrows(() => new iterator([])); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(arrayIteratorFunction().length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + arrayIteratorFunction().name, + "values", + ); + }); + }); + describe("::next", () => { it("[[Call]] throws if there are values and the mapper is not a generator function", () => { const iterator = arrayIteratorFunction(function () {}); @@ -253,6 +276,26 @@ describe("generatorIteratorFunction", () => { }); }); + it("[[Construct]] throws an error", () => { + const iterator = generatorIteratorFunction(); + assertThrows(() => new iterator(function* () {})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(generatorIteratorFunction().length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + generatorIteratorFunction().name, + "yields", + ); + }); + }); + describe("::next", () => { it("[[Call]] throws if there are values and the mapper is not a generator function", () => { const generator = function* () { @@ -378,6 +421,26 @@ describe("mapIteratorFunction", () => { }); }); + it("[[Construct]] throws an error", () => { + const iterator = mapIteratorFunction(); + assertThrows(() => new iterator(new Map())); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(mapIteratorFunction().length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + mapIteratorFunction().name, + "entries", + ); + }); + }); + describe("::next", () => { it("[[Call]] throws if there are values and the mapper is not a generator function", () => { const iterator = mapIteratorFunction(function () {}); @@ -493,6 +556,26 @@ describe("setIteratorFunction", () => { }); }); + it("[[Construct]] throws an error", () => { + const iterator = setIteratorFunction(); + assertThrows(() => new iterator(new Set())); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(setIteratorFunction().length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + setIteratorFunction().name, + "values", + ); + }); + }); + describe("::next", () => { it("[[Call]] throws if there are values and the mapper is not a generator function", () => { const iterator = setIteratorFunction(function () {}); @@ -613,6 +696,26 @@ describe("stringIteratorFunction", () => { }); }); + it("[[Construct]] throws an error", () => { + const iterator = stringIteratorFunction(); + assertThrows(() => new iterator("")); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(stringIteratorFunction().length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + stringIteratorFunction().name, + "characters", + ); + }); + }); + describe("::next", () => { it("[[Call]] throws if there are values and the mapper is not a generator function", () => { const iterator = stringIteratorFunction(function () {}); 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/object.js b/object.js index bd310ca..d382b31 100644 --- a/object.js +++ b/object.js @@ -1,15 +1,19 @@ -// ♓🌟 Piscēs ∷ object.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 ∷ object.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 { IS_CONCAT_SPREADABLE, isAccessorDescriptor, + isDataDescriptor, SPECIES, toFunctionName, toLength, @@ -17,6 +21,9 @@ import { UNDEFINED, } from "./value.js"; +const PISCĒS = "♓🧩 Piscēs"; + +const createArray = Array; const { isArray } = Array; const object = Object; const { @@ -71,24 +78,25 @@ const { * Methods will be called with the resulting object as their this * value. * - * `LazyLoader` objects have the same prototype as the passed methods + * `LazyLoader´ objects have the same prototype as the passed methods * object. */ export class LazyLoader extends null { /** - * Constructs a new `LazyLoader` object. + * Constructs a new `LazyLoader´ object. * * ☡ This function throws if the provided value is not an object. */ constructor(loadMethods) { if (type(loadMethods) !== "object") { - // The provided value is not an object; throw an error. + // The provided value is not an object; this is an error. throw new TypeError( - `Piscēs: Cannot construct LazyLoader: Provided argument is not an object: ${loadMethods}.`, + `${PISCĒS}: Cannot construct LazyLoader: Provided argument is not an object: ${loadMethods}.`, ); } else { - // The provided value is an object; process it and build the - // result. + // The provided value is an object. + // + // Process it and build the result. const result = create(getPrototypeOf(loadMethods)); const methodKeys = ownKeys(loadMethods); for (let index = 0; index < methodKeys.length; ++index) { @@ -186,7 +194,7 @@ export const defineOwnNonenumerableDataProperty = (O, P, V) => * Defines an own property on the provided object on the provided * property key using the provided property descriptor. * - * ※ This is effectively an alias for `Object.defineProperty`. + * ※ This is effectively an alias for `Object.defineProperty´. */ export const defineOwnProperty = (O, P, Desc) => defineProperty(O, P, Desc); @@ -195,12 +203,14 @@ export const defineOwnProperty = (O, P, Desc) => * Defines own properties on the provided object using the descriptors * on the enumerable own properties of the provided additional objects. * - * ※ This differs from `Object.defineProperties` in that it can take - * multiple source objects. + * ※ This function differs from `Object.defineProperties´ in that it + * can take multiple source objects. */ export const defineOwnProperties = (O, ...sources) => { const { length } = sources; for (let k = 0; k < length; ++k) { + // Iterate over each source and define the appropriate properties + // on the provided object. defineProperties(O, sources[k]); } return O; @@ -210,22 +220,28 @@ export const defineOwnProperties = (O, ...sources) => { * Removes the provided property key from the provided object and * returns the object. * - * ※ This function differs from `Reflect.deleteProperty` and the - * `delete` operator in that it throws if the deletion is + * ※ This function differs from `Reflect.deleteProperty´ and the + * `delete´ operator in that it throws if the deletion is * unsuccessful. * * ☡ This function throws if the first argument is not an object. */ export const deleteOwnProperty = (O, P) => { if (type(O) !== "object") { + // The provided value is not an object; this is an error. throw new TypeError( - `Piscēs: Tried to set property but provided value was not an object: ${V}`, + `${PISCĒS}: Tried to set property but provided value was not an object: ${V}`, ); } else if (!deleteProperty(O, P)) { + // The provided property could not be deleted on the provided + // value; this is an error. throw new TypeError( - `Piscēs: Tried to delete property from object but [[Delete]] returned false: ${P}`, + `${PISCĒS}: Tried to delete property from object but [[Delete]] returned false: ${P}`, ); } else { + // The provided property was successfully deleted. + // + // Return the provided value. return O; } }; @@ -235,7 +251,7 @@ export const deleteOwnProperty = (O, P) => { * properties as nonconfigurable and (if data properties) nonwritable, * and returns the object. * - * ※ This is effectively an alias for `Object.freeze`. + * ※ This is effectively an alias for `Object.freeze´. */ export const freeze = (O) => objectFreeze(O); @@ -247,15 +263,15 @@ export const freeze = (O) => objectFreeze(O); * property with the same value. * * - For accessor properties, create a nonconfigurable accessor - * property with the same getter *and* setter. + * property with the same getter ⹐and⹑ setter. * * The prototype for the resulting object will be taken from the - * `.prototype` property of the provided constructor, or the - * `.prototype` of the `.constructor` of the provided object if the + * `.prototype´ property of the provided constructor, or the + * `.prototype´ of the `.constructor´ of the provided object if the * provided constructor is undefined. If the used constructor has a - * nonnullish `.[Symbol.species]`, that will be used instead. If the + * nonnullish `.[Symbol.species]´, that will be used instead. If the * used constructor or species is nullish or does not have a - * `.prototype` property, the prototype is set to null. + * `.prototype´ property, the prototype is set to null. * * ※ The prototype of the provided object itself is ignored. */ @@ -263,13 +279,13 @@ export const frozenCopy = (O, constructor = O?.constructor) => { if (O == null) { // O is null or undefined. throw new TypeError( - "Piscēs: Cannot copy properties of null or undefined.", + `${PISCĒS}: Cannot copy properties of null or undefined.`, ); } else { // O is not null or undefined. // // (If not provided, the constructor will be the value of getting - // the `.constructor` property of O.) + // the `.constructor´ property of O.) const species = constructor?.[SPECIES] ?? constructor; const copy = create( species == null || !("prototype" in species) @@ -278,6 +294,8 @@ export const frozenCopy = (O, constructor = O?.constructor) => { ); const keys = ownKeys(O); for (let k = 0; k < keys.length; ++k) { + // Iterate over each key and define the appropriate value on the + // result. const P = keys[k]; const Desc = getOwnPropertyDescriptor(O, P); if (Desc.enumerable) { @@ -316,15 +334,23 @@ export const frozenCopy = (O, constructor = O?.constructor) => { * key. * * ☡ This function throws if the provided property key does not have an - * associated value which is callable. + * associated value which is either nullish or callable. */ export const getMethod = (V, P) => { const func = V[P]; if (func == null) { - return undefined; + // The value of the provided property is nullish. + // + // Return undefined. + return UNDEFINED; } else if (typeof func !== "function") { - throw new TypeError(`Piscēs: Method not callable: ${P}`); + // The value of the provided property is not callable; this is an + // error. + throw new TypeError(`${PISCĒS}: Method not callable: ${P}`); } else { + // The value of the provided property is callable. + // + // Return it. return func; } }; @@ -334,7 +360,7 @@ export const getMethod = (V, P) => { * provided property key on the provided value, or null if none exists. * * ※ This is effectively an alias for - * `Object.getOwnPropertyDescriptor`, but the return value is a proxied + * `Object.getOwnPropertyDescriptor´, but the return value is a proxied * object with null prototype. */ export const getOwnPropertyDescriptor = (O, P) => { @@ -349,7 +375,7 @@ export const getOwnPropertyDescriptor = (O, P) => { * provided value. * * ※ This is effectively an alias for - * `Object.getOwnPropertyDescriptors`, but the values on the resulting + * `Object.getOwnPropertyDescriptors´, but the values on the resulting * object are proxied objects with null prototypes. */ export const getOwnPropertyDescriptors = (O) => { @@ -357,6 +383,8 @@ export const getOwnPropertyDescriptors = (O) => { const keys = ownKeys(obj); const descriptors = {}; for (let k = 0; k < keys.length; ++k) { + // Iterate over the keys of the provided object and collect its + // descriptors. const key = keys[k]; defineOwnDataProperty( descriptors, @@ -368,9 +396,31 @@ export const getOwnPropertyDescriptors = (O) => { }; /** - * Returns an array of property keys on the provided value. + * Returns an array of own property entries on the provided value, + * using the provided receiver if given. + */ +export const getOwnPropertyEntries = (O, Receiver = O) => { + const obj = toObject(O); + const keys = ownKeys(obj); + const target = Receiver === UNDEFINED ? obj : toObject(Receiver); + const result = createArray(keys.length); + for (let k = 0; k < keys.length; ++k) { + // Iterate over each key and add the corresponding entry to the + // result. + const key = keys[k]; + defineOwnDataProperty( + result, + k, + [key, getOwnPropertyValue(obj, keys[k], target)], + ); + } + return result; +}; + +/** + * Returns an array of own property keys on the provided value. * - * ※ This is effectively an alias for `Reflect.ownKeys`, except that + * ※ This is effectively an alias for `Reflect.ownKeys´, except that * it does not require that the argument be an object. */ export const getOwnPropertyKeys = (O) => ownKeys(toObject(O)); @@ -381,7 +431,7 @@ export const getOwnPropertyKeys = (O) => ownKeys(toObject(O)); * * ☡ This includes both enumerable and non·enumerable properties. * - * ※ This is effectively an alias for `Object.getOwnPropertyNames`. + * ※ This is effectively an alias for `Object.getOwnPropertyNames´. */ export const getOwnPropertyStrings = (O) => getOwnPropertyNames(O); @@ -392,16 +442,68 @@ export const getOwnPropertyStrings = (O) => getOwnPropertyNames(O); * ☡ This includes both enumerable and non·enumerable properties. * * ※ This is effectively an alias for - * `Object.getOwnPropertySymbols`. + * `Object.getOwnPropertySymbols´. */ export const getOwnPropertySymbols = (O) => objectGetOwnPropertySymbols(O); +/** + * Returns the value of the provided own property on the provided + * value using the provided receiver, or undefined if the provided + * property is not an own property on the provided value. + * + * ※ If the receiver is not provided, it defaults to the provided + * value. + */ +export const getOwnPropertyValue = (O, P, Receiver = UNDEFINED) => { + const obj = toObject(O); + const desc = getOwnPropertyDescriptor(O, P); + if (desc === UNDEFINED) { + // The provided property is not an own property on the provided + // value. + // + // Return undefined. + return UNDEFINED; + } + if (isDataDescriptor(desc)) { + // The provided property is a data property. + // + // Return its value. + return desc.value; + } else { + // The provided property is an accessor property. + // + // Get its value using the appropriate receiver. + const target = Receiver === UNDEFINED ? obj : toObject(Receiver); + return call(desc.get, target, []); + } +}; + +/** + * Returns an array of own property values on the provided value, using + * the provided receiver if given. + */ +export const getOwnPropertyValues = (O, Receiver = O) => { + const obj = toObject(O); + const keys = ownKeys(obj); + const target = Receiver === UNDEFINED ? obj : toObject(Receiver); + const result = createArray(keys.length); + for (let k = 0; k < keys.length; ++k) { + // Iterate over each key and collect the values. + defineOwnDataProperty( + result, + k, + getOwnPropertyValue(obj, keys[k], target), + ); + } + return result; +}; + /** * Returns the value of the provided property key on the provided * value. * - * ※ This is effectively an alias for `Reflect.get`, except that it + * ※ This is effectively an alias for `Reflect.get´, except that it * does not require that the argument be an object. */ export const getPropertyValue = (O, P, Receiver = O) => @@ -410,7 +512,7 @@ export const getPropertyValue = (O, P, Receiver = O) => /** * Returns the prototype of the provided value. * - * ※ This is effectively an alias for `Object.getPrototypeOf`. + * ※ This is effectively an alias for `Object.getPrototypeOf´. */ export const getPrototype = (O) => getPrototypeOf(O); @@ -418,7 +520,7 @@ export const getPrototype = (O) => getPrototypeOf(O); * Returns whether the provided value has an own property with the * provided property key. * - * ※ This is effectively an alias for `Object.hasOwn`. + * ※ This is effectively an alias for `Object.hasOwn´. */ export const hasOwnProperty = (O, P) => hasOwn(O, P); @@ -426,7 +528,7 @@ export const hasOwnProperty = (O, P) => hasOwn(O, P); * Returns whether the provided property key exists on the provided * value. * - * ※ This is effectively an alias for `Reflect.has`, except that it + * ※ This is effectively an alias for `Reflect.has´, except that it * does not require that the argument be an object. * * ※ This includes properties present on the prototype chain. @@ -436,12 +538,18 @@ export const hasProperty = (O, P) => has(toObject(O), P); /** Returns whether the provided value is an arraylike object. */ export const isArraylikeObject = ($) => { if (type($) !== "object") { + // The provided value is not an object. return false; } else { + // The provided value is an object. try { - lengthOfArraylike($); // throws if not arraylike + // Try to get the length and return true. + // + // ※ If this throws, the object is not arraylike. + lengthOfArraylike($); return true; } catch { + // Getting the length failed; return false. return false; } } @@ -470,35 +578,35 @@ export const isConcatSpreadableObject = ($) => { * * ※ This function returns false for nonobjects. * - * ※ This is effectively an alias for `Object.isExtensible`. + * ※ This is effectively an alias for `Object.isExtensible´. */ export const isExtensibleObject = (O) => isExtensible(O); export const { /** * Returns whether the provided value is a property descriptor record - * as created by `toPropertyDescriptor`. + * as created by `toPropertyDescriptor´. * * ※ This function is provided to enable inspection of whether an - * object uses the property descriptor record proxy implementation, - * not as a general test of whether an object satisfies the - * requirements for property descriptors. In most cases, a more - * specific test, like `isAccessorDescriptor`, `isDataDescriptor`, or - * `isGenericDescriptor`, is preferrable. + * object uses the ♓🧩 Piscēs property descriptor record proxy + * implementation, not as a general test of whether an object + * satisfies the requirements for property descriptors. In most + * cases, a more specific test, like `isAccessorDescriptor´, + * `isDataDescriptor´, or `isGenericDescriptor´, is preferrable. */ isPropertyDescriptorRecord, /** * Converts the provided value to a property descriptor record. * - * ※ The prototype of a property descriptor record is always `null`. + * ※ The prototype of a property descriptor record is always `null´. * * ※ Actually constructing a property descriptor object using this * class is only necessary if you need strict guarantees about the * types of its properties; the resulting object is proxied to ensure * the types match what one would expect from composing - * FromPropertyDescriptor and ToPropertyDescriptor in the Ecmascript - * specification. + * `FromPropertyDescriptor´ and `ToPropertyDescriptor´ in the + * Ecmascript specification. */ toPropertyDescriptorRecord, } = (() => { @@ -514,19 +622,23 @@ export const { case "value": return V; case "get": - if (V !== undefined && typeof V !== "function") { + if (V !== UNDEFINED && typeof V !== "function") { + // The provided value is not callable; this is an error. throw new TypeError( - "Piscēs: Getters must be callable.", + `${PISCĒS}: Getters must be callable.`, ); } else { + // The provided value is callable. return V; } case "set": - if (V !== undefined && typeof V !== "function") { + if (V !== UNDEFINED && typeof V !== "function") { + // The provided value is not callable; this is an error. throw new TypeError( - "Piscēs: Setters must be callable.", + `${PISCĒS}: Setters must be callable.`, ); } else { + // The provided value is callable. return V; } default: @@ -539,54 +651,55 @@ export const { { defineProperty(O, P, Desc) { if ( - P === "configurable" || P === "enumerable" || - P === "writable" || P === "value" || - P === "get" || P === "set" + P === "configurable" || P === "enumerable" + || P === "writable" || P === "value" + || P === "get" || P === "set" ) { - // P is a property descriptor attribute. + // `P´ is a property descriptor attribute. const desc = assign(objectCreate(null), Desc); if ("get" in desc || "set" in desc) { - // Desc is an accessor property descriptor. + // `Desc´ is an accessor property descriptor. throw new TypeError( - "Piscēs: Property descriptor attributes must be data properties.", + `${PISCĒS}: Property descriptor attributes must be data properties.`, ); } else if ("value" in desc || !(P in O)) { - // Desc has a value or P does not already exist on O. + // `Desc´ has a value or `P´ does not already exist on + // `O´. desc.value = coercePropertyDescriptorValue( P, desc.value, ); } else { - // Desc is not an accessor property descriptor and has no - // value, but an existing value is present on O. + // `Desc´ is not an accessor property descriptor and has + // no value, but an existing value is present on `O´. /* do nothing */ } - const isAccessorDescriptor = "get" === P || "set" === P || - "get" in O || "set" in O; - const isDataDescriptor = "value" === P || - "writable" === P || - "value" in O || "writable" in O; + const isAccessorDescriptor = "get" === P || "set" === P + || "get" in O || "set" in O; + const isDataDescriptor = "value" === P + || "writable" === P + || "value" in O || "writable" in O; if (isAccessorDescriptor && isDataDescriptor) { - // Both accessor and data attributes will be present on O - // after defining P. + // Both accessor and data attributes will be present on + // `O´ after defining `P´; this is an error. throw new TypeError( - "Piscēs: Property descriptors cannot specify both accessor and data attributes.", + `${PISCĒS}: Property descriptors cannot specify both accessor and data attributes.`, ); } else { - // P can be safely defined on O. + // `P´ can be safely defined on `O´. return reflectDefineProperty(O, P, desc); } } else { - // P is not a property descriptor attribute. + // `P´ is not a property descriptor attribute. return reflectDefineProperty(O, P, Desc); } }, setPrototypeOf(O, V) { if (V !== null) { - // V is not the property descriptor prototype. + // `V´ is not the property descriptor prototype. return false; } else { - // V is the property descriptor prototype. + // `V´ is the property descriptor prototype. return reflectSetPrototypeOf(O, V); } }, @@ -599,9 +712,9 @@ export const { call(weakSetHas, propertyDescriptorRecords, [$]), toPropertyDescriptorRecord: (Obj) => { if (type(Obj) !== "object") { - // The provided value is not an object. + // The provided value is not an object; this is an error. throw new TypeError( - `Piscēs: Cannot convert primitive to property descriptor: ${O}.`, + `${PISCĒS}: Cannot convert primitive to property descriptor: ${O}.`, ); } else { // The provided value is an object. @@ -642,8 +755,10 @@ export const { // A get property is specified. const getter = Obj.get; if (getter !== UNDEFINED && typeof getter !== "function") { - // The getter is not callable. - throw new TypeError("Piscēs: Getters must be callable."); + // The getter is not callable; this is an error. + throw new TypeError( + `${PISCĒS}: Getters must be callable.`, + ); } else { // The getter is callable. defineOwnDataProperty(desc, "get", Obj.get); @@ -656,8 +771,10 @@ export const { // A set property is specified. const setter = Obj.set; if (setter !== UNDEFINED && typeof setter !== "function") { - // The setter is not callable. - throw new TypeError("Piscēs: Setters must be callable."); + // The setter is not callable; this is an error. + throw new TypeError( + `${PISCĒS}: Setters must be callable.`, + ); } else { // The setter is callable. defineOwnDataProperty(desc, "set", Obj.set); @@ -667,12 +784,13 @@ export const { /* do nothing */ } if ( - ("get" in desc || "set" in desc) && - ("value" in desc || "writable" in desc) + ("get" in desc || "set" in desc) + && ("value" in desc || "writable" in desc) ) { - // Both accessor and data attributes have been defined. + // Both accessor and data attributes have been defined; this + // is an error. throw new TypeError( - "Piscēs: Property descriptors cannot specify both accessor and data attributes.", + `${PISCĒS}: Property descriptors cannot specify both accessor and data attributes.`, ); } else { // The property descriptor is valid. @@ -693,7 +811,7 @@ export const { * * ※ This function returns false for nonobjects. * - * ※ This is effectively an alias for `!Object.isFrozen`. + * ※ This is effectively an alias for `!Object.isFrozen´. */ export const isUnfrozenObject = (O) => !isFrozen(O); @@ -702,7 +820,7 @@ export const isUnfrozenObject = (O) => !isFrozen(O); * * ※ This function returns false for nonobjects. * - * ※ This is effectively an alias for `!Object.isSealed`. + * ※ This is effectively an alias for `!Object.isSealed´. */ export const isUnsealedObject = (O) => !isSealed(O); @@ -720,7 +838,7 @@ export const lengthOfArraylike = ({ length }) => toLength(length); * Returns an array of key~value pairs for the enumerable, * string‐valued property keys on the provided value. * - * ※ This is effectively an alias for `Object.entries`. + * ※ This is effectively an alias for `Object.entries´. */ export const namedEntries = (O) => entries(O); @@ -728,7 +846,7 @@ export const namedEntries = (O) => entries(O); * Returns an array of the enumerable, string‐valued property keys on * the provided value. * - * ※ This is effectively an alias for `Object.keys`. + * ※ This is effectively an alias for `Object.keys´. */ export const namedKeys = (O) => keys(O); @@ -736,7 +854,7 @@ export const namedKeys = (O) => keys(O); * Returns an array of property values for the enumerable, * string‐valued property keys on the provided value. * - * ※ This is effectively an alias for `Object.values`. + * ※ This is effectively an alias for `Object.values´. */ export const namedValues = (O) => values(O); @@ -744,7 +862,7 @@ export const namedValues = (O) => values(O); * Returns a new object with the provided prototype and property * descriptors. * - * ※ This is effectively an alias for `Object.create`. + * ※ This is effectively an alias for `Object.create´. */ export const objectCreate = (O, Properties) => create(O, Properties); @@ -752,7 +870,7 @@ export const objectCreate = (O, Properties) => create(O, Properties); * Returns a new object with property keys and values from the provided * iterable value. * - * ※ This is effectively an alias for `Object.fromEntries`. + * ※ This is effectively an alias for `Object.fromEntries´. */ export const objectFromEntries = (iterable) => fromEntries(iterable); @@ -760,7 +878,7 @@ export const objectFromEntries = (iterable) => fromEntries(iterable); * Marks the provided object as non·extensible, and returns the * object. * - * ※ This is effectively an alias for `Object.preventExtensions`. + * ※ This is effectively an alias for `Object.preventExtensions´. */ export const preventExtensions = (O) => objectPreventExtensions(O); @@ -768,7 +886,7 @@ export const preventExtensions = (O) => objectPreventExtensions(O); * Marks the provided object as non·extensible and marks all its * properties as nonconfigurable, and returns the object. * - * ※ This is effectively an alias for `Object.seal`. + * ※ This is effectively an alias for `Object.seal´. */ export const seal = (O) => objectSeal(O); @@ -776,21 +894,26 @@ export const seal = (O) => objectSeal(O); * Sets the provided property key to the provided value on the provided * object and returns the object. * - * ※ This function differs from `Reflect.set` in that it throws if the + * ※ This function differs from `Reflect.set´ in that it throws if the * setting is unsuccessful. * * ☡ This function throws if the first argument is not an object. */ export const setPropertyValue = (O, P, V, Receiver = O) => { if (type(O) !== "object") { + // The provided value is not an object; this is an error. throw new TypeError( - `Piscēs: Tried to set property but provided value was not an object: ${V}`, + `${PISCĒS}: Tried to set property but provided value was not an object: ${V}`, ); } else if (!set(O, P, V, Receiver)) { + // Setting the property fails; this is an error. throw new TypeError( - `Piscēs: Tried to set property on object but [[Set]] returned false: ${P}`, + `${PISCĒS}: Tried to set property on object but [[Set]] returned false: ${P}`, ); } else { + // The property was successfully set. + // + // Return the provided object. return O; } }; @@ -799,7 +922,7 @@ export const setPropertyValue = (O, P, V, Receiver = O) => { * Sets the values of the enumerable own properties of the provided * additional objects on the provided values. * - * ※ This is effectively an alias for `Object.assign`. + * ※ This is effectively an alias for `Object.assign´. */ export const setPropertyValues = (target, source, ...sources) => { const to = toObject(target); @@ -808,7 +931,9 @@ export const setPropertyValues = (target, source, ...sources) => { // values. const nextSource = i === -1 ? source : sources[i]; if (nextSource != null) { - // The current source is not nullish; handle its own properties. + // The current source is not nullish. + // + // Handle its own properties. const from = toObject(nextSource); const keys = ownKeys(from); for (let k = 0; k < keys.length; ++k) { @@ -817,8 +942,9 @@ export const setPropertyValues = (target, source, ...sources) => { const nextKey = keys[k]; const desc = reflectGetOwnPropertyDescriptor(from, nextKey); if (desc !== UNDEFINED && desc.enumerable) { - // The current key is present and enumerable; set it to its - // corresponding value. + // The current key is present and enumerable. + // + // Set it to its corresponding value. const propValue = from[nextKey]; to[nextKey] = propValue; } else { @@ -838,21 +964,24 @@ export const setPropertyValues = (target, source, ...sources) => { * Sets the prototype of the provided object to the provided value and * returns the object. * - * ※ This is effectively an alias for `Object.setPrototypeOf`, but it - * won’t throw when setting the prototype of a primitive to its current + * ※ This is effectively an alias for `Object.setPrototypeOf´, but it + * won¦t throw when setting the prototype of a primitive to its current * value. */ export const setPrototype = (O, proto) => { const obj = toObject(O); if (O === obj) { - // The provided value is an object; set its prototype normally. + // The provided value is an object. + // + // Set its prototype normally. return setPrototypeOf(O, proto); } else { - // The provided value is not an object; attempt to set the - // prototype on a coerced version with extensions prevented, then - // return the provided value. + // The provided value is not an object. + // + // Attempt to set the prototype on a coerced version with + // extensions prevented, then return the provided value. // - // This will throw if the given prototype does not match the + // ☡ This will throw if the given prototype does not match the // existing one on the coerced object. setPrototypeOf(objectPreventExtensions(obj), proto); return O; @@ -870,10 +999,12 @@ export const toObject = ($) => { if ($ == null) { // The provided value is nullish; this is an error. throw new TypeError( - `Piscēs: Cannot convert ${$} into an object.`, + `${PISCĒS}: Cannot convert ${$} into an object.`, ); } else { - // The provided value is not nullish; coerce it to an object. + // The provided value is not nullish. + // + // Coerce it to an object. return object($); } }; diff --git a/object.test.js b/object.test.js index 348d06d..aa1796a 100644 --- a/object.test.js +++ b/object.test.js @@ -1,11 +1,14 @@ -// ♓🌟 Piscēs ∷ object.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 ∷ object.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, @@ -30,9 +33,12 @@ import { getMethod, getOwnPropertyDescriptor, getOwnPropertyDescriptors, + getOwnPropertyEntries, getOwnPropertyKeys, getOwnPropertyStrings, getOwnPropertySymbols, + getOwnPropertyValue, + getOwnPropertyValues, getPropertyValue, getPrototype, hasOwnProperty, @@ -668,8 +674,8 @@ describe("frozenCopy", () => { it("[[Call]] does not copy properties on the prototype", () => { assert( - !("failure" in - frozenCopy(Object.create({ failure: undefined }))), + !("failure" + in frozenCopy(Object.create({ failure: undefined }))), ); }); @@ -840,6 +846,58 @@ describe("getOwnPropertyDescriptors", () => { }); }); +describe("getOwnPropertyEntries", () => { + it("[[Call]] gets own (but not inherited) property entries", () => { + assertEquals( + getOwnPropertyEntries({ success: true }), + [["success", true]], + ); + }); + + it("[[Call]] works for values coercible to objects", () => { + assertEquals( + getOwnPropertyEntries("foo"), + [["0", "f"], ["1", "o"], ["2", "o"], ["length", 3]], + ); + }); + + it("[[Call]] uses the provided receiver", () => { + const target = {}; + assertEquals( + getOwnPropertyEntries({ + get success() { + return this; + }, + }, target), + [["success", target]], + ); + }); + + it("[[Call]] throws for null and undefined", () => { + assertThrows(() => getOwnPropertyEntries(null)); + assertThrows(() => getOwnPropertyEntries(undefined)); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new getOwnPropertyEntries({})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(getOwnPropertyEntries.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + getOwnPropertyEntries.name, + "getOwnPropertyEntries", + ); + }); + }); +}); + describe("getOwnPropertyKeys", () => { it("[[Call]] gets own (but not inherited) property keys", () => { assertEquals(getOwnPropertyKeys({ success: true }), ["success"]); @@ -950,6 +1008,115 @@ describe("getOwnPropertySymbols", () => { }); }); +describe("getOwnPropertyValue", () => { + it("[[Call]] gets the own property value", () => { + assertStrictEquals( + getOwnPropertyValue({ success: true }, "success"), + true, + ); + }); + + it("[[Call]] returns undefined for non‐own properties", () => { + assertStrictEquals( + getOwnPropertyValue(Object.create({ success: true }), "success"), + undefined, + ); + }); + + it("[[Call]] works for values coercible to objects", () => { + assertStrictEquals(getOwnPropertyValue("foo", "length"), 3); + }); + + it("[[Call]] uses the provided receiver", () => { + const target = {}; + assertStrictEquals( + getOwnPropertyValue( + { + get success() { + return this; + }, + }, + "success", + target, + ), + target, + ); + }); + + it("[[Call]] throws for null and undefined", () => { + assertThrows(() => getOwnPropertyValue(null)); + assertThrows(() => getOwnPropertyValue(undefined)); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new getOwnPropertyValue({})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(getOwnPropertyValue.length, 2); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + getOwnPropertyValue.name, + "getOwnPropertyValue", + ); + }); + }); +}); + +describe("getOwnPropertyValues", () => { + it("[[Call]] gets own (but not inherited) property values", () => { + assertEquals(getOwnPropertyValues({ success: true }), [true]); + }); + + it("[[Call]] works for values coercible to objects", () => { + assertEquals( + getOwnPropertyValues("foo"), + ["f", "o", "o", 3], + ); + }); + + it("[[Call]] uses the provided receiver", () => { + const target = {}; + assertEquals( + getOwnPropertyValues({ + get success() { + return this; + }, + }, target), + [target], + ); + }); + + it("[[Call]] throws for null and undefined", () => { + assertThrows(() => getOwnPropertyValues(null)); + assertThrows(() => getOwnPropertyValues(undefined)); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new getOwnPropertyValues({})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(getOwnPropertyValues.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + getOwnPropertyValues.name, + "getOwnPropertyValues", + ); + }); + }); +}); + describe("getPropertyValue", () => { it("[[Call]] gets property values on the provided object", () => { assertStrictEquals( diff --git a/string.js b/string.js index e8126d1..99528a8 100644 --- a/string.js +++ b/string.js @@ -1,15 +1,19 @@ -// ♓🌟 Piscēs ∷ string.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 ∷ string.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 { bind, call, + completesNormally, createArrowFunction, createCallableFunction, identity, @@ -28,6 +32,8 @@ import { } from "./object.js"; import { sameValue, toLength, UNDEFINED } from "./value.js"; +const PISCĒS = "♓🧩 Piscēs"; + const RE = RegExp; const { prototype: rePrototype } = RE; const { prototype: arrayPrototype } = Array; @@ -37,14 +43,14 @@ const { exec: reExec } = rePrototype; export const { /** - * A `RegExp`like object which only matches entire strings, and may + * A `RegExp´‐like object which only matches entire strings, and may * have additional constraints specified. * * Matchers are callable objects and will return true if they are * called with a string that they match, and false otherwise. - * Matchers will always return false if called with nonstrings, - * although other methods like `::exec` coerce their arguments and - * may still return true. + * Matchers will always return false if called with nonstrings, altho + * other methods like `::exec´ coerce their arguments and may still + * return true. */ Matcher, } = (() => { @@ -67,17 +73,28 @@ export const { Object.getOwnPropertyDescriptor(rePrototype, "sticky").get; const getUnicode = Object.getOwnPropertyDescriptor(rePrototype, "unicode").get; + const getUnicodeSets = + Object.getOwnPropertyDescriptor(rePrototype, "unicodeSets").get; + /** + * The internal implementation of `Matcher´. + * + * ※ This class extends the identity function to enable the addition + * of private fields to the callable matcher function it constructs. + * + * ※ This class is not exposed. + */ const Matcher = class extends identity { #constraint; #regExp; /** - * Constructs a new `Matcher` from the provided source. + * Constructs a new `Matcher´ from the provided source. * * If the provided source is a regular expression, then it must - * have the unicode flag set. Otherwise, it is interpreted as the - * string source of a regular expression with the unicode flag set. + * have either the unicode flag set or the unicode sets flag set. + * Otherwise, it is interpreted as the string source of a regular + * expression with the unicode flag set. * * Other flags are taken from the provided regular expression * object, if any are present. @@ -88,13 +105,13 @@ export const { * third argument. If provided, it will be called with three * arguments whenever a match appears successful: first, the string * being matched, second, the match result, and third, the - * `Matcher` object itself. If the return value of this call is + * `Matcher´ object itself. If the return value of this call is * falsey, then the match will be considered a failure. * * ☡ If the provided source regular expression uses nongreedy * quantifiers, it may not match the whole string even if a match * with the whole string is possible. Surround the regular - * expression with `^(?:` and `)$` if you don’t want nongreedy + * expression with `^(?:´ and `)$´ if you don¦t want nongreedy * regular expressions to fail when shorter matches are possible. */ constructor(source, name = UNDEFINED, constraint = null) { @@ -104,38 +121,46 @@ export const { // The provided value is not a string. return false; } else { - // The provided value is a string. Set the `.lastIndex` of - // the regular expression to 0 and see if the first attempt - // at a match matches the whole string and passes the - // provided constraint (if present). + // The provided value is a string. + // + // Set the `.lastIndex´ of the regular expression to 0, and + // see if the first attempt at a match successfully matches + // the whole string and passes the provided constraint (if + // present). regExp.lastIndex = 0; const result = call(reExec, regExp, [$]); - return result?.[0] === $ && - (constraint === null || constraint($, result, this)); + return result?.[0] === $ + && (constraint === null || constraint($, result, this)); } }, ); const regExp = this.#regExp = (() => { - try { - call(reExec, source, [""]); // throws if source not a RegExp - } catch { - return new RE(`${source}`, "u"); - } - const unicode = call(getUnicode, source, []); - if (!unicode) { - // The provided regular expression does not have a unicode - // flag. - throw new TypeError( - `Piscēs: Cannot create Matcher from non‐Unicode RegExp: ${source}`, - ); + if (completesNormally(() => call(reExec, source, [""]))) { + // The provided source is a `RegExp´. + if ( + !call(getUnicode, source, []) + && !call(getUnicodeSets, source, []) + ) { + // The provided regular expression does not have a unicode + // flag or unicode sets flag. + throw new TypeError( + `${PISCĒS}: Cannot create Matcher from non‐Unicode RegExp: ${source}`, + ); + } else { + // The provided regular expression has a unicode flag or + // unicode sets flag. + return new RE(source); + } } else { - // The provided regular expression has a unicode flag. - return new RE(source); + // The provided source is not a `RegExp´. + // + // Create one using it as the source string. + return new RE(`${source}`, "u"); } })(); if (constraint !== null && typeof constraint !== "function") { throw new TypeError( - "Piscēs: Cannot construct Matcher: Constraint is not callable.", + `${PISCĒS}: Cannot construct Matcher: Constraint is not callable.`, ); } else { this.#constraint = constraint; @@ -160,13 +185,13 @@ export const { } } - /** Gets whether the dot‐all flag is present on this `Matcher`. */ + /** Gets whether the dot‐all flag is present on this `Matcher´. */ get dotAll() { return call(getDotAll, this.#regExp, []); } /** - * Executes this `Matcher` on the provided value and returns the + * Executes this `Matcher´ on the provided value and returns the * result if there is a match, or null otherwise. * * Matchers only match if they can match the entire value on the @@ -183,8 +208,8 @@ export const { regExp.lastIndex = 0; const result = call(reExec, regExp, [string]); if ( - result?.[0] === string && - (constraint === null || constraint(string, result, this)) + result?.[0] === string + && (constraint === null || constraint(string, result, this)) ) { // The entire string was matched and the constraint, if // present, returned a truthy value. @@ -197,59 +222,64 @@ export const { } /** - * Gets the flags present on this `Matcher`. + * Gets the flags present on this `Matcher´. * - * ※ This needs to be defined because the internal `RegExp` object - * may have flags which are not yet recognized by ♓🌟 Piscēs. + * ※ This needs to be defined because the internal `RegExp´ object + * may have flags which are not yet recognized by ♓🧩 Piscēs. */ get flags() { return call(getFlags, this.#regExp, []); } - /** Gets whether the global flag is present on this `Matcher`. */ + /** Gets whether the global flag is present on this `Matcher´. */ get global() { return call(getGlobal, this.#regExp, []); } /** - * Gets whether the has‐indices flag is present on this `Matcher`. + * Gets whether the has‐indices flag is present on this `Matcher´. */ get hasIndices() { return call(getHasIndices, this.#regExp, []); } /** - * Gets whether the ignore‐case flag is present on this `Matcher`. + * Gets whether the ignore‐case flag is present on this `Matcher´. */ get ignoreCase() { return call(getIgnoreCase, this.#regExp, []); } /** - * Gets whether the multiline flag is present on this `Matcher`. + * Gets whether the multiline flag is present on this `Matcher´. */ get multiline() { return call(getMultiline, this.#regExp, []); } - /** Gets the regular expression source for this `Matcher`. */ + /** Gets the regular expression source for this `Matcher´. */ get source() { return call(getSource, this.#regExp, []); } - /** Gets whether the sticky flag is present on this `Matcher`. */ + /** Gets whether the sticky flag is present on this `Matcher´. */ get sticky() { return call(getSticky, this.#regExp, []); } /** - * Gets whether the unicode flag is present on this `Matcher`. - * - * ※ This will always be true. + * Gets whether the unicode flag is present on this `Matcher´. */ get unicode() { return call(getUnicode, this.#regExp, []); } + + /** + * Gets whether the unicode sets flag is present on this `Matcher´. + */ + get unicodeSets() { + return call(getUnicodeSets, this.#regExp, []); + } }; const matcherConstructor = Object.defineProperties( @@ -315,30 +345,11 @@ 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 * of the provided value according to the algorithm of - * `String::[Symbol.iterator]`. + * `String::[Symbol.iterator]´. */ characters, @@ -405,7 +416,7 @@ export const { /** * Returns the character at the provided position in the string * representation of the provided value according to the algorithm of - * `String::codePointAt`. + * `String::codePointAt´. */ export const getCharacter = ($, pos) => { const codepoint = getCodepoint($, pos); @@ -418,16 +429,16 @@ export const { /** * Returns the code unit at the provided position in the string * representation of the provided value according to the algorithm of - * `String::charAt`, except that out‐of‐bounds values return undefined - * in place of nan. + * `String::charAt´, except that out‐of‐bounds values return + * undefined in place of nan. */ getCodeUnit, /** * Returns a string created from the provided code units. * - * ※ This is effectively an alias for `String.fromCharCode`, but - * with the same error behaviour as `String.fromCodePoint`. + * ※ This is effectively an alias for `String.fromCharCode´, but + * with the same error behaviour as `String.fromCodePoint´. * * ☡ This function throws an error if provided with an argument which * is not an integral number from 0 to FFFF₁₆ inclusive. @@ -437,12 +448,12 @@ export const { /** * Returns the result of catenating the string representations of the * provided values, returning a new string according to the algorithm - * of `String::concat`. + * of `String::concat´. * * ※ If no arguments are given, this function returns the empty * string. This is different behaviour than if an explicit undefined * first argument is given, in which case the resulting string will - * begin with `"undefined"`. + * begin with `"undefined"´. */ stringCatenate, } = (() => { @@ -472,9 +483,9 @@ export const { !isIntegralNumber(nextCU) || nextCU < 0 || nextCU > 0xFFFF ) { // The code unit is not an integral number between 0 and - // 0xFFFF. + // 0xFFFF; this is an error. throw new RangeError( - `Piscēs: Code unit out of range: ${nextCU}.`, + `${PISCĒS}: Code unit out of range: ${nextCU}.`, ); } else { // The code unit is acceptable. @@ -491,7 +502,7 @@ export const { /** * Returns the codepoint at the provided position in the string * representation of the provided value according to the algorithm of - * `String::codePointAt`. + * `String::codePointAt´. */ export const getCodepoint = createCallableFunction( stringPrototype.codePointAt, @@ -501,7 +512,7 @@ export const getCodepoint = createCallableFunction( /** * Returns the index of the first occurrence of the search string in * the string representation of the provided value according to the - * algorithm of `String::indexOf`. + * algorithm of `String::indexOf´. */ export const getFirstSubstringIndex = createCallableFunction( stringPrototype.indexOf, @@ -511,45 +522,17 @@ export const getFirstSubstringIndex = createCallableFunction( /** * Returns the index of the last occurrence of the search string in the * string representation of the provided value according to the - * algorithm of `String::lastIndexOf`. + * algorithm of `String::lastIndexOf´. */ export const getLastSubstringIndex = createCallableFunction( stringPrototype.lastIndexOf, { 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. * - * If no separator is provided, it defaults to ",". + * If no separator is provided, it defaults to `","´. * * If a value is nullish, it will be stringified as the empty string. */ @@ -568,7 +551,7 @@ export const join = (() => { * Returns a string created from the raw value of the tagged template * literal. * - * ※ This is effectively an alias for `String.raw`. + * ※ This is effectively an alias for `String.raw´. */ export const rawString = createArrowFunction(String.raw, { name: "rawString", @@ -577,7 +560,7 @@ export const rawString = createArrowFunction(String.raw, { /** * Returns a string created from the provided codepoints. * - * ※ This is effectively an alias for `String.fromCodePoint`. + * ※ This is effectively an alias for `String.fromCodePoint´. * * ☡ This function throws an error if provided with an argument which * is not an integral number from 0 to 10FFFF₁₆ inclusive. @@ -613,7 +596,7 @@ export const splitOnCommas = ($) => /** * Returns whether the string representation of the provided value ends * with the provided search string according to the algorithm of - * `String::endsWith`. + * `String::endsWith´. */ export const stringEndsWith = createCallableFunction( stringPrototype.endsWith, @@ -623,7 +606,7 @@ export const stringEndsWith = createCallableFunction( /** * Returns whether the string representation of the provided value * contains the provided search string according to the algorithm of - * `String::includes`. + * `String::includes´. */ export const stringIncludes = createCallableFunction( stringPrototype.includes, @@ -633,7 +616,7 @@ export const stringIncludes = createCallableFunction( /** * Returns the result of matching the string representation of the * provided value with the provided matcher according to the algorithm - * of `String::match`. + * of `String::match´. */ export const stringMatch = createCallableFunction( stringPrototype.match, @@ -643,7 +626,7 @@ export const stringMatch = createCallableFunction( /** * Returns the result of matching the string representation of the * provided value with the provided matcher according to the algorithm - * of `String::matchAll`. + * of `String::matchAll´. */ export const stringMatchAll = createCallableFunction( stringPrototype.matchAll, @@ -652,7 +635,7 @@ export const stringMatchAll = createCallableFunction( /** * Returns the normalized form of the string representation of the - * provided value according to the algorithm of `String::normalize`. + * provided value according to the algorithm of `String::normalize´. */ export const stringNormalize = createCallableFunction( stringPrototype.normalize, @@ -662,7 +645,7 @@ export const stringNormalize = createCallableFunction( /** * Returns the result of padding the end of the string representation * of the provided value padded until it is the desired length - * according to the algorithm of `String::padEnd`. + * according to the algorithm of `String::padEnd´. */ export const stringPadEnd = createCallableFunction( stringPrototype.padEnd, @@ -672,7 +655,7 @@ export const stringPadEnd = createCallableFunction( /** * Returns the result of padding the start of the string representation * of the provided value padded until it is the desired length - * according to the algorithm of `String::padStart`. + * according to the algorithm of `String::padStart´. */ export const stringPadStart = createCallableFunction( stringPrototype.padStart, @@ -682,7 +665,7 @@ export const stringPadStart = createCallableFunction( /** * Returns the result of repeating the string representation of the * provided value the provided number of times according to the - * algorithm of `String::repeat`. + * algorithm of `String::repeat´. */ export const stringRepeat = createCallableFunction( stringPrototype.repeat, @@ -692,7 +675,7 @@ export const stringRepeat = createCallableFunction( /** * Returns the result of replacing the string representation of the * provided value with the provided replacement, using the provided - * matcher and according to the algorithm of `String::replace`. + * matcher and according to the algorithm of `String::replace´. */ export const stringReplace = createCallableFunction( stringPrototype.replace, @@ -702,7 +685,7 @@ export const stringReplace = createCallableFunction( /** * Returns the result of replacing the string representation of the * provided value with the provided replacement, using the provided - * matcher and according to the algorithm of `String::replaceAll`. + * matcher and according to the algorithm of `String::replaceAll´. */ export const stringReplaceAll = createCallableFunction( stringPrototype.replaceAll, @@ -712,7 +695,7 @@ export const stringReplaceAll = createCallableFunction( /** * Returns the result of searching the string representation of the * provided value using the provided matcher and according to the - * algorithm of `String::search`. + * algorithm of `String::search´. */ export const stringSearch = createCallableFunction( stringPrototype.search, @@ -721,7 +704,7 @@ export const stringSearch = createCallableFunction( /** * Returns a slice of the string representation of the provided value - * according to the algorithm of `String::slice`. + * according to the algorithm of `String::slice´. */ export const stringSlice = createCallableFunction( stringPrototype.slice, @@ -731,7 +714,7 @@ export const stringSlice = createCallableFunction( /** * Returns the result of splitting of the string representation of the * provided value on the provided separator according to the algorithm - * of `String::split`. + * of `String::split´. */ export const stringSplit = createCallableFunction( stringPrototype.split, @@ -741,7 +724,7 @@ export const stringSplit = createCallableFunction( /** * Returns whether the string representation of the provided value * starts with the provided search string according to the algorithm of - * `String::startsWith`. + * `String::startsWith´. */ export const stringStartsWith = createCallableFunction( stringPrototype.startsWith, @@ -751,10 +734,10 @@ export const stringStartsWith = createCallableFunction( /** * Returns the value of the provided string. * - * ※ This is effectively an alias for the `String::valueOf`. + * ※ This is effectively an alias for the `String::valueOf´. * * ☡ This function throws if the provided argument is not a string and - * does not have a `[[StringData]]` slot. + * does not have a `[[StringData]]´ slot. */ export const stringValue = createCallableFunction( stringPrototype.valueOf, @@ -784,7 +767,7 @@ export const stripLeadingAndTrailingAsciiWhitespace = ($) => /** * Returns a substring of the string representation of the provided - * value according to the algorithm of `String::substring`. + * value according to the algorithm of `String::substring´. */ export const substring = createCallableFunction( stringPrototype.substring, diff --git a/string.test.js b/string.test.js index ee0e209..e567060 100644 --- a/string.test.js +++ b/string.test.js @@ -1,11 +1,14 @@ -// ♓🌟 Piscēs ∷ string.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 ∷ string.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, @@ -20,7 +23,6 @@ import { import { asciiLowercase, asciiUppercase, - canonicalNumericIndexString, characters, codepoints, codeUnits, @@ -29,8 +31,6 @@ import { getCodeUnit, getFirstSubstringIndex, getLastSubstringIndex, - isArrayIndexString, - isIntegerIndexString, join, Matcher, rawString, @@ -75,7 +75,11 @@ describe("Matcher", () => { assert(new Matcher(/(?:)/u)); }); - it("[[Construct]] throws with a non·unicode regular expression first argument", () => { + it("[[Construct]] accepts a unicode sets regular expression first argument", () => { + assert(new Matcher(/(?:)/v)); + }); + + it("[[Construct]] throws with a non·unicode·aware regular expression first argument", () => { assertThrows(() => new Matcher(/(?:)/)); }); @@ -125,7 +129,7 @@ describe("Matcher", () => { assertStrictEquals(new Matcher(/(?:)/u).dotAll, false); }); - describe(".length", () => { + describe("[[GetOwnProperty]].get.length", () => { it("[[Get]] returns the correct length", () => { assertStrictEquals( Object.getOwnPropertyDescriptor( @@ -137,7 +141,7 @@ describe("Matcher", () => { }); }); - describe(".name", () => { + describe("[[GetOwnProperty]].get.name", () => { it("[[Get]] returns the correct name", () => { assertStrictEquals( Object.getOwnPropertyDescriptor( @@ -200,6 +204,11 @@ describe("Matcher", () => { ); }); + it("[[Construct]] throws an error", () => { + const matcher = new Matcher(""); + assertThrows(() => new matcher.exec()); + }); + describe(".length", () => { it("[[Get]] returns the correct length", () => { assertStrictEquals(Matcher.prototype.exec.length, 1); @@ -222,7 +231,7 @@ describe("Matcher", () => { assertStrictEquals(new Matcher(/(?:)/u).global, false); }); - describe(".length", () => { + describe("[[GetOwnProperty]].get.length", () => { it("[[Get]] returns the correct length", () => { assertStrictEquals( Object.getOwnPropertyDescriptor( @@ -234,7 +243,7 @@ describe("Matcher", () => { }); }); - describe(".name", () => { + describe("[[GetOwnProperty]].get.name", () => { it("[[Get]] returns the correct name", () => { assertStrictEquals( Object.getOwnPropertyDescriptor( @@ -256,7 +265,7 @@ describe("Matcher", () => { assertStrictEquals(new Matcher(/(?:)/u).hasIndices, false); }); - describe(".length", () => { + describe("[[GetOwnProperty]].get.length", () => { it("[[Get]] returns the correct length", () => { assertStrictEquals( Object.getOwnPropertyDescriptor( @@ -268,7 +277,7 @@ describe("Matcher", () => { }); }); - describe(".name", () => { + describe("[[GetOwnProperty]].get.name", () => { it("[[Get]] returns the correct name", () => { assertStrictEquals( Object.getOwnPropertyDescriptor( @@ -290,7 +299,7 @@ describe("Matcher", () => { assertStrictEquals(new Matcher(/(?:)/u).ignoreCase, false); }); - describe(".length", () => { + describe("[[GetOwnProperty]].get.length", () => { it("[[Get]] returns the correct length", () => { assertStrictEquals( Object.getOwnPropertyDescriptor( @@ -302,7 +311,7 @@ describe("Matcher", () => { }); }); - describe(".name", () => { + describe("[[GetOwnProperty]].get.name", () => { it("[[Get]] returns the correct name", () => { assertStrictEquals( Object.getOwnPropertyDescriptor( @@ -324,7 +333,7 @@ describe("Matcher", () => { assertStrictEquals(new Matcher(/(?:)/u).multiline, false); }); - describe(".length", () => { + describe("[[GetOwnProperty]].get.length", () => { it("[[Get]] returns the correct length", () => { assertStrictEquals( Object.getOwnPropertyDescriptor( @@ -336,7 +345,7 @@ describe("Matcher", () => { }); }); - describe(".name", () => { + describe("[[GetOwnProperty]].get.name", () => { it("[[Get]] returns the correct name", () => { assertStrictEquals( Object.getOwnPropertyDescriptor( @@ -355,7 +364,7 @@ describe("Matcher", () => { assertStrictEquals(new Matcher(/.*/su).source, ".*"); }); - describe(".length", () => { + describe("[[GetOwnProperty]].get.length", () => { it("[[Get]] returns the correct length", () => { assertStrictEquals( Object.getOwnPropertyDescriptor( @@ -367,7 +376,7 @@ describe("Matcher", () => { }); }); - describe(".name", () => { + describe("[[GetOwnProperty]].get.name", () => { it("[[Get]] returns the correct name", () => { assertStrictEquals( Object.getOwnPropertyDescriptor( @@ -389,7 +398,7 @@ describe("Matcher", () => { assertStrictEquals(new Matcher(/(?:)/u).sticky, false); }); - describe(".length", () => { + describe("[[GetOwnProperty]].get.length", () => { it("[[Get]] returns the correct length", () => { assertStrictEquals( Object.getOwnPropertyDescriptor( @@ -401,7 +410,7 @@ describe("Matcher", () => { }); }); - describe(".name", () => { + describe("[[GetOwnProperty]].get.name", () => { it("[[Get]] returns the correct name", () => { assertStrictEquals( Object.getOwnPropertyDescriptor( @@ -418,6 +427,26 @@ describe("Matcher", () => { it("[[Call]] returns the string source", () => { assertStrictEquals(new Matcher(/(?:)/u).toString(), "/(?:)/u"); }); + + it("[[Construct]] throws an error", () => { + const matcher = new Matcher(""); + assertThrows(() => new matcher.toString()); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(Matcher.prototype.toString.length, 0); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + Matcher.prototype.toString.name, + "toString", + ); + }); + }); }); describe("::unicode", () => { @@ -425,7 +454,7 @@ describe("Matcher", () => { assertStrictEquals(new Matcher(/(?:)/u).unicode, true); }); - describe(".length", () => { + describe("[[GetOwnProperty]].get.length", () => { it("[[Get]] returns the correct length", () => { assertStrictEquals( Object.getOwnPropertyDescriptor( @@ -437,7 +466,7 @@ describe("Matcher", () => { }); }); - describe(".name", () => { + describe("[[GetOwnProperty]].get.name", () => { it("[[Get]] returns the correct name", () => { assertStrictEquals( Object.getOwnPropertyDescriptor( @@ -450,6 +479,36 @@ describe("Matcher", () => { }); }); + describe("::unicodeSets", () => { + it("[[Get]] returns true when the unicode sets flag is present", () => { + assertStrictEquals(new Matcher(/(?:)/v).unicodeSets, true); + }); + + describe("[[GetOwnProperty]].get.length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals( + Object.getOwnPropertyDescriptor( + Matcher.prototype, + "unicodeSets", + ).get.length, + 0, + ); + }); + }); + + describe("[[GetOwnProperty]].get.name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + Object.getOwnPropertyDescriptor( + Matcher.prototype, + "unicodeSets", + ).get.name, + "get unicodeSets", + ); + }); + }); + }); + describe("~", () => { it("[[Call]] returns true for a complete match", () => { assertStrictEquals(new Matcher("")(""), true); @@ -491,6 +550,11 @@ describe("Matcher", () => { false, ); }); + + it("[[Construct]] throws an error", () => { + const matcher = new Matcher(""); + assertThrows(() => new matcher("")); + }); }); describe("~lastIndex", () => { @@ -568,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( @@ -941,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"); @@ -1708,8 +1594,8 @@ describe("stringReplace", () => { "very success full", /([sc]+)[ue]?/g, (...$s) => - `${$s[0].length}`.repeat($s[1].length) + - $s[0].substring($s[1].length), + `${$s[0].length}`.repeat($s[1].length) + + $s[0].substring($s[1].length), ), "very 2u33e22 full", ); @@ -1743,8 +1629,8 @@ describe("stringReplaceAll", () => { "very success full", /([sc]+)[ue]?/g, (...$s) => - `${$s[0].length}`.repeat($s[1].length) + - $s[0].substring($s[1].length), + `${$s[0].length}`.repeat($s[1].length) + + $s[0].substring($s[1].length), ), "very 2u33e22 full", ); diff --git a/symbol.js b/symbol.js index 2bd3717..d303aec 100644 --- a/symbol.js +++ b/symbol.js @@ -1,18 +1,23 @@ -// ♓🌟 Piscēs ∷ symbol.js -// ==================================================================== -// -// Copyright © 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: 2023, 2025 Lady +// SPDX-License-Identifier: MPL-2.0 +/** + * ⁌ ♓🧩 Piscēs ∷ symbol.js + * + * Copyright © 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 { createCallableFunction } from "./function.js"; +const PISCĒS = "♓🧩 Piscēs"; + /** * Returns the description for the provided symbol. * - * ※ This is effectively an alias for the `Symbol::description` + * ※ This is effectively an alias for the `Symbol::description´ * getter. * * ☡ This function throws if the provided argument is not a symbol. @@ -25,10 +30,10 @@ export const getSymbolDescription = createCallableFunction( /** * Returns a string representation of the provided symbol. * - * ※ Use `getSymbolDescription` instead if you just want the text + * ※ Use `getSymbolDescription´ instead if you just want the text * description of a symbol. * - * ※ This is effectively an alias for the `Symbol::toString`. + * ※ This is effectively an alias for the `Symbol::toString´. * * ☡ This function throws if the provided argument is not a symbol. */ @@ -40,10 +45,10 @@ export const symbolToString = createCallableFunction( /** * Returns the value of the provided symbol. * - * ※ This is effectively an alias for the `Symbol::valueOf`. + * ※ This is effectively an alias for the `Symbol::valueOf´. * * ☡ This function throws if the provided argument is not a symbol and - * does not have a `[[SymbolData]]` slot. + * does not have a `[[SymbolData]]´ slot. */ export const symbolValue = createCallableFunction( Symbol.prototype.valueOf, diff --git a/symbol.test.js b/symbol.test.js index 03abffc..22bc58e 100644 --- a/symbol.test.js +++ b/symbol.test.js @@ -1,11 +1,14 @@ -// ♓🌟 Piscēs ∷ symbol.test.js -// ==================================================================== -// -// Copyright © 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: 2023, 2025 Lady +// SPDX-License-Identifier: MPL-2.0 +/** + * ⁌ ♓🧩 Piscēs ∷ symbol.test.js + * + * Copyright © 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 { assertStrictEquals, diff --git a/value.js b/value.js index cf858ad..341ecdf 100644 --- a/value.js +++ b/value.js @@ -1,47 +1,52 @@ -// ♓🌟 Piscēs ∷ value.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 ∷ value.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 . + */ + +const PISCĒS = "♓🧩 Piscēs"; export const { - /** The welknown `@@asyncIterator` symbol. */ + /** The welknown `@@asyncIterator´ symbol. */ asyncIterator: ASYNC_ITERATOR, - /** The welknown `@@hasInstance` symbol. */ + /** The welknown `@@hasInstance´ symbol. */ hasInstance: HAS_INSTANCE, - /** The welknown `@@isConcatSpreadable` symbol. */ + /** The welknown `@@isConcatSpreadable´ symbol. */ isConcatSpreadable: IS_CONCAT_SPREADABLE, - /** The welknown `@@iterator` symbol. */ + /** The welknown `@@iterator´ symbol. */ iterator: ITERATOR, - /** The welknown `@@match` symbol. */ + /** The welknown `@@match´ symbol. */ match: MATCH, - /** The welknown `@@matchAll` symbol. */ + /** The welknown `@@matchAll´ symbol. */ matchAll: MATCH_ALL, - /** The welknown `@@replace` symbol. */ + /** The welknown `@@replace´ symbol. */ replace: REPLACE, - /** The welknown `@@species` symbol. */ + /** The welknown `@@species´ symbol. */ species: SPECIES, - /** The welknown `@@split` symbol. */ + /** The welknown `@@split´ symbol. */ split: SPLIT, - /** The welknown `@@toPrimitive` symbol. */ + /** The welknown `@@toPrimitive´ symbol. */ toPrimitive: TO_PRIMITIVE, - /** The welknown `@@toStringTag` symbol. */ + /** The welknown `@@toStringTag´ symbol. */ toStringTag: TO_STRING_TAG, - /** The welknown `@@unscopables` symbol. */ + /** The welknown `@@unscopables´ symbol. */ unscopables: UNSCOPABLES, } = Symbol; @@ -49,116 +54,116 @@ export const { /** * ln(10). * - * ※ This is an alias for `Math.LN10`. + * ※ This is an alias for `Math.LN10´. */ - LN10, + LN10: LN_10, /** * ln(2). * - * ※ This is an alias for `Math.LN2`. + * ※ This is an alias for `Math.LN2´. */ - LN2, + LN2: LN_2, /** - * log10(ℇ). + * log10(e). * - * ※ This is an alias for `Math.LOG10E`. + * ※ This is an alias for `Math.LOG10E´. */ - LOG10E: LOG10ℇ, + LOG10E: LOG10_𝑒, /** - * log2(ℇ). + * log2(e). * - * ※ This is an alias for `Math.LOG2E`. + * ※ This is an alias for `Math.LOG2E´. */ - LOG2E: LOG2ℇ, + LOG2E: LOG2_𝑒, /** * sqrt(½). * - * ※ This is an alias for `Math.SQRT1_2`. + * ※ This is an alias for `Math.SQRT1_2´. */ - SQRT1_2: RECIPROCAL_SQRT2, + SQRT1_2: RECIPROCAL_SQRT_2, /** * sqrt(2). * - * ※ This is an alias for `Math.SQRT2`. + * ※ This is an alias for `Math.SQRT2´. */ - SQRT2, + SQRT2: SQRT_2, /** - * The mathematical constant π. + * The mathematical constant 𝑒. * - * ※ This is an alias for `Math.PI`. + * ※ This is an alias for `Math.E´. */ - PI: Π, + E: 𝑒, /** - * The Euler number. + * The mathematical constant 𝜋. * - * ※ This is an alias for `Math.E`. + * ※ This is an alias for `Math.PI´. */ - E: ℇ, + PI: 𝜋, } = Math; export const { /** * The largest number value less than infinity. * - * ※ This is an alias for `Number.MAX_VALUE`. + * ※ This is an alias for `Number.MAX_VALUE´. */ MAX_VALUE: MAXIMUM_NUMBER, /** - * 2**53 - 1. + * 2^53 - 1. * - * ※ This is an alias for `Number.MAX_SAFE_INTEGER`. + * ※ This is an alias for `Number.MAX_SAFE_INTEGER´. */ MAX_SAFE_INTEGER: MAXIMUM_SAFE_INTEGRAL_NUMBER, /** * The smallest number value greater than negative infinity. * - * ※ This is an alias for `Number.MIN_VALUE`. + * ※ This is an alias for `Number.MIN_VALUE´. */ MIN_VALUE: MINIMUM_NUMBER, /** - * -(2**53 - 1). + * -(2^53 - 1). * - * ※ This is an alias for `Number.MIN_SAFE_INTEGER`. + * ※ This is an alias for `Number.MIN_SAFE_INTEGER´. */ MIN_SAFE_INTEGER: MINIMUM_SAFE_INTEGRAL_NUMBER, /** * Negative infinity. * - * ※ This is an alias for `Number.NEGATIVE_INFINITY`. + * ※ This is an alias for `Number.NEGATIVE_INFINITY´. */ NEGATIVE_INFINITY, /** * Nan. * - * ※ This is an alias for `Number.NaN`. + * ※ This is an alias for `Number.NaN´. */ NaN: NAN, /** * Positive infinity. * - * ※ This is an alias for `Number.POSITIVE_INFINITY`. + * ※ This is an alias for `Number.POSITIVE_INFINITY´. */ POSITIVE_INFINITY, /** * The difference between 1 and the smallest number greater than 1. * - * ※ This is an alias for `Number.EPSILON`. + * ※ This is an alias for `Number.EPSILON´. */ - EPSILON: Ε, + EPSILON: 𝜀, } = Number; /** Negative zero. */ @@ -173,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. @@ -181,54 +205,55 @@ export const UNDEFINED = undefined; */ export const completePropertyDescriptor = (Desc) => { if (Desc === UNDEFINED) { + // A description was not provided; this is an error. throw new TypeError( - "Piscēs: Cannot complete undefined property descriptor.", + `${PISCĒS}: Cannot complete undefined property descriptor.`, ); } else if (!("get" in Desc || "set" in Desc)) { // This is a generic or data descriptor. if (!("value" in Desc)) { - // `value` is not defined on this. + // `value´ is not defined on this. Desc.value = UNDEFINED; } else { - // `value` is already defined on this. + // `value´ is already defined on this. /* do nothing */ } if (!("writable" in Desc)) { - // `writable` is not defined on this. + // `writable´ is not defined on this. Desc.writable = false; } else { - // `writable` is already defined on this. + // `writable´ is already defined on this. /* do nothing */ } } else { // This is not a generic or data descriptor. if (!("get" in Desc)) { - // `get` is not defined on this. + // `get´ is not defined on this. Desc.get = UNDEFINED; } else { - // `get` is already defined on this. + // `get´ is already defined on this. /* do nothing */ } if (!("set" in Desc)) { - // `set` is not defined on this. + // `set´ is not defined on this. Desc.set = UNDEFINED; } else { - // `set` is already defined on this. + // `set´ is already defined on this. /* do nothing */ } } if (!("enumerable" in Desc)) { - // `enumerable` is not defined on this. + // `enumerable´ is not defined on this. Desc.enumerable = false; } else { - // `enumerable` is already defined on this. + // `enumerable´ is already defined on this. /* do nothing */ } if (!("configurable" in Desc)) { - // `configurable` is not defined on this. + // `configurable´ is not defined on this. Desc.configurable = false; } else { - // `configurable` is already defined on this. + // `configurable´ is already defined on this. /* do nothing */ } }; @@ -237,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); @@ -246,27 +286,42 @@ export const isDataDescriptor = (Desc) => * descriptor. */ export const isFullyPopulatedDescriptor = (Desc) => - Desc !== UNDEFINED && - ("value" in Desc && "writable" in Desc || - "get" in Desc && "set" in Desc) && - "enumerable" in Desc && "configurable" in Desc; + Desc !== UNDEFINED + && ("value" in Desc && "writable" in Desc + || "get" in Desc && "set" in Desc) + && "enumerable" in Desc && "configurable" in Desc; /** * Gets whether the provided value is a generic (not accessor or data) * descrtiptor. */ export const isGenericDescriptor = (Desc) => - Desc !== UNDEFINED && - !("get" in Desc || "set" in Desc || "value" in Desc || - "writable" in Desc); + Desc !== UNDEFINED + && !("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 - * `.toString` and `.valueOf` methods. + * `.toString´ and `.valueOf´ methods. * - * If the provided hint is "string", then `.toString` takes - * precedence; otherwise, `.valueOf` does. + * If the provided hint is "string", then `.toString´ takes + * precedence; otherwise, `.valueOf´ does. * * Throws an error if both of these methods are not callable or do * not return a primitive. @@ -285,7 +340,7 @@ export const { * * The provided preferred type, if specified, should be "string", * "number", or "default". If the provided input has a - * `.[Symbol.toPrimitive]` method, this function will throw rather + * `.[Symbol.toPrimitive]´ method, this function will throw rather * than calling that method with a preferred type other than one of * the above. */ @@ -303,6 +358,11 @@ export const { ? ["toString", "valueOf"] : ["valueOf", "toString"]; for (let index = 0; index < methodNames.length; ++index) { + // Test the methods in the order determined above (based on the + // hint) and return the result if the method returns a + // primitive. + // + // ☡ If this loop exits with·out returning, it is an error. const method = O[methodNames[index]]; if (typeof method === "function") { // Method is callable. @@ -320,19 +380,22 @@ export const { } } throw new TypeError( - "Piscēs: Unable to convert object to primitive", + `${PISCĒS}: Unable to convert object to primitive.`, ); }, toFunctionName: ($, prefix = UNDEFINED) => { const key = toPrimitive($, "string"); const name = (() => { if (typeof key === "symbol") { - // The provided value is a symbol; format its description. + // The provided value is a symbol. + // + // Format its description. const description = call(getSymbolDescription, key, []); return description === UNDEFINED ? "" : `[${description}]`; } else { - // The provided value not a symbol; convert it to a string - // property key. + // The provided value not a symbol. + // + // Convert it to a string property key. return `${key}`; } })(); @@ -341,12 +404,12 @@ export const { toPrimitive: ($, preferredType = "default") => { const hint = `${preferredType}`; if ( - "default" !== hint && "string" !== hint && - "number" !== hint + "default" !== hint && "string" !== hint + && "number" !== hint ) { // An invalid preferred type was specified. throw new TypeError( - `Piscēs: Invalid preferred type: ${preferredType}.`, + `${PISCĒS}: Invalid preferred type: ${preferredType}.`, ); } else if (type($) === "object") { // The provided value is an object. @@ -357,7 +420,7 @@ export const { if (typeof exoticToPrim !== "function") { // The method is not callable. throw new TypeError( - "Piscēs: `.[Symbol.toPrimitive]` was neither nullish nor callable.", + `${PISCĒS}: .[Symbol.toPrimitive] was neither nullish nor callable.`, ); } else { // The method is callable. @@ -379,7 +442,7 @@ export const { /** * Returns whether the provided values are the same value. * - * ※ This differs from `===` in the cases of nan and zero. + * ※ This differs from `===´ in the cases of nan and zero. */ sameValue, @@ -387,7 +450,7 @@ export const { * Returns whether the provided values are either the same value or * both zero (either positive or negative). * - * ※ This differs from `===` in the case of nan. + * ※ This differs from `===´ in the case of nan. */ sameValueZero, @@ -406,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); @@ -414,11 +477,14 @@ export const { // The provided values are not of the same type. return false; } else if (type1 === "number") { - // The provided values are numbers; check if they are nan and - // use strict equality otherwise. + // The provided values are numbers. + // + // Check if they are nan and use strict equality otherwise. return isNan($1) && isNan($2) || $1 === $2; } else { - // The provided values are not numbers; use strict equality. + // The provided values are not numbers. + // + // Use strict equality. return $1 === $2; } }, @@ -426,15 +492,19 @@ export const { const integer = floor($); if (isNan(integer) || integer == 0) { // The value is zero·like. + // + // Return positive zero. return 0; } else { // The value is not zero·like. const clamped = toLength(integer); if (clamped !== integer) { - // Clamping the value changes it. - throw new RangeError(`Piscēs: Index out of range: ${$}.`); + // Clamping the value changes it; this is an error. + throw new RangeError(`${PISCĒS}: Index out of range: ${$}.`); } else { // The value is within appropriate bounds. + // + // Return it. return integer; } } @@ -461,8 +531,8 @@ export const toPropertyKey = ($) => { * Returns a lowercase string identifying the type of the provided * value. * - * This differs from the value of the `typeof` operator only in the - * cases of objects and null. + * This differs from the value of the `typeof´ operator only in the + * cases of callable objects and null. */ export const type = ($) => { if ($ === null) { diff --git a/value.test.js b/value.test.js index ce6464d..c55b591 100644 --- a/value.test.js +++ b/value.test.js @@ -1,11 +1,14 @@ -// ♓🌟 Piscēs ∷ value.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 ∷ value.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 { assertEquals, @@ -16,18 +19,21 @@ import { } from "./dev-deps.js"; import { ASYNC_ITERATOR, + canonicalNumericIndexSting, completePropertyDescriptor, HAS_INSTANCE, IS_CONCAT_SPREADABLE, isAccessorDescriptor, + isArrayIndexString, isDataDescriptor, isFullyPopulatedDescriptor, isGenericDescriptor, + isIntegerIndexString, ITERATOR, - LN10, - LN2, - LOG10ℇ, - LOG2ℇ, + LN_10, + LN_2, + LOG10_𝑒, + LOG2_𝑒, MATCH, MATCH_ALL, MAXIMUM_NUMBER, @@ -41,13 +47,13 @@ import { ordinaryToPrimitive, POSITIVE_INFINITY, POSITIVE_ZERO, - RECIPROCAL_SQRT2, + RECIPROCAL_SQRT_2, REPLACE, sameValue, sameValueZero, SPECIES, SPLIT, - SQRT2, + SQRT_2, TO_PRIMITIVE, TO_STRING_TAG, toFunctionName, @@ -58,9 +64,9 @@ import { type, UNDEFINED, UNSCOPABLES, - Ε, - Π, - ℇ, + 𝑒, + 𝜀, + 𝜋, } from "./value.js"; describe("ASYNC_ITERATOR", () => { @@ -90,27 +96,27 @@ describe("ITERATOR", () => { }); }); -describe("LN10", () => { +describe("LN_10", () => { it("[[Get]] is ln(10)", () => { - assertStrictEquals(LN10, Math.LN10); + assertStrictEquals(LN_10, Math.LN10); }); }); -describe("LN2", () => { +describe("LN_2", () => { it("[[Get]] is ln(2)", () => { - assertStrictEquals(LN2, Math.LN2); + assertStrictEquals(LN_2, Math.LN2); }); }); -describe("LOG10ℇ", () => { - it("[[Get]] is log10(ℇ)", () => { - assertStrictEquals(LOG10ℇ, Math.LOG10E); +describe("LOG10_𝑒", () => { + it("[[Get]] is log10(𝑒)", () => { + assertStrictEquals(LOG10_𝑒, Math.LOG10E); }); }); -describe("LOG2ℇ", () => { +describe("LOG2_𝑒", () => { it("[[Get]] is log2(ℇ)", () => { - assertStrictEquals(LOG2ℇ, Math.LOG2E); + assertStrictEquals(LOG2_𝑒, Math.LOG2E); }); }); @@ -192,9 +198,9 @@ describe("POSITIVE_ZERO", () => { }); }); -describe("RECIPROCAL_SQRT2", () => { +describe("RECIPROCAL_SQRT_2", () => { it("[[Get]] is sqrt(½)", () => { - assertStrictEquals(RECIPROCAL_SQRT2, Math.SQRT1_2); + assertStrictEquals(RECIPROCAL_SQRT_2, Math.SQRT1_2); }); }); @@ -216,9 +222,9 @@ describe("SPLIT", () => { }); }); -describe("SQRT2", () => { +describe("SQRT_2", () => { it("[[Get]] is sqrt(2)", () => { - assertStrictEquals(SQRT2, Math.SQRT2); + assertStrictEquals(SQRT_2, Math.SQRT2); }); }); @@ -246,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 = {}; @@ -349,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); @@ -502,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 = { @@ -1071,20 +1255,20 @@ describe("type", () => { }); }); -describe("Ε", () => { - it("[[Get]] is ε", () => { - assertStrictEquals(Ε, Number.EPSILON); +describe("𝑒", () => { + it("[[Get]] is 𝑒", () => { + assertStrictEquals(𝑒, Math.E); }); }); -describe("Π", () => { - it("[[Get]] is π", () => { - assertStrictEquals(Π, Math.PI); +describe("𝜀", () => { + it("[[Get]] is 𝜀", () => { + assertStrictEquals(𝜀, Number.EPSILON); }); }); -describe("ℇ", () => { - it("[[Get]] is ℇ", () => { - assertStrictEquals(ℇ, Math.E); +describe("𝜋", () => { + it("[[Get]] is 𝜋", () => { + assertStrictEquals(𝜋, Math.PI); }); });