summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2025-04-16 10:25:23 +0200
committerTor Andersson <tor@ccxvii.net>2025-04-25 16:06:05 +0200
commitb431c667423dc9f3717610170b65d6193a66f614 (patch)
tree6bacf24924da432088be5f5a90439b3a79318fc5
parentab1d804788daa053d1624dfad87ae024bef9ef02 (diff)
downloadserver-b431c667423dc9f3717610170b65d6193a66f614.tar.gz
Add common/framework.js
-rw-r--r--public/common/framework.js851
-rw-r--r--public/common/util.js366
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
-}