Home Reference Source Repository

lib/lseqnode.js

'use strict';

const Triple = require('./triple.js');

/**
 * A node of the LSeq tree.
 */
class LSeqNode {
    /**
     * @param {Triple[]} triples The list of triples composing the path to the
     * element.
     * @param {Object} element The element to insert in the structure, e.g., a
     * character in a text document.
     */
    constructor (triples = [], element = null) {
        this.t = triples.shift();
        this.e = null;
        if (triples.length === 0) { this.e = element; };
        this.subCounter = (triples.length > 0 && 1) || 0;
        this.children = [];
        triples.length > 0 &&
            this.children.push(new LSeqNode(triples, element));
    };
    
    /**
     * Getter to the first child.
     * @returns {LSeqNode} The first child of this node. Null if it does not
     * exists.
     */
    get child () {
        return ((this.children.length > 0) && this.children[0]) || null;
    };

    /**
     * Comparator between to LSeqNodes.
     * @param {LSeqNode} o The other LSeqNode to compare to.
     */
    compareTo (o) {
        return this.t.compareTo(o.t);
    };
    
    /**
     * Add a node to the current node.
     * @param {LSeqNode} node The node to add as a children of this node.
     * @return {Boolean} False if the element already exists, True otherwise.
     */
    add (node) {
        const index = this._binaryIndexOf(node);
        
        // #1 if the path do no exist, create it
        if (!this._contains(node)) {
            this.children.splice(-index, 0, node);
            this.subCounter += 1;
            // #2 otherwise, continue to explore the subtrees
        } else if (node.children.length === 0) {
            // #2a check if the element already exists
            if (this.children[index].e !== null){
                return false;
            } else {
                this.children[index].e = node.e;
                this.subCounter += 1;
            };
            // #3 if didnot exist, increment the counter
        } else if (this.children[index].add(node.child)) {
            this.subCounter += 1;
        };
        return true;
    };


    /**
     * Remove the node of the tree and all node within path being useless.
     * @param {LSeqNode} node the node containing the path to remove
     * @return {Boolean} True if the node has been removed, False if it does not
     * exist.
     */
    del (node) {
        const indexes = this._getIndexes(node);
        let currentTree = this, i = 0, isSplitted = false;

        // #1 The element does not exists, stop
        if (indexes.length === 0) { return false; };

        // #2 Crawl the path and remove the element
        currentTree.subCounter -= 1;
        while (i < indexes.length && !(isSplitted)) {
            let isLast = currentTree.children[indexes[i]]._hasElement &&
                i === indexes.length - 1;
            if (!isLast) {
                currentTree.children[indexes[i]].subCounter -= 1;     
            };
            if (currentTree.children[indexes[i]].subCounter <= 0 &&
                (!currentTree.children[indexes[i]]._hasElement || isLast)) {
                currentTree.children.splice(indexes[i], 1);
                isSplitted = true;
            };
            currentTree = currentTree.children[indexes[i]];
            ++i;
        };
        if (!isSplitted){ currentTree.e = null;};

        return true;
    };


    /**
     * The ordered tree can be linearized into a sequence. This function get the
     * index of the path represented by the list of triples.
     * @param {LSeqNode} node The node containing -- at least -- the path to the
     * element.
     * @return {Number} The index of the node in the linearized sequence; -1 if
     * the element does not exist.
     */
    indexOf (node) {
        const indexes = this._getIndexes(node);
        let sum = 0, currentTree = this, j;
        
        // #1 If the node does not exist, stop
        if (indexes.length === 0) { return -1; };

        // #2 Otherwise, start counting
        if (currentTree._hasElement) { sum += 1; };
        
        for (let i = 0; i < indexes.length; ++i) {
            if (indexes[i] < currentTree.children.length/2) {
                // #A start from the beginning [---->|     ]
                j = 0;
                while (j < indexes[i]) {
                    if (currentTree.children[j]._hasElement) { sum += 1; };
                    sum += currentTree.children[j].subCounter;
                    ++j;
                };
            } else {
                // #B start from the end [     |<----]
                sum += currentTree.subCounter;
                j = currentTree.children.length - 1;
                while (j >= indexes[i]) {
                    if (currentTree.children[j]._hasElement){ sum -= 1; };
                    sum -= currentTree.children[j].subCounter;
                    --j;
                };
                j += 1;
            };
            if (currentTree.children[j]._hasElement) { sum += 1; };
            currentTree = currentTree.children[j];
        };
        return sum - 1; // -1 because algorithm counted the element itself
    };


