From d8467dda4ec928b3a10e6ae0212a17e47fba9059 Mon Sep 17 00:00:00 2001 From: EuAndreh Date: Sat, 5 Jul 2025 06:36:34 -0300 Subject: Use .mjs extension; add reduce(); finish tests; export names correctly. --- .gitignore | 2 + Makefile | 22 +- src/sjs.js | 304 ----------------- src/sjs.mjs | 449 +++++++++++++++++++++++++ tests/sjs.js | 607 ---------------------------------- tests/sjs.mjs | 1011 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 1477 insertions(+), 918 deletions(-) create mode 100644 .gitignore delete mode 100644 src/sjs.js create mode 100644 src/sjs.mjs delete mode 100644 tests/sjs.js create mode 100644 tests/sjs.mjs 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 = { - '&': '&', - '<': '<', - '>': '>', - "'": ''', - '"': '"' -}; - -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 => `${s}`; - const green = s => `${s}`; - const yellow = s => `${s}`; - 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 = { + '&': '&', + '<': '<', + '>': '>', + "'": ''', + '"': '"' +}; + +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 => + `${s}`; + 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 = `&gt;`; + const expected = + `<img href="img'ln.png" ` + + `/>&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, +]); -- cgit v1.2.3