]> Lady’s Gitweb - Pisces/commitdiff
Add iterator function builders; use in string.js
authorLady <redacted>
Sat, 22 Jul 2023 06:30:50 +0000 (23:30 -0700)
committerLady <redacted>
Sat, 22 Jul 2023 06:33:21 +0000 (23:33 -0700)
This commit adds a number of new `⸺IteratorFunction` functions in a
new `iterable.js` module, for building “iterator functions” from
various kinds of iterable object (as well as generator functions, which
are not themselves iterable). These functions are useful
metaprogramming tools for creating iterators akin to those produced by
the native Ecmascript builtins like `Array::values` or `Map::entries`
when starting from an already‐iterable base value but needing to map it
somehow.

The existing functions `codeUnits`, `codepoints`, and `scalarValues`
already fell into this category and have been refactored to make use of
the new technique.

iterable.js [new file with mode: 0644]
iterable.test.js [new file with mode: 0644]
mod.js
string.js
string.test.js

diff --git a/iterable.js b/iterable.js
new file mode 100644 (file)
index 0000000..d89e35f
--- /dev/null
@@ -0,0 +1,394 @@
+// ♓🌟 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 <https://mozilla.org/MPL/2.0/>.
+
+import { bind, call, identity } from "./function.js";
+import {
+  defineOwnProperty,
+  getPrototype,
+  objectCreate,
+} from "./object.js";
+import { ITERATOR, TO_STRING_TAG } from "./value.js";
+
+export const {
+  /**
+   * Returns an iterator function which iterates over the values in its
+   * provided arraylike object and yields them.
+   *
+   * If a first argument is provided, it must be a generator function.
+   * The yielded values are instead those yielded by calling that
+   * function with each value in turn.
+   *
+   * If a second argument is provided, it is used as the string tag for
+   * the resulting iterator.
+   *
+   * The resulting function also takes a second argument, which will be
+   * used as the `this` value when calling the provided generator
+   * function, if provided.
+   *
+   * ※ The returned function is an ordinary nonconstructible (arrow)
+   * function which, when called with an array, returns an iterator.
+   *
+   * ※ The prototypes of iterators returned by a single iterator
+   * function will all be the same.
+   */
+  arrayIteratorFunction,
+
+  /**
+   * Returns an iterator function which iterates over the values
+   * yielded by its provided generator function and yields them.
+   *
+   * If a first argument is provided, it must be a generator function.
+   * The yielded values are instead those yielded by calling that
+   * function with each value in turn.
+   *
+   * If a second argument is provided, it is used as the string tag for
+   * the resulting iterator.
+   *
+   * The resulting function also takes a second argument, which will be
+   * used as the `this` value when calling the provided generator
+   * function, if provided.
+   *
+   * ※ The returned function is an ordinary nonconstructible (arrow)
+   * function which, when called with a generator function, returns an
+   * iterator.
+   *
+   * ☡ There is no way to detect whether the function provided to the
+   * returned function is, in fact, a generator function. Consequently,
+   * if a non‐generator function is provided, it will not throw until
+   * the first attempt at reading a value.
+   *
+   * ※ The prototypes of iterators returned by a single iterator
+   * function will all be the same.
+   */
+  generatorIteratorFunction,
+
+  /**
+   * Returns an iterator function which iterates over the values in its
+   * provided map and yields them.
+   *
+   * If a first argument is provided, it must be a generator function.
+   * The yielded values are instead those yielded by calling that
+   * function with each value in turn.
+   *
+   * If a second argument is provided, it is used as the string tag for
+   * the resulting iterator.
+   *
+   * The resulting function also takes a second argument, which will be
+   * used as the `this` value when calling the provided generator
+   * function, if provided.
+   *
+   * ※ The returned function is an ordinary nonconstructible (arrow)
+   * function which, when called with a map, returns an iterator.
+   *
+   * ※ The prototypes of iterators returned by a single iterator
+   * function will all be the same.
+   */
+  mapIteratorFunction,
+
+  /**
+   * Returns an iterator function which iterates over the values in its
+   * provided set and yields them.
+   *
+   * If a first argument is provided, it must be a generator function.
+   * The yielded values are instead those yielded by calling that
+   * function with each value in turn.
+   *
+   * If a second argument is provided, it is used as the string tag for
+   * the resulting iterator.
+   *
+   * The resulting function also takes a second argument, which will be
+   * used as the `this` value when calling the provided generator
+   * function, if provided.
+   *
+   * ※ The returned function is an ordinary nonconstructible (arrow)
+   * function which, when called with a set, returns an iterator.
+   *
+   * ※ The prototypes of iterators returned by a single iterator
+   * function will all be the same.
+   */
+  setIteratorFunction,
+
+  /**
+   * Returns an iterator function which iterates over the characters in
+   * its provided string and yields them.
+   *
+   * If a first argument is provided, it must be a generator function.
+   * The yielded values are instead those yielded by calling that
+   * function with each value in turn.
+   *
+   * If a second argument is provided, it is used as the string tag for
+   * the resulting iterator.
+   *
+   * The resulting function also takes a second argument, which will be
+   * 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.
+   *
+   * ※ The returned function is an ordinary nonconstructible (arrow)
+   * function which, when called with a string, returns an iterator.
+   *
+   * ※ The prototypes of iterators returned by a single iterator
+   * function will all be the same.
+   */
+  stringIteratorFunction,
+} = (() => {
+  const { [ITERATOR]: arrayIterator } = Array.prototype;
+  const arrayIteratorPrototype = getPrototype(
+    [][ITERATOR](),
+  );
+  const { next: arrayIteratorNext } = arrayIteratorPrototype;
+  const { [ITERATOR]: stringIterator } = String.prototype;
+  const stringIteratorPrototype = getPrototype(
+    ""[ITERATOR](),
+  );
+  const { next: stringIteratorNext } = stringIteratorPrototype;
+  const { [ITERATOR]: mapIterator } = Map.prototype;
+  const mapIteratorPrototype = getPrototype(
+    new Map()[ITERATOR](),
+  );
+  const { next: mapIteratorNext } = mapIteratorPrototype;
+  const { [ITERATOR]: setIterator } = Set.prototype;
+  const setIteratorPrototype = getPrototype(
+    new Set()[ITERATOR](),
+  );
+  const { next: setIteratorNext } = setIteratorPrototype;
+  const generatorIteratorPrototype = getPrototype(
+    function* () {}.prototype,
+  );
+  const { next: generatorIteratorNext } = generatorIteratorPrototype;
+  const iteratorPrototype = getPrototype(
+    generatorIteratorPrototype,
+  );
+
+  /**
+   * 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.
+   *
+   * ※ This class extends the identity function to allow for arbitrary
+   * construction of its superclass instance based on the provided
+   * prototype.
+   *
+   * ※ This class is not exposed.
+   */
+  const Iterator = class extends identity {
+    #baseIterator;
+    #baseIteratorNext;
+    #generateNext;
+    #nextIterator = null;
+
+    /**
+     * Constructs a new iterator with the provided prototype which
+     * wraps the provided base iterator (advance·able using the
+     * provided next method) and yields the result of calling the
+     * provided generator function with each value (or just yields each
+     * value if no generator function is provided).
+     *
+     * ☡ 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
+     * when attempting to yield the first value.
+     */
+    constructor(
+      prototype,
+      baseIterator,
+      baseIteratorNext,
+      generateNext = null,
+    ) {
+      super(objectCreate(prototype));
+      this.#baseIterator = baseIterator;
+      this.#baseIteratorNext = baseIteratorNext;
+      this.#generateNext = generateNext;
+    }
+
+    /**
+     * Returns an object conforming to the requirements of the iterator
+     * protocol, yielding successive iterator values.
+     */
+    next() {
+      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.
+        //
+        // To avoid potentially large amounts of recursive calls, it is
+        // defined in a loop which will exit the first time a suitable
+        // response is acquired.
+        const baseIterator = this.#baseIterator;
+        const nextIterator = this.#nextIterator;
+        if (baseIterator === null) {
+          // The base iterator has already been exhausted.
+          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
+          // exist or because its values have been exhausted.
+          //
+          // Get the next value in the base iterator and either provide
+          // it to the generator function or yield it, as appropriate.
+          const {
+            value,
+            done,
+          } = call(baseIteratorNext, baseIterator, []);
+          if (done) {
+            // The base iterator is exhausted.
+            //
+            // Free it up from memory and rerun these steps to signal
+            // that the iteration has completed.
+            this.#baseIterator = null;
+            this.#generateNext = null;
+            continue;
+          } else if (generateNext !== null) {
+            // The base iterator has yielded another value and there is
+            // a generator function.
+            //
+            // Initialize a new iterator of result values by calling
+            // the function with the current value, and then rerun
+            // these steps to actually yield a value from it.
+            this.#nextIterator = generateNext(value);
+            continue;
+          } else {
+            // The base iterator has yielded another value and there is
+            // no generator function.
+            //
+            // Just yield the value.
+            return { value, done: false };
+          }
+        } else {
+          // This iterator is currently yielding values from the
+          // provided generator function.
+          const {
+            value,
+            done,
+          } = call(generatorIteratorNext, nextIterator, []);
+          if (done) {
+            // The current iterator of values from the provided
+            // generator function is exhausted.
+            //
+            // Remove it, and rerun these steps to advance to the next
+            // value in the base iterator.
+            this.#nextIterator = null;
+            continue;
+          } else {
+            // The current iterator of values has yielded another
+            // value; reyield it.
+            return { value, done: false };
+          }
+        }
+      }
+    }
+  };
+
+  const {
+    next: iteratorNext,
+  } = Iterator.prototype;
+  const makePrototype = (stringTag) =>
+    objectCreate(
+      iteratorPrototype,
+      {
+        next: {
+          configurable: true,
+          enumerable: false,
+          value: function next() {
+            return call(iteratorNext, this, []);
+          },
+          writable: true,
+        },
+        [TO_STRING_TAG]: {
+          configurable: true,
+          enumerable: false,
+          value: stringTag,
+          writable: false,
+        },
+      },
+    );
+
+  /**
+   * Returns a new function capable of generating iterators using the
+   * provided iterator generation method, iterator advancement method,
+   * optional generator function, and optional string tag.
+   *
+   * ※ The first two arguments to this function are bound to generate
+   * the actual exported iterator function makers.
+   *
+   * ※ This function is not exposed.
+   */
+  const iteratorFunction = (
+    makeBaseIterator,
+    baseIteratorNext,
+    generateNext = null,
+    stringTag = "Iterator",
+  ) => {
+    const prototype = makePrototype(stringTag); // intentionally cached
+    return ($, thisArg = undefined) =>
+      new Iterator(
+        prototype,
+        call(makeBaseIterator, $, []),
+        baseIteratorNext,
+        generateNext === null ? null : bind(generateNext, thisArg, []),
+      );
+  };
+
+  return {
+    arrayIteratorFunction: defineOwnProperty(
+      bind(
+        iteratorFunction,
+        undefined,
+        [arrayIterator, arrayIteratorNext],
+      ),
+      "name",
+      { value: "arrayIteratorFunction" },
+    ),
+    generatorIteratorFunction: defineOwnProperty(
+      bind(
+        iteratorFunction,
+        undefined,
+        [
+          function () {
+            return this();
+          },
+          generatorIteratorNext,
+        ],
+      ),
+      "name",
+      { value: "generatorIteratorFunction" },
+    ),
+    mapIteratorFunction: defineOwnProperty(
+      bind(
+        iteratorFunction,
+        undefined,
+        [mapIterator, mapIteratorNext],
+      ),
+      "name",
+      { value: "mapIteratorFunction" },
+    ),
+    setIteratorFunction: defineOwnProperty(
+      bind(
+        iteratorFunction,
+        undefined,
+        [setIterator, setIteratorNext],
+      ),
+      "name",
+      { value: "setIteratorFunction" },
+    ),
+    stringIteratorFunction: defineOwnProperty(
+      bind(
+        iteratorFunction,
+        undefined,
+        [stringIterator, stringIteratorNext],
+      ),
+      "name",
+      { value: "stringIteratorFunction" },
+    ),
+  };
+})();
diff --git a/iterable.test.js b/iterable.test.js
new file mode 100644 (file)
index 0000000..2e62d5c
--- /dev/null
@@ -0,0 +1,530 @@
+// ♓🌟 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 <https://mozilla.org/MPL/2.0/>.
+
+import {
+  assertEquals,
+  assertStrictEquals,
+  assertThrows,
+  describe,
+  it,
+} from "./dev-deps.js";
+import {
+  arrayIteratorFunction,
+  generatorIteratorFunction,
+  mapIteratorFunction,
+  setIteratorFunction,
+  stringIteratorFunction,
+} from "./iterable.js";
+
+describe("arrayIteratorFunction", () => {
+  it("[[Call]] returns a function", () => {
+    assertStrictEquals(typeof arrayIteratorFunction(), "function");
+  });
+
+  it("[[Call]] returns a value which has a prototype of %FunctionPrototype%", () => {
+    assertStrictEquals(
+      Object.getPrototypeOf(arrayIteratorFunction()),
+      Function.prototype,
+    );
+  });
+
+  describe("()", () => {
+    it("[[Call]] returns a value which inherits from %IteratorPrototype%", () => {
+      const iteratorProto = Object.getPrototypeOf(
+        Object.getPrototypeOf([][Symbol.iterator]),
+      );
+      const iterator = arrayIteratorFunction();
+      assertStrictEquals(
+        iterator([]) instanceof Object.assign(
+          function () {},
+          { prototype: iteratorProto },
+        ),
+        true,
+      );
+    });
+
+    it("[[Call]] returns a value with the provided string tag", () => {
+      const iterator = arrayIteratorFunction(null, "My Iterator");
+      assertStrictEquals(
+        iterator([])[Symbol.toStringTag],
+        "My Iterator",
+      );
+    });
+
+    it("[[Call]] yields the values", () => {
+      const iterator = arrayIteratorFunction();
+      assertEquals(
+        [...iterator(["etaoin", "shrdlu"])],
+        ["etaoin", "shrdlu"],
+      );
+    });
+
+    it("[[Call]] maps the values", () => {
+      const iterator = arrayIteratorFunction(function* ($) {
+        yield $.toUpperCase();
+      });
+      assertEquals(
+        [...iterator(["etaoin", "shrdlu"])],
+        ["ETAOIN", "SHRDLU"],
+      );
+    });
+
+    it("[[Call]] can map to nothing", () => {
+      const iterator = arrayIteratorFunction(function* () {});
+      assertEquals(
+        [...iterator(["etaoin", "shrdlu"])],
+        [],
+      );
+    });
+
+    it("[[Call]] can map to multiple values", () => {
+      const iterator = arrayIteratorFunction(function* ($) {
+        yield* $;
+      });
+      assertEquals(
+        [...iterator(["etaoin", "shrdlu"])],
+        [..."etaoinshrdlu"],
+      );
+    });
+
+    it("[[Call]] throws if not provided with any arguments", () => {
+      const iterator = arrayIteratorFunction();
+      assertThrows(() => {
+        iterator();
+      });
+    });
+
+    it("[[Call]] throws if not provided an arraylike", () => {
+      const iterator = arrayIteratorFunction();
+      assertThrows(() => {
+        iterator(null);
+      });
+    });
+
+    describe("::next", () => {
+      it("[[Call]] throws if there are values and the mapper is not a generator function", () => {
+        const iterator = arrayIteratorFunction(function () {});
+        assertThrows(() => {
+          iterator(["etaoin"]).next();
+        });
+      });
+    });
+  });
+});
+
+describe("generatorIteratorFunction", () => {
+  it("[[Call]] returns a function", () => {
+    assertStrictEquals(typeof generatorIteratorFunction(), "function");
+  });
+
+  it("[[Call]] returns a value which has a prototype of %FunctionPrototype%", () => {
+    assertStrictEquals(
+      Object.getPrototypeOf(generatorIteratorFunction()),
+      Function.prototype,
+    );
+  });
+
+  describe("()", () => {
+    it("[[Call]] returns a value which inherits from %IteratorPrototype%", () => {
+      const iteratorProto = Object.getPrototypeOf(
+        Object.getPrototypeOf([][Symbol.iterator]),
+      );
+      const iterator = generatorIteratorFunction();
+      assertStrictEquals(
+        iterator(function* () {}) instanceof Object.assign(
+          function () {},
+          { prototype: iteratorProto },
+        ),
+        true,
+      );
+    });
+
+    it("[[Call]] returns a value with the provided string tag", () => {
+      const iterator = generatorIteratorFunction(null, "My Iterator");
+      assertStrictEquals(
+        iterator(function* () {})[Symbol.toStringTag],
+        "My Iterator",
+      );
+    });
+
+    it("[[Call]] yields the values", () => {
+      const generator = function* () {
+        yield* ["etaoin", "shrdlu"];
+      };
+      const iterator = generatorIteratorFunction();
+      assertEquals(
+        [...iterator(generator)],
+        ["etaoin", "shrdlu"],
+      );
+    });
+
+    it("[[Call]] maps the values", () => {
+      const generator = function* () {
+        yield* ["etaoin", "shrdlu"];
+      };
+      const iterator = generatorIteratorFunction(function* ($) {
+        yield $.toUpperCase();
+      });
+      assertEquals(
+        [...iterator(generator)],
+        ["ETAOIN", "SHRDLU"],
+      );
+    });
+
+    it("[[Call]] can map to nothing", () => {
+      const generator = function* () {
+        yield* ["etaoin", "shrdlu"];
+      };
+      const iterator = generatorIteratorFunction(function* () {});
+      assertEquals(
+        [...iterator(generator)],
+        [],
+      );
+    });
+
+    it("[[Call]] can map to multiple values", () => {
+      const generator = function* () {
+        yield* ["etaoin", "shrdlu"];
+      };
+      const iterator = generatorIteratorFunction(function* ($) {
+        yield* $;
+      });
+      assertEquals(
+        [...iterator(generator)],
+        [..."etaoinshrdlu"],
+      );
+    });
+
+    it("[[Call]] throws if not provided with any arguments", () => {
+      const iterator = generatorIteratorFunction();
+      assertThrows(() => {
+        iterator();
+      });
+    });
+
+    it("[[Call]] throws if not provided a function", () => {
+      const iterator = generatorIteratorFunction();
+      assertThrows(() => {
+        iterator([]);
+      });
+    });
+
+    describe("::next", () => {
+      it("[[Call]] throws if there are values and the mapper is not a generator function", () => {
+        const generator = function* () {
+          yield "etaoin";
+        };
+        const iterator = generatorIteratorFunction(function () {});
+        assertThrows(() => {
+          iterator(generator).next();
+        });
+      });
+
+      it("[[Call]] throws if not constructed with a generator function", () => {
+        const iterator = generatorIteratorFunction();
+        assertThrows(() => {
+          iterator(Array.prototype[Symbol.iterator].bind([])).next();
+        });
+      });
+    });
+  });
+});
+
+describe("mapIteratorFunction", () => {
+  it("[[Call]] returns a function", () => {
+    assertStrictEquals(typeof mapIteratorFunction(), "function");
+  });
+
+  it("[[Call]] returns a value which has a prototype of %FunctionPrototype%", () => {
+    assertStrictEquals(
+      Object.getPrototypeOf(mapIteratorFunction()),
+      Function.prototype,
+    );
+  });
+
+  describe("()", () => {
+    it("[[Call]] returns a value which inherits from %IteratorPrototype%", () => {
+      const iteratorProto = Object.getPrototypeOf(
+        Object.getPrototypeOf([][Symbol.iterator]),
+      );
+      const iterator = mapIteratorFunction();
+      assertStrictEquals(
+        iterator(new Map()) instanceof Object.assign(
+          function () {},
+          { prototype: iteratorProto },
+        ),
+        true,
+      );
+    });
+
+    it("[[Call]] returns a value with the provided string tag", () => {
+      const iterator = mapIteratorFunction(null, "My Iterator");
+      assertStrictEquals(
+        iterator(new Map())[Symbol.toStringTag],
+        "My Iterator",
+      );
+    });
+
+    it("[[Call]] yields the values", () => {
+      const iterator = mapIteratorFunction();
+      assertEquals(
+        [...iterator(new Map([["etaoin", "shrdlu"]]))],
+        [["etaoin", "shrdlu"]],
+      );
+    });
+
+    it("[[Call]] maps the values", () => {
+      const iterator = mapIteratorFunction(function* ([k, v]) {
+        yield [k.toUpperCase(), v.toUpperCase()];
+      });
+      assertEquals(
+        [...iterator(new Map([["etaoin", "shrdlu"]]))],
+        [["ETAOIN", "SHRDLU"]],
+      );
+    });
+
+    it("[[Call]] can map to nothing", () => {
+      const iterator = mapIteratorFunction(function* () {});
+      assertEquals(
+        [...iterator(new Map([["etaoin", "shrdlu"]]))],
+        [],
+      );
+    });
+
+    it("[[Call]] can map to multiple values", () => {
+      const iterator = mapIteratorFunction(function* ($) {
+        yield* $;
+      });
+      assertEquals(
+        [...iterator(new Map([["etaoin", "shrdlu"]]))],
+        ["etaoin", "shrdlu"],
+      );
+    });
+
+    it("[[Call]] throws if not provided with any arguments", () => {
+      const iterator = mapIteratorFunction();
+      assertThrows(() => {
+        iterator();
+      });
+    });
+
+    it("[[Call]] throws if not provided a map", () => {
+      const iterator = mapIteratorFunction();
+      assertThrows(() => {
+        iterator([]);
+      });
+    });
+
+    describe("::next", () => {
+      it("[[Call]] throws if there are values and the mapper is not a generator function", () => {
+        const iterator = mapIteratorFunction(function () {});
+        assertThrows(() => {
+          iterator(new Map([["etaoin", "shrdlu"]])).next();
+        });
+      });
+    });
+  });
+});
+
+describe("setIteratorFunction", () => {
+  it("[[Call]] returns a function", () => {
+    assertStrictEquals(typeof setIteratorFunction(), "function");
+  });
+
+  it("[[Call]] returns a value which has a prototype of %FunctionPrototype%", () => {
+    assertStrictEquals(
+      Object.getPrototypeOf(setIteratorFunction()),
+      Function.prototype,
+    );
+  });
+
+  describe("()", () => {
+    it("[[Call]] returns a value which inherits from %IteratorPrototype%", () => {
+      const iteratorProto = Object.getPrototypeOf(
+        Object.getPrototypeOf([][Symbol.iterator]),
+      );
+      const iterator = setIteratorFunction();
+      assertStrictEquals(
+        iterator(new Set()) instanceof Object.assign(
+          function () {},
+          { prototype: iteratorProto },
+        ),
+        true,
+      );
+    });
+
+    it("[[Call]] returns a value with the provided string tag", () => {
+      const iterator = setIteratorFunction(null, "My Iterator");
+      assertStrictEquals(
+        iterator(new Set())[Symbol.toStringTag],
+        "My Iterator",
+      );
+    });
+
+    it("[[Call]] yields the values", () => {
+      const iterator = setIteratorFunction();
+      assertEquals(
+        [...iterator(new Set(["etaoin", "shrdlu"]))],
+        ["etaoin", "shrdlu"],
+      );
+    });
+
+    it("[[Call]] maps the values", () => {
+      const iterator = setIteratorFunction(function* ($) {
+        yield $.toUpperCase();
+      });
+      assertEquals(
+        [...iterator(new Set(["etaoin", "shrdlu"]))],
+        ["ETAOIN", "SHRDLU"],
+      );
+    });
+
+    it("[[Call]] can map to nothing", () => {
+      const iterator = setIteratorFunction(function* () {});
+      assertEquals(
+        [...iterator(new Set(["etaoin", "shrdlu"]))],
+        [],
+      );
+    });
+
+    it("[[Call]] can map to multiple values", () => {
+      const iterator = setIteratorFunction(function* ($) {
+        yield* $;
+      });
+      assertEquals(
+        [...iterator(new Set(["etaoin", "shrdlu"]))],
+        [..."etaoinshrdlu"],
+      );
+    });
+
+    it("[[Call]] throws if not provided with any arguments", () => {
+      const iterator = setIteratorFunction();
+      assertThrows(() => {
+        iterator();
+      });
+    });
+
+    it("[[Call]] throws if not provided a set", () => {
+      const iterator = setIteratorFunction();
+      assertThrows(() => {
+        iterator([]);
+      });
+    });
+
+    describe("::next", () => {
+      it("[[Call]] throws if there are values and the mapper is not a generator function", () => {
+        const iterator = setIteratorFunction(function () {});
+        assertThrows(() => {
+          iterator(new Set(["etaoin"])).next();
+        });
+      });
+    });
+  });
+});
+
+describe("stringIteratorFunction", () => {
+  it("[[Call]] returns a function", () => {
+    assertStrictEquals(typeof stringIteratorFunction(), "function");
+  });
+
+  it("[[Call]] returns a value which has a prototype of %FunctionPrototype%", () => {
+    assertStrictEquals(
+      Object.getPrototypeOf(stringIteratorFunction()),
+      Function.prototype,
+    );
+  });
+
+  describe("()", () => {
+    it("[[Call]] returns a value which inherits from %IteratorPrototype%", () => {
+      const iteratorProto = Object.getPrototypeOf(
+        Object.getPrototypeOf([][Symbol.iterator]),
+      );
+      const iterator = stringIteratorFunction();
+      assertStrictEquals(
+        iterator("") instanceof Object.assign(
+          function () {},
+          { prototype: iteratorProto },
+        ),
+        true,
+      );
+    });
+
+    it("[[Call]] returns a value with the provided string tag", () => {
+      const iterator = stringIteratorFunction(null, "My Iterator");
+      assertStrictEquals(
+        iterator("")[Symbol.toStringTag],
+        "My Iterator",
+      );
+    });
+
+    it("[[Call]] yields the values", () => {
+      const iterator = stringIteratorFunction();
+      assertEquals(
+        [...iterator("etaoin👀")],
+        [..."etaoin👀"],
+      );
+    });
+
+    it("[[Call]] maps the values", () => {
+      const iterator = stringIteratorFunction(function* ($) {
+        yield $.toUpperCase();
+      });
+      assertEquals(
+        [...iterator("etaoin👀")],
+        [..."ETAOIN👀"],
+      );
+    });
+
+    it("[[Call]] can map to nothing", () => {
+      const iterator = stringIteratorFunction(function* () {});
+      assertEquals(
+        [...iterator("etaoin👀")],
+        [],
+      );
+    });
+
+    it("[[Call]] can map to multiple values", () => {
+      const iterator = stringIteratorFunction(function* ($) {
+        yield $;
+        yield $;
+      });
+      assertEquals(
+        [...iterator("etaoin👀")],
+        [..."eettaaooiinn👀👀"],
+      );
+    });
+
+    it("[[Call]] throws if not provided with any arguments", () => {
+      const iterator = stringIteratorFunction();
+      assertThrows(() => {
+        iterator();
+      });
+    });
+
+    it("[[Call]] throws if not provided something convertible to a string", () => {
+      const iterator = stringIteratorFunction();
+      assertThrows(() => {
+        iterator({
+          toString() {
+            throw null;
+          },
+        });
+      });
+    });
+
+    describe("::next", () => {
+      it("[[Call]] throws if there are values and the mapper is not a generator function", () => {
+        const iterator = stringIteratorFunction(function () {});
+        assertThrows(() => {
+          iterator("etaoin").next();
+        });
+      });
+    });
+  });
+});
diff --git a/mod.js b/mod.js
index 13840c95b1fb48e5c25085130d26d682e26f607b..41bce2763d43e5de10d920b9c5f5db198e2c46ff 100644 (file)
--- a/mod.js
+++ b/mod.js
@@ -1,7 +1,7 @@
 // ♓🌟 Piscēs ∷ mod.js
 // ====================================================================
 //
