summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEuAndreh <eu@euandre.org>2025-07-05 06:36:34 -0300
committerEuAndreh <eu@euandre.org>2025-07-07 05:53:44 -0300
commitd8467dda4ec928b3a10e6ae0212a17e47fba9059 (patch)
tree6c1be27535b81126076fbc7a2a691db120db5999
parentImport existing code as-is (diff)
downloadsjs-d8467dda4ec928b3a10e6ae0212a17e47fba9059.tar.gz
sjs-d8467dda4ec928b3a10e6ae0212a17e47fba9059.tar.xz
Use .mjs extension; add reduce(); finish tests; export names correctly.
-rw-r--r--.gitignore2
-rw-r--r--Makefile22
-rw-r--r--src/sjs.js304
-rw-r--r--src/sjs.mjs449
-rw-r--r--tests/sjs.js607
-rw-r--r--tests/sjs.mjs1011
6 files changed, 1477 insertions, 918 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bf8600d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/node_modules/
+/src/*.exported.mjs
diff --git a/Makefile b/Makefile
index c89a0a8..e91c876 100644
--- a/Makefile
+++ b/Makefile
@@ -22,7 +22,7 @@ JSIMPL = node
.SUFFIXES:
-.SUFFIXES: .js .js-check
+.SUFFIXES: .mjs .mjs-check
@@ -30,10 +30,11 @@ all:
include deps.mk
sources = \
- src/$(NAME).js \
+ src/$(NAME).mjs \
derived-assets = \
+ src/$(NAME).exported.mjs \
side-assets = \
@@ -44,11 +45,18 @@ side-assets = \
all: $(derived-assets)
+src/$(NAME).exported.mjs: src/$(NAME).mjs Makefile
+ cp src/$(NAME).mjs $@
+ printf '\n\nexport {\n' >> $@
+ awk '/^const / { printf "\t%s,\n", $$2 }' src/$(NAME).mjs >> $@
+ printf '}\n' >> $@
-tests/$(NAME).js-check:
- $(JSIMPL) $*.js
-check-unit: tests/$(NAME).js-check
+
+tests/$(NAME).mjs-check: src/$(NAME).exported.mjs
+ $(JSIMPL) $*.mjs
+
+check-unit: tests/$(NAME).mjs-check
integration-tests = \
@@ -81,7 +89,7 @@ install: all
'$(DESTDIR)$(JSLIBDIR)' \
'$(DESTDIR)$(SRCDIR)' \
- cp src/$(NAME).js '$(DESTDIR)$(JSLIBDIR)'
+ cp src/$(NAME).mjs '$(DESTDIR)$(JSLIBDIR)'
cp $(sources) '$(DESTDIR)$(SRCDIR)'
## Uninstalls from $(DESTDIR)$(PREFIX). This is a perfect mirror
@@ -89,7 +97,7 @@ install: all
## A dedicated test asserts that this is always true.
uninstall:
rm -rf \
- '$(DESTDIR)$(JSLIBDIR)'/$(NAME).js \
+ '$(DESTDIR)$(JSLIBDIR)'/$(NAME).mjs \
'$(DESTDIR)$(SRCDIR)' \
diff --git a/src/sjs.js b/src/sjs.js
deleted file mode 100644
index 799f964..0000000
--- a/src/sjs.js
+++ /dev/null
@@ -1,304 +0,0 @@
-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/src/sjs.mjs b/src/sjs.mjs
new file mode 100644
index 0000000..0b2c705
--- /dev/null
+++ b/src/sjs.mjs
@@ -0,0 +1,449 @@
+export const max = (a, b) => a > b ? a : b;
+export const min = (a, b) => a < b ? a : b;
+
+export const explode = s => s.split("");
+
+export const getIn = (obj, path) =>
+ path.length === 0
+ ? obj
+ : getIn(obj?.[path[0]], path.slice(1));
+
+export const merge = (lhs, rhs) => {
+ if (lhs === undefined || lhs === null) {
+ return rhs;
+ }
+
+ if (rhs === undefined || rhs === null) {
+ return lhs;
+ }
+
+ 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 compareString = (a, b) => a.localeCompare(b, "POSIX");
+
+export const compareNumber = (a, b) => a - b;
+
+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 escapeHTML = s =>
+ String.prototype.replace.call(s, FROM, mappingFn);
+
+export const union = (a, b) =>
+ [...new Set(a.concat(b))]
+
+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 partial = (fn, ...startArgs) =>
+ (...endArgs) =>
+ fn(...startArgs, ...endArgs);
+
+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 complement = fn => (...args) => !fn(...args);
+
+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 MATRIX_A = 0x9908b0df;
+
+const CONSTANTS = {
+ MAX_INT: 4294967296.0,
+ N: 624,
+ M: 397,
+ UPPER_MASK: 0x80000000,
+ LOWER_MASK: 0x7fffffff,
+ MATRIX_A,
+ MAG01: new Array(0, MATRIX_A),
+};
+
+const initFn = () => ({
+ mt: new Array(CONSTANTS.N),
+ mti: CONSTANTS.N + 1,
+});
+
+const seedFn = (ref, seed) => {
+ ref.mt[0] = seed;
+ for (ref.mti = 1; ref.mti < CONSTANTS.N; ref.mti++) {
+ const s = ref.mt[ref.mti - 1] ^ (ref.mt[ref.mti - 1] >>> 30);
+ ref.mt[ref.mti] = (
+ ((((s & 0xffff0000) >>> 16) * 1812433253) << 16)
+ +
+ (s & 0x0000ffff)
+ *
+ 1812433253
+ ) + ref.mti;
+ ref.mt[ref.mti] >>>= 0;
+ }
+};
+
+const generateFn = (ref, newSeed) => {
+ if (newSeed) {
+ seedFn(ref, newSeed);
+ return;
+ }
+
+ let y = null;
+ if (ref.mti >= CONSTANTS.N) {
+ if (ref.mti === CONSTANTS.N + 1) {
+ seedFn(ref, 5489);
+ }
+
+ {
+ let kk = null;
+ for (kk = 0; kk < CONSTANTS.N - CONSTANTS.M; kk++) {
+ y = (ref.mt[kk] & CONSTANTS.UPPER_MASK) |
+ (ref.mt[kk + 1] & CONSTANTS.LOWER_MASK);
+ ref.mt[kk] =
+ ref.mt[kk + CONSTANTS.M] ^
+ (y >>> 1) ^
+ CONSTANTS.MAG01[y & 1];
+ }
+
+ for (; kk < CONSTANTS.N - 1; kk++) {
+ y = (ref.mt[kk] & CONSTANTS.UPPER_MASK) |
+ (ref.mt[kk + 1] & CONSTANTS.LOWER_MASK);
+ ref.mt[kk] =
+ ref.mt[
+ kk +
+ (CONSTANTS.M - CONSTANTS.N)
+ ] ^
+ (y >>> 1) ^
+ CONSTANTS.MAG01[y & 1];
+ }
+ }
+
+ y = (ref.mt[CONSTANTS.N - 1] & CONSTANTS.UPPER_MASK) |
+ (ref.mt[0] & CONSTANTS.LOWER_MASK);
+ ref.mt[CONSTANTS.N - 1] =
+ ref.mt[CONSTANTS.M - 1] ^
+ (y >>> 1) ^
+ CONSTANTS.MAG01[y & 1];
+ ref.mti = 0;
+ }
+
+ y = ref.mt[ref.mti++];
+
+ y ^= (y >>> 11);
+ y ^= (y << 7) & 0x9d2c5680;
+ y ^= (y << 15) & 0xefc60000;
+ y ^= (y >>> 18);
+
+ return y >>> 0;
+};
+
+export const makeRandom = (seed = new Date().getTime()) => {
+ const state = initFn();
+ seedFn(state, seed);
+ return partial(generateFn, state);
+};
+
+class Reduced {
+ constructor(value) {
+ this.value = value;
+ }
+}
+
+export const isReduced = x => x instanceof Reduced;
+
+export const reduced = x => new Reduced(x);
+
+const reduceRec = (coll, fn, acc, index) =>
+ isReduced(acc) ? acc.value :
+ index === coll.length
+ ? acc
+ : reduceRec(
+ coll,
+ fn,
+ fn(acc, coll[index], index, coll),
+ index + 1,
+ );
+
+export const reduce = (coll, fn, init) =>
+ init === undefined
+ ? reduceRec(coll.slice(1), fn, coll[0], 0)
+ : reduceRec(coll, fn, init, 0);
+
+export const mapValues = (obj, fn) =>
+ Object.keys(obj).reduce(
+ (o, k) => ({
+ ...o,
+ [k]: fn(obj[k], k),
+ }),
+ {},
+ );
+
+export const repeat = (n, x) => (new Array(n)).fill(x);
+
+export const red = s => `\x1b[0;31m${s}\x1b[0m`;
+export const redb = s => `\x1b[1;31m${s}\x1b[0m`;
+export const green = s => `\x1b[0;32m${s}\x1b[0m`;
+export const greenb = s => `\x1b[1;32m${s}\x1b[0m`;
+export const yellow = s => `\x1b[0;33m${s}\x1b[0m`;
+export const yellowb = s => `\x1b[1;33m${s}\x1b[0m`;
+export const yellowi = s => `\x1b[0;93m${s}\x1b[0m`;
+export const bluei = s => `\x1b[0;94m${s}\x1b[0m`;
+export const purple = s => `\x1b[0;95m${s}\x1b[0m`;
+export const purpleb = s => `\x1b[1;95m${s}\x1b[0m`;
+export const whiteb = s => `\x1b[1;37m${s}\x1b[0m`;
+export const grey = s => `\x1b[0;90m${s}\x1b[0m`;
+export const greyb = s => `\x1b[1;90m${s}\x1b[0m`;
+
+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 {
+ if (fn.constructor.name === "AsyncFunction") {
+ await fn();
+ } else {
+ fn();
+ }
+ } catch (e) {
+ err(`${colors.red("ERR")}.\n`);
+ throw e;
+ }
+ err(`${colors.green("OK")}.\n`);
+ },
+});
+
+const getJSImpl = () => {
+ if (typeof scriptArgs !== "undefined") {
+ return "qjs";
+ }
+ if (typeof Deno !== "undefined") {
+ return "deno";
+ }
+ if (typeof process !== "undefined") {
+ return "node";
+ }
+ if (typeof window !== "undefined") {
+ return "browser";
+ }
+};
+
+const mappings = {
+ ARGV: {
+ qjs: () => scriptArgs,
+ node: () => process.argv.slice(1),
+ deno: () => Deno.mainModule.substring("file://".length),
+ browser: () => null,
+ },
+ exit: {
+ qjs: async () => {
+ const { exit } = await import("std");
+ return exit;
+ },
+ node: async () => process.exit,
+ deno: async () => Deno.exit,
+ browser: () => null,
+ },
+ tconf: {
+ qjs: async () => await mappings.testConf.unknown(),
+ deno: async () => await mappings.testConf.node(),
+ browser: async () => {
+ class DOMAssertionError extends Error {}
+
+ const w = s => {
+ const $testReport =
+ document.querySelector("#test-report");
+ $testReport.innerHTML += s;
+ };
+
+
+ const color = c => s =>
+ `<span style="color: ${c}; ">${s}</span>`;
+ const red = s => color("red");
+ const green = s => color("green");
+ const yellow = s => color("darkgoldenrod");
+ return {
+ testStart: msg => w(`${msg}:\n`),
+ testing: msg =>
+ w(`${yellow("testing")}: ${msg}...`),
+ testOK: () => w(` ${green("OK")}.\n`),
+ assertEq: (x, msg = "") => {
+ if (x) {
+ return;
+ }
+
+ 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");
+ return {
+ err: x => process.stderr.write(x),
+ assertEq: assert.deepEqual,
+ colors: {
+ red,
+ green,
+ yellow,
+ },
+ };
+ },
+ },
+};
+
+export const JSImpl = getJSImpl();
+export const exit = await mappings.exit[ JSImpl]?.();
+export const ARGV = await mappings.ARGV[ JSImpl]?.();
+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
deleted file mode 100644
index bf5f3dd..0000000
--- a/tests/sjs.js
+++ /dev/null
@@ -1,607 +0,0 @@
-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,
-]);
diff --git a/tests/sjs.mjs b/tests/sjs.mjs
new file mode 100644
index 0000000..dbcd635
--- /dev/null
+++ b/tests/sjs.mjs
@@ -0,0 +1,1011 @@
+import {
+ max,
+ min,
+ explode,
+ getIn,
+ merge,
+ compareString,
+ compareNumber,
+ compareObject,
+ escapeHTML,
+ union,
+ eq,
+ keys,
+ difference,
+ assocIn,
+ dissoc,
+ partial,
+ undefinedAsNull,
+ first,
+ rest,
+ butlast,
+ last,
+ complement,
+ take,
+ range,
+ initFn,
+ seedFn,
+ generateFn,
+ makeRandom,
+ isReduced,
+ reduced,
+ reduceRec,
+ reduce,
+ mapValues,
+ repeat,
+ runTests,
+} from "../src/sjs.exported.mjs";
+
+
+
+const test_max = t => {
+ t.start("max()");
+
+ t.testing("gets the greater number", () => {
+ t.assertEq(max(0, 0), 0);
+ t.assertEq(max(1, 2), 2);
+ t.assertEq(max(4, 3), 4);
+ });
+};
+
+const test_min = t => {
+ t.start("min()");
+
+ t.testing("gets the smaller number", () => {
+ t.assertEq(min(0, 0), 0);
+ t.assertEq(min(1, 2), 1);
+ t.assertEq(min(4, 3), 3);
+ });
+};
+
+const test_explode = t => {
+ t.start("explode()");
+
+ t.testing("empty array on empty string", () => {
+ t.assertEq(explode(""), []);
+ });
+
+ t.testing("string to array of chars", () => {
+ t.assertEq(explode("a"), ["a"]);
+ t.assertEq(explode("string"), ["s", "t", "r", "i", "n", "g"]);
+ });
+};
+
+const test_getIn = t => {
+ t.start("getIn()");
+
+ t.testing("empty values", () => {
+ t.assertEq(getIn({}, []), {});
+ t.assertEq(getIn({ k: "v" }, []), { k: "v" });
+ t.assertEq(getIn({}, [0]), undefined);
+ });
+
+ t.testing("undefined on bad path", () => {
+ t.assertEq(getIn({a: 1}, ["b", "c"]), undefined);
+ });
+
+ t.testing("mix of arrays and objects", () => {
+ t.assertEq(getIn(
+ {a: [{}, {b: [{c: [123]}]}]},
+ ["a", 1, "b", 0, "c", 0],
+ ), 123);
+ });
+
+ 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);
+ });
+
+ t.testing("nested valeues", () => {
+ t.assertEq(getIn(
+ { a: { b: { c: { d: "e" }}}},
+ ["a", "b", "c", "d"],
+ ), "e");
+ });
+};
+
+const test_merge = t => {
+ t.start("merge()");
+
+ t.testing("empty values", () => {
+ t.assertEq(merge({}, {}), {});
+ });
+
+ t.testing("rhs gets preference over lhs", () => {
+ t.assertEq(
+ merge({a: 1, b: 2}, {b: 3, c: 4}),
+ {a: 1, b: 3, c: 4},
+ );
+ });
+
+ t.testing("deep merge", () => {
+ const lhs = {
+ a: 1,
+ b: 2,
+ c: {
+ 0: "zero",
+ 1: "one",
+ 2: {
+ two: [2, 3],
+ },
+ },
+ };
+ const rhs = {
+ a: 11,
+ c: {
+ 0: "ZERO",
+ 2: {
+ three: 123,
+ },
+ },
+ };
+ const expected = {
+ a: 11,
+ b: 2,
+ c: {
+ 0: "ZERO",
+ 1: "one",
+ 2: {
+ two: [2, 3],
+ three: 123,
+ },
+ },
+ };
+ t.assertEq(merge(lhs, rhs), expected);
+ });
+};
+
+const test_compareString = t => {
+ t.start("compareString()");
+
+ t.testing("empty value", () => {
+ t.assertEq(compareString("", ""), 0);
+ });
+
+ t.testing("default sort", () => {
+ const arr = [ "a", "Z" ];
+ t.assertEq(
+ arr.toSorted(compareString),
+ arr.toSorted().reverse(),
+ );
+ });
+};
+
+const test_compareNumber = t => {
+ t.start("compareNumber()");
+
+ t.testing("ascending sort", () => {
+ const input = [ 9, 3, 8, 4, 6, 7 ];
+ const expected = [ 3, 4, 6, 7, 8, 9 ];
+ t.assertEq(input.toSorted(compareNumber), expected);
+ });
+};
+
+const test_compareObject = t => {
+ t.start("compareObject()");
+
+ t.testing("sort array of objects", () => {
+ const input = [
+ { a: 3, b: 1, c: 1 },
+ { a: 1, b: 3, c: 1 },
+ { a: 1, b: 1, c: 3 },
+ ];
+ const expected = [
+ { a: 1, b: 1, c: 3 },
+ { a: 1, b: 3, c: 1 },
+ { a: 3, b: 1, c: 1 },
+ ];
+ t.assertEq(input.toSorted(compareObject), expected);
+ });
+};
+
+const test_escapeHTML = t => {
+ t.start("escapeHTML()");
+
+ t.testing("noop on empty string", () => {
+ t.assertEq(escapeHTML(""), "");
+ });
+
+ t.testing("noop on string without special chars", () => {
+ t.assertEq(
+ escapeHTML("my non-special string"),
+ "my non-special string",
+ );
+ });
+
+ t.testing("escapeable chars", () => {
+ const input = `<img href="img'ln.png" />&amp;gt;`;
+ const expected =
+ `&lt;img href=&quot;img&#39;ln.png&quot; ` +
+ `/&gt;&amp;amp;gt;`;
+ t.assertEq(escapeHTML(input), expected);
+ });
+};
+
+const test_union = t => {
+ t.start("union()");
+
+ t.testing("empty values", () => {
+ t.assertEq(union([], []), []);
+ t.assertEq(union([1], []), [1]);
+ t.assertEq(union([], [1]), [1]);
+ });
+
+ t.testing("non intersecting arrays", () => {
+ t.assertEq(
+ union([1, 2], [3, 4]).toSorted(compareNumber),
+ [1, 2, 3, 4],
+ );
+ });
+
+ t.testing("intersecting arrays", () => {
+ t.assertEq(
+ union([1, 2, 3, 4], [3, 4, 5]).toSorted(compareNumber),
+ [1, 2, 3, 4, 5],
+ )
+ });
+};
+
+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.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);
+ });
+
+ 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(12) }, { d: new Date(12) }), true);
+ });
+};
+
+const test_keys = t => {
+ t.start("keys()");
+ t.testing("happy paths", () => {
+ t.assertEq(
+ { a: 1, b: 2 },
+ keys(["a", "b"], { a: 1, b: 2, c: 3 }),
+ );
+ });
+
+ 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 = t => {
+ t.start("difference()");
+
+ 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]),
+ );
+ });
+
+ 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 = t => {
+ t.start("assocIn()");
+
+ t.testing("empty values", () => {
+ t.assertEq(assocIn({}, [], null), {});
+ t.assertEq(assocIn({ k: "v" }, [], null), { k: "v" });
+ });
+
+ 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" }},
+ );
+ });
+
+ t.testing("replacing values", () => {
+ t.assertEq(
+ assocIn(
+ { k1: { k2: { k3: "before" }}},
+ ["k1", "k2", "k3"],
+ "after"
+ ),
+ { k1: { k2: { k3: "after" }}}
+ );
+ });
+};
+
+const test_dissoc = t => {
+ t.start("dissoc()");
+
+ t.testing("empty values", () => {
+ t.assertEq(dissoc({}, "k"), {});
+ });
+
+ t.testing("noop when key does not exist", () => {
+ t.assertEq(dissoc({ a: 1 }, "b"), { a: 1 });
+ });
+
+ t.testing("removes the key", () => {
+ t.assertEq(dissoc({ a: 1, b: 2}, "b"), { a: 1 });
+ });
+};
+
+const test_partial = t => {
+ t.start("partial()");
+
+ 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());
+ });
+
+ t.testing("too few arguments", () => {
+ const threeArgs = (a, b, c) => {
+ if (c === undefined) {
+ throw new Error("missing args");
+ }
+ return a + b + c;
+ };
+
+ const add1 = partial(threeArgs, 1);
+ try {
+ add1(2);
+ t.assertEq(true, false);
+ } catch (e) {
+ t.assertEq(e.message, "missing args");
+ }
+
+ const add1And2 = partial(threeArgs, 1, 2);
+ try {
+ add1And2();
+ t.assertEq(true, false);
+ } catch (e) {
+ t.assertEq(e.message, "missing args");
+ }
+
+ const addNothing = partial(threeArgs);
+ try {
+ addNothing();
+ t.assertEq(true, false);
+ } catch (e) {
+ t.assertEq(e.message, "missing args");
+ }
+ });
+
+ 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);
+ });
+
+ 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 ],
+ );
+ });
+
+ 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_undefinedAsNull = t => {
+ t.start("undefinedAsNull()");
+
+ t.testing("null for undefined or null", () => {
+ t.assertEq(undefinedAsNull(undefined), null);
+ t.assertEq(undefinedAsNull(null), null);
+ });
+
+ 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 = t => {
+ t.start("first()");
+
+ t.testing("undefined for an empty array", () => {
+ t.assertEq(undefined, first([]));
+ t.assertEq(undefined, first(""));
+ });
+
+ t.testing("the first element otherwise", () => {
+ t.assertEq(1, first([1, 2, 3]));
+ t.assertEq("a", first("abc"));
+ });
+};
+
+const test_rest = t => {
+ t.start("rest()");
+
+ 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"));
+ });
+
+ t.testing("the rest of the collection otherwise", () => {
+ t.assertEq([2, 3], rest([1, 2, 3]));
+ t.assertEq("bc", rest("abc"));
+ });
+
+ t.testing("combines with first() well", () => {
+ const arr = ["anything", "can", "go", "here"];
+ t.assertEq(arr, [ first(arr), ...rest(arr) ]);
+ });
+};
+
+const test_butlast = t => {
+ t.start("butlast()");
+
+ t.testing("empty array when ther are no more elements", () => {
+ t.assertEq([], butlast([]));
+ t.assertEq([], butlast([1]));
+ t.assertEq("", butlast(""));
+ t.assertEq("", butlast("c"));
+ });
+
+ t.testing("the beginning of the array otherwise", () => {
+ t.assertEq([1, 2], butlast([1, 2, 3]));
+ t.assertEq("ab", butlast("abc"));
+ });
+};
+
+const test_last = t => {
+ t.start("last()");
+
+ t.testing("undefined for an empty array", () => {
+ t.assertEq(undefined, last([]));
+ t.assertEq(undefined, last(""));
+ });
+
+ t.testing("the last element otherwise", () => {
+ t.assertEq(3, last([1, 2, 3]));
+ t.assertEq("c", last("abc"));
+ });
+
+ t.testing("combines with butlast() well", () => {
+ const arr = ["anything", "goes", "here", "too"];
+ t.assertEq(arr, [ ...butlast(arr), last(arr) ]);
+ });
+};
+
+const test_complement = t => {
+ t.start("complement()");
+
+ t.testing("even to odd", () => {
+ const even = n => (n % 2) === 0;
+ const odd = complement(even);
+ const input = [1, 2, 3, 4, 5];
+ const expected1 = [2, 4];
+ const expected2 = [1, 3, 5];
+ t.assertEq(input.filter(even), expected1);
+ t.assertEq(input.filter(odd), expected2);
+ });
+};
+
+const test_take = t => {
+ t.start("take()");
+
+ 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 = t => {
+ t.start("range()");
+
+ 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())),
+ [],
+ );
+ });
+
+ 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 ],
+ );
+ });
+};
+
+const test_initFn = t => {
+ t.start("initFn()");
+
+ t.testing("constant values", () => {
+ t.assertEq(initFn(), {
+ mt: new Array(624),
+ mti: 625,
+ });
+ });
+};
+
+const test_seedFn = t => {
+ t.start("seedFn()");
+
+ t.testing("expected values", () => {
+ const expected = [
+ 1, 1812433254,
+ 3713160357, 3109174145,
+ 64984499, 3392658084,
+ 446538473, 2629760756,
+ 2453345558, 1394803949,
+ ];
+ const ref = initFn();
+ seedFn(ref, 1);
+ t.assertEq(ref.mt.slice(0, 10), expected);
+ });
+};
+
+const test_generateFn = t => {
+ t.start("generateFn()");
+
+ t.testing("expected values, created from a seed", () => {
+ const n1 = 1791095845;
+ const n2 = 4282876139;
+ const n3 = 3093770124;
+
+ const ref = initFn();
+ seedFn(ref, 1);
+ const rand = partial(generateFn, ref);
+ t.assertEq(rand(), n1);
+ t.assertEq(rand(), n2);
+ t.assertEq(rand(), n3);
+ rand(1);
+ t.assertEq(rand(), n1);
+ t.assertEq(rand(), n2);
+ t.assertEq(rand(), n3);
+ });
+};
+
+const test_makeRandom = t => {
+ t.start("random()");
+
+ t.testing("generates the expected numbers", () => {
+ const expected = [
+ 1791095845, 4282876139, 3093770124, 4005303368,
+ 491263, 550290313, 1298508491, 4290846341,
+ 630311759, 1013994432, 396591248, 1703301249,
+ 799981516, 1666063943, 1484172013, 2876537340,
+ 1704103302, 4018109721, 2314200242, 3634877716,
+ 1800426750, 1345499493, 2942995346, 2252917204,
+ 878115723, 1904615676, 3771485674, 986026652,
+ 117628829, 2295290254, 2879636018, 3925436996,
+ 1792310487, 1963679703, 2399554537, 1849836273,
+ 602957303, 4033523166, 850839392, 3343156310,
+ 3439171725, 3075069929, 4158651785, 3447817223,
+ 1346146623, 398576445, 2973502998, 2225448249,
+ 3764062721, 3715233664, 3842306364, 3561158865,
+ 365262088, 3563119320, 167739021, 1172740723,
+ 729416111, 254447594, 3771593337, 2879896008,
+ 422396446, 2547196999, 1808643459, 2884732358,
+ 4114104213, 1768615473, 2289927481, 848474627,
+ 2971589572, 1243949848, 1355129329, 610401323,
+ 2948499020, 3364310042, 3584689972, 1771840848,
+ 78547565, 146764659, 3221845289, 2680188370,
+ 4247126031, 2837408832, 3213347012, 1282027545,
+ 1204497775, 1916133090, 3389928919, 954017671,
+ 443352346, 315096729, 1923688040, 2015364118,
+ 3902387977, 413056707, 1261063143, 3879945342,
+ 1235985687, 513207677, 558468452, 2253996187,
+ 83180453, 359158073, 2915576403, 3937889446,
+ 908935816, 3910346016, 1140514210, 1283895050,
+ 2111290647, 2509932175, 229190383, 2430573655,
+ 2465816345, 2636844999, 630194419, 4108289372,
+ 2531048010, 1120896190, 3005439278, 992203680,
+ 439523032, 2291143831, 1778356919, 4079953217,
+ 2982425969, 2117674829, 1778886403, 2321861504,
+ 214548472, 3287733501, 2301657549, 194758406,
+ 2850976308, 601149909, 2211431878, 3403347458,
+ 4057003596, 127995867, 2519234709, 3792995019,
+ 3880081671, 2322667597, 590449352, 1924060235,
+ 598187340, 3831694379, 3467719188, 1621712414,
+ 1708008996, 2312516455, 710190855, 2801602349,
+ 3983619012, 1551604281, 1493642992, 2452463100,
+ 3224713426, 2739486816, 3118137613, 542518282,
+ 3793770775, 2964406140, 2678651729, 2782062471,
+ 3225273209, 1520156824, 1498506954, 3278061020,
+ 1159331476, 1531292064, 3847801996, 3233201345,
+ 1838637662, 3785334332, 4143956457, 50118808,
+ 2849459538, 2139362163, 2670162785, 316934274,
+ 492830188, 3379930844, 4078025319, 275167074,
+ 1932357898, 1526046390, 2484164448, 4045158889,
+ 1752934226, 1631242710, 1018023110, 3276716738,
+ 3879985479, 3313975271, 2463934640, 1294333494,
+ 12327951, 3318889349, 2650617233, 656828586,
+ ];
+
+ const firstRand = makeRandom(1);
+ const given1 = expected.map(_ => firstRand());
+ firstRand(1);
+ const given2 = expected.map(_ => firstRand());
+
+ const secondRand = makeRandom(1);
+ const given3 = expected.map(_ => secondRand());
+ secondRand(1);
+ const given4 = expected.map(_ => secondRand());
+
+ t.assertEq(given1, expected);
+ t.assertEq(given2, expected);
+ t.assertEq(given3, expected);
+ t.assertEq(given4, expected);
+ });
+};
+
+const test_isReduced = t => {
+ t.start("isReduced()");
+
+ t.testing("simple boolean response", () => {
+ t.assertEq(isReduced(null), false);
+ t.assertEq(isReduced(undefined), false);
+ t.assertEq(isReduced(0), false);
+ t.assertEq(isReduced([]), false);
+ t.assertEq(isReduced("reduce"), false);
+ t.assertEq(isReduced({}), false);
+ t.assertEq(isReduced(reduced(null)), true);
+ });
+};
+
+const test_reduced = t => {
+ t.start("reduced()");
+
+ t.testing("simple constructor", () => {
+ t.assertEq(isReduced(reduced(123)), true);
+ });
+
+ t.testing("we can extract the value", () => {
+ t.assertEq(reduced("data").value, "data");
+ });
+};
+
+const test_reduceRec = t => {
+ t.start("reduceRec()");
+
+ t.testing("return the value if isReduced()", () => {
+ t.assertEq(reduceRec(null, null, reduced("abc"), null), "abc");
+ });
+
+ t.testing("the bare value if at the end of the collection", () => {
+ t.assertEq(reduceRec([], null, "acc", 0), "acc");
+ t.assertEq(reduceRec([1, 2], null, "acc", 2), "acc");
+ });
+
+ t.testing("recurse while applying function", () => {
+ const expected = [{
+ acc: -5,
+ el: 4,
+ index: 3,
+ coll: [1, 2, 3, 4, 5],
+ ret: -1,
+ }, {
+ acc: -1,
+ el: 5,
+ index: 4,
+ coll: [1, 2, 3, 4, 5],
+ ret: 4,
+ }];
+ const calls = [];
+ const fn = (acc, el, index, coll) => {
+ const ret = acc + el;
+ calls.push({
+ acc,
+ el,
+ index,
+ coll,
+ ret,
+ });
+ return ret;
+ };
+ const coll = [1, 2, 3, 4, 5];
+ t.assertEq(reduceRec(coll, fn, -5, 3), 4);
+ t.assertEq(calls, expected);
+ });
+};
+
+const test_reduce = t => {
+ t.start("reduce()");
+
+ t.testing("empty value", () => {
+ t.assertEq(reduce([], null), undefined);
+ t.assertEq(reduce([], null, 123), 123);
+ });
+
+ t.testing("without init", () => {
+ t.assertEq(reduce([1], null), 1);
+ t.assertEq(reduce([1, 300, 2, 3], max), 300);
+ });
+
+ t.testing("with init", () => {
+ t.assertEq(reduce([1], max, 100), 100);
+ t.assertEq(reduce([1, 300, 2, 3], max, 400), 400);
+ });
+};
+
+const test_mapValues = t => {
+ t.start("mapValues()");
+
+ t.testing("empty object", () => {
+ t.assertEq(mapValues({}, null), {});
+ });
+
+ t.testing("update the values", () => {
+ const obj = { a: 1, b: 2, c: 3 };
+ t.assertEq(mapValues(obj, _ => 1, ), {a: 1, b: 1, c: 1});
+ t.assertEq(mapValues(obj, x => x * x, ), {a: 1, b: 4, c: 9});
+ });
+};
+
+const test_repeat = t => {
+ t.start("repeat()");
+
+ t.testing("empty value", () => {
+ t.assertEq(repeat(0, null), []);
+ });
+
+ t.testing("just repeat the values", () => {
+ t.assertEq(repeat(1, ""), [""]);
+ t.assertEq(repeat(10, 1), [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]);
+ });
+};
+
+
+
+runTests([
+ test_max,
+ test_min,
+ test_explode,
+ test_getIn,
+ test_merge,
+ test_compareString,
+ test_compareNumber,
+ test_compareObject,
+ test_escapeHTML,
+ test_union,
+ test_eq,
+ test_keys,
+ test_difference,
+ test_assocIn,
+ test_dissoc,
+ test_partial,
+ test_undefinedAsNull,
+ test_first,
+ test_rest,
+ test_butlast,
+ test_last,
+ test_complement,
+ test_take,
+ test_range,
+ test_initFn,
+ test_seedFn,
+ test_generateFn,
+ test_makeRandom,
+ test_isReduced,
+ test_reduced,
+ test_reduceRec,
+ test_reduce,
+ test_mapValues,
+ test_repeat,
+]);