Newer
Older
/* eslint no-unused-vars: [2, { "vars": "local" }] */
/* This file contains only JS functions without dependencies on FC specific variables/conventions and do not rely on
* custom functions outside this file
*/
/**
if (typeof x === "undefined" || x === null || x === undefined) {
return false;
}
return true;
/**
* @param {number} n
* @returns {boolean}
*/
return Number.isFinite(n) && Math.floor(n) !== n;
/**
* Determines if a is between low and high
* @param {number} a
* @param {number} low
* @param {number} high
* @param {"exclusive"|"inclusive"} [mode='exclusive'] defaults to 'exclusive' but also supports 'inclusive'.
if (low === null) { low = -Infinity; }
if (high === null) { high = Infinity; }
if (mode === 'exclusive') {
return a > low && a < high;
} else if (mode === 'inclusive') {
return a >= low && a <= high;
}
* @template {PropertyKey} T
* @param {Object.<T, number>} obj
* @returns {T}
let randint = Math.floor(Math.random() * hashSum(obj));
let ret;
Object.keys(obj).some((key) => {
if (randint < obj[key]) {
ret = key;
return true;
} else {
randint -= obj[key];
return false;
}
});
return ret;
let sum = 0;
Object.keys(obj).forEach((key) => {
sum += obj[key];
});
return sum;
* @template {PropertyKey} T
* @param {T[]} arr
* @returns {Object.<T, number>}
const obj = {};
arr.forEach((item) => {
obj[item] = 1;
});
return obj;
* @param {object} object
* @param {PropertyKey[]} rest
rest.forEach((item) => {
if (object[item] === undefined) {
object[item] = 1;
} else {
object[item] += 1;
}
});
/**
* @param {Object.<PropertyKey, number>} obj
* @param {Object.<PropertyKey, number>} other
*/
function hashMerge(obj, other) {
Object.keys(other).forEach((key) => {
if (obj[key] === undefined) {
obj[key] = other[key];
} else {
obj[key] += other[key];
}
});
}
* @template {PropertyKey} T
* @param {T[]} array
* @returns {Object.<T, number>}
const obj = {};
array.forEach((item) => {
if (obj[item] === undefined) {
obj[item] = 1;
} else {
obj[item] += 1;
}
});
return obj;
/**
* generate a random, almost unique ID that is compliant (possibly) with RFC 4122
*
let date = Date.now(); // high-precision timer
let uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
let r = (date + Math.random() * 16) % 16 | 0;
date = Math.floor(date / 16);
return (c === "x" ? r : (r & 0x3 | 0x8)).toString(16);
});
return uuid;
/**
* @param {Array} array
* @param {number} indexA
* @param {number} indexB
*/
const tmp = array[indexA];
array[indexA] = array[indexB];
array[indexB] = tmp;
/**
* @param {string} string
* @returns {string}
*/
return string.charAt(0).toUpperCase() + string.substr(1);
/**
* @param {string} string
* @returns {string}
*/
function uncapFirstChar(string) {
return string.charAt(0).toLowerCase() + string.substr(1);
}
/**
* @param {string} word
* @returns {string}
*/
let vocal = ['a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'];
if (vocal.includes(word.charAt(0))) {
return `an ${word}`;
}
return `a ${word}`;
Object.defineProperty(Array.prototype, 'toStringExt', {
/**
* @param {string} delimiter
* @param {string} lastDelimiter
* @returns {string}
*/
value(delimiter = ', ', lastDelimiter = ' and ') {
if (this == null) {
throw new TypeError('Array.prototype.toStringExt called on null or undefined');
}
if (this.length === 0) {
return "none";
}
if (this.length === 1) {
return this[0].toString();
}
const last = this.pop();
let r = this.join(delimiter);
this.push(last); // don't leave the array modified
return `${r}${lastDelimiter}${last}`;
/**
* @param {number} i
* @returns {string}
*/
let j = i % 10;
let k = i % 100;
if (j === 1 && k !== 11) {
return `${i}st`;
}
if (j === 2 && k !== 12) {
return `${i}nd`;
}
if (j === 3 && k !== 13) {
return `${i}rd`;
}
return `${i}th`;
/**
* @param {number} i
* @returns {string}
*/
const text = ["zeroth", "first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", "ninth", "tenth", "eleventh", "twelfth", "thirteenth", "fourteenth", "fifteenth", "sixteenth", "seventeenth", "eighteenth", "nineteenth"];
if (i < text.length) {
return text[i];
}
return ordinalSuffix(i);
* @template T
* @param {Iterable<T>} array
* @returns {T[]}
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
/**
* Maps an index from one list onto a matching index on the other.
* The first and last indexes will be matched to the first and last indexes of the other list,
* while indexes in between will go to the nearest index.
* @param {number} index The index in original list to map to new list.
* @param {*} originalList The original list the index refers into.
* @param {*} newList The new list which we want an index for
* @returns {number} The new index into newList
*/
App.Utils.mapIndexBetweenLists = function(index, originalList, newList) {
if (index === 0) { return 0; }
if (index === originalList.length - 1) { return newList.length - 1; }
index--;
const originalLimitedLength = originalList.length - 2;
const newLimitedLength = newList.length - 2;
return Math.round((index / originalLimitedLength) * newLimitedLength) + 1;
};
/**
* replaces special HTML characters with their '&xxx' forms
* @param {string} text
* @returns {string}
*/
App.Utils.escapeHtml = function(text) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return text.replace(/[&<>"']/g, m => map[m]);
};
/**
* Creates an object where the items are accessible via their ids.
*
* @param {Iterable} list
let mappedList = {};
for (const item of list) {
mappedList[item.id] = item;
}
return mappedList;
/**
* Topological sorting algorithm
* https://gist.github.com/shinout/1232505
* Added keys parameter since it better suits our needs and updated to project code style.
*
* @template {PropertyKey} T
* @param {T[]} keys
* @param {T[][]} edges list of edges. each edge forms Array<ID,ID> e.g. [12, 3]
* @returns {T[]} Array: topological sorted list of IDs
* @throws Error: in case there is a closed chain.
**/
App.Utils.topologicalSort = function(keys, edges) {
class Node {
/** @param {T} id */
constructor(id) {
this.id = id;
/** @type {T[]} */
this.afters = [];
}
}
/** @type {Object.<T, Node>} */
let nodes = {}; // hash: stringified id of the node => { id: id, afters: list of ids }
let sorted = []; // sorted list of IDs ( returned value )
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
let visited = {}; // hash: id of already visited node => true
// 1. build data structures
keys.forEach(key => {
nodes[key] = new Node(key);
});
edges.forEach(edge => {
const from = edge[0], to = edge[1];
if (!nodes[from]) { nodes[from] = new Node(from); }
if (!nodes[to]) { nodes[to] = new Node(to); }
nodes[from].afters.push(to);
});
// 2. topological sort
Object.keys(nodes).forEach(function visit(idstr, ancestors) {
let node = nodes[idstr];
let id = node.id;
// if already exists, do nothing
if (visited[idstr]) { return; }
if (!Array.isArray(ancestors)) { ancestors = []; }
ancestors.push(id);
visited[idstr] = true;
node.afters.forEach((afterID) => {
if (ancestors.indexOf(afterID) >= 0) { // if already in ancestors, a closed chain exists.
throw new Error('closed chain : ' + afterID + ' is in ' + id);
}
visit(afterID.toString(), ancestors.map(v => v)); // recursive call
});
sorted.unshift(id);
});
return sorted;
};
/**
* Sorts an array of objects based on the order of the first array.
* Sorts by values accessible by unsorted[key]
* Values of the second array must be a subset of sorted
*
* @param {Array<any>} sorted
* @param {Array<object>} unsorted
* @param {string} key
* @returns {Array<object>}
*/
function sortArrayByArray(sorted, unsorted, key) {
const map = {};
sorted.forEach((value, index) => {
map[value] = index;
});
return unsorted.sort((a, b) => {
return map[a[key]] - map[b[key]];
});
}
/**
* @param {object} target
* @param {object} source
*/
function deepAssign(target, source) {
function isObject(o) {
return (o !== undefined && o !== null && typeof o === 'object' && !Array.isArray(o));
}
if (isObject(target) && isObject(source)) {
for (const key in source) {
if (!source.hasOwnProperty(key)) { continue; }
if (isObject(source[key])) {
if (!target.hasOwnProperty(key) || !isObject(target[key])) {
target[key] = {};
}
deepAssign(target[key], source[key]);
} else {
Object.assign(target, {
[key]: source[key]
});
}
}
}
}
* For more information about mean vs. median see
* https://www.clinfo.eu/mean-median/
* @param {number[]} arr Does not need to be sorted
const mid = Math.floor(arr.length / 2);
const nums = [...arr].sort((a, b) => a - b);