    /**
     * The ordered tree can be linearized. This function gets the node at the
     * index in the projected sequence.
     * @param {Number} index The index in the sequence.
     * @return {LSeqNode} The node at the index.
     */
    get (index) {

        /**
         * @param {Number} leftSum The sum of all element at the left of the
         * current inspected node.
         * @param {LSeqNode} buildingNode The head part of the node being built
         * as we crawl.
         * @param {LSeqNode} queue The queue part of the node being built.
         * @param {LSeqNode} currentNode The subtree being crawled.
         */
        const _get = (leftSum, buildingNode, queue, currentNode) => {
            let startBeginning = true, useFunction, i = 0, p, temp;
            // #0 The node is found, return the incrementally built node and
            // praise the sun !
            if (leftSum === index && currentNode._hasElement) {
                // 1a copy the value of the element in the path
                queue.e = currentNode.e;
                return buildingNode;
            };
            if (currentNode._hasElement){ leftSum += 1; };
            
            // #1 search: do I start from the beginning or the end
            startBeginning = index-leftSum < currentNode.subCounter/2;
            if (startBeginning) {
                useFunction = (a, b) => a + b;
            } else {
                leftSum += currentNode.subCounter;
                useFunction = (a, b) => a - b;
            }
            
            // #2a counting the element from left to right
            if (!startBeginning) { i = currentNode.children.length - 1; };
            while ((startBeginning && leftSum <= index) ||
                   (!startBeginning && leftSum > index)) {
                if (currentNode.children[i]._hasElement) {
                    leftSum = useFunction(leftSum, 1);
                };
                leftSum = useFunction(leftSum,
                                      currentNode.children[i].subCounter);
                i = useFunction(i, 1);
            };
            
            // #2b decreasing the incrementation
            i = useFunction(i, -1);
            if (startBeginning) {
                if (currentNode.children[i]._hasElement) {
                    leftSum = useFunction(leftSum, -1);
                };
                leftSum = useFunction(leftSum,
                                      -currentNode.children[i].subCounter);
            };
            
            // #3 build path
            p = []; p.push(currentNode.children[i].t);
            if (buildingNode === null) {
                buildingNode = new LSeqNode(p, null);
                queue = buildingNode;
            } else {
                temp = new LSeqNode(p, null);
                queue.add(temp);
                queue = temp;
            };
            return _get(leftSum, buildingNode, queue, currentNode.children[i]);
        };
        return _get(0, null, null, this);
    };

    /**
     * Cast a JSON object to an LSeqNode. 
     * @param {Object} o The JSON object.
     * @return {LSeqNode} An LSeqNode.
     */
    static fromJSON (o) {
        let beingBuilt;

        // #1 leaf
        if (o.children.length === 0){
            beingBuilt = new LSeqNode([new Triple(o.t.p, o.t.s, o.t.c)], o.e);
        } else {
            // #2 branch
            beingBuilt = new LSeqNode([new Triple(o.t.p, o.t.s, o.t.c)]);
            beingBuilt.children.push(LSeqNode.fromJSON(o.children[0]));
        };
        
        return beingBuilt;
    };
    
    /**
     * @private Get the list of indexes of the arrays representing the children
     * in the tree.  
     * @param {LSeqNode} node The node containing the path.
     * @return {Number[]} The successive indexes to get to the node. An empty
     * list if the node does not exist.
     */
    _getIndexes (node) {
        const __getIndexes = (indexes, currentTree, currentNode) => {
            if (!currentTree._contains(currentNode)) {
                return [];
            };
            
            const index = currentTree._binaryIndexOf(currentNode);
            
            indexes.push(index);
            
            return ((currentNode.children.length === 0 ||
                     currentTree.children.length === 0) && indexes) ||
                __getIndexes(indexes,
                             currentTree.children[index],
                             currentNode.child);            
        };
        
        return __getIndexes([], this, node);
    };
    
    


    /**
     * @private from: [https://gist.github.com/Wolfy87/5734530] Performs a
     * binary search on the host array.
     * @param {LSeqNode} searchElement The item to search for within the array.
     * @return {Number} The index of the element which defaults to -1 when not
     * found.
     */
    _binaryIndexOf (searchElement) {
        let minIndex = 0;
        let maxIndex = this.children.length - 1;
        let currentIndex;
        let currentElement;
        
        while (minIndex <= maxIndex) {
            currentIndex = Math.floor((minIndex + maxIndex) / 2);
            currentElement = this.children[currentIndex];
            if (currentElement.compareTo(searchElement) < 0) {
                minIndex = currentIndex + 1;
            } else if (currentElement.compareTo(searchElement) > 0) {
                maxIndex = currentIndex - 1;
            } else {
                return currentIndex;
            };
        };
        return ~maxIndex;
    };

    /**
     * @private Check whether this node contains the searchElement as children.
     * @param {LSeqNode} searchElement The element to look for.
     * @return {Boolean} True if this node contains the node in its
     * children, False otherwise.
     */
    _contains (searchElement) {
        const index = this._binaryIndexOf(searchElement);
        return this.children.length > 0 &&
            (index > 0 ||
             ((index === 0) &&
              this.child.compareTo(searchElement) === 0));
    };

    /**
     * @private Check if the node contains an element.
     * @return {Boolean} True if the node has an element, false otherwise.
     */
    get _hasElement () {
        return this.e !== null;
    };

    /**
     * Check if the node has children.
     * @return {Boolean} True if the node has children, false otherwise.
     */
    get isLeaf () {
        return this.children.length === 0;
    };
    
};

module.exports = LSeqNode;