summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEuAndreh <eu@euandre.org>2025-06-01 13:12:58 -0300
committerEuAndreh <eu@euandre.org>2025-06-01 13:16:50 -0300
commit3fe8fa31541308cbfdfcf4f861d68f46b5ecc4f8 (patch)
tree03c86fe79481a9a52f83b284d5f6c9d7df64d37f
parentInitial empty commit (diff)
downloadsjs-3fe8fa31541308cbfdfcf4f861d68f46b5ecc4f8.tar.gz
sjs-3fe8fa31541308cbfdfcf4f861d68f46b5ecc4f8.tar.xz
Import existing code as-is
-rw-r--r--Makefile97
-rw-r--r--deps.mk0
-rwxr-xr-xmkdeps.sh4
-rw-r--r--src/sjs.js304
-rw-r--r--tests/sjs.js607
5 files changed, 1012 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..c89a0a8
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,97 @@
+.POSIX:
+DATE = 1970-01-01
+VERSION = 0.1.0
+NAME = sjs
+NAME_UC = $(NAME)
+## Installation prefix. Defaults to "/usr".
+PREFIX = /usr
+BINDIR = $(PREFIX)/bin
+LIBDIR = $(PREFIX)/lib
+JSLIBDIR = $(LIBDIR)/javascript
+INCLUDEDIR = $(PREFIX)/include
+SRCDIR = $(PREFIX)/src/$(NAME)
+SHAREDIR = $(PREFIX)/share
+LOCALEDIR = $(SHAREDIR)/locale
+MANDIR = $(SHAREDIR)/man
+EXEC = ./
+## Where to store the installation. Empty by default.
+DESTDIR =
+LDLIBS =
+JSIMPL = node
+
+
+
+.SUFFIXES:
+.SUFFIXES: .js .js-check
+
+
+
+all:
+include deps.mk
+
+sources = \
+ src/$(NAME).js \
+
+
+derived-assets = \
+
+side-assets = \
+
+
+
+## Default target. Builds all artifacts required for testing
+## and installation.
+all: $(derived-assets)
+
+
+
+tests/$(NAME).js-check:
+ $(JSIMPL) $*.js
+
+check-unit: tests/$(NAME).js-check
+
+
+integration-tests = \
+
+.PRECIOUS: $(integration-tests)
+$(integration-tests): ALWAYS
+ sh $@
+
+check-integration: $(integration-tests)
+
+
+## Run all tests. Each test suite is isolated, so that a parallel
+## build can run tests at the same time. The required artifacts
+## are created if missing.
+check: check-unit check-integration
+
+
+
+## Remove *all* derived artifacts produced during the build.
+## A dedicated test asserts that this is always true.
+clean:
+ rm -rf $(derived-assets) $(side-assets)
+
+
+## Installs into $(DESTDIR)$(PREFIX). Its dependency target
+## ensures that all installable artifacts are crafted beforehand.
+install: all
+ mkdir -p \
+ '$(DESTDIR)$(BINDIR)' \
+ '$(DESTDIR)$(JSLIBDIR)' \
+ '$(DESTDIR)$(SRCDIR)' \
+
+ cp src/$(NAME).js '$(DESTDIR)$(JSLIBDIR)'
+ cp $(sources) '$(DESTDIR)$(SRCDIR)'
+
+## Uninstalls from $(DESTDIR)$(PREFIX). This is a perfect mirror
+## of the "install" target, and removes *all* that was installed.
+## A dedicated test asserts that this is always true.
+uninstall:
+ rm -rf \
+ '$(DESTDIR)$(JSLIBDIR)'/$(NAME).js \
+ '$(DESTDIR)$(SRCDIR)' \
+
+
+
+ALWAYS:
diff --git a/deps.mk b/deps.mk
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/deps.mk
diff --git a/mkdeps.sh b/mkdeps.sh
new file mode 100755
index 0000000..e5606ff
--- /dev/null
+++ b/mkdeps.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+set -eu
+
+export LANG=POSIX.UTF-8
diff --git a/src/sjs.js b/src/sjs.js
new file mode 100644
index 0000000..799f964
--- /dev/null
+++ b/src/sjs.js
@@ -0,0 +1,304 @@
+export const getIn = (obj, path) =>
+ path.length === 0
+ ? obj
+ : getIn(obj?.[path[0]], path.slice(1));
+
+export const merge = (lhs, rhs) => {
+ if (lhs === undefined) {
+ return rhs;
+ }
+
+ if (rhs === undefined) {
+ return lfs;
+ }
+
+ if (typeof lhs !== "object") {
+ return rhs;
+ }
+
+ const lkeys = Object.keys(lhs);
+ const rkeys = Object.keys(rhs);
+ const allKeys = lkeys.concat(rkeys);
+ return [...new Set(allKeys)].reduce(
+ (o, key) => ({ ...o, [key]: merge(lhs[key], rhs[key]) }),
+ {},
+ );
+};
+
+export const compareObject = (lhs, rhs) => {
+ const lstr = Object.entries(lhs).toSorted();
+ const rstr = Object.entries(rhs).toSorted();
+
+ if (lstr < rstr) {
+ return -1;
+ }
+
+ if (lstr > rstr) {
+ return 1;
+ }
+
+ throw new Error("unreachable");
+};
+
+const FROM = /[&<>'"]/g;
+
+const ESCAPES = {
+ '&': '&amp;',
+ '<': '&lt;',
+ '>': '&gt;',
+ "'": '&#39;',
+ '"': '&quot;'
+};
+
+const mappingFn = c => ESCAPES[c];
+
+export const escape = s =>
+ String.prototype.replace.call(s, FROM, mappingFn);
+
+export const eq = (a, b) => {
+ if (a === b) {
+ return true;
+ }
+
+ if (a === null || b === null) {
+ return false;
+ }
+
+ if (typeof a != "object" || typeof b != "object") {
+ return false;
+ }
+
+ if (Array.isArray(a) !== Array.isArray(b)) {
+ return false;
+ }
+
+ if (Object.keys(a).length !== Object.keys(b).length) {
+ return false;
+ }
+
+ for (const k in a) {
+ if (!b.hasOwnProperty(k)) {
+ return false;
+ }
+ if (!eq(a[k], b[k])) {
+ return false;
+ }
+ }
+ return true;
+};
+
+export const keys = (ks, obj) =>
+ ks.reduce(
+ (ret, k) =>
+ obj.hasOwnProperty(k) ? {...ret, [k]: obj[k]} : ret,
+ {},
+ );
+
+export const difference = (a, b) => {
+ const diff = new Set(a);
+ for (const el of b) {
+ diff.delete(el);
+ }
+ return diff;
+};
+
+export const assocIn = (obj, path, value) =>
+ path.length === 0 ? obj :
+ path.length === 1 ? { ...obj, [path[0]]: value } :
+ {
+ ...obj,
+ [path[0]]: assocIn(
+ (obj[path[0]] || {}),
+ path.slice(1),
+ value
+ )
+ };
+
+export const dissoc = (obj, key) => {
+ const ret = { ...obj };
+ delete ret[key]
+ return ret;
+};
+
+export const findFirst = (arr, fn) => {
+ for (const x of arr) {
+ const ret = fn(x);
+ if (ret) {
+ return ret;
+ }
+ }
+
+ return null;
+};
+
+export const partial = (fn, ...startArgs) =>
+ (...endArgs) =>
+ fn(...startArgs, ...endArgs);
+
+export const strSortFn = (a, b) => a.localeCompare(b, "POSIX");
+
+export const undefinedAsNull = x => x === undefined ? null : x;
+
+export const first = a => a[0];
+export const rest = a => a.slice(1);
+export const butlast = a => a.slice(a, a.length - 1);
+export const last = a => a[a.length - 1];
+
+export const take = function*(n, gen) {
+ let i = 0n;
+ for (const x of gen) {
+ if (i >= n) {
+ break;
+ }
+ i++;
+ yield x;
+ }
+};
+
+export const range = function*(x, y, step = 1n) {
+ if (x === undefined) {
+ let i = 0n;
+ while (true) {
+ yield i++;
+ }
+ }
+ const [from, to] = y === undefined ?
+ [0n, x] :
+ [x, y];
+ const fromn = BigInt(from);
+ const ton = BigInt(to);
+ const stepn = BigInt(step);
+ if (stepn === 0n) {
+ while (true) {
+ yield fromn;
+ }
+ }
+ if (step < 0n) {
+ for (let i = fromn; i > ton; i+= stepn) {
+ yield i;
+ }
+ } else {
+ for (let i = fromn; i < ton; i += stepn) {
+ yield i;
+ }
+ }
+};
+
+const t = ({ colors, err, assertEq }) => ({
+ assertEq,
+ tap: x => {
+ err(`tap: ${x}\n`);
+ return x;
+ },
+ start: msg => {
+ err(`${msg}:\n`);
+ },
+ testing: async (msg, fn) => {
+ err(`${colors.yellow("testing")}: ${msg}... `);
+ try {
+ await fn();
+ } catch (e) {
+ err(`${colors.red("ERR")}.\n`);
+ throw e;
+ }
+ err(`${colors.green("OK")}.\n`);
+ },
+});
+
+class UnimplementedError extends Error {}
+
+export const getJSImpl = () => {
+ if (typeof scriptArgs !== "undefined") {
+ return "qjs";
+ }
+ if (typeof Deno !== "undefined") {
+ return "deno";
+ }
+ if (typeof process !== "undefined") {
+ return "node";
+ }
+ // FIXME: add browser and bun
+ return "unknown";
+};
+
+export const JSImpl = getJSImpl();
+
+const mappings = {
+ ARGV: {
+ qjs: () => scriptArgs,
+ node: () => process.argv.slice(1),
+ deno: () => Deno.mainModule.substring("file://".length),
+ unknown: () => {
+ throw new UnimplementedError(`ARGV["${JSImpl}"]`);
+ },
+ },
+ exit: {
+ qjs: async () => {
+ const { exit } = await import("std");
+ return exit;
+ },
+ node: async () => process.exit,
+ deno: async () => Deno.exit,
+ unknown: () => {
+ throw new UnimplementedError(`exit["${JSImpl}"]`);
+ },
+ },
+ tconf: {
+ qjs: async () => await mappings.testConf.unknown(),
+ deno: async () => await mappings.testConf.node(),
+ browser: async () => {
+ class DOMAssertionError extends Error {}
+
+ const w = s => document.querySelector("#test-report").innerHTML += s;
+
+ const red = s => `<span style="color: red; ">${s}</span>`;
+ const green = s => `<span style="color: green; ">${s}</span>`;
+ const yellow = s => `<span style="color: darkgoldenrod;">${s}</span>`;
+ return {
+ testStart: msg => w(`${msg}:\n`),
+ testing: msg => w(`${yellow("testing")}: ${msg}...`),
+ testOK: () => w(` ${green("OK")}.\n`),
+ assert: (x, msg = "") => {
+ if (!x) {
+ w(` ${red("ERROR")}.\n`);
+ w("\nSee console for details.\n");
+ w("If possible, do report them.");
+ throw new DOMAssertionError(msg);
+ }
+ },
+ };
+ },
+
+ node: async () => {
+ const assert = await import("node:assert/strict");
+ const process = await import("node:process");
+
+ const err = x => process.stderr.write(x);
+ const red = s => `\x1b[31m${s}\x1b[0m`;
+ const green = s => `\x1b[32m${s}\x1b[0m`;
+ const yellow = s => `\x1b[33m${s}\x1b[0m`;
+
+ return {
+ err,
+ assertEq: assert.deepEqual,
+ colors: {
+ red,
+ green,
+ yellow,
+ },
+ };
+ },
+ unknown: () => { throw new UnimplementedError(`exit["${JSImpl}"]`); },
+ },
+};
+
+export const exit = await mappings.exit[ JSImpl]();
+export const ARGV = await mappings.ARGV[ JSImpl]();
+export const tconf = await mappings.tconf[JSImpl]();
+
+export const runTests = async tests => {
+ const tFinal = t(tconf);
+ for (const testFn of tests) {
+ await testFn(tFinal);
+ }
+};
diff --git a/tests/sjs.js b/tests/sjs.js
new file mode 100644
index 0000000..bf5f3dd
--- /dev/null
+++ b/tests/sjs.js
@@ -0,0 +1,607 @@
+import {
+ getIn,
+ merge,
+ compareObject,
+ escape,
+ eq,
+ keys,
+ difference,
+ assocIn,
+ dissoc,
+ findFirst,
+ partial,
+ strSortFn,
+ undefinedAsNull,
+ first,
+ rest,
+ butlast,
+ last,
+ take,
+ range,
+ runTests,
+} from "../src/sjs.js";
+
+
+
+const test_getIn = async t => {
+ t.start("getIn()");
+
+ await t.testing("empty values", () => {
+ t.assertEq(getIn({}, []), {});
+ t.assertEq(getIn({ k: "v" }, []), { k: "v" });
+ t.assertEq(getIn({}, [0]), undefined);
+ });
+
+ await t.testing("error when getting bad path", () => {
+ });
+
+ await t.testing("mix of arrays and objects", () => {
+ });
+
+ await t.testing("missing values", () => {
+ t.assertEq(getIn({}, ["a", "b", "c"]), undefined);
+ t.assertEq(getIn({ a: {}}, ["a", "b", "c"]), undefined);
+ t.assertEq(getIn({ a: { b: {}}}, ["a", "b", "c"]), undefined);
+ t.assertEq(getIn({ a: { b: {}, c: {}}}, ["a", "b", "c"]), undefined);
+ });
+
+ await t.testing("nested valeues", () => {
+ t.assertEq(getIn({ a: { b: { c: { d: "e" }}}}, ["a", "b", "c", "d"]), "e");
+ });
+};
+
+const test_merge = async t => {
+ t.start("merge()");
+
+ await t.testing("empty values", () => {
+ });
+
+ await t.testing("rhs gets preference over lhs", () => {
+ });
+
+ await t.testing("deep merge", () => {
+ });
+};
+
+const test_compareObject = async t => {
+ t.start("compareObject()");
+
+ await t.testing("", () => {});
+};
+
+const test_escape = async t => {
+ t.start("escape()");
+ await t.testing("", () => {});
+};
+
+const test_eq = t => {
+ t.start("eq()");
+ t.testing("scalar values equality", () => {
+ t.assertEq(eq(0, 0), true);
+ t.assertEq(eq(100, 100), true);
+ t.assertEq(eq(1.5, 1.5), true);
+ t.assertEq(eq(-9, -9), true);
+
+ t.assertEq(eq(0, 1), false);
+ t.assertEq(eq(100, 99), false);
+ t.assertEq(eq(1.5, 1.4), false);
+ t.assertEq(eq(-9, 9), false);
+
+
+ t.assertEq(eq(null, null), true);
+ t.assertEq(eq(undefined, undefined), true);
+ t.assertEq(eq("", ""), true);
+ t.assertEq(eq("a string", "a string"), true);
+
+ t.assertEq(eq(null, undefined), false);
+ t.assertEq(eq(undefined, 0), false);
+ t.assertEq(eq("", "0"), false);
+ t.assertEq(eq("a string", "another string"), false);
+
+
+ t.assertEq(eq(true, true), true);
+ t.assertEq(eq(false, false), true);
+
+ t.assertEq(eq(true, false), false);
+
+
+ t.assertEq(eq(1n, 1n), true);
+ t.assertEq(eq(99999999999999n, 99999999999999n), true);
+
+ // t.assertEq(eq(1, 1n), true);
+ });
+
+ t.testing("array equality", () => {
+ t.assertEq(eq([], []), true);
+
+
+ t.assertEq(eq([0, 1, 2], [0, 1, 2]), true);
+ t.assertEq(eq([0, 1, 2], new Array(0, 1, 2)), true);
+
+ t.assertEq(eq([0, 1, 2], [0, 1]), false);
+ t.assertEq(eq([0, 1], new Array(0, 1, 2)), false);
+
+
+ t.assertEq(eq([undefined], [undefined]), true);
+ t.assertEq(eq([null, 0, "", true], [null, 0, "", true]), true);
+
+
+ t.assertEq(eq([[[[0]]]], [[[[0]]]]), true);
+
+ t.assertEq(eq([[[[0]]]], [0]), false);
+ });
+
+ t.testing("object equality", () => {
+ t.assertEq(eq({}, {}), true);
+ t.assertEq(eq({ a: 1 }, { a: 1 }), true);
+
+ t.assertEq(eq({ a: 1, b: undefined }, { a: 1 }), false);
+
+
+ t.assertEq(eq(
+ { a: 1, b: { c: { d: "e" } } },
+ { a: 1, b: { c: { d: "e" } } },
+ ), true);
+
+ class C {}
+ // ...
+ });
+
+ t.testing("mixed values", () => {
+ t.assertEq(eq(
+ {a: ["", 1, 2, 3, [{ b: { c: [ "d", "e" ]}}]]},
+ {a: ["", 1, 2, 3, [{ b: { c: [ "d", "e" ]}}]]},
+ ), true);
+
+ t.assertEq(eq(null, {}), false);
+
+ t.assertEq(eq([], {}), false);
+
+
+ t.assertEq(eq(new Date(123), new Date(123)), true);
+ t.assertEq(eq({ d: new Date(123) }, { d: new Date(123) }), true);
+
+ // FIXME
+ // t.assertEq(!eq(new Date(123), new Date(321)));
+ // t.assertEq(!eq({ d: new Date(123) }, { d: new Date(321) }));
+ });
+};
+
+const test_keys = async t => {
+ t.start("keys()");
+ await t.testing("happy paths", () => {
+ t.assertEq(
+ { a: 1, b: 2 },
+ keys(["a", "b"], { a: 1, b: 2, c: 3 }),
+ );
+ });
+
+ await t.testing("stress scenarios", () => {
+ t.assertEq(
+ {},
+ keys([], {}),
+ "empty selection of empty object",
+ );
+
+ t.assertEq(
+ {},
+ keys([], {a: 1}),
+ "empty selection of non-empty object",
+ );
+
+ t.assertEq(
+ {},
+ keys(["a"], {}),
+ "non-empty selection of empty object",
+ );
+
+ t.assertEq(
+ { a: undefined, b: null },
+ keys(["a", "b", "c"], { a: undefined, b: null }),
+ "falsy values",
+ );
+ });
+};
+
+const test_difference = async t => {
+ t.start("difference()");
+
+ await t.testing("empty values", () => {
+ t.assertEq(
+ difference(new Set(), new Set()),
+ new Set(),
+ );
+
+ t.assertEq(
+ difference(new Set(), new Set([1, 2])),
+ new Set(),
+ );
+
+ t.assertEq(
+ difference(new Set([1, 2]), new Set()),
+ new Set([1, 2]),
+ );
+ });
+
+ await t.testing("different subsets", () => {
+ t.assertEq(
+ difference(new Set([1, 2]), new Set([3, 4])),
+ new Set([1, 2]),
+ );
+
+ t.assertEq(
+ difference(new Set([1, 2, 3]), new Set([2, 4, 5])),
+ new Set([1, 3]),
+ );
+
+ t.assertEq(
+ difference(new Set([1]), new Set([1, 2, 3, 4, 5])),
+ new Set(),
+ );
+
+ t.assertEq(
+ difference(new Set([1, 2, 3]), new Set([1, 2, 3])),
+ new Set(),
+ );
+ });
+};
+
+const test_assocIn = async t => {
+ t.start("assocIn()");
+
+ await t.testing("empty values", () => {
+ t.assertEq(assocIn({}, [], null), {});
+ t.assertEq(assocIn({ k: "v" }, [], null), { k: "v" });
+ });
+
+ await t.testing("adding values", () => {
+ t.assertEq(assocIn({}, ["k"], "v"), { k: "v" });
+ t.assertEq(assocIn({}, ["k1", "k2"], "v"), { k1: { k2: "v" }});
+ t.assertEq(assocIn({}, ["k1", "k2", "k3"], "v"), { k1: { k2: { k3: "v" }}});
+ t.assertEq(assocIn({ k: "v" }, ["k1", "k2"], "v"), { k: "v", k1: { k2: "v" }});
+ });
+
+ await t.testing("replacing values", () => {
+ t.assertEq(
+ assocIn(
+ { k1: { k2: { k3: "before" }}},
+ ["k1", "k2", "k3"],
+ "after"
+ ),
+ { k1: { k2: { k3: "after" }}}
+ );
+ });
+};
+
+const test_dissoc = async t => {
+ t.start("dissoc()");
+
+ await t.testing("empty values", () => {
+ t.assertEq(dissoc({}, "k"), {});
+ });
+
+ await t.testing("noop when key does not exist", () => {
+ t.assertEq(dissoc({ a: 1 }, "b"), { a: 1 });
+ });
+
+ await t.testing("removes the key", () => {
+ t.assertEq(dissoc({ a: 1, b: 2}, "b"), { a: 1 });
+ });
+};
+
+const test_findFirst = async t => {
+ t.start("findFirst()");
+
+ await t.testing("empty values", () => {
+ t.assertEq(findFirst([], () => {}), null);
+ });
+
+ await t.testing("when function doesn't transform, it behaves similarly to [].find()", () => {
+ const arr1 = [ 0, null, undefined, "", 1, 2 ];
+ t.assertEq(findFirst(arr1, x => x), 1);
+ t.assertEq(arr1.find(x => x), 1);
+
+ const arr2 = [ 0, null, undefined, "", false ];
+ t.assertEq(findFirst(arr2, x => x), null);
+ t.assertEq(arr2.find(x => x), undefined);
+ });
+
+ await t.testing("when it does transform, we return the transformed value", () => {
+ const arr = [ 1, 3, 5, 6 ];
+
+ t.assertEq(
+ findFirst(arr, x => x % 2 === 0 && "a brand new value"),
+ "a brand new value",
+ );
+ });
+};
+
+const test_partial = async t => {
+ t.start("partial()");
+
+ await t.testing("empty values", () => {
+ const adder = (a, b, c) => a + b + c;
+
+ const adder1 = partial(adder);
+ t.assertEq(adder1(1, 2, 3), 6);
+
+ const adder2 = partial(adder, 4, 5, 6);
+ t.assertEq(adder2(), 15);
+
+ const noargs = () => "static";
+ t.assertEq(partial(noargs)(), noargs());
+ });
+
+ await t.testing("too few arguments", () => {
+ const threeArgs = (a, b, c) => {
+ assert.notEqual(c, undefined);
+ return a + b + c;
+ };
+
+ const add1 = partial(threeArgs, 1);
+ return; // FIXME
+ assert.throws(
+ () => add1(2),
+ assert.AssertionError,
+ );
+
+ const add1And2 = partial(threeArgs, 1, 2);
+ assert.throws(
+ () => add1And2(),
+ assert.AssertionError,
+ );
+
+ const addNothing = partial(threeArgs);
+ assert.throws(
+ () => addNothing(),
+ assert.AssertionError,
+ );
+ });
+
+ await t.testing("too many arguments", () => {
+ const twoArgs = (a, b) => a + b;
+
+ t.assertEq(partial(twoArgs, 1)(2, 3), 3);
+ t.assertEq(partial(twoArgs, 1, 2)(3), 3);
+ });
+
+ await t.testing("daily usage", () => {
+ const twoArg = (a, b) => a + b;
+
+ const numbers = [ 1, 2, 3, 4, 5 ];
+ t.assertEq(
+ numbers.map(partial(twoArg, 2)),
+ [ 3, 4, 5, 6, 7 ],
+ );
+ });
+
+ await t.testing("nested partials", () => {
+ const threeArgs = (a, b, c) => a + b + c;
+
+ const add1 = partial(threeArgs, 1);
+ const add1And2 = partial(add1, 2);
+
+ t.assertEq(add1And2(3), 6);
+ });
+};
+
+const test_strSortFn = async t => {
+ t.start("strSortFn()");
+
+ await t.testing("empty value", () => {
+ t.assertEq(strSortFn("", ""), 0);
+ });
+
+ await t.testing("default sort", () => {
+ const arr = [ "a", "Z" ];
+ t.assertEq(
+ [...arr].sort(strSortFn),
+ [...arr].sort().reverse(),
+ );
+ });
+};
+
+const test_undefinedAsNull = async t => {
+ t.start("undefinedAsNull()");
+
+ await t.testing("null for undefined or null", () => {
+ t.assertEq(undefinedAsNull(undefined), null);
+ t.assertEq(undefinedAsNull(null), null);
+ });
+
+ await t.testing("identity otherwise", () => {
+ const expected = [
+ " ", "", 0, 1, -1.1, true, false,
+ [], [ "" ], {}, { k: "v" },
+ ];
+ const given = expected.map(undefinedAsNull);
+ t.assertEq(given, expected);
+ });
+};
+
+const test_first = async t => {
+ t.start("first()");
+
+ await t.testing("undefined for an empty array", () => {
+ t.assertEq(undefined, first([]));
+ t.assertEq(undefined, first(""));
+ });
+
+ await t.testing("the first element otherwise", () => {
+ t.assertEq(1, first([1, 2, 3]));
+ t.assertEq("a", first("abc"));
+ });
+};
+
+const test_rest = async t => {
+ t.start("rest()");
+
+ await t.testing("an empty array when no more elements are available", () => {
+ t.assertEq([], rest([]));
+ t.assertEq([], rest([1]));
+ t.assertEq("", rest(""));
+ t.assertEq("bc", rest("abc"));
+ });
+
+ await t.testing("the rest of the collection otherwise", () => {
+ t.assertEq([2, 3], rest([1, 2, 3]));
+ t.assertEq("bc", rest("abc"));
+ });
+
+ await t.testing("combines with first() well", () => {
+ const arr = ["anything", "can", "go", "here"];
+ t.assertEq(arr, [ first(arr), ...rest(arr) ]);
+ });
+};
+
+const test_butlast = async t => {
+ t.start("butlast()");
+
+ await t.testing("empty array when ther are no more elements", () => {
+ t.assertEq([], butlast([]));
+ t.assertEq([], butlast([1]));
+ t.assertEq("", butlast(""));
+ t.assertEq("", butlast("c"));
+ });
+
+ await t.testing("the beginning of the array otherwise", () => {
+ t.assertEq([1, 2], butlast([1, 2, 3]));
+ t.assertEq("ab", butlast("abc"));
+ });
+};
+
+const test_last = async t => {
+ t.start("last()");
+
+ await t.testing("undefined for an empty array", () => {
+ t.assertEq(undefined, last([]));
+ t.assertEq(undefined, last(""));
+ });
+
+ await t.testing("the last element otherwise", () => {
+ t.assertEq(3, last([1, 2, 3]));
+ t.assertEq("c", last("abc"));
+ });
+
+ await t.testing("combines with butlast() well", () => {
+ const arr = ["anything", "goes", "here", "too"];
+ t.assertEq(arr, [ ...butlast(arr), last(arr) ]);
+ });
+};
+
+const test_take = async t => {
+ t.start("take()");
+
+ await t.testing("example usage", () => {
+ t.assertEq(Array.from(take(3, [1, 2, 3, 4, 5, 6])), [1, 2, 3]);
+ t.assertEq([...take(3, [1, 2, 3, 4, 5, 6])], [1, 2, 3]);
+
+ const gen = function*() {
+ yield* [1, 2, 3, 4, 5, 6];
+ }
+ t.assertEq([...take(3, gen())], [1, 2, 3]);
+
+ t.assertEq([...take(3, [1, 2])], [1, 2]);
+
+ t.assertEq([...take(1, [])], []);
+ t.assertEq([...take(0, [1])], []);
+ t.assertEq([...take(-1, [1])], []);
+ });
+};
+
+const test_range = async t => {
+ t.start("range()");
+
+ await t.testing("empty values", () => {
+ const [] = range();
+
+ const [ a ] = range();
+ t.assertEq(a, 0n);
+
+ const [ b, c, d, e ] = range();
+ t.assertEq(
+ [ b, c, d, e ],
+ [ 0n, 1n, 2n, 3n ],
+ );
+
+ t.assertEq(
+ Array.from(take(5, range())),
+ [ 0n, 1n, 2n, 3n, 4n ],
+ );
+
+ t.assertEq(
+ Array.from(take(1, range())),
+ [ 0n ],
+ );
+
+ t.assertEq(
+ Array.from(take(0, range())),
+ [],
+ );
+ });
+
+ await t.testing("example usage", () => {
+ t.assertEq(
+ [...range(-5, 5)],
+ [ -5n, -4n, -3n, -2n, -1n, 0n, 1n, 2n, 3n, 4n ],
+ );
+ t.assertEq(
+ [...range(-100, 100, 10)],
+ [
+ -100n, -90n, -80n, -70n, -60n, -50n, -40n, -30n,
+ -20n, -10n, 0n, 10n, 20n, 30n, 40n, 50n, 60n,
+ 70n, 80n, 90n,
+ ],
+ );
+
+ t.assertEq([...range(0, 4, 2)], [0n, 2n]);
+ t.assertEq([...range(0, 5, 2)], [0n, 2n, 4n]);
+ t.assertEq([...range(0, 6, 2)], [0n, 2n, 4n]);
+ t.assertEq([...range(0, 7, 2)], [0n, 2n, 4n, 6n]);
+
+ t.assertEq(
+ [...range(100, 0, -10)],
+ [ 100n, 90n, 80n, 70n, 60n, 50n, 40n, 30n, 20n, 10n ]
+ );
+ t.assertEq(
+ [...range(10, -10, -1)],
+ [
+ 10n, 9n, 8n, 7n, 6n, 5n, 4n, 3n, 2n, 1n, 0n,
+ -1n, -2n, -3n, -4n, -5n, -6n, -7n, -8n, -9n,
+ ],
+ );
+
+ t.assertEq(
+ [...take(3, range(1, 10, 0))],
+ [ 1n, 1n, 1n ],
+ );
+ t.assertEq(
+ [...take(3, range(10, 1, 0))],
+ [ 10n, 10n, 10n ],
+ );
+ });
+};
+
+
+
+await runTests([
+ test_getIn,
+ test_merge,
+ test_compareObject,
+ test_escape,
+ test_eq,
+ test_keys,
+ test_difference,
+ test_assocIn,
+ test_dissoc,
+ test_findFirst,
+ test_partial,
+ test_strSortFn,
+ test_undefinedAsNull,
+ test_first,
+ test_rest,
+ test_butlast,
+ test_last,
+ test_take,
+ test_range,
+]);