From: Lady Date: Sun, 29 Oct 2023 23:26:53 +0000 (-0400) Subject: Move arraylike & index functions → values.js X-Git-Url: https://git.ladys.computer/Pisces/commitdiff_plain/00a99e780024dae2f15b5def2064f9d29d738e9f?ds=inline Move arraylike & index functions → values.js Lengths and indices are properly kinds of values, not kinds of collections, and this enables their use much earlier in the dependency chain. --- diff --git a/collection.js b/collection.js index dbdc8ed..4545321 100644 --- a/collection.js +++ b/collection.js @@ -9,14 +9,17 @@ import { call, createCallableFunction } from "./function.js"; import { - floor, isIntegralNumber, - isNan, - max, MAXIMUM_SAFE_INTEGRAL_NUMBER, - min, } from "./numeric.js"; -import { sameValue, type } from "./value.js"; +import { + canonicalNumericIndexString, + lengthOfArraylike, + sameValue, + toIndex, + toLength, + type, +} from "./value.js"; const { prototype: arrayPrototype } = Array; @@ -31,25 +34,6 @@ export const { from: toArray, } = Array; -/** - * 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; - } -}; - /** * Returns the result of catenating the provided arraylikes into a new * collection according to the algorithm of `Array::concat`. @@ -316,16 +300,6 @@ export const items = createCallableFunction( "items", ); -/** - * Returns the length of the provided arraylike object. - * - * Will throw if the provided object is not arraylike. - * - * This can produce larger lengths than can actually be stored in - * arrays, because no such restrictions exist on arraylike methods. - */ -export const lengthOfArraylike = ({ length }) => toLength(length); - /** * Returns the result of mapping the provided value with the provided * callback according to the algorithm of `Array::map`. @@ -380,36 +354,6 @@ export const sort = createCallableFunction(arrayPrototype.sort); */ export const splice = createCallableFunction(arrayPrototype.splice); -/** - * Returns the result of converting the provided value to an array - * index, or throws an error if it is out of range. - */ -export const toIndex = ($) => { - const integer = floor($); - if (isNan(integer) || integer == 0) { - // The value is zero·like. - return 0; - } else { - // The value is not zero·like. - const clamped = toLength(integer); - if (clamped !== integer) { - // Clamping the value changes it. - throw new RangeError(`Piscēs: Index out of range: ${$}.`); - } else { - // The value is within appropriate bounds. - return integer; - } - } -}; - -/** Returns the result of converting the provided value to a length. */ -export const toLength = ($) => { - const len = floor($); - return isNan(len) || len == 0 - ? 0 - : max(min(len, MAXIMUM_SAFE_INTEGRAL_NUMBER), 0); -}; - /** * Unshifts the provided value according to the algorithm of * `Array::unshift`. diff --git a/collection.test.js b/collection.test.js index bdded97..544f454 100644 --- a/collection.test.js +++ b/collection.test.js @@ -12,58 +12,19 @@ import { assertSpyCall, assertSpyCalls, assertStrictEquals, - assertThrows, describe, it, spy, } from "./dev-deps.js"; import { - canonicalNumericIndexString, findIndexedEntry, isArrayIndexString, isArraylikeObject, isCollection, isConcatSpreadable, isIntegerIndexString, - lengthOfArraylike, - toIndex, - toLength, } from "./collection.js"; -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, - ); - }); -}); - describe("findIndexedEntry", () => { it("[[Call]] returns undefined if no matching entry exists", () => { assertStrictEquals(findIndexedEntry([], () => true), void {}); @@ -343,93 +304,3 @@ describe("isIntegerIndexString", () => { assertStrictEquals(isIntegerIndexString("9007199254740991"), true); }); }); - -describe("lengthOfArraylike", () => { - it("[[Call]] returns the length", () => { - assertStrictEquals( - lengthOfArraylike({ length: 9007199254740991 }), - 9007199254740991, - ); - }); - - it("[[Call]] returns a non·nan result", () => { - assertStrictEquals(lengthOfArraylike({ length: NaN }), 0); - assertStrictEquals(lengthOfArraylike({ length: "failure" }), 0); - }); - - it("[[Call]] returns an integral result", () => { - assertStrictEquals(lengthOfArraylike({ length: 0.25 }), 0); - assertStrictEquals(lengthOfArraylike({ length: 1.1 }), 1); - }); - - it("[[Call]] returns a result greater than or equal to zero", () => { - assertStrictEquals(lengthOfArraylike({ length: -0 }), 0); - assertStrictEquals(lengthOfArraylike({ length: -1 }), 0); - assertStrictEquals(lengthOfArraylike({ length: -Infinity }), 0); - }); - - it("[[Call]] returns a result less than 2 ** 53", () => { - assertStrictEquals( - lengthOfArraylike({ length: 9007199254740992 }), - 9007199254740991, - ); - assertStrictEquals( - lengthOfArraylike({ length: Infinity }), - 9007199254740991, - ); - }); -}); - -describe("toIndex", () => { - it("[[Call]] returns an index", () => { - assertStrictEquals(toIndex(9007199254740991), 9007199254740991); - }); - - it("[[Call]] returns zero for a zerolike result", () => { - assertStrictEquals(toIndex(NaN), 0); - assertStrictEquals(toIndex("failure"), 0); - assertStrictEquals(toIndex(-0), 0); - }); - - it("[[Call]] rounds down to the nearest integer", () => { - assertStrictEquals(toIndex(0.25), 0); - assertStrictEquals(toIndex(1.1), 1); - }); - - it("[[Call]] throws when provided a negative number", () => { - assertThrows(() => toIndex(-1)); - assertThrows(() => toIndex(-Infinity)); - }); - - it("[[Call]] throws when provided a number greater than or equal to 2 ** 53", () => { - assertThrows(() => toIndex(9007199254740992)); - assertThrows(() => toIndex(Infinity)); - }); -}); - -describe("toLength", () => { - it("[[Call]] returns a length", () => { - assertStrictEquals(toLength(9007199254740991), 9007199254740991); - }); - - it("[[Call]] returns a non·nan result", () => { - assertStrictEquals(toLength(NaN), 0); - assertStrictEquals(toLength("failure"), 0); - }); - - it("[[Call]] returns an integral result", () => { - assertStrictEquals(toLength(0.25), 0); - assertStrictEquals(toLength(1.1), 1); - }); - - it("[[Call]] returns a result greater than or equal to zero", () => { - assertStrictEquals(toLength(-0), 0); - assertStrictEquals(toLength(-1), 0); - assertStrictEquals(toLength(-Infinity), 0); - }); - - it("[[Call]] returns a result less than 2 ** 53", () => { - assertStrictEquals(toLength(9007199254740992), 9007199254740991); - assertStrictEquals(toLength(Infinity), 9007199254740991); - }); -}); diff --git a/value.js b/value.js index bda9180..dca20e8 100644 --- a/value.js +++ b/value.js @@ -51,6 +51,35 @@ export const NULL = null; /** 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; + } +}; + +/** + * Returns the length of the provided arraylike value. + * + * This can produce larger lengths than can actually be stored in + * arrays, because no such restrictions exist on arraylike methods. + * + * ☡ This function throws if the provided value is not arraylike. + */ +export const lengthOfArraylike = ({ length }) => toLength(length); + export const { /** * Returns the primitive value of the provided object per its @@ -156,8 +185,23 @@ export const { * ※ This differs from `===` in the case of nan. */ sameValueZero, + + /** + * Returns the result of converting the provided value to an index, + * or throws an error if it is out of range. + */ + toIndex, + + /** + * Returns the result of converting the provided value to a length. + */ + toLength, } = (() => { - const { isNaN: isNan } = Number; + const { floor, max, min } = Math; + const { + MAX_SAFE_INTEGER: MAXIMUM_SAFE_INTEGRAL_NUMBER, + isNaN: isNan, + } = Number; const { is } = Object; return { sameValue: (a, b) => is(a, b), @@ -176,6 +220,29 @@ export const { return $1 === $2; } }, + toIndex: ($) => { + const integer = floor($); + if (isNan(integer) || integer == 0) { + // The value is zero·like. + return 0; + } else { + // The value is not zero·like. + const clamped = toLength(integer); + if (clamped !== integer) { + // Clamping the value changes it. + throw new RangeError(`Piscēs: Index out of range: ${$}.`); + } else { + // The value is within appropriate bounds. + return integer; + } + } + }, + toLength: ($) => { + const len = floor($); + return isNan(len) || len == 0 + ? 0 + : max(min(len, MAXIMUM_SAFE_INTEGRAL_NUMBER), 0); + }, }; })(); diff --git a/value.test.js b/value.test.js index ae2b7d8..cecafbb 100644 --- a/value.test.js +++ b/value.test.js @@ -15,9 +15,11 @@ import { } from "./dev-deps.js"; import { ASYNC_ITERATOR, + canonicalNumericIndexString, HAS_INSTANCE, IS_CONCAT_SPREADABLE, ITERATOR, + lengthOfArraylike, MATCH, MATCH_ALL, NULL, @@ -29,6 +31,8 @@ import { SPLIT, TO_PRIMITIVE, TO_STRING_TAG, + toIndex, + toLength, toPrimitive, type, UNDEFINED, @@ -122,6 +126,116 @@ 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("lengthOfArraylike", () => { + it("[[Call]] returns the length", () => { + assertStrictEquals( + lengthOfArraylike({ length: 9007199254740991 }), + 9007199254740991, + ); + }); + + it("[[Call]] returns a non·nan result", () => { + assertStrictEquals(lengthOfArraylike({ length: NaN }), 0); + assertStrictEquals(lengthOfArraylike({ length: "failure" }), 0); + }); + + it("[[Call]] returns an integral result", () => { + assertStrictEquals(lengthOfArraylike({ length: 0.25 }), 0); + assertStrictEquals(lengthOfArraylike({ length: 1.1 }), 1); + }); + + it("[[Call]] returns a result greater than or equal to zero", () => { + assertStrictEquals(lengthOfArraylike({ length: -0 }), 0); + assertStrictEquals(lengthOfArraylike({ length: -1 }), 0); + assertStrictEquals(lengthOfArraylike({ length: -Infinity }), 0); + }); + + it("[[Call]] returns a result less than 2 ** 53", () => { + assertStrictEquals( + lengthOfArraylike({ length: 9007199254740992 }), + 9007199254740991, + ); + assertStrictEquals( + lengthOfArraylike({ length: Infinity }), + 9007199254740991, + ); + }); + + it("[[Call]] does not require an object argument", () => { + assertStrictEquals(lengthOfArraylike("string"), 6); + assertStrictEquals(lengthOfArraylike(Symbol()), 0); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new lengthOfArraylike("")); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(lengthOfArraylike.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(lengthOfArraylike.name, "lengthOfArraylike"); + }); + }); +}); + describe("ordinaryToPrimitive", () => { it("[[Call]] prefers `valueOf` by default", () => { const obj = { @@ -335,6 +449,92 @@ describe("sameValueZero", () => { }); }); +describe("toIndex", () => { + it("[[Call]] returns an index", () => { + assertStrictEquals(toIndex(9007199254740991), 9007199254740991); + }); + + it("[[Call]] returns zero for a zerolike argument", () => { + assertStrictEquals(toIndex(NaN), 0); + assertStrictEquals(toIndex("failure"), 0); + assertStrictEquals(toIndex(-0), 0); + }); + + it("[[Call]] rounds down to the nearest integer", () => { + assertStrictEquals(toIndex(0.25), 0); + assertStrictEquals(toIndex(1.1), 1); + }); + + it("[[Call]] throws when provided a negative number", () => { + assertThrows(() => toIndex(-1)); + assertThrows(() => toIndex(-Infinity)); + }); + + it("[[Call]] throws when provided a number greater than or equal to 2 ** 53", () => { + assertThrows(() => toIndex(9007199254740992)); + assertThrows(() => toIndex(Infinity)); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new toIndex(0)); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(toIndex.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(toIndex.name, "toIndex"); + }); + }); +}); + +describe("toLength", () => { + it("[[Call]] returns a length", () => { + assertStrictEquals(toLength(9007199254740991), 9007199254740991); + }); + + it("[[Call]] returns zero for a nan argument", () => { + assertStrictEquals(toLength(NaN), 0); + assertStrictEquals(toLength("failure"), 0); + }); + + it("[[Call]] rounds down to the nearest integer", () => { + assertStrictEquals(toLength(0.25), 0); + assertStrictEquals(toLength(1.1), 1); + }); + + it("[[Call]] returns a result greater than or equal to zero", () => { + assertStrictEquals(toLength(-0), 0); + assertStrictEquals(toLength(-1), 0); + assertStrictEquals(toLength(-Infinity), 0); + }); + + it("[[Call]] returns a result less than 2 ** 53", () => { + assertStrictEquals(toLength(9007199254740992), 9007199254740991); + assertStrictEquals(toLength(Infinity), 9007199254740991); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new toLength(0)); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(toLength.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(toLength.name, "toLength"); + }); + }); +}); + describe("toPrimitive", () => { it("[[Call]] returns the argument when passed a primitive", () => { const value = Symbol();