From: Lady Date: Sat, 23 Jul 2022 02:42:47 +0000 (-0700) Subject: Add arraylike functions and collection unit tests X-Git-Tag: 0.1.0~4 X-Git-Url: https://git.ladys.computer/Pisces/commitdiff_plain/f1283dfa8f4f1482dac6325fbcb66f8778af1002?hp=2989f964f0325a2d9c9294a8d3dab722313d5518 Add arraylike functions and collection unit tests --- diff --git a/collection.js b/collection.js index 10172e6..20b6fef 100644 --- a/collection.js +++ b/collection.js @@ -7,8 +7,27 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at . -import { MAX_SAFE_INTEGER } from "./numeric.js"; -import { isObject, sameValue } from "./object.js"; +import { call, makeCallable } from "./function.js"; +import { + floor, + isIntegralNumber, + isNan, + max, + MAXIMUM_SAFE_INTEGRAL_NUMBER, + min, +} from "./numeric.js"; +import { sameValue, type } from "./value.js"; + +export const { + /** Returns an array of the provided values. */ + of: array, + + /** Returns whether the provided value is an array. */ + isArray, + + /** Returns an array created from the provided arraylike. */ + from: toArray, +} = Array; /** * Returns -0 if the provided argument is "-0"; returns a number @@ -19,7 +38,7 @@ import { isObject, sameValue } from "./object.js"; * above 2^53 − 1 are not safe nor valid integer indices. */ export const canonicalNumericIndexString = ($) => { - if (typeof $ != "string") { + if (typeof $ !== "string") { return undefined; } else if ($ === "-0") { return -0; @@ -29,12 +48,142 @@ export const canonicalNumericIndexString = ($) => { } }; -/** Returns whether the provided value is an array index. */ -export const isArrayIndex = ($) => { +/** + * Returns the result of catenating the provided arraylikes, returning + * a new collection according to the algorithm of Array::concat. + */ +export const catenate = makeCallable(Array.prototype.concat); + +/** + * Copies the items in the provided object to a new location according + * to the algorithm of Array::copyWithin. + */ +export const copyWithin = makeCallable(Array.prototype.copyWithin); + +/** + * Fills the provided object with the provided value using the + * algorithm of Array::fill. + */ +export const fill = makeCallable(Array.prototype.fill); + +/** + * Returns the result of filtering the provided object with the + * provided callback, using the algorithm of Array::filter. + */ +export const filter = makeCallable(Array.prototype.filter); + +/** + * Returns the first index in the provided object whose value satisfies + * the provided callback using the algorithm of Array::findIndex. + */ +export const findIndex = makeCallable(Array.prototype.findIndex); + +/** + * 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. + */ +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; +}; + +/** + * Returns the first indexed value in the provided object which + * satisfies the provided callback, using the algorithm of Array::find. + */ +export const findItem = makeCallable(Array.prototype.find); + +/** + * Returns the result of flatmapping the provided value with the + * provided callback using the algorithm of Array::flatMap. + */ +export const flatmap = makeCallable(Array.prototype.flatMap); + +/** + * Returns the result of flattening the provided object using the + * algorithm of Array::flat. + */ +export const flatten = makeCallable(Array.prototype.flat); + +/** + * Returns the first index of the provided object with a value + * equivalent to the provided value according to the algorithm of + * Array::indexOf. + */ +export const getFirstIndex = makeCallable(Array.prototype.indexOf); + +/** + * Returns the item on the provided object at the provided index using + * the algorithm of Array::at. + */ +export const getItem = makeCallable(Array.prototype.at); + +/** + * Returns the last index of the provided object with a value + * equivalent to the provided value according to the algorithm of + * Array::lastIndexOf. + */ +export const getLastIndex = makeCallable(Array.prototype.lastIndexOf); + +/** + * Returns whether every indexed value in the provided object satisfies + * the provided function, using the algorithm of Array::every. + */ +export const hasEvery = makeCallable(Array.prototype.every); + +/** + * Returns whether the provided object has an indexed value which + * satisfies the provided function, using the algorithm of Array::some. + */ +export const hasSome = makeCallable(Array.prototype.some); + +/** + * Returns whether the provided object has an indexed value equivalent + * to the provided value according to the algorithm of Array::includes. + * + * > ☡ This algorithm treats missing values as `undefined` rather than + * > skipping them. + */ +export const includes = makeCallable(Array.prototype.includes); + +/** + * Returns an iterator over the indexed entries in the provided value + * according to the algorithm of Array::entries. + */ +export const indexedEntries = makeCallable(Array.prototype.entries); + +/** + * Returns an iterator over the indices in the provided value according + * to the algorithm of Array::keys. + */ +export const indices = makeCallable(Array.prototype.keys); + +/** Returns whether the provided value is an array index string. */ +export const isArrayIndexString = ($) => { const value = canonicalNumericIndexString($); if (value !== undefined) { - return sameValue(value, 0) || value > 0 && value < -1 >>> 0; + // The provided value is a canonical numeric index string. + return sameValue(value, 0) || value > 0 && value < -1 >>> 0 && + value === toLength(value); } else { + // The provided value is not a canonical numeric index string. return false; } }; @@ -48,14 +197,13 @@ export const isArrayIndex = ($) => { * * - It requires the provided value to be a proper object. * - * - It requires the `length` property to be an integer corresponding - * to an integer index. + * - It requires the `length` property to be an integer index. * * - It requires the object to be concat‐spreadable, meaning it must * either be an array or have `[Symbol.isConcatSpreadable]` be true. */ export const isCollection = ($) => { - if (!(isObject($) && "length" in $)) { + if (!(type($) === "object" && "length" in $)) { // The provided value is not an object or does not have a `length`. return false; } else { @@ -76,46 +224,99 @@ export const isCollection = ($) => { * collections. */ export const isConcatSpreadable = ($) => { - if (!isObject($)) { + if (type($) !== "object") { // The provided value is not an object. return false; } else { // The provided value is an object. - return !!($[Symbol.isConcatSpreadable] ?? Array.isArray($)); + const spreadable = $[Symbol.isConcatSpreadable]; + return spreadable !== undefined ? !!spreadable : isArray($); } }; -/** Returns whether the provided value is an integer index. */ -export const isIntegerIndex = ($) => { +/** Returns whether the provided value is an integer index string. */ +export const isIntegerIndexString = ($) => { const value = canonicalNumericIndexString($); - if (value !== undefined) { + if (value !== undefined && isIntegralNumber(value)) { + // The provided value is a canonical numeric index string. return sameValue(value, 0) || - value > 0 && value <= MAX_SAFE_INTEGER; + value > 0 && value <= MAXIMUM_SAFE_INTEGRAL_NUMBER && + value === toLength(value); } else { + // The provided value is not a canonical numeric index string. return false; } }; +/** + * Returns an iterator over the items in the provided value according + * to the algorithm of Array::values. + */ +export const items = makeCallable(Array.prototype.values); + /** * Returns the length of the provided arraylike object. * * Will throw if the provided object is not arraylike. * - * This produces larger lengths than can actually be stored in arrays, - * because no such restrictions exist on arraylike methods. Use - * `isIndex` to determine if a value is an actual array index. + * This can produce larger lengths than can actually be stored in + * arrays, because no such restrictions exist on arraylike methods. */ -export const lengthOfArrayLike = ({ length }) => { - return toLength(length); -}; +export const lengthOfArrayLike = ({ length }) => toLength(length); + +/** + * Returns the result of mapping the provided value with the provided + * callback using the algorithm of Array::map. + */ +export const map = makeCallable(Array.prototype.map); + +/** Pops from the provided value using the algorithm of Array::pop. */ +export const pop = makeCallable(Array.prototype.pop); /** - * Converts the provided value to an array index, or throws an error if - * it is out of range. + * Pushes onto the provided value using the algorithm of Array::push. + */ +export const push = makeCallable(Array.prototype.push); + +/** + * Returns the result of reducing the provided value with the provided + * callback, using the algorithm of Array::reduce. + */ +export const reduce = makeCallable(Array.prototype.reduce); + +/** + * Reverses the provided value using the algorithm of Array::reverse. + */ +export const reverse = makeCallable(Array.prototype.reverse); + +/** Shifts the provided value using the algorithm of Array::shift. */ +export const shift = makeCallable(Array.prototype.shift); + +/** + * Returns a slice of the provided value using the algorithm of + * Array::slice. + */ +export const slice = makeCallable(Array.prototype.slice); + +/** + * Sorts the provided value in‐place using the algorithm of + * Array::sort. + */ +export const sort = makeCallable(Array.prototype.sort); + +/** + * Splices into and out of the provided value using the algorithm of + * Array::splice. + */ +export const splice = makeCallable(Array.prototype.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 = Math.floor($); - if (isNaN(integer) || integer == 0) { + const integer = floor($); + if (isNan(integer) || integer == 0) { // The value is zero·like. return 0; } else { @@ -131,10 +332,15 @@ export const toIndex = ($) => { } }; -/** Converts the provided value to a length. */ +/** Returns the result of converting the provided value to a length. */ export const toLength = ($) => { - const len = Math.floor($); - return isNaN(len) || len == 0 + const len = floor($); + return isNan(len) || len == 0 ? 0 - : Math.max(Math.min(len, MAX_SAFE_INTEGER), 0); + : max(min(len, MAXIMUM_SAFE_INTEGRAL_NUMBER), 0); }; + +/** + * Unshifts the provided value using the algorithm of Array::unshift. + */ +export const unshift = makeCallable(Array.prototype.unshift); diff --git a/collection.test.js b/collection.test.js new file mode 100644 index 0000000..b490572 --- /dev/null +++ b/collection.test.js @@ -0,0 +1,401 @@ +// ♓🌟 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 . + +import { + assertEquals, + assertSpyCall, + assertSpyCalls, + assertStrictEquals, + assertThrows, + describe, + it, + spy, +} from "./dev-deps.js"; +import { + canonicalNumericIndexString, + findIndexedEntry, + isArrayIndexString, + 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 {}); + assertStrictEquals(findIndexedEntry([1], () => false), void {}); + }); + + it("[[Call]] returns an entry for the first match", () => { + assertEquals( + findIndexedEntry([, true, false], ($) => $ ?? true), + [0, void {}], + ); + assertEquals( + findIndexedEntry(["failure", "success"], ($) => $ == "success"), + [1, "success"], + ); + }); + + it("[[Call]] works on arraylike objects", () => { + assertEquals( + findIndexedEntry({ 1: "success", length: 2 }, ($) => $), + [1, "success"], + ); + assertEquals( + findIndexedEntry({ 1: "failure", length: 1 }, ($) => $), + void {}, + ); + }); + + it("[[Call]] only gets the value once", () => { + const get1 = spy(() => true); + findIndexedEntry({ + 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 = {}; + findIndexedEntry(arr, callback, thisArg); + assertSpyCalls(callback, 2); + assertSpyCall(callback, 0, { + args: ["failure", 0, arr], + self: thisArg, + }); + assertSpyCall(callback, 1, { + args: ["success", 1, arr], + self: thisArg, + }); + }); +}); + +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, + ); + }); +}); + +describe("isCollection", () => { + it("[[Call]] returns false for primitives", () => { + assertStrictEquals(isCollection("failure"), false); + }); + + it("[[Call]] returns false if length throws", () => { + assertStrictEquals( + isCollection({ + get length() { + throw void {}; + }, + }), + false, + ); + }); + + it("[[Call]] returns false if length is not an integer index and cannot be converted to one", () => { + assertStrictEquals( + isCollection({ length: -1, [Symbol.isConcatSpreadable]: true }), + false, + ); + assertStrictEquals( + isCollection({ + length: Infinity, + [Symbol.isConcatSpreadable]: true, + }), + false, + ); + assertStrictEquals( + isCollection({ + length: 9007199254740992, + [Symbol.isConcatSpreadable]: true, + }), + false, + ); + }); + + it("[[Call]] returns true if length is an integer index and the object is concat spreadable", () => { + assertStrictEquals( + isCollection({ length: 1, [Symbol.isConcatSpreadable]: true }), + true, + ); + assertStrictEquals( + isCollection({ length: 0, [Symbol.isConcatSpreadable]: true }), + true, + ); + assertStrictEquals( + isCollection({ + length: 9007199254740991, + [Symbol.isConcatSpreadable]: true, + }), + true, + ); + }); + + it("[[Call]] returns true if length can be converted to an index without throwing an error and the object is concat spreadable", () => { + assertStrictEquals( + isCollection({ length: -0, [Symbol.isConcatSpreadable]: true }), + true, + ); + assertStrictEquals( + isCollection({ length: NaN, [Symbol.isConcatSpreadable]: true }), + true, + ); + }); +}); + +describe("isConcatSpreadable", () => { + it("[[Call]] returns false for primitives", () => { + assertStrictEquals(isConcatSpreadable("failure"), false); + }); + + it("[[Call]] returns false if [Symbol.isConcatSpreadable] is null or false", () => { + assertStrictEquals( + isConcatSpreadable( + Object.assign([], { [Symbol.isConcatSpreadable]: null }), + ), + false, + ); + assertStrictEquals( + isConcatSpreadable( + Object.assign([], { [Symbol.isConcatSpreadable]: false }), + ), + false, + ); + }); + + it("[[Call]] returns true if [Symbol.isConcatSpreadable] is undefined and the object is an array", () => { + assertStrictEquals( + isConcatSpreadable( + Object.assign([], { [Symbol.isConcatSpreadable]: undefined }), + ), + true, + ); + }); + + it("[[Call]] returns true if [Symbol.isConcatSpreadable] is true", () => { + assertStrictEquals( + isConcatSpreadable({ [Symbol.isConcatSpreadable]: true }), + true, + ); + }); +}); + +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); + }); +}); + +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); + }); +});