diff options
author | Tor Andersson <tor@ccxvii.net> | 2025-04-16 10:25:23 +0200 |
---|---|---|
committer | Tor Andersson <tor@ccxvii.net> | 2025-04-25 16:06:05 +0200 |
commit | b431c667423dc9f3717610170b65d6193a66f614 (patch) | |
tree | 6bacf24924da432088be5f5a90439b3a79318fc5 | |
parent | ab1d804788daa053d1624dfad87ae024bef9ef02 (diff) | |
download | server-b431c667423dc9f3717610170b65d6193a66f614.tar.gz |
Add common/framework.js
-rw-r--r-- | public/common/framework.js | 851 | ||||
-rw-r--r-- | public/common/util.js | 366 |
2 files changed, 851 insertions, 366 deletions
diff --git a/public/common/framework.js b/public/common/framework.js new file mode 100644 index 0000000..63adc57 --- /dev/null +++ b/public/common/framework.js @@ -0,0 +1,851 @@ +/* FRAMEWORK */ + +/* +const ROLES = [] +const SCENARIOS = [] +var G, L, R, V, S = {}, P = {} +*/ + +function log(s) { + if (s === undefined) { + if (G.log.length > 0 && G.log[G.log.length - 1] !== "") + G.log.push("") + } else { + G.log.push(s) + } +} + +function prompt(s) { + V.prompt = s +} + +function button(action, enabled = true) { + V.actions[action] = !!enabled | 0 +} + +function action(action, argument) { + if (!(action in V.actions)) + V.actions[action] = [] + set_add(V.actions[action], argument) +} + +function finish(result, message) { + G.active = -1 + G.result = ROLES[result] ?? result + G.L = { message } + log() + log(message) +} + +function call_or_goto(pred, name, env) { + if (pred) + call(name, env) + else + goto(name, env) +} + +function call(name, env) { + G.L = L = { ...env, P: name, I: 0, L: L } + S[name]?._begin?.() +} + +function goto(name, env) { + S[L.P]?._end?.() + G.L = L = { ...env, P: name, I: 0, L: L.L } + S[name]?._begin?.() +} + +function end(result) { + S[L.P]?._end?.() + G.L = L = L.L + if (result !== undefined) + L.$ = result + S[L.P]?._resume?.() +} + +exports.roles ??= ROLES + +exports.scenarios ??= (typeof SCENARIOS !== "undefined") ? SCENARIOS : [ "Standard" ] + +exports.setup = function (seed, scenario, options) { + G = { + active: null, + seed, + log: [], + undo: [], + } + L = null + R = null + V = null + + on_setup(scenario, options) + run() + save() + + return G +} + +exports.view = function (state, role) { + G = state + L = G.L + R = role + V = { + log: G.log, + prompt: null, + } + + if ((Array.isArray(G.active) && G.active.includes(R)) || G.active === R) { + load() + on_view() + + V.actions = {} + + if (S[L.P]) + S[L.P].prompt() + else + V.prompt = "TODO: " + L.P + + if (V.actions.undo === undefined) + button("undo", G.undo?.length > 0) + + save() + } else { + load() + on_view() + save() + + if (G.active === "None") { + V.prompt = L.message + } else { + var inactive = S[L.P]?.inactive + if (inactive) { + if (Array.isArray(G.active)) + V.prompt = `Waiting for ${G.active.join(" and ")} to ${inactive}.` + else + V.prompt = `Waiting for ${G.active} to ${inactive}.` + } else { + if (Array.isArray(G.active)) + V.prompt = `Waiting for ${G.active.join(" and ")}.` + else + V.prompt = `Waiting for ${G.active}.` + } + } + } + + return V +} + +exports.action = function (state, role, action, argument) { + G = state + L = G.L + R = role + V = null + + var old_active = G.active + + load() + + var this_state = S[L.P] + if (this_state && typeof this_state[action] === "function") { + this_state[action](argument) + run() + } else if (action === "undo" && G.undo.length > 0) { + pop_undo() + } else { + throw new Error("Invalid action: " + action) + } + + save() + + if (old_active !== G.active) + clear_undo() + + return G +} + +exports.finish = function (state, result, message) { + G = state + L = G.L + R = null + V = null + + load() + finish(result, message) + save() + + return G +} + +function load() { + R = ROLES.indexOf(R) + if (Array.isArray(G.active)) + G.active = G.active.map(r => ROLES.indexOf(r)) + else + G.active = ROLES.indexOf(G.active) +} + +function save() { + if (Array.isArray(G.active)) + G.active = G.active.map(r => ROLES[r]) + else + G.active = ROLES[G.active] ?? "None" +} + +function run() { + for (var i = 0; i < 1000 && L; ++i) { + var prog = P[L.P] + if (prog) { + if (typeof prog === "function") + prog() + else if (L.I < prog.length) + prog[L.I++]() + else + end() + } else { + return + } + } + if (L) + throw new Error("runaway script") +} + +function parse(text) { + var prog = [] + + function lex(s) { + var words = [] + var p = 0, n = s.length, m + + function lex_flush() { + if (words.length > 0) { + command(words) + words = [] + } + } + + function lex_newline() { + while (p < n && s[p] === "\n") + ++p + lex_flush() + } + + function lex_semi() { + ++p + lex_flush() + } + + function lex_comment() { + while (p < n && s[p] !== "\n") + ++p + } + + function lex_word() { + while (p < n && !" \t\n".includes(s[p])) + ++p + words.push(s.substring(m, p)) + } + + function lex_qstring(q) { + var x = 1 + ++p + while (p < n && x > 0) { + if (s[p] === q) + --x + ++p + } + if (p >= n && x > 0) + throw new Error("unterminated string") + words.push(s.substring(m, p)) + } + + function lex_bstring(a, b) { + var x = 1 + ++p + while (p < n && x > 0) { + if (s[p] === a) + ++x + else if (s[p] === b) + --x + ++p + } + if (p >= n && x > 0) + throw new Error("unterminated string") + words.push(s.substring(m, p)) + } + + while (p < n) { + while (s[p] === " " || s[p] === "\t") + ++p + if (p >= n) break + m = p + if (s[p] === "{") lex_bstring("{", "}") + else if (s[p] === "[") lex_bstring("[", "]") + else if (s[p] === "(") lex_bstring("(", ")") + else if (s[p] === '"') lex_qstring('"') + else if (s[p] === "\n") lex_newline() + else if (s[p] === ";") lex_semi() + else if (s[p] === "#") lex_comment() + else if (s[p] === "/" && s[p+1] === "/") lex_comment() + else if (s[p] === "-" && s[p+1] === "-") lex_comment() + else lex_word() + } + + if (words.length > 0) + command(words) + } + + function command(line) { + var ix_loop, ix1, ix2 + var i, k, start, end, array, body + + switch (line[0]) { + case "set": + if (line.length !== 3) + throw new Error("invalid set - " + line.join(" ")) + emit(line[1] + " = " + line[2]) + break + + case "incr": + if (line.length !== 2) + throw new Error("invalid incr - " + line.join(" ")) + emit("++(" + line[1] + ")") + break + + case "decr": + if (line.length !== 2) + throw new Error("invalid decr - " + line.join(" ")) + emit("--(" + line[1] + ")") + break + + case "eval": + emit(line.slice(1).join(" ")) + break + + case "log": + emit("log(" + line.slice(1).join(" ") + ")") + break + + case "call": + if (line.length === 3) + emit("call(" + quote(line[1]) + ", " + line[2] + ")") + else if (line.length === 2) + emit("call(" + quote(line[1]) + ")") + else + throw new Error("invalid call - " + line.join(" ")) + break + + case "goto": + if (line.length === 3) + emit("goto(" + quote(line[1]) + ", " + line[2] + ")") + else if (line.length === 2) + emit("goto(" + quote(line[1]) + ")") + else + throw new Error("invalid goto - " + line.join(" ")) + break + + case "return": + if (line.length === 1) + emit(`end()`) + else if (line.length === 2) + emit(`end(${line[1]})`) + else + throw new Error("invalid return - " + line.join(" ")) + break + + case "while": + // while (exp) { block } + if (line.length !== 3) + throw new Error("invalid while - " + line.join(" ")) + ix_loop = emit_jz(line[1]) + block(line[2]) + emit_jump(ix_loop) + label(ix_loop) + break + + case "for": + // for i in (start) to (end) { block } + if (line.length === 7 && line[2] === "in" && line[4] === "to") { + i = line[1] + start = line[3] + end = line[5] + body = line[6] + emit(`${i} = ${start}`) + ix_loop = prog.length + block(body) + emit(`if (++(${i}) <= ${end}) L.I = ${ix_loop}`) + return + } + // for i in (array) { block } + // NOTE: array is evaluated repeatedly so should be a constant! + else if (line.length === 5 && line[2] === "in") { + k = line[1] + i = k.replace(/^G\./, "L.G_") + "_" + array = line[3] + body = line[4] + ix_loop = emit(`if (${i} < ${array}.length) { ${k} = ${array}[${i}++] } else { delete ${i} ; L.I = % }`) + block(body) + emit_jump(ix_loop) + label(ix_loop) + } else { + throw new Error("invalid for - " + line.join(" ")) + } + break + + case "if": + // if (exp) { block} + // if (exp) { block } else { block } + // TODO: if (exp) { block } elseif (exp) { block } else { block } + ix1, ix2 + if (line.length === 3) { + ix1 = emit_jz(line[1]) + block(line[2]) + label(ix1) + } else if (line.length === 5 && line[3] === "else") { + ix1 = emit_jz(line[1]) + block(line[2]) + ix2 = emit_jump() + label(ix1) + block(line[4]) + label(ix2) + } else { + throw new Error("invalid if - " + line.join(" ")) + } + break + + default: + throw new Error("unknown command - " + line.join(" ")) + } + } + + function quote(s) { + if ("{[(`'\"".includes(s[0])) + return s + return '"' + s + '"' + } + + function emit_jz(exp, to = "%") { + return emit("if (!(" + exp + ")) L.I = " + to) + } + + function emit_jump(to = "%") { + return emit("L.I = " + to) + } + + function emit(s) { + prog.push(s) + return prog.length - 1 + } + + function label(ix) { + prog[ix] = prog[ix].replace("%", prog.length) + } + + function block(body) { + if (body[0] !== "{") + throw new Error("expected block") + lex(body.slice(1, -1)) + } + + lex(text) + + return prog +} + +function script(text) { + script.cache ??= {} + try { + return parse(text).map(inst => script.cache[inst] ??= eval("(function(){" + inst + "})")) + } catch (err) { + // return the error message so we can attach the script name and re-raise later + return err.message + } +} + +(function () { + script.cache = null + // catch and report delayed error messages + var errors = [] + for (var name in P) { + var prog = P[name] + if (typeof prog === "string") + errors.push("P." + name + ": " + prog) + if (S[name]) + errors.push("P." + name + " shadows S." + name) + } + if (errors.length > 0) + throw new Error("script errors found:\n\t" + errors.join("\n\t")) +})() + +/* LIBRARY */ + +function clear_undo() { + if (G.undo) { + G.undo.length = 0 + } +} + +function push_undo() { + var copy, k, v + if (G.undo) { + copy = {} + for (k in G) { + v = G[k] + if (k === "undo") + continue + else if (k === "log") + v = v.length + else if (typeof v === "object" && v !== null) + v = object_copy(v) + copy[k] = v + } + G.undo.push(copy) + } +} + +function pop_undo() { + if (G.undo) { + var save_log = G.log + var save_undo = G.undo + G = save_undo.pop() + save_log.length = G.log + G.log = save_log + G.undo = save_undo + } +} + +function random(range) { + // An MLCG using integer arithmetic with doubles. + // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf + // m = 2**35 − 31 + return (G.seed = G.seed * 200105 % 34359738337) % range +} + +function random_bigint(range) { + // Largest MLCG that will fit its state in a double. + // Uses BigInt for arithmetic, so is an order of magnitude slower. + // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf + // m = 2**53 - 111 + return (G.seed = Number(BigInt(G.seed) * 5667072534355537n % 9007199254740881n)) % range +} + +function shuffle(list) { + // Fisher-Yates shuffle + var i, j, tmp + for (i = list.length - 1; i > 0; --i) { + j = random(i + 1) + tmp = list[j] + list[j] = list[i] + list[i] = tmp + } +} + +function shuffle_bigint(list) { + // Fisher-Yates shuffle + var i, j, tmp + for (i = list.length - 1; i > 0; --i) { + j = random_bigint(i + 1) + tmp = list[j] + list[j] = list[i] + list[i] = tmp + } +} + +// Fast deep copy for objects without cycles +function object_copy(original) { + var copy, i, n, v + if (Array.isArray(original)) { + n = original.length + copy = new Array(n) + for (i = 0; i < n; ++i) { + v = original[i] + if (typeof v === "object" && v !== null) + copy[i] = object_copy(v) + else + copy[i] = v + } + return copy + } else { + copy = {} + for (i in original) { + v = original[i] + if (typeof v === "object" && v !== null) + copy[i] = object_copy(v) + else + copy[i] = v + } + return copy + } +} + +// Fast deep object comparison for objects without cycles +function object_diff(a, b) { + var i, key + var a_length + if (a === b) + return false + if (a !== null && b !== null && typeof a === "object" && typeof b === "object") { + if (Array.isArray(a)) { + if (!Array.isArray(b)) + return true + a_length = a.length + if (b.length !== a_length) + return true + for (i = 0; i < a_length; ++i) + if (object_diff(a[i], b[i])) + return true + return false + } + for (key in a) + if (object_diff(a[key], b[key])) + return true + for (key in b) + if (!(key in a)) + return true + return false + } + return true +} + +// Array remove and insert (faster than splice) + +function array_delete(array, index) { + var i, n = array.length + for (i = index + 1; i < n; ++i) + array[i - 1] = array[i] + array.length = n - 1 +} + +function array_delete_item(array, item) { + var i, n = array.length + for (i = 0; i < n; ++i) + if (array[i] === item) + return array_delete(array, i) +} + +function array_insert(array, index, item) { + for (var i = array.length; i > index; --i) + array[i] = array[i - 1] + array[index] = item +} + +function array_delete_pair(array, index) { + var i, n = array.length + for (i = index + 2; i < n; ++i) + array[i - 2] = array[i] + array.length = n - 2 +} + +function array_insert_pair(array, index, key, value) { + for (var i = array.length; i > index; i -= 2) { + array[i] = array[i-2] + array[i+1] = array[i-1] + } + array[index] = key + array[index+1] = value +} + +// Set as plain sorted array + +function set_clear(set) { + set.length = 0 +} + +function set_has(set, item) { + var a = 0 + var b = set.length - 1 + while (a <= b) { + var m = (a + b) >> 1 + var x = set[m] + if (item < x) + b = m - 1 + else if (item > x) + a = m + 1 + else + return true + } + return false +} + +function set_add(set, item) { + var a = 0 + var b = set.length - 1 + // optimize fast case of appending items in order + if (item > set[b]) { + set.push(item) + return + } + while (a <= b) { + var m = (a + b) >> 1 + var x = set[m] + if (item < x) + b = m - 1 + else if (item > x) + a = m + 1 + else + return + } + array_insert(set, a, item) +} + +function set_delete(set, item) { + var a = 0 + var b = set.length - 1 + while (a <= b) { + var m = (a + b) >> 1 + var x = set[m] + if (item < x) + b = m - 1 + else if (item > x) + a = m + 1 + else { + array_delete(set, m) + return + } + } +} + +function set_toggle(set, item) { + var a = 0 + var b = set.length - 1 + while (a <= b) { + var m = (a + b) >> 1 + var x = set[m] + if (item < x) + b = m - 1 + else if (item > x) + a = m + 1 + else { + array_delete(set, m) + return + } + } + array_insert(set, a, item) +} + +// Map as plain sorted array of key/value pairs + +function map_clear(map) { + map.length = 0 +} + +function map_has(map, key) { + var a = 0 + var b = (map.length >> 1) - 1 + while (a <= b) { + var m = (a + b) >> 1 + var x = map[m<<1] + if (key < x) + b = m - 1 + else if (key > x) + a = m + 1 + else + return true + } + return false +} + +function map_get(map, key, missing) { + var a = 0 + var b = (map.length >> 1) - 1 + while (a <= b) { + var m = (a + b) >> 1 + var x = map[m<<1] + if (key < x) + b = m - 1 + else if (key > x) + a = m + 1 + else + return map[(m<<1)+1] + } + return missing +} + +function map_set(map, key, value) { + var a = 0 + var b = (map.length >> 1) - 1 + while (a <= b) { + var m = (a + b) >> 1 + var x = map[m<<1] + if (key < x) + b = m - 1 + else if (key > x) + a = m + 1 + else { + map[(m<<1)+1] = value + return + } + } + array_insert_pair(map, a<<1, key, value) +} + +function map_delete(map, key) { + var a = 0 + var b = (map.length >> 1) - 1 + while (a <= b) { + var m = (a + b) >> 1 + var x = map[m<<1] + if (key < x) + b = m - 1 + else if (key > x) + a = m + 1 + else { + array_delete_pair(map, m<<1) + return + } + } +} + +function map_for_each(map, f) { + for (var i = 0; i < map.length; i += 2) + f(map[i], map[i+1]) +} + +// same as Object.groupBy +function object_group_by(items, callback) { + var item, key + var groups = {} + if (typeof callback === "function") { + for (item of items) { + key = callback(item) + if (key in groups) + groups[key].push(item) + else + groups[key] = [ item ] + } + } else { + for (item of items) { + key = item[callback] + if (key in groups) + groups[key].push(item) + else + groups[key] = [ item ] + } + } + return groups +} + +// like Object.groupBy but for plain array maps +function map_group_by(items, callback) { + var item, key, arr + var groups = [] + if (typeof callback === "function") { + for (item of items) { + key = callback(item) + arr = map_get(groups, key) + if (arr) + arr.push(item) + else + map_set(groups, key, [ item ]) + } + } else { + for (item of items) { + key = item[callback] + arr = map_get(groups, key) + if (arr) + arr.push(item) + else + map_set(groups, key, [ item ]) + } + } + return groups +} diff --git a/public/common/util.js b/public/common/util.js deleted file mode 100644 index 85684b0..0000000 --- a/public/common/util.js +++ /dev/null @@ -1,366 +0,0 @@ -/* COMMON LIBRARY */ - -function clear_undo() { - if (G.undo) { - G.undo.length = 0 - } -} - -function push_undo() { - var copy, k, v - if (G.undo) { - copy = {} - for (k in G) { - v = G[k] - if (k === "undo") - continue - else if (k === "log") - v = v.length - else if (typeof v === "object" && v !== null) - v = object_copy(v) - copy[k] = v - } - G.undo.push(copy) - } -} - -function pop_undo() { - if (G.undo) { - var save_log = G.log - var save_undo = G.undo - G = save_undo.pop() - save_log.length = G.log - G.log = save_log - G.undo = save_undo - } -} - -function random(range) { - // An MLCG using integer arithmetic with doubles. - // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf - // m = 2**35 − 31 - return (G.seed = G.seed * 200105 % 34359738337) % range -} - -function random_bigint(range) { - // Largest MLCG that will fit its state in a double. - // Uses BigInt for arithmetic, so is an order of magnitude slower. - // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf - // m = 2**53 - 111 - return (G.seed = Number(BigInt(G.seed) * 5667072534355537n % 9007199254740881n)) % range -} - -function shuffle(list) { - // Fisher-Yates shuffle - var i, j, tmp - for (i = list.length - 1; i > 0; --i) { - j = random(i + 1) - tmp = list[j] - list[j] = list[i] - list[i] = tmp - } -} - -function shuffle_bigint(list) { - // Fisher-Yates shuffle - var i, j, tmp - for (i = list.length - 1; i > 0; --i) { - j = random_bigint(i + 1) - tmp = list[j] - list[j] = list[i] - list[i] = tmp - } -} - -// Fast deep copy for objects without cycles -function object_copy(original) { - var copy, i, n, v - if (Array.isArray(original)) { - n = original.length - copy = new Array(n) - for (i = 0; i < n; ++i) { - v = original[i] - if (typeof v === "object" && v !== null) - copy[i] = object_copy(v) - else - copy[i] = v - } - return copy - } else { - copy = {} - for (i in original) { - v = original[i] - if (typeof v === "object" && v !== null) - copy[i] = object_copy(v) - else - copy[i] = v - } - return copy - } -} - -// Array remove and insert (faster than splice) - -function array_remove(array, index) { - var i, n = array.length - for (i = index + 1; i < n; ++i) - array[i - 1] = array[i] - array.length = n - 1 -} - -function array_remove_item(array, item) { - var i, n = array.length - for (i = 0; i < n; ++i) - if (array[i] === item) - return array_remove(array, i) -} - -function array_insert(array, index, item) { - for (var i = array.length; i > index; --i) - array[i] = array[i - 1] - array[index] = item -} - -function array_remove_pair(array, index) { - var i, n = array.length - for (i = index + 2; i < n; ++i) - array[i - 2] = array[i] - array.length = n - 2 -} - -function array_insert_pair(array, index, key, value) { - for (var i = array.length; i > index; i -= 2) { - array[i] = array[i-2] - array[i+1] = array[i-1] - } - array[index] = key - array[index+1] = value -} - -// Set as plain sorted array - -function set_clear(set) { - set.length = 0 -} - -function set_has(set, item) { - var a = 0 - var b = set.length - 1 - while (a <= b) { - var m = (a + b) >> 1 - var x = set[m] - if (item < x) - b = m - 1 - else if (item > x) - a = m + 1 - else - return true - } - return false -} - -function set_add(set, item) { - var a = 0 - var b = set.length - 1 - while (a <= b) { - var m = (a + b) >> 1 - var x = set[m] - if (item < x) - b = m - 1 - else if (item > x) - a = m + 1 - else - return - } - array_insert(set, a, item) -} - -function set_delete(set, item) { - var a = 0 - var b = set.length - 1 - while (a <= b) { - var m = (a + b) >> 1 - var x = set[m] - if (item < x) - b = m - 1 - else if (item > x) - a = m + 1 - else { - array_remove(set, m) - return - } - } -} - -function set_toggle(set, item) { - var a = 0 - var b = set.length - 1 - while (a <= b) { - var m = (a + b) >> 1 - var x = set[m] - if (item < x) - b = m - 1 - else if (item > x) - a = m + 1 - else { - array_remove(set, m) - return - } - } - array_insert(set, a, item) -} - -// Map as plain sorted array of key/value pairs - -function map_clear(map) { - map.length = 0 -} - -function map_has(map, key) { - var a = 0 - var b = (map.length >> 1) - 1 - while (a <= b) { - var m = (a + b) >> 1 - var x = map[m<<1] - if (key < x) - b = m - 1 - else if (key > x) - a = m + 1 - else - return true - } - return false -} - -function map_get(map, key, missing) { - var a = 0 - var b = (map.length >> 1) - 1 - while (a <= b) { - var m = (a + b) >> 1 - var x = map[m<<1] - if (key < x) - b = m - 1 - else if (key > x) - a = m + 1 - else - return map[(m<<1)+1] - } - return missing -} - -function map_set(map, key, value) { - var a = 0 - var b = (map.length >> 1) - 1 - while (a <= b) { - var m = (a + b) >> 1 - var x = map[m<<1] - if (key < x) - b = m - 1 - else if (key > x) - a = m + 1 - else { - map[(m<<1)+1] = value - return - } - } - array_insert_pair(map, a<<1, key, value) -} - -function map_delete(map, key) { - var a = 0 - var b = (map.length >> 1) - 1 - while (a <= b) { - var m = (a + b) >> 1 - var x = map[m<<1] - if (key < x) - b = m - 1 - else if (key > x) - a = m + 1 - else { - array_remove_pair(map, m<<1) - return - } - } -} - -function map_for_each(map, f) { - for (var i = 0; i < map.length; i += 2) - f(map[i], map[i+1]) -} - -function object_diff(a, b) { - var i, key - var a_length - if (a === b) - return false - if (a !== null && b !== null && typeof a === "object" && typeof b === "object") { - if (Array.isArray(a)) { - if (!Array.isArray(b)) - return true - a_length = a.length - if (b.length !== a_length) - return true - for (i = 0; i < a_length; ++i) - if (object_diff(a[i], b[i])) - return true - return false - } - for (key in a) - if (object_diff(a[key], b[key])) - return true - for (key in b) - if (!(key in a)) - return true - return false - } - return true -} - -// same as Object.groupBy -function object_group_by(items, callback) { - var item, key - var groups = {} - if (typeof callback === "function") { - for (item of items) { - key = callback(item) - if (key in groups) - groups[key].push(item) - else - groups[key] = [ item ] - } - } else { - for (item of items) { - key = item[callback] - if (key in groups) - groups[key].push(item) - else - groups[key] = [ item ] - } - } - return groups -} - -function map_group_by(items, callback) { - var item, key, arr - var groups = [] - if (typeof callback === "function") { - for (item of items) { - key = callback(item) - arr = map_get(groups, key) - if (arr) - arr.push(item) - else - map_set(groups, key, [ item ]) - } - } else { - for (item of items) { - key = item[callback] - arr = map_get(groups, key) - if (arr) - arr.push(item) - else - map_set(groups, key, [ item ]) - } - } - return groups -} |