UNPKG

bali-component-framework

Version:

This library provides a JavaScript based implementation of the Bali Nebula™ Component Framework.

609 lines (538 loc) 17.9 kB
/************************************************************************ * Copyright (c) Crater Dog Technologies(TM). All Rights Reserved. * ************************************************************************ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * * * This code is free software; you can redistribute it and/or modify it * * under the terms of The MIT License (MIT), as published by the Open * * Source Initiative. (See http://opensource.org/licenses/MIT) * ************************************************************************/ 'use strict'; /** * This collection class implements an ordered set of components that does not allow * duplicate items. By default a set orders its items based on the natural ordering * of its items. */ const moduleName = '/bali/collections/Set'; const utilities = require('../utilities'); const abstractions = require('../abstractions'); const agents = require('../agents'); /** * This constructor creates a new set component with optional parameters that are * used to parameterize its type. * * An optional debug argument may be specified that controls the level of debugging that * should be applied during execution. The allowed levels are as follows: * <pre> * 0: no debugging is applied (this is the default value and has the best performance) * 1: log any exceptions to console.error before throwing them * 2: perform argument validation checks on each call (poor performance) * 3: log interesting arguments, states and results to console.log * </pre> * * @param {Object} parameters Optional parameters used to parameterize this set. * @returns {Set} The new set. */ const Set = function(parameters, debug) { abstractions.Collection.call( this, [ moduleName ], ['/bali/libraries/Logical'], parameters, debug ); if (!this.getParameter('$type')) this.setParameter('$type', '/nebula/collections/Set/v1'); // private attributes const comparator = new agents.CanonicalComparator(this.debug); const tree = new RandomizedTree(comparator); this.toArray = function() { const array = []; var node = minimum(tree.root); while (node) { array.push(node.value); node = successor(node); } return array; }; this.getSize = function() { return tree.size; }; this.getIndex = function(item) { if (this.debug > 1) { this.validateArgument('$getIndex', '$item', item, [ '/javascript/Undefined', '/javascript/Boolean', '/javascript/Number', '/javascript/String', '/javascript/Array', '/javascript/Object', '/bali/abstractions/Component' ]); } var index = 0; item = this.componentize(item); index = tree.index(item) + 1; // convert to ordinal based indexing return index; }; this.getItem = function(index) { if (this.debug > 1) { this.validateArgument('$getItem', '$index', index, [ '/javascript/Number' ]); } index = abstractions.Component.normalizedIndex(this,index) - 1; // convert to javascript zero based indexing return tree.node(index).value; }; this.addItem = function(item) { if (this.debug > 1) { this.validateArgument('$addItem', '$item', item, [ '/javascript/Undefined', '/javascript/Boolean', '/javascript/Number', '/javascript/String', '/javascript/Array', '/javascript/Object', '/bali/abstractions/Component' ]); } item = this.componentize(item); return tree.insert(item); }; this.removeItem = function(item) { if (this.debug > 1) { this.validateArgument('$addItem', '$item', item, [ '/javascript/Undefined', '/javascript/Boolean', '/javascript/Number', '/javascript/String', '/javascript/Array', '/javascript/Object', '/bali/abstractions/Component' ]); } item = this.componentize(item); return tree.remove(item); // returns true if removed }; this.removeItems = function(items) { if (this.debug > 1) { this.validateArgument('$removeItems', '$items', items, [ '/javascript/String', '/javascript/Array', '/bali/interfaces/Sequential' ]); } items = this.componentize(items); var count = 0; const iterator = items.getIterator(); while (iterator.hasNext()) { const item = iterator.getNext(); if (this.removeItem(item)) count++; } return count; // returns the number of removed items }; this.emptyCollection = function() { tree.clear(); return this; }; return this; }; Set.prototype = Object.create(abstractions.Collection.prototype); Set.prototype.constructor = Set; exports.Set = Set; // LOGICAL LIBRARY FUNCTIONS /** * This function returns a new set that is the logical NOT of the specified * set. Since this is meaningless this function throws an exception. * * @param {Set} set The set. * @param {Number} debug A number in the range 0..3. * @returns {Set} The resulting set. */ Set.not = function(set, debug) { const exception = new abstractions.Exception({ $module: moduleName, $procedure: '$not', $exception: '$meaningless', $text: '"The logical NOT of a set is meaningless."' }, undefined, debug); throw exception; }; /** * This function returns a new set that contains the items that are in * both the first set and the second set. * * @param {Set} first The first set to be operated on. * @param {Set} second The second set to be operated on. * @param {Number} debug A number in the range 0..3. * @returns {Set} The resulting set. */ Set.and = function(first, second, debug) { if (debug > 1) { abstractions.Component.validateArgument(moduleName, '$and', '$first', first, [ moduleName, ]); abstractions.Component.validateArgument(moduleName, '$and', '$second', second, [ moduleName, ]); } const result = new Set(first.getParameters(), debug); const iterator = first.getIterator(); while (iterator.hasNext()) { const item = iterator.getNext(); if (second.containsItem(item)) { result.addItem(item); } } return result; }; /** * This function returns a new set that contains the items that are in * the first set but not in the second set. * * @param {Set} first The first set to be operated on. * @param {Set} second The second set to be operated on. * @param {Number} debug A number in the range 0..3. * @returns {Set} The resulting set. */ Set.sans = function(first, second, debug) { if (debug > 1) { abstractions.Component.validateArgument(moduleName, '$sans', '$first', first, [ moduleName, ]); abstractions.Component.validateArgument(moduleName, '$sans', '$second', second, [ moduleName, ]); } const result = new Set(first.getParameters(), debug); result.addItems(first); result.removeItems(second); return result; }; /** * This function returns a new set that contains all the items that are in * the first set or the second set or both. * * @param {Set} first The first set to be operated on. * @param {Set} second The second set to be operated on. * @param {Number} debug A number in the range 0..3. * @returns {Set} The resulting set. */ Set.or = function(first, second, debug) { if (debug > 1) { abstractions.Component.validateArgument(moduleName, '$or', '$first', first, [ moduleName, ]); abstractions.Component.validateArgument(moduleName, '$or', '$second', second, [ moduleName, ]); } const result = new Set(first.getParameters(), debug); result.addItems(first); result.addItems(second); return result; }; /** * This function returns a new set that contains all the items that are in * the first set or the second set but not both. * * @param {Set} first The first set to be operated on. * @param {Set} second The second set to be operated on. * @param {Number} debug A number in the range 0..3. * @returns {Set} The resulting set. */ Set.xor = function(first, second, debug) { if (debug > 1) { abstractions.Component.validateArgument(moduleName, '$xor', '$first', first, [ moduleName, ]); abstractions.Component.validateArgument(moduleName, '$xor', '$second', second, [ moduleName, ]); } const result = new Set(first.getParameters(), debug); const comparator = new agents.CanonicalComparator(debug); const iterator1 = first.getIterator(); var item1; const iterator2 = second.getIterator(); var item2; while (iterator1.hasNext() && iterator2.hasNext()) { if (item1 === undefined) item1 = iterator1.getNext(); if (item2 === undefined) item2 = iterator2.getNext(); const signum = comparator.ranking(item1, item2); switch (signum) { case -1: result.addItem(item1); item1 = undefined; break; case 0: item1 = undefined; item2 = undefined; break; case 1: result.addItem(item2); item2 = undefined; break; } } while (iterator1.hasNext()) { item1 = iterator1.getNext(); result.addItem(item1); } while (iterator2.hasNext()) { item2 = iterator2.getNext(); result.addItem(item2); } return result; }; // PRIVATE CLASSES /* * This class implements a randomized self balancing binary search tree composite (treap). * It maintains an ordering of the values in the tree and provides O(log(n)) search and * update performance. */ const RandomizedTree = function(comparator) { // NOTE: we don't want to make these attributes private because of the performance // issues with having each node in the tree have its own local methods. this.size = 0; this.comparator = comparator; return this; }; RandomizedTree.prototype.constructor = RandomizedTree; RandomizedTree.prototype.contains = function(value) { return this.find(value) !== undefined; }; RandomizedTree.prototype.index = function(value) { var index = 0; var candidate = minimum(this.root); while (candidate && !this.comparator.areEqual(candidate.value, value)) { candidate = successor(candidate); index++; } if (candidate) { return index; } else { return -1; } }; RandomizedTree.prototype.node = function(index) { var candidate = minimum(this.root); while (index > 0 && index < this.size) { candidate = successor(candidate); index--; } return candidate; }; RandomizedTree.prototype.insert = function(value) { // handle the empty tree case if (this.root === undefined) { this.root = {value: value, priority: Math.random()}; this.size++; return true; } // find the parent of the new node var parent; var candidate = this.root; while (candidate && candidate.value) { parent = candidate; switch (this.comparator.ranking(candidate.value, value)) { case 1: candidate = candidate.left; break; case 0: // the value is already in the tree return false; case -1: candidate = candidate.right; break; } } // insert the new node as a child of the parent const child = { value: value, parent: parent, priority: Math.random()}; switch (this.comparator.ranking(parent.value, value)) { case 1: parent.left = child; break; case 0: case -1: parent.right = child; break; } this.size++; // rebalance the tree by randomized priorities while (child !== this.root) { parent = child.parent; if (parent.priority < child.priority) { if (child === parent.left) { this.rotateRight(parent); } else { this.rotateLeft(parent); } } else { break; } } return true; }; RandomizedTree.prototype.remove = function(value) { const candidate = this.find(value); if (candidate) { // rotate the candidate down to leaf this.rotateDown(candidate); // then remove it if (candidate.left === undefined) { this.replace(candidate, candidate.right); } else if (candidate.right === undefined) { this.replace(candidate, candidate.left); } else { const successor = minimum(candidate.right); if (successor.parent !== candidate) { this.replace(successor, successor.right); successor.right = candidate.right; successor.right.parent = successor; } this.replace(candidate, successor); successor.left = candidate.left; successor.left.parent = successor; } // clean up candidate.value = undefined; candidate.parent = undefined; candidate.left = undefined; candidate.right = undefined; candidate.priority = undefined; this.size--; return true; } else { // the value was not found in the tree return false; } }; RandomizedTree.prototype.clear = function() { this.root = undefined; this.size = 0; }; RandomizedTree.prototype.find = function(value) { var candidate = this.root; while (candidate && candidate.value) { switch (this.comparator.ranking(candidate.value, value)) { case -1: candidate = candidate.right; break; case 0: return candidate; case 1: candidate = candidate.left; break; } } return candidate; }; RandomizedTree.prototype.replace = function(old, replacement) { if (old.parent === undefined) { this.root = replacement; } else if (old === old.parent.left) { old.parent.left = replacement; } else { old.parent.right = replacement; } if (replacement) { replacement.parent = old.parent; } }; RandomizedTree.prototype.rotateLeft = function(node) { const temporary = node.right; temporary.parent = node.parent; node.right = temporary.left; if (node.right) { node.right.parent = node; } temporary.left = node; node.parent = temporary; if (temporary.parent) { if (node === temporary.parent.left) { temporary.parent.left = temporary; } else { temporary.parent.right = temporary; } } else { this.root = temporary; } }; RandomizedTree.prototype.rotateRight = function(node) { const temporary = node.left; temporary.parent = node.parent; node.left = temporary.right; if (node.left) { node.left.parent = node; } temporary.right = node; node.parent = temporary; if (temporary.parent) { if (node === temporary.parent.left) { temporary.parent.left = temporary; } else { temporary.parent.right = temporary; } } else { this.root = temporary; } }; RandomizedTree.prototype.rotateDown = function(node) { while (true) { if (node.left) { var leftHigherPriority = node.right === undefined || node.left.priority >= node.right.priority; if (leftHigherPriority) { this.rotateRight(node); } else { this.rotateLeft(node); } } else if (node.right) { this.rotateLeft(node); } else { break; } } }; // PRIVATE FUNCTIONS const minimum = function(node) { while (node && node.left) { node = node.left; } return node; }; const maximum = function(node) { while (node && node.right) { node = node.right; } return node; }; const predecessor = function(node) { if (node.left) { // there is a left branch, so the predecessor is the rightmost node of that subtree return maximum(node.left); } else { // it is the lowest ancestor whose right child is also an ancestor of node var current = node; var parent = node.parent; while (parent && current === parent.left) { current = parent; parent = parent.parent; } return parent; } }; const successor = function(node) { if (node.right) { // there is a right branch, so the successor is the leftmost node of that subtree return minimum(node.right); } else { // it is the lowest ancestor whose left child is also an ancestor of node var current = node; var parent = node.parent; while (parent && current === parent.right) { current = parent; parent = parent.parent; } return parent; } };