-// Copyright © 2020–2022 Lady [@ Lady’s Computer].
+// 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
@@ -10,6 +10,7 @@
 export * from "./binary.js";
 export * from "./collection.js";
 export * from "./function.js";
+export * from "./iterable.js";
 export * from "./iri.js";
 export * from "./numeric.js";
 export * from "./object.js";
index e697a1fcad38f51cfba5e0fe8ae6d2dc3d34d7e1..4eeb14af420d2ac579d6d94dbf56a07741016ee1 100644 (file)
--- a/string.js
+++ b/string.js
@@ -8,6 +8,10 @@
 // file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
 
 import { bind, call, identity, makeCallable } from "./function.js";
+import {
+  arrayIteratorFunction,
+  stringIteratorFunction,
+} from "./iterable.js";
 import {
   defineOwnProperties,
   getOwnPropertyDescriptors,
@@ -15,7 +19,7 @@ import {
   objectCreate,
   setPrototype,
 } from "./object.js";
-import { ITERATOR, TO_STRING_TAG } from "./value.js";
+import { ITERATOR } from "./value.js";
 
 const RE = RegExp;
 const { prototype: rePrototype } = RE;
@@ -310,7 +314,7 @@ export const {
    * representation of the provided value.
    *
    * Codepoints which are not valid Unicode scalar values are replaced
-   * with U+FFFF.
+   * with U+FFFD.
    */
   scalarValues,
 
@@ -321,122 +325,38 @@ export const {
    */
   scalarValueString,
 } = (() => {
-  const { [ITERATOR]: arrayIterator } = arrayPrototype;
-  const arrayIteratorPrototype = Object.getPrototypeOf(
-    [][ITERATOR](),
+  const generateCodeUnits = function* (ucsCharacter) {
+    yield getCodeUnit(ucsCharacter, 0);
+  };
+  const generateCodepoints = function* (character) {
+    const { allowSurrogates } = this;
+    const codepoint = getCodepoint(character, 0);
+    yield allowSurrogates || codepoint <= 0xD7FF || codepoint >= 0xE000
+      ? codepoint
+      : 0xFFFD;
+  };
+
+  const codeUnitsIterator = arrayIteratorFunction(
+    generateCodeUnits,
+    "String Code Unit Iterator",
   );
-  const { next: arrayIteratorNext } = arrayIteratorPrototype;
-  const iteratorPrototype = Object.getPrototypeOf(
-    arrayIteratorPrototype,
+  const codepointsIterator = stringIteratorFunction(
+    bind(generateCodepoints, { allowSurrogates: true }, []),
+    "String Codepoint Iterator",
   );
-  const { [ITERATOR]: stringIterator } = stringPrototype;
-  const stringIteratorPrototype = Object.getPrototypeOf(
-    ""[ITERATOR](),
+  const scalarValuesIterator = stringIteratorFunction(
+    bind(generateCodepoints, { allowSurrogates: false }, []),
+    "String Scalar Value Iterator",
   );
-  const { next: stringIteratorNext } = stringIteratorPrototype;
-
-  /**
-   * An iterator object for iterating over code values (either code
-   * units or codepoints) in a string.
-   *
-   * ※ This class is not exposed, although its methods are (through
-   * the prototypes of string code value iterator objects).
-   */
-  const StringCodeValueIterator = class extends identity {
-    #allowSurrogates;
-    #baseIterator;
-
-    /**
-     * Constructs a new string code value iterator from the provided
-     * base iterator.
-     *
-     * If the provided base iterator is an array iterator, this is a
-     * code unit iterator.  If the provided iterator is a string
-     * iterator and surrogates are allowed, this is a codepoint
-     * iterator. If the provided iterator is a string iterator and
-     * surrogates are not allowed, this is a scalar value iterator.
-     */
-    constructor(baseIterator, allowSurrogates = true) {
-      super(objectCreate(stringCodeValueIteratorPrototype));
-      this.#allowSurrogates = !!allowSurrogates;
-      this.#baseIterator = baseIterator;
-    }
-
-    /** Provides the next code value in the iterator. */
-    next() {
-      const baseIterator = this.#baseIterator;
-      switch (getPrototype(baseIterator)) {
-        case arrayIteratorPrototype: {
-          // The base iterator is iterating over U·C·S characters.
-          const {
-            value: ucsCharacter,
-            done,
-          } = call(arrayIteratorNext, baseIterator, []);
-          return done
-            ? { value: undefined, done: true }
-            : { value: getCodeUnit(ucsCharacter, 0), done: false };
-        }
-        case stringIteratorPrototype: {
-          // The base iterator is iterating over Unicode characters.
-          const {
-            value: character,
-            done,
-          } = call(stringIteratorNext, baseIterator, []);
-          if (done) {
-            // The base iterator has been exhausted.
-            return { value: undefined, done: true };
-          } else {
-            // The base iterator provided a character; yield the
-            // codepoint.
-            const codepoint = getCodepoint(character, 0);
-            return {
-              value: this.#allowSurrogates || codepoint <= 0xD7FF ||
-                  codepoint >= 0xE000
-                ? codepoint
-                : 0xFFFD,
-              done: false,
-            };
-          }
-        }
-        default: {
-          // Should not be possible!
-          throw new TypeError(
-            "Piscēs: Unrecognized base iterator type in %StringCodeValueIterator%.",
-          );
-        }
-      }
-    }
-  };
-
   const {
-    next: stringCodeValueIteratorNext,
-  } = StringCodeValueIterator.prototype;
-  const stringCodeValueIteratorPrototype = objectCreate(
-    iteratorPrototype,
-    {
-      next: {
-        configurable: true,
-        enumerable: false,
-        value: stringCodeValueIteratorNext,
-        writable: true,
-      },
-      [TO_STRING_TAG]: {
-        configurable: true,
-        enumerable: false,
-        value: "String Code Value Iterator",
-        writable: false,
-      },
-    },
-  );
+    next: scalarValuesNext,
+  } = getPrototype(scalarValuesIterator(""));
   const scalarValueIterablePrototype = {
     [ITERATOR]() {
       return {
         next: bind(
-          stringCodeValueIteratorNext,
-          new StringCodeValueIterator(
-            call(stringIterator, this.source, []),
-            false,
-          ),
+          scalarValuesNext,
+          scalarValuesIterator(this.source),
           [],
         ),
       };
@@ -444,18 +364,9 @@ export const {
   };
 
   return {
-    codeUnits: ($) =>
-      new StringCodeValueIterator(call(arrayIterator, `${$}`, [])),
-    codepoints: ($) =>
-      new StringCodeValueIterator(
-        call(stringIterator, `${$}`, []),
-        true,
-      ),
-    scalarValues: ($) =>
-      new StringCodeValueIterator(
-        call(stringIterator, `${$}`, []),
-        false,
-      ),
+    codeUnits: ($) => codeUnitsIterator(`${$}`),
+    codepoints: ($) => codepointsIterator(`${$}`),
+    scalarValues: ($) => scalarValuesIterator(`${$}`),
     scalarValueString: ($) =>
       stringFromCodepoints(...objectCreate(
         scalarValueIterablePrototype,
index e3d10968b0bb3bd401a2e564846a32b9314d4b60..066770b39471f5f58f1822292744b6627a27521b 100644 (file)
@@ -294,10 +294,10 @@ describe("codeUnits", () => {
     assertStrictEquals(typeof codeUnits("").next, "function");
   });
 
-  it("[[Call]] returns a string code value iterator", () => {
+  it("[[Call]] returns a string code unit iterator", () => {
     assertStrictEquals(
       codeUnits("")[Symbol.toStringTag],
-      "String Code Value Iterator",
+      "String Code Unit Iterator",
     );
   });
 
@@ -332,10 +332,10 @@ describe("codepoints", () => {
     assertStrictEquals(typeof codepoints("").next, "function");
   });
 
-  it("[[Call]] returns a string code value iterator", () => {
+  it("[[Call]] returns a string codepoint iterator", () => {
     assertStrictEquals(
       codepoints("")[Symbol.toStringTag],
-      "String Code Value Iterator",
+      "String Codepoint Iterator",
     );
   });
 
@@ -409,10 +409,10 @@ describe("scalarValues", () => {
     assertStrictEquals(typeof scalarValues("").next, "function");
   });
 
-  it("[[Call]] returns a string code value iterator", () => {
+  it("[[Call]] returns a string scalar value iterator", () => {
     assertStrictEquals(
       scalarValues("")[Symbol.toStringTag],
-      "String Code Value Iterator",
+      "String Scalar Value Iterator",
     );
   });
 
This page took 0.047707 seconds and 4 git commands to generate.