Source: helper.js

/**
 * @file Helper functions.
 * @version March 26, 2017
 *
 * @author Olivier Pirson --- http://www.opimedia.be/
 * @license GPLv3 --- Copyright (C) 2017 Olivier Pirson
 */

/**
 * Returns true iff arrays are equal.
 *
 * @param {Array} arrayA
 * @param {Array} arrayB
 * @param {function} compareFct: function (a, b) --> number
 *
 * @returns {Boolean}
 */
function arrayIsEquals(arrayA, arrayB, compareFct=compare) {
    assert(arrayA instanceof Array, arrayA);
    assert(arrayB instanceof Array, arrayB);
    assert(typeof compareFct === "function", compareFct);

    if (arrayA.length !== arrayB.length) {
        return false;
    }
    else {
        for (let i = 0; i < arrayA.length; ++i) {
            if (compareFct(arrayA[i], arrayB[i]) !== 0) {
                return false;
            }
        }

        return true;
    }
}


/**
 * Remove from the array
 * the first item equals to value according to compareFct.
 *
 * Returns true iff one item was removed.
 *
 * @param {Array} array
 * @param value
 * @param {function} compareFct: function (a, b) --> number
 *
 * @returns {Boolean}
 */
function arrayRemoveFirst(array, value, compareFct=compare) {
    assert(array instanceof Array, array);
    assert(typeof compareFct === "function", compareFct);

    for (let i = 0; i < array.length; ++i) {
        if (compareFct(value, array[i]) === 0) {
            array.splice(i, 1);

            return true;
        }
    }

    return false;
}


/**
 * Shuffle the array (in place)
 * by Knuth algorithm.
 *
 * @param {Array} array
 */
function arrayShuffle(array) {
    "use strict";

    assert(array instanceof Array, array);

    for (let i = 0; i < array.length; ++i) {
        const j = getRandomInteger(0, array.length);
        const tmp = array[i];

        array[i] = array[j];
        array[j] = tmp;
    }
}


/**
 * If a < 0 then return -1,
 * if a = 0 then return  0,
 * if a > 0 then return  1.
 *
 * @param a object that can be compared with <
 * @param b object that can be compared with <
 *
 * @returns {integer} -1, 0, 1
 */
function compare(a, b) {
    "use strict";

    return (b < a
            ? 1
            : (a < b
               ? -1
               : 0));
}


/**
 * Add the CSS class name to a HTML element (if doesn't exist yet).
 *
 * @param {HTMLElement} htmlElement
 * @param {String} name !== ""
 */
function cssAddClass(htmlElement, name) {
    "use strict";

    assert(htmlElement instanceof HTMLElement, htmlElement);

    assert(typeof name === "string", name);
    assert(name !== "", name);

    const classes = (typeof htmlElement.className === "string"
                     ? htmlElement.className.split(/\s+/)
                     : []);

    for (let i = 0; i < classes.length; ++i) {
        if (classes[i] === name) {
            return;
        }
    }
    classes.push(name);

    htmlElement.className = classes.join(" ");
}


/**
 * Returns true iff the HTML element has the CSS class name.
 *
 * @param {HTMLElement} htmlElement
 * @param {String} name !== ""
 *
 * @returns {Boolean}
 */
function cssHasClass(htmlElement, name) {
    "use strict";

    assert(htmlElement instanceof HTMLElement, htmlElement);

    assert(typeof name === "string", name);
    assert(name !== "", name);

    const classes = new Set(typeof htmlElement.className === "string"
                            ? htmlElement.className.split(/\s+/)
                            : []);

    return classes.has(name);
}


/**
 * If the HTML element has the CSS class name
 * then remove it,
 * else add it.
 *
 * @param {HTMLElement} htmlElement
 * @param {String} name !== ""
 */
function cssInvertClass(htmlElement, name) {
    "use strict";

    assert(htmlElement instanceof HTMLElement, htmlElement);

    assert(typeof name === "string", name);
    assert(name !== "", name);

    cssSetClass(htmlElement, name, !cssHasClass(htmlElement, name));
}


/**
 * Remove the CSS class name to a HTML element (if exist).
 *
 * @param {HTMLElement} htmlElement
 * @param {String} name !== ""
 */
function cssRemoveClass(htmlElement, name) {
    "use strict";

    assert(htmlElement instanceof HTMLElement, htmlElement);

    assert(typeof name === "string", name);
    assert(name !== "", name);

    const classes = (typeof htmlElement.className === "string"
                     ? htmlElement.className.split(/\s+/)
                     : []);

    for (let i = 0; i < classes.length; ++i) {
        if (classes[i] === name) {
            classes.splice(i, 1);
        }
    }

    htmlElement.className = classes.join(" ");
}


/**
 * Add or remove a CSS class to a HTML element.
 *
 * @param {HTMLElement} htmlElement
 * @param {String} name !== ""
 * @param {Boolean} add
 */
function cssSetClass(htmlElement, name, add) {
    "use strict";

    assert(htmlElement instanceof HTMLElement, htmlElement);

    assert(typeof name === "string", name);
    assert(name !== "", name);

    assert(typeof add === "boolean", add);

    if (add) {
        cssAddClass(htmlElement, name);
    }
    else {
        cssRemoveClass(htmlElement, name);
    }
}


/**
 * Returns a number between min (included) and max (excluded),
 * choiced randomly.
 *
 * @param {Number} min
 * @param {Number} max
 *
 * @returns {Number}
 */
function getRandom(min, max) {
    "use strict";

    assert(typeof min === "number", min);
    assert(typeof max === "number", max);
    assert(min < max, min, max);

    return Math.random()*(max - min) + min;
}


/**
 * Returns false or true,
 * choiced randomly.
 *
 * @returns {Boolean}
 */
function getRandomBoolean() {
    "use strict";

    return (getRandomInt(0, 2) !== 0);
}


/**
 * Returns a integer between min (included) and max (excluded),
 * choiced randomly.
 *
 * @param {Number} min
 * @param {Number} max
 *
 * @returns {integer}
 */
function getRandomInteger(min, max) {
    "use strict";

    assert(typeof min === "number", min);
    assert(typeof max === "number", max);
    assert(min < max, min, max);

    return Math.floor(getRandom(min, max));
}


/**
 * Returns true iff |x - y| <= max(|x|, |y|)*epsilon.
 *
 * Cf. "Relative epsilon comparisons" in
 * https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/
 *
 * @param {Number} x
 * @param {Number} y
 * @param {Number} epsilon >= 0
 *
 * @returns {Boolean}
 */
function isFloatAlmostEqual(x, y, epsilon=0.0001) {
    assert(typeof x === "number", x);
    assert(typeof y === "number", y);

    assert(typeof epsilon === "number", epsilon);
    assert(epsilon >= 0, epsilon);

    return (Math.abs(x - y) <= Math.max(Math.abs(x), Math.abs(y)) * epsilon);
}


/**
 * Returns true iff |x| <= epsilon.
 *
 * @param {Number} x
 * @param {Number} epsilon >= 0
 *
 * @returns {Boolean}
 */
function isFloat0(x, epsilon=0.000001) {
    assert(typeof x === "number", x);

    assert(typeof epsilon === "number", epsilon);
    assert(epsilon >= 0, epsilon);

    return (Math.abs(x) <= epsilon);
}


/**
 * Returns true iff |x - y| <= epsilon.
 *
 * @param {Number} x
 * @param {Number} y
 * @param {Number} epsilon >= 0
 *
 * @returns {Boolean}
 */
function isFloatEqual(x, y, epsilon=0.000001) {
    assert(typeof x === "number", x);
    assert(typeof y === "number", y);

    assert(typeof epsilon === "number", epsilon);
    assert(epsilon >= 0, epsilon);

    return (Math.abs(x - y) <= epsilon);
}


/**
 * Returns "s" if n >= 2
 * else return "".
 *
 * @param {Number} n
 *
 * @returns {String}
 */
function s(n) {
    "use strict";

    return (n >= 2
            ? "s"
            : "");
}


/**
 * Returns a new set union of setA and setB.
 *
 * @param {Set} setA
 * @param {Set} setB
 *
 * @returns {Set}
 */
function setUnion(setA, setB) {
    "use strict";

    assert(setA instanceof Set, setA);
    assert(setB instanceof Set, setB);

    const union = new Set(setA);

    for (let item of setB) {
        union.add(item);
    }

    return union;
}


/**
 * Returns the insertion point of the value in the array
 * to maintain sorted order according to compareFct.
 *
 * Like
 * https://docs.python.org/3/library/bisect.html#bisect.bisect_left
 *
 * @param {Array} sorted Array according to compareFct
 * @param value
 * @param {function} compareFct: function (a, b) --> boolean
 *
 * @returns {integer} 0 <= integer <= array.length
 */
function sortedArrayBisectionLeft(array, value, compareFct=compare) {
    assert(array instanceof Array, array);
    assert(typeof compareFct === "function", compareFct);

    var first = 0;
    var afterLast = array.length;
    var middle;

    while (first < afterLast) {
        const middle = Math.floor((first + afterLast)/2);

        if (compareFct(array[middle], value) < 0) {
            first = middle + 1;
        }
        else {
            afterLast = middle;
        }
    }

    return first;
}


/**
 * Returns the insertion point of the value in the array
 * to maintain sorted order according to compareFct.
 *
 * Like
 * https://docs.python.org/3/library/bisect.html#bisect.insort_left
 *
 * @param {Array} sorted Array according to compareFct
 * @param value
 * @param {function} compareFct: function (a, b) --> number
 */
function sortedArrayInsert(array, value, compareFct=compare) {
    assert(array instanceof Array, array);
    assert(typeof compareFct === "function", compareFct);

    var first = 0;
    var afterLast = array.length;
    var middle;

    while (first < afterLast) {
        const middle = Math.floor((first + afterLast)/2);

        if (compareFct(array[middle], value) < 0) {
            first = middle + 1;
        }
        else {
            afterLast = middle;
        }
    }

    array.splice(first, 0, value);
}


/**
 * Returns x in a string
 * with left padding to have at least length characters.
 *
 * @param x
 * @param {Number} length > 0
 * @param {String} pad not empty
 */
function strPad(x, length, pad=" ") {
    assert(typeof length === "number", length);
    assert(length > 0, length);

    assert(typeof pad === "string", pad);
    assert(pad !== "");

    x = x.toString();

    if (x.length >= length) {
        return x;
    }
    else {
        const result = pad.repeat(Math.ceil((length - x.length)/pad.length)) + x;

        return result;
    }
}