From fc7e116d54d7416095378d75ecdf4821af4f605b Mon Sep 17 00:00:00 2001 From: Adam Solove Date: Mon, 16 Dec 2013 09:55:56 -0500 Subject: [PATCH 01/83] find() -> findValue() --- collections.cat.js | 6 +++--- collections.min.js | 6 +++--- demo/list-demo.js | 8 ++++---- list.js | 6 +++--- shim-array.js | 6 +++--- sorted-array.js | 2 +- sorted-set.js | 2 +- spec/array-spec.js | 6 +++--- spec/list-spec.js | 34 +++++++++++++++++----------------- spec/order.js | 4 ++-- 10 files changed, 40 insertions(+), 40 deletions(-) diff --git a/collections.cat.js b/collections.cat.js index 2c83a1a..1916aa9 100644 --- a/collections.cat.js +++ b/collections.cat.js @@ -78,7 +78,7 @@ Iterator$$module$iterator.range=function(a,b,c){3>arguments.length&&(c=1);2>argu "undefined"===typeof ReturnValue&&(global.ReturnValue=function(a){if(!(this instanceof global.ReturnValue))return new global.ReturnValue(a);this.value=a});module$iterator.module$exports&&(module$iterator=module$iterator.module$exports);var module$shim_array={},Function$$module$shim_array=module$shim_function,GenericCollection$$module$shim_array=module$generic_collection,GenericOrder$$module$shim_array=module$generic_order,WeakMap$$module$shim_array=module$weak_map;module$shim_array.module$exports=Array;Array.empty=[];Object.freeze&&Object.freeze(Array.empty);Array.from=function(a){var b=[];b.addEach(a);return b};Array.prototype.addEach=GenericCollection$$module$shim_array.prototype.addEach;Array.prototype.deleteEach=GenericCollection$$module$shim_array.prototype.deleteEach; Array.prototype.toArray=GenericCollection$$module$shim_array.prototype.toArray;Array.prototype.toObject=GenericCollection$$module$shim_array.prototype.toObject;Array.prototype.all=GenericCollection$$module$shim_array.prototype.all;Array.prototype.any=GenericCollection$$module$shim_array.prototype.any;Array.prototype.min=GenericCollection$$module$shim_array.prototype.min;Array.prototype.max=GenericCollection$$module$shim_array.prototype.max;Array.prototype.sum=GenericCollection$$module$shim_array.prototype.sum; Array.prototype.average=GenericCollection$$module$shim_array.prototype.average;Array.prototype.only=GenericCollection$$module$shim_array.prototype.only;Array.prototype.flatten=GenericCollection$$module$shim_array.prototype.flatten;Array.prototype.zip=GenericCollection$$module$shim_array.prototype.zip;Array.prototype.sorted=GenericCollection$$module$shim_array.prototype.sorted;Array.prototype.reversed=GenericCollection$$module$shim_array.prototype.reversed; -Array.prototype.constructClone=function(a){var b=new this.constructor;b.addEach(a);return b};Array.prototype.has=function(a,b){return-1!==this.find(a,b)};Array.prototype.get=function(a){if(+a!==a)throw Error("Indicies must be numbers");return this[a]};Array.prototype.set=function(a,b){this.splice(a,1,b);return!0};Array.prototype.add=function(a){this.push(a);return!0};Array.prototype["delete"]=function(a,b){var c=this.find(a,b);return-1!==c?(this.splice(c,1),!0):!1}; +Array.prototype.constructClone=function(a){var b=new this.constructor;b.addEach(a);return b};Array.prototype.has=function(a,b){return-1!==this.findValue(a,b)};Array.prototype.get=function(a){if(+a!==a)throw Error("Indicies must be numbers");return this[a]};Array.prototype.set=function(a,b){this.splice(a,1,b);return!0};Array.prototype.add=function(a){this.push(a);return!0};Array.prototype["delete"]=function(a,b){var c=this.findValue(a,b);return-1!==c?(this.splice(c,1),!0):!1}; Array.prototype.find=function(a,b){for(var b=b||this.contentEquals||Object.equals,c=0;cd&&!(d++,a=a.prev,a==c););return a}return a||b};List$$module$list.prototype.slice=function(a,b){for(var c=[],d=this.head,a=this.scan(a,d.next),b=this.scan(b,d);a!==b&&a!==d;)c.push(a.value),a=a.next;return c}; List$$module$list.prototype.splice=function(a,b){return this.swap(a,b,Array.prototype.slice.call(arguments,2))};List$$module$list.prototype.swap=function(a,b,c){var d=[],e=a,a=this.scan(a,this.head);for(void 0===b&&(b=Infinity);b--&&0<=b&&a!==this.head;)d.push(a.value),a["delete"](),a=a.next,this.length--;if(c){null===e&&a===this.head&&(a=this.head.next);for(b=0;b=b)throw e;b--;d=g.iterate(a if(b)throw StopIteration;return c})};g.zip=function(){return g.transpose(Array.prototype.slice.call(arguments))};g.chain=function(){return g.concat(Array.prototype.slice.call(arguments))};g.range=function(a,b,c){3>arguments.length&&(c=1);2>arguments.length&&(b=a,a=0);a=a||0;return new g(function(){if(a>=b)throw StopIteration;if(isNaN(a))throw"";var d=a;a+=c;return d})};g.count=function(a,b){return g.range(a,Infinity,b||1)};g.repeat=function(a,b){2>arguments.length&&(b=Infinity);return(new g.range(+b)).mapIterator(function(){return a})}; "undefined"===typeof isStopIteration&&(m.isStopIteration=function(a){return"[object StopIteration]"===w.prototype.toString.call(a)});if("undefined"===typeof StopIteration){m.StopIteration={};var Za=w.prototype.toString;w.prototype.toString=function(){return this===m.StopIteration||this instanceof m.ReturnValue?"[object StopIteration]":Za.call(this,arguments)}}"undefined"===typeof ReturnValue&&(m.ReturnValue=function(a){if(!(this instanceof m.ReturnValue))return new m.ReturnValue(a);this.value=a}); ea.module$exports&&(ea=ea.module$exports);var sa={},v=q,Ma=U,$a=G;sa.module$exports=Array;Array.empty=[];Object.freeze&&Object.freeze(Array.empty);Array.from=function(a){var b=[];b.addEach(a);return b};Array.prototype.addEach=v.prototype.addEach;Array.prototype.deleteEach=v.prototype.deleteEach;Array.prototype.toArray=v.prototype.toArray;Array.prototype.toObject=v.prototype.toObject;Array.prototype.all=v.prototype.all;Array.prototype.any=v.prototype.any;Array.prototype.min=v.prototype.min;Array.prototype.max= -v.prototype.max;Array.prototype.sum=v.prototype.sum;Array.prototype.average=v.prototype.average;Array.prototype.only=v.prototype.only;Array.prototype.flatten=v.prototype.flatten;Array.prototype.zip=v.prototype.zip;Array.prototype.sorted=v.prototype.sorted;Array.prototype.reversed=v.prototype.reversed;Array.prototype.constructClone=function(a){var b=new this.constructor;b.addEach(a);return b};Array.prototype.has=function(a,b){return-1!==this.find(a,b)};Array.prototype.get=function(a){if(+a!==a)throw Error("Indicies must be numbers"); -return this[a]};Array.prototype.set=function(a,b){this.splice(a,1,b);return!0};Array.prototype.add=function(a){this.push(a);return!0};Array.prototype["delete"]=function(a,b){var c=this.find(a,b);return-1!==c?(this.splice(c,1),!0):!1};Array.prototype.find=function(a,b){for(var b=b||this.contentEquals||Object.equals,c=0;cd&&!(d++,a=a.prev,a==c););return a}return a||b};l.prototype.slice=function(a,b){for(var c=[],d=this.head,a=this.scan(a,d.next),b=this.scan(b,d);a!==b&&a!==d;)c.push(a.value),a=a.next;return c};l.prototype.splice=function(a,b){return this.swap(a,b,Array.prototype.slice.call(arguments,2))};l.prototype.swap=function(a,b,c){var d=[],e=a,a=this.scan(a,this.head);for(void 0===b&&(b=Infinity);b--&&0<=b&&a!==this.head;)d.push(a.value), a["delete"](),a=a.next,this.length--;if(c){null===e&&a===this.head&&(a=this.head.next);for(b=0;b Date: Mon, 16 Dec 2013 10:26:44 -0500 Subject: [PATCH 02/83] findLast() -> findLastValue() --- collections.cat.js | 4 ++-- collections.min.js | 4 ++-- list.js | 4 ++-- listen/map-changes.js | 2 +- shim-array.js | 2 +- sorted-array.js | 2 +- spec/array-spec.js | 8 ++++---- spec/list-spec.js | 28 ++++++++++++++-------------- spec/order.js | 4 ++-- 9 files changed, 29 insertions(+), 29 deletions(-) diff --git a/collections.cat.js b/collections.cat.js index 1916aa9..e7418ee 100644 --- a/collections.cat.js +++ b/collections.cat.js @@ -87,7 +87,7 @@ Array.prototype.clone=function(a,b){if(void 0===a)a=Infinity;else if(0===a)retur ArrayIterator$$module$shim_array.prototype.next=function(){if(this.start===(null==this.end?this.array.length:this.end))throw StopIteration;return this.array[this.start++]};module$shim_array.module$exports&&(module$shim_array=module$shim_array.module$exports);var module$shim={},Es5Array$$module$shim=module$shim_array_es5,Array$$module$shim=module$shim_array,Object$$module$shim=module$shim_object,Function$$module$shim=module$shim_function;var module$list={};module$list.module$exports=List$$module$list;var Shim$$module$list=module$shim,GenericCollection$$module$list=module$generic_collection,GenericOrder$$module$list=module$generic_order,PropertyChanges$$module$list=module$listen$property_changes; function List$$module$list(a,b,c){if(!(this instanceof List$$module$list))return new List$$module$list(a,b,c);var d=this.head=new this.Node;d.next=d;d.prev=d;this.contentEquals=b||Object.equals;this.content=c||Function.noop;this.length=0;this.addEach(a)}Object.addEach(List$$module$list.prototype,GenericCollection$$module$list.prototype);Object.addEach(List$$module$list.prototype,GenericOrder$$module$list.prototype);Object.addEach(List$$module$list.prototype,PropertyChanges$$module$list.prototype); List$$module$list.prototype.constructClone=function(a){return new this.constructor(a,this.contentEquals,this.content)};List$$module$list.prototype.find=function(a,b){for(var b=b||this.contentEquals,c=this.head,d=c.next;d!==c;){if(b(d.value,a))return d;d=d.next}};List$$module$list.prototype.findLast=function(a,b){for(var b=b||this.contentEquals,c=this.head,d=c.prev;d!==c;){if(b(d.value,a))return d;d=d.prev}};List$$module$list.prototype.has=function(a,b){return!!this.findValue(a,b)}; -List$$module$list.prototype.get=function(a,b){var c=this.findValue(a,b);return c?c.value:this.content()};List$$module$list.prototype["delete"]=function(a,b){var c=this.findLast(a,b);return c?(c["delete"](),this.length--,!0):!1};List$$module$list.prototype.clear=function(){this.head.next=this.head.prev=this.head;this.length=0};List$$module$list.prototype.add=function(a){this.head.addBefore(new this.Node(a));this.length++;return!0}; +List$$module$list.prototype.get=function(a,b){var c=this.findValue(a,b);return c?c.value:this.content()};List$$module$list.prototype["delete"]=function(a,b){var c=this.findLastValue(a,b);return c?(c["delete"](),this.length--,!0):!1};List$$module$list.prototype.clear=function(){this.head.next=this.head.prev=this.head;this.length=0};List$$module$list.prototype.add=function(a){this.head.addBefore(new this.Node(a));this.length++;return!0}; List$$module$list.prototype.push=function(){for(var a=this.head,b=0;bd&&!(d++,a=a.prev,a==c););return a}return a||b};List$$module$list.prototype.slice=function(a,b){for(var c=[],d=this.head,a=this.scan(a,d.next),b=this.scan(b,d);a!==b&&a!==d;)c.push(a.value),a=a.next;return c}; List$$module$list.prototype.splice=function(a,b){return this.swap(a,b,Array.prototype.slice.call(arguments,2))};List$$module$list.prototype.swap=function(a,b,c){var d=[],e=a,a=this.scan(a,this.head);for(void 0===b&&(b=Infinity);b--&&0<=b&&a!==this.head;)d.push(a.value),a["delete"](),a=a.next,this.length--;if(c){null===e&&a===this.head&&(a=this.head.next);for(b=0;bd&&!(d++,a=a.prev,a==c););return a}return a||b};l.prototype.slice=function(a,b){for(var c=[],d=this.head,a=this.scan(a,d.next),b=this.scan(b,d);a!==b&&a!==d;)c.push(a.value),a=a.next;return c};l.prototype.splice=function(a,b){return this.swap(a,b,Array.prototype.slice.call(arguments,2))};l.prototype.swap=function(a,b,c){var d=[],e=a,a=this.scan(a,this.head);for(void 0===b&&(b=Infinity);b--&&0<=b&&a!==this.head;)d.push(a.value), a["delete"](),a=a.next,this.length--;if(c){null===e&&a===this.head&&(a=this.head.next);for(b=0;b Date: Mon, 16 Dec 2013 13:09:42 -0500 Subject: [PATCH 03/83] Fix docs for find -> findValue --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9b3002d..cc9bcb3 100644 --- a/README.md +++ b/README.md @@ -435,7 +435,7 @@ not found. Returns the position of the last of equivalent values. (Array, ~~List~~, SortedArray, SortedArraySet) -### find(value, opt_equals) +### findValue(value, opt_equals) Finds a value. For List and SortedSet, returns the node at which the value was found. For SortedSet, the optional `equals` argument @@ -443,7 +443,7 @@ is ignored. (Array+, List, SortedSet) -### findLast(value, opt_equals) +### findLastValue(value, opt_equals) Finds the last equivalent value, returning the node at which the value was found. From 9dee1fc8f25369cb3fa3474c56aafd46fde07b98 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Thu, 10 Oct 2013 19:53:53 -0700 Subject: [PATCH 04/83] ObservePropertyChanges proposed interface This is a proposal for a new interface for property change observers, intended to subvert property change listeners. --- observe-property-changes.js | 254 ++++++++++++++++++++++++++ spec/observe/property-changes-spec.js | 173 ++++++++++++++++++ 2 files changed, 427 insertions(+) create mode 100644 observe-property-changes.js create mode 100644 spec/observe/property-changes-spec.js diff --git a/observe-property-changes.js b/observe-property-changes.js new file mode 100644 index 0000000..35e6ebf --- /dev/null +++ b/observe-property-changes.js @@ -0,0 +1,254 @@ +/*jshint node: true*/ +/*global -WeakMap*/ +"use strict"; + +require("./shim-object"); +var WeakMap = require("weak-map"); + +var handlerRecordsByObject = new WeakMap(); +var handlerRecordFreeList = []; +var superObjectDescriptors = new WeakMap(); + +/** + */ +exports.observeProperty = observeProperty; +function observeProperty(object, name, handler, note) { + makePropertyObservable(object, name); + var handlers = getPropertyChangeObservers(object, name); + var handlerRecord; + if (handlerRecordFreeList.length) { + handlerRecord = handlerRecordFreeList.pop(); + handlerRecord.handler = handler; + handlerRecord.note = note; + } else { + handlerRecord = {handler: handler, note: note, innerCancel: null}; + } + handlers.push(handlerRecord); + return function cancelPropertyObserver() { + var index = handlers.indexOf(handlerRecord); + if (index >= 0) { + if (handlerRecord.innerCancel) { + handlerRecord.innerCancel(); + } + handlers.splice(index, 1); + handlerRecord.handler = null; + handlerRecord.note = null; + handlerRecord.innerCancel = null; + handlerRecordFreeList.push(handlerRecord); + } + }; +} + +/** + */ +exports.dispatchPropertyChange = dispatchPropertyChange; +function dispatchPropertyChange(object, name, plus, minus) { + var specificHandlerMethodName = "handle" + name.slice(0, 1).toUpperCase() + name.slice(1) + "Change"; + var handlers = getPropertyChangeObservers(object, name); + for (var index = 0; index < handlers.length; index++) { + var handlerRecord = handlers[index]; + var handler = handlerRecord.handler; + var cancel = handler.innerCancel; + handler.innerCancel = null; + if (cancel) { + cancel(); + } + if (handler[specificHandlerMethodName]) { + cancel = handler[specificHandlerMethodName](name, plus, minus, object); + } else if (handler.propertyChange) { + cancel = handler.propertyChange(name, plus, minus, object); + } else if (typeof handler === "function") { + cancel = handler(name, plus, minus, object); + } else { + throw new Error( + "Can't dispatch to " + JSON.stringify(specificHandlerMethodName) + + " or handlePropertyChange " + + " on " + object + ); + } + handler.innerCancel = cancel; + } +} + +/** + */ +exports.getPropertyChangeObservers = getPropertyChangeObservers; +function getPropertyChangeObservers(object, name) { + if (!handlerRecordsByObject.has(object)) { + handlerRecordsByObject.set(object, {}); + } + var handlersByName = handlerRecordsByObject.get(object); + if (!Object.owns(handlersByName, name)) { + handlersByName[name] = []; + } + return handlersByName[name]; +} + +/** + */ +exports.makePropertyObservable = makePropertyObservable; +function makePropertyObservable(object, name) { + // arrays are special. we do not support direct setting of properties + // on an array. instead, call .set(index, value). this is observable. + // 'length' property is observable for all mutating methods because + // our overrides explicitly dispatch that change. + if (Array.isArray(object)) { + return; + } + + if (!Object.isExtensible(object, name)) { + return; + } + + // memoize super property descriptor table + if (!superObjectDescriptors.has(object)) { + superPropertyDescriptors = {}; + superObjectDescriptors.set(object, superPropertyDescriptors); + } + var superPropertyDescriptors = superObjectDescriptors.get(object); + + if (Object.owns.call(superPropertyDescriptors, name)) { + // if we have already recorded an super property descriptor, + // we have already installed the observer, so short-here + return; + } + + var superDescriptor = getSuperPropertyDescriptor(object, name); + + if (!superDescriptor.configurable) { + return; + } + + // memoize the descriptor so we know not to install another layer. we + // could use it to uninstall the observer, but we do not to avoid GC + // thrashing. + superPropertyDescriptors[name] = superDescriptor; + + // give up *after* storing the super property descriptor so it + // can be restored by uninstall. Unwritable properties are + // silently not overriden. Since success is indistinguishable from + // failure, we let it pass but don't waste time on intercepting + // get/set. + if (!superDescriptor.writable && !superDescriptor.set) { + return; + } + + // we put a __state__ property on every object where we're intercepting + // changes, so that folks can easily see the present value in their + // run-time inspector + var state; + if (typeof object.__state__ === "object") { + state = object.__state__; + } else { + state = {}; + if (Object.isExtensible(object, "__state__")) { + Object.defineProperty(object, "__state__", { + value: state, + writable: true, + enumerable: false + }); + } + } + state[name] = object[name]; + + var thunk; + // in both of these new descriptor variants, we reuse the super + // descriptor to either store the current value or apply getters + // and setters. this is handy since we can reuse the super + // descriptor if we uninstall the observer. We even preserve the + // assignment semantics, where we get the value from up the + // prototype chain, and set as an owned property. + if ('value' in superDescriptor) { + thunk = makeValuePropertyThunk(name, state, superDescriptor); + } else { // 'get' or 'set', but not necessarily both + thunk = makeGetSetPropertyThunk(name, state, superDescriptor); + } + + Object.defineProperty(object, name, thunk); +} + +function getSuperPropertyDescriptor(object, name) { + // walk up the prototype chain to find a property descriptor for + // the property name + var superDescriptor; + var superObject = object; + do { + superDescriptor = Object.getOwnPropertyDescriptor(superObject, name); + if (superDescriptor) { + break; + } + superObject = Object.getPrototypeOf(superObject); + } while (superObject); + // or default to an undefined value + return superDescriptor || { + value: undefined, + enumerable: true, + writable: true, + configurable: true + }; + +} + +function makeValuePropertyThunk(name, state, superDescriptor) { + return { + get: function () { + return superDescriptor.value; + }, + set: function (plus) { + if (plus === superDescriptor.value) { + return plus; + } + var minus = superDescriptor.value; + superDescriptor.value = plus; + state[name] = plus; + dispatchPropertyChange(this, name, plus, minus); + return plus; + }, + enumerable: superDescriptor.enumerable, + configurable: true + }; +} + +function makeGetSetPropertyThunk(name, state, superDescriptor) { + return { + get: function () { + if (superDescriptor.get) { + return superDescriptor.get.apply(this, arguments); + } + }, + set: function (plus) { + var minus; + + // get the actual former value if possible + if (superDescriptor.get) { + minus = superDescriptor.get.apply(this, arguments); + } + + // call through to actual setter + if (superDescriptor.set) { + superDescriptor.set.apply(this, arguments); + } + + // use getter, if possible, to discover whether the set + // was successful + if (superDescriptor.get) { + plus = superDescriptor.get.apply(this, arguments); + state[name] = plus; + } + + // if it has not changed, suppress a notification + if (plus === minus) { + return plus; + } + + // dispatch the new value: the given value if there is + // no getter, or the actual value if there is one + dispatchPropertyChange(this, name, plus, minus); + + return plus; + }, + enumerable: superDescriptor.enumerable, + configurable: true + }; +} + diff --git a/spec/observe/property-changes-spec.js b/spec/observe/property-changes-spec.js new file mode 100644 index 0000000..87aefb5 --- /dev/null +++ b/spec/observe/property-changes-spec.js @@ -0,0 +1,173 @@ + +/* + Based in part on observable arrays from Motorola Mobility’s Montage + Copyright (c) 2012, Motorola Mobility LLC. All Rights Reserved. + 3-Clause BSD License + https://github.com/motorola-mobility/montage/blob/master/LICENSE.md +*/ + +require("../../shim"); +var ObservePropertyChanges = require("../../observe-property-changes"); +var observeProperty = ObservePropertyChanges.observeProperty; + +describe("ObservePropertyChanges", function () { + + describe("observeProperty", function () { + + it("property change", function () { + var object = {}; + var spy = jasmine.createSpy(); + var cancel = observeProperty(object, "foo", function (name, plus, minus, object) { + spy(name, plus, minus, object); + }); + object.foo = 10; + expect(spy).toHaveBeenCalledWith("foo", 10, undefined, object); + }); + + it("property non-change", function () { + var object = {foo: 10}; + var spy = jasmine.createSpy(); + var cancel = observeProperty(object, "foo", function (name, plus, minus, object) { + spy(name, plus, minus, object); + }); + object.foo = 10; + expect(spy).not.toHaveBeenCalled(); + }); + + it("property change, property non-change", function () { + var object = {}; + var spy = jasmine.createSpy(); + var cancel = observeProperty(object, "foo", function (name, plus, minus, object) { + spy(name, plus, minus, object); + }); + object.foo = 10; + expect(spy).toHaveBeenCalledWith("foo", 10, undefined, object); + + spy = jasmine.createSpy(); + object.foo = 10; + expect(spy).not.toHaveBeenCalled(); + }); + + it("property change, cancel", function () { + var object = {}; + var spy = jasmine.createSpy(); + var cancel = observeProperty(object, "foo", function (name, plus, minus, object) { + spy(name, plus, minus, object); + }); + object.foo = 10; + + cancel(); + spy = jasmine.createSpy(); + object.foo = 20; + expect(spy).not.toHaveBeenCalled(); + }); + + it("just cancel", function () { + var object = {}; + var cancel = observeProperty(object, "foo", function (name, plus, minus, object) { + spy(name, plus, minus, object); + }); + + cancel(); + spy = jasmine.createSpy(); + object.foo = 20; + expect(spy).not.toHaveBeenCalled(); + }); + + it("multiple observers", function () { + var object = {}; + var spy1 = jasmine.createSpy(); + var cancel1 = observeProperty(object, "foo", function (name, plus, minus, object) { + spy1(name, plus, minus, object); + }); + var spy2 = jasmine.createSpy(); + var cancel2 = observeProperty(object, "foo", function (name, plus, minus, object) { + spy2(name, plus, minus, object); + }); + object.foo = 10; + expect(spy1).toHaveBeenCalledWith("foo", 10, undefined, object); + expect(spy2).toHaveBeenCalledWith("foo", 10, undefined, object); + }); + + it("multiple observers, one canceled", function () { + var object = {}; + var spy1 = jasmine.createSpy(); + var cancel1 = observeProperty(object, "foo", function (name, plus, minus, object) { + spy1(name, plus, minus, object); + }); + var spy2 = jasmine.createSpy(); + var cancel2 = observeProperty(object, "foo", function (name, plus, minus, object) { + spy2(name, plus, minus, object); + }); + cancel1(); + object.foo = 10; + expect(spy1).not.toHaveBeenCalled(); + expect(spy2).toHaveBeenCalledWith("foo", 10, undefined, object); + }); + + it("multiple observers, other canceled", function () { + var object = {}; + var spy1 = jasmine.createSpy(); + var cancel1 = observeProperty(object, "foo", function (name, plus, minus, object) { + spy1(name, plus, minus, object); + }); + var spy2 = jasmine.createSpy(); + var cancel2 = observeProperty(object, "foo", function (name, plus, minus, object) { + spy2(name, plus, minus, object); + }); + cancel2(); + object.foo = 10; + expect(spy1).toHaveBeenCalledWith("foo", 10, undefined, object); + expect(spy2).not.toHaveBeenCalled(); + }); + + it("multiple observers, both canceled", function () { + var object = {}; + var spy1 = jasmine.createSpy(); + var cancel1 = observeProperty(object, "foo", function (name, plus, minus, object) { + spy1(name, plus, minus, object); + }); + var spy2 = jasmine.createSpy(); + var cancel2 = observeProperty(object, "foo", function (name, plus, minus, object) { + spy2(name, plus, minus, object); + }); + cancel1(); + cancel2(); + object.foo = 10; + expect(spy1).not.toHaveBeenCalled(); + expect(spy2).not.toHaveBeenCalled(); + }); + + it("observe, cancel, observe", function () { + var object = {}; + var spy1 = jasmine.createSpy(); + var cancel1 = observeProperty(object, "foo", function (name, plus, minus, object) { + spy1(name, plus, minus, object); + }); + cancel1(); + var spy2 = jasmine.createSpy(); + var cancel2 = observeProperty(object, "foo", function (name, plus, minus, object) { + spy2(name, plus, minus, object); + }); + object.foo = 10; + expect(spy1).not.toHaveBeenCalled(); + expect(spy2).toHaveBeenCalledWith("foo", 10, undefined, object); + }); + + it("dispatches to specific handler method", function () { + var object = { + foo: 10, + handleFooChange: function (name, plus, minus, object) { + spy(name, plus, minus, object); + } + }; + var cancel = observeProperty(object, "foo", object); + var spy = jasmine.createSpy(); + object.foo = 20; + expect(spy).toHaveBeenCalledWith("foo", 20, 10, object); + }); + + }); + +}); + From 77826a978099bd9b02053e93d0fab51b65197b06 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Fri, 3 Jan 2014 10:03:56 -0800 Subject: [PATCH 05/83] Normalize property change observer argument order --- observe-property-changes.js | 6 +- spec/observe/property-changes-spec.js | 80 +++++++++++++-------------- 2 files changed, 43 insertions(+), 43 deletions(-) diff --git a/observe-property-changes.js b/observe-property-changes.js index 35e6ebf..0ff68e8 100644 --- a/observe-property-changes.js +++ b/observe-property-changes.js @@ -54,11 +54,11 @@ function dispatchPropertyChange(object, name, plus, minus) { cancel(); } if (handler[specificHandlerMethodName]) { - cancel = handler[specificHandlerMethodName](name, plus, minus, object); + cancel = handler[specificHandlerMethodName](plus, minus, name, object); } else if (handler.propertyChange) { - cancel = handler.propertyChange(name, plus, minus, object); + cancel = handler.propertyChange(plus, minus, name, object); } else if (typeof handler === "function") { - cancel = handler(name, plus, minus, object); + cancel = handler(plus, minus, name, object); } else { throw new Error( "Can't dispatch to " + JSON.stringify(specificHandlerMethodName) + diff --git a/spec/observe/property-changes-spec.js b/spec/observe/property-changes-spec.js index 87aefb5..0dd5801 100644 --- a/spec/observe/property-changes-spec.js +++ b/spec/observe/property-changes-spec.js @@ -17,18 +17,18 @@ describe("ObservePropertyChanges", function () { it("property change", function () { var object = {}; var spy = jasmine.createSpy(); - var cancel = observeProperty(object, "foo", function (name, plus, minus, object) { - spy(name, plus, minus, object); + var cancel = observeProperty(object, "foo", function (plus, minus, name, object) { + spy(plus, minus, name, object); }); object.foo = 10; - expect(spy).toHaveBeenCalledWith("foo", 10, undefined, object); + expect(spy).toHaveBeenCalledWith(10, undefined, "foo", object); }); it("property non-change", function () { var object = {foo: 10}; var spy = jasmine.createSpy(); - var cancel = observeProperty(object, "foo", function (name, plus, minus, object) { - spy(name, plus, minus, object); + var cancel = observeProperty(object, "foo", function (plus, minus, name, object) { + spy(plus, minus, name, object); }); object.foo = 10; expect(spy).not.toHaveBeenCalled(); @@ -37,11 +37,11 @@ describe("ObservePropertyChanges", function () { it("property change, property non-change", function () { var object = {}; var spy = jasmine.createSpy(); - var cancel = observeProperty(object, "foo", function (name, plus, minus, object) { - spy(name, plus, minus, object); + var cancel = observeProperty(object, "foo", function (plus, minus, name, object) { + spy(plus, minus, name, object); }); object.foo = 10; - expect(spy).toHaveBeenCalledWith("foo", 10, undefined, object); + expect(spy).toHaveBeenCalledWith(10, undefined, "foo", object); spy = jasmine.createSpy(); object.foo = 10; @@ -51,8 +51,8 @@ describe("ObservePropertyChanges", function () { it("property change, cancel", function () { var object = {}; var spy = jasmine.createSpy(); - var cancel = observeProperty(object, "foo", function (name, plus, minus, object) { - spy(name, plus, minus, object); + var cancel = observeProperty(object, "foo", function (plus, minus, name, object) { + spy(plus, minus, name, object); }); object.foo = 10; @@ -64,8 +64,8 @@ describe("ObservePropertyChanges", function () { it("just cancel", function () { var object = {}; - var cancel = observeProperty(object, "foo", function (name, plus, minus, object) { - spy(name, plus, minus, object); + var cancel = observeProperty(object, "foo", function (plus, minus, name, object) { + spy(plus, minus, name, object); }); cancel(); @@ -77,59 +77,59 @@ describe("ObservePropertyChanges", function () { it("multiple observers", function () { var object = {}; var spy1 = jasmine.createSpy(); - var cancel1 = observeProperty(object, "foo", function (name, plus, minus, object) { - spy1(name, plus, minus, object); + var cancel1 = observeProperty(object, "foo", function (plus, minus, name, object) { + spy1(plus, minus, name, object); }); var spy2 = jasmine.createSpy(); - var cancel2 = observeProperty(object, "foo", function (name, plus, minus, object) { - spy2(name, plus, minus, object); + var cancel2 = observeProperty(object, "foo", function (plus, minus, name, object) { + spy2(plus, minus, name, object); }); object.foo = 10; - expect(spy1).toHaveBeenCalledWith("foo", 10, undefined, object); - expect(spy2).toHaveBeenCalledWith("foo", 10, undefined, object); + expect(spy1).toHaveBeenCalledWith(10, undefined, "foo", object); + expect(spy2).toHaveBeenCalledWith(10, undefined, "foo", object); }); it("multiple observers, one canceled", function () { var object = {}; var spy1 = jasmine.createSpy(); - var cancel1 = observeProperty(object, "foo", function (name, plus, minus, object) { - spy1(name, plus, minus, object); + var cancel1 = observeProperty(object, "foo", function (plus, minus, name, object) { + spy1(plus, minus, name, object); }); var spy2 = jasmine.createSpy(); - var cancel2 = observeProperty(object, "foo", function (name, plus, minus, object) { - spy2(name, plus, minus, object); + var cancel2 = observeProperty(object, "foo", function (plus, minus, name, object) { + spy2(plus, minus, name, object); }); cancel1(); object.foo = 10; expect(spy1).not.toHaveBeenCalled(); - expect(spy2).toHaveBeenCalledWith("foo", 10, undefined, object); + expect(spy2).toHaveBeenCalledWith(10, undefined, "foo", object); }); it("multiple observers, other canceled", function () { var object = {}; var spy1 = jasmine.createSpy(); - var cancel1 = observeProperty(object, "foo", function (name, plus, minus, object) { - spy1(name, plus, minus, object); + var cancel1 = observeProperty(object, "foo", function (plus, minus, name, object) { + spy1(plus, minus, name, object); }); var spy2 = jasmine.createSpy(); - var cancel2 = observeProperty(object, "foo", function (name, plus, minus, object) { - spy2(name, plus, minus, object); + var cancel2 = observeProperty(object, "foo", function (plus, minus, name, object) { + spy2(plus, minus, name, object); }); cancel2(); object.foo = 10; - expect(spy1).toHaveBeenCalledWith("foo", 10, undefined, object); + expect(spy1).toHaveBeenCalledWith(10, undefined, "foo", object); expect(spy2).not.toHaveBeenCalled(); }); it("multiple observers, both canceled", function () { var object = {}; var spy1 = jasmine.createSpy(); - var cancel1 = observeProperty(object, "foo", function (name, plus, minus, object) { - spy1(name, plus, minus, object); + var cancel1 = observeProperty(object, "foo", function (plus, minus, name, object) { + spy1(plus, minus, name, object); }); var spy2 = jasmine.createSpy(); - var cancel2 = observeProperty(object, "foo", function (name, plus, minus, object) { - spy2(name, plus, minus, object); + var cancel2 = observeProperty(object, "foo", function (plus, minus, name, object) { + spy2(plus, minus, name, object); }); cancel1(); cancel2(); @@ -141,30 +141,30 @@ describe("ObservePropertyChanges", function () { it("observe, cancel, observe", function () { var object = {}; var spy1 = jasmine.createSpy(); - var cancel1 = observeProperty(object, "foo", function (name, plus, minus, object) { - spy1(name, plus, minus, object); + var cancel1 = observeProperty(object, "foo", function (plus, minus, name, object) { + spy1(plus, minus, name, object); }); cancel1(); var spy2 = jasmine.createSpy(); - var cancel2 = observeProperty(object, "foo", function (name, plus, minus, object) { - spy2(name, plus, minus, object); + var cancel2 = observeProperty(object, "foo", function (plus, minus, name, object) { + spy2(plus, minus, name, object); }); object.foo = 10; expect(spy1).not.toHaveBeenCalled(); - expect(spy2).toHaveBeenCalledWith("foo", 10, undefined, object); + expect(spy2).toHaveBeenCalledWith(10, undefined, "foo", object); }); it("dispatches to specific handler method", function () { var object = { foo: 10, - handleFooChange: function (name, plus, minus, object) { - spy(name, plus, minus, object); + handleFooChange: function (plus, minus, name, object) { + spy(plus, minus, name, object); } }; var cancel = observeProperty(object, "foo", object); var spy = jasmine.createSpy(); object.foo = 20; - expect(spy).toHaveBeenCalledWith("foo", 20, 10, object); + expect(spy).toHaveBeenCalledWith(20, 10, "foo", object); }); }); From cfefefeaf4338133144c00f9ae9d749b51d6f35e Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Sun, 5 Jan 2014 20:47:13 -0800 Subject: [PATCH 06/83] Preliminary property change implementation shift This change makes it possible for a deprecated shim of the old property change listener system to coexist using the new property change observer system. --- listen/property-changes.js | 387 +++++--------------------- observe-property-changes.js | 244 +++++++++++----- spec/listen/property-changes-spec.js | 6 +- spec/observe-property-changes-spec.js | 267 ++++++++++++++++++ spec/observe/property-changes-spec.js | 173 ------------ 5 files changed, 524 insertions(+), 553 deletions(-) create mode 100644 spec/observe-property-changes-spec.js delete mode 100644 spec/observe/property-changes-spec.js diff --git a/listen/property-changes.js b/listen/property-changes.js index a3538d3..c9f5c9c 100644 --- a/listen/property-changes.js +++ b/listen/property-changes.js @@ -1,376 +1,135 @@ -/* - Based in part on observable arrays from Motorola Mobility’s Montage - Copyright (c) 2012, Motorola Mobility LLC. All Rights Reserved. - 3-Clause BSD License - https://github.com/motorola-mobility/montage/blob/master/LICENSE.md -*/ - -/* - This module is responsible for observing changes to owned properties of - objects and changes to the content of arrays caused by method calls. - The interface for observing array content changes establishes the methods - necessary for any collection with observable content. -*/ - -require("../shim"); + var WeakMap = require("weak-map"); +var ObservePropertyChanges = require("../observe-property-changes"); +var makePropertyObservable = ObservePropertyChanges.makePropertyObservable; +var observePropertyChange = ObservePropertyChanges.observePropertyChange; +var dispatchPropertyChange = ObservePropertyChanges.dispatchPropertyChange; var object_owns = Object.prototype.hasOwnProperty; -/* - Object property descriptors carry information necessary for adding, - removing, dispatching, and shorting events to listeners for property changes - for a particular key on a particular object. These descriptors are used - here for shallow property changes. +module.exports = PropertyChanges; - { - willChangeListeners:Array(Function) - changeListeners:Array(Function) - } -*/ var propertyChangeDescriptors = new WeakMap(); -// Maybe remove entries from this table if the corresponding object no longer -// has any property change listeners for any key. However, the cost of -// book-keeping is probably not warranted since it would be rare for an -// observed object to no longer be observed unless it was about to be disposed -// of or reused as an observable. The only benefit would be in avoiding bulk -// calls to dispatchOwnPropertyChange events on objects that have no listeners. - -/* - To observe shallow property changes for a particular key of a particular - object, we install a property descriptor on the object that overrides the previous - descriptor. The overridden descriptors are stored in this weak map. The - weak map associates an object with another object that maps property names - to property descriptors. - - overriddenObjectDescriptors.get(object)[key] - - We retain the old descriptor for various purposes. For one, if the property - is no longer being observed by anyone, we revert the property descriptor to - the original. For "value" descriptors, we store the actual value of the - descriptor on the overridden descriptor, so when the property is reverted, it - retains the most recently set value. For "get" and "set" descriptors, - we observe then forward "get" and "set" operations to the original descriptor. -*/ -var overriddenObjectDescriptors = new WeakMap(); - -module.exports = PropertyChanges; - function PropertyChanges() { throw new Error("This is an abstract interface. Mix it. Don't construct it"); } -PropertyChanges.debug = true; - -PropertyChanges.prototype.getOwnPropertyChangeDescriptor = function (key) { +PropertyChanges.prototype.getOwnPropertyChangeDescriptor = function (name) { if (!propertyChangeDescriptors.has(this)) { propertyChangeDescriptors.set(this, {}); } var objectPropertyChangeDescriptors = propertyChangeDescriptors.get(this); - if (!object_owns.call(objectPropertyChangeDescriptors, key)) { - objectPropertyChangeDescriptors[key] = { + if (!object_owns.call(objectPropertyChangeDescriptors, name)) { + objectPropertyChangeDescriptors[name] = { willChangeListeners: [], - changeListeners: [] + willChangeObservers: [], + changeListeners: [], + changeObservers: [] }; } - return objectPropertyChangeDescriptors[key]; + return objectPropertyChangeDescriptors[name]; }; -PropertyChanges.prototype.hasOwnPropertyChangeDescriptor = function (key) { +PropertyChanges.prototype.hasOwnPropertyChangeDescriptor = function (name) { if (!propertyChangeDescriptors.has(this)) { return false; } - if (!key) { + if (!name) { return true; } var objectPropertyChangeDescriptors = propertyChangeDescriptors.get(this); - if (!object_owns.call(objectPropertyChangeDescriptors, key)) { + if (!object_owns.call(objectPropertyChangeDescriptors, name)) { return false; } return true; }; -PropertyChanges.prototype.addOwnPropertyChangeListener = function (key, listener, beforeChange) { - if (this.makeObservable && !this.isObservable) { - this.makeObservable(); // particularly for observable arrays, for - // their length property - } - var descriptor = PropertyChanges.getOwnPropertyChangeDescriptor(this, key); - var listeners; - if (beforeChange) { +PropertyChanges.prototype.addOwnPropertyChangeListener = function (name, listener, capture) { + var descriptor = PropertyChanges.getOwnPropertyChangeDescriptor(this, name); + + var listeners, observers; + if (capture) { listeners = descriptor.willChangeListeners; + observers = descriptor.willChangeObservers; } else { listeners = descriptor.changeListeners; + observers = descriptor.changeObservers; } - PropertyChanges.makePropertyObservable(this, key); + + var changeName = (capture ? "Will" : "") + "Change"; + var genericHandlerName = "handleProperty" + changeName; + var propertyName = name; + propertyName = propertyName && propertyName[0].toUpperCase() + propertyName.slice(1); + var specificHandlerName = "handle" + propertyName + changeName; + var thisp = listener; + + var observer = observePropertyChange(this, name, function (plus, minus, name, object) { + var value = capture ? minus : plus; + listener = ( + listener[specificHandlerName] || + listener[genericHandlerName] || + listener + ); + if (!listener.call) { + throw new Error("No event listener for " + specificHandlerName + " or " + genericHandlerName + " or call on " + listener); + } + listener.call(thisp, value, name, this); + }, capture); listeners.push(listener); + observers.push(observer); var self = this; return function cancelOwnPropertyChangeListener() { - PropertyChanges.removeOwnPropertyChangeListener(self, key, listeners, beforeChange); + PropertyChanges.removeOwnPropertyChangeListener(self, name, listeners, capture); self = null; }; }; -PropertyChanges.prototype.addBeforeOwnPropertyChangeListener = function (key, listener) { - return PropertyChanges.addOwnPropertyChangeListener(this, key, listener, true); +PropertyChanges.prototype.addBeforeOwnPropertyChangeListener = function (name, listener) { + return PropertyChanges.addOwnPropertyChangeListener(this, name, listener, true); }; -PropertyChanges.prototype.removeOwnPropertyChangeListener = function (key, listener, beforeChange) { - var descriptor = PropertyChanges.getOwnPropertyChangeDescriptor(this, key); +PropertyChanges.prototype.removeOwnPropertyChangeListener = function (name, listener, capture) { + var descriptor = PropertyChanges.getOwnPropertyChangeDescriptor(this, name); - var listeners; - if (beforeChange) { + var listeners, observers; + if (capture) { listeners = descriptor.willChangeListeners; + observers = descriptor.willChangeObservers; } else { listeners = descriptor.changeListeners; + observers = descriptor.changeObservers; } var index = listeners.lastIndexOf(listener); if (index === -1) { throw new Error("Can't remove listener: does not exist."); } + var observer = observers[index]; listeners.splice(index, 1); + observers.splice(index, 1); - if (descriptor.changeListeners.length + descriptor.willChangeListeners.length === 0) { - PropertyChanges.makePropertyUnobservable(this, key); - } + observer.cancel(); }; -PropertyChanges.prototype.removeBeforeOwnPropertyChangeListener = function (key, listener) { - return PropertyChanges.removeOwnPropertyChangeListener(this, key, listener, true); +PropertyChanges.prototype.removeBeforeOwnPropertyChangeListener = function (name, listener) { + return PropertyChanges.removeOwnPropertyChangeListener(this, name, listener, true); }; PropertyChanges.prototype.dispatchOwnPropertyChange = function (key, value, beforeChange) { - var descriptor = PropertyChanges.getOwnPropertyChangeDescriptor(this, key); - - if (descriptor.isActive) { - return; - } - descriptor.isActive = true; - - var listeners; - if (beforeChange) { - listeners = descriptor.willChangeListeners; - } else { - listeners = descriptor.changeListeners; - } - - var changeName = (beforeChange ? "Will" : "") + "Change"; - var genericHandlerName = "handleProperty" + changeName; - var propertyName = String(key); - propertyName = propertyName && propertyName[0].toUpperCase() + propertyName.slice(1); - var specificHandlerName = "handle" + propertyName + changeName; - - try { - // dispatch to each listener - listeners.slice().forEach(function (listener) { - var thisp = listener; - listener = ( - listener[specificHandlerName] || - listener[genericHandlerName] || - listener - ); - if (!listener.call) { - throw new Error("No event listener for " + specificHandlerName + " or " + genericHandlerName + " or call on " + listener); - } - listener.call(thisp, value, key, this); - }, this); - } finally { - descriptor.isActive = false; - } + dispatchPropertyChange(this, name, value, this[name], capture); }; -PropertyChanges.prototype.dispatchBeforeOwnPropertyChange = function (key, listener) { - return PropertyChanges.dispatchOwnPropertyChange(this, key, listener, true); +PropertyChanges.prototype.dispatchBeforeOwnPropertyChange = function (name, listener) { + return PropertyChanges.dispatchOwnPropertyChange(this, name, listener, true); }; -PropertyChanges.prototype.makePropertyObservable = function (key) { - // arrays are special. we do not support direct setting of properties - // on an array. instead, call .set(index, value). this is observable. - // 'length' property is observable for all mutating methods because - // our overrides explicitly dispatch that change. - if (Array.isArray(this)) { - return; - } - - if (!Object.isExtensible(this, key)) { - throw new Error("Can't make property " + JSON.stringify(key) + " observable on " + this + " because object is not extensible"); - } - - var state; - if (typeof this.__state__ === "object") { - state = this.__state__; - } else { - state = {}; - if (Object.isExtensible(this, "__state__")) { - Object.defineProperty(this, "__state__", { - value: state, - writable: true, - enumerable: false - }); - } - } - state[key] = this[key]; - - // memoize overridden property descriptor table - if (!overriddenObjectDescriptors.has(this)) { - overriddenPropertyDescriptors = {}; - overriddenObjectDescriptors.set(this, overriddenPropertyDescriptors); - } - var overriddenPropertyDescriptors = overriddenObjectDescriptors.get(this); - - if (object_owns.call(overriddenPropertyDescriptors, key)) { - // if we have already recorded an overridden property descriptor, - // we have already installed the observer, so short-here - return; - } - - // walk up the prototype chain to find a property descriptor for - // the property name - var overriddenDescriptor; - var attached = this; - var formerDescriptor = Object.getOwnPropertyDescriptor(attached, key); - do { - overriddenDescriptor = Object.getOwnPropertyDescriptor(attached, key); - if (overriddenDescriptor) { - break; - } - attached = Object.getPrototypeOf(attached); - } while (attached); - // or default to an undefined value - overriddenDescriptor = overriddenDescriptor || { - value: undefined, - enumerable: true, - writable: true, - configurable: true - }; - - if (!overriddenDescriptor.configurable) { - throw new Error("Can't observe non-configurable properties"); - } - - // memoize the descriptor so we know not to install another layer, - // and so we can reuse the overridden descriptor when uninstalling - overriddenPropertyDescriptors[key] = overriddenDescriptor; - - // give up *after* storing the overridden property descriptor so it - // can be restored by uninstall. Unwritable properties are - // silently not overriden. Since success is indistinguishable from - // failure, we let it pass but don't waste time on intercepting - // get/set. - if (!overriddenDescriptor.writable && !overriddenDescriptor.set) { - return; - } - - // TODO reflect current value on a displayed property - - var propertyListener; - // in both of these new descriptor variants, we reuse the overridden - // descriptor to either store the current value or apply getters - // and setters. this is handy since we can reuse the overridden - // descriptor if we uninstall the observer. We even preserve the - // assignment semantics, where we get the value from up the - // prototype chain, and set as an owned property. - if ('value' in overriddenDescriptor) { - propertyListener = { - get: function () { - return overriddenDescriptor.value - }, - set: function (value) { - if (value === overriddenDescriptor.value) { - return value; - } - PropertyChanges.dispatchBeforeOwnPropertyChange(this, key, overriddenDescriptor.value); - overriddenDescriptor.value = value; - state[key] = value; - PropertyChanges.dispatchOwnPropertyChange(this, key, value); - return value; - }, - enumerable: overriddenDescriptor.enumerable, - configurable: true - }; - } else { // 'get' or 'set', but not necessarily both - propertyListener = { - get: function () { - if (overriddenDescriptor.get) { - return overriddenDescriptor.get.apply(this, arguments); - } - }, - set: function (value) { - var formerValue; - - // get the actual former value if possible - if (overriddenDescriptor.get) { - formerValue = overriddenDescriptor.get.apply(this, arguments); - } - // call through to actual setter - if (overriddenDescriptor.set) { - overriddenDescriptor.set.apply(this, arguments) - } - // use getter, if possible, to discover whether the set - // was successful - if (overriddenDescriptor.get) { - value = overriddenDescriptor.get.apply(this, arguments); - state[key] = value; - } - // if it has not changed, suppress a notification - if (value === formerValue) { - return value; - } - PropertyChanges.dispatchBeforeOwnPropertyChange(this, key, formerValue); - - // dispatch the new value: the given value if there is - // no getter, or the actual value if there is one - PropertyChanges.dispatchOwnPropertyChange(this, key, value); - return value; - }, - enumerable: overriddenDescriptor.enumerable, - configurable: true - }; - } - - Object.defineProperty(this, key, propertyListener); +PropertyChanges.prototype.makePropertyObservable = function (name) { + return makePropertyObservable(this, name); }; -PropertyChanges.prototype.makePropertyUnobservable = function (key) { - // arrays are special. we do not support direct setting of properties - // on an array. instead, call .set(index, value). this is observable. - // 'length' property is observable for all mutating methods because - // our overrides explicitly dispatch that change. - if (Array.isArray(this)) { - return; - } - - if (!overriddenObjectDescriptors.has(this)) { - throw new Error("Can't uninstall observer on property"); - } - var overriddenPropertyDescriptors = overriddenObjectDescriptors.get(this); - - if (!overriddenPropertyDescriptors[key]) { - throw new Error("Can't uninstall observer on property"); - } - - var overriddenDescriptor = overriddenPropertyDescriptors[key]; - delete overriddenPropertyDescriptors[key]; - - var state; - if (typeof this.__state__ === "object") { - state = this.__state__; - } else { - state = {}; - if (Object.isExtensible(this, "__state__")) { - Object.defineProperty(this, "__state__", { - value: state, - writable: true, - enumerable: false - }); - } - } - delete state[key]; - - Object.defineProperty(this, key, overriddenDescriptor); +PropertyChanges.prototype.makePropertyUnobservable = function (name) { }; // constructor functions @@ -391,30 +150,30 @@ PropertyChanges.hasOwnPropertyChangeDescriptor = function (object, key) { } }; -PropertyChanges.addOwnPropertyChangeListener = function (object, key, listener, beforeChange) { +PropertyChanges.addOwnPropertyChangeListener = function (object, key, listener, capture) { if (!Object.isObject(object)) { } else if (object.addOwnPropertyChangeListener) { - return object.addOwnPropertyChangeListener(key, listener, beforeChange); + return object.addOwnPropertyChangeListener(key, listener, capture); } else { - return PropertyChanges.prototype.addOwnPropertyChangeListener.call(object, key, listener, beforeChange); + return PropertyChanges.prototype.addOwnPropertyChangeListener.call(object, key, listener, capture); } }; -PropertyChanges.removeOwnPropertyChangeListener = function (object, key, listener, beforeChange) { +PropertyChanges.removeOwnPropertyChangeListener = function (object, key, listener, capture) { if (!Object.isObject(object)) { } else if (object.removeOwnPropertyChangeListener) { - return object.removeOwnPropertyChangeListener(key, listener, beforeChange); + return object.removeOwnPropertyChangeListener(key, listener, capture); } else { - return PropertyChanges.prototype.removeOwnPropertyChangeListener.call(object, key, listener, beforeChange); + return PropertyChanges.prototype.removeOwnPropertyChangeListener.call(object, key, listener, capture); } }; -PropertyChanges.dispatchOwnPropertyChange = function (object, key, value, beforeChange) { +PropertyChanges.dispatchOwnPropertyChange = function (object, key, value, capture) { if (!Object.isObject(object)) { } else if (object.dispatchOwnPropertyChange) { - return object.dispatchOwnPropertyChange(key, value, beforeChange); + return object.dispatchOwnPropertyChange(key, value, capture); } else { - return PropertyChanges.prototype.dispatchOwnPropertyChange.call(object, key, value, beforeChange); + return PropertyChanges.prototype.dispatchOwnPropertyChange.call(object, key, value, capture); } }; diff --git a/observe-property-changes.js b/observe-property-changes.js index 0ff68e8..aeb3d94 100644 --- a/observe-property-changes.js +++ b/observe-property-changes.js @@ -2,90 +2,196 @@ /*global -WeakMap*/ "use strict"; +// XXX Note: exceptions thrown from handlers and handler cancelers may +// interfere with dispatching to subsequent handlers of any change in progress. +// It is unlikely that plans are recoverable once an exception interferes with +// change dispatch. The internal records should not be corrupt, but observers +// might miss an intermediate property change. + +require("./shim-array"); require("./shim-object"); var WeakMap = require("weak-map"); -var handlerRecordsByObject = new WeakMap(); -var handlerRecordFreeList = []; +var observersByObject = new WeakMap(); +var observerFreeList = []; +var observerToFreeList = []; var superObjectDescriptors = new WeakMap(); +var dispatching = false; -/** - */ -exports.observeProperty = observeProperty; -function observeProperty(object, name, handler, note) { +exports.observePropertyChange = observePropertyChange; +function observePropertyChange(object, name, handler, note, capture) { makePropertyObservable(object, name); - var handlers = getPropertyChangeObservers(object, name); - var handlerRecord; - if (handlerRecordFreeList.length) { - handlerRecord = handlerRecordFreeList.pop(); - handlerRecord.handler = handler; - handlerRecord.note = note; + var observers = getPropertyChangeObservers(object, name, capture); + var observer; + if (observerFreeList.length) { + observer = observerFreeList.pop(); } else { - handlerRecord = {handler: handler, note: note, innerCancel: null}; + observer = new PropertyChangeObserver(); } - handlers.push(handlerRecord); - return function cancelPropertyObserver() { - var index = handlers.indexOf(handlerRecord); - if (index >= 0) { - if (handlerRecord.innerCancel) { - handlerRecord.innerCancel(); - } - handlers.splice(index, 1); - handlerRecord.handler = null; - handlerRecord.note = null; - handlerRecord.innerCancel = null; - handlerRecordFreeList.push(handlerRecord); - } - }; + observer.observers = observers; + observer.handler = handler; + observer.note = note; + observers.push(observer); + // TODO issue warnings if the number of handler records exceeds some + // concerning quantity as a harbinger of a memory leak. + // TODO Note that if this is garbage collected without ever being called, + // it probably indicates a programming error. + return observer; +} + +exports.observePropertyWillChange = observePropertyWillChange; +function observePropertyWillChange(object, name, handler, note) { + return observePropertyChange(object, name, handler, note, true); } -/** - */ exports.dispatchPropertyChange = dispatchPropertyChange; -function dispatchPropertyChange(object, name, plus, minus) { - var specificHandlerMethodName = "handle" + name.slice(0, 1).toUpperCase() + name.slice(1) + "Change"; - var handlers = getPropertyChangeObservers(object, name); - for (var index = 0; index < handlers.length; index++) { - var handlerRecord = handlers[index]; - var handler = handlerRecord.handler; - var cancel = handler.innerCancel; - handler.innerCancel = null; - if (cancel) { - cancel(); +function dispatchPropertyChange(object, name, plus, minus, capture) { + if (!dispatching) { + return startPropertyChangeDispatchContext(object, name, plus, minus, capture); + } + var phase = capture ? "WillChange" : "Change"; + var genericHandlerMethodName = "handleProperty" + phase; + var propertyName = "" + name; // array indicies must be coerced + var specificHandlerMethodName = "handle" + propertyName.slice(0, 1).toUpperCase() + propertyName.slice(1) + "Property" + phase; + var observers = getPropertyChangeObservers(object, name, capture).slice(); + for (var index = 0; index < observers.length; index++) { + var observer = observers[index]; + var handler = observer.handler; + // A null handler implies that an observer was canceled during the + // dispatch of a change. The handler record is pending addition to the + // free list. + if (!handler) { + continue; + } + var childObserver = observer.childObserver; + observer.childObserver = null; + // XXX plan interference hazards calling cancel and handler methods: + if (childObserver) { + childObserver.cancel(); } if (handler[specificHandlerMethodName]) { - cancel = handler[specificHandlerMethodName](plus, minus, name, object); - } else if (handler.propertyChange) { - cancel = handler.propertyChange(plus, minus, name, object); - } else if (typeof handler === "function") { - cancel = handler(plus, minus, name, object); + childObserver = handler[specificHandlerMethodName](plus, minus, name, object); + } else if (handler[genericHandlerMethodName]) { + childObserver = handler[genericHandlerMethodName](plus, minus, name, object); + } else if (handler.call) { + childObserver = handler.call(observer, plus, minus, name, object); } else { throw new Error( - "Can't dispatch to " + JSON.stringify(specificHandlerMethodName) + - " or handlePropertyChange " + + "Can't dispatch " + JSON.stringify(specificHandlerMethodName) + + " or " + JSON.stringify(genericHandlerMethodName) + " on " + object ); } - handler.innerCancel = cancel; + observer.childObserver = childObserver; + } +} + +exports.dispatchPropertyWillChange = dispatchPropertyWillChange; +function dispatchPropertyWillChange(object, name, plus, minus) { + dispatchPropertyChange(object, name, plus, minus, true); +} + +function startPropertyChangeDispatchContext(object, name, plus, minus, capture) { + dispatching = true; + try { + dispatchPropertyChange(object, name, plus, minus, capture); + } catch (error) { + if (typeof error === "object" && typeof error.message === "string") { + error.message = "Property change dispatch possibly corrupted by error: " + error.message; + throw error; + } else { + throw new Error("Property change dispatch possibly corrupted by error: " + error); + } + } finally { + dispatching = false; + if (observerToFreeList.length) { + // Using push.apply instead of addEach because push will definitely + // be much faster than the generic addEach, which also handles + // non-array collections. + observerFreeList.push.apply( + observerFreeList, + observerToFreeList + ); + // Using clear because it is observable. The handler record array + // is obtainable by getPropertyChangeObservers, and is observable. + observerToFreeList.clear(); + } } } -/** - */ exports.getPropertyChangeObservers = getPropertyChangeObservers; -function getPropertyChangeObservers(object, name) { - if (!handlerRecordsByObject.has(object)) { - handlerRecordsByObject.set(object, {}); +function getPropertyChangeObservers(object, name, capture) { + if (!observersByObject.has(object)) { + observersByObject.set(object, {}); } - var handlersByName = handlerRecordsByObject.get(object); - if (!Object.owns(handlersByName, name)) { - handlersByName[name] = []; + var observersByName = observersByObject.get(object); + var phase = capture ? "WillChange" : "Change"; + var key = name + phase; + if (!Object.owns(observersByName, key)) { + observersByName[key] = []; } - return handlersByName[name]; + return observersByName[key]; } -/** - */ +exports.getPropertyWillChangeObservers = getPropertyWillChangeObservers; +function getPropertyWillChangeObservers(object, name) { + return getPropertyChangeObservers(object, name, true); +} + +exports.PropertyChangeObserver = PropertyChangeObserver; +function PropertyChangeObserver() { + this.initialize(); + // Object.seal(this); // Maybe one day, this won't deoptimize. +} + +PropertyChangeObserver.prototype.initialize = function () { + // Peer observers, from which to pluck itself upon cancelation. + this.observers = null; + // On which to dispatch property change notifications. + this.handler = null; + // Returned by the last property change notification, which must be + // canceled before the next change notification, or when this observer is + // finally canceled. + this.childObserver = null; + // For the discretionary use of the user, perhaps to track why this + // observer has been created, or whether this observer should be + // serialized. + this.note = null; +}; + +PropertyChangeObserver.prototype.cancel = function () { + var observers = this.observers; + var index = observers.indexOf(this); + if (index >= 0) { + var childObserver = this.childObserver; + observers.splice(index, 1); + this.initialize(); + // If this observer is canceled while dispatching a change + // notification for the same property... + // 1. We cannot put the handler record onto the free list because + // it may have been captured in the array of records to which + // the change notification would be sent. We must mark it as + // canceled by nulling out the handler property so the dispatcher + // passes over it. + // 2. We also cannot put the handler record onto the free list + // until all change dispatches have been completed because it could + // conceivably be reused, confusing the current dispatcher. + if (dispatching) { + // All handlers added to this list will be moved over to the + // actual free list when there are no longer any property + // change dispatchers on the stack. + observerToFreeList.push(this); + } else { + observerFreeList.push(this); + } + if (childObserver) { + // Calling user code on our stack. + // Done in tail position to avoid a plan interference hazard. + childObserver.cancel(); + } + } +}; + exports.makePropertyObservable = makePropertyObservable; function makePropertyObservable(object, name) { // arrays are special. we do not support direct setting of properties @@ -100,11 +206,12 @@ function makePropertyObservable(object, name) { return; } - // memoize super property descriptor table - if (!superObjectDescriptors.has(object)) { - superPropertyDescriptors = {}; - superObjectDescriptors.set(object, superPropertyDescriptors); + if (superObjectDescriptors.has(object)) { + return; } + + superPropertyDescriptors = {}; + superObjectDescriptors.set(object, superPropertyDescriptors); var superPropertyDescriptors = superObjectDescriptors.get(object); if (Object.owns.call(superPropertyDescriptors, name)) { @@ -186,7 +293,6 @@ function getSuperPropertyDescriptor(object, name) { writable: true, configurable: true }; - } function makeValuePropertyThunk(name, state, superDescriptor) { @@ -199,9 +305,16 @@ function makeValuePropertyThunk(name, state, superDescriptor) { return plus; } var minus = superDescriptor.value; + + // XXX plan interference hazard: + dispatchPropertyWillChange(this, name, plus, minus); + superDescriptor.value = plus; state[name] = plus; + + // XXX plan interference hazard: dispatchPropertyChange(this, name, plus, minus); + return plus; }, enumerable: superDescriptor.enumerable, @@ -224,6 +337,9 @@ function makeGetSetPropertyThunk(name, state, superDescriptor) { minus = superDescriptor.get.apply(this, arguments); } + // XXX plan interference hazard: + dispatchPropertyWillChange(this, name, plus, minus); + // call through to actual setter if (superDescriptor.set) { superDescriptor.set.apply(this, arguments); @@ -243,6 +359,8 @@ function makeGetSetPropertyThunk(name, state, superDescriptor) { // dispatch the new value: the given value if there is // no getter, or the actual value if there is one + // TODO spec + // XXX plan interference hazard: dispatchPropertyChange(this, name, plus, minus); return plus; diff --git a/spec/listen/property-changes-spec.js b/spec/listen/property-changes-spec.js index 5ab5576..cc31bcd 100644 --- a/spec/listen/property-changes-spec.js +++ b/spec/listen/property-changes-spec.js @@ -21,9 +21,9 @@ describe("PropertyChanges", function () { }); object.x = 10; expect(object.x).toEqual(10); - PropertyChanges.makePropertyUnobservable(object, 'x'); - object.x = 20; - expect(object.x).toEqual(20); + //PropertyChanges.makePropertyUnobservable(object, 'x'); + //object.x = 20; + //expect(object.x).toEqual(20); expect(spy.argsForCall).toEqual([ ['from', undefined, 'x'], ['to', 10, 'x'], diff --git a/spec/observe-property-changes-spec.js b/spec/observe-property-changes-spec.js new file mode 100644 index 0000000..dfdd1fd --- /dev/null +++ b/spec/observe-property-changes-spec.js @@ -0,0 +1,267 @@ + +/* + * Based in part on observable arrays from Motorola Mobility’s Montage + * Copyright (c) 2012, Motorola Mobility LLC. All Rights Reserved. + * 3-Clause BSD License + * https://github.com/motorola-mobility/montage/blob/master/LICENSE.md + */ + +// TODO observePropertyWillChange +// TODO access observer notes + +var ObservePropertyChanges = require("../observe-property-changes"); +var observePropertyChange = ObservePropertyChanges.observePropertyChange; + +describe("ObservePropertyChanges", function () { + + describe("observePropertyChange", function () { + + it("property change", function () { + var object = {}; + var spy = jasmine.createSpy(); + var observer = observePropertyChange(object, "foo", function (plus, minus, name, object) { + spy(plus, minus, name, object); + }); + object.foo = 10; + expect(spy).toHaveBeenCalledWith(10, undefined, "foo", object); + }); + + it("property non-change", function () { + var object = {foo: 10}; + var spy = jasmine.createSpy(); + var observer = observePropertyChange(object, "foo", function (plus, minus, name, object) { + spy(plus, minus, name, object); + }); + object.foo = 10; + expect(spy).not.toHaveBeenCalled(); + }); + + it("property change, property non-change", function () { + var object = {}; + var spy = jasmine.createSpy(); + var observer = observePropertyChange(object, "foo", function (plus, minus, name, object) { + spy(plus, minus, name, object); + }); + object.foo = 10; + expect(spy).toHaveBeenCalledWith(10, undefined, "foo", object); + + spy = jasmine.createSpy(); + object.foo = 10; + expect(spy).not.toHaveBeenCalled(); + }); + + it("property change, observer", function () { + var object = {}; + var spy = jasmine.createSpy(); + var observer = observePropertyChange(object, "foo", function (plus, minus, name, object) { + spy(plus, minus, name, object); + }); + object.foo = 10; + + observer.cancel(); + spy = jasmine.createSpy(); + object.foo = 20; + expect(spy).not.toHaveBeenCalled(); + }); + + it("just observer", function () { + var object = {}; + var observer = observePropertyChange(object, "foo", function (plus, minus, name, object) { + spy(plus, minus, name, object); + }); + + observer.cancel(); + spy = jasmine.createSpy(); + object.foo = 20; + expect(spy).not.toHaveBeenCalled(); + }); + + it("multiple observers", function () { + var object = {}; + var spy1 = jasmine.createSpy(); + var observer1 = observePropertyChange(object, "foo", function (plus, minus, name, object) { + spy1(plus, minus, name, object); + }); + var spy2 = jasmine.createSpy(); + var observer2 = observePropertyChange(object, "foo", function (plus, minus, name, object) { + spy2(plus, minus, name, object); + }); + object.foo = 10; + expect(spy1).toHaveBeenCalledWith(10, undefined, "foo", object); + expect(spy2).toHaveBeenCalledWith(10, undefined, "foo", object); + }); + + it("multiple observers, one observered", function () { + var object = {}; + var spy1 = jasmine.createSpy(); + var observer1 = observePropertyChange(object, "foo", function (plus, minus, name, object) { + spy1(plus, minus, name, object); + }); + var spy2 = jasmine.createSpy(); + var observer2 = observePropertyChange(object, "foo", function (plus, minus, name, object) { + spy2(plus, minus, name, object); + }); + observer1.cancel(); + object.foo = 10; + expect(spy1).not.toHaveBeenCalled(); + expect(spy2).toHaveBeenCalledWith(10, undefined, "foo", object); + }); + + it("multiple observers, other observered", function () { + var object = {}; + var spy1 = jasmine.createSpy(); + var observer1 = observePropertyChange(object, "foo", function (plus, minus, name, object) { + spy1(plus, minus, name, object); + }); + var spy2 = jasmine.createSpy(); + var observer2 = observePropertyChange(object, "foo", function (plus, minus, name, object) { + spy2(plus, minus, name, object); + }); + observer2.cancel(); + object.foo = 10; + expect(spy1).toHaveBeenCalledWith(10, undefined, "foo", object); + expect(spy2).not.toHaveBeenCalled(); + }); + + it("multiple observers, both observered", function () { + var object = {}; + var spy1 = jasmine.createSpy(); + var observer1 = observePropertyChange(object, "foo", function (plus, minus, name, object) { + spy1(plus, minus, name, object); + }); + var spy2 = jasmine.createSpy(); + var observer2 = observePropertyChange(object, "foo", function (plus, minus, name, object) { + spy2(plus, minus, name, object); + }); + observer1.cancel(); + observer2.cancel(); + object.foo = 10; + expect(spy1).not.toHaveBeenCalled(); + expect(spy2).not.toHaveBeenCalled(); + }); + + it("observe, observer, observe", function () { + var object = {}; + var spy1 = jasmine.createSpy(); + var observer1 = observePropertyChange(object, "foo", function (plus, minus, name, object) { + spy1(plus, minus, name, object); + }); + observer1.cancel(); + var spy2 = jasmine.createSpy(); + var observer2 = observePropertyChange(object, "foo", function (plus, minus, name, object) { + spy2(plus, minus, name, object); + }); + object.foo = 10; + expect(spy1).not.toHaveBeenCalled(); + expect(spy2).toHaveBeenCalledWith(10, undefined, "foo", object); + }); + + it("dispatches to generic handler method", function () { + var object = { + foo: 10, + handlePropertyChange: function (plus, minus, name, object) { + spy(plus, minus, name, object); + } + }; + var observer = observePropertyChange(object, "foo", object); + var spy = jasmine.createSpy(); + object.foo = 20; + expect(spy).toHaveBeenCalledWith(20, 10, "foo", object); + }); + + it("dispatches to specific handler method", function () { + var object = { + foo: 10, + handleFooPropertyChange: function (plus, minus, name, object) { + spy(plus, minus, name, object); + } + }; + var observer = observePropertyChange(object, "foo", object); + var spy = jasmine.createSpy(); + object.foo = 20; + expect(spy).toHaveBeenCalledWith(20, 10, "foo", object); + }); + + it("is robust against observeration of an intermediate observer", function () { + var object = {foo: 10}; + var spy1 = jasmine.createSpy(); + var observer1 = observePropertyChange(object, "foo", function (plus, minus, name, object) { + spy1(plus, minus, name, object); + if (observer2) observer2.cancel(); + }); + var spy2 = jasmine.createSpy(); + var observer2 = observePropertyChange(object, "foo", spy2); + var spy3 = jasmine.createSpy(); + var observer3 = observePropertyChange(object, "foo", spy3); + var spy4 = jasmine.createSpy(); + var observer4 = observePropertyChange(object, "foo", spy4); + expect(spy1.callCount).toBe(0); + expect(spy2.callCount).toBe(0); + expect(spy3.callCount).toBe(0); + object.foo = 20; + expect(spy1.callCount).toBe(1); + expect(spy2.callCount).toBe(0); + expect(spy3.callCount).toBe(1); + }); + + it("is robust against property changes during dispatch of a property change", function () { + var object = {}; + var spy = jasmine.createSpy(); + var observer = observePropertyChange(object, "foo", function (plus, minus, name, object) { + if (object.foo >= 10) { + return observer.cancel(); + } + spy(); + object.foo = object.foo + 1; + }); + object.foo = 0; + expect(spy.callCount).toBe(10); + }); + + it("should observer nested observer", function () { + var object = {}; + var spy = jasmine.createSpy(); + var innerCancel = jasmine.createSpy(); + var observer = observePropertyChange(object, "foo", function () { + spy(); + return {cancel: innerCancel}; + }); + + expect(spy.callCount).toBe(0); + expect(innerCancel.callCount).toBe(0); + + object.foo = 10; + expect(spy.callCount).toBe(1); + expect(innerCancel.callCount).toBe(0); + + object.foo = 20; + expect(spy.callCount).toBe(2); + expect(innerCancel.callCount).toBe(1); + + observer.cancel(); + expect(spy.callCount).toBe(2); + expect(innerCancel.callCount).toBe(2); + }); + + it("should note exceptions that interrupt property change dispatch", function () { + var object = {}; + var observer = observePropertyChange(object, "foo", function (child) { + throw new Error("X"); + }); + var spy = jasmine.createSpy(); + observePropertyChange(object, "foo", spy); + var error; + try { + object.foo = 10; + } catch (_error) { + error = _error; + } + expect(error && error.message).toBe("Property change dispatch possibly corrupted by error: X"); + // it was indeed, thanks for the warning. + expect(spy).not.toHaveBeenCalled(); + }); + + }); + +}); + diff --git a/spec/observe/property-changes-spec.js b/spec/observe/property-changes-spec.js deleted file mode 100644 index 0dd5801..0000000 --- a/spec/observe/property-changes-spec.js +++ /dev/null @@ -1,173 +0,0 @@ - -/* - Based in part on observable arrays from Motorola Mobility’s Montage - Copyright (c) 2012, Motorola Mobility LLC. All Rights Reserved. - 3-Clause BSD License - https://github.com/motorola-mobility/montage/blob/master/LICENSE.md -*/ - -require("../../shim"); -var ObservePropertyChanges = require("../../observe-property-changes"); -var observeProperty = ObservePropertyChanges.observeProperty; - -describe("ObservePropertyChanges", function () { - - describe("observeProperty", function () { - - it("property change", function () { - var object = {}; - var spy = jasmine.createSpy(); - var cancel = observeProperty(object, "foo", function (plus, minus, name, object) { - spy(plus, minus, name, object); - }); - object.foo = 10; - expect(spy).toHaveBeenCalledWith(10, undefined, "foo", object); - }); - - it("property non-change", function () { - var object = {foo: 10}; - var spy = jasmine.createSpy(); - var cancel = observeProperty(object, "foo", function (plus, minus, name, object) { - spy(plus, minus, name, object); - }); - object.foo = 10; - expect(spy).not.toHaveBeenCalled(); - }); - - it("property change, property non-change", function () { - var object = {}; - var spy = jasmine.createSpy(); - var cancel = observeProperty(object, "foo", function (plus, minus, name, object) { - spy(plus, minus, name, object); - }); - object.foo = 10; - expect(spy).toHaveBeenCalledWith(10, undefined, "foo", object); - - spy = jasmine.createSpy(); - object.foo = 10; - expect(spy).not.toHaveBeenCalled(); - }); - - it("property change, cancel", function () { - var object = {}; - var spy = jasmine.createSpy(); - var cancel = observeProperty(object, "foo", function (plus, minus, name, object) { - spy(plus, minus, name, object); - }); - object.foo = 10; - - cancel(); - spy = jasmine.createSpy(); - object.foo = 20; - expect(spy).not.toHaveBeenCalled(); - }); - - it("just cancel", function () { - var object = {}; - var cancel = observeProperty(object, "foo", function (plus, minus, name, object) { - spy(plus, minus, name, object); - }); - - cancel(); - spy = jasmine.createSpy(); - object.foo = 20; - expect(spy).not.toHaveBeenCalled(); - }); - - it("multiple observers", function () { - var object = {}; - var spy1 = jasmine.createSpy(); - var cancel1 = observeProperty(object, "foo", function (plus, minus, name, object) { - spy1(plus, minus, name, object); - }); - var spy2 = jasmine.createSpy(); - var cancel2 = observeProperty(object, "foo", function (plus, minus, name, object) { - spy2(plus, minus, name, object); - }); - object.foo = 10; - expect(spy1).toHaveBeenCalledWith(10, undefined, "foo", object); - expect(spy2).toHaveBeenCalledWith(10, undefined, "foo", object); - }); - - it("multiple observers, one canceled", function () { - var object = {}; - var spy1 = jasmine.createSpy(); - var cancel1 = observeProperty(object, "foo", function (plus, minus, name, object) { - spy1(plus, minus, name, object); - }); - var spy2 = jasmine.createSpy(); - var cancel2 = observeProperty(object, "foo", function (plus, minus, name, object) { - spy2(plus, minus, name, object); - }); - cancel1(); - object.foo = 10; - expect(spy1).not.toHaveBeenCalled(); - expect(spy2).toHaveBeenCalledWith(10, undefined, "foo", object); - }); - - it("multiple observers, other canceled", function () { - var object = {}; - var spy1 = jasmine.createSpy(); - var cancel1 = observeProperty(object, "foo", function (plus, minus, name, object) { - spy1(plus, minus, name, object); - }); - var spy2 = jasmine.createSpy(); - var cancel2 = observeProperty(object, "foo", function (plus, minus, name, object) { - spy2(plus, minus, name, object); - }); - cancel2(); - object.foo = 10; - expect(spy1).toHaveBeenCalledWith(10, undefined, "foo", object); - expect(spy2).not.toHaveBeenCalled(); - }); - - it("multiple observers, both canceled", function () { - var object = {}; - var spy1 = jasmine.createSpy(); - var cancel1 = observeProperty(object, "foo", function (plus, minus, name, object) { - spy1(plus, minus, name, object); - }); - var spy2 = jasmine.createSpy(); - var cancel2 = observeProperty(object, "foo", function (plus, minus, name, object) { - spy2(plus, minus, name, object); - }); - cancel1(); - cancel2(); - object.foo = 10; - expect(spy1).not.toHaveBeenCalled(); - expect(spy2).not.toHaveBeenCalled(); - }); - - it("observe, cancel, observe", function () { - var object = {}; - var spy1 = jasmine.createSpy(); - var cancel1 = observeProperty(object, "foo", function (plus, minus, name, object) { - spy1(plus, minus, name, object); - }); - cancel1(); - var spy2 = jasmine.createSpy(); - var cancel2 = observeProperty(object, "foo", function (plus, minus, name, object) { - spy2(plus, minus, name, object); - }); - object.foo = 10; - expect(spy1).not.toHaveBeenCalled(); - expect(spy2).toHaveBeenCalledWith(10, undefined, "foo", object); - }); - - it("dispatches to specific handler method", function () { - var object = { - foo: 10, - handleFooChange: function (plus, minus, name, object) { - spy(plus, minus, name, object); - } - }; - var cancel = observeProperty(object, "foo", object); - var spy = jasmine.createSpy(); - object.foo = 20; - expect(spy).toHaveBeenCalledWith(20, 10, "foo", object); - }); - - }); - -}); - From 6c80714463a921add1ddcd583efd5501cc4f7253 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Mon, 6 Jan 2014 13:36:07 -0800 Subject: [PATCH 07/83] Factor dispatch code into observer method Retain last seen value, precompute dispatch method. --- observe-property-changes.js | 186 +++++++++++++++++++++++------------- 1 file changed, 122 insertions(+), 64 deletions(-) diff --git a/observe-property-changes.js b/observe-property-changes.js index aeb3d94..87ea184 100644 --- a/observe-property-changes.js +++ b/observe-property-changes.js @@ -22,16 +22,54 @@ exports.observePropertyChange = observePropertyChange; function observePropertyChange(object, name, handler, note, capture) { makePropertyObservable(object, name); var observers = getPropertyChangeObservers(object, name, capture); + var observer; if (observerFreeList.length) { observer = observerFreeList.pop(); } else { observer = new PropertyChangeObserver(); } + + observer.object = object; + observer.propertyName = name; + observer.capture = capture; observer.observers = observers; observer.handler = handler; observer.note = note; + + // Precompute dispatch method names. + + var stringName = "" + name; // Array indicides must be coerced to string. + var propertyName = stringName.slice(0, 1).toUpperCase() + stringName.slice(1); + + if (!capture) { + var specificChangeMethodName = "handle" + propertyName + "PropertyChange"; + var genericChangeMethodName = "handlePropertyChange"; + if (handler[specificChangeMethodName]) { + observer.handlerChangeMethodName = specificChangeMethodName; + } else if (handler[genericChangeMethodName]) { + observer.handlerChangeMethodName = genericChangeMethodName; + } else if (handler.call) { + observer.handlerChangeMethodName = null; + } else { + throw new Error("Can't arrange to dispatch " + JSON.stringify(name) + " property changes on " + object); + } + } else { + var specificWillChangeMethodName = "handle" + propertyName + "PropertyWillChange"; + var genericWillChangeMethodName = "handlePropertyWillChange"; + if (handler[specificWillChangeMethodName]) { + observer.handlerChangeMethodName = specificWillChangeMethodName; + } else if (handler[genericWillChangeMethodName]) { + observer.handlerChangeMethodName = genericWillChangeMethodName; + } else if (handler.call) { + observer.handlerChangeMethodName = null; + } else { + throw new Error("Can't arrange to dispatch " + JSON.stringify(name) + " property changes on " + object); + } + } + observers.push(observer); + // TODO issue warnings if the number of handler records exceeds some // concerning quantity as a harbinger of a memory leak. // TODO Note that if this is garbage collected without ever being called, @@ -49,40 +87,10 @@ function dispatchPropertyChange(object, name, plus, minus, capture) { if (!dispatching) { return startPropertyChangeDispatchContext(object, name, plus, minus, capture); } - var phase = capture ? "WillChange" : "Change"; - var genericHandlerMethodName = "handleProperty" + phase; - var propertyName = "" + name; // array indicies must be coerced - var specificHandlerMethodName = "handle" + propertyName.slice(0, 1).toUpperCase() + propertyName.slice(1) + "Property" + phase; var observers = getPropertyChangeObservers(object, name, capture).slice(); for (var index = 0; index < observers.length; index++) { var observer = observers[index]; - var handler = observer.handler; - // A null handler implies that an observer was canceled during the - // dispatch of a change. The handler record is pending addition to the - // free list. - if (!handler) { - continue; - } - var childObserver = observer.childObserver; - observer.childObserver = null; - // XXX plan interference hazards calling cancel and handler methods: - if (childObserver) { - childObserver.cancel(); - } - if (handler[specificHandlerMethodName]) { - childObserver = handler[specificHandlerMethodName](plus, minus, name, object); - } else if (handler[genericHandlerMethodName]) { - childObserver = handler[genericHandlerMethodName](plus, minus, name, object); - } else if (handler.call) { - childObserver = handler.call(observer, plus, minus, name, object); - } else { - throw new Error( - "Can't dispatch " + JSON.stringify(specificHandlerMethodName) + - " or " + JSON.stringify(genericHandlerMethodName) + - " on " + object - ); - } - observer.childObserver = childObserver; + observer.dispatch(plus, minus); } } @@ -122,15 +130,15 @@ function startPropertyChangeDispatchContext(object, name, plus, minus, capture) exports.getPropertyChangeObservers = getPropertyChangeObservers; function getPropertyChangeObservers(object, name, capture) { if (!observersByObject.has(object)) { - observersByObject.set(object, {}); + observersByObject.set(object, Object.create(null)); } - var observersByName = observersByObject.get(object); + var observersByKey = observersByObject.get(object); var phase = capture ? "WillChange" : "Change"; var key = name + phase; - if (!Object.owns(observersByName, key)) { - observersByName[key] = []; + if (!Object.owns(observersByKey, key)) { + observersByKey[key] = []; } - return observersByName[key]; + return observersByKey[key]; } exports.getPropertyWillChangeObservers = getPropertyWillChangeObservers; @@ -145,10 +153,14 @@ function PropertyChangeObserver() { } PropertyChangeObserver.prototype.initialize = function () { + this.object = null; + this.propertyName = null; // Peer observers, from which to pluck itself upon cancelation. this.observers = null; // On which to dispatch property change notifications. this.handler = null; + // Precomputed handler method name for change dispatch + this.handlerChangeMethodName = null; // Returned by the last property change notification, which must be // canceled before the next change notification, or when this observer is // finally canceled. @@ -157,39 +169,85 @@ PropertyChangeObserver.prototype.initialize = function () { // observer has been created, or whether this observer should be // serialized. this.note = null; + // Whether this observer dispatches before a change occurs, or after + this.capture = null; + // The last known value + this.value = null; }; PropertyChangeObserver.prototype.cancel = function () { var observers = this.observers; var index = observers.indexOf(this); - if (index >= 0) { - var childObserver = this.childObserver; - observers.splice(index, 1); - this.initialize(); - // If this observer is canceled while dispatching a change - // notification for the same property... - // 1. We cannot put the handler record onto the free list because - // it may have been captured in the array of records to which - // the change notification would be sent. We must mark it as - // canceled by nulling out the handler property so the dispatcher - // passes over it. - // 2. We also cannot put the handler record onto the free list - // until all change dispatches have been completed because it could - // conceivably be reused, confusing the current dispatcher. - if (dispatching) { - // All handlers added to this list will be moved over to the - // actual free list when there are no longer any property - // change dispatchers on the stack. - observerToFreeList.push(this); - } else { - observerFreeList.push(this); - } - if (childObserver) { - // Calling user code on our stack. - // Done in tail position to avoid a plan interference hazard. - childObserver.cancel(); - } + // Unfortunately, if this observer was reused, this would not be sufficient + // to detect a duplicate cancel. Do not cancel more than once. + if (index < 0) { + throw new Error( + "Can't cancel observer for " + + JSON.stringify(this.propertyName) + " on " + this.object + + " because it has already been canceled" + ); + } + var childObserver = this.childObserver; + observers.splice(index, 1); + this.initialize(); + // If this observer is canceled while dispatching a change + // notification for the same property... + // 1. We cannot put the handler record onto the free list because + // it may have been captured in the array of records to which + // the change notification would be sent. We must mark it as + // canceled by nulling out the handler property so the dispatcher + // passes over it. + // 2. We also cannot put the handler record onto the free list + // until all change dispatches have been completed because it could + // conceivably be reused, confusing the current dispatcher. + if (dispatching) { + // All handlers added to this list will be moved over to the + // actual free list when there are no longer any property + // change dispatchers on the stack. + observerToFreeList.push(this); + } else { + observerFreeList.push(this); + } + if (childObserver) { + // Calling user code on our stack. + // Done in tail position to avoid a plan interference hazard. + childObserver.cancel(); + } +}; + +PropertyChangeObserver.prototype.dispatch = function (plus, minus) { + var handler = this.handler; + // A null handler implies that an observer was canceled during the dispatch + // of a change. The observer is pending addition to the free list. + if (!handler) { + return; + } + + // Retain the last seen value for debugging + if (this.capture) { + this.value = minus; + } else { + this.value = plus; + } + + var childObserver = this.childObserver; + this.childObserver = null; + // XXX plan interference hazards calling cancel and handler methods: + if (childObserver) { + childObserver.cancel(); + } + var changeMethodName = this.handlerChangeMethodName; + if (handler[changeMethodName]) { + childObserver = handler[changeMethodName](plus, minus, this.propertyName, this.object); + } else if (handler.call) { + childObserver = handler.call(this, plus, minus, this.propertyName, this.object); + } else { + throw new Error( + "Can't dispatch " + JSON.stringify(changeMethodName) + " property change on " + object + ); } + this.childObserver = childObserver; + return this; }; exports.makePropertyObservable = makePropertyObservable; @@ -214,7 +272,7 @@ function makePropertyObservable(object, name) { superObjectDescriptors.set(object, superPropertyDescriptors); var superPropertyDescriptors = superObjectDescriptors.get(object); - if (Object.owns.call(superPropertyDescriptors, name)) { + if (Object.owns(superPropertyDescriptors, name)) { // if we have already recorded an super property descriptor, // we have already installed the observer, so short-here return; From 06bb03622ea583c1ff5915b0b9cabfb30357c6aa Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Mon, 20 Jan 2014 12:01:30 -0800 Subject: [PATCH 08/83] Test manual property change dispatch --- observe-property-changes.js | 14 ++++++-------- spec/observe-property-changes-spec.js | 21 ++++++++++++++++++++- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/observe-property-changes.js b/observe-property-changes.js index 87ea184..28b9fb7 100644 --- a/observe-property-changes.js +++ b/observe-property-changes.js @@ -24,7 +24,7 @@ function observePropertyChange(object, name, handler, note, capture) { var observers = getPropertyChangeObservers(object, name, capture); var observer; - if (observerFreeList.length) { + if (observerFreeList.length) { // TODO && !debug? observer = observerFreeList.pop(); } else { observer = new PropertyChangeObserver(); @@ -36,6 +36,7 @@ function observePropertyChange(object, name, handler, note, capture) { observer.observers = observers; observer.handler = handler; observer.note = note; + observer.value = object[name]; // Precompute dispatch method names. @@ -215,7 +216,7 @@ PropertyChangeObserver.prototype.cancel = function () { } }; -PropertyChangeObserver.prototype.dispatch = function (plus, minus) { +PropertyChangeObserver.prototype.dispatch = function (plus) { var handler = this.handler; // A null handler implies that an observer was canceled during the dispatch // of a change. The observer is pending addition to the free list. @@ -224,11 +225,8 @@ PropertyChangeObserver.prototype.dispatch = function (plus, minus) { } // Retain the last seen value for debugging - if (this.capture) { - this.value = minus; - } else { - this.value = plus; - } + var minus = this.value; + this.value = plus; var childObserver = this.childObserver; this.childObserver = null; @@ -240,7 +238,7 @@ PropertyChangeObserver.prototype.dispatch = function (plus, minus) { if (handler[changeMethodName]) { childObserver = handler[changeMethodName](plus, minus, this.propertyName, this.object); } else if (handler.call) { - childObserver = handler.call(this, plus, minus, this.propertyName, this.object); + childObserver = handler.call(void 0, plus, minus, this.propertyName, this.object); } else { throw new Error( "Can't dispatch " + JSON.stringify(changeMethodName) + " property change on " + object diff --git a/spec/observe-property-changes-spec.js b/spec/observe-property-changes-spec.js index dfdd1fd..3dad4b7 100644 --- a/spec/observe-property-changes-spec.js +++ b/spec/observe-property-changes-spec.js @@ -218,7 +218,7 @@ describe("ObservePropertyChanges", function () { expect(spy.callCount).toBe(10); }); - it("should observer nested observer", function () { + it("should observe nested observer", function () { var object = {}; var spy = jasmine.createSpy(); var innerCancel = jasmine.createSpy(); @@ -261,6 +261,25 @@ describe("ObservePropertyChanges", function () { expect(spy).not.toHaveBeenCalled(); }); + it("handles manual dispatch", function () { + var object = {}; + var spy = jasmine.createSpy(); + var observer = observePropertyChange(object, "foo", function (value) { + spy.apply(this, arguments); + if (value === 2) { + observer.cancel(); + observer = null; + } + }); + expect(spy.callCount).toBe(0); + observer.dispatch(1); + expect(spy.callCount).toBe(1); + expect(spy).toHaveBeenCalledWith(1, undefined, "foo", object); + observer.dispatch(2); + expect(spy).toHaveBeenCalledWith(2, 1, "foo", object); + expect(observer).toBe(null); + }); + }); }); From 7bff2ccf46462542d5c819d8d934f3a862cfdc3b Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Tue, 21 Jan 2014 00:33:42 -0800 Subject: [PATCH 09/83] Further advancements on property change observers - Allow a thunk to be shared by instances if the prototype is made observable. - Fix a bug where observers on multiple properties of the same object would interfere. - Test coverage for the working edge case of an object with a setter but no getter. --- observe-property-changes.js | 226 ++++++++++++++------------ spec/observe-property-changes-spec.js | 99 +++++++++++ 2 files changed, 225 insertions(+), 100 deletions(-) diff --git a/observe-property-changes.js b/observe-property-changes.js index 28b9fb7..b5fd513 100644 --- a/observe-property-changes.js +++ b/observe-property-changes.js @@ -15,7 +15,7 @@ var WeakMap = require("weak-map"); var observersByObject = new WeakMap(); var observerFreeList = []; var observerToFreeList = []; -var superObjectDescriptors = new WeakMap(); +var wrappedObjectDescriptors = new WeakMap(); var dispatching = false; exports.observePropertyChange = observePropertyChange; @@ -84,26 +84,26 @@ function observePropertyWillChange(object, name, handler, note) { } exports.dispatchPropertyChange = dispatchPropertyChange; -function dispatchPropertyChange(object, name, plus, minus, capture) { +function dispatchPropertyChange(object, name, plus, capture) { if (!dispatching) { - return startPropertyChangeDispatchContext(object, name, plus, minus, capture); + return startPropertyChangeDispatchContext(object, name, plus, capture); } var observers = getPropertyChangeObservers(object, name, capture).slice(); for (var index = 0; index < observers.length; index++) { var observer = observers[index]; - observer.dispatch(plus, minus); + observer.dispatch(plus); } } exports.dispatchPropertyWillChange = dispatchPropertyWillChange; -function dispatchPropertyWillChange(object, name, plus, minus) { - dispatchPropertyChange(object, name, plus, minus, true); +function dispatchPropertyWillChange(object, name, plus) { + dispatchPropertyChange(object, name, plus, true); } -function startPropertyChangeDispatchContext(object, name, plus, minus, capture) { +function startPropertyChangeDispatchContext(object, name, plus, capture) { dispatching = true; try { - dispatchPropertyChange(object, name, plus, minus, capture); + dispatchPropertyChange(object, name, plus, capture); } catch (error) { if (typeof error === "object" && typeof error.message === "string") { error.message = "Property change dispatch possibly corrupted by error: " + error.message; @@ -149,11 +149,11 @@ function getPropertyWillChangeObservers(object, name) { exports.PropertyChangeObserver = PropertyChangeObserver; function PropertyChangeObserver() { - this.initialize(); + this.init(); // Object.seal(this); // Maybe one day, this won't deoptimize. } -PropertyChangeObserver.prototype.initialize = function () { +PropertyChangeObserver.prototype.init = function () { this.object = null; this.propertyName = null; // Peer observers, from which to pluck itself upon cancelation. @@ -190,7 +190,7 @@ PropertyChangeObserver.prototype.cancel = function () { } var childObserver = this.childObserver; observers.splice(index, 1); - this.initialize(); + this.init(); // If this observer is canceled while dispatching a change // notification for the same property... // 1. We cannot put the handler record onto the free list because @@ -224,7 +224,6 @@ PropertyChangeObserver.prototype.dispatch = function (plus) { return; } - // Retain the last seen value for debugging var minus = this.value; this.value = plus; @@ -250,9 +249,9 @@ PropertyChangeObserver.prototype.dispatch = function (plus) { exports.makePropertyObservable = makePropertyObservable; function makePropertyObservable(object, name) { - // arrays are special. we do not support direct setting of properties - // on an array. instead, call .set(index, value). this is observable. - // 'length' property is observable for all mutating methods because + // Arrays are special. We do not support direct setting of properties + // on an array. instead, call .set(index, value). This is observable. + // "length" property is observable for all mutating methods because // our overrides explicitly dispatch that change. if (Array.isArray(object)) { return; @@ -262,167 +261,194 @@ function makePropertyObservable(object, name) { return; } - if (superObjectDescriptors.has(object)) { + var wrappedDescriptor = getPropertyDescriptor(object, name); + var wrappedPrototype = wrappedDescriptor.prototype; + + var existingWrappedDescriptors = wrappedObjectDescriptors.get(wrappedPrototype); + if (existingWrappedDescriptors && Object.owns(existingWrappedDescriptors, name)) { return; } - superPropertyDescriptors = {}; - superObjectDescriptors.set(object, superPropertyDescriptors); - var superPropertyDescriptors = superObjectDescriptors.get(object); + if (!wrappedObjectDescriptors.has(object)) { + wrappedPropertyDescriptors = {}; + wrappedObjectDescriptors.set(object, wrappedPropertyDescriptors); + } + + var wrappedPropertyDescriptors = wrappedObjectDescriptors.get(object); - if (Object.owns(superPropertyDescriptors, name)) { - // if we have already recorded an super property descriptor, - // we have already installed the observer, so short-here + if (Object.owns(wrappedPropertyDescriptors, name)) { + // If we have already recorded a wrapped property descriptor, + // we have already installed the observer, so short-here. return; } - var superDescriptor = getSuperPropertyDescriptor(object, name); - - if (!superDescriptor.configurable) { + if (!wrappedDescriptor.configurable) { return; } - // memoize the descriptor so we know not to install another layer. we + // Memoize the descriptor so we know not to install another layer. We // could use it to uninstall the observer, but we do not to avoid GC // thrashing. - superPropertyDescriptors[name] = superDescriptor; + wrappedPropertyDescriptors[name] = wrappedDescriptor; - // give up *after* storing the super property descriptor so it - // can be restored by uninstall. Unwritable properties are - // silently not overriden. Since success is indistinguishable from + // Give up *after* storing the wrapped property descriptor so it + // can be restored by uninstall. Unwritable properties are + // silently not overriden. Since success is indistinguishable from // failure, we let it pass but don't waste time on intercepting // get/set. - if (!superDescriptor.writable && !superDescriptor.set) { + if (!wrappedDescriptor.writable && !wrappedDescriptor.set) { return; } - - // we put a __state__ property on every object where we're intercepting - // changes, so that folks can easily see the present value in their - // run-time inspector - var state; - if (typeof object.__state__ === "object") { - state = object.__state__; - } else { - state = {}; - if (Object.isExtensible(object, "__state__")) { - Object.defineProperty(object, "__state__", { - value: state, - writable: true, - enumerable: false - }); - } + // If there is no setter, it is not mutable, and observing is moot. + // Manual dispatch may still apply. + if (wrappedDescriptor.get && !wrappedDescriptor.set) { + return; } - state[name] = object[name]; var thunk; - // in both of these new descriptor variants, we reuse the super + // in both of these new descriptor variants, we reuse the wrapped // descriptor to either store the current value or apply getters - // and setters. this is handy since we can reuse the super - // descriptor if we uninstall the observer. We even preserve the + // and setters. this is handy since we can reuse the wrapped + // descriptor if we uninstall the observer. We even preserve the // assignment semantics, where we get the value from up the // prototype chain, and set as an owned property. - if ('value' in superDescriptor) { - thunk = makeValuePropertyThunk(name, state, superDescriptor); - } else { // 'get' or 'set', but not necessarily both - thunk = makeGetSetPropertyThunk(name, state, superDescriptor); + if ("value" in wrappedDescriptor) { + thunk = makeValuePropertyThunk(name, wrappedDescriptor); + } else { // "get" or "set", but not necessarily both + thunk = makeGetSetPropertyThunk(name, wrappedDescriptor); } Object.defineProperty(object, name, thunk); } -function getSuperPropertyDescriptor(object, name) { - // walk up the prototype chain to find a property descriptor for - // the property name - var superDescriptor; - var superObject = object; +function getPropertyDescriptor(object, name) { + // walk up the prototype chain to find a property descriptor for the + // property name. + var descriptor; + var prototype = object; do { - superDescriptor = Object.getOwnPropertyDescriptor(superObject, name); - if (superDescriptor) { + descriptor = Object.getOwnPropertyDescriptor(prototype, name); + if (descriptor) { break; } - superObject = Object.getPrototypeOf(superObject); - } while (superObject); - // or default to an undefined value - return superDescriptor || { - value: undefined, - enumerable: true, - writable: true, - configurable: true - }; + prototype = Object.getPrototypeOf(prototype); + } while (prototype); + if (descriptor) { + descriptor.prototype = prototype; + return descriptor; + } else { + // or default to an undefined value + return { + prototype: object, + value: undefined, + enumerable: true, + writable: true, + configurable: true + }; + } } -function makeValuePropertyThunk(name, state, superDescriptor) { +function makeValuePropertyThunk(name, wrappedDescriptor) { return { get: function () { - return superDescriptor.value; + // Uses __this__ to quickly distinguish __state__ properties from + // upward in the prototype chain. + if (this.__state__ === void 0 || this.__state__.__this__ !== this) { + initState(this); + // Get the initial value from up the prototype chain + this.__state__[name] = wrappedDescriptor.value; + } + var state = this.__state__; + + return state[name]; }, set: function (plus) { - if (plus === superDescriptor.value) { + // Uses __this__ to quickly distinguish __state__ properties from + // upward in the prototype chain. + if (this.__state__ === void 0 || this.__state__.__this__ !== this) { + initState(this); + this.__state__[name] = this[name]; + } + var state = this.__state__; + + if (plus === state[name]) { return plus; } - var minus = superDescriptor.value; // XXX plan interference hazard: - dispatchPropertyWillChange(this, name, plus, minus); + dispatchPropertyWillChange(this, name, plus); - superDescriptor.value = plus; + wrappedDescriptor.value = plus; state[name] = plus; // XXX plan interference hazard: - dispatchPropertyChange(this, name, plus, minus); + dispatchPropertyChange(this, name, plus); return plus; }, - enumerable: superDescriptor.enumerable, + enumerable: wrappedDescriptor.enumerable, configurable: true }; } -function makeGetSetPropertyThunk(name, state, superDescriptor) { +function makeGetSetPropertyThunk(name, wrappedDescriptor) { return { get: function () { - if (superDescriptor.get) { - return superDescriptor.get.apply(this, arguments); + if (wrappedDescriptor.get) { + return wrappedDescriptor.get.apply(this, arguments); } }, set: function (plus) { - var minus; + // Uses __this__ to quickly distinguish __state__ properties from + // upward in the prototype chain. + if (this.__state__ === void 0 || this.__state__.__this__ !== this) { + initState(this); + this.__state__[name] = this[name]; + } + var state = this.__state__; - // get the actual former value if possible - if (superDescriptor.get) { - minus = superDescriptor.get.apply(this, arguments); + if (state[name] === plus) { + return plus; } // XXX plan interference hazard: - dispatchPropertyWillChange(this, name, plus, minus); + dispatchPropertyWillChange(this, name, plus); // call through to actual setter - if (superDescriptor.set) { - superDescriptor.set.apply(this, arguments); - } - - // use getter, if possible, to discover whether the set - // was successful - if (superDescriptor.get) { - plus = superDescriptor.get.apply(this, arguments); + if (wrappedDescriptor.set) { + wrappedDescriptor.set.apply(this, arguments); state[name] = plus; } - // if it has not changed, suppress a notification - if (plus === minus) { - return plus; + // use getter, if possible, to adjust the plus value if the setter + // adjusted it, for example a setter for an array property that + // retains the original array and replaces its content, or a setter + // that coerces the value to an expected type. + if (wrappedDescriptor.get) { + plus = wrappedDescriptor.get.apply(this, arguments); } // dispatch the new value: the given value if there is // no getter, or the actual value if there is one // TODO spec // XXX plan interference hazard: - dispatchPropertyChange(this, name, plus, minus); + dispatchPropertyChange(this, name, plus); return plus; }, - enumerable: superDescriptor.enumerable, + enumerable: wrappedDescriptor.enumerable, configurable: true }; } +function initState(object) { + Object.defineProperty(object, "__state__", { + value: { + __this__: object + }, + writable: true, + enumerable: false, + configurable: true + }); +} + diff --git a/spec/observe-property-changes-spec.js b/spec/observe-property-changes-spec.js index 3dad4b7..4644765 100644 --- a/spec/observe-property-changes-spec.js +++ b/spec/observe-property-changes-spec.js @@ -11,6 +11,7 @@ var ObservePropertyChanges = require("../observe-property-changes"); var observePropertyChange = ObservePropertyChanges.observePropertyChange; +var makePropertyObservable = ObservePropertyChanges.makePropertyObservable; describe("ObservePropertyChanges", function () { @@ -280,6 +281,104 @@ describe("ObservePropertyChanges", function () { expect(observer).toBe(null); }); + it("operates on objects with getters and setters in the prototype", function () { + function Foo() { + this._foo = 0; + } + Object.defineProperty(Foo.prototype, "foo", { + get: function () { + return this._foo; + }, + set: function (foo) { + this._foo = +foo; + }, + configurable: true, + enumerable: true + }); + var object = new Foo(); + expect(object.foo).toBe(0); + + var spy = jasmine.createSpy(); + var observer = observePropertyChange(object, "foo", function (plus, minus, name, object) { + spy(plus, minus); + }); + expect(spy.callCount).toBe(0); + + object.foo = "10"; + expect(spy.callCount).toBe(1); + expect(spy).toHaveBeenCalledWith(10, 0); + }); + + it("operates on objects with merely a setter in the prototype", function () { + function Foo() { + this._foo = 0; + } + Object.defineProperty(Foo.prototype, "foo", { + set: function (foo) { + this._foo = +foo; + }, + configurable: true, + enumerable: true + }); + var object = new Foo(); + expect(object._foo).toBe(0); + + var spy = jasmine.createSpy(); + var observer = observePropertyChange(object, "foo", function (plus, minus, name, object) { + spy(plus, minus); + }); + expect(spy.callCount).toBe(0); + + object.foo = "10"; + expect(spy.callCount).toBe(1); + expect(spy).toHaveBeenCalledWith("10", undefined); + expect(object._foo).toBe(10); + + object.foo = "20"; + expect(spy.callCount).toBe(2); + expect(spy).toHaveBeenCalledWith("20", "10"); + expect(object._foo).toBe(20); + }); + + it("observes changes to different properties", function () { + var object = {}; + var fooSpy = jasmine.createSpy(); + var fooObserver = observePropertyChange(object, "foo", fooSpy); + var barSpy = jasmine.createSpy(); + var barObserver = observePropertyChange(object, "bar", barSpy); + + object.foo = 10; + expect(fooSpy).toHaveBeenCalledWith(10, undefined, "foo", object); + expect(barSpy).not.toHaveBeenCalled(); + + object.bar = "a"; + expect(barSpy).toHaveBeenCalledWith("a", undefined, "bar", object); + expect(fooSpy.callCount).toBe(1); + }); + + it("observes changes when a property is made observable on the prototype", function () { + function Foo() { + this.foo = 10; + } + makePropertyObservable(Foo.prototype, "foo"); + var object = new Foo(); + expect(object.foo).toBe(10); + + var observer = observePropertyChange(object, "foo", spy); + + object.foo = 20; + expect(object.hasOwnProperty("foo")).toBe(false); + expect(object.foo).toBe(20); + expect(spy).toHaveBeenCalledWith(20, 10, "foo", object); + + // non-interference + var other = new Foo(); + other.foo = 30; + expect(object.foo).toBe(20); + expect(spy.callCount).toBe(1); + + }); + }); }); From 68f0b15ce9e1b1f9dfb3c4d8bbacbbc9610fb462 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Sat, 25 Jan 2014 16:09:39 -0800 Subject: [PATCH 10/83] Support preventPropertyObserver --- observe-property-changes.js | 52 +++++++++++++++++++-------- spec/observe-property-changes-spec.js | 36 +++++++++++++++++++ 2 files changed, 73 insertions(+), 15 deletions(-) diff --git a/observe-property-changes.js b/observe-property-changes.js index b5fd513..24f2550 100644 --- a/observe-property-changes.js +++ b/observe-property-changes.js @@ -249,6 +249,40 @@ PropertyChangeObserver.prototype.dispatch = function (plus) { exports.makePropertyObservable = makePropertyObservable; function makePropertyObservable(object, name) { + var wrappedDescriptor = wrapPropertyDescriptor(object, name); + + if (!wrappedDescriptor) { + return; + } + + var thunk; + // in both of these new descriptor variants, we reuse the wrapped + // descriptor to either store the current value or apply getters + // and setters. this is handy since we can reuse the wrapped + // descriptor if we uninstall the observer. We even preserve the + // assignment semantics, where we get the value from up the + // prototype chain, and set as an owned property. + if ("value" in wrappedDescriptor) { + thunk = makeValuePropertyThunk(name, wrappedDescriptor); + } else { // "get" or "set", but not necessarily both + thunk = makeGetSetPropertyThunk(name, wrappedDescriptor); + } + + Object.defineProperty(object, name, thunk); +} + +/** + * Prevents a thunk from being installed on a property, assuming that the + * underlying type will dispatch the change manually, or intends the property + * to stick on all instances. + */ +exports.preventPropertyObserver = preventPropertyObserver; +function preventPropertyObserver(object, name) { + var wrappedDescriptor = wrapPropertyDescriptor(object, name); + Object.defineProperty(object, name, wrappedDescriptor); +} + +function wrapPropertyDescriptor(object, name) { // Arrays are special. We do not support direct setting of properties // on an array. instead, call .set(index, value). This is observable. // "length" property is observable for all mutating methods because @@ -299,26 +333,14 @@ function makePropertyObservable(object, name) { if (!wrappedDescriptor.writable && !wrappedDescriptor.set) { return; } + // If there is no setter, it is not mutable, and observing is moot. // Manual dispatch may still apply. if (wrappedDescriptor.get && !wrappedDescriptor.set) { return; } - var thunk; - // in both of these new descriptor variants, we reuse the wrapped - // descriptor to either store the current value or apply getters - // and setters. this is handy since we can reuse the wrapped - // descriptor if we uninstall the observer. We even preserve the - // assignment semantics, where we get the value from up the - // prototype chain, and set as an owned property. - if ("value" in wrappedDescriptor) { - thunk = makeValuePropertyThunk(name, wrappedDescriptor); - } else { // "get" or "set", but not necessarily both - thunk = makeGetSetPropertyThunk(name, wrappedDescriptor); - } - - Object.defineProperty(object, name, thunk); + return wrappedDescriptor; } function getPropertyDescriptor(object, name) { @@ -341,7 +363,7 @@ function getPropertyDescriptor(object, name) { return { prototype: object, value: undefined, - enumerable: true, + enumerable: false, writable: true, configurable: true }; diff --git a/spec/observe-property-changes-spec.js b/spec/observe-property-changes-spec.js index 4644765..dcccd75 100644 --- a/spec/observe-property-changes-spec.js +++ b/spec/observe-property-changes-spec.js @@ -12,6 +12,8 @@ var ObservePropertyChanges = require("../observe-property-changes"); var observePropertyChange = ObservePropertyChanges.observePropertyChange; var makePropertyObservable = ObservePropertyChanges.makePropertyObservable; +var preventPropertyObserver = ObservePropertyChanges.preventPropertyObserver; +var dispatchPropertyChange = ObservePropertyChanges.dispatchPropertyChange; describe("ObservePropertyChanges", function () { @@ -379,6 +381,40 @@ describe("ObservePropertyChanges", function () { }); + it("should not alter a property marked as observable", function () { + var object = {}; + preventPropertyObserver(object, "foo"); + expect(Object.getOwnPropertyDescriptor(object, "foo")).toEqual({ + value: undefined, + writable: true, + enumerable: false, + configurable: true + }); + var spy = jasmine.createSpy(); + var observer = observePropertyChange(object, "foo", spy); + object.foo = 10; + expect(spy).not.toHaveBeenCalled(); + dispatchPropertyChange(object, "foo", 10); + expect(spy).toHaveBeenCalledWith(10, undefined, "foo", object); + }); + + it("should not alter a property marked as observable on the prototype", function () { + function Foo() { + } + preventPropertyObserver(Foo.prototype, "foo"); + var object = new Foo(); + expect(object.hasOwnProperty("foo")).toBe(false); + var spy = jasmine.createSpy(); + var observer = observePropertyChange(object, "foo", spy); + expect(object.hasOwnProperty("foo")).toBe(false); + expect(object.foo).toBe(undefined); + expect(Object.getOwnPropertyDescriptor(object, "foo")).toBe(undefined); + object.foo = 10; + expect(spy).not.toHaveBeenCalled(); + dispatchPropertyChange(object, "foo", 10); + expect(spy).toHaveBeenCalledWith(10, undefined, "foo", object); + }); + }); }); From ad0a2bd93b8b2a640d349b6a2b016ec83d5378ba Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Mon, 27 Jan 2014 12:39:28 -0800 Subject: [PATCH 11/83] Finish refactoring of listeners to observers This adds support for map, range, and array observers, with a design coherent with the new property observers. --- dict.js | 12 +- generic-map.js | 25 +- heap.js | 40 +- list.js | 26 +- listen/property-changes.js | 10 +- lru-map.js | 12 +- lru-set.js | 12 +- observable-array.js | 357 ++++++++++++++ observable-map.js | 222 +++++++++ ...roperty-changes.js => observable-object.js | 164 +++++-- observable-range.js | 221 +++++++++ spec/array-spec.js | 13 - spec/dict.js | 9 - spec/heap-spec.js | 4 +- spec/list-spec.js | 2 +- spec/listen/array-changes-spec.js | 438 ------------------ spec/listen/map-changes.js | 58 --- spec/listen/property-changes-spec.js | 172 ------- spec/listen/range-changes.js | 303 ------------ spec/lru-map-spec.js | 13 +- spec/lru-set-spec.js | 10 +- spec/map-spec.js | 2 - spec/map.js | 6 +- spec/observable-array-spec.js | 316 +++++++++++++ spec/observable-map-spec.js | 60 +++ spec/observable-map.js | 31 ++ ...nges-spec.js => observable-object-spec.js} | 12 +- spec/observable-range-spec.js | 23 + spec/observable-range.js | 8 + spec/shim-functions-spec.js | 1 + spec/sorted-array-map-spec.js | 4 +- 31 files changed, 1467 insertions(+), 1119 deletions(-) create mode 100644 observable-array.js create mode 100644 observable-map.js rename observe-property-changes.js => observable-object.js (75%) create mode 100644 observable-range.js delete mode 100644 spec/listen/array-changes-spec.js delete mode 100644 spec/listen/map-changes.js delete mode 100644 spec/listen/property-changes-spec.js delete mode 100644 spec/listen/range-changes.js create mode 100644 spec/observable-array-spec.js create mode 100644 spec/observable-map-spec.js create mode 100644 spec/observable-map.js rename spec/{observe-property-changes-spec.js => observable-object-spec.js} (97%) create mode 100644 spec/observable-range-spec.js create mode 100644 spec/observable-range.js diff --git a/dict.js b/dict.js index 72449cc..fea6329 100644 --- a/dict.js +++ b/dict.js @@ -58,23 +58,25 @@ Dict.prototype.get = function (key, defaultValue) { Dict.prototype.set = function (key, value) { this.assertString(key); var mangled = mangle(key); + var from; if (mangled in this.store) { // update - if (this.dispatchesBeforeMapChanges) { - this.dispatchBeforeMapChange(key, this.store[mangled]); + if (this.dispatchesMapChanges) { + from = this.store[mangled]; + this.dispatchMapWillChange("update", key, value, from); } this.store[mangled] = value; if (this.dispatchesMapChanges) { - this.dispatchMapChange(key, value); + this.dispatchMapChange("update", key, value, from); } return false; } else { // create if (this.dispatchesMapChanges) { - this.dispatchBeforeMapChange(key, undefined); + this.dispatchMapWillChange("create", key, value); } this.length++; this.store[mangled] = value; if (this.dispatchesMapChanges) { - this.dispatchMapChange(key, value); + this.dispatchMapChange("create", key, value); } return true; } diff --git a/generic-map.js b/generic-map.js index c289398..deb7d4a 100644 --- a/generic-map.js +++ b/generic-map.js @@ -1,16 +1,16 @@ "use strict"; var Object = require("./shim-object"); -var MapChanges = require("./listen/map-changes"); -var PropertyChanges = require("./listen/property-changes"); +var ObservableMap = require("./observable-map"); +var ObservableObject = require("./observable-object"); module.exports = GenericMap; function GenericMap() { throw new Error("Can't construct. GenericMap is a mixin."); } -Object.addEach(GenericMap.prototype, MapChanges.prototype); -Object.addEach(GenericMap.prototype, PropertyChanges.prototype); +Object.addEach(GenericMap.prototype, ObservableMap.prototype); +Object.addEach(GenericMap.prototype, ObservableObject.prototype); // all of these methods depend on the constructor providing a `store` set @@ -56,23 +56,25 @@ GenericMap.prototype.set = function (key, value) { var found = this.store.get(item); var grew = false; if (found) { // update + var from; if (this.dispatchesMapChanges) { - this.dispatchBeforeMapChange(key, found.value); + from = found.value; + this.dispatchMapWillChange("update", key, value, from); } found.value = value; if (this.dispatchesMapChanges) { - this.dispatchMapChange(key, value); + this.dispatchMapChange("update", key, value, from); } } else { // create if (this.dispatchesMapChanges) { - this.dispatchBeforeMapChange(key, undefined); + this.dispatchMapWillChange("create", key, value); } if (this.store.add(item)) { this.length++; grew = true; } if (this.dispatchesMapChanges) { - this.dispatchMapChange(key, value); + this.dispatchMapChange("create", key, value); } } return grew; @@ -89,14 +91,15 @@ GenericMap.prototype.has = function (key) { GenericMap.prototype['delete'] = function (key) { var item = new this.Item(key); if (this.store.has(item)) { - var from = this.store.get(item).value; + var from; if (this.dispatchesMapChanges) { - this.dispatchBeforeMapChange(key, from); + from = this.store.get(item).value; + this.dispatchMapWillChange("delete", key, undefined, from); } this.store["delete"](item); this.length--; if (this.dispatchesMapChanges) { - this.dispatchMapChange(key, undefined); + this.dispatchMapChange("delete", key, undefined, from); } return true; } diff --git a/heap.js b/heap.js index 62f78ac..61984c6 100644 --- a/heap.js +++ b/heap.js @@ -2,12 +2,12 @@ // Adapted from Eloquent JavaScript by Marijn Haverbeke // http://eloquentjavascript.net/appendix2.html -var ArrayChanges = require("./listen/array-changes"); -var Shim = require("./shim"); +require("./observable-array"); +require("./shim"); var GenericCollection = require("./generic-collection"); -var MapChanges = require("./listen/map-changes"); -var RangeChanges = require("./listen/range-changes"); -var PropertyChanges = require("./listen/property-changes"); +var ObservableObject = require("./observable-object"); +var ObservableRange = require("./observable-range"); +var ObservableMap = require("./observable-map"); // Max Heap by default. Comparison can be reversed to produce a Min Heap. @@ -27,9 +27,9 @@ function Heap(values, equals, compare) { Heap.Heap = Heap; // hack so require("heap").Heap will work in MontageJS Object.addEach(Heap.prototype, GenericCollection.prototype); -Object.addEach(Heap.prototype, PropertyChanges.prototype); -Object.addEach(Heap.prototype, RangeChanges.prototype); -Object.addEach(Heap.prototype, MapChanges.prototype); +Object.addEach(Heap.prototype, ObservableObject.prototype); +Object.addEach(Heap.prototype, ObservableRange.prototype); +Object.addEach(Heap.prototype, ObservableMap.prototype); Heap.prototype.constructClone = function (values) { return new this.constructor( @@ -210,12 +210,14 @@ Heap.prototype.reduceRight = function (callback, basis /*, thisp*/) { }, basis, this); }; -Heap.prototype.makeObservable = function () { - // TODO refactor dispatchers to allow direct forwarding - this.content.addRangeChangeListener(this, "content"); - this.content.addBeforeRangeChangeListener(this, "content"); - this.content.addMapChangeListener(this, "content"); - this.content.addBeforeMapChangeListener(this, "content"); +Heap.prototype.makeMapChangesObservable = function () { + this.content.observeMapChange(this, "content"); + this.content.observeMapWillChange(this, "content"); +}; + +Heap.prototype.makeRangeChangesObservable = function () { + this.content.observeRangeChange(this, "content"); + this.content.observeRangeWillChange(this, "content"); }; Heap.prototype.handleContentRangeChange = function (plus, minus, index) { @@ -223,14 +225,14 @@ Heap.prototype.handleContentRangeChange = function (plus, minus, index) { }; Heap.prototype.handleContentRangeWillChange = function (plus, minus, index) { - this.dispatchBeforeRangeChange(plus, minus, index); + this.dispatchRangeWillChange(plus, minus, index); }; -Heap.prototype.handleContentMapChange = function (value, key) { - this.dispatchMapChange(key, value); +Heap.prototype.handleContentMapChange = function (plus, minus, key, type) { + this.dispatchMapChange(type, key, plus, minus); }; -Heap.prototype.handleContentMapWillChange = function (value, key) { - this.dispatchBeforeMapChange(key, value); +Heap.prototype.handleContentMapWillChange = function (plus, minus, key, type) { + this.dispatchMapWillChange(type, key, plus, minus); }; diff --git a/list.js b/list.js index e65961e..277a437 100644 --- a/list.js +++ b/list.js @@ -5,8 +5,8 @@ module.exports = List; var Shim = require("./shim"); var GenericCollection = require("./generic-collection"); var GenericOrder = require("./generic-order"); -var PropertyChanges = require("./listen/property-changes"); -var RangeChanges = require("./listen/range-changes"); +var ObservableObject = require("./observable-object"); +var ObservableRange = require("./observable-range"); function List(values, equals, getDefault) { if (!(this instanceof List)) { @@ -25,8 +25,8 @@ List.List = List; // hack so require("list").List will work in MontageJS Object.addEach(List.prototype, GenericCollection.prototype); Object.addEach(List.prototype, GenericOrder.prototype); -Object.addEach(List.prototype, PropertyChanges.prototype); -Object.addEach(List.prototype, RangeChanges.prototype); +Object.addEach(List.prototype, ObservableObject.prototype); +Object.addEach(List.prototype, ObservableRange.prototype); List.prototype.constructClone = function (values) { return new this.constructor(values, this.contentEquals, this.getDefault); @@ -75,7 +75,7 @@ List.prototype['delete'] = function (value, equals) { if (this.dispatchesRangeChanges) { var plus = []; var minus = [value]; - this.dispatchBeforeRangeChange(plus, minus, found.index); + this.dispatchRangeWillChange(plus, minus, found.index); } found['delete'](); this.length--; @@ -93,7 +93,7 @@ List.prototype.clear = function () { if (this.dispatchesRangeChanges) { minus = this.toArray(); plus = []; - this.dispatchBeforeRangeChange(plus, minus, 0); + this.dispatchRangeWillChange(plus, minus, 0); } this.head.next = this.head.prev = this.head; this.length = 0; @@ -106,7 +106,7 @@ List.prototype.add = function (value) { var node = new this.Node(value) if (this.dispatchesRangeChanges) { node.index = this.length; - this.dispatchBeforeRangeChange([value], [], node.index); + this.dispatchRangeWillChange([value], [], node.index); } this.head.addBefore(node); this.length++; @@ -122,7 +122,7 @@ List.prototype.push = function () { var plus = Array.prototype.slice.call(arguments); var minus = [] var index = this.length; - this.dispatchBeforeRangeChange(plus, minus, index); + this.dispatchRangeWillChange(plus, minus, index); var start = this.head.prev; } for (var i = 0; i < arguments.length; i++) { @@ -141,7 +141,7 @@ List.prototype.unshift = function () { if (this.dispatchesRangeChanges) { var plus = Array.prototype.slice.call(arguments); var minus = []; - this.dispatchBeforeRangeChange(plus, minus, 0); + this.dispatchRangeWillChange(plus, minus, 0); } var at = this.head; for (var i = 0; i < arguments.length; i++) { @@ -166,7 +166,7 @@ List.prototype.pop = function () { var plus = []; var minus = [value]; var index = this.length - 1; - this.dispatchBeforeRangeChange(plus, minus, index); + this.dispatchRangeWillChange(plus, minus, index); } head.prev['delete'](); this.length--; @@ -185,7 +185,7 @@ List.prototype.shift = function () { if (this.dispatchesRangeChanges) { var plus = []; var minus = [value]; - this.dispatchBeforeRangeChange(plus, minus, 0); + this.dispatchRangeWillChange(plus, minus, 0); } head.next['delete'](); this.length--; @@ -294,7 +294,7 @@ List.prototype.swap = function (start, length, plus) { index = start.index; } startNode = start.prev; - this.dispatchBeforeRangeChange(plus, minus, index); + this.dispatchRangeWillChange(plus, minus, index); } // delete minus @@ -330,7 +330,7 @@ List.prototype.reverse = function () { if (this.dispatchesRangeChanges) { var minus = this.toArray(); var plus = minus.reversed(); - this.dispatchBeforeRangeChange(plus, minus, 0); + this.dispatchRangeWillChange(plus, minus, 0); } var at = this.head; do { diff --git a/listen/property-changes.js b/listen/property-changes.js index c9f5c9c..cdc764c 100644 --- a/listen/property-changes.js +++ b/listen/property-changes.js @@ -1,9 +1,9 @@ var WeakMap = require("weak-map"); -var ObservePropertyChanges = require("../observe-property-changes"); -var makePropertyObservable = ObservePropertyChanges.makePropertyObservable; -var observePropertyChange = ObservePropertyChanges.observePropertyChange; -var dispatchPropertyChange = ObservePropertyChanges.dispatchPropertyChange; +var ObservableObject = require("../observable-object"); +var makePropertyObservable = ObservableObject.makePropertyObservable; +var observePropertyChange = ObservableObject.observePropertyChange; +var dispatchPropertyChange = ObservableObject.dispatchPropertyChange; var object_owns = Object.prototype.hasOwnProperty; @@ -117,7 +117,7 @@ PropertyChanges.prototype.removeBeforeOwnPropertyChangeListener = function (name return PropertyChanges.removeOwnPropertyChangeListener(this, name, listener, true); }; -PropertyChanges.prototype.dispatchOwnPropertyChange = function (key, value, beforeChange) { +PropertyChanges.prototype.dispatchOwnPropertyChange = function (name, value, capture) { dispatchPropertyChange(this, name, value, this[name], capture); }; diff --git a/lru-map.js b/lru-map.js index 2a1a78d..bbbf89b 100644 --- a/lru-map.js +++ b/lru-map.js @@ -57,23 +57,23 @@ LruMap.prototype.stringify = function (item, leader) { return leader + JSON.stringify(item.key) + ": " + JSON.stringify(item.value); }; -LruMap.prototype.addMapChangeListener = function () { +LruMap.prototype.observeMapChange = function () { if (!this.dispatchesMapChanges) { // Detect LRU deletions in the LruSet and emit as MapChanges. // Array and Heap have no store. // Dict and FastMap define no listeners on their store. var self = this; - this.store.addBeforeRangeChangeListener(function(plus, minus) { + this.store.observeRangeWillChange(function(plus, minus) { if (plus.length && minus.length) { // LRU item pruned - self.dispatchBeforeMapChange(minus[0].key, undefined); + self.dispatchMapWillChange("delete", minus[0].key, undefined, minus[0].value); } }); - this.store.addRangeChangeListener(function(plus, minus) { + this.store.observeRangeChange(function(plus, minus) { if (plus.length && minus.length) { - self.dispatchMapChange(minus[0].key, undefined); + self.dispatchMapChange("delete", minus[0].key, undefined, minus[0].value); } }); } - GenericMap.prototype.addMapChangeListener.apply(this, arguments); + return GenericMap.prototype.observeMapChange.apply(this, arguments); }; diff --git a/lru-set.js b/lru-set.js index 12be4a8..3a030f3 100644 --- a/lru-set.js +++ b/lru-set.js @@ -4,8 +4,8 @@ var Shim = require("./shim"); var Set = require("./set"); var GenericCollection = require("./generic-collection"); var GenericSet = require("./generic-set"); -var PropertyChanges = require("./listen/property-changes"); -var RangeChanges = require("./listen/range-changes"); +var ObservableObject = require("./observable-object"); +var ObservableRange = require("./observable-range"); module.exports = LruSet; @@ -30,8 +30,8 @@ LruSet.LruSet = LruSet; // hack so require("lru-set").LruSet will work in Montag Object.addEach(LruSet.prototype, GenericCollection.prototype); Object.addEach(LruSet.prototype, GenericSet.prototype); -Object.addEach(LruSet.prototype, PropertyChanges.prototype); -Object.addEach(LruSet.prototype, RangeChanges.prototype); +Object.addEach(LruSet.prototype, ObservableObject.prototype); +Object.addEach(LruSet.prototype, ObservableRange.prototype); LruSet.prototype.constructClone = function (values) { return new this.constructor( @@ -75,7 +75,7 @@ LruSet.prototype.add = function (value) { minus.push(eldest.value); } if (this.dispatchesRangeChanges) { - this.dispatchBeforeRangeChange(plus, minus, 0); + this.dispatchRangeWillChange(plus, minus, 0); } this.store.add(value); if (minus.length > 0) { @@ -96,7 +96,7 @@ LruSet.prototype["delete"] = function (value) { var found = this.store.has(value); if (found) { if (this.dispatchesRangeChanges) { - this.dispatchBeforeRangeChange([], [value], 0); + this.dispatchRangeWillChange([], [value], 0); } this.store["delete"](value); this.length--; diff --git a/observable-array.js b/observable-array.js new file mode 100644 index 0000000..1894064 --- /dev/null +++ b/observable-array.js @@ -0,0 +1,357 @@ + +/* + * Based in part on observable arrays from Motorola Mobility’s Montage + * Copyright (c) 2012, Motorola Mobility LLC. All Rights Reserved. + * 3-Clause BSD License + * https://github.com/motorola-mobility/montage/blob/master/LICENSE.md + */ + +/** + * This module is responsible for observing changes to owned properties of + * objects and changes to the content of arrays caused by method calls. The + * interface for observing array content changes establishes the methods + * necessary for any collection with observable content. + */ + +require("./shim"); +var WeakMap = require("weak-map"); + +var observedLengthForObject = new WeakMap(); + +var ObservableObject = require("./observable-object"); +var ObservableRange = require("./observable-range"); +var ObservableMap = require("./observable-map"); + +var array_swap = Array.prototype.swap; +var array_splice = Array.prototype.splice; +var array_slice = Array.prototype.slice; +var array_reverse = Array.prototype.reverse; +var array_sort = Array.prototype.sort; + +var observableArrayProperties = { + + makeRangeChangesObservable: { + value: Function.noop, // idempotent + writable: true, + configurable: true + }, + + makeMapChangesObservable: { + value: function () { + this.makeIndexObservable(Infinity); + }, + writable: true, + configurable: true + }, + + makePropertyObservable: { + value: function (index) { + // Is a valid array index: + if (~~index === index && index > 0) { // Note: NaN !== NaN, ~~"foo" !== "foo" + this.makeIndexObservable(index); + } + // Does not call through to super because property dispatch on + // Arrays is handled by the mutation methods, particularly swap, + // not by property descriptor thunks. + }, + writable: true, + configurable: true + }, + + makeIndexObservable: { + value: function (index) { + var maxObservedIndex = observedLengthForObject.get(this) || 0; + if (index > maxObservedIndex) { + observedLengthForObject.set(this, index + 1); + } + }, + writable: true, + configurable: true + }, + + swap: { + value: function swap(start, length, plus) { + if (plus) { + if (!Array.isArray(plus)) { + plus = array_slice.call(plus); + } + } else { + plus = Array.empty; + } + + if (start < 0) { + start = this.length + start; + } + var minus; + if (length === 0) { + // minus will be empty + if (plus.length === 0) { + // at this point if plus is empty there is nothing to do. + return []; // [], but spare us an instantiation + } + minus = Array.empty; + } else { + minus = array_slice.call(this, start, start + length); + } + var diff = plus.length - minus.length; + var oldLength = this.length; + var newLength = Math.max(this.length + diff, start + plus.length); + var longest = Math.max(oldLength, newLength); + var observedLength = Math.min(longest, observedLengthForObject.get(this) || 0); + + // dispatch before change events + if (diff) { + this.dispatchPropertyWillChange("length", newLength, oldLength); + } + this.dispatchRangeWillChange(plus, minus, start); + if (diff === 0) { + // Substring replacement + for (var i = start, j = 0; i < start + plus.length; i++, j++) { + this.dispatchPropertyWillChange(i, plus[j], minus[j]); + this.dispatchMapWillChange("update", i, plus[j], minus[j]); + } + } else { + // All subsequent values changed or shifted. + // Avoid (observedLength - start) long walks if there are no + // registered descriptors. + for (var i = start, j = 0; i < observedLength; i++, j++) { + if (i < oldLength && i < newLength) { // update + if (j < plus.length) { + this.dispatchPropertyWillChange(i, plus[j], this[i]); + this.dispatchMapWillChange("update", i, plus[j], this[i]); + } else { + this.dispatchPropertyWillChange(i, this[i - diff], this[i]); + this.dispatchMapWillChange("update", i, this[i - diff], this[i]); + } + } else if (i < newLength) { // but i >= oldLength, create + if (j < plus.length) { + this.dispatchPropertyWillChange(i, plus[j]); + this.dispatchMapWillChange("create", i, plus[j]); + } else { + this.dispatchPropertyWillChange(i, this[i - diff]); + this.dispatchMapWillChange("create", i, this[i - diff]); + } + } else if (i < oldLength) { // but i >= newLength, delete + this.dispatchPropertyWillChange(i, void 0, this[i]); + this.dispatchMapWillChange("delete", i, void 0, this[i]); + } else { + throw new Error("assertion error"); + } + } + } + + // actual work + if (start > oldLength) { + this.length = start; + } + var result = array_swap.call(this, start, length, plus); + + // dispatch after change events + if (diff === 0) { // substring replacement + for (var i = start, j = 0; i < start + plus.length; i++, j++) { + this.dispatchPropertyChange(i, plus[j], minus[j]); + this.dispatchMapChange("update", i, plus[j], minus[j]); + } + } else { + // All subsequent values changed or shifted. + // Avoid (observedLength - start) long walks if there are no + // registered descriptors. + for (var i = start, j = 0; i < observedLength; i++, j++) { + if (i < oldLength && i < newLength) { // update + if (j < minus.length) { + this.dispatchPropertyChange(i, this[i], minus[j]); + this.dispatchMapChange("update", i, this[i], minus[j]); + } else { + this.dispatchPropertyChange(i, this[i], this[i + diff]); + this.dispatchMapChange("update", i, this[i], this[i + diff]); + } + } else if (i < newLength) { // but i >= oldLength, create + if (j < minus.length) { + this.dispatchPropertyChange(i, this[i], minus[j]); + this.dispatchMapChange("create", i, this[i], minus[j]); + } else { + this.dispatchPropertyChange(i, this[i], this[i + diff]); + this.dispatchMapChange("create", i, this[i], this[i + diff]); + } + } else if (i < oldLength) { // but i >= newLength, delete + if (j < minus.length) { + this.dispatchPropertyChange(i, void 0, minus[j]); + this.dispatchMapChange("delete", i, void 0, minus[j]); + } else { + this.dispatchPropertyChange(i, void 0, this[i + diff]); + this.dispatchMapChange("delete", i, void 0, this[i + diff]); + } + } else { + throw new Error("assertion error"); + } + } + } + + this.dispatchRangeChange(plus, minus, start); + if (diff) { + this.dispatchPropertyChange("length", newLength, oldLength); + } + + return result; + }, + writable: true, + configurable: true + }, + + splice: { + value: function splice(start, length) { + return this.swap.call(this, start, length, array_slice.call(arguments, 2)); + }, + writable: true, + configurable: true + }, + + // splice is the array content change utility belt. forward all other + // content changes to splice so we only have to write observer code in one + // place + + reverse: { + value: function reverse() { + var reversed = this.constructClone(this); + reversed.reverse(); + this.swap(0, this.length, reversed); + return this; + }, + writable: true, + configurable: true + }, + + sort: { + value: function sort() { + var sorted = this.constructClone(this); + array_sort.apply(sorted, arguments); + this.swap(0, this.length, sorted); + return this; + }, + writable: true, + configurable: true + }, + + set: { + value: function set(index, value) { + this.splice(index, 1, value); + return this; + }, + writable: true, + configurable: true + }, + + shift: { + value: function shift() { + return this.splice(0, 1)[0]; + }, + writable: true, + configurable: true + }, + + pop: { + value: function pop() { + if (this.length) { + return this.splice(this.length - 1, 1)[0]; + } + }, + writable: true, + configurable: true + }, + + push: { + value: function push(arg) { + if (arguments.length === 1) { + return this.splice(this.length, 0, arg); + } else { + var args = array_slice.call(arguments); + return this.swap(this.length, 0, args); + } + }, + writable: true, + configurable: true + }, + + unshift: { + value: function unshift(arg) { + if (arguments.length === 1) { + return this.splice(0, 0, arg); + } else { + var args = array_slice.call(arguments); + return this.swap(0, 0, args); + } + }, + writable: true, + configurable: true + }, + + clear: { + value: function clear() { + return this.splice(0, this.length); + }, + writable: true, + configurable: true + } + +}; + +// use different strategies for making arrays observable between Internet +// Explorer and other browsers. +var protoIsSupported = {}.__proto__ === Object.prototype; +var array_makeObservable; +var observableArrayPrototype = Object.create(Array.prototype, observableArrayProperties); +if (protoIsSupported) { + array_makeObservable = function () { + this.__proto__ = observableArrayPrototype; + }; +} else { + array_makeObservable = function () { + Object.defineProperties(this, observableArrayProperties); + }; +} + +defineEach(ObservableObject.prototype); +defineEach(ObservableRange.prototype); +defineEach(ObservableMap.prototype); + +// Overrides ObservableRange +Object.defineProperty(Array.prototype, "makeRangeChangesObservable", { + value: array_makeObservable, + writable: true, + configurable: true, + enumerable: false +}); + +// Overrides ObservableMap +Object.defineProperty(Array.prototype, "makeMapChangesObservable", { + value: function () { + array_makeObservable.call(this); + observableArrayPrototype.makeMapChangesObservable.call(this); + }, + writable: true, + configurable: true, + enumerable: false +}); + +// Overrides ObservableObject +Object.defineProperty(Array.prototype, "makePropertyObservable", { + value: function (name) { + array_makeObservable.call(this); + observableArrayPrototype.makePropertyObservable.call(this, name); + }, + writable: true, + configurable: true, + enumerable: false +}); + +function defineEach(prototype) { + for (var name in prototype) { + Object.defineProperty(Array.prototype, name, { + value: prototype[name], + writable: true, + configurable: true, + enumerable: false + }); + } +} + diff --git a/observable-map.js b/observable-map.js new file mode 100644 index 0000000..da60060 --- /dev/null +++ b/observable-map.js @@ -0,0 +1,222 @@ +/*global -WeakMap*/ +"use strict"; + +require("./shim-array"); +var WeakMap = require("weak-map"); + +var changeObserversByObject = new WeakMap(); +var willChangeObserversByObject = new WeakMap(); +var observerFreeList = []; +var observerToFreeList = []; +var dispatching = false; + +module.exports = ObservableMap; +function ObservableMap() { + throw new Error("Can't construct. ObservableMap is a mixin."); +} + +ObservableMap.prototype.observeMapChange = function (handler, name, note, capture) { + this.makeMapChangesObservable(); + var observers = this.getMapChangeObservers(capture); + + var observer; + if (observerFreeList.length) { // TODO !debug? + observer = observerFreeList.pop(); + } else { + observer = new MapChangeObserver(); + } + + observer.object = this; + observer.name = name; + observer.capture = capture; + observer.observers = observers; + observer.handler = handler; + observer.note = note; + + // Precompute dispatch method name + + var stringName = "" + name; // Array indicides must be coerced to string. + var propertyName = stringName.slice(0, 1).toUpperCase() + stringName.slice(1); + + if (!capture) { + var methodName = "handle" + propertyName + "MapChange"; + if (handler[methodName]) { + observer.handlerMethodName = methodName; + } else if (handler.handleMapChange) { + observer.handlerMethodName = "handleMapChange"; + } else if (handler.call) { + observer.handlerMethodName = null; + } else { + throw new Error("Can't arrange to dispatch map changes to " + handler); + } + } else { + var methodName = "handle" + propertyName + "MapWillChange"; + if (handler[methodName]) { + observer.handlerMethodName = methodName; + } else if (handler.handleMapWillChange) { + observer.handlerMethodName = "handleMapWillChange"; + } else if (handler.call) { + observer.handlerMethodName = null; + } else { + throw new Error("Can't arrange to dispatch map changes to " + handler); + } + } + + observers.push(observer); + + // TODO issue warning if the number of handler records is worrisome + return observer; +}; + +ObservableMap.prototype.observeMapWillChange = function (handler, name, note) { + return this.observeMapChange(handler, name, note, true); +}; + +ObservableMap.prototype.dispatchMapChange = function (type, key, plus, minus, capture) { + if (plus === minus) { + return; + } + if (!dispatching) { // TODO && !debug? + return this.startMapChangeDispatchContext(type, key, plus, minus, capture); + } + var observers = this.getMapChangeObservers(capture); + for (var index = 0; index < observers.length; index++) { + var observer = observers[index]; + observer.dispatch(type, key, plus, minus); + } +}; + +ObservableMap.prototype.dispatchMapWillChange = function (type, key, plus, minus) { + return this.dispatchMapChange(type, key, plus, minus, true); +}; + +ObservableMap.prototype.startMapChangeDispatchContext = function (type, key, plus, minus, capture) { + dispatching = true; + try { + this.dispatchMapChange(type, key, plus, minus, capture); + } catch (error) { + if (typeof error === "object" && typeof error.message === "string") { + error.message = "Map change dispatch possibly corrupted by error: " + error.message; + throw error; + } else { + throw new Error("Map change dispatch possibly corrupted by error: " + error); + } + } finally { + dispatching = false; + if (observerToFreeList.length) { + // Using push.apply instead of addEach because push will definitely + // be much faster than the generic addEach, which also handles + // non-array collections. + observerFreeList.push.apply( + observerFreeList, + observerToFreeList + ); + // Using clear because it is observable. The handler record array + // is obtainable by getPropertyChangeObservers, and is observable. + observerToFreeList.clear(); + } + } +}; + +ObservableMap.prototype.makeMapChangesObservable = function () { + this.dispatchesMapChanges = true; +}; + +ObservableMap.prototype.getMapChangeObservers = function (capture) { + var byObject = capture ? willChangeObserversByObject : changeObserversByObject; + if (!byObject.has(this)) { + byObject.set(this, []); + } + return byObject.get(this); +}; + +ObservableMap.prototype.getMapWillChangeObservers = function () { + return this.getMapChangeObservers(true); +}; + +function MapChangeObserver() { + this.init(); +} + +MapChangeObserver.prototype.init = function () { + this.object = null; + this.name = null; + this.observers = null; + this.handler = null; + this.handlerMethodName = null; + this.childObserver = null; + this.note = null; + this.capture = null; +}; + +MapChangeObserver.prototype.cancel = function () { + var observers = this.observers; + var index = observers.indexOf(this); + // Unfortunately, if this observer was reused, this would not be sufficient + // to detect a duplicate cancel. Do not cancel more than once. + if (index < 0) { + throw new Error( + "Can't cancel observer for " + + JSON.stringify(this.name) + " map changes" + + " because it has already been canceled" + ); + } + var childObserver = this.childObserver; + observers.splice(index, 1); + this.init(); + // If this observer is canceled while dispatching a change + // notification for the same property... + // 1. We cannot put the handler record onto the free list because + // it may have been captured in the array of records to which + // the change notification would be sent. We must mark it as + // canceled by nulling out the handler property so the dispatcher + // passes over it. + // 2. We also cannot put the handler record onto the free list + // until all change dispatches have been completed because it could + // conceivably be reused, confusing the current dispatcher. + if (dispatching) { + // All handlers added to this list will be moved over to the + // actual free list when there are no longer any property + // change dispatchers on the stack. + observerToFreeList.push(this); + } else { + observerFreeList.push(this); + } + if (childObserver) { + // Calling user code on our stack. + // Done in tail position to avoid a plan interference hazard. + childObserver.cancel(); + } +}; + +MapChangeObserver.prototype.dispatch = function (type, key, plus, minus) { + var handler = this.handler; + // A null handler implies that an observer was canceled during the dispatch + // of a change. The observer is pending addition to the free list. + if (!handler) { + return; + } + + var childObserver = this.childObserver; + this.childObserver = null; + // XXX plan interference hazards calling cancel and handler methods: + if (childObserver) { + childObserver.cancel(); + } + + var handlerMethodName = this.handlerMethodName; + if (handlerMethodName && typeof handler[handlerMethodName] === "function") { + childObserver = handler[handlerMethodName](plus, minus, key, type, this.object); + } else if (handler.call) { + childObserver = handler.call(void 0, plus, minus, key, type, this.object); + } else { + throw new Error( + "Can't dispatch map change for " + JSON.stringify(this.name) + " to " + handler + + " because there is no handler method" + ); + } + + this.childObserver = childObserver; + return this; +}; + diff --git a/observe-property-changes.js b/observable-object.js similarity index 75% rename from observe-property-changes.js rename to observable-object.js index 24f2550..c922e89 100644 --- a/observe-property-changes.js +++ b/observable-object.js @@ -18,9 +18,115 @@ var observerToFreeList = []; var wrappedObjectDescriptors = new WeakMap(); var dispatching = false; -exports.observePropertyChange = observePropertyChange; +module.exports = ObservableObject; +function ObservableObject() { + throw new Error("Can't construct. ObservableObject is a mixin."); +} + +ObservableObject.prototype.observePropertyChange = function (name, handler, note, capture) { + return observePropertyChange(this, name, handler, note, capture); +}; + +ObservableObject.prototype.observePropertyWillChange = function (name, handler, note) { + return observePropertyWillChange(this, name, handler, note); +}; + +ObservableObject.prototype.dispatchPropertyChange = function (name, plus, minus, capture) { + return dispatchPropertyChange(this, name, plus, minus, capture); +}; + +ObservableObject.prototype.dispatchPropertyWillChange = function (name, plus, minus) { + return dispatchPropertyWillChange(this, name, plus, minus); +}; + +ObservableObject.prototype.getPropertyChangeObservers = function (name, capture) { + return getPropertyChangeObservers(this, name, capture); +}; + +ObservableObject.prototype.getPropertyWillChangeObservers = function (name) { + return getPropertyWillChangeObservers(this, name); +}; + +ObservableObject.prototype.makePropertyObservable = function (name) { + return makePropertyObservable(this, name); +}; + +ObservableObject.prototype.preventPropertyObserver = function (name) { + return preventPropertyObserver(this, name); +}; + +ObservableObject.prototype.PropertyChangeObserver = PropertyChangeObserver; + +// Constructor interface with polymorphic delegation if available + +ObservableObject.observePropertyChange = function (object, name, handler, note, capture) { + if (object.observePropertyChange) { + return object.observePropertyChange(name, handler, note, capture); + } else { + return observePropertyChange(object, name, handler, note, capture); + } +}; + +ObservableObject.observePropertyWillChange = function (object, name, handler, note) { + if (object.observePropertyWillChange) { + return object.observePropertyWillChange(name, handler, note); + } else { + return observePropertyWillChange(object, name, handler, note); + } +}; + +ObservableObject.dispatchPropertyChange = function (object, name, plus, minus, capture) { + if (object.dispatchPropertyChange) { + return object.dispatchPropertyChange(name, plus, minus, capture); + } else { + return dispatchPropertyChange(object, name, plus, minus, capture); + } +}; + +ObservableObject.dispatchPropertyWillChange = function (object, name, plus, minus) { + if (object.dispatchPropertyWillChange) { + return object.dispatchPropertyWillChange(name, plus, minus); + } else { + return dispatchPropertyWillChange(object, name, plus, minus); + } +}; + +ObservableObject.getPropertyChangeObservers = function (object, name, capture) { + if (object.getPropertyChangeObservers) { + return object.getPropertyChangeObservers(name, capture); + } else { + return getPropertyChangeObservers(object, name, capture); + } +}; + +ObservableObject.getPropertyWillChangeObservers = function (object, name) { + if (object.getPropertyWillChangeObservers) { + return object.getPropertyWillChangeObservers(name); + } else { + return getPropertyWillChangeObservers(object, name); + } +}; + +ObservableObject.makePropertyObservable = function (object, name) { + if (object.makePropertyObservable) { + return object.makePropertyObservable(name); + } else { + return makePropertyObservable(object, name); + } +}; + +ObservableObject.preventPropertyObserver = function (object, name) { + if (object.preventPropertyObserver) { + return object.preventPropertyObserver(name); + } else { + return preventPropertyObserver(object, name); + } +}; + +// Implementation + function observePropertyChange(object, name, handler, note, capture) { - makePropertyObservable(object, name); + ObservableObject.makePropertyObservable(object, name); var observers = getPropertyChangeObservers(object, name, capture); var observer; @@ -47,11 +153,11 @@ function observePropertyChange(object, name, handler, note, capture) { var specificChangeMethodName = "handle" + propertyName + "PropertyChange"; var genericChangeMethodName = "handlePropertyChange"; if (handler[specificChangeMethodName]) { - observer.handlerChangeMethodName = specificChangeMethodName; + observer.handlerMethodName = specificChangeMethodName; } else if (handler[genericChangeMethodName]) { - observer.handlerChangeMethodName = genericChangeMethodName; + observer.handlerMethodName = genericChangeMethodName; } else if (handler.call) { - observer.handlerChangeMethodName = null; + observer.handlerMethodName = null; } else { throw new Error("Can't arrange to dispatch " + JSON.stringify(name) + " property changes on " + object); } @@ -59,11 +165,11 @@ function observePropertyChange(object, name, handler, note, capture) { var specificWillChangeMethodName = "handle" + propertyName + "PropertyWillChange"; var genericWillChangeMethodName = "handlePropertyWillChange"; if (handler[specificWillChangeMethodName]) { - observer.handlerChangeMethodName = specificWillChangeMethodName; + observer.handlerMethodName = specificWillChangeMethodName; } else if (handler[genericWillChangeMethodName]) { - observer.handlerChangeMethodName = genericWillChangeMethodName; + observer.handlerMethodName = genericWillChangeMethodName; } else if (handler.call) { - observer.handlerChangeMethodName = null; + observer.handlerMethodName = null; } else { throw new Error("Can't arrange to dispatch " + JSON.stringify(name) + " property changes on " + object); } @@ -78,32 +184,29 @@ function observePropertyChange(object, name, handler, note, capture) { return observer; } -exports.observePropertyWillChange = observePropertyWillChange; function observePropertyWillChange(object, name, handler, note) { return observePropertyChange(object, name, handler, note, true); } -exports.dispatchPropertyChange = dispatchPropertyChange; -function dispatchPropertyChange(object, name, plus, capture) { - if (!dispatching) { - return startPropertyChangeDispatchContext(object, name, plus, capture); +function dispatchPropertyChange(object, name, plus, minus, capture) { + if (!dispatching) { // TODO && !debug? + return startPropertyChangeDispatchContext(object, name, plus, minus, capture); } var observers = getPropertyChangeObservers(object, name, capture).slice(); for (var index = 0; index < observers.length; index++) { var observer = observers[index]; - observer.dispatch(plus); + observer.dispatch(plus, minus); } } -exports.dispatchPropertyWillChange = dispatchPropertyWillChange; -function dispatchPropertyWillChange(object, name, plus) { - dispatchPropertyChange(object, name, plus, true); +function dispatchPropertyWillChange(object, name, plus, minus) { + dispatchPropertyChange(object, name, plus, minus, true); } -function startPropertyChangeDispatchContext(object, name, plus, capture) { +function startPropertyChangeDispatchContext(object, name, plus, minus, capture) { dispatching = true; try { - dispatchPropertyChange(object, name, plus, capture); + dispatchPropertyChange(object, name, plus, minus, capture); } catch (error) { if (typeof error === "object" && typeof error.message === "string") { error.message = "Property change dispatch possibly corrupted by error: " + error.message; @@ -128,7 +231,6 @@ function startPropertyChangeDispatchContext(object, name, plus, capture) { } } -exports.getPropertyChangeObservers = getPropertyChangeObservers; function getPropertyChangeObservers(object, name, capture) { if (!observersByObject.has(object)) { observersByObject.set(object, Object.create(null)); @@ -142,12 +244,10 @@ function getPropertyChangeObservers(object, name, capture) { return observersByKey[key]; } -exports.getPropertyWillChangeObservers = getPropertyWillChangeObservers; function getPropertyWillChangeObservers(object, name) { return getPropertyChangeObservers(object, name, true); } -exports.PropertyChangeObserver = PropertyChangeObserver; function PropertyChangeObserver() { this.init(); // Object.seal(this); // Maybe one day, this won't deoptimize. @@ -161,7 +261,7 @@ PropertyChangeObserver.prototype.init = function () { // On which to dispatch property change notifications. this.handler = null; // Precomputed handler method name for change dispatch - this.handlerChangeMethodName = null; + this.handlerMethodName = null; // Returned by the last property change notification, which must be // canceled before the next change notification, or when this observer is // finally canceled. @@ -216,7 +316,7 @@ PropertyChangeObserver.prototype.cancel = function () { } }; -PropertyChangeObserver.prototype.dispatch = function (plus) { +PropertyChangeObserver.prototype.dispatch = function (plus, minus) { var handler = this.handler; // A null handler implies that an observer was canceled during the dispatch // of a change. The observer is pending addition to the free list. @@ -224,7 +324,9 @@ PropertyChangeObserver.prototype.dispatch = function (plus) { return; } - var minus = this.value; + if (minus === void 0) { + minus = this.value; + } this.value = plus; var childObserver = this.childObserver; @@ -233,21 +335,22 @@ PropertyChangeObserver.prototype.dispatch = function (plus) { if (childObserver) { childObserver.cancel(); } - var changeMethodName = this.handlerChangeMethodName; - if (handler[changeMethodName]) { - childObserver = handler[changeMethodName](plus, minus, this.propertyName, this.object); + var handlerMethodName = this.handlerMethodName; + if (handlerMethodName && typeof handler[handlerMethodName] === "function") { + childObserver = handler[handlerMethodName](plus, minus, this.propertyName, this.object); } else if (handler.call) { childObserver = handler.call(void 0, plus, minus, this.propertyName, this.object); } else { throw new Error( - "Can't dispatch " + JSON.stringify(changeMethodName) + " property change on " + object + "Can't dispatch " + JSON.stringify(handlerMethodName) + " property change on " + object + + " because there is no handler method" ); } + this.childObserver = childObserver; return this; }; -exports.makePropertyObservable = makePropertyObservable; function makePropertyObservable(object, name) { var wrappedDescriptor = wrapPropertyDescriptor(object, name); @@ -276,7 +379,6 @@ function makePropertyObservable(object, name) { * underlying type will dispatch the change manually, or intends the property * to stick on all instances. */ -exports.preventPropertyObserver = preventPropertyObserver; function preventPropertyObserver(object, name) { var wrappedDescriptor = wrapPropertyDescriptor(object, name); Object.defineProperty(object, name, wrappedDescriptor); diff --git a/observable-range.js b/observable-range.js new file mode 100644 index 0000000..093ec03 --- /dev/null +++ b/observable-range.js @@ -0,0 +1,221 @@ +/*global -WeakMap*/ +"use strict"; + +// TODO review all error messages for consistency and helpfulness across observables + +var WeakMap = require("weak-map"); + +var changeObserversByObject = new WeakMap(); +var willChangeObserversByObject = new WeakMap(); +var observerFreeList = []; +var observerToFreeList = []; +var dispatching = false; + +module.exports = ObservableRange; +function ObservableRange() { + throw new Error("Can't construct. ObservableRange is a mixin."); +} + +ObservableRange.prototype.observeRangeChange = function (handler, name, note, capture) { + this.makeRangeChangesObservable(); + var observers = this.getRangeChangeObservers(capture); + + var observer; + if (observerFreeList.length) { // TODO !debug? + observer = observerFreeList.pop(); + } else { + observer = new RangeChangeObserver(); + } + + observer.object = this; + observer.name = name; + observer.capture = capture; + observer.observers = observers; + observer.handler = handler; + observer.note = note; + + // Precompute dispatch method name + + var stringName = "" + name; // Array indicides must be coerced to string. + var propertyName = stringName.slice(0, 1).toUpperCase() + stringName.slice(1); + + if (!capture) { + var methodName = "handle" + propertyName + "RangeChange"; + if (handler[methodName]) { + observer.handlerMethodName = methodName; + } else if (handler.handleRangeChange) { + observer.handlerMethodName = "handleRangeChange"; + } else if (handler.call) { + observer.handlerMethodName = null; + } else { + throw new Error("Can't arrange to dispatch " + JSON.stringify(name) + " map changes"); + } + } else { + var methodName = "handle" + propertyName + "RangeWillChange"; + if (handler[methodName]) { + observer.handlerMethodName = methodName; + } else if (handler.handleRangeWillChange) { + observer.handlerMethodName = "handleRangeWillChange"; + } else if (handler.call) { + observer.handlerMethodName = null; + } else { + throw new Error("Can't arrange to dispatch " + JSON.stringify(name) + " map changes"); + } + } + + observers.push(observer); + + // TODO issue warning if the number of handler records is worrisome + return observer; +}; + +ObservableRange.prototype.observeRangeWillChange = function (handler, name, note) { + return this.observeRangeChange(handler, name, note, true); +}; + +ObservableRange.prototype.dispatchRangeChange = function (plus, minus, index, capture) { + if (!dispatching) { // TODO && !debug? + return this.startRangeChangeDispatchContext(plus, minus, index, capture); + } + var observers = this.getRangeChangeObservers(capture); + for (var observerIndex = 0; observerIndex < observers.length; observerIndex++) { + var observer = observers[observerIndex]; + // The slicing ensures that handlers cannot interfere with another by + // altering these arguments. + observer.dispatch(plus.slice(), minus.slice(), index); + } +}; + +ObservableRange.prototype.dispatchRangeWillChange = function (plus, minus, index) { + return this.dispatchRangeChange(plus, minus, index, true); +}; + +ObservableRange.prototype.startRangeChangeDispatchContext = function (plus, minus, index, capture) { + dispatching = true; + try { + this.dispatchRangeChange(plus, minus, index, capture); + } catch (error) { + if (typeof error === "object" && typeof error.message === "string") { + error.message = "Range change dispatch possibly corrupted by error: " + error.message; + throw error; + } else { + throw new Error("Range change dispatch possibly corrupted by error: " + error); + } + } finally { + dispatching = false; + if (observerToFreeList.length) { + // Using push.apply instead of addEach because push will definitely + // be much faster than the generic addEach, which also handles + // non-array collections. + observerFreeList.push.apply( + observerFreeList, + observerToFreeList + ); + // Using clear because it is observable. The handler record array + // is obtainable by getPropertyChangeObservers, and is observable. + observerToFreeList.clear(); + } + } +}; + +ObservableRange.prototype.makeRangeChangesObservable = function () { + this.dispatchesRangeChanges = true; +}; + +ObservableRange.prototype.getRangeChangeObservers = function (capture) { + var byObject = capture ? willChangeObserversByObject : changeObserversByObject; + if (!byObject.has(this)) { + byObject.set(this, []); + } + return byObject.get(this); +}; + +ObservableRange.prototype.getRangeWillChangeObservers = function () { + return this.getRangeChangeObservers(true); +}; + +function RangeChangeObserver() { + this.init(); +} + +RangeChangeObserver.prototype.init = function () { + this.object = null; + this.name = null; + this.observers = null; + this.handler = null; + this.handlerMethodName = null; + this.childObserver = null; + this.note = null; + this.capture = null; +}; + +RangeChangeObserver.prototype.cancel = function () { + var observers = this.observers; + var index = observers.indexOf(this); + // Unfortunately, if this observer was reused, this would not be sufficient + // to detect a duplicate cancel. Do not cancel more than once. + if (index < 0) { + throw new Error( + "Can't cancel observer for " + + JSON.stringify(this.name) + " range changes" + + " because it has already been canceled" + ); + } + var childObserver = this.childObserver; + observers.splice(index, 1); + this.init(); + // If this observer is canceled while dispatching a change + // notification for the same property... + // 1. We cannot put the handler record onto the free list because + // it may have been captured in the array of records to which + // the change notification would be sent. We must mark it as + // canceled by nulling out the handler property so the dispatcher + // passes over it. + // 2. We also cannot put the handler record onto the free list + // until all change dispatches have been completed because it could + // conceivably be reused, confusing the current dispatcher. + if (dispatching) { + // All handlers added to this list will be moved over to the + // actual free list when there are no longer any property + // change dispatchers on the stack. + observerToFreeList.push(this); + } else { + observerFreeList.push(this); + } + if (childObserver) { + // Calling user code on our stack. + // Done in tail position to avoid a plan interference hazard. + childObserver.cancel(); + } +}; + +RangeChangeObserver.prototype.dispatch = function (plus, minus, index) { + var handler = this.handler; + // A null handler implies that an observer was canceled during the dispatch + // of a change. The observer is pending addition to the free list. + if (!handler) { + return; + } + + var childObserver = this.childObserver; + this.childObserver = null; + // XXX plan interference hazards calling cancel and handler methods: + if (childObserver) { + childObserver.cancel(); + } + + var handlerMethodName = this.handlerMethodName; + if (handlerMethodName && typeof handler[handlerMethodName] === "function") { + childObserver = handler[handlerMethodName](plus, minus, index, this.object); + } else if (handler.call) { + childObserver = handler.call(void 0, plus, minus, index, this.object); + } else { + throw new Error( + "Can't dispatch range change to " + handler + ); + } + + this.childObserver = childObserver; + return this; +}; + diff --git a/spec/array-spec.js b/spec/array-spec.js index a44095d..29f9444 100644 --- a/spec/array-spec.js +++ b/spec/array-spec.js @@ -5,7 +5,6 @@ var GenericCollection = require("../generic-collection"); var describeDequeue = require("./dequeue"); var describeCollection = require("./collection"); var describeOrder = require("./order"); -var describeMapChanges = require("./listen/map-changes"); describe("Array", function () { describeDequeue(Array.from); @@ -13,18 +12,6 @@ describe("Array", function () { describeCollection(Array.from, [{id: 0}, {id: 1}, {id: 2}, {id: 3}]); describeOrder(Array.from); - function mapAlike(entries) { - var array = []; - if (entries) { - entries.forEach(function (pair) { - array.set(pair[0], pair[1]); - }); - } - return array; - } - - describeMapChanges(mapAlike); - /* The following tests are from Montage. Copyright (c) 2012, Motorola Mobility LLC. diff --git a/spec/dict.js b/spec/dict.js index 3f70e88..326d081 100644 --- a/spec/dict.js +++ b/spec/dict.js @@ -45,15 +45,6 @@ function describeDict(Dict) { expect(dict.delete("__proto__")).toBe(false); }); - it("should send a value for MapChange events", function () { - var dict = Dict({a: 1}); - - var listener = function(value, key) { - expect(value).toBe(2); - }; - dict.addMapChangeListener(listener); - dict.set('a', 2); - }) } function shouldHaveTheUsualContent(dict) { diff --git a/spec/heap-spec.js b/spec/heap-spec.js index 9f59839..52dbfbb 100644 --- a/spec/heap-spec.js +++ b/spec/heap-spec.js @@ -53,9 +53,9 @@ describe("Heap", function () { var heap = new Heap([1,2,3,4,5]); var top; - heap.addMapChangeListener(function (value, key) { + heap.observeMapChange(function (plus, minus, key, type) { if (key === 0) { - top = value; + top = plus; } }); diff --git a/spec/list-spec.js b/spec/list-spec.js index dbddf17..b39d7af 100644 --- a/spec/list-spec.js +++ b/spec/list-spec.js @@ -2,7 +2,7 @@ var List = require("../list"); var describeDequeue = require("./dequeue"); var describeCollection = require("./collection"); -var describeRangeChanges = require("./listen/range-changes"); +var describeRangeChanges = Function.noop; /// TODO describe("List", function () { // new List() diff --git a/spec/listen/array-changes-spec.js b/spec/listen/array-changes-spec.js deleted file mode 100644 index 0834266..0000000 --- a/spec/listen/array-changes-spec.js +++ /dev/null @@ -1,438 +0,0 @@ - -require("../../listen/array-changes"); -var describeRangeChanges = require("./range-changes"); - -describe("Array change dispatch", function () { - - // TODO (make consistent with List) - // describeRangeChanges(Array.from); - - var array = [1, 2, 3]; - var spy; - - // the following tests all share the same initial array so they - // are sensitive to changes in order - - it("set up listeners", function () { - - array.addBeforeOwnPropertyChangeListener("length", function (length) { - spy("length change from", length); - }); - - array.addOwnPropertyChangeListener("length", function (length) { - spy("length change to", length); - }); - - array.addBeforeRangeChangeListener(function (plus, minus, index) { - spy("before content change at", index, "to add", plus.slice(), "to remove", minus.slice()); - }); - - array.addRangeChangeListener(function (plus, minus, index) { - spy("content change at", index, "added", plus.slice(), "removed", minus.slice()); - }); - - array.addBeforeMapChangeListener(function (value, key) { - spy("change at", key, "from", value); - }); - - array.addMapChangeListener(function (value, key) { - spy("change at", key, "to", value); - }); - - }); - - it("change dispatch properties should not be enumerable", function () { - // this verifies that dispatchesRangeChanges and dispatchesMapChanges - // are both non-enumerable, and any other properties that might get - // added in the future. - for (var name in array) { - expect(isNaN(+name)).toBe(false); - } - }); - - it("clear initial values", function () { - spy = jasmine.createSpy(); - expect(array).toEqual([1, 2, 3]); - array.clear(); - expect(array).toEqual([]); - expect(spy.argsForCall).toEqual([ - ["length change from", 3], - ["before content change at", 0, "to add", [], "to remove", [1, 2, 3]], - ["change at", 0, "from", 1], - ["change at", 1, "from", 2], - ["change at", 2, "from", 3], - ["change at", 0, "to", undefined], - ["change at", 1, "to", undefined], - ["change at", 2, "to", undefined], - ["content change at", 0, "added", [], "removed", [1, 2, 3]], - ["length change to", 0] - ]); - }); - - it("push two values on empty array", function () { - spy = jasmine.createSpy(); - expect(array).toEqual([]); // initial - array.push(10, 20); - expect(array).toEqual([10, 20]); - expect(spy.argsForCall).toEqual([ - ["length change from", 0], - ["before content change at", 0, "to add", [10, 20], "to remove", []], - ["change at", 0, "from", undefined], - ["change at", 1, "from", undefined], - ["change at", 0, "to", 10], - ["change at", 1, "to", 20], - ["content change at", 0, "added", [10, 20], "removed", []], - ["length change to", 2], - ]); - - }); - - it("pop one value", function () { - spy = jasmine.createSpy(); - expect(array).toEqual([10, 20]); - array.pop(); - expect(array).toEqual([10]); - expect(spy.argsForCall).toEqual([ - ["length change from", 2], - ["before content change at", 1, "to add", [], "to remove", [20]], - ["change at", 1, "from", 20], - ["change at", 1, "to", undefined], - ["content change at", 1, "added", [], "removed", [20]], - ["length change to", 1], - ]); - }); - - it("push two values on top of existing one, with hole open for splice", function () { - spy = jasmine.createSpy(); - expect(array).toEqual([10]); - array.push(40, 50); - expect(array).toEqual([10, 40, 50]); - expect(spy.argsForCall).toEqual([ - ["length change from", 1], - ["before content change at", 1, "to add", [40, 50], "to remove", []], - ["change at", 1, "from", undefined], - ["change at", 2, "from", undefined], - ["change at", 1, "to", 40], - ["change at", 2, "to", 50], - ["content change at", 1, "added", [40, 50], "removed", []], - ["length change to", 3] - ]); - }); - - it("splices two values into middle", function () { - spy = jasmine.createSpy(); - expect(array).toEqual([10, 40, 50]); - expect(array.splice(1, 0, 20, 30)).toEqual([]); - expect(array).toEqual([10, 20, 30, 40, 50]); - expect(spy.argsForCall).toEqual([ - ["length change from", 3], - ["before content change at", 1, "to add", [20, 30], "to remove", []], - ["change at", 1, "from", 40], - ["change at", 2, "from", 50], - ["change at", 3, "from", undefined], - ["change at", 4, "from", undefined], - ["change at", 1, "to", 20], - ["change at", 2, "to", 30], - ["change at", 3, "to", 40], - ["change at", 4, "to", 50], - ["content change at", 1, "added", [20, 30], "removed", []], - ["length change to", 5] - ]); - }); - - it("pushes one value to end", function () { - spy = jasmine.createSpy(); - expect(array).toEqual([10, 20, 30, 40, 50]); - array.push(60); - expect(array).toEqual([10, 20, 30, 40, 50, 60]); - expect(spy.argsForCall).toEqual([ - ["length change from", 5], - ["before content change at", 5, "to add", [60], "to remove", []], - ["change at", 5, "from", undefined], - ["change at", 5, "to", 60], - ["content change at", 5, "added", [60], "removed", []], - ["length change to", 6] - ]); - }); - - it("splices in place", function () { - spy = jasmine.createSpy(); - expect(array).toEqual([10, 20, 30, 40, 50, 60]); - expect(array.splice(2, 2, "A", "B")).toEqual([30, 40]); - expect(array).toEqual([10, 20, "A", "B", 50, 60]); - expect(spy.argsForCall).toEqual([ - // no length change - ["before content change at", 2, "to add", ["A", "B"], "to remove", [30, 40]], - ["change at", 2, "from", 30], - ["change at", 3, "from", 40], - ["change at", 2, "to", "A"], - ["change at", 3, "to", "B"], - ["content change at", 2, "added", ["A", "B"], "removed", [30, 40]], - ]); - }); - - // ---- fresh start - - it("shifts one from the beginning", function () { - array.clear(); // start over fresh - array.push(10, 20, 30); - spy = jasmine.createSpy(); - expect(array).toEqual([10, 20, 30]); - expect(array.shift()).toEqual(10); - expect(array).toEqual([20, 30]); - expect(spy.argsForCall).toEqual([ - ["length change from", 3], - ["before content change at", 0, "to add", [], "to remove", [10]], - ["change at", 0, "from", 10], - ["change at", 1, "from", 20], - ["change at", 2, "from", 30], - ["change at", 0, "to", 20], - ["change at", 1, "to", 30], - ["change at", 2, "to", undefined], - ["content change at", 0, "added", [], "removed", [10]], - ["length change to", 2] - ]); - }); - - it("sets new value at end", function () { - spy = jasmine.createSpy(); - expect(array).toEqual([20, 30]); - expect(array.set(2, 40)).toBe(array); - expect(array).toEqual([20, 30, 40]); - expect(spy.argsForCall).toEqual([ - ["length change from", 2], - ["before content change at", 2, "to add", [40], "to remove", []], - ["change at", 2, "from", undefined], - ["change at", 2, "to", 40], - ["content change at", 2, "added", [40], "removed", []], - ["length change to", 3] - ]); - }); - - it("sets new value at beginning", function () { - spy = jasmine.createSpy(); - expect(array).toEqual([20, 30, 40]); - expect(array.set(0, 10)).toBe(array); - expect(array).toEqual([10, 30, 40]); - expect(spy.argsForCall).toEqual([ - ["before content change at", 0, "to add", [10], "to remove", [20]], - ["change at", 0, "from", 20], - ["change at", 0, "to", 10], - ["content change at", 0, "added", [10], "removed", [20]] - ]); - }); - - // ---- fresh start - - it("unshifts one to the beginning", function () { - array.clear(); // start over fresh - expect(array).toEqual([]); - spy = jasmine.createSpy(); - array.unshift(30); - expect(array).toEqual([30]); - expect(spy.argsForCall).toEqual([ - ["length change from", 0], - ["before content change at", 0, "to add", [30], "to remove", []], - ["change at", 0, "from", undefined], - ["change at", 0, "to", 30], - ["content change at", 0, "added", [30], "removed", []], - ["length change to", 1] - ]); - }); - - it("unshifts two values on beginning of already populated array", function () { - spy = jasmine.createSpy(); - expect(array).toEqual([30]); - array.unshift(10, 20); - expect(array).toEqual([10, 20, 30]); - expect(spy.argsForCall).toEqual([ - ["length change from", 1], - // added and removed values reflect the ending values, not the values at the time of the call - ["before content change at", 0, "to add", [10, 20], "to remove", []], - ["change at", 0, "from", 30], - ["change at", 1, "from", undefined], - ["change at", 2, "from", undefined], - ["change at", 0, "to", 10], - ["change at", 1, "to", 20], - ["change at", 2, "to", 30], - ["content change at", 0, "added", [10, 20], "removed", []], - ["length change to", 3] - ]); - }); - - it("reverses in place", function () { - spy = jasmine.createSpy(); - expect(array).toEqual([10, 20, 30]); - array.reverse(); - expect(array).toEqual([30, 20, 10]); - expect(spy.argsForCall).toEqual([ - ["before content change at", 0, "to add", [30, 20, 10], "to remove", [10, 20, 30]], - ["change at", 0, "from", 10], - ["change at", 1, "from", 20], - ["change at", 2, "from", 30], - ["change at", 0, "to", 30], - ["change at", 1, "to", 20], - ["change at", 2, "to", 10], - ["content change at", 0, "added", [30, 20, 10], "removed", [10, 20, 30]], - ]); - }); - - it("sorts in place", function () { - spy = jasmine.createSpy(); - expect(array).toEqual([30, 20, 10]); - array.sort(); - expect(array).toEqual([10, 20, 30]); - expect(spy.argsForCall).toEqual([ - // added and removed values reflect the ending values, not the values at the time of the call - ["before content change at", 0, "to add", [30, 20, 10], "to remove", [30, 20, 10]], - ["change at", 0, "from", 30], - ["change at", 1, "from", 20], - ["change at", 2, "from", 10], - ["change at", 0, "to", 10], - ["change at", 1, "to", 20], - ["change at", 2, "to", 30], - ["content change at", 0, "added", [10, 20, 30], "removed", [10, 20, 30]], - ]); - }); - - it("deletes one value", function () { - spy = jasmine.createSpy(); - expect(array).toEqual([10, 20, 30]); - expect(array.delete(40)).toBe(false); // to exercise deletion of non-existing entry - expect(array.delete(20)).toBe(true); - expect(array).toEqual([10, 30]); - expect(spy.argsForCall).toEqual([ - ["length change from", 3], - ["before content change at", 1, "to add", [], "to remove", [20]], - ["change at", 1, "from", 20], - ["change at", 2, "from", 30], - ["change at", 1, "to", 30], - ["change at", 2, "to", undefined], - ["content change at", 1, "added", [], "removed", [20]], - ["length change to", 2] - ]); - }); - - it("clears all values finally", function () { - spy = jasmine.createSpy(); - expect(array).toEqual([10, 30]); - array.clear(); - expect(array).toEqual([]); - expect(spy.argsForCall).toEqual([ - ["length change from", 2], - ["before content change at", 0, "to add", [], "to remove", [10, 30]], - ["change at", 0, "from", 10], - ["change at", 1, "from", 30], - ["change at", 0, "to", undefined], - ["change at", 1, "to", undefined], - ["content change at", 0, "added", [], "removed", [10, 30]], - ["length change to", 0] - ]); - }); - - it("removes content change listeners", function () { - spy = jasmine.createSpy(); - - // mute all listeners - - var descriptor = array.getOwnPropertyChangeDescriptor('length'); - descriptor.willChangeListeners.forEach(function (listener) { - array.removeBeforeOwnPropertyChangeListener('length', listener); - }); - descriptor.changeListeners.forEach(function (listener) { - array.removeOwnPropertyChangeListener('length', listener); - }); - - var descriptor = array.getRangeChangeDescriptor(); - descriptor.willChangeListeners.forEach(function (listener) { - array.removeBeforeRangeChangeListener(listener); - }); - descriptor.changeListeners.forEach(function (listener) { - array.removeRangeChangeListener(listener); - }); - - var descriptor = array.getMapChangeDescriptor(); - descriptor.willChangeListeners.forEach(function (listener) { - array.removeBeforeMapChangeListener(listener); - }); - descriptor.changeListeners.forEach(function (listener) { - array.removeMapChangeListener(listener); - }); - - // modify - array.splice(0, 0, 1, 2, 3); - - // note silence - expect(spy).wasNotCalled(); - }); - - // --------------- FIN ----------------- - - it("handles cyclic content change listeners", function () { - var foo = []; - var bar = []; - foo.addRangeChangeListener(function (plus, minus, index) { - // if this is a change in response to a change in bar, - // do not send back - if (bar.getRangeChangeDescriptor().isActive) - return; - bar.splice.apply(bar, [index, minus.length].concat(plus)); - }); - bar.addRangeChangeListener(function (plus, minus, index) { - if (foo.getRangeChangeDescriptor().isActive) - return; - foo.splice.apply(foo, [index, minus.length].concat(plus)); - }); - foo.push(10, 20, 30); - expect(bar).toEqual([10, 20, 30]); - bar.pop(); - expect(foo).toEqual([10, 20]); - }); - - it("observes length changes on arrays that are not otherwised observed", function () { - var array = [1, 2, 3]; - var spy = jasmine.createSpy(); - array.addOwnPropertyChangeListener("length", spy); - array.push(4); - expect(spy).toHaveBeenCalled(); - }); - - describe("splice", function () { - var array; - beforeEach(function () { - array = [0, 1, 2]; - array.makeObservable(); - }); - - it("handles a negative start", function () { - var removed = array.splice(-1, 1); - expect(removed).toEqual([2]); - expect(array).toEqual([0, 1]); - }); - - it("handles a negative length", function () { - var removed = array.splice(1, -1); - expect(removed).toEqual([]); - expect(array).toEqual([0, 1, 2]); - }); - - }); - - // Disabled because it takes far too long - xdescribe("swap", function () { - var otherArray; - beforeEach(function () { - array.makeObservable(); - }); - it("should work with large arrays", function () { - otherArray = new Array(200000); - // Should not throw a Maximum call stack size exceeded error. - expect(function () { - array.swap(0, array.length, otherArray); - }).not.toThrow(); - expect(array.length).toEqual(200000); - }); - }); - -}); - diff --git a/spec/listen/map-changes.js b/spec/listen/map-changes.js deleted file mode 100644 index bcbb1b5..0000000 --- a/spec/listen/map-changes.js +++ /dev/null @@ -1,58 +0,0 @@ - -module.exports = describeMapChanges; -function describeMapChanges(Map) { - - it("should dispatch addition", function () { - var map = Map(); - var spy = jasmine.createSpy(); - map.addBeforeMapChangeListener(function (value, key) { - spy('before', key, value); - }); - map.addMapChangeListener(function (value, key) { - spy('after', key, value); - }); - map.set(0, 10); - expect(spy.argsForCall).toEqual([ - ['before', 0, undefined], - ['after', 0, 10] - ]); - }); - - it("should dispatch alteration", function () { - var map = Map([[0, 10]]); - var spy = jasmine.createSpy(); - map.addBeforeMapChangeListener(function (value, key) { - spy('before', key, value); - }); - map.addMapChangeListener(function (value, key) { - spy('after', key, value); - }); - map.set(0, 20); - expect(spy.argsForCall).toEqual([ - ['before', 0, 10], - ['after', 0, 20] - ]); - }); - - it("should dispatch deletion", function () { - var map = Map([[0, 20]]); - // Arrays do not behave like maps for deletion. - if (Array.isArray(map)) { - return; - } - var spy = jasmine.createSpy(); - map.addBeforeMapChangeListener(function (value, key) { - spy('before', key, value); - }); - map.addMapChangeListener(function (value, key) { - spy('after', key, value); - }); - map.delete(0); - expect(spy.argsForCall).toEqual([ - ['before', 0, 20], - ['after', 0, undefined] - ]); - }); - -} - diff --git a/spec/listen/property-changes-spec.js b/spec/listen/property-changes-spec.js deleted file mode 100644 index cc31bcd..0000000 --- a/spec/listen/property-changes-spec.js +++ /dev/null @@ -1,172 +0,0 @@ -/* - Based in part on observable arrays from Motorola Mobility’s Montage - Copyright (c) 2012, Motorola Mobility LLC. All Rights Reserved. - 3-Clause BSD License - https://github.com/motorola-mobility/montage/blob/master/LICENSE.md -*/ - -require("../../shim"); -var PropertyChanges = require("../../listen/property-changes"); - -describe("PropertyChanges", function () { - - it("observes setter on object", function () { - spy = jasmine.createSpy(); - var object = {}; - PropertyChanges.addBeforeOwnPropertyChangeListener(object, 'x', function (value, key) { - spy('from', value, key); - }); - PropertyChanges.addOwnPropertyChangeListener(object, 'x', function (value, key) { - spy('to', value, key); - }); - object.x = 10; - expect(object.x).toEqual(10); - //PropertyChanges.makePropertyUnobservable(object, 'x'); - //object.x = 20; - //expect(object.x).toEqual(20); - expect(spy.argsForCall).toEqual([ - ['from', undefined, 'x'], - ['to', 10, 'x'], - ]); - }); - - it("observes setter on object with getter/setter", function () { - spy = jasmine.createSpy(); - var value; - var object = Object.create(Object.prototype, { - _x: { - value: 10, - writable: true - }, - x: { - get: function () { - return this._x; - }, - set: function (_value) { - this._x = _value; - }, - enumerable: false, - configurable: true - } - }); - PropertyChanges.addBeforeOwnPropertyChangeListener(object, 'x', function (value, key) { - spy('from', value, key); - }); - PropertyChanges.addOwnPropertyChangeListener(object, 'x', function (value, key) { - spy('to', value, key); - }); - object.x = 20; - expect(object.x).toEqual(20); - expect(spy.argsForCall).toEqual([ - ['from', 10, 'x'], - ['to', 20, 'x'], // reports no change - ]); - }); - - it("shouldn't call the listener if the new value is the same after calling the object setter", function () { - spy = jasmine.createSpy(); - var value; - var object = Object.create(Object.prototype, { - x: { - get: function () { - return 20; - }, - set: function (_value) { - // refuse to change internal state - }, - enumerable: false, - configurable: true - } - }); - PropertyChanges.addBeforeOwnPropertyChangeListener(object, 'x', function (value, key) { - spy('from', value, key); - }); - PropertyChanges.addOwnPropertyChangeListener(object, 'x', function (value, key) { - spy('to', value, key); - }); - object.x = 10; - expect(object.x).toEqual(20); - expect(spy).not.toHaveBeenCalled(); - }); - - it("calls setter on object when the new value is the current value", function() { - var object = Object.create(Object.prototype, { - _x: {value: 0, writable: true}, - x: { - get: function() { - return this._x; - }, - set: function(value) { - this._x = value * 2; - }, - configurable: true, - enumerable: true - } - }); - - PropertyChanges.addOwnPropertyChangeListener(object, "x", function() {}); - - object.x = 1; - object.x = 2; - - expect(object.x).toBe(4); - }); - - it("handles cyclic own property change listeners", function () { - var a = {}; - var b = {}; - PropertyChanges.addOwnPropertyChangeListener(a, 'foo', function (value) { - b.bar = value; - }); - PropertyChanges.addOwnPropertyChangeListener(b, 'bar', function (value) { - a.foo = value; - }); - a.foo = 10; - expect(b.bar).toEqual(10); - }); - - it("handles generic own property change listeners", function () { - var object = { - handlePropertyChange: function (value, key) { - expect(value).toBe(10); - expect(key).toBe("foo"); - } - }; - spyOn(object, "handlePropertyChange").andCallThrough(); - PropertyChanges.addOwnPropertyChangeListener(object, "foo", object); - object.foo = 10; - expect(object.handlePropertyChange).toHaveBeenCalled(); - }); - - it("handles specific own property change listeners", function () { - var object = { - handleFooChange: function (value) { - expect(value).toBe(10); - } - }; - spyOn(object, "handleFooChange").andCallThrough(); - PropertyChanges.addOwnPropertyChangeListener(object, "foo", object); - object.foo = 10; - expect(object.handleFooChange).toHaveBeenCalled(); - }); - - it("calls later handlers if earlier ones remove themselves", function () { - var object = { - foo: true - }; - var listener1 = { - handleFooChange: function (value, key, object) { - PropertyChanges.removeOwnPropertyChangeListener(object, key, listener1); - } - }; - var listener2 = jasmine.createSpyObj("listener2", ["handleFooChange"]); - - PropertyChanges.addOwnPropertyChangeListener(object, "foo", listener1); - PropertyChanges.addOwnPropertyChangeListener(object, "foo", listener2); - - object.foo = false; - expect(listener2.handleFooChange).toHaveBeenCalled(); - }); - -}); - diff --git a/spec/listen/range-changes.js b/spec/listen/range-changes.js deleted file mode 100644 index 217b9de..0000000 --- a/spec/listen/range-changes.js +++ /dev/null @@ -1,303 +0,0 @@ - -module.exports = describeRangeChanges; -function describeRangeChanges(Collection) { - - var collection = Collection([1, 2, 3]); - var spy; - - // the following tests all share the same initial collection so they - // are sensitive to changes in order - - it("set up listeners", function () { - - collection.addBeforeOwnPropertyChangeListener("length", function (length) { - spy("length change from", length); - }); - - collection.addOwnPropertyChangeListener("length", function (length) { - spy("length change to", length); - }); - - collection.addBeforeRangeChangeListener(function (plus, minus, index) { - spy("before content change at", index, "to add", plus.slice(), "to remove", minus.slice()); - }); - - collection.addRangeChangeListener(function (plus, minus, index) { - spy("content change at", index, "added", plus.slice(), "removed", minus.slice()); - }); - - }); - - it("clear initial values", function () { - spy = jasmine.createSpy(); - expect(collection.slice()).toEqual([1, 2, 3]); - collection.clear(); - expect(collection.slice()).toEqual([]); - expect(spy.argsForCall).toEqual([ - ["before content change at", 0, "to add", [], "to remove", [1, 2, 3]], - ["length change from", 3], - ["length change to", 0], - ["content change at", 0, "added", [], "removed", [1, 2, 3]], - ]); - }); - - it("push two values on empty collection", function () { - spy = jasmine.createSpy(); - expect(collection.slice()).toEqual([]); // initial - collection.push(10, 20); - expect(collection.slice()).toEqual([10, 20]); - expect(spy.argsForCall).toEqual([ - ["before content change at", 0, "to add", [10, 20], "to remove", []], - ["length change from", 0], - ["length change to", 2], - ["content change at", 0, "added", [10, 20], "removed", []], - ]); - - }); - - it("pop one value", function () { - spy = jasmine.createSpy(); - expect(collection.slice()).toEqual([10, 20]); - collection.pop(); - expect(collection.slice()).toEqual([10]); - expect(spy.argsForCall).toEqual([ - ["before content change at", 1, "to add", [], "to remove", [20]], - ["length change from", 2], - ["length change to", 1], - ["content change at", 1, "added", [], "removed", [20]], - ]); - }); - - it("push two values on top of existing one, with hole open for splice", function () { - spy = jasmine.createSpy(); - expect(collection.slice()).toEqual([10]); - collection.push(40, 50); - expect(collection.slice()).toEqual([10, 40, 50]); - expect(spy.argsForCall).toEqual([ - ["before content change at", 1, "to add", [40, 50], "to remove", []], - ["length change from", 1], - ["length change to", 3], - ["content change at", 1, "added", [40, 50], "removed", []], - ]); - }); - - it("splices two values into middle", function () { - spy = jasmine.createSpy(); - expect(collection.slice()).toEqual([10, 40, 50]); - expect(collection.splice(1, 0, 20, 30)).toEqual([]); - expect(collection.slice()).toEqual([10, 20, 30, 40, 50]); - expect(spy.argsForCall).toEqual([ - ["before content change at", 1, "to add", [20, 30], "to remove", []], - ["length change from", 3], - ["length change to", 5], - ["content change at", 1, "added", [20, 30], "removed", []], - ]); - }); - - it("pushes one value to end", function () { - spy = jasmine.createSpy(); - expect(collection.slice()).toEqual([10, 20, 30, 40, 50]); - collection.push(60); - expect(collection.slice()).toEqual([10, 20, 30, 40, 50, 60]); - expect(spy.argsForCall).toEqual([ - ["before content change at", 5, "to add", [60], "to remove", []], - ["length change from", 5], - ["length change to", 6], - ["content change at", 5, "added", [60], "removed", []], - ]); - }); - - it("splices in place", function () { - spy = jasmine.createSpy(); - expect(collection.slice()).toEqual([10, 20, 30, 40, 50, 60]); - expect(collection.splice(2, 2, "A", "B")).toEqual([30, 40]); - expect(collection.slice()).toEqual([10, 20, "A", "B", 50, 60]); - expect(spy.argsForCall).toEqual([ - // no length change - ["before content change at", 2, "to add", ["A", "B"], "to remove", [30, 40]], - ["content change at", 2, "added", ["A", "B"], "removed", [30, 40]], - ]); - }); - - // ---- fresh start - - it("shifts one from the beginning", function () { - collection.clear(); // start over fresh - collection.push(10, 20, 30); - spy = jasmine.createSpy(); - expect(collection.slice()).toEqual([10, 20, 30]); - expect(collection.shift()).toEqual(10); - expect(collection.slice()).toEqual([20, 30]); - expect(spy.argsForCall).toEqual([ - ["before content change at", 0, "to add", [], "to remove", [10]], - ["length change from", 3], - ["length change to", 2], - ["content change at", 0, "added", [], "removed", [10]], - ]); - }); - - it("sets new value at end", function () { - spy = jasmine.createSpy(); - expect(collection.slice()).toEqual([20, 30]); - expect(collection.splice(2, 0, 40)).toEqual([]); - expect(collection.slice()).toEqual([20, 30, 40]); - expect(spy.argsForCall).toEqual([ - ["before content change at", 2, "to add", [40], "to remove", []], - ["length change from", 2], - ["length change to", 3], - ["content change at", 2, "added", [40], "removed", []], - ]); - }); - - it("sets new value at beginning", function () { - spy = jasmine.createSpy(); - expect(collection.slice()).toEqual([20, 30, 40]); - expect(collection.splice(0, 1, 10)).toEqual([20]); - expect(collection.slice()).toEqual([10, 30, 40]); - expect(spy.argsForCall).toEqual([ - ["before content change at", 0, "to add", [10], "to remove", [20]], - ["content change at", 0, "added", [10], "removed", [20]], - ]); - }); - - // ---- fresh start - - it("unshifts one to the beginning", function () { - collection.clear(); // start over fresh - expect(collection.slice()).toEqual([]); - spy = jasmine.createSpy(); - collection.unshift(30); - expect(collection.slice()).toEqual([30]); - expect(spy.argsForCall).toEqual([ - ["before content change at", 0, "to add", [30], "to remove", []], - ["length change from", 0], - ["length change to", 1], - ["content change at", 0, "added", [30], "removed", []], - ]); - }); - - it("unshifts two values on beginning of already populated collection", function () { - spy = jasmine.createSpy(); - expect(collection.slice()).toEqual([30]); - collection.unshift(10, 20); - expect(collection.slice()).toEqual([10, 20, 30]); - expect(spy.argsForCall).toEqual([ - // added and removed values reflect the ending values, not the values at the time of the call - ["before content change at", 0, "to add", [10, 20], "to remove", []], - ["length change from", 1], - ["length change to", 3], - ["content change at", 0, "added", [10, 20], "removed", []], - ]); - }); - - it("reverses in place", function () { - spy = jasmine.createSpy(); - expect(collection.slice()).toEqual([10, 20, 30]); - collection.reverse(); - expect(collection.slice()).toEqual([30, 20, 10]); - expect(spy.argsForCall).toEqual([ - ["before content change at", 0, "to add", [30, 20, 10], "to remove", [10, 20, 30]], - ["content change at", 0, "added", [30, 20, 10], "removed", [10, 20, 30]], - ]); - }); - - it("sorts in place", function () { - spy = jasmine.createSpy(); - expect(collection.slice()).toEqual([30, 20, 10]); - collection.sort(); - expect(collection.slice()).toEqual([10, 20, 30]); - expect(spy.argsForCall).toEqual([ - // added and removed values reflect the ending values, not the values at the time of the call - ["before content change at", 0, "to add", [10, 20, 30], "to remove", [30, 20, 10]], - ["content change at", 0, "added", [10, 20, 30], "removed", [30, 20, 10]], - ]); - }); - - it("deletes one value", function () { - spy = jasmine.createSpy(); - expect(collection.slice()).toEqual([10, 20, 30]); - expect(collection.delete(40)).toBe(false); // to exercise deletion of non-existing entry - expect(collection.delete(20)).toBe(true); - expect(collection.slice()).toEqual([10, 30]); - expect(spy.argsForCall).toEqual([ - ["before content change at", 1, "to add", [], "to remove", [20]], - ["length change from", 3], - ["length change to", 2], - ["content change at", 1, "added", [], "removed", [20]], - ]); - }); - - it("clears all values finally", function () { - spy = jasmine.createSpy(); - expect(collection.slice()).toEqual([10, 30]); - collection.clear(); - expect(collection.slice()).toEqual([]); - expect(spy.argsForCall).toEqual([ - ["before content change at", 0, "to add", [], "to remove", [10, 30]], - ["length change from", 2], - ["length change to", 0], - ["content change at", 0, "added", [], "removed", [10, 30]], - ]); - }); - - it("removes content change listeners", function () { - spy = jasmine.createSpy(); - - // mute all listeners - - var descriptor = collection.getOwnPropertyChangeDescriptor('length'); - descriptor.willChangeListeners.forEach(function (listener) { - collection.removeBeforeOwnPropertyChangeListener('length', listener); - }); - descriptor.changeListeners.forEach(function (listener) { - collection.removeOwnPropertyChangeListener('length', listener); - }); - - var descriptor = collection.getRangeChangeDescriptor(); - descriptor.willChangeListeners.forEach(function (listener) { - collection.removeBeforeRangeChangeListener(listener); - }); - descriptor.changeListeners.forEach(function (listener) { - collection.removeRangeChangeListener(listener); - }); - - // modify - collection.splice(0, 0, 1, 2, 3); - - // note silence - expect(spy).wasNotCalled(); - }); - - // --------------- FIN ----------------- - - it("handles cyclic content change listeners", function () { - var foo = Collection([]); - var bar = Collection([]); - foo.addRangeChangeListener(function (plus, minus, index) { - // if this is a change in response to a change in bar, - // do not send back - if (bar.getRangeChangeDescriptor().isActive) - return; - bar.splice.apply(bar, [index, minus.length].concat(plus)); - }); - bar.addRangeChangeListener(function (plus, minus, index) { - if (foo.getRangeChangeDescriptor().isActive) - return; - foo.splice.apply(foo, [index, minus.length].concat(plus)); - }); - foo.push(10, 20, 30); - expect(bar.slice()).toEqual([10, 20, 30]); - bar.pop(); - expect(foo.slice()).toEqual([10, 20]); - }); - - it("observes length changes on collections that are not otherwised observed", function () { - var collection = new Collection([1, 2, 3]); - var spy = jasmine.createSpy(); - collection.addOwnPropertyChangeListener("length", spy); - collection.push(4); - expect(spy).toHaveBeenCalled(); - }); - -} - diff --git a/spec/lru-map-spec.js b/spec/lru-map-spec.js index b9f7746..814ffc3 100644 --- a/spec/lru-map-spec.js +++ b/spec/lru-map-spec.js @@ -63,18 +63,13 @@ describe("LruMap", function () { it("should dispatch deletion for stale entries", function () { var map = LruMap({a: 10, b: 20, c: 30}, 3); var spy = jasmine.createSpy(); - map.addBeforeMapChangeListener(function (value, key) { - spy('before', key, value); - }); - map.addMapChangeListener(function (value, key) { - spy('after', key, value); + map.observeMapChange(function (plus, minus, key, type) { + spy(plus, minus, key, type); }); map.set('d', 40); expect(spy.argsForCall).toEqual([ - ['before', 'd', undefined], // d will be added - ['before', 'a', undefined], // then a is pruned (stale) - ['after', 'a', undefined], // afterwards a is still pruned - ['after', 'd', 40] // and now d has a value + [undefined, 10, "a", "delete"], // a pruned + [40, undefined, "d", "create"] // d added ]); }); }); diff --git a/spec/lru-set-spec.js b/spec/lru-set-spec.js index 3c28bcb..b2bc9bf 100644 --- a/spec/lru-set-spec.js +++ b/spec/lru-set-spec.js @@ -16,7 +16,7 @@ describe("LruSet", function () { describeSet(LruSet); }); - + it("should remove stale entries", function () { var set = LruSet([4, 3, 1, 2, 3], 3); expect(set.length).toBe(3); @@ -25,11 +25,11 @@ describe("LruSet", function () { set.add(4); expect(set.toArray()).toEqual([2, 3, 4]); }); - + it("should emit LRU changes as singleton operation", function () { var a = 1, b = 2, c = 3, d = 4; var lruset = LruSet([d, c, a, b, c], 3); - lruset.addRangeChangeListener(function(plus, minus) { + lruset.observeRangeChange(function(plus, minus) { expect(plus).toEqual([d]); expect(minus).toEqual([a]); }); @@ -39,11 +39,11 @@ describe("LruSet", function () { it("should dispatch LRU changes as singleton operation", function () { var set = LruSet([4, 3, 1, 2, 3], 3); var spy = jasmine.createSpy(); - set.addBeforeRangeChangeListener(function (plus, minus) { + set.observeRangeWillChange(function (plus, minus) { spy('before-plus', plus); spy('before-minus', minus); }); - set.addRangeChangeListener(function (plus, minus) { + set.observeRangeChange(function (plus, minus) { spy('after-plus', plus); spy('after-minus', minus); }); diff --git a/spec/map-spec.js b/spec/map-spec.js index bbd4559..9709635 100644 --- a/spec/map-spec.js +++ b/spec/map-spec.js @@ -3,11 +3,9 @@ var Map = require("../map"); var describeDict = require("./dict"); var describeMap = require("./map"); -var describeMapChanges = require("./listen/map-changes"); describe("Map", function () { describeDict(Map); describeMap(Map); - describeMapChanges(Map); }); diff --git a/spec/map.js b/spec/map.js index fab55c4..104d6e0 100644 --- a/spec/map.js +++ b/spec/map.js @@ -1,13 +1,11 @@ // Tests that are equally applicable to Map, unbounded LruMap, FastMap. // These do not apply to SortedMap since keys are not comparable. -var describeMapChanges = require("./listen/map-changes"); +var describeObservableMap = require("./observable-map"); module.exports = describeMap; function describeMap(Map, values) { - describeMapChanges(Map); - values = values || []; var a = values[0] || {}; var b = values[1] || {}; @@ -85,5 +83,7 @@ function describeMap(Map, values) { expect(map.equals(clone)).toBe(true); }); + describeObservableMap(Map); + } diff --git a/spec/observable-array-spec.js b/spec/observable-array-spec.js new file mode 100644 index 0000000..d3ca114 --- /dev/null +++ b/spec/observable-array-spec.js @@ -0,0 +1,316 @@ + +require("../observable-array"); +// TODO var describeObservableRange = require("./observable-range"); +// TODO make Array.from consistent with List + +describe("Array", function () { + it("change dispatch properties should not be enumerable", function () { + // this verifies that dispatchesRangeChanges and dispatchesMapChanges + // are both non-enumerable, and any other properties that might get + // added in the future. + for (var name in [1, 2,, 3]) { + expect(isNaN(+name)).toBe(false); + } + }); +}); + +describe("Array change dispatch with map observers", function () { + + var array; + var spy; + var continued; + + beforeEach(function () { + if (continued) { + continued = false; + return; + } + + array = []; + + array.observePropertyWillChange("length", function (plus, minus) { + spy("length will change from", minus, "to", plus); + }); + + array.observePropertyChange("length", function (plus, minus) { + spy("length change from", minus, "to", plus); + }); + + array.observeRangeWillChange(function (plus, minus, index) { + spy("range will change from", minus, "to", plus, "at", index); + }); + + array.observeRangeChange(function (plus, minus, index) { + spy("range change from", minus, "to", plus, "at", index); + }); + + array.observeMapWillChange(function (plus, minus, index, type) { + spy("map will", type, index, "from", minus, "to", plus); + }); + + array.observeMapChange(function (plus, minus, index, type) { + spy("map", type, index, "from", minus, "to", plus); + }); + + }); + + it("push", function () { + spy = jasmine.createSpy(); + array.push(1, 2, 3); + expect(spy.argsForCall).toEqual([ + ["length will change from", 0, "to", 3], + ["range will change from", [], "to", [1, 2, 3], "at", 0], + ["map will", "create", 0, "from", undefined, "to", 1], + ["map will", "create", 1, "from", undefined, "to", 2], + ["map will", "create", 2, "from", undefined, "to", 3], + ["map", "create", 0, "from", undefined, "to", 1], + ["map", "create", 1, "from", undefined, "to", 2], + ["map", "create", 2, "from", undefined, "to", 3], + ["range change from", [], "to", [1, 2, 3], "at", 0], + ["length change from", 0, "to", 3] + ]); + continued = true; + }); + + it("clear", function () { + spy = jasmine.createSpy(); + array.clear(); + expect(array).toEqual([]); + expect(spy.argsForCall).toEqual([ + ["length will change from", 3, "to", 0], + ["range will change from", [1, 2, 3], "to", [], "at", 0], + ["map will", "delete", 0, "from", 1, "to", undefined], + ["map will", "delete", 1, "from", 2, "to", undefined], + ["map will", "delete", 2, "from", 3, "to", undefined], + ["map", "delete", 0, "from", 1, "to", undefined], + ["map", "delete", 1, "from", 2, "to", undefined], + ["map", "delete", 2, "from", 3, "to", undefined], + ["range change from", [1, 2, 3], "to", [], "at", 0], + ["length change from", 3, "to", 0] + ]); + }); + + it("pop one value from an array", function () { + array.push(1, 2, 3); + spy = jasmine.createSpy(); + array.pop(); + expect(spy.argsForCall).toEqual([ + ["length will change from", 3, "to", 2], + ["range will change from", [3], "to", [], "at", 2], + ["map will", "delete", 2, "from", 3, "to", undefined], + ["map", "delete", 2, "from", 3, "to", undefined], + ["range change from", [3], "to", [], "at", 2], + ["length change from", 3, "to", 2] + ]); + }); + + it("shift one value off an array", function () { + array.push(1, 2, 3); + spy = jasmine.createSpy(); + array.shift(); + expect(spy.argsForCall).toEqual([ + ["length will change from", 3, "to", 2], + ["range will change from", [1], "to", [], "at", 0], + ["map will", "update", 0, "from", 1, "to", 2], + ["map will", "update", 1, "from", 2, "to", 3], + ["map will", "delete", 2, "from", 3, "to", undefined], + ["map", "update", 0, "from", 1, "to", 2], + ["map", "update", 1, "from", 2, "to", 3], + ["map", "delete", 2, "from", 3, "to", undefined], + ["range change from", [1], "to", [], "at", 0], + ["length change from", 3, "to", 2] + ]); + }); + + it("replaces values into the midst of an array", function () { + array.push(1, 3, 2, 4); + spy = jasmine.createSpy(); + array.splice(1, 2, 2, 3); + expect(spy.argsForCall).toEqual([ + ["range will change from", [3, 2], "to", [2, 3], "at", 1], + ["map will", "update", 1, "from", 3, "to", 2], + ["map will", "update", 2, "from", 2, "to", 3], + ["map", "update", 1, "from", 3, "to", 2], + ["map", "update", 2, "from", 2, "to", 3], + ["range change from", [3, 2], "to", [2, 3], "at", 1] + ]); + }); + + it("replaces values into the midst of an array, from a negative index", function () { + array.push(1, 3, 2, 4); + spy = jasmine.createSpy(); + array.splice(-3, 2, 2, 3); + expect(spy.argsForCall).toEqual([ + ["range will change from", [3, 2], "to", [2, 3], "at", 1], + ["map will", "update", 1, "from", 3, "to", 2], + ["map will", "update", 2, "from", 2, "to", 3], + ["map", "update", 1, "from", 3, "to", 2], + ["map", "update", 2, "from", 2, "to", 3], + ["range change from", [3, 2], "to", [2, 3], "at", 1] + ]); + }); + + it("splices values into the midst of an array", function () { + array.push(1, 4); + spy = jasmine.createSpy(); + array.splice(1, 0, 2, 3); + expect(spy.argsForCall).toEqual([ + ["length will change from", 2, "to", 4], + ["range will change from", [], "to", [2, 3], "at", 1], + ["map will", "update", 1, "from", 4, "to", 2], + ["map will", "create", 2, "from", undefined, "to", 3], + ["map will", "create", 3, "from", undefined, "to", 4], + ["map", "update", 1, "from", 4, "to", 2], + ["map", "create", 2, "from", undefined, "to", 3], + ["map", "create", 3, "from", undefined, "to", 4], + ["range change from", [], "to", [2, 3], "at", 1], + ["length change from", 2, "to", 4] + ]); + }); + + it("splices values into the midst of an array, with a negative length", function () { + array.push(1, 4); + spy = jasmine.createSpy(); + array.splice(1, -1, 2, 3); + expect(spy.argsForCall).toEqual([ + ["length will change from", 2, "to", 4], + ["range will change from", [], "to", [2, 3], "at", 1], + ["map will", "update", 1, "from", 4, "to", 2], + ["map will", "create", 2, "from", undefined, "to", 3], + ["map will", "create", 3, "from", undefined, "to", 4], + ["map", "update", 1, "from", 4, "to", 2], + ["map", "create", 2, "from", undefined, "to", 3], + ["map", "create", 3, "from", undefined, "to", 4], + ["range change from", [], "to", [2, 3], "at", 1], + ["length change from", 2, "to", 4] + ]); + }); + + it("set at end", function () { + array.push(1, 2, 3); + spy = jasmine.createSpy(); + array.set(3, 4); + expect(spy.argsForCall).toEqual([ + ["length will change from", 3, "to", 4], + ["range will change from", [], "to", [4], "at", 3], + ["map will", "create", 3, "from", undefined, "to", 4], + ["map", "create", 3, "from", undefined, "to", 4], + ["range change from", [], "to", [4], "at", 3], + ["length change from", 3, "to", 4] + ]); + }); + + it("set at beginning", function () { + array.push(1, 2, 3); + spy = jasmine.createSpy(); + array.set(0, 3); + expect(spy.argsForCall).toEqual([ + ["range will change from", [1], "to", [3], "at", 0], + ["map will", "update", 0, "from", 1, "to", 3], + ["map", "update", 0, "from", 1, "to", 3], + ["range change from", [1], "to", [3], "at", 0] + ]); + }); + + it("unshifts", function () { + array.push(3, 4); + spy = jasmine.createSpy(); + array.unshift(1, 2); + expect(spy.argsForCall).toEqual([ + ["length will change from", 2, "to", 4], + ["range will change from", [], "to", [1, 2], "at", 0], + ["map will", "update", 0, "from", 3, "to", 1], + ["map will", "update", 1, "from", 4, "to", 2], + ["map will", "create", 2, "from", undefined, "to", 3], + ["map will", "create", 3, "from", undefined, "to", 4], + ["map", "update", 0, "from", 3, "to", 1], + ["map", "update", 1, "from", 4, "to", 2], + ["map", "create", 2, "from", undefined, "to", 3], + ["map", "create", 3, "from", undefined, "to", 4], + ["range change from", [], "to", [1, 2], "at", 0], + ["length change from", 2, "to", 4] + ]); + }); + + it("reverses in place", function () { + array.push(10, 20, 30); + spy = jasmine.createSpy(); + array.reverse(); + expect(spy.argsForCall).toEqual([ + ["range will change from", [10, 20, 30], "to", [30, 20, 10], "at", 0], + ["map will", "update", 0, "from", 10, "to", 30], + ["map will", "update", 2, "from", 30, "to", 10], + ["map", "update", 0, "from", 10, "to", 30], + ["map", "update", 2, "from", 30, "to", 10], + ["range change from", [10, 20, 30], "to", [30, 20, 10], "at", 0] + ]); + }); + + it("sorts in place", function () { + array.push(30, 20, 10); + spy = jasmine.createSpy(); + array.sort(); + expect(spy.argsForCall).toEqual([ + ["range will change from", [30, 20, 10], "to", [10, 20, 30], "at", 0], + ["map will", "update", 0, "from", 30, "to", 10], + ["map will", "update", 2, "from", 10, "to", 30], + ["map", "update", 0, "from", 30, "to", 10], + ["map", "update", 2, "from", 10, "to", 30], + ["range change from", [30, 20, 10], "to", [10, 20, 30], "at", 0] + ]); + }); + + // TODO cancel observers + +}); + +describe("Array changes", function () { + + it("observes range changes on arrays that are not otherwised observed", function () { + var array = [1, 2, 3]; + var spy = jasmine.createSpy(); + array.observeRangeChange(spy); + array.push(4); + expect(spy).toHaveBeenCalledWith([4], [], 3, array); + }); + + it("observes length changes on arrays that are not otherwised observed", function () { + var array = [1, 2, 3]; + var spy = jasmine.createSpy(); + array.observePropertyChange("length", spy); + array.push(4); + expect(spy).toHaveBeenCalledWith(4, 3, "length", array); + }); + + it("observes map changes on arrays that are not otherwised observed", function () { + var array = [1, 2, 3]; + var spy = jasmine.createSpy(); + array.observeMapChange(spy); + array.push(4); + expect(spy).toHaveBeenCalledWith(4, undefined, 3, "create", array); + }); + + it("observes index changes on arrays that are not otherwised observed", function () { + var array = [1, 2, 3]; + var spy = jasmine.createSpy(); + array.observePropertyChange(3, spy); + array.push(4); + expect(spy).toHaveBeenCalledWith(4, undefined, 3, array); + }); + + describe("swap", function () { + it("works with large arrays", function () { + var array = []; + array.makeRangeChangesObservable(); + var otherArray; + otherArray = new Array(200000); + // Should not throw a Maximum call stack size exceeded error. + expect(function () { + array.swap(0, array.length, otherArray); + }).not.toThrow(); + expect(array.length).toEqual(200000); + }); + }); + +}); + diff --git a/spec/observable-map-spec.js b/spec/observable-map-spec.js new file mode 100644 index 0000000..fb7a74c --- /dev/null +++ b/spec/observable-map-spec.js @@ -0,0 +1,60 @@ +"use strict"; + +var ObservableMap = require("../observable-map"); + +describe("ObservableMap", function () { + + describe("observeMapChange", function () { + it("observe, dispatch", function () { + + var map = Object.create(ObservableMap.prototype); + var spy; + + var observer = map.observeMapChange(function (plus, minus, key, type, object) { + spy(plus, minus, key, type, object); + }); + + spy = jasmine.createSpy(); + map.dispatchMapChange("create", "foo", 10, undefined); + expect(spy).toHaveBeenCalledWith(10, undefined, "foo", "create", map); + + }); + + it("observe, cancel, dispatch", function () { + + var map = Object.create(ObservableMap.prototype); + var spy; + + var observer = map.observeMapChange(function (plus, minus, key, type, object) { + spy(plus, minus, key, type, object); + }); + + spy = jasmine.createSpy(); + observer.cancel(); + map.dispatchMapChange("create", "foo", 10, undefined); + expect(spy).not.toHaveBeenCalled(); + + }); + + it("observe, dispatch, cancel, dispatch", function () { + + var map = Object.create(ObservableMap.prototype); + var spy; + + var observer = map.observeMapChange(function (plus, minus, key, type, object) { + spy(plus, minus, key, type, object); + }); + + spy = jasmine.createSpy(); + map.dispatchMapChange("create", "foo", 10, undefined); + expect(spy).toHaveBeenCalledWith(10, undefined, "foo", "create", map); + + observer.cancel(); + spy = jasmine.createSpy(); + map.dispatchMapChange("create", "foo", 10, undefined); + expect(spy).not.toHaveBeenCalled(); + }); + }); + +}); + diff --git a/spec/observable-map.js b/spec/observable-map.js new file mode 100644 index 0000000..9e0126f --- /dev/null +++ b/spec/observable-map.js @@ -0,0 +1,31 @@ + +module.exports = describeObservableMap; +function describeObservableMap(Map) { + it("create, update, delete", function () { + var map = new Map(); + var spy = jasmine.createSpy(); + var observer = map.observeMapChange(function (plus, minus, key, type) { + spy(plus, minus, key, type); + }); + + map.set("a", 10); + expect(spy).toHaveBeenCalledWith(10, undefined, "a", "create"); + map.set("a", 20); + expect(spy).toHaveBeenCalledWith(20, 10, "a", "update"); + map.delete("a"); + expect(spy).toHaveBeenCalledWith(undefined, 20, "a", "delete"); + + spy = jasmine.createSpy(); + map.set("a", 30); + expect(spy).toHaveBeenCalledWith(30, undefined, "a", "create"); + map.set("a", undefined); + expect(spy).toHaveBeenCalledWith(undefined, 30, "a", "update"); + + spy = jasmine.createSpy(); + observer.cancel(); + + map.set("b", 20); + expect(spy).not.toHaveBeenCalled(); + }); +} + diff --git a/spec/observe-property-changes-spec.js b/spec/observable-object-spec.js similarity index 97% rename from spec/observe-property-changes-spec.js rename to spec/observable-object-spec.js index dcccd75..be94579 100644 --- a/spec/observe-property-changes-spec.js +++ b/spec/observable-object-spec.js @@ -9,13 +9,13 @@ // TODO observePropertyWillChange // TODO access observer notes -var ObservePropertyChanges = require("../observe-property-changes"); -var observePropertyChange = ObservePropertyChanges.observePropertyChange; -var makePropertyObservable = ObservePropertyChanges.makePropertyObservable; -var preventPropertyObserver = ObservePropertyChanges.preventPropertyObserver; -var dispatchPropertyChange = ObservePropertyChanges.dispatchPropertyChange; +var ObservableObject = require("../observable-object"); +var observePropertyChange = ObservableObject.observePropertyChange; +var makePropertyObservable = ObservableObject.makePropertyObservable; +var preventPropertyObserver = ObservableObject.preventPropertyObserver; +var dispatchPropertyChange = ObservableObject.dispatchPropertyChange; -describe("ObservePropertyChanges", function () { +describe("ObservableObject", function () { describe("observePropertyChange", function () { diff --git a/spec/observable-range-spec.js b/spec/observable-range-spec.js new file mode 100644 index 0000000..2178e0c --- /dev/null +++ b/spec/observable-range-spec.js @@ -0,0 +1,23 @@ +"use strict"; + +var ObservableRange = require("../observable-range"); + +describe("ObservableRange", function () { + + describe("observeRangeChange", function () { + it("observe, dispatch", function () { + var range = Object.create(ObservableRange.prototype); + var spy; + + var observer = range.observeRangeChange(function (plus, minus, index) { + spy(plus, minus, index); + }); + + spy = jasmine.createSpy(); + range.dispatchRangeChange([1, 2, 3], [], 0); + expect(spy).toHaveBeenCalledWith([1, 2, 3], [], 0); + }); + }); + +}); + diff --git a/spec/observable-range.js b/spec/observable-range.js new file mode 100644 index 0000000..fa91b7f --- /dev/null +++ b/spec/observable-range.js @@ -0,0 +1,8 @@ +// Applied by list-spec +// TODO Applied by array-spec +// TODO or conslidate in order + +module.exports = describeRangeChanges; +function describeRangeChanges(Range) { +} + diff --git a/spec/shim-functions-spec.js b/spec/shim-functions-spec.js index 63b9de2..b9fc2ae 100644 --- a/spec/shim-functions-spec.js +++ b/spec/shim-functions-spec.js @@ -1,5 +1,6 @@ require("../shim-object"); +require("../shim-function"); describe("Function", function () { diff --git a/spec/sorted-array-map-spec.js b/spec/sorted-array-map-spec.js index 0bcc0e0..713ff41 100644 --- a/spec/sorted-array-map-spec.js +++ b/spec/sorted-array-map-spec.js @@ -2,11 +2,11 @@ var SortedArrayMap = require("../sorted-array-map"); var describeDict = require("./dict"); var describeMap = require("./map"); -var describeMapChanges = require("./listen/map-changes"); +var describeObservableMap = require("./observable-map"); describe("SortedArrayMap", function () { describeDict(SortedArrayMap); describeMap(SortedArrayMap, [1, 2, 3]); - describeMapChanges(SortedArrayMap); + describeObservableMap(SortedArrayMap); }); From 71b54e1e4014cd284379c5189dfec53a3125c0ad Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Fri, 31 Jan 2014 13:14:09 -0800 Subject: [PATCH 12/83] Update goals and future work --- README.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/README.md b/README.md index 9b3002d..9ebcdc5 100644 --- a/README.md +++ b/README.md @@ -1259,16 +1259,8 @@ Goals - array set (a set, for fast lookup, backed by an array for meaningful range changes) - fast list splicing -- dict map changes -- revise map changes to use separate handlers for add/delete -- revise tokens for range and map changes to specify complete alternate - delegate methods, particularly for forwarding directly to dispatch -- implement on/once/off listeners - Make it easier to created a SortedSet with a criterion like Function.by(Function.get('name')) -- evaluate exposing observeProperty, observeRangeChange, and observeMapChange - instead of the aEL/rEL inspired API FRB exposes today, to minimize - book-keeping and garbage collection More possible collections From c32707fed41bf90464ed73ea98488af5c6359d4e Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Fri, 31 Jan 2014 13:42:41 -0800 Subject: [PATCH 13/83] Update all collections for new change observers Such that all specs pass. --- dict.js | 4 ++-- fast-map.js | 4 ++-- fast-set.js | 4 ++-- list.js | 4 ++-- lru-map.js | 4 ++-- map.js | 4 ++-- set.js | 19 ++++++++++--------- sorted-array-map.js | 4 ++-- sorted-array-set.js | 8 ++++---- sorted-array.js | 8 ++++---- sorted-map.js | 4 ++-- sorted-set.js | 16 ++++++++-------- spec/array-spec.js | 2 +- spec/set-spec.js | 42 +++++++++++++++++++++++++++++------------ spec/sorted-set-spec.js | 4 ++-- 15 files changed, 75 insertions(+), 56 deletions(-) diff --git a/dict.js b/dict.js index fea6329..6808f1a 100644 --- a/dict.js +++ b/dict.js @@ -3,7 +3,7 @@ var Shim = require("./shim"); var GenericCollection = require("./generic-collection"); var GenericMap = require("./generic-map"); -var PropertyChanges = require("./listen/property-changes"); +var ObservableObject = require("./observable-object"); // Burgled from https://github.com/domenic/dict @@ -31,7 +31,7 @@ function unmangle(mangled) { Object.addEach(Dict.prototype, GenericCollection.prototype); Object.addEach(Dict.prototype, GenericMap.prototype); -Object.addEach(Dict.prototype, PropertyChanges.prototype); +Object.addEach(Dict.prototype, ObservableObject.prototype); Dict.prototype.constructClone = function (values) { return new this.constructor(values, this.mangle, this.getDefault); diff --git a/fast-map.js b/fast-map.js index 4dd1727..ebb7d50 100644 --- a/fast-map.js +++ b/fast-map.js @@ -4,7 +4,7 @@ var Shim = require("./shim"); var Set = require("./fast-set"); var GenericCollection = require("./generic-collection"); var GenericMap = require("./generic-map"); -var PropertyChanges = require("./listen/property-changes"); +var ObservableObject = require("./observable-object"); module.exports = FastMap; @@ -35,7 +35,7 @@ FastMap.FastMap = FastMap; // hack so require("fast-map").FastMap will work in M Object.addEach(FastMap.prototype, GenericCollection.prototype); Object.addEach(FastMap.prototype, GenericMap.prototype); -Object.addEach(FastMap.prototype, PropertyChanges.prototype); +Object.addEach(FastMap.prototype, ObservableObject.prototype); FastMap.prototype.constructClone = function (values) { return new this.constructor( diff --git a/fast-set.js b/fast-set.js index a571453..8e5eefb 100644 --- a/fast-set.js +++ b/fast-set.js @@ -6,7 +6,7 @@ var List = require("./list"); var GenericCollection = require("./generic-collection"); var GenericSet = require("./generic-set"); var TreeLog = require("./tree-log"); -var PropertyChanges = require("./listen/property-changes"); +var ObservableObject = require("./observable-object"); var object_has = Object.prototype.hasOwnProperty; @@ -31,7 +31,7 @@ FastSet.FastSet = FastSet; // hack so require("fast-set").FastSet will work in M Object.addEach(FastSet.prototype, GenericCollection.prototype); Object.addEach(FastSet.prototype, GenericSet.prototype); -Object.addEach(FastSet.prototype, PropertyChanges.prototype); +Object.addEach(FastSet.prototype, ObservableObject.prototype); FastSet.prototype.Buckets = Dict; FastSet.prototype.Bucket = List; diff --git a/list.js b/list.js index 277a437..fd0660b 100644 --- a/list.js +++ b/list.js @@ -379,10 +379,10 @@ List.prototype.updateIndexes = function (node, index) { } }; -List.prototype.makeObservable = function () { +List.prototype.makeRangeChangesObservable = function () { this.head.index = -1; this.updateIndexes(this.head.next, 0); - this.dispatchesRangeChanges = true; + ObservableRange.prototype.makeRangeChangesObservable.call(this); }; List.prototype.iterate = function () { diff --git a/lru-map.js b/lru-map.js index bbbf89b..fee012a 100644 --- a/lru-map.js +++ b/lru-map.js @@ -4,7 +4,7 @@ var Shim = require("./shim"); var LruSet = require("./lru-set"); var GenericCollection = require("./generic-collection"); var GenericMap = require("./generic-map"); -var PropertyChanges = require("./listen/property-changes"); +var ObservableObject = require("./observable-object"); module.exports = LruMap; @@ -36,7 +36,7 @@ LruMap.LruMap = LruMap; // hack so require("lru-map").LruMap will work in Montag Object.addEach(LruMap.prototype, GenericCollection.prototype); Object.addEach(LruMap.prototype, GenericMap.prototype); -Object.addEach(LruMap.prototype, PropertyChanges.prototype); +Object.addEach(LruMap.prototype, ObservableObject.prototype); LruMap.prototype.constructClone = function (values) { return new this.constructor( diff --git a/map.js b/map.js index cac86c0..5c2ca64 100644 --- a/map.js +++ b/map.js @@ -4,7 +4,7 @@ var Shim = require("./shim"); var Set = require("./set"); var GenericCollection = require("./generic-collection"); var GenericMap = require("./generic-map"); -var PropertyChanges = require("./listen/property-changes"); +var ObservableObject = require("./observable-object"); module.exports = Map; @@ -35,7 +35,7 @@ Map.Map = Map; // hack so require("map").Map will work in MontageJS Object.addEach(Map.prototype, GenericCollection.prototype); Object.addEach(Map.prototype, GenericMap.prototype); // overrides GenericCollection -Object.addEach(Map.prototype, PropertyChanges.prototype); +Object.addEach(Map.prototype, ObservableObject.prototype); Map.prototype.constructClone = function (values) { return new this.constructor( diff --git a/set.js b/set.js index 354e304..b973edb 100644 --- a/set.js +++ b/set.js @@ -5,8 +5,8 @@ var List = require("./list"); var FastSet = require("./fast-set"); var GenericCollection = require("./generic-collection"); var GenericSet = require("./generic-set"); -var PropertyChanges = require("./listen/property-changes"); -var RangeChanges = require("./listen/range-changes"); +var ObservableObject = require("./observable-object"); +var ObservableRange = require("./observable-range"); module.exports = Set; @@ -42,8 +42,8 @@ Set.Set = Set; // hack so require("set").Set will work in MontageJS Object.addEach(Set.prototype, GenericCollection.prototype); Object.addEach(Set.prototype, GenericSet.prototype); -Object.addEach(Set.prototype, PropertyChanges.prototype); -Object.addEach(Set.prototype, RangeChanges.prototype); +Object.addEach(Set.prototype, ObservableObject.prototype); +Object.addEach(Set.prototype, ObservableRange.prototype); Set.prototype.Order = List; Set.prototype.Store = FastSet; @@ -72,7 +72,7 @@ Set.prototype.add = function (value) { if (!this.store.has(node)) { var index = this.length; if (this.dispatchesRangeChanges) { - this.dispatchBeforeRangeChange([value], [], index); + this.dispatchRangeWillChange([value], [], index); } this.order.add(value); node = this.order.head.prev; @@ -91,7 +91,7 @@ Set.prototype["delete"] = function (value) { if (this.store.has(node)) { var node = this.store.get(node); if (this.dispatchesRangeChanges) { - this.dispatchBeforeRangeChange([], [value], node.index); + this.dispatchRangeWillChange([], [value], node.index); } this.store["delete"](node); // removes from the set this.order.splice(node, 1); // removes the node from the list @@ -130,7 +130,7 @@ Set.prototype.clear = function () { var clearing; if (this.dispatchesRangeChanges) { clearing = this.toArray(); - this.dispatchBeforeRangeChange([], clearing, 0); + this.dispatchRangeWillChange([], clearing, 0); } this.store.clear(); this.order.clear(); @@ -167,7 +167,8 @@ Set.prototype.log = function () { return set.log.apply(set, arguments); }; -Set.prototype.makeObservable = function () { - this.order.makeObservable(); +Set.prototype.makeRangeChangesObservable = function () { + this.order.makeRangeChangesObservable(); + ObservableRange.prototype.makeRangeChangesObservable.call(this); }; diff --git a/sorted-array-map.js b/sorted-array-map.js index 5bab17a..f614120 100644 --- a/sorted-array-map.js +++ b/sorted-array-map.js @@ -4,7 +4,7 @@ var Shim = require("./shim"); var SortedArraySet = require("./sorted-array-set"); var GenericCollection = require("./generic-collection"); var GenericMap = require("./generic-map"); -var PropertyChanges = require("./listen/property-changes"); +var ObservableObject = require("./observable-object"); module.exports = SortedArrayMap; @@ -36,7 +36,7 @@ SortedArrayMap.SortedArrayMap = SortedArrayMap; Object.addEach(SortedArrayMap.prototype, GenericCollection.prototype); Object.addEach(SortedArrayMap.prototype, GenericMap.prototype); -Object.addEach(SortedArrayMap.prototype, PropertyChanges.prototype); +Object.addEach(SortedArrayMap.prototype, ObservableObject.prototype); SortedArrayMap.prototype.constructClone = function (values) { return new this.constructor( diff --git a/sorted-array-set.js b/sorted-array-set.js index 4a0d7d2..4b74cf7 100644 --- a/sorted-array-set.js +++ b/sorted-array-set.js @@ -1,11 +1,11 @@ "use strict"; -module.exports = SortedArraySet; - var Shim = require("./shim"); var SortedArray = require("./sorted-array"); var GenericSet = require("./generic-set"); -var PropertyChanges = require("./listen/property-changes"); +var ObservableObject = require("./observable-object"); + +module.exports = SortedArraySet; function SortedArraySet(values, equals, compare, getDefault) { if (!(this instanceof SortedArraySet)) { @@ -22,7 +22,7 @@ SortedArraySet.prototype = Object.create(SortedArray.prototype); SortedArraySet.prototype.constructor = SortedArraySet; Object.addEach(SortedArraySet.prototype, GenericSet.prototype); -Object.addEach(SortedArraySet.prototype, PropertyChanges.prototype); +Object.addEach(SortedArraySet.prototype, ObservableObject.prototype); SortedArraySet.prototype.add = function (value) { if (!this.has(value)) { diff --git a/sorted-array.js b/sorted-array.js index 1931f9a..571c385 100644 --- a/sorted-array.js +++ b/sorted-array.js @@ -4,8 +4,8 @@ module.exports = SortedArray; var Shim = require("./shim"); var GenericCollection = require("./generic-collection"); -var PropertyChanges = require("./listen/property-changes"); -var RangeChanges = require("./listen/range-changes"); +var ObservableObject = require("./observable-object"); +var ObservableRange = require("./observable-range"); function SortedArray(values, equals, compare, getDefault) { if (!(this instanceof SortedArray)) { @@ -29,8 +29,8 @@ function SortedArray(values, equals, compare, getDefault) { SortedArray.SortedArray = SortedArray; Object.addEach(SortedArray.prototype, GenericCollection.prototype); -Object.addEach(SortedArray.prototype, PropertyChanges.prototype); -Object.addEach(SortedArray.prototype, RangeChanges.prototype); +Object.addEach(SortedArray.prototype, ObservableObject.prototype); +Object.addEach(SortedArray.prototype, ObservableRange.prototype); function search(array, value, compare) { var first = 0; diff --git a/sorted-map.js b/sorted-map.js index 33d33c3..2b3a6f1 100644 --- a/sorted-map.js +++ b/sorted-map.js @@ -4,7 +4,7 @@ var Shim = require("./shim"); var SortedSet = require("./sorted-set"); var GenericCollection = require("./generic-collection"); var GenericMap = require("./generic-map"); -var PropertyChanges = require("./listen/property-changes"); +var ObservableObject = require("./observable-object"); module.exports = SortedMap; @@ -36,7 +36,7 @@ SortedMap.SortedMap = SortedMap; Object.addEach(SortedMap.prototype, GenericCollection.prototype); Object.addEach(SortedMap.prototype, GenericMap.prototype); -Object.addEach(SortedMap.prototype, PropertyChanges.prototype); +Object.addEach(SortedMap.prototype, ObservableObject.prototype); SortedMap.prototype.constructClone = function (values) { return new this.constructor( diff --git a/sorted-set.js b/sorted-set.js index 33bc838..e643115 100644 --- a/sorted-set.js +++ b/sorted-set.js @@ -5,8 +5,8 @@ module.exports = SortedSet; var Shim = require("./shim"); var GenericCollection = require("./generic-collection"); var GenericSet = require("./generic-set"); -var PropertyChanges = require("./listen/property-changes"); -var RangeChanges = require("./listen/range-changes"); +var ObservableObject = require("./observable-object"); +var ObservableRange = require("./observable-range"); var TreeLog = require("./tree-log"); function SortedSet(values, equals, compare, getDefault) { @@ -26,8 +26,8 @@ SortedSet.SortedSet = SortedSet; Object.addEach(SortedSet.prototype, GenericCollection.prototype); Object.addEach(SortedSet.prototype, GenericSet.prototype); -Object.addEach(SortedSet.prototype, PropertyChanges.prototype); -Object.addEach(SortedSet.prototype, RangeChanges.prototype); +Object.addEach(SortedSet.prototype, ObservableObject.prototype); +Object.addEach(SortedSet.prototype, ObservableRange.prototype); SortedSet.prototype.constructClone = function (values) { return new this.constructor( @@ -67,7 +67,7 @@ SortedSet.prototype.add = function (value) { throw new Error("SortedSet cannot contain incomparable but inequal values: " + value + " and " + this.root.value); } if (this.dispatchesRangeChanges) { - this.dispatchBeforeRangeChange([value], [], this.root.index); + this.dispatchRangeWillChange([value], [], this.root.index); } if (comparison < 0) { // rotate right @@ -104,7 +104,7 @@ SortedSet.prototype.add = function (value) { } } else { if (this.dispatchesRangeChanges) { - this.dispatchBeforeRangeChange([value], [], 0); + this.dispatchRangeWillChange([value], [], 0); } this.root = node; this.length++; @@ -122,7 +122,7 @@ SortedSet.prototype['delete'] = function (value) { if (this.contentEquals(value, this.root.value)) { var index = this.root.index; if (this.dispatchesRangeChanges) { - this.dispatchBeforeRangeChange([], [value], index); + this.dispatchRangeWillChange([], [value], index); } if (!this.root.left) { this.root = this.root.right; @@ -497,7 +497,7 @@ SortedSet.prototype.clear = function () { var minus; if (this.dispatchesRangeChanges) { minus = this.toArray(); - this.dispatchBeforeRangeChange([], minus, 0); + this.dispatchRangeWillChange([], minus, 0); } this.root = null; this.length = 0; diff --git a/spec/array-spec.js b/spec/array-spec.js index 29f9444..5c9b499 100644 --- a/spec/array-spec.js +++ b/spec/array-spec.js @@ -1,6 +1,6 @@ require("../shim"); -require("../listen/array-changes"); +require("../observable-array"); var GenericCollection = require("../generic-collection"); var describeDequeue = require("./dequeue"); var describeCollection = require("./collection"); diff --git a/spec/set-spec.js b/spec/set-spec.js index 29b9b4f..72f0b80 100644 --- a/spec/set-spec.js +++ b/spec/set-spec.js @@ -31,51 +31,69 @@ describe("Set", function () { it("should dispatch range change on clear", function () { var set = Set([1, 2, 3]); var spy = jasmine.createSpy(); - set.addRangeChangeListener(spy); + set.observeRangeChange(function (plus, minus, index, _set) { + spy(plus, minus, index); + expect(_set).toBe(set); + }); set.clear(); - expect(spy).toHaveBeenCalledWith([], [1, 2, 3], 0, set, undefined); + expect(spy).toHaveBeenCalledWith([], [1, 2, 3], 0); }); it("should dispatch range change on add", function () { var set = Set([1, 3]); var spy = jasmine.createSpy(); - set.addRangeChangeListener(spy); + set.observeRangeChange(function (plus, minus, index, _set) { + spy(plus, minus, index); + expect(_set).toBe(set); + }); set.add(2); expect(set.toArray()).toEqual([1, 3, 2]); - expect(spy).toHaveBeenCalledWith([2], [], 2, set, undefined); + expect(spy).toHaveBeenCalledWith([2], [], 2); }); it("should dispatch range change on delete", function () { var set = Set([1, 2, 3]); var spy = jasmine.createSpy(); - set.addRangeChangeListener(spy); + set.observeRangeChange(function (plus, minus, index, _set) { + spy(plus, minus, index); + expect(_set).toBe(set); + }); set["delete"](2); expect(set.toArray()).toEqual([1, 3]); - expect(spy).toHaveBeenCalledWith([], [2], 1, set, undefined); + expect(spy).toHaveBeenCalledWith([], [2], 1); }); it("should dispatch range change on pop", function () { var set = Set([1, 3, 2]); var spy = jasmine.createSpy(); - set.addRangeChangeListener(spy); + set.observeRangeChange(function (plus, minus, index, _set) { + spy(plus, minus, index); + expect(_set).toBe(set); + }); expect(set.pop()).toEqual(2); expect(set.toArray()).toEqual([1, 3]); - expect(spy).toHaveBeenCalledWith([], [2], 2, set, undefined); + expect(spy).toHaveBeenCalledWith([], [2], 2); }); it("should dispatch range change on shift", function () { var set = Set([1, 3, 2]); var spy = jasmine.createSpy(); - set.addRangeChangeListener(spy); + set.observeRangeChange(function (plus, minus, index, _set) { + spy(plus, minus, index); + expect(_set).toBe(set); + }); expect(set.shift()).toEqual(1); expect(set.toArray()).toEqual([3, 2]); - expect(spy).toHaveBeenCalledWith([], [1], 0, set, undefined); + expect(spy).toHaveBeenCalledWith([], [1], 0); }); + // Need to reevaluate whether sets fully support range changes, or whether + // they support merely set changes (no index). it("should dispatch range change on shift then pop", function () { var set = Set([1, 3]); - set.addRangeChangeListener(function (plus, minus, index) { - spy(plus, minus, index); // ignore all others + set.observeRangeChange(function (plus, minus, index, _set) { + spy(plus, minus, index); + expect(_set).toBe(set); }); var spy = jasmine.createSpy(); diff --git a/spec/sorted-set-spec.js b/spec/sorted-set-spec.js index 1e295d4..1f5af9f 100644 --- a/spec/sorted-set-spec.js +++ b/spec/sorted-set-spec.js @@ -195,7 +195,7 @@ describe("SortedSet", function () { } }); - describe("addRangeChangeListener", function () { + describe("observeRangeChange", function () { // fuzz cases for (var seed = 0; seed < 20; seed++) { (function (seed) { @@ -207,7 +207,7 @@ describe("SortedSet", function () { it("should bind content changes to an array for " + numbers.join(", "), function () { var mirror = []; var set = SortedSet(); - set.addRangeChangeListener(function (plus, minus, index) { + set.observeRangeChange(function (plus, minus, index) { mirror.swap(index, minus.length, plus); }); set.addEach(numbers); From 5b465728bc14e8f2ef606857770a0aa54152d99d Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Tue, 28 Jan 2014 11:53:40 -0800 Subject: [PATCH 14/83] Add queue --- queue.js | 122 +++++++++++++++++++++++++++++++++++++++++++++ spec/queue-spec.js | 57 +++++++++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 queue.js create mode 100644 spec/queue-spec.js diff --git a/queue.js b/queue.js new file mode 100644 index 0000000..23ef183 --- /dev/null +++ b/queue.js @@ -0,0 +1,122 @@ +"use strict"; + +var GenericCollection = require("./generic-collection"); + +// by Petka Antonov +// Queue specifically uses +// http://en.wikipedia.org/wiki/Circular_buffer#Use_a_Fill_Count +// 1. Incrementally maintained length +// 2. Modulus avoided by using only powers of two for the capacity + +module.exports = Queue; +function Queue(values, capacity) { + if (!(this instanceof Queue)) { + return new Queue(values, capacity); + } + this.capacity = this.snap(capacity); + this.length = 0; + this.front = 0; + this.init(); + this.addEach(values); +} + +Queue.prototype.addEach = GenericCollection.prototype.addEach; + +Queue.prototype.add = function (value) { + this.push(value); +}; + +Queue.prototype.push = function (value) { + var length = this.length; + if (this.capacity <= length) { + this.grow(this.snap(this.capacity * this.growFactor)); + } + var index = (this.front + length) & (this.capacity - 1); + this[index] = value; + this.length = length + 1; +}; + +Queue.prototype.shift = function () { + if (this.length !== 0) { + var front = this.front; + var result = this[front]; + + this[front] = void 0; + this.front = (front + 1) & (this.capacity - 1); + this.length--; + return result; + } +}; + +Queue.prototype.grow = function (capacity) { + var oldFront = this.front; + var oldCapacity = this.capacity; + var oldQueue = new Array(oldCapacity); + var length = this.length; + + copy(this, 0, oldQueue, 0, oldCapacity); + this.capacity = capacity; + this.init(); + this.front = 0; + if (oldFront + length <= oldCapacity) { + // Can perform direct linear copy + copy(oldQueue, oldFront, this, 0, length); + } else { + // Cannot perform copy directly, perform as much as possible at the + // end, and then copy the rest to the beginning of the buffer + var lengthBeforeWrapping = + length - ((oldFront + length) & (oldCapacity - 1)); + copy( + oldQueue, + oldFront, + this, + 0, + lengthBeforeWrapping + ); + copy( + oldQueue, + 0, + this, + lengthBeforeWrapping, + length - lengthBeforeWrapping + ); + } +}; + +Queue.prototype.init = function () { + var length = this.capacity; + for (var i = 0; i < length; ++i) { + this[i] = void 0; + } +}; + +Queue.prototype.snap = function (capacity) { + if (typeof capacity !== "number") { + return this.minCapacity; + } + return pow2AtLeast( + Math.min(this.maxCapacity, Math.max(this.minCapacity, capacity)) + ); +}; + +Queue.prototype.maxCapacity = (1 << 30) | 0; +Queue.prototype.minCapacity = 16; +Queue.prototype.growFactor = 8; + +function copy(source, sourceIndex, target, targetIndex, length) { + for (var index = 0; index < length; ++index) { + target[index + targetIndex] = source[index + sourceIndex]; + } +} + +function pow2AtLeast(n) { + n = n >>> 0; + n = n - 1; + n = n | (n >> 1); + n = n | (n >> 2); + n = n | (n >> 4); + n = n | (n >> 8); + n = n | (n >> 16); + return n + 1; +} + diff --git a/spec/queue-spec.js b/spec/queue-spec.js new file mode 100644 index 0000000..287fbba --- /dev/null +++ b/spec/queue-spec.js @@ -0,0 +1,57 @@ + +var Queue = require("../queue"); + +describe("Queue", function () { + + it("just the facts", function () { + var queue = new Queue(); + expect(queue.length).toBe(0); + expect(queue.capacity).toBe(16); + + queue.push(10); + expect(queue.length).toBe(1); + expect(queue.shift()).toBe(10); + expect(queue.length).toBe(0); + + queue.push(20); + expect(queue.length).toBe(1); + queue.push(30); + expect(queue.length).toBe(2); + expect(queue.shift()).toBe(20); + expect(queue.length).toBe(1); + expect(queue.shift()).toBe(30); + expect(queue.length).toBe(0); + + expect(queue.capacity).toBe(16); + + }); + + it("grows", function () { + var queue = Queue(); + + for (var i = 0; i < 16; i++) { + expect(queue.length).toBe(i); + queue.push(i); + expect(queue.capacity).toBe(16); + } + queue.push(i); + expect(queue.capacity).toBe(128); + }); + + it("initializes", function () { + var queue = new Queue([1, 2, 3]); + expect(queue.length).toBe(3); + expect(queue.shift()).toBe(1); + expect(queue.shift()).toBe(2); + expect(queue.shift()).toBe(3); + }); + + it("does not get in a funk", function () { + var queue = Queue(); + expect(queue.shift()).toBe(undefined); + queue.push(4); + expect(queue.shift()).toBe(4); + }); + +}); + From 6fd4b9d6b8d90b1b23da5f94e62ac0595af98192 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Tue, 28 Jan 2014 12:15:41 -0800 Subject: [PATCH 15/83] Implement range change dispatch on Queue --- queue.js | 21 +++++++++++++++++++++ spec/queue-spec.js | 19 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/queue.js b/queue.js index 23ef183..22b346d 100644 --- a/queue.js +++ b/queue.js @@ -1,6 +1,8 @@ "use strict"; +require("./shim-object"); var GenericCollection = require("./generic-collection"); +var RangeChanges = require("./listen/range-changes"); // by Petka Antonov // Queue specifically uses @@ -8,6 +10,8 @@ var GenericCollection = require("./generic-collection"); // 1. Incrementally maintained length // 2. Modulus avoided by using only powers of two for the capacity +// TODO variadic push + module.exports = Queue; function Queue(values, capacity) { if (!(this instanceof Queue)) { @@ -20,6 +24,8 @@ function Queue(values, capacity) { this.addEach(values); } +Object.addEach(Queue.prototype, RangeChanges.prototype); + Queue.prototype.addEach = GenericCollection.prototype.addEach; Queue.prototype.add = function (value) { @@ -27,6 +33,9 @@ Queue.prototype.add = function (value) { }; Queue.prototype.push = function (value) { + if (this.dispatchesRangeChanges) { + this.dispatchBeforeRangeChange([value], [], this.length); + } var length = this.length; if (this.capacity <= length) { this.grow(this.snap(this.capacity * this.growFactor)); @@ -34,6 +43,9 @@ Queue.prototype.push = function (value) { var index = (this.front + length) & (this.capacity - 1); this[index] = value; this.length = length + 1; + if (this.dispatchesRangeChanges) { + this.dispatchRangeChange([value], [], this.length - 1); + } }; Queue.prototype.shift = function () { @@ -41,9 +53,18 @@ Queue.prototype.shift = function () { var front = this.front; var result = this[front]; + if (this.dispatchesRangeChanges) { + this.dispatchBeforeRangeChange([], [result], 0); + } + this[front] = void 0; this.front = (front + 1) & (this.capacity - 1); this.length--; + + if (this.dispatchesRangeChanges) { + this.dispatchRangeChange([], [result], 0); + } + return result; } }; diff --git a/spec/queue-spec.js b/spec/queue-spec.js index 287fbba..225ad31 100644 --- a/spec/queue-spec.js +++ b/spec/queue-spec.js @@ -53,5 +53,24 @@ describe("Queue", function () { expect(queue.shift()).toBe(4); }); + it("dispatches range changes", function () { + var spy = jasmine.createSpy(); + var handler = function (plus, minus, value) { + spy(plus, minus, value); // ignore last arg + }; + var queue = Queue(); + queue.addRangeChangeListener(handler); + queue.push(1); + queue.push(2); + queue.shift(); + queue.removeRangeChangeListener(handler); + queue.shift(); + expect(spy.argsForCall).toEqual([ + [[1], [], 0], + [[2], [], 1], + [[], [1], 0] + ]); + }); + }); From 42f09806686a19d87de035445d6d3beb55860acb Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Thu, 30 Jan 2014 23:33:55 -0800 Subject: [PATCH 16/83] Failing spec for equals of undefined/null --- spec/shim-object-spec.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/shim-object-spec.js b/spec/shim-object-spec.js index c18366f..3b74854 100644 --- a/spec/shim-object-spec.js +++ b/spec/shim-object-spec.js @@ -342,6 +342,12 @@ describe("Object", function () { }, { 'NaN': NaN + }, + { + 'undefined': undefined + }, + { + 'null': null } ]; From b383200cbdb7d3476395da5252c83591d115c000 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Thu, 30 Jan 2014 23:34:10 -0800 Subject: [PATCH 17/83] Fix for Object.equals for null and undefined --- shim-object.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shim-object.js b/shim-object.js index 6126751..9245e9b 100644 --- a/shim-object.js +++ b/shim-object.js @@ -345,7 +345,7 @@ Object.equals = function (a, b, equals) { if (a === b) // 0 === -0, but they are not equal return a !== 0 || 1 / a === 1 / b; - if (a === null || b === null) + if (a == null || b == null) return a === b; if (typeof a.equals === "function") return a.equals(b, equals); From 3348378adf17ebf10850a91b847df8ef289833f3 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Sun, 2 Feb 2014 01:35:04 -0800 Subject: [PATCH 18/83] Upgrade Queue to Deque --- deque.js | 437 ++++++++++++++++++++++++++++++++++ generic-set.js | 2 + list.js | 8 + queue.js | 143 ----------- sorted-array-set.js | 2 + sorted-array.js | 2 + sorted-set.js | 2 + spec/array-spec.js | 4 +- spec/deque-fuzz.js | 88 +++++++ spec/deque-spec.js | 119 +++++++++ spec/{dequeue.js => deque.js} | 172 ++++++++++--- spec/fuzz.js | 13 +- spec/list-spec.js | 11 +- spec/order.js | 127 ++++++++-- spec/prng.js | 9 + spec/queue-spec.js | 76 ------ spec/shim-functions-spec.js | 1 + spec/sorted-array-set-spec.js | 6 +- spec/sorted-array-spec.js | 6 +- spec/sorted-set-spec.js | 8 +- 20 files changed, 941 insertions(+), 295 deletions(-) create mode 100644 deque.js delete mode 100644 queue.js create mode 100644 spec/deque-fuzz.js create mode 100644 spec/deque-spec.js rename spec/{dequeue.js => deque.js} (62%) create mode 100644 spec/prng.js delete mode 100644 spec/queue-spec.js diff --git a/deque.js b/deque.js new file mode 100644 index 0000000..9e6e93f --- /dev/null +++ b/deque.js @@ -0,0 +1,437 @@ +"use strict"; + +require("./shim-object"); +var GenericCollection = require("./generic-collection"); +var GenericOrder = require("./generic-order"); +var GenericOrder = require("./generic-order"); +var RangeChanges = require("./listen/range-changes"); + +// by Petka Antonov +// https://github.com/petkaantonov/deque/blob/master/js/deque.js +// Deque specifically uses +// http://en.wikipedia.org/wiki/Circular_buffer#Use_a_Fill_Count +// 1. Incrementally maintained length +// 2. Modulus avoided by using only powers of two for the capacity + +module.exports = Deque; +function Deque(values, capacity) { + if (!(this instanceof Deque)) { + return new Deque(values, capacity); + } + this.capacity = this.snap(capacity); + this.init(); + this.length = 0; + this.front = 0; + this.addEach(values); +} + +Object.addEach(Deque.prototype, GenericCollection.prototype); +Object.addEach(Deque.prototype, GenericOrder.prototype); +Object.addEach(Deque.prototype, RangeChanges.prototype); + +Deque.prototype.maxCapacity = (1 << 30) | 0; +Deque.prototype.minCapacity = 16; + +Deque.prototype.constructClone = function (values) { + return new this.constructor(values, this.capacity) +}; + +Deque.prototype.add = function (value) { + this.push(value); +}; + +Deque.prototype.push = function (value /* or ...values */) { + var argsLength = arguments.length; + var length = this.length; + + if (this.dispatchesRangeChanges) { + var plus = Array.prototype.slice.call(arguments); + var minus = []; + this.dispatchBeforeRangeChange(plus, minus, length); + } + + if (argsLength > 1) { + var capacity = this.capacity; + if (length + argsLength > capacity) { + for (var argsIndex = 0; argsIndex < argsLength; ++argsIndex) { + this.ensureCapacity(length + 1); + var j = (this.front + length) & (this.capacity - 1); + this[j] = arguments[argsIndex]; + length++; + this.length = length; + } + } + else { + var j = this.front; + for (var argsIndex = 0; argsIndex < argsLength; ++argsIndex) { + this[(j + length) & (capacity - 1)] = arguments[argsIndex]; + j++; + } + this.length = length + argsLength; + } + + } else if (argsLength === 1) { + this.ensureCapacity(length + 1); + var index = (this.front + length) & (this.capacity - 1); + this[index] = value; + this.length = length + 1; + } + + if (this.dispatchesRangeChanges) { + this.dispatchRangeChange(plus, minus, length); + } + + return this.length; +}; + +Deque.prototype.pop = function () { + var length = this.length; + if (length === 0) { + return; + } + var index = (this.front + length - 1) & (this.capacity - 1); + var result = this[index]; + + if (this.dispatchesRangeChanges) { + this.dispatchBeforeRangeChange([], [result], length - 1); + } + + this[index] = void 0; + this.length = length - 1; + + if (this.dispatchesRangeChanges) { + this.dispatchRangeChange([], [result], length - 1); + } + + return result; +}; + +Deque.prototype.shift = function () { + if (this.length !== 0) { + var front = this.front; + var result = this[front]; + + if (this.dispatchesRangeChanges) { + this.dispatchBeforeRangeChange([], [result], 0); + } + + this[front] = void 0; + this.front = (front + 1) & (this.capacity - 1); + this.length--; + + if (this.dispatchesRangeChanges) { + this.dispatchRangeChange([], [result], 0); + } + + return result; + } +}; + +Deque.prototype.unshift = function (value /*, ...values */) { + var length = this.length; + var argsLength = arguments.length; + + if (this.dispatchesRangeChanges) { + var plus = Array.prototype.slice.call(arguments); + var minus = []; + this.dispatchBeforeRangeChange(plus, minus, 0); + } + + if (argsLength > 1) { + var capacity = this.capacity; + if (length + argsLength > capacity) { + for (var argIndex = argsLength - 1; argIndex >= 0; argIndex--) { + this.ensureCapacity(length + 1); + var capacity = this.capacity; + var index = ( + ( + ( + ( this.front - 1 ) & + ( capacity - 1) + ) ^ capacity + ) - capacity + ); + this[index] = arguments[argIndex]; + length++; + this.front = index; + this.length = length; + } + } else { + var front = this.front; + for (var argIndex = argsLength - 1; argIndex >= 0; argIndex--) { + var index = ( + ( + ( + (front - 1) & + (capacity - 1) + ) ^ capacity + ) - capacity + ); + this[index] = arguments[argIndex]; + front = index; + } + this.front = front; + this.length = length + argsLength; + } + } else if (argsLength === 1) { + this.ensureCapacity(length + 1); + var capacity = this.capacity; + var index = ( + ( + ( + (this.front - 1) & + (capacity - 1) + ) ^ capacity + ) - capacity + ); + this[index] = value; + this.length = length + 1; + this.front = index; + } + + if (this.dispatchesRangeChanges) { + this.dispatchRangeChange(plus, minus, 0); + } + + return this.length; +}; + +Deque.prototype.clear = function () { + this.length = 0; + this.front = 0; + this.init(); +}; + +Deque.prototype.ensureCapacity = function (capacity) { + if (this.capacity < capacity) { + this.grow(this.snap(this.capacity * 1.5 + 16)); + } +}; + +Deque.prototype.grow = function (capacity) { + var oldFront = this.front; + var oldCapacity = this.capacity; + var oldContent = new Array(oldCapacity); + var length = this.length; + + copy(this, 0, oldContent, 0, oldCapacity); + this.capacity = capacity; + this.init(); + this.front = 0; + if (oldFront + length <= oldCapacity) { + // Can perform direct linear copy. + copy(oldContent, oldFront, this, 0, length); + } else { + // Cannot perform copy directly, perform as much as possible at the + // end, and then copy the rest to the beginning of the buffer. + var lengthBeforeWrapping = length - ((oldFront + length) & (oldCapacity - 1)); + copy(oldContent, oldFront, this, 0, lengthBeforeWrapping); + copy(oldContent, 0, this, lengthBeforeWrapping, length - lengthBeforeWrapping); + } +}; + +Deque.prototype.init = function () { + for (var index = 0; index < this.capacity; ++index) { + this[index] = "nil"; // TODO void 0 + } +}; + +Deque.prototype.snap = function (capacity) { + if (typeof capacity !== "number") { + return this.minCapacity; + } + return pow2AtLeast( + Math.min(this.maxCapacity, Math.max(this.minCapacity, capacity)) + ); +}; + +Deque.prototype.one = function () { + if (this.length > 0) { + return this[this.front]; + } +}; + +Deque.prototype.peek = function () { + if (this.length === 0) { + return; + } + return this[this.front]; +}; + +Deque.prototype.poke = function (value) { + if (this.length === 0) { + return; + } + this[this.front] = value; +}; + +Deque.prototype.peekBack = function () { + var length = this.length; + if (length === 0) { + return; + } + var index = (this.front + length - 1) & (this.capacity - 1); + return this[index]; +}; + +Deque.prototype.pokeBack = function (value) { + var length = this.length; + if (length === 0) { + return; + } + var index = (this.front + length - 1) & (this.capacity - 1); + this[index] = value; +}; + +Deque.prototype.get = function (index) { + // Domain only includes integers + if (index !== (index | 0)) { + return; + } + // Support negative indicies + if (index < 0) { + index = index + this.length; + } + // Out of bounds + if (index < 0 || index >= this.length) { + return; + } + return this[(this.front + index) & (this.capacity - 1)]; +}; + +Deque.prototype.indexOf = function (value, index) { + // Default start index at beginning + if (index == null) { + index = 0; + } + // Support negative indicies + if (index < 0) { + index = index + this.length; + } + // Left to right walk + var mask = this.capacity - 1; + for (; index < this.length; index++) { + var offset = (this.front + index) & mask; + if (this[offset] === value) { + return index; + } + } + return -1; +}; + +Deque.prototype.lastIndexOf = function (value, index) { + // Default start position at the end + if (index == null) { + index = this.length - 1; + } + // Support negative indicies + if (index < 0) { + index = index + this.length; + } + // Right to left walk + var mask = this.capacity - 1; + for (; index >= 0; index--) { + var offset = (this.front + index) & mask; + if (this[offset] === value) { + return index; + } + } + return -1; +} + +// TODO rename findValue +Deque.prototype.find = function (value, equals, index) { + equals = equals || Object.equals; + // Default start index at beginning + if (index == null) { + index = 0; + } + // Support negative indicies + if (index < 0) { + index = index + this.length; + } + // Left to right walk + var mask = this.capacity - 1; + for (; index < this.length; index++) { + var offset = (this.front + index) & mask; + if (equals(value, this[offset])) { + return index; + } + } + return -1; +}; + +// TODO rename findLastValue +Deque.prototype.findLast = function (value, equals, index) { + equals = equals || Object.equals; + // Default start position at the end + if (index == null) { + index = this.length - 1; + } + // Support negative indicies + if (index < 0) { + index = index + this.length; + } + // Right to left walk + var mask = this.capacity - 1; + for (; index >= 0; index--) { + var offset = (this.front + index) & mask; + if (equals(value, this[offset])) { + return index; + } + } + return -1; +}; + +Deque.prototype.has = function (value, equals) { + equals = equals || Object.equals; + // Left to right walk + var mask = this.capacity - 1; + for (var index = 0; index < this.length; index++) { + var offset = (this.front + index) & mask; + if (this[offset] === value) { + return true; + } + } + return false; +}; + +Deque.prototype.reduce = function (callback, basis /*, thisp*/) { + // TODO account for missing basis argument + var thisp = arguments[2]; + var mask = this.capacity - 1; + for (var index = 0; index < this.length; index++) { + var offset = (this.front + index) & mask; + basis = callback.call(thisp, basis, this[offset], index, this); + } + return basis; +}; + +Deque.prototype.reduceRight = function (callback, basis /*, thisp*/) { + // TODO account for missing basis argument + var thisp = arguments[2]; + var mask = this.capacity - 1; + for (var index = this.length - 1; index >= 0; index--) { + var offset = (this.front + index) & mask; + basis = callback.call(thisp, basis, this[offset], index, this); + } + return basis; +}; + +function copy(source, sourceIndex, target, targetIndex, length) { + for (var index = 0; index < length; ++index) { + target[index + targetIndex] = source[index + sourceIndex]; + } +} + +function pow2AtLeast(n) { + n = n >>> 0; + n = n - 1; + n = n | (n >> 1); + n = n | (n >> 2); + n = n | (n >> 4); + n = n | (n >> 8); + n = n | (n >> 16); + return n + 1; +} + diff --git a/generic-set.js b/generic-set.js index 728a78e..f2df9fa 100644 --- a/generic-set.js +++ b/generic-set.js @@ -4,6 +4,8 @@ function GenericSet() { throw new Error("Can't construct. GenericSet is a mixin."); } +GenericSet.prototype.isSet = true; + GenericSet.prototype.union = function (that) { var union = this.constructClone(this); union.addEach(that); diff --git a/list.js b/list.js index e65961e..dfb83d4 100644 --- a/list.js +++ b/list.js @@ -215,6 +215,14 @@ List.prototype.one = function () { return this.peek(); }; +// TODO +// List.prototype.indexOf = function (value) { +// }; + +// TODO +// List.prototype.lastIndexOf = function (value) { +// }; + // an internal utility for coercing index offsets to nodes List.prototype.scan = function (at, fallback) { var head = this.head; diff --git a/queue.js b/queue.js deleted file mode 100644 index 22b346d..0000000 --- a/queue.js +++ /dev/null @@ -1,143 +0,0 @@ -"use strict"; - -require("./shim-object"); -var GenericCollection = require("./generic-collection"); -var RangeChanges = require("./listen/range-changes"); - -// by Petka Antonov -// Queue specifically uses -// http://en.wikipedia.org/wiki/Circular_buffer#Use_a_Fill_Count -// 1. Incrementally maintained length -// 2. Modulus avoided by using only powers of two for the capacity - -// TODO variadic push - -module.exports = Queue; -function Queue(values, capacity) { - if (!(this instanceof Queue)) { - return new Queue(values, capacity); - } - this.capacity = this.snap(capacity); - this.length = 0; - this.front = 0; - this.init(); - this.addEach(values); -} - -Object.addEach(Queue.prototype, RangeChanges.prototype); - -Queue.prototype.addEach = GenericCollection.prototype.addEach; - -Queue.prototype.add = function (value) { - this.push(value); -}; - -Queue.prototype.push = function (value) { - if (this.dispatchesRangeChanges) { - this.dispatchBeforeRangeChange([value], [], this.length); - } - var length = this.length; - if (this.capacity <= length) { - this.grow(this.snap(this.capacity * this.growFactor)); - } - var index = (this.front + length) & (this.capacity - 1); - this[index] = value; - this.length = length + 1; - if (this.dispatchesRangeChanges) { - this.dispatchRangeChange([value], [], this.length - 1); - } -}; - -Queue.prototype.shift = function () { - if (this.length !== 0) { - var front = this.front; - var result = this[front]; - - if (this.dispatchesRangeChanges) { - this.dispatchBeforeRangeChange([], [result], 0); - } - - this[front] = void 0; - this.front = (front + 1) & (this.capacity - 1); - this.length--; - - if (this.dispatchesRangeChanges) { - this.dispatchRangeChange([], [result], 0); - } - - return result; - } -}; - -Queue.prototype.grow = function (capacity) { - var oldFront = this.front; - var oldCapacity = this.capacity; - var oldQueue = new Array(oldCapacity); - var length = this.length; - - copy(this, 0, oldQueue, 0, oldCapacity); - this.capacity = capacity; - this.init(); - this.front = 0; - if (oldFront + length <= oldCapacity) { - // Can perform direct linear copy - copy(oldQueue, oldFront, this, 0, length); - } else { - // Cannot perform copy directly, perform as much as possible at the - // end, and then copy the rest to the beginning of the buffer - var lengthBeforeWrapping = - length - ((oldFront + length) & (oldCapacity - 1)); - copy( - oldQueue, - oldFront, - this, - 0, - lengthBeforeWrapping - ); - copy( - oldQueue, - 0, - this, - lengthBeforeWrapping, - length - lengthBeforeWrapping - ); - } -}; - -Queue.prototype.init = function () { - var length = this.capacity; - for (var i = 0; i < length; ++i) { - this[i] = void 0; - } -}; - -Queue.prototype.snap = function (capacity) { - if (typeof capacity !== "number") { - return this.minCapacity; - } - return pow2AtLeast( - Math.min(this.maxCapacity, Math.max(this.minCapacity, capacity)) - ); -}; - -Queue.prototype.maxCapacity = (1 << 30) | 0; -Queue.prototype.minCapacity = 16; -Queue.prototype.growFactor = 8; - -function copy(source, sourceIndex, target, targetIndex, length) { - for (var index = 0; index < length; ++index) { - target[index + targetIndex] = source[index + sourceIndex]; - } -} - -function pow2AtLeast(n) { - n = n >>> 0; - n = n - 1; - n = n | (n >> 1); - n = n | (n >> 2); - n = n | (n >> 4); - n = n | (n >> 8); - n = n | (n >> 16); - return n + 1; -} - diff --git a/sorted-array-set.js b/sorted-array-set.js index 4a0d7d2..55bf6d0 100644 --- a/sorted-array-set.js +++ b/sorted-array-set.js @@ -24,6 +24,8 @@ SortedArraySet.prototype.constructor = SortedArraySet; Object.addEach(SortedArraySet.prototype, GenericSet.prototype); Object.addEach(SortedArraySet.prototype, PropertyChanges.prototype); +SortedArraySet.prototype.isSorted = true; + SortedArraySet.prototype.add = function (value) { if (!this.has(value)) { SortedArray.prototype.add.call(this, value); diff --git a/sorted-array.js b/sorted-array.js index 1931f9a..6d2e9ad 100644 --- a/sorted-array.js +++ b/sorted-array.js @@ -32,6 +32,8 @@ Object.addEach(SortedArray.prototype, GenericCollection.prototype); Object.addEach(SortedArray.prototype, PropertyChanges.prototype); Object.addEach(SortedArray.prototype, RangeChanges.prototype); +SortedArray.prototype.isSorted = true; + function search(array, value, compare) { var first = 0; var last = array.length - 1; diff --git a/sorted-set.js b/sorted-set.js index 33bc838..06920c6 100644 --- a/sorted-set.js +++ b/sorted-set.js @@ -29,6 +29,8 @@ Object.addEach(SortedSet.prototype, GenericSet.prototype); Object.addEach(SortedSet.prototype, PropertyChanges.prototype); Object.addEach(SortedSet.prototype, RangeChanges.prototype); +SortedSet.prototype.isSorted = true; + SortedSet.prototype.constructClone = function (values) { return new this.constructor( values, diff --git a/spec/array-spec.js b/spec/array-spec.js index a44095d..4a9e10a 100644 --- a/spec/array-spec.js +++ b/spec/array-spec.js @@ -2,13 +2,13 @@ require("../shim"); require("../listen/array-changes"); var GenericCollection = require("../generic-collection"); -var describeDequeue = require("./dequeue"); +var describeDeque = require("./deque"); var describeCollection = require("./collection"); var describeOrder = require("./order"); var describeMapChanges = require("./listen/map-changes"); describe("Array", function () { - describeDequeue(Array.from); + describeDeque(Array.from); describeCollection(Array.from, [1, 2, 3, 4]); describeCollection(Array.from, [{id: 0}, {id: 1}, {id: 2}, {id: 3}]); describeOrder(Array.from); diff --git a/spec/deque-fuzz.js b/spec/deque-fuzz.js new file mode 100644 index 0000000..53ed541 --- /dev/null +++ b/spec/deque-fuzz.js @@ -0,0 +1,88 @@ + +var Deque = require("../deque"); +require("../shim-array"); +var prng = require("./prng"); + +exports.fuzzDeque = fuzzDeque; +function fuzzDeque(Deque) { + for (var biasWeight = .3; biasWeight < .8; biasWeight += .2) { + for (var maxAddLength = 1; maxAddLength < 5; maxAddLength += 3) { + for (var seed = 0; seed < 10; seed++) { + var plan = makePlan(100, seed, biasWeight, maxAddLength); + execute(Deque, plan.ops); + } + } + } +} + +exports.makePlan = makePlan; +function makePlan(length, seed, biasWeight, maxAddLength) { + maxAddLength = maxAddLength || 1; + var random = prng(seed); + var ops = []; + while (ops.length < length) { + var bias = ops.length / length; + var choice1 = random() * (1 - biasWeight) + bias * biasWeight; + var choice2 = random(); + if (choice1 < 1 / (maxAddLength + 1)) { + if (choice2 < .5) { + ops.push(["push", makeRandomArray(1 + ~~(random() * maxAddLength - .5))]); + } else { + ops.push(["unshift", makeRandomArray(1 + ~~(random() * maxAddLength - .5))]); + } + } else { + if (choice2 < .5) { + ops.push(["shift", []]); + } else { + ops.push(["pop", []]); + } + } + } + return { + seed: seed, + length: length, + biasWeight: biasWeight, + maxAddLength: maxAddLength, + ops: ops + } +} + +function makeRandomArray(length, random) { + var array = []; + for (var index = 0; index < length; index++) { + array.push(~~(Math.random() * 100)); + } + return array; +} + +exports.execute = execute; +function execute(Collection, ops) { + var oracle = []; + var actual = new Collection(); + ops.forEach(function (op) { + executeOp(oracle, op); + executeOp(actual, op); + if (typeof expect === "function") { + expect(actual.toArray()).toEqual(oracle); + } else if (!actual.toArray().equals(oracle)) { + console.log(actual.front, actual.toArray(), oracle); + throw new Error("Did not match after " + stringifyOp(op)); + } + }); +} + +exports.executeOp = executeOp; +function executeOp(collection, op) { + collection[op[0]].apply(collection, op[1]); +} + +exports.stringify = stringify; +function stringify(ops) { + return ops.map(stringifyOp).join(" "); +} + +exports.stringifyOp = stringifyOp; +function stringifyOp(op) { + return op[0] + "(" + op[1].join(", ") + ")"; +} + diff --git a/spec/deque-spec.js b/spec/deque-spec.js new file mode 100644 index 0000000..f410ba0 --- /dev/null +++ b/spec/deque-spec.js @@ -0,0 +1,119 @@ + +var Deque = require("../deque"); +var describeDeque = require("./deque"); +var describeOrder = require("./order"); + +describe("Deque", function () { + + it("just the facts", function () { + var deque = new Deque(); + expect(deque.length).toBe(0); + expect(deque.capacity).toBe(16); + + deque.push(10); + expect(deque.length).toBe(1); + expect(deque.shift()).toBe(10); + expect(deque.length).toBe(0); + + deque.push(20); + expect(deque.length).toBe(1); + deque.push(30); + expect(deque.length).toBe(2); + expect(deque.shift()).toBe(20); + expect(deque.length).toBe(1); + expect(deque.shift()).toBe(30); + expect(deque.length).toBe(0); + + expect(deque.capacity).toBe(16); + + }); + + it("grows", function () { + var deque = Deque(); + + for (var i = 0; i < 16; i++) { + expect(deque.length).toBe(i); + deque.push(i); + expect(deque.capacity).toBe(16); + } + deque.push(i); + expect(deque.capacity).toBe(64); + }); + + it("initializes", function () { + var deque = new Deque([1, 2, 3]); + expect(deque.length).toBe(3); + expect(deque.shift()).toBe(1); + expect(deque.shift()).toBe(2); + expect(deque.shift()).toBe(3); + }); + + it("does not get in a funk", function () { + var deque = Deque(); + expect(deque.shift()).toBe(undefined); + deque.push(4); + expect(deque.shift()).toBe(4); + }); + + it("dispatches range changes", function () { + var spy = jasmine.createSpy(); + var handler = function (plus, minus, value) { + spy(plus, minus, value); // ignore last arg + }; + var deque = Deque(); + deque.addRangeChangeListener(handler); + deque.push(1); + deque.push(2, 3); + deque.pop(); + deque.shift(); + deque.unshift(4, 5); + deque.removeRangeChangeListener(handler); + deque.shift(); + expect(spy.argsForCall).toEqual([ + [[1], [], 0], + [[2, 3], [], 1], + [[], [3], 2], + [[], [1], 0], + [[4, 5], [], 0] + ]); + }); + + // from https://github.com/petkaantonov/deque + + describe('get', function () { + it("should return undefined on nonsensical argument", function() { + var a = new Deque([1,2,3,4]); + expect(a.get(-5)).toBe(void 0); + expect(a.get(-100)).toBe(void 0); + expect(a.get(void 0)).toBe(void 0); + expect(a.get("1")).toBe(void 0); + expect(a.get(NaN)).toBe(void 0); + expect(a.get(Infinity)).toBe(void 0); + expect(a.get(-Infinity)).toBe(void 0); + expect(a.get(1.5)).toBe(void 0); + expect(a.get(4)).toBe(void 0); + }); + + + it("should support positive indexing", function() { + var a = new Deque([1,2,3,4]); + expect(a.get(0)).toBe(1); + expect(a.get(1)).toBe(2); + expect(a.get(2)).toBe(3); + expect(a.get(3)).toBe(4); + }); + + it("should support negative indexing", function() { + var a = new Deque([1,2,3,4]); + expect(a.get(-1)).toBe(4); + expect(a.get(-2)).toBe(3); + expect(a.get(-3)).toBe(2); + expect(a.get(-4)).toBe(1); + }); + }); + + describeDeque(Deque); + describeOrder(Deque); + +}); + diff --git a/spec/dequeue.js b/spec/deque.js similarity index 62% rename from spec/dequeue.js rename to spec/deque.js index 483f9b1..dcfed0e 100644 --- a/spec/dequeue.js +++ b/spec/deque.js @@ -3,12 +3,14 @@ // put the values at the ends, but for sake of reusing these tests for // SortedSet, all of these tests maintain the sorted order of the collection. -module.exports = describeDequeue; -function describeDequeue(Collection) { +var fuzzDeque = require("./deque-fuzz").fuzzDeque; + +module.exports = describeDeque; +function describeDeque(Deque) { describe("add(value)", function () { it("should be an alias for push", function () { - var collection = Collection([1, 2, 3]); + var collection = Deque([1, 2, 3]); collection.add(4); expect(collection.toArray()).toEqual([1, 2, 3, 4]); }); @@ -16,7 +18,7 @@ function describeDequeue(Collection) { describe("push(value)", function () { it("should add one value to the end", function () { - var collection = Collection([1, 2, 3]); + var collection = Deque([1, 2, 3]); collection.push(4); expect(collection.toArray()).toEqual([1, 2, 3, 4]); }); @@ -24,13 +26,13 @@ function describeDequeue(Collection) { describe("push(...values)", function () { it("should add many values to the end", function () { - var collection = Collection([1, 2, 3]); + var collection = Deque([1, 2, 3]); collection.push(4, 5, 6); expect(collection.toArray()).toEqual([1, 2, 3, 4, 5, 6]); }); it("should add many values to the end variadically", function () { - var collection = Collection([1, 2, 3]); + var collection = Deque([1, 2, 3]); collection.push.apply(collection, [4, 5, 6]); expect(collection.toArray()).toEqual([1, 2, 3, 4, 5, 6]); }); @@ -38,7 +40,7 @@ function describeDequeue(Collection) { describe("unshift(value)", function () { it("should add a value to the beginning", function () { - var collection = Collection([1, 2, 3]); + var collection = Deque([1, 2, 3]); collection.unshift(0); expect(collection.toArray()).toEqual([0, 1, 2, 3]); }); @@ -46,29 +48,29 @@ function describeDequeue(Collection) { describe("unshift(...values)", function () { it("should add many values to the beginning", function () { - var collection = Collection([1, 2, 3]); + var collection = Deque([1, 2, 3]); collection.unshift(-2, -1, 0); expect(collection.toArray()).toEqual([-2, -1, 0, 1, 2, 3]); }); it("should add many values to the beginning", function () { - var collection = Collection([1, 2, 3]); + var collection = Deque([1, 2, 3]); collection.unshift.apply(collection, [-2, -1, 0]); expect(collection.toArray()).toEqual([-2, -1, 0, 1, 2, 3]); }); }); - describe("pop()", function () { + describe("pop", function () { it("should remove one value from the end and return it", function () { - var collection = Collection([1, 2, 3]); + var collection = Deque([1, 2, 3]); expect(collection.pop()).toEqual(3); expect(collection.toArray()).toEqual([1, 2]); }); }); - describe("shift()", function () { + describe("shift", function () { it("should remove one value from the beginning and return it", function () { - var collection = Collection([1, 2, 3]); + var collection = Deque([1, 2, 3]); expect(collection.shift()).toEqual(1); expect(collection.toArray()).toEqual([2, 3]); }); @@ -76,16 +78,19 @@ function describeDequeue(Collection) { describe("concat", function () { it("should concatenate variadic mixed-type collections", function () { - var collection = Collection([1, 2, 3]).concat( + var collection = Deque([1, 2, 3]).concat( [4, 5, 6], - Collection([7, 8, 9]) + Deque([7, 8, 9]) ); expect(collection.toArray()).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]); }); }); - describe("slice()", function () { - var collection = Collection([1, 2, 3, 4]); + describe("slice", function () { + if (!Deque.prototype.slice) + return; + + var collection = Deque([1, 2, 3, 4]); it("should slice all values with no arguments", function () { expect(collection.slice()).toEqual([1, 2, 3, 4]); @@ -111,6 +116,7 @@ function describeDequeue(Collection) { expect(collection.slice(-2, -1)).toEqual([3]); }); + // TODO /* it("should slice from a negative index to zero", function () { expect(collection.slice(-2, 0)).toEqual([]); // Array @@ -120,120 +126,212 @@ function describeDequeue(Collection) { }); - describe("splice()", function () { + describe("splice", function () { + if (!Deque.prototype.splice) + return; it("should do nothing with no arguments", function () { - var collection = Collection([1, 2, 3, 4]); + var collection = Deque([1, 2, 3, 4]); expect(collection.splice()).toEqual([]); expect(collection.toArray()).toEqual([1, 2, 3, 4]); }); it("should splice to end with only an offset argument", function () { - var collection = Collection([1, 2, 3, 4]); + var collection = Deque([1, 2, 3, 4]); expect(collection.splice(2)).toEqual([3, 4]); expect(collection.toArray()).toEqual([1, 2]); }); it("should splice nothing with no length", function () { - var collection = Collection([1, 2, 3, 4]); + var collection = Deque([1, 2, 3, 4]); expect(collection.splice(2, 0)).toEqual([]); expect(collection.toArray()).toEqual([1, 2, 3, 4]); }); it("should splice all values", function () { - var collection = Collection([1, 2, 3, 4]); + var collection = Deque([1, 2, 3, 4]); expect(collection.splice(0, collection.length)).toEqual([1, 2, 3, 4]); expect(collection.toArray()).toEqual([]); }); it("should splice from negative offset", function () { - var collection = Collection([1, 2, 3, 4]); + var collection = Deque([1, 2, 3, 4]); expect(collection.splice(-2)).toEqual([3, 4]); expect(collection.toArray()).toEqual([1, 2]); }); it("should inject values at a numeric offset", function () { - var collection = Collection([1, 2, 5, 6]); + var collection = Deque([1, 2, 5, 6]); expect(collection.splice(2, 0, 3, 4)).toEqual([]); expect(collection.toArray()).toEqual([1, 2, 3, 4, 5, 6]); }); it("should replace values at a numeric offset", function () { - var collection = Collection([1, 2, 3, 6]); + var collection = Deque([1, 2, 3, 6]); expect(collection.splice(1, 2, 4, 5)).toEqual([2, 3]); expect(collection.toArray()).toEqual([1, 4, 5, 6]); }); it("should inject values with implied position and length", function () { - var collection = Collection([1, 2, 3, 4]); + var collection = Deque([1, 2, 3, 4]); expect(collection.splice(null, null, -1, 0)).toEqual([]); expect(collection.toArray()).toEqual([-1, 0, 1, 2, 3, 4]); }); it("should append values", function () { - var collection = Collection([1, 2, 3, 4]); + var collection = Deque([1, 2, 3, 4]); expect(collection.splice(4, 0, 5, 6)).toEqual([]); expect(collection.toArray()).toEqual([1, 2, 3, 4, 5, 6]); }); }); - describe("swap()", function () { + describe("swap", function () { + if (!Deque.prototype.swap) + return; it("should do nothing with no arguments", function () { - var collection = Collection([1, 2, 3, 4]); + var collection = Deque([1, 2, 3, 4]); expect(collection.swap()).toEqual([]); expect(collection.toArray()).toEqual([1, 2, 3, 4]); }); it("should splice to end with only an offset argument", function () { - var collection = Collection([1, 2, 3, 4]); + var collection = Deque([1, 2, 3, 4]); expect(collection.swap(2)).toEqual([3, 4]); expect(collection.toArray()).toEqual([1, 2]); }); it("should splice nothing with no length", function () { - var collection = Collection([1, 2, 3, 4]); + var collection = Deque([1, 2, 3, 4]); expect(collection.swap(2, 0)).toEqual([]); expect(collection.toArray()).toEqual([1, 2, 3, 4]); }); it("should splice all values", function () { - var collection = Collection([1, 2, 3, 4]); + var collection = Deque([1, 2, 3, 4]); expect(collection.swap(0, collection.length)).toEqual([1, 2, 3, 4]); expect(collection.toArray()).toEqual([]); }); it("should splice from negative offset", function () { - var collection = Collection([1, 2, 3, 4]); + var collection = Deque([1, 2, 3, 4]); expect(collection.swap(-2)).toEqual([3, 4]); expect(collection.toArray()).toEqual([1, 2]); }); it("should inject values at a numeric offset", function () { - var collection = Collection([1, 2, 5, 6]); + var collection = Deque([1, 2, 5, 6]); expect(collection.swap(2, 0, [3, 4])).toEqual([]); expect(collection.toArray()).toEqual([1, 2, 3, 4, 5, 6]); }); it("should replace values at a numeric offset", function () { - var collection = Collection([1, 2, 3, 6]); + var collection = Deque([1, 2, 3, 6]); expect(collection.swap(1, 2, [4, 5])).toEqual([2, 3]); expect(collection.toArray()).toEqual([1, 4, 5, 6]); }); it("should inject values with implied position and length", function () { - var collection = Collection([1, 2, 3, 4]); + var collection = Deque([1, 2, 3, 4]); expect(collection.swap(null, null, [-1, 0])).toEqual([]); expect(collection.toArray()).toEqual([-1, 0, 1, 2, 3, 4]); }); it("should append values", function () { - var collection = Collection([1, 2, 3, 4]); + var collection = Deque([1, 2, 3, 4]); expect(collection.swap(4, 0, [5, 6])).toEqual([]); expect(collection.toArray()).toEqual([1, 2, 3, 4, 5, 6]); }); }); + if (!Deque.prototype.isSorted) { + fuzzDeque(Deque); + } + + describe("peek and poke", function () { + if (!Deque.prototype.poke && !Deque.prototype.peek) + return; + var deque = Deque([1, 2, 3, 4, 5, 6, 7, 8]); + expect(deque.peek()).toBe(1); + expect(deque.poke(2)).toBe(undefined); + expect(deque.shift()).toBe(2); + expect(deque.peek()).toBe(2); + }); + + describe("peekBack and pokeBack", function () { + if (!Deque.prototype.pokeBack && !Deque.prototype.peekBack) + return; + var deque = Deque([1, 2, 3, 4, 5, 6, 7, 8]); + expect(deque.peekBack()).toBe(8); + expect(deque.pokeBack(9)).toBe(undefined); + expect(deque.pop()).toBe(9); + expect(deque.peekBack()).toBe(7); + }); + + // TODO peekBack + // TODO pokeBack + + // from https://github.com/petkaantonov/deque + + describe("peek", function () { + if (!Deque.prototype.peek) + return; + + it("returns undefined when empty deque", function() { + var a = new Deque(); + expect(a.length).toBe(0); + expect(a.peek()).toBe(undefined); + expect(a.peek()).toBe(undefined); + expect(a.length).toBe(0); + }); + + it("returns the item at the front of the deque", function() { + var a = new Deque(); + a.push(1,2,3,4,5,6,7,8,9); + + expect(a.peek()).toBe(1); + + var l = 5; + while(l--) a.pop(); + + expect(a.toArray()).toEqual([1, 2, 3, 4]); + + expect(a.peek()).toBe(1); + + var l = 2; + while (l--) a.shift(); + + expect(a.peek()).toBe(3); + + expect(a.toArray()).toEqual([3, 4]); + + a.unshift(1,2,3,4,5,6,78,89,12901,10121,0,12, 1,2,3,4,5,6,78,89,12901,10121,0,12); + + expect(a.toArray()).toEqual([1,2,3,4,5,6,78,89,12901,10121,0,12, 1,2,3,4,5,6,78,89,12901,10121,0,12, 3, 4]); + + expect(a.peek()).toBe(1); + + a.push(1,3,4); + + expect(a.peek()).toBe(1); + + a.pop(); + a.shift(); + + expect(a.peek()).toBe(2); + expect(a.toArray()).toEqual([2,3,4,5,6,78,89,12901,10121,0,12, 1,2,3,4,5,6,78,89,12901,10121,0,12, 3, 4, 1, 3]); + + }); + }); + + describe("clear", function () { + it("should clear the deque", function() { + var a = new Deque([1,2,3,4]); + a.clear(); + expect(a.length).toBe(0); + }); + }); + } diff --git a/spec/fuzz.js b/spec/fuzz.js index cdfa3a0..eccbc9e 100644 --- a/spec/fuzz.js +++ b/spec/fuzz.js @@ -1,4 +1,9 @@ +// TODO rename set-fuzz + +var makeRandom = require("./prng"); +exports.makeRandom = makeRandom; + exports.make = makeFuzz; function makeFuzz(length, seed, max) { var random = makeRandom(seed); @@ -72,11 +77,3 @@ function executeFuzz(set, operations, log) { }); } -exports.makeRandom = makeRandom; -function makeRandom(seed) { - return function () { - seed = ((seed * 60271) + 70451) % 99991; - return seed / 99991; - } -} - diff --git a/spec/list-spec.js b/spec/list-spec.js index dbddf17..23a02e6 100644 --- a/spec/list-spec.js +++ b/spec/list-spec.js @@ -1,6 +1,6 @@ var List = require("../list"); -var describeDequeue = require("./dequeue"); +var describeDeque = require("./deque"); var describeCollection = require("./collection"); var describeRangeChanges = require("./listen/range-changes"); @@ -60,11 +60,14 @@ describe("List", function () { // splice // swap + newList.prototype = List; + // push, pop, shift, unshift, slice, splice with numeric indicies - describeDequeue(List); - describeDequeue(function (values) { + describeDeque(List); + describeDeque(newList); + function newList(values) { return new List(values); - }); + } // construction, has, add, get, delete function newList(values) { diff --git a/spec/order.js b/spec/order.js index 79898c8..a46e1e2 100644 --- a/spec/order.js +++ b/spec/order.js @@ -26,16 +26,16 @@ function describeOrder(Collection) { describe("equals", function () { - it("should equal itself", function () { + it("identifies itself", function () { var collection = Collection([1, 2]); expect(collection.equals(collection)).toBe(true); }); - it("should be able to distinguish incomparable objects", function () { + it("distinguishes incomparable objects", function () { expect(Collection([]).equals(null)).toEqual(false); }); - it("should compare itself to an array-like collection", function () { + it("compares itself to an array-like collection", function () { expect(Collection([10, 20, 30]).equals(fakeArray)).toEqual(true); }); @@ -43,7 +43,7 @@ function describeOrder(Collection) { describe("compare", function () { - it("should compare to itself", function () { + it("compares to itself", function () { var collection = Collection([1, 2]); expect(collection.compare(collection)).toBe(0); }); @@ -83,9 +83,77 @@ function describeOrder(Collection) { }); + describe("indexOf", function () { + if (!Collection.prototype.indexOf) + return; + + it("finds first value", function () { + var collection = Collection([1, 2, 3]); + expect(collection.indexOf(2)).toBe(1); + }); + + it("finds first identical value", function () { + if (Collection.prototype.isSet) + return; + var collection = Collection([1, 1, 2, 2, 3, 3]); + expect(collection.indexOf(2)).toBe(2); + }); + + it("finds first value after index", function () { + if (Collection.prototype.isSet || Collection.prototype.isSorted) + return; + var collection = Collection([1, 2, 3, 1, 2, 3]); + expect(collection.indexOf(2, 3)).toBe(4); + }); + + it("finds first value after negative index", function () { + if (Collection.prototype.isSet || Collection.prototype.isSorted) + return; + var collection = Collection([1, 2, 3, 1, 2, 3]); + expect(collection.indexOf(2, -3)).toBe(4); + }); + + }); + + describe("lastIndexOf", function () { + if (!Collection.prototype.lastIndexOf) + return; + + it("finds last value", function () { + var collection = Collection([1, 2, 3]); + expect(collection.lastIndexOf(2)).toBe(1); + }); + + it("finds last identical value", function () { + if (Collection.prototype.isSet) + return; + var collection = Collection([1, 1, 2, 2, 3, 3]); + expect(collection.lastIndexOf(2)).toBe(3); + }); + + it("finds the last value before index", function () { + if (Collection.prototype.isSet || Collection.prototype.isSorted) + return; + var collection = Collection([1, 2, 3, 1, 2, 3]); + expect(collection.lastIndexOf(2, 3)).toBe(1); + }); + + it("finds the last value before negative index", function () { + if (Collection.prototype.isSet || Collection.prototype.isSorted) + return; + var collection = Collection([1, 2, 3, 1, 2, 3]); + expect(collection.lastIndexOf(2, -3)).toBe(1); + }); + + }); + describe("find", function () { - it("should find equivalent values", function () { + it("finds equivalent values", function () { + expect(Collection([10, 10, 10]).find(10)).toEqual(0); + }); + + it("finds equivalent values", function () { expect(Collection([10, 10, 10]).find(10)).toEqual(0); }); @@ -93,7 +161,7 @@ function describeOrder(Collection) { describe("findLast", function () { - it("should find equivalent values", function () { + it("finds equivalent values", function () { expect(Collection([10, 10, 10]).findLast(10)).toEqual(2); }); @@ -101,16 +169,39 @@ function describeOrder(Collection) { describe("has", function () { - it("should find equivalent values", function () { + it("finds equivalent values", function () { expect(Collection([10]).has(10)).toBe(true); }); - it("should not find non-contained values", function () { + it("does not find absent values", function () { expect(Collection([]).has(-1)).toBe(false); }); }); + describe("has", function () { + + it("finds a value", function () { + var collection = Collection([1, 2, 3]); + expect(collection.has(2)).toBe(true); + }); + + it("does not find an absent value", function () { + var collection = Collection([1, 2, 3]); + expect(collection.has(4)).toBe(false); + }); + + // TODO + // it("makes use of equality override", function () { + // var collection = Collection([1, 2, 3]); + // expect(collection.has(4, function (a, b) { + // return a - 1 === b; + // })).toBe(true); + // }); + + }); + + describe("any", function () { var tests = [ @@ -154,7 +245,7 @@ function describeOrder(Collection) { describe("min", function () { - it("should find the minimum of numeric values", function () { + it("finds the minimum of numeric values", function () { expect(Collection([1, 2, 3]).min()).toEqual(1); }); @@ -162,7 +253,7 @@ function describeOrder(Collection) { describe("max", function () { - it("should find the maximum of numeric values", function () { + it("finds the maximum of numeric values", function () { expect(Collection([1, 2, 3]).max()).toEqual(3); }); @@ -170,7 +261,7 @@ function describeOrder(Collection) { describe("sum", function () { - it("should compute the sum of numeric values", function () { + it("computes the sum of numeric values", function () { expect(Collection([1, 2, 3]).sum()).toEqual(6); }); @@ -181,7 +272,7 @@ function describeOrder(Collection) { describe("average", function () { - it("should compute the arithmetic mean of values", function () { + it("computes the arithmetic mean of values", function () { expect(Collection([1, 2, 3]).average()).toEqual(2); }); @@ -189,7 +280,7 @@ function describeOrder(Collection) { describe("flatten", function () { - it("should flatten an array one level", function () { + it("flattens an array one level", function () { var collection = Collection([ [[1, 2, 3], [4, 5, 6]], Collection([[7, 8, 9], [10, 11, 12]]) @@ -206,11 +297,11 @@ function describeOrder(Collection) { describe("one", function () { - it("should get the first value", function () { + it("gets the first value", function () { expect(Collection([0]).one()).toEqual(0); }); - it("should throw if empty", function () { + it("throws if empty", function () { expect(Collection([]).one()).toBe(undefined); }); @@ -218,15 +309,15 @@ function describeOrder(Collection) { describe("only", function () { - it("should get the first value", function () { + it("gets the first value", function () { expect(Collection([0]).only()).toEqual(0); }); - it("should be undefined if empty", function () { + it("is undefined if empty", function () { expect(Collection([]).only()).toBeUndefined(); }); - it("should be undefined if more than one value", function () { + it("is undefined if more than one value", function () { expect(Collection([1, 2]).only()).toBeUndefined(); }); diff --git a/spec/prng.js b/spec/prng.js new file mode 100644 index 0000000..3894340 --- /dev/null +++ b/spec/prng.js @@ -0,0 +1,9 @@ + +module.exports = prng; +function prng(seed) { + return function () { + seed = ((seed * 60271) + 70451) % 99991; + return seed / 99991; + } +} + diff --git a/spec/queue-spec.js b/spec/queue-spec.js deleted file mode 100644 index 225ad31..0000000 --- a/spec/queue-spec.js +++ /dev/null @@ -1,76 +0,0 @@ - -var Queue = require("../queue"); - -describe("Queue", function () { - - it("just the facts", function () { - var queue = new Queue(); - expect(queue.length).toBe(0); - expect(queue.capacity).toBe(16); - - queue.push(10); - expect(queue.length).toBe(1); - expect(queue.shift()).toBe(10); - expect(queue.length).toBe(0); - - queue.push(20); - expect(queue.length).toBe(1); - queue.push(30); - expect(queue.length).toBe(2); - expect(queue.shift()).toBe(20); - expect(queue.length).toBe(1); - expect(queue.shift()).toBe(30); - expect(queue.length).toBe(0); - - expect(queue.capacity).toBe(16); - - }); - - it("grows", function () { - var queue = Queue(); - - for (var i = 0; i < 16; i++) { - expect(queue.length).toBe(i); - queue.push(i); - expect(queue.capacity).toBe(16); - } - queue.push(i); - expect(queue.capacity).toBe(128); - }); - - it("initializes", function () { - var queue = new Queue([1, 2, 3]); - expect(queue.length).toBe(3); - expect(queue.shift()).toBe(1); - expect(queue.shift()).toBe(2); - expect(queue.shift()).toBe(3); - }); - - it("does not get in a funk", function () { - var queue = Queue(); - expect(queue.shift()).toBe(undefined); - queue.push(4); - expect(queue.shift()).toBe(4); - }); - - it("dispatches range changes", function () { - var spy = jasmine.createSpy(); - var handler = function (plus, minus, value) { - spy(plus, minus, value); // ignore last arg - }; - var queue = Queue(); - queue.addRangeChangeListener(handler); - queue.push(1); - queue.push(2); - queue.shift(); - queue.removeRangeChangeListener(handler); - queue.shift(); - expect(spy.argsForCall).toEqual([ - [[1], [], 0], - [[2], [], 1], - [[], [1], 0] - ]); - }); - -}); - diff --git a/spec/shim-functions-spec.js b/spec/shim-functions-spec.js index 63b9de2..b9fc2ae 100644 --- a/spec/shim-functions-spec.js +++ b/spec/shim-functions-spec.js @@ -1,5 +1,6 @@ require("../shim-object"); +require("../shim-function"); describe("Function", function () { diff --git a/spec/sorted-array-set-spec.js b/spec/sorted-array-set-spec.js index 92518ca..50e3762 100644 --- a/spec/sorted-array-set-spec.js +++ b/spec/sorted-array-set-spec.js @@ -1,6 +1,6 @@ var SortedArraySet = require("../sorted-array-set"); -var describeDequeue = require("./dequeue"); +var describeDeque = require("./deque"); var describeCollection = require("./collection"); var describeSet = require("./set"); @@ -10,8 +10,10 @@ describe("SortedArraySet", function () { return new SortedArraySet(values); } + newSortedArraySet.prototype.isSorted = true; + [SortedArraySet, newSortedArraySet].forEach(function (SortedArraySet) { - describeDequeue(SortedArraySet); + describeDeque(SortedArraySet); describeCollection(SortedArraySet, [1, 2, 3, 4]); describeSet(SortedArraySet); }); diff --git a/spec/sorted-array-spec.js b/spec/sorted-array-spec.js index 7b8f2a9..f3f5ae7 100644 --- a/spec/sorted-array-spec.js +++ b/spec/sorted-array-spec.js @@ -1,7 +1,7 @@ var SortedArray = require("../sorted-array"); var describeCollection = require("./collection"); -var describeDequeue = require("./dequeue"); +var describeDeque = require("./deque"); var describeOrder = require("./order"); describe("SortedArray", function () { @@ -10,8 +10,10 @@ describe("SortedArray", function () { return new SortedArray(values); } + newSortedArray.prototype = SortedArray.prototype; + [SortedArray, newSortedArray].forEach(function (SortedArray) { - describeDequeue(SortedArray); + describeDeque(SortedArray); describeCollection(SortedArray, [1, 2, 3, 4]); describeOrder(SortedArray); }); diff --git a/spec/sorted-set-spec.js b/spec/sorted-set-spec.js index 1e295d4..2e7643e 100644 --- a/spec/sorted-set-spec.js +++ b/spec/sorted-set-spec.js @@ -2,7 +2,7 @@ require("../shim-array"); var SortedSet = require("../sorted-set"); var TreeLog = require("../tree-log"); -var describeDequeue = require("./dequeue"); +var describeDeque = require("./deque"); var describeCollection = require("./collection"); var describeSet = require("./set"); var Fuzz = require("./fuzz"); @@ -13,6 +13,8 @@ describe("SortedSet", function () { return new SortedSet(values); } + newSortedSet.prototype.isSorted = true; + [SortedSet, newSortedSet].forEach(function (SortedSet) { // TODO SortedSet compare and equals argument overrides @@ -34,11 +36,11 @@ describe("SortedSet", function () { var values = [a, b, c, d]; describeCollection(SortedSet, values, true); - // Happens to qualify as a dequeue, since the tests keep the content in + // Happens to qualify as a deque, since the tests keep the content in // sorted order. SortedSet has meaningful pop and shift operations, but // push and unshift just add the arguments into their proper sorted // positions rather than the ends. - describeDequeue(SortedSet); + describeDeque(SortedSet); describeSet(SortedSet, "sorted"); From 4732a7fbe76838057d6decf77552cae551127454 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Sun, 2 Feb 2014 15:33:30 -0800 Subject: [PATCH 19/83] Implement {peek,poke}{,Back} on Array shim --- shim-array.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/shim-array.js b/shim-array.js index faa8861..c8be9b1 100644 --- a/shim-array.js +++ b/shim-array.js @@ -184,6 +184,28 @@ define("swap", function (start, length, plus) { } }); +define("peek", function () { + return this[0]; +}); + +define("poke", function (value) { + if (this.length > 0) { + this[0] = value; + } +}); + +define("peekBack", function () { + if (this.length > 0) { + return this[this.length - 1]; + } +}); + +define("pokeBack", function (value) { + if (this.length > 0) { + this[this.length - 1] = value; + } +}); + define("one", function () { for (var i in this) { if (Object.owns(this, i)) { From 06519a6e6f30d688cfa6f07f5373d07eab3739e4 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Sun, 2 Feb 2014 15:34:05 -0800 Subject: [PATCH 20/83] Document Deque --- README.md | 200 +++++++++++++++++++++++++++++------------------------- 1 file changed, 107 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index 3f442d6..6c18e48 100644 --- a/README.md +++ b/README.md @@ -31,14 +31,26 @@ var List = require("collections/list"); ``` An ordered collection of values with fast insertion and deletion and -forward and backward traversal, backed by a cyclic doubly linked -list with a head node. Lists support most of the Array interface, -except that they use and return nodes instead of integer indicies in -analogous functions. +forward and backward traversal and splicing, backed by a cyclic doubly +linked list with a head node. Lists support most of the Array +interface, except that they use and return nodes instead of integer +indicies in analogous functions. Lists have a `head` `Node`. The node type is available as `Node` on -the list prototype and can be overridden by inheritors. Each node -has `prev` and `next` properties. +the list prototype and can be overridden by inheritors. Each node has +`prev` and `next` properties. + +### Deque(values, capacity) + +```javascript +var Deque = require("collections/deque"); +``` + +An ordered collection of values with fast insertion and deletion and +forward and backward traversal, backed by a circular buffer that +doubles its capacity at need. Deques support most of the Array +interface. A Deque is generally faster and produces less garbage +collector churn than a List, but does not support fast splicing. ### Set(values, equals, hash, getDefault) @@ -342,7 +354,8 @@ The value for a key. If a Map or SortedMap lacks a key, returns Gets the equivalent value, or falls back to `getDefault(value)`. -(List, Set, SortedSet, LruSet, SortedArray, SortedArraySet, FastSet) +(List, Deque, Set, SortedSet, LruSet, SortedArray, SortedArraySet, +FastSet) ### set(key or index, value) @@ -358,8 +371,8 @@ Dict) Adds a value. Ignores the operation and returns false if an equivalent value already exists. -(Array+, List, Set, SortedSet, LruSet, SortedArray, SortedArraySet, -FastSet, Heap) +(Array+, List, Deque, Set, SortedSet, LruSet, SortedArray, +SortedArraySet, FastSet, Heap) #### add(value, key) @@ -406,15 +419,15 @@ Deletes a value. Returns whether the value was found. Deletes the equivalent value. Returns whether the value was found. -(Array+, List) +(Array+, List, Deque) ### deleteEach(values or keys) Deletes every value or every value for each key. -(Array+, List, Set, Map, MultiMap, SortedSet, SortedMap, -LruSet, LruMap, SortedArray, SortedArraySet, SortedArrayMap, -FastSet, FastMap, Dict, Heap) +(Array+, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, +LruSet, LruMap, SortedArray, SortedArraySet, SortedArrayMap, FastSet, +FastMap, Dict, Heap) ### indexOf(value) @@ -426,14 +439,14 @@ search. For a SortedSet, this takes ammortized logarithmic time since it incrementally updates the number of nodes under each subtree as it rotates. -(Array, ~~List~~, SortedSet, SortedArray, SortedArraySet) +(Array, ~~List~~, Deque, SortedSet, SortedArray, SortedArraySet) ### lastIndexOf(value) Returns the position in the collection of a value, or `-1` if it is not found. Returns the position of the last of equivalent values. -(Array, ~~List~~, SortedArray, SortedArraySet) +(Array, ~~List~~, Deque, SortedArray, SortedArraySet) ### find(value, opt_equals) @@ -441,14 +454,14 @@ Finds a value. For List and SortedSet, returns the node at which the value was found. For SortedSet, the optional `equals` argument is ignored. -(Array+, List, SortedSet) +(Array+, List, Deque, SortedSet) ### findLast(value, opt_equals) Finds the last equivalent value, returning the node at which the value was found. -(Array+, List, SortedArray, SortedArraySet) +(Array+, List, Deque, SortedArray, SortedArraySet) ### findLeast() @@ -489,9 +502,9 @@ This is fast (logarithmic) but does cause rotations. Adds values to the end of a collection. -(Array, List) +(Array, List, Deque) -#### push(...values) for non-dequeues +#### push(...values) for non-deques Adds values to their proper places in a collection. This method exists only to have the same interface as other @@ -505,9 +518,9 @@ collections. Adds values to the beginning of a collection. -(Array, List) +(Array, List, Deque) -#### unshift(...values) for non-dequeues +#### unshift(...values) for non-deques Adds values to their proper places in a collection. This method exists only to have the same interface as other @@ -521,26 +534,43 @@ Removes and returns the value at the end of a collection. For a Heap, this means the greatest contained value, as defined by the comparator. -(Array, List, Set, SortedSet, LruSet, SortedArray, SortedArraySet, -Heap) +(Array, List, Deque, Set, SortedSet, LruSet, SortedArray, +SortedArraySet, Heap) ### shift() Removes and returns the value at the beginning of a collection. -(Array, List, Set, SortedSet, LruSet, SortedArray, SortedArraySet) +(Array, List, Deque, Set, SortedSet, LruSet, SortedArray, +SortedArraySet) ### peek() -Returns the last value in an ordered collection. +Returns the next value in an deque, as would be returned by the next +`shift`. -(List) +(Array, List, Deque) ### poke(value) -Replaces the last value in an ordered collection. +Replaces the next value in an ordered collection, such that it will be +returned by `shift` instead of what was there. + +(Array, List, Deque) + +### peekBack() + +Returns the last value in an deque, as would be returned by the next +`pop`. + +(Array, List, Deque) + +### pokeBack(value) -(List) +Replaces the last value in an ordered collection, such that it will be +returned by `pop` instead of what was there. + +(Array, List, Deque) ### slice(start, end) @@ -569,7 +599,7 @@ Performs a splice without variadic arguments. Deletes the all values. -(Array+, Object+, List, Set, Map, MultiMap, SortedSet, +(Array+, Object+, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, SortedArrayMap, FastSet, FastMap, Dict, Heap) @@ -595,7 +625,7 @@ with `compare` and `by` functions. The default `compare` value is The `by` function must be a function that accepts a value from the collection and returns a representative value on which to sort. -(Array+, List, Set, Map, SortedSet, LruSet, SortedArray, +(Array+, List, Deque, Set, Map, SortedSet, LruSet, SortedArray, SortedArraySet, FastSet, Heap) ### group(callback, thisp, equals) @@ -605,7 +635,7 @@ element from the collection is placed into an equivalence class if they have the same corresponding return value from the given callback. -(Array+, Object+, List, Set, Map, MultiMap, SortedSet, +(Array+, Object+, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, SortedArrayMap, FastSet, FastMap, Dict, Heap, Iterator) @@ -670,14 +700,14 @@ Dict) ### reduce(callback(result, value, key, object, depth), basis, thisp) -(Array, Iterator, List, Set, Map, MultiMap, SortedSet, SortedMap, -LruSet, LruMap, SortedArray, SortedArraySet, SortedArrayMap, -FastSet, FastMap, Dict, Heap) +(Array, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, +SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, +SortedArrayMap, FastSet, FastMap, Dict, Heap) ### reduceRight(callback(result, value, key, object, depth), basis, thisp) -(Array, List, SortedSet, ~~SortedMap~~, SortedArray, SortedArraySet, -~~SortedArrayMap~~, Heap) +(Array, List, Deque, SortedSet, ~~SortedMap~~, SortedArray, +SortedArraySet, ~~SortedArrayMap~~, Heap) ### forEach(callback(value, key, object, depth), thisp) @@ -686,35 +716,35 @@ of lists is resilient to changes to the list. Particularly, nodes added after the current node will be visited and nodes added before the current node will be ignored, and no node will be visited twice. -(Array, Object+, Iterator, List, Set, Map, MultiMap, WeakMap, +(Array, Object+, Iterator, List, Deque, Set, Map, MultiMap, WeakMap, SortedSet, SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, SortedArrayMap, FastSet, FastMap, Dict, Heap) ### map(callback(value, key, object, depth), thisp) -(Array, Object+, Iterator, List, Set, Map, MultiMap, WeakMap, +(Array, Object+, Iterator, List, Deque, Set, Map, MultiMap, WeakMap, SortedSet, SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, SortedArrayMap, FastSet, FastMap, Dict, Heap) ### toArray() -(Array+, Iterator, List, Set, Map, MultiMap, SortedSet, SortedMap, -LruSet, LruMap, SortedArray, SortedArraySet, SortedArrayMap, -FastSet, FastMap, Dict, Heap) +(Array+, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, +SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, +SortedArrayMap, FastSet, FastMap, Dict, Heap) ### toObject() Converts any collection to an object, treating this collection as a map-like object. Array is like a map from index to value. -(Array+ Iterator, List, Map, MultiMap, SortedMap, LruMap, +(Array+ Iterator, List, Deque, Map, MultiMap, SortedMap, LruMap, SortedArrayMap, FastMap, Dict, Heap) ### filter(callback(value, key, object, depth), thisp) -(Array, Iterator, List, Set, Map, MultiMap, SortedSet, SortedMap, -LruSet, LruMap, SortedArray, SortedArraySet, SortedArrayMap, -FastSet, FastMap, Dict, Heap) +(Array, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, +SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, +SortedArrayMap, FastSet, FastMap, Dict, Heap) ### every(callback(value, key, object, depth), thisp) @@ -722,9 +752,9 @@ Whether every value passes a given guard. Stops evaluating the guard after the first failure. Iterators stop consuming after the the first failure. -(Array, Iterator, List, Set, Map, MultiMap, SortedSet, SortedMap, -LruSet, LruMap, SortedArray, SortedArraySet, SortedArrayMap, -FastSet, FastMap, Dict, Heap) +(Array, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, +SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, +SortedArrayMap, FastSet, FastMap, Dict, Heap) ### some(callback(value, key, object, depth), thisp) @@ -732,25 +762,9 @@ Whether there is a value that passes a given guard. Stops evaluating the guard after the first success. Iterators stop consuming after the first success. -(Array, Iterator, List, Set, Map, MultiMap, SortedSet, SortedMap, -LruSet, LruMap, SortedArray, SortedArraySet, SortedArrayMap, -FastSet, FastMap, Dict, Heap) - -### any() - -Whether any value is truthy. - -(Array+, Iterator, List, Set, Map, MultiMap, SortedSet, SortedMap, -LruSet, LruMap, SortedArray, SortedArraySet, SortedArrayMap, -FastSet, FastMap, Dict, Heap) - -### all() - -Whether all values are truthy. - -(Array+, Iterator, List, Set, Map, MultiMap, SortedSet, SortedMap, -LruSet, LruMap, SortedArray, SortedArraySet, SortedArrayMap, -FastSet, FastMap, Dict, Heap) +(Array, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, +SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, +SortedArrayMap, FastSet, FastMap, Dict, Heap) ### min() @@ -758,9 +772,9 @@ The smallest value. This is fast for sorted collections (logarithic for SortedSet, constant for SortedArray, SortedArraySet, and SortedArrayMap), but slow for everything else (linear). -(Array+, Iterator, List, Set, Map, MultiMap, SortedSet, SortedMap, -LruSet, LruMap, SortedArray, SortedArraySet, SortedArrayMap, -FastSet, FastMap, Dict) +(Array+, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, +SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, +SortedArrayMap, FastSet, FastMap, Dict) ### max() @@ -768,9 +782,9 @@ The largest value. This is fast for sorted collections (logarithic for SortedSet, constant for SortedArray, SortedArraySet, and SortedArrayMap), but slow for everything else (linear). -(Array+, Iterator, List, Set, Map, MultiMap, SortedSet, SortedMap, -LruSet, LruMap, SortedArray, SortedArraySet, SortedArrayMap, -FastSet, FastMap, Dict, Heap) +(Array+, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, +SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, +SortedArrayMap, FastSet, FastMap, Dict, Heap) ### one() @@ -781,28 +795,28 @@ is very fast (constant time) for most collections. For a sorted set, being consistent across accesses, and only changing in response to mutation, `one` returns the `min` of the set in logarithmic time. -(Array+, List, Set, Map, MultiMap, SortedSet, SortedMap, LruSet, -LruMap, SortedArray, SortedArray, SortedArraySet, SortedArrayMap, -FastSet, FastMap, Dict, Heap) +(Array+, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, +LruSet, LruMap, SortedArray, SortedArray, SortedArraySet, +SortedArrayMap, FastSet, FastMap, Dict, Heap) ### only() The one and only value, or throws an exception if there are no values or more than one value. -(Array+, List, Set, Map, MultiMap, SortedSet, SortedMap, LruSet, -LruMap, SortedArray, SortedArray, SortedArraySet, SortedArrayMap, -FastSet, FastMap, Dict, Heap) +(Array+, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, +LruSet, LruMap, SortedArray, SortedArray, SortedArraySet, +SortedArrayMap, FastSet, FastMap, Dict, Heap) ### sum() -(Array+, Iterator, List, Set, Map, MultiMap, SortedSet, +(Array+, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, SortedArrayMap, FastSet, FastMap, Dict) ### average() -(Array+, Iterator, List, Set, Map, MultiMap, SortedSet, +(Array+, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, SortedArrayMap, FastSet, FastMap, Dict) @@ -814,15 +828,15 @@ SortedArrayMap, FastSet, FastMap, Dict, Heap) ### zip(...collections) -(Array+, Iterator, List, Set, Map, MultiMap, SortedSet, +(Array+, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, SortedArrayMap, FastSet, FastMap, Dict, Heap) ### enumerate(zero) -(Array+, Iterator, List, Set, Map, MultiMap, SortedSet, SortedMap, -LruSet, LruMap, SortedArray, SortedArraySet, SortedArrayMap, -FastSet, FastMap, Dict, Heap) +(Array+, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, +SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, +SortedArrayMap, FastSet, FastMap, Dict, Heap) ### clone(depth, memo) @@ -841,7 +855,7 @@ The `clone` method on any other objects is not intended to be used directly since they do not necessarily supply a default depth and memo. -(Array+, Object+, List, Set, Map, MultiMap, SortedSet, +(Array+, Object+, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, SortedArrayMap, FastSet, FastMap, Dict, Heap) @@ -853,19 +867,19 @@ same options (`equals`, `compare`, `hash` options), but it leaves the job of deeply cloning the values to the more general `clone` method. -(Array+, Object+, List, Set, Map, MultiMap, SortedSet, +(Array+, Object+, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, SortedArrayMap, FastSet, FastMap, Dict, Heap) ### equals(that, equals) -(Array+, Object+, List, Set, Map, MultiMap, SortedSet, SortedMap, -LruSet, LruMap, ~~SortedArray~~, SortedArraySet, SortedArrayMap, -FastSet, FastMap, Dict) +(Array+, Object+, List, Deque, Set, Map, MultiMap, SortedSet, +SortedMap, LruSet, LruMap, ~~SortedArray~~, SortedArraySet, +SortedArrayMap, FastSet, FastMap, Dict) ### compare(that) -(Array+, Object+, List, ~~SortedArray~~, ~~SortedArraySet~~) +(Array+, Object+, List, Deque, ~~SortedArray~~, ~~SortedArraySet~~) ### iterate @@ -876,8 +890,8 @@ richer iterators by wrapping this iterator with an `Iterator` from the `iterator` module. Iteration order of lists is resilient to changes to the list. -(Array+, Iterator, List, Set, SortedSet, LruSet, SortedArray, -SortedArraySet, FastSet) +(Array+, Iterator, List, ~~Deque~~, Set, SortedSet, LruSet, +SortedArray, SortedArraySet, FastSet) #### iterate(start, end) From 7935c8a40c53a8af6c198a1f5a1101fd6e3612ac Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Mon, 3 Feb 2014 00:21:51 -0800 Subject: [PATCH 21/83] Reimplement Iterator Based in changes to the iterator specification slated for ECMAScript 6, but extended to pass indicies through on iteration objects. This reimplementation largely avoids using closures for iterator instances. Methods of iterators that return new iterators are now conjugated differently, to avoid colliding with the variant that will produce a plain array. For example, `zipIterator` is now `iterateZip`. I have removed the type checks for callbacks. Any object implementing `call` will suffice. :warning: This commit introduces backward incompatible changes and should await a major version. --- generic-collection.js | 4 - iterator.js | 576 +++++++++++++++----------- list.js | 9 +- shim-array.js | 21 +- sorted-array.js | 7 +- sorted-set.js | 15 +- spec/dict.js | 6 +- spec/fast-set-spec.js | 1 + spec/iterator-spec.js | 918 ++++++++++++++++-------------------------- 9 files changed, 712 insertions(+), 845 deletions(-) diff --git a/generic-collection.js b/generic-collection.js index 9a05752..6d65a7d 100644 --- a/generic-collection.js +++ b/generic-collection.js @@ -253,9 +253,5 @@ GenericCollection.prototype.only = function () { } }; -GenericCollection.prototype.iterator = function () { - return this.iterate.apply(this, arguments); -}; - require("./shim-array"); diff --git a/iterator.js b/iterator.js index 21dff8c..8fe4899 100644 --- a/iterator.js +++ b/iterator.js @@ -2,40 +2,36 @@ module.exports = Iterator; -var Object = require("./shim-object"); +var WeakMap = require("./weak-map"); var GenericCollection = require("./generic-collection"); // upgrades an iterable to a Iterator -function Iterator(iterable) { - - if (!(this instanceof Iterator)) { +function Iterator(iterable, start, stop, step) { + if (iterable instanceof Iterator) { + return iterable; + } else if (!(this instanceof Iterator)) { return new Iterator(iterable); + } else if (Array.isArray(iterable) || typeof iterable === "string") { + iterators.set(this, new IndexIterator(iterable, start, stop, step)); + return; } - - if (Array.isArray(iterable) || typeof iterable === "string") - return Iterator.iterate(iterable); - iterable = Object(iterable); - - if (iterable instanceof Iterator) { - return iterable; - } else if (iterable.next) { - this.next = function () { - return iterable.next(); - }; + if (iterable.next) { + iterators.set(this, iterable); } else if (iterable.iterate) { - var iterator = iterable.iterate(); - this.next = function () { - return iterator.next(); - }; + iterators.set(this, iterable.iterate(start, stop, step)); } else if (Object.prototype.toString.call(iterable) === "[object Function]") { this.next = iterable; } else { throw new TypeError("Can't iterate " + iterable); } - } +// Using iterators as a hidden table associating a full-fledged Iterator with +// an underlying, usually merely "nextable", iterator. +var iterators = new WeakMap(); + +// Selectively apply generic methods of GenericCollection Iterator.prototype.forEach = GenericCollection.prototype.forEach; Iterator.prototype.map = GenericCollection.prototype.map; Iterator.prototype.filter = GenericCollection.prototype.filter; @@ -55,252 +51,344 @@ Iterator.prototype.group = GenericCollection.prototype.group; Iterator.prototype.reversed = GenericCollection.prototype.reversed; Iterator.prototype.toArray = GenericCollection.prototype.toArray; Iterator.prototype.toObject = GenericCollection.prototype.toObject; -Iterator.prototype.iterator = GenericCollection.prototype.iterator; -// this is a bit of a cheat so flatten and such work with the generic -// reducible +// This is a bit of a cheat so flatten and such work with the generic reducible Iterator.prototype.constructClone = function (values) { var clone = []; clone.addEach(values); return clone; }; -Iterator.prototype.mapIterator = function (callback /*, thisp*/) { +// A level of indirection so a full-interface iterator can proxy for a simple +// nextable iterator, and to allow the child iterator to replace its governing +// iterator, as with drop-while iterators. +Iterator.prototype.next = function () { + var nextable = iterators.get(this); + if (nextable) { + return nextable.next(); + } else { + return Iterator.done; + } +}; + +Iterator.prototype.iterateMap = function (callback /*, thisp*/) { var self = Iterator(this), - thisp = arguments[1], - i = 0; + thisp = arguments[1]; + return new Iterator(new MapIterator(self, callback, thisp)); +}; - if (Object.prototype.toString.call(callback) != "[object Function]") - throw new TypeError(); +function MapIterator(iterator, callback, thisp) { + this.iterator = iterator; + this.callback = callback; + this.thisp = thisp; +} - return new self.constructor(function () { - return callback.call(thisp, self.next(), i++, self); - }); +MapIterator.prototype.next = function () { + var iteration = this.iterator.next(); + if (iteration.done) { + return iteration; + } else { + return new Iteration( + this.callback.call( + this.thisp, + iteration.value, + iteration.index, + this.iteration + ), + iteration.index + ); + } }; -Iterator.prototype.filterIterator = function (callback /*, thisp*/) { +Iterator.prototype.iterateFilter = function (callback /*, thisp*/) { var self = Iterator(this), thisp = arguments[1], - i = 0; + index = 0; - if (Object.prototype.toString.call(callback) != "[object Function]") - throw new TypeError(); + return new Iterator(new FilterIterator(self, callback, thisp)); +}; - return new self.constructor(function () { - var value; - while (true) { - value = self.next(); - if (callback.call(thisp, value, i++, self)) - return value; +function FilterIterator(iterator, callback, thisp) { + this.iterator = iterator; + this.callback = callback; + this.thisp = thisp; + this.index = 0; +} + +FilterIterator.prototype.next = function () { + var iteration; + while (true) { + iteration = this.iterator.next(); + if (iteration.done) { + return iteration; + } else if (this.callback.call( + this.thisp, + iteration.value, + iteration.index, + this.iteration + )) { + return new Iteration( + iteration.value, + this.index++ + ); } - }); + + } }; Iterator.prototype.reduce = function (callback /*, initial, thisp*/) { var self = Iterator(this), result = arguments[1], thisp = arguments[2], - i = 0, - value; - - if (Object.prototype.toString.call(callback) != "[object Function]") - throw new TypeError(); + iteration; - // first iteration unrolled - try { - value = self.next(); + // First iteration unrolled + iteration = self.next(); + if (iteration.done) { if (arguments.length > 1) { - result = callback.call(thisp, result, value, i, self); - } else { - result = value; - } - i++; - } catch (exception) { - if (isStopIteration(exception)) { - if (arguments.length > 1) { - return arguments[1]; // initial - } else { - throw TypeError("cannot reduce a value from an empty iterator with no initial value"); - } + return arguments[1]; } else { - throw exception; + throw TypeError("Reduce of empty iterator with no initial value"); } + } else if (arguments.length > 1) { + result = callback.call( + thisp, + result, + iteration.value, + iteration.index, + self + ); + } else { + result = iteration.value; } - // remaining entries - try { - while (true) { - value = self.next(); - result = callback.call(thisp, result, value, i, self); - i++; - } - } catch (exception) { - if (isStopIteration(exception)) { + // Remaining entries + while (true) { + iteration = self.next(); + if (iteration.done) { return result; } else { - throw exception; + result = callback.call( + thisp, + result, + iteration.value, + iteration.index, + self + ); } } - -}; - -Iterator.prototype.concat = function () { - return Iterator.concat( - Array.prototype.concat.apply(this, arguments) - ); }; Iterator.prototype.dropWhile = function (callback /*, thisp */) { var self = Iterator(this), thisp = arguments[1], - stopped = false, - stopValue; - - if (Object.prototype.toString.call(callback) != "[object Function]") - throw new TypeError(); - - self.forEach(function (value, i) { - if (!callback.call(thisp, value, i, self)) { - stopped = true; - stopValue = value; - throw StopIteration; + iteration; + + while (true) { + iteration = self.next(); + if (iteration.done) { + return Iterator.empty; + } else if (!callback.call(thisp, iteration.value, iteration.index, self)) { + var iterator = new Iterator(new DropWhileIterator(iteration, self)); + iterators.get(iterator).parent = iterator; + return iterator; } - }); - - if (stopped) { - return self.constructor([stopValue]).concat(self); - } else { - return self.constructor([]); } }; +function DropWhileIterator(iteration, nextIterator) { + this.iteration = iteration; + this.nextIterator = nextIterator; + this.parent = null; +} + +DropWhileIterator.prototype.next = function () { + var result = this.iteration; + iterators.set(this.parent, this.nextIterator); + return result; +}; + Iterator.prototype.takeWhile = function (callback /*, thisp*/) { var self = Iterator(this), thisp = arguments[1]; + return new Iterator(new TakeWhileIterator(self, callback, thisp)); +}; - if (Object.prototype.toString.call(callback) != "[object Function]") - throw new TypeError(); +function TakeWhileIterator(iterator, callback, thisp) { + this.iterator = iterator; + this.callback = callback; + this.thisp = thisp; +} - return self.mapIterator(function (value, i) { - if (!callback.call(thisp, value, i, self)) - throw StopIteration; - return value; - }); +TakeWhileIterator.prototype.next = function () { + var iteration = this.iterator.next(); + if (iteration.done) { + return iteration; + } else if (this.callback.call( + this.thisp, + iteration.value, + iteration.index, + this.iterator + )) { + return iteration; + } else { + return Iterator.done; + } }; -Iterator.prototype.zipIterator = function () { - return Iterator.unzip( - Array.prototype.concat.apply(this, arguments) - ); +Iterator.prototype.iterateZip = function () { + return Iterator.unzip(Array.prototype.concat.apply(this, arguments)); }; -Iterator.prototype.enumerateIterator = function (start) { - return Iterator.count(start).zipIterator(this); +Iterator.prototype.iterateUnzip = function () { + return Iterator.unzip(this); +}; + +Iterator.prototype.iterateEnumerate = function (start) { + return Iterator.count(start).iterateZip(this); +}; + +Iterator.prototype.iterateConcat = function () { + return Iterator.flatten(Array.prototype.concat.apply(this, arguments)); +}; + +Iterator.prototype.iterateFlatten = function () { + return Iterator.flatten(this); }; // creates an iterator for Array and String -Iterator.iterate = function (iterable) { - var start; - start = 0; - return new Iterator(function () { - // advance to next owned entry - if (typeof iterable === "object") { - while (!(start in iterable)) { - // deliberately late bound - if (start >= iterable.length) - throw StopIteration; - start += 1; +function IndexIterator(iterable, start, stop, step) { + if (step == null) { + step = 1; + } + if (stop == null) { + stop = start; + start = 0; + } + if (start == null) { + start = 0; + } + if (step == null) { + step = 1; + } + if (stop == null) { + stop = iterable.length; + } + this.iterable = iterable; + this.start = start; + this.stop = stop; + this.step = step; +} + +IndexIterator.prototype.next = function () { + // Advance to next owned entry + if (typeof this.iterable === "object") { // as opposed to string + while (!(this.start in this.iterable)) { + if (this.start >= this.stop) { + return Iterator.done; + } else { + this.start += this.step; } - } else if (start >= iterable.length) { - throw StopIteration; } - var result = iterable[start]; - start += 1; - return result; - }); + } else if (this.start >= this.stop) { // end of string + return Iterator.done; + } + var iteration = new Iteration( + this.iterable[this.start], + this.start + ); + this.start += this.step; + return iteration; }; Iterator.cycle = function (cycle, times) { - if (arguments.length < 2) + if (arguments.length < 2) { times = Infinity; - //cycle = Iterator(cycle).toArray(); - var next = function () { - throw StopIteration; - }; - return new Iterator(function () { - var iteration; - try { - return next(); - } catch (exception) { - if (isStopIteration(exception)) { - if (times <= 0) - throw exception; - times--; - iteration = Iterator.iterate(cycle); - next = iteration.next.bind(iteration); - return next(); - } else { - throw exception; - } + } + return new Iterator(new CycleIterator(cycle, times)); +}; + +function CycleIterator(cycle, times) { + this.cycle = cycle; + this.times = times; + this.iterator = Iterator.empty; +} + +CycleIterator.prototype.next = function () { + var iteration = this.iterator.next(); + if (iteration.done) { + if (this.times > 0) { + this.times--; + this.iterator = new Iterator(this.cycle); + return this.iterator.next(); + } else { + return iteration; } - }); + } else { + return iteration; + } }; -Iterator.concat = function (iterators) { +Iterator.concat = function (/* ...iterators */) { + return Iterator.flatten(Array.prototype.slice.call(arguments)); +}; + +Iterator.flatten = function (iterators) { iterators = Iterator(iterators); - var next = function () { - throw StopIteration; - }; - return new Iterator(function (){ - var iteration; - try { - return next(); - } catch (exception) { - if (isStopIteration(exception)) { - iteration = Iterator(iterators.next()); - next = iteration.next.bind(iteration); - return next(); - } else { - throw exception; - } + return new Iterator(new ChainIterator(iterators)); +}; + +function ChainIterator(iterators) { + this.iterators = iterators; + this.iterator = Iterator.empty; +} + +ChainIterator.prototype.next = function () { + var iteration = this.iterator.next(); + if (iteration.done) { + var iteratorIteration = this.iterators.next(); + if (iteratorIteration.done) { + return Iterator.done; + } else { + this.iterator = new Iterator(iteratorIteration.value); + return this.iterator.next(); } - }); + } else { + return iteration; + } }; Iterator.unzip = function (iterators) { iterators = Iterator(iterators).map(Iterator); if (iterators.length === 0) - return new Iterator([]); - return new Iterator(function () { - var stopped; - var result = iterators.map(function (iterator) { - try { - return iterator.next(); - } catch (exception) { - if (isStopIteration(exception)) { - stopped = true; - } else { - throw exception; - } - } - }); - if (stopped) { - throw StopIteration; + return new Iterator.empty; + return new Iterator(new UnzipIterator(iterators)); +}; + +function UnzipIterator(iterators) { + this.iterators = iterators; + this.index = 0; +} + +UnzipIterator.prototype.next = function () { + var done = false + var result = this.iterators.map(function (iterator) { + var iteration = iterator.next(); + if (iteration.done) { + done = true; + } else { + return iteration.value; } - return result; }); + if (done) { + return Iterator.done; + } else { + return new Iteration(result, this.index++); + } }; Iterator.zip = function () { - return Iterator.unzip( - Array.prototype.slice.call(arguments) - ); -}; - -Iterator.chain = function () { - return Iterator.concat( - Array.prototype.slice.call(arguments) - ); + return Iterator.unzip(Array.prototype.slice.call(arguments)); }; Iterator.range = function (start, stop, step) { @@ -313,59 +401,83 @@ Iterator.range = function (start, stop, step) { } start = start || 0; step = step || 1; - return new Iterator(function () { - if (start >= stop) - throw StopIteration; - var result = start; - start += step; - return result; - }); + return new Iterator(new RangeIterator(start, stop, step)); }; Iterator.count = function (start, step) { return Iterator.range(start, Infinity, step); }; +function RangeIterator(start, stop, step) { + this.start = start; + this.stop = stop; + this.step = step; + this.index = 0; +} + +RangeIterator.prototype.next = function () { + if (this.start >= this.stop) { + return Iterator.done; + } else { + var result = this.start; + this.start += this.step; + return new Iteration(result, this.index++); + } +}; + Iterator.repeat = function (value, times) { - return new Iterator.range(times).mapIterator(function () { - return value; - }); + if (times == null) { + times = Infinity; + } + return new Iterator(new RepeatIterator(value, times)); }; -// shim isStopIteration -if (typeof isStopIteration === "undefined") { - global.isStopIteration = function (exception) { - return Object.prototype.toString.call(exception) === "[object StopIteration]"; - }; +function RepeatIterator(value, times) { + this.value = value; + this.times = times; + this.index = 0; } -// shim StopIteration -if (typeof StopIteration === "undefined") { - global.StopIteration = {}; - Object.prototype.toString = (function (toString) { - return function () { - if ( - this === global.StopIteration || - this instanceof global.ReturnValue - ) - return "[object StopIteration]"; - else - return toString.call(this, arguments); - }; - })(Object.prototype.toString); +RepeatIterator.prototype.next = function () { + if (this.index < this.times) { + return new Iteration(this.value, this.index++); + } else { + return Iterator.done; + } +}; + +Iterator.enumerate = function (values, start) { + return Iterator.count(start).iterateZip(new Iterator(values)); +}; + +function EmptyIterator() {} + +EmptyIterator.prototype.next = function () { + return Iterator.done; +}; + +Iterator.empty = new Iterator(new EmptyIterator()); + +// Iteration and DoneIteration exist here only to encourage hidden classes. +// Otherwise, iterations are merely duck-types. + +function Iteration(value, index) { + this.value = value; + this.index = index; } -// shim ReturnValue -if (typeof ReturnValue === "undefined") { - global.ReturnValue = function ReturnValue(value) { - this.message = "Iteration stopped with " + value; - if (Error.captureStackTrace) { - Error.captureStackTrace(this, ReturnValue); - } - if (!(this instanceof global.ReturnValue)) - return new global.ReturnValue(value); - this.value = value; - }; - ReturnValue.prototype = Error.prototype; +Iteration.prototype.done = false; + +function DoneIteration(value) { + Iteration.call(this, value); + this.done = true; // reflected on the instance to make it more obvious } +DoneIteration.prototype = Object.create(Iteration.prototype); +DoneIteration.prototype.constructor = DoneIteration; +DoneIteration.prototype.done = true; + +Iterator.Iteration = Iteration; +Iterator.DoneIteration = DoneIteration; +Iterator.done = new DoneIteration(); + diff --git a/list.js b/list.js index e65961e..1871167 100644 --- a/list.js +++ b/list.js @@ -7,6 +7,7 @@ var GenericCollection = require("./generic-collection"); var GenericOrder = require("./generic-order"); var PropertyChanges = require("./listen/property-changes"); var RangeChanges = require("./listen/range-changes"); +var Iterator = require("./iterator"); function List(values, equals, getDefault) { if (!(this instanceof List)) { @@ -386,7 +387,7 @@ List.prototype.makeObservable = function () { }; List.prototype.iterate = function () { - return new ListIterator(this.head); + return new Iterator(new ListIterator(this.head)); }; function ListIterator(head) { @@ -396,11 +397,11 @@ function ListIterator(head) { ListIterator.prototype.next = function () { if (this.at === this.head) { - throw StopIteration; + return Iterator.done; } else { - var value = this.at.value; + var at = this.at; this.at = this.at.next; - return value; + return at; } }; diff --git a/shim-array.js b/shim-array.js index faa8861..c3d48fd 100644 --- a/shim-array.js +++ b/shim-array.js @@ -10,6 +10,7 @@ var Function = require("./shim-function"); var GenericCollection = require("./generic-collection"); var GenericOrder = require("./generic-order"); +var Iterator = require("./iterator"); var WeakMap = require("weak-map"); module.exports = Array; @@ -288,23 +289,7 @@ define("clone", function (depth, memo) { return clone; }); -define("iterate", function (start, end) { - return new ArrayIterator(this, start, end); +define("iterate", function (start, stop, step) { + return new Iterator(this, start, stop, step); }); -define("Iterator", ArrayIterator); - -function ArrayIterator(array, start, end) { - this.array = array; - this.start = start == null ? 0 : start; - this.end = end; -}; - -ArrayIterator.prototype.next = function () { - if (this.start === (this.end == null ? this.array.length : this.end)) { - throw StopIteration; - } else { - return this.array[this.start++]; - } -}; - diff --git a/sorted-array.js b/sorted-array.js index 1931f9a..463f838 100644 --- a/sorted-array.js +++ b/sorted-array.js @@ -6,6 +6,7 @@ var Shim = require("./shim"); var GenericCollection = require("./generic-collection"); var PropertyChanges = require("./listen/property-changes"); var RangeChanges = require("./listen/range-changes"); +var Iterator = require("./iterator"); function SortedArray(values, equals, compare, getDefault) { if (!(this instanceof SortedArray)) { @@ -261,9 +262,9 @@ SortedArray.prototype.compare = function (that, compare) { return this.array.compare(that, compare); }; -SortedArray.prototype.iterate = function (start, end) { - return new this.Iterator(this.array, start, end); +SortedArray.prototype.iterate = function (start, stop, step) { + return new this.Iterator(this.array, start, stop, step); }; -SortedArray.prototype.Iterator = Array.prototype.Iterator; +SortedArray.prototype.Iterator = Iterator; diff --git a/sorted-set.js b/sorted-set.js index 33bc838..4f990f2 100644 --- a/sorted-set.js +++ b/sorted-set.js @@ -7,6 +7,7 @@ var GenericCollection = require("./generic-collection"); var GenericSet = require("./generic-set"); var PropertyChanges = require("./listen/property-changes"); var RangeChanges = require("./listen/range-changes"); +var Iterator = require("./iterator"); var TreeLog = require("./tree-log"); function SortedSet(values, equals, compare, getDefault) { @@ -507,10 +508,10 @@ SortedSet.prototype.clear = function () { }; SortedSet.prototype.iterate = function (start, end) { - return new this.Iterator(this, start, end); + return new Iterator(new this.Iterator(this, start, end)); }; -SortedSet.prototype.Iterator = Iterator; +SortedSet.prototype.Iterator = SortedSetIterator; SortedSet.prototype.summary = function () { if (this.root) { @@ -701,7 +702,7 @@ Node.prototype.log = function (charmap, logNode, log, logAbove) { ); }; -function Iterator(set, start, end) { +function SortedSetIterator(set, start, end) { this.set = set; this.prev = null; this.end = end; @@ -714,7 +715,7 @@ function Iterator(set, start, end) { } } -Iterator.prototype.next = function () { +SortedSetIterator.prototype.next = function () { var next; if (this.prev) { next = this.set.findLeastGreaterThan(this.prev.value); @@ -722,15 +723,15 @@ Iterator.prototype.next = function () { next = this.set.findLeast(); } if (!next) { - throw StopIteration; + return Iterator.done; } if ( this.end !== undefined && this.set.contentCompare(next.value, this.end) >= 0 ) { - throw StopIteration; + return Iterator.done; } this.prev = next; - return next.value; + return next; }; diff --git a/spec/dict.js b/spec/dict.js index 3f70e88..445717f 100644 --- a/spec/dict.js +++ b/spec/dict.js @@ -68,9 +68,9 @@ function shouldHaveTheUsualContent(dict) { expect(dict.get('c')).toBe(undefined); expect(dict.get('c', 30)).toBe(30); - expect(dict.keys()).toEqual(['a', 'b']); - expect(dict.values()).toEqual([10, 20]); - expect(dict.entries()).toEqual([['a', 10], ['b', 20]]); + expect(dict.keys().toArray()).toEqual(['a', 'b']); + expect(dict.values().toArray()).toEqual([10, 20]); + expect(dict.entries().toArray()).toEqual([['a', 10], ['b', 20]]); expect(dict.reduce(function (basis, value, key) { return basis + value; }, 0)).toEqual(30); diff --git a/spec/fast-set-spec.js b/spec/fast-set-spec.js index 2132f15..ab14a37 100644 --- a/spec/fast-set-spec.js +++ b/spec/fast-set-spec.js @@ -173,5 +173,6 @@ describe("Set", function () { }); }); }); + }); diff --git a/spec/iterator-spec.js b/spec/iterator-spec.js index b65b783..6a73f1c 100644 --- a/spec/iterator-spec.js +++ b/spec/iterator-spec.js @@ -1,711 +1,481 @@ var Iterator = require("../iterator"); +var Iterator_ = Iterator; // For referencing things on the constructor describe("Iterator", function () { - - shouldWorkWithConstructor(function withoutNew(iterable) { + describeIterator(function withoutNew(iterable) { return Iterator(iterable); }); - - shouldWorkWithConstructor(function withNew(iterable) { - return new Iterator(iterable); - }); - - describe("Iterator.cycle", function () { - - it("should work", function () { - var iterator = Iterator.cycle([1, 2, 3]); - for (var i = 0; i < 10; i++) { - expect(iterator.next()).toBe(1); - expect(iterator.next()).toBe(2); - expect(iterator.next()).toBe(3); - } - }); - - it("should work with specified number of times", function () { - var iterator = Iterator.cycle([1, 2, 3], 2); - for (var i = 0; i < 2; i++) { - expect(iterator.next()).toBe(1); - expect(iterator.next()).toBe(2); - expect(iterator.next()).toBe(3); - } - expect(function () { - iterator.next(); - }).toThrow(); - expect(function () { - iterator.next(); - }).toThrow(); - }); - - it("should work with specified 0 times", function () { - var iterator = Iterator.cycle([1, 2, 3], 0); - expect(function () { - iterator.next(); - }).toThrow(); - expect(function () { - iterator.next(); - }).toThrow(); - }); - - it("should work with specified -1 times", function () { - var iterator = Iterator.cycle([1, 2, 3], 0); - expect(function () { - iterator.next(); - }).toThrow(); - expect(function () { - iterator.next(); - }).toThrow(); - }); - - }); - - describe("Iterator.repeat", function () { - - it("should repeat a value indefinite times by default", function () { - var iterator = Iterator.repeat(1); - for (var i = 0; i < 10; i++) { - expect(iterator.next()).toEqual(1); - } - }); - - it("should repeat a value specified times", function () { - var iterator = Iterator.repeat(1, 3); - for (var i = 0; i < 3; i++) { - expect(iterator.next()).toEqual(1); - } - expect(function () { - iterator.next(); - }).toThrow(); - expect(function () { - iterator.next(); - }).toThrow(); - }); - - }); - - describe("Iterator.concat", function () { - it("should work", function () { - var iterator = Iterator.concat([ - Iterator([1, 2, 3]), - Iterator([4, 5, 6]), - Iterator([7, 8, 9]) - ]); - for (var i = 0; i < 9; i++) { - expect(iterator.next()).toEqual(i + 1); - } - expect(function () { - iterator.next(); - }).toThrow(); - expect(function () { - iterator.next(); - }).toThrow(); - }); - }); - - describe("Iterator.chain", function () { - it("should work", function () { - var iterator = Iterator.chain( - Iterator([1, 2, 3]), - Iterator([4, 5, 6]), - Iterator([7, 8, 9]) - ); - for (var i = 0; i < 9; i++) { - expect(iterator.next()).toEqual(i + 1); - } - expect(function () { - iterator.next(); - }).toThrow(); - expect(function () { - iterator.next(); - }).toThrow(); - }); - }); - - describe("Iterator.unzip", function () { - it("should work", function () { - var iterator = Iterator.unzip([ - Iterator([0, 'A', 'x']), - Iterator([1, 'B', 'y', 'I']), - Iterator([2, 'C']) - ]); - - expect(iterator.next()).toEqual([0, 1, 2]); - expect(iterator.next()).toEqual(['A', 'B', 'C']); - - expect(function () { - iterator.next(); - }).toThrow(); - expect(function () { - iterator.next(); - }).toThrow(); - }); - }); - - describe("Iterator.zip", function () { - it("should work", function () { - var iterator = Iterator.zip( - Iterator([0, 'A', 'x']), - Iterator([1, 'B', 'y', 'I']), - Iterator([2, 'C']) - ); - - expect(iterator.next()).toEqual([0, 1, 2]); - expect(iterator.next()).toEqual(['A', 'B', 'C']); - - expect(function () { - iterator.next(); - }).toThrow(); - expect(function () { - iterator.next(); - }).toThrow(); - }); - }); - - describe("Iterator.range", function () { - }); - - describe("Iterator.count", function () { - }); - + //describeIterator(function withNew(iterable) { + // return new Iterator(iterable); + //}); }); -function shouldWorkWithConstructor(Iterator) { - - function definiteIterator() { - return Iterator([1, 2, 3]); - } +function expectCommonIterator(iterator) { + expect(iterator.next()).toEqual({value: 1, index: 0, done: false}); + expect(iterator.next()).toEqual({value: 2, index: 1, done: false}); + expect(iterator.next()).toEqual({value: 3, index: 2, done: false}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); +} - function indefiniteIterator() { - var n = 0; - return Iterator(function () { - return n++; - }); - } +function describeIterator(Iterator) { - it("should iterate an array", function () { + it("iterates an array", function () { var iterator = Iterator([1, 2, 3]); - expect(iterator.next()).toEqual(1); - expect(iterator.next()).toEqual(2); - expect(iterator.next()).toEqual(3); - expect(function () { - iterator.next(); - }).toThrow(); - expect(function () { - iterator.next(); - }).toThrow(); + expectCommonIterator(iterator); }); - it("should iterate an sparse array", function () { - var array = []; - array[0] = 1; - array[100] = 2; - array[1000] = 3; - var iterator = Iterator(array); - expect(iterator.next()).toEqual(1); - expect(iterator.next()).toEqual(2); - expect(iterator.next()).toEqual(3); - expect(function () { - iterator.next(); - }).toThrow(); - expect(function () { - iterator.next(); - }).toThrow(); + it("iterates a sparse array", function () { + var iterator = Iterator([1,, 2,, 3]); + expect(iterator.next()).toEqual({value: 1, index: 0, done: false}); + expect(iterator.next()).toEqual({value: 2, index: 2, done: false}); + expect(iterator.next()).toEqual({value: 3, index: 4, done: false}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); }); - it("should iterate a string", function () { + it("iterates a string", function () { var iterator = Iterator("abc"); - expect(iterator.next()).toEqual("a"); - expect(iterator.next()).toEqual("b"); - expect(iterator.next()).toEqual("c"); - expect(function () { - iterator.next(); - }).toThrow(); - expect(function () { - iterator.next(); - }).toThrow(); + expect(iterator.next()).toEqual({value: "a", index: 0, done: false}); + expect(iterator.next()).toEqual({value: "b", index: 1, done: false}); + expect(iterator.next()).toEqual({value: "c", index: 2, done: false}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); }); - it("should gracefully fail to iterate null", function () { + it("fails to iterate null", function () { expect(function () { Iterator(null); }).toThrow(); }); - it("should gracefully fail to iterate undefined", function () { + it("fails to iterate undefined", function () { expect(function () { Iterator(); }).toThrow(); }); - it("should gracefully fail to iterate a number", function () { + it("fails to iterate a number", function () { expect(function () { Iterator(42); }).toThrow(); }); - it("should gracefully pass an existing iterator through", function () { - var iterator = Iterator([1, 2, 3]); - iterator = Iterator(iterator); - expect(iterator.next()).toEqual(1); - expect(iterator.next()).toEqual(2); - expect(iterator.next()).toEqual(3); - expect(function () { - iterator.next(); - }).toThrow(); - expect(function () { - iterator.next(); - }).toThrow(); + it("wraps an existing iterator", function () { + var iterator = Iterator(Iterator([1, 2, 3])); + expectCommonIterator(iterator); }); - it("should iterate an iterator", function () { + it("iterates an iterable", function () { var iterator = Iterator({ iterate: function () { return Iterator([1, 2, 3]); } }); - iterator = Iterator(iterator); - expect(iterator.next()).toEqual(1); - expect(iterator.next()).toEqual(2); - expect(iterator.next()).toEqual(3); - expect(function () { - iterator.next(); - }).toThrow(); - expect(function () { - iterator.next(); - }).toThrow(); + expectCommonIterator(iterator); }); - it("should iterate an iterable", function () { + it("wraps a duck iterator", function () { var n = 0; var iterator = Iterator({ next: function next() { if (++n > 3) { - throw new ReturnValue(); + return Iterator_.done; } else { - return n; + return new Iterator_.Iteration( + n, + n - 1 + ); } } }); - expect(iterator.next()).toEqual(1); - expect(iterator.next()).toEqual(2); - expect(iterator.next()).toEqual(3); - expect(function () { - iterator.next(); - }).toThrow(); - expect(function () { - iterator.next(); - }).toThrow(); + expectCommonIterator(iterator); }); - it("should create an iterator from a function", function () { + it("wraps a pure next function", function () { var n = 0; - var iterator = Iterator(function next() { + var iterator = Iterator(function () { if (++n > 3) { - throw new ReturnValue(); + return Iterator_.done; } else { - return n; + return new Iterator_.Iteration( + n, + n - 1 + ); } }); - expect(iterator.next()).toEqual(1); - expect(iterator.next()).toEqual(2); - expect(iterator.next()).toEqual(3); - expect(function () { - iterator.next(); - }).toThrow(); - expect(function () { - iterator.next(); - }).toThrow(); + expectCommonIterator(iterator); }); - describe("reduce", function () { - it("should work", function () { - var iterator = definiteIterator(); - var count = 0; - var result = iterator.reduce(function (result, value, key, object) { - expect(value).toBe(count + 1); - expect(key).toBe(count); - expect(object).toBe(iterator); - count++; - return value + 1; - }, 0); - expect(result).toBe(4); - }); - }); + describe("iterateMap", function () { - describe("forEach", function () { - it("should work", function () { - var iterator = definiteIterator(); - var count = 0; - iterator.forEach(function (value, key, object) { - expect(value).toBe(count + 1); - expect(key).toBe(count); - expect(object).toBe(iterator); - count++; + it("maps an iterator", function () { + var iterator = Iterator([1, 2, 3]).iterateMap(function (n, i) { + expect(i).toBe(n - 1); + return n * 2; }); - expect(count).toBe(3); + expect(iterator.next()).toEqual({value: 2, index: 0, done: false}); + expect(iterator.next()).toEqual({value: 4, index: 1, done: false}); + expect(iterator.next()).toEqual({value: 6, index: 2, done: false}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); }); - }); - describe("map", function () { - it("should work", function () { - var iterator = definiteIterator(); - var count = 0; - var result = iterator.map(function (value, key, object) { - expect(value).toBe(count + 1); - expect(key).toBe(count); - expect(object).toBe(iterator); - count++; - return "abc".charAt(key); - }); - expect(result).toEqual(["a", "b", "c"]); - expect(count).toBe(3); - }); }); - describe("filter", function () { - it("should work", function () { - var iterator = definiteIterator(); - var count = 0; - var result = iterator.filter(function (value, key, object) { - expect(value).toBe(count + 1); - expect(key).toBe(count); - expect(object).toBe(iterator); - count++; - return value === 2; + describe("iterateFilter", function () { + + it("maps an iterator", function () { + var iterator = Iterator([1, 2, 3]).iterateMap(function (n, i) { + expect(i).toBe(n - 1); + return n * 2; }); - expect(result).toEqual([2]); - expect(count).toBe(3); + expect(iterator.next()).toEqual({value: 2, index: 0, done: false}); + expect(iterator.next()).toEqual({value: 4, index: 1, done: false}); + expect(iterator.next()).toEqual({value: 6, index: 2, done: false}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); }); - }); - describe("every", function () { - it("should work", function () { - expect(Iterator([1, 2, 3]).every(function (n) { - return n < 10; - })).toBe(true); - expect(Iterator([1, 2, 3]).every(function (n) { - return n > 1; - })).toBe(false); - }); }); - describe("some", function () { - it("should work", function () { - expect(Iterator([1, 2, 3]).some(function (n) { - return n === 2; - })).toBe(true); - expect(Iterator([1, 2, 3]).some(function (n) { - return n > 10; - })).toBe(false); + describe("reduce", function () { + + it("reduces", function () { + expect(Iterator([1, 2, 3]).reduce(function (n, m) { + return n + m; + }, 0)).toBe(6); }); - }); - describe("any", function () { - [ - [[false, false], false], - [[false, true], true], - [[true, false], true], - [[true, true], true] - ].forEach(function (test) { - test = Iterator(test); - var input = test.next(); - var output = test.next(); - it("any of " + JSON.stringify(input) + " should be " + output, function () { - expect(Iterator(input).any()).toEqual(output); - }); + it("reduces without a basis", function () { + expect(Iterator([1, 2, 3]).reduce(function (n, m) { + return n + m; + })).toBe(6); }); - }); - describe("all", function () { - [ - [[false, false], false], - [[false, true], false], - [[true, false], false], - [[true, true], true] - ].forEach(function (test) { - test = Iterator(test); - var input = test.next(); - var output = test.next(); - it("all of " + JSON.stringify(input) + " should be " + output, function () { - expect(Iterator(input).all()).toEqual(output); - }); + it("fails to reduce an empty iteration without a basis", function () { + expect(function () { + Iterator([]).reduce(function () { + }); + }).toThrow(); }); - }); - describe("min", function () { - it("should work", function () { - expect(definiteIterator().min()).toBe(1); + it("reduces with a thisp", function () { + var o = {}; + expect(Iterator([1, 2, 3]).reduce(function (n, m, i) { + expect(i).toBe(m - 1); + expect(this).toBe(o); + return n + m; + }, 0, o)).toBe(6); }); + }); - describe("max", function () { - it("should work", function () { - expect(definiteIterator().max()).toBe(3); + describe("dropWhile", function () { + var iterator = new Iterator([-1, -2, -3, 1, 2, 3]) + .dropWhile(function (n) { + return n < 0; }); + expect(iterator.next()).toEqual({value: 1, index: 3, done: false}); + expect(iterator.next()).toEqual({value: 2, index: 4, done: false}); + expect(iterator.next()).toEqual({value: 3, index: 5, done: false}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); }); - describe("sum", function () { - it("should work", function () { - expect(definiteIterator().sum()).toBe(6); + describe("takeWhile", function () { + var iterator = new Iterator([1, 2, 3, 4, 5, 6]) + .takeWhile(function (n) { + return n < 4; + }); + expectCommonIterator(iterator); + }); + + describe("iterateFlatten", function () { + it("flattens iterators", function () { + var iterator = Iterator([ + Iterator([1, 2]), + Iterator([3, 4]), + Iterator([5, 6]) + ]).iterateFlatten(); + expect(iterator.next()).toEqual({value: 1, index: 0, done: false}); + expect(iterator.next()).toEqual({value: 2, index: 1, done: false}); + expect(iterator.next()).toEqual({value: 3, index: 0, done: false}); + expect(iterator.next()).toEqual({value: 4, index: 1, done: false}); + expect(iterator.next()).toEqual({value: 5, index: 0, done: false}); + expect(iterator.next()).toEqual({value: 6, index: 1, done: false}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + }); + }); + + describe("iterateZip", function () { + it("zips iterators", function () { + var iterator = Iterator([0, 'A', 'x']).iterateZip( + Iterator([1, 'B', 'y', 'I']), + Iterator([2, 'C']) + ); + expect(iterator.next()).toEqual({ + value: [0, 1, 2], + index: 0, done: false + }); + expect(iterator.next()).toEqual({ + value: ["A", "B", "C"], + index: 1, done: false + }); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); }); }); - describe("average", function () { - it("should work", function () { - expect(definiteIterator().average()).toBe(2); + describe("iterateUnzip", function () { + it("unzips iterators", function () { + var iterator = Iterator([ + Iterator([0, 'A', 'x']), + Iterator([1, 'B', 'y', 'I']), + Iterator([2, 'C']) + ]).iterateUnzip(); + expect(iterator.next()).toEqual({ + value: [0, 1, 2], + index: 0, done: false + }); + expect(iterator.next()).toEqual({ + value: ["A", "B", "C"], + index: 1, done: false + }); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); }); }); - describe("flatten", function () { - it("should work", function () { - expect( - Iterator([ - definiteIterator(), - definiteIterator(), - definiteIterator() - ]).flatten() - ).toEqual([ - 1, 2, 3, - 1, 2, 3, - 1, 2, 3 - ]); + describe("iterateEnumerate", function () { + it("should enumerate an array", function () { + var iterator = Iterator([1, 2, 3]).iterateEnumerate(); + expect(iterator.next()).toEqual({value: [0, 1], index: 0, done: false}); + expect(iterator.next()).toEqual({value: [1, 2], index: 1, done: false}); + expect(iterator.next()).toEqual({value: [2, 3], index: 2, done: false}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); }); }); - describe("zip", function () { - it("should work", function () { - var cardinals = definiteIterator().mapIterator(function (n) { - return n - 1; - }); - var ordinals = definiteIterator(); - expect(cardinals.zip(ordinals)).toEqual([ - [0, 1], - [1, 2], - [2, 3] - ]); + describe("iterateConcat", function () { + it("concats iterators", function () { + var iterator = Iterator([1, 2]).iterateConcat( + Iterator([3, 4]), + Iterator([5, 6]) + ); + expect(iterator.next()).toEqual({value: 1, index: 0, done: false}); + expect(iterator.next()).toEqual({value: 2, index: 1, done: false}); + expect(iterator.next()).toEqual({value: 3, index: 0, done: false}); + expect(iterator.next()).toEqual({value: 4, index: 1, done: false}); + expect(iterator.next()).toEqual({value: 5, index: 0, done: false}); + expect(iterator.next()).toEqual({value: 6, index: 1, done: false}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); }); }); - describe("enumerate", function () { +} - it("should work with default start", function () { - var cardinals = definiteIterator(); - expect(cardinals.enumerate()).toEqual([ - [0, 1], - [1, 2], - [2, 3] - ]); - }); - - it("should work with given start", function () { - var cardinals = definiteIterator(); - expect(cardinals.enumerate(1)).toEqual([ - [1, 1], - [2, 2], - [3, 3] - ]); - }); +describe("Iterator.cycle", function () { + it("cycles an array", function () { + var iterator = Iterator.cycle([1, 2, 3]); + expect(iterator.next()).toEqual({value: 1, index: 0, done: false}); + expect(iterator.next()).toEqual({value: 2, index: 1, done: false}); + expect(iterator.next()).toEqual({value: 3, index: 2, done: false}); + expect(iterator.next()).toEqual({value: 1, index: 0, done: false}); + expect(iterator.next()).toEqual({value: 2, index: 1, done: false}); + expect(iterator.next()).toEqual({value: 3, index: 2, done: false}); }); - describe("sorted", function () { - it("should work", function () { - expect(Iterator([5, 2, 4, 1, 3]).sorted()).toEqual([1, 2, 3, 4, 5]); - }); + it("cycles an array twice", function () { + var iterator = Iterator.cycle([1, 2], 2); + expect(iterator.next()).toEqual({value: 1, index: 0, done: false}); + expect(iterator.next()).toEqual({value: 2, index: 1, done: false}); + expect(iterator.next()).toEqual({value: 1, index: 0, done: false}); + expect(iterator.next()).toEqual({value: 2, index: 1, done: false}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); }); - describe("group", function () { - it("should work", function () { - expect(Iterator([5, 2, 4, 1, 3]).group(function (n) { - return n % 2 === 0; - })).toEqual([ - [false, [5, 1, 3]], - [true, [2, 4]] - ]); - }); + it("cycles zero times", function () { + var iterator = Iterator.cycle([1, 2, 3], 0); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); }); - describe("reversed", function () { - it("should work", function () { - expect(Iterator([5, 2, 4, 1, 3]).reversed()).toEqual([3, 1, 4, 2, 5]); - }); +}); + +describe("Iterator.concat", function () { + + it("concats iterators", function () { + var iterator = Iterator.concat( + Iterator([1, 2]), + Iterator([3, 4]), + Iterator([5, 6]) + ); + expect(iterator.next()).toEqual({value: 1, index: 0, done: false}); + expect(iterator.next()).toEqual({value: 2, index: 1, done: false}); + expect(iterator.next()).toEqual({value: 3, index: 0, done: false}); + expect(iterator.next()).toEqual({value: 4, index: 1, done: false}); + expect(iterator.next()).toEqual({value: 5, index: 0, done: false}); + expect(iterator.next()).toEqual({value: 6, index: 1, done: false}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); }); - describe("toArray", function () { - it("should work", function () { - expect(Iterator([5, 2, 4, 1, 3]).toArray()).toEqual([5, 2, 4, 1, 3]); - }); +}); + +describe("Iterator.flatten", function () { + + it("flattens iterators", function () { + var iterator = Iterator.flatten([ + Iterator([1, 2]), + Iterator([3, 4]), + Iterator([5, 6]) + ]); + expect(iterator.next()).toEqual({value: 1, index: 0, done: false}); + expect(iterator.next()).toEqual({value: 2, index: 1, done: false}); + expect(iterator.next()).toEqual({value: 3, index: 0, done: false}); + expect(iterator.next()).toEqual({value: 4, index: 1, done: false}); + expect(iterator.next()).toEqual({value: 5, index: 0, done: false}); + expect(iterator.next()).toEqual({value: 6, index: 1, done: false}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); }); - describe("toObject", function () { - it("should work", function () { - expect(Iterator("AB").toObject()).toEqual({ - 0: "A", - 1: "B" - }); +}); + +describe("Iterator.unzip", function () { + + it("unzips iterators", function () { + var iterator = Iterator.unzip([ + Iterator([0, 'A', 'x']), + Iterator([1, 'B', 'y', 'I']), + Iterator([2, 'C']) + ]); + expect(iterator.next()).toEqual({ + value: [0, 1, 2], + index: 0, done: false }); + expect(iterator.next()).toEqual({ + value: ["A", "B", "C"], + index: 1, done: false + }); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); }); - describe("mapIterator", function () { +}); - it("should work", function () { - var iterator = indefiniteIterator() - .mapIterator(function (n, i, o) { - return n * 2; - }); - expect(iterator.next()).toBe(0); - expect(iterator.next()).toBe(2); - expect(iterator.next()).toBe(4); - expect(iterator.next()).toBe(6); - }); +describe("Iterator.zip", function () { - it("should pass the correct arguments to the callback", function () { - var iterator = indefiniteIterator() - var result = iterator.mapIterator(function (n, i, o) { - expect(i).toBe(n); - expect(o).toBe(iterator); - return n * 2; - }); - result.next(); - result.next(); - result.next(); - result.next(); + it("zips iterators", function () { + var iterator = Iterator.zip( + Iterator([0, 'A', 'x']), + Iterator([1, 'B', 'y', 'I']), + Iterator([2, 'C']) + ); + expect(iterator.next()).toEqual({ + value: [0, 1, 2], + index: 0, done: false }); - + expect(iterator.next()).toEqual({ + value: ["A", "B", "C"], + index: 1, done: false + }); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); }); - describe("filterIterator", function () { +}); - it("should work", function () { - var iterator = indefiniteIterator() - .filterIterator(function (n, i, o) { - expect(i).toBe(n); - //expect(o).toBe(iterator); - return n % 2 === 0; - }); - expect(iterator.next()).toBe(0); - expect(iterator.next()).toBe(2); - expect(iterator.next()).toBe(4); - expect(iterator.next()).toBe(6); - }); +describe("Iterator.range", function () { - it("should pass the correct arguments to the callback", function () { - var iterator = indefiniteIterator() - var result = iterator.filterIterator(function (n, i, o) { - expect(i).toBe(n); - expect(o).toBe(iterator); - return n * 2; - }); - result.next(); - result.next(); - result.next(); - result.next(); - }); + it("iterates a range", function () { + var iterator = new Iterator.range(3); + expect(iterator.next()).toEqual({value: 0, index: 0, done: false}); + expect(iterator.next()).toEqual({value: 1, index: 1, done: false}); + expect(iterator.next()).toEqual({value: 2, index: 2, done: false}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + }); + it("iterates an offset range", function () { + var iterator = new Iterator.range(1, 4); + expectCommonIterator(iterator); }); - describe("concat", function () { - it("should work", function () { - var iterator = definiteIterator().concat(definiteIterator()); - expect(iterator.next()).toBe(1); - expect(iterator.next()).toBe(2); - expect(iterator.next()).toBe(3); - expect(iterator.next()).toBe(1); - expect(iterator.next()).toBe(2); - expect(iterator.next()).toBe(3); - expect(function () { - iterator.next(); - }).toThrow(); - }); + it("iterates an offset, strided range", function () { + var iterator = new Iterator.range(0, 5, 2); + expect(iterator.next()).toEqual({value: 0, index: 0, done: false}); + expect(iterator.next()).toEqual({value: 2, index: 1, done: false}); + expect(iterator.next()).toEqual({value: 4, index: 2, done: false}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); }); - describe("dropWhile", function () { +}); - it("should work", function () { - var iterator = indefiniteIterator() - .dropWhile(function (n) { - return n < 10; - }); - expect(iterator.next()).toBe(10); - expect(iterator.next()).toBe(11); - expect(iterator.next()).toBe(12); - }); +describe("Iterator.count", function () { - it("should pass the correct arguments to the callback", function () { - var iterator = indefiniteIterator() - var result = iterator.dropWhile(function (n, i, o) { - expect(i).toBe(n); - expect(o).toBe(iterator); - }); - result.next(); - result.next(); - result.next(); - }); + it("iterates an open range", function () { + var iterator = new Iterator.count(); + expect(iterator.next()).toEqual({value: 0, index: 0, done: false}); + expect(iterator.next()).toEqual({value: 1, index: 1, done: false}); + expect(iterator.next()).toEqual({value: 2, index: 2, done: false}); + expect(iterator.next()).toEqual({value: 3, index: 3, done: false}); + }); + it("iterates an open range starting with one", function () { + var iterator = new Iterator.count(1); + expect(iterator.next()).toEqual({value: 1, index: 0, done: false}); + expect(iterator.next()).toEqual({value: 2, index: 1, done: false}); + expect(iterator.next()).toEqual({value: 3, index: 2, done: false}); + expect(iterator.next()).toEqual({value: 4, index: 3, done: false}); }); - describe("takeWhile", function () { + it("iterates an open range with stride", function () { + var iterator = new Iterator.count(0, 2); + expect(iterator.next()).toEqual({value: 0, index: 0, done: false}); + expect(iterator.next()).toEqual({value: 2, index: 1, done: false}); + expect(iterator.next()).toEqual({value: 4, index: 2, done: false}); + expect(iterator.next()).toEqual({value: 6, index: 3, done: false}); + }); - it("should work", function () { - var iterator = indefiniteIterator() - .takeWhile(function (n) { - return n < 3; - }); - expect(iterator.next()).toBe(0); - expect(iterator.next()).toBe(1); - expect(iterator.next()).toBe(2); - expect(function () { - iterator.next(); - }).toThrow(); - }); +}); - it("should pass the correct arguments to the callback", function () { - var iterator = indefiniteIterator() - var result = iterator.takeWhile(function (n, i, o) { - expect(i).toBe(n); - expect(o).toBe(iterator); - return n < 3; - }); - result.next(); - result.next(); - result.next(); - }); +describe("Iterator.repeat", function () { + it("repeats a value indefinitely", function () { + var iterator = Iterator.repeat(1); + for (var index = 0; index < 10; index++) { + expect(iterator.next()).toEqual({value: 1, index: index, done: false}); + } }); - describe("zipIterator", function () { + it("repeats a value some times", function () { + var iterator = Iterator.repeat(1, 3); + expect(iterator.next()).toEqual({value: 1, index: 0, done: false}); + expect(iterator.next()).toEqual({value: 1, index: 1, done: false}); + expect(iterator.next()).toEqual({value: 1, index: 2, done: false}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + }); - it("should work", function () { - var cardinals = indefiniteIterator(); - var ordinals = indefiniteIterator().mapIterator(function (n) { - return n + 1; - }); - var iterator = cardinals.zipIterator(ordinals); - expect(iterator.next()).toEqual([0, 1]); - expect(iterator.next()).toEqual([1, 2]); - expect(iterator.next()).toEqual([2, 3]); - }); +}); - it("should work, even for crazy people", function () { - var cardinals = indefiniteIterator(); - var iterator = cardinals.zipIterator(cardinals, cardinals); - expect(iterator.next()).toEqual([0, 1, 2]); - expect(iterator.next()).toEqual([3, 4, 5]); - expect(iterator.next()).toEqual([6, 7, 8]); - }); - }); +describe("Iterator.enumerate", function () { - describe("enumerateIterator", function () { - it("should work", function () { - var ordinals = indefiniteIterator().mapIterator(function (n) { - return n + 1; - }); - var iterator = ordinals.enumerateIterator(); - expect(iterator.next()).toEqual([0, 1]); - expect(iterator.next()).toEqual([1, 2]); - expect(iterator.next()).toEqual([2, 3]); - }); + it("should enumerate an array", function () { + var iterator = Iterator.enumerate([1, 2, 3]); + expect(iterator.next()).toEqual({value: [0, 1], index: 0, done: false}); + expect(iterator.next()).toEqual({value: [1, 2], index: 1, done: false}); + expect(iterator.next()).toEqual({value: [2, 3], index: 2, done: false}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); }); -} +}); From 10f99b1bb503502d8da8cc08ed8db75c563b4853 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Mon, 3 Feb 2014 00:53:51 -0800 Subject: [PATCH 22/83] Update Deque for new observers --- deque.js | 14 ++++++++------ spec/deque-spec.js | 4 ++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/deque.js b/deque.js index 9e6e93f..f0e363d 100644 --- a/deque.js +++ b/deque.js @@ -4,7 +4,8 @@ require("./shim-object"); var GenericCollection = require("./generic-collection"); var GenericOrder = require("./generic-order"); var GenericOrder = require("./generic-order"); -var RangeChanges = require("./listen/range-changes"); +var ObservableRange = require("./observable-range"); +var ObservableObject = require("./observable-object"); // by Petka Antonov // https://github.com/petkaantonov/deque/blob/master/js/deque.js @@ -27,7 +28,8 @@ function Deque(values, capacity) { Object.addEach(Deque.prototype, GenericCollection.prototype); Object.addEach(Deque.prototype, GenericOrder.prototype); -Object.addEach(Deque.prototype, RangeChanges.prototype); +Object.addEach(Deque.prototype, ObservableRange.prototype); +Object.addEach(Deque.prototype, ObservableObject.prototype); Deque.prototype.maxCapacity = (1 << 30) | 0; Deque.prototype.minCapacity = 16; @@ -47,7 +49,7 @@ Deque.prototype.push = function (value /* or ...values */) { if (this.dispatchesRangeChanges) { var plus = Array.prototype.slice.call(arguments); var minus = []; - this.dispatchBeforeRangeChange(plus, minus, length); + this.dispatchRangeWillChange(plus, minus, length); } if (argsLength > 1) { @@ -93,7 +95,7 @@ Deque.prototype.pop = function () { var result = this[index]; if (this.dispatchesRangeChanges) { - this.dispatchBeforeRangeChange([], [result], length - 1); + this.dispatchRangeWillChange([], [result], length - 1); } this[index] = void 0; @@ -112,7 +114,7 @@ Deque.prototype.shift = function () { var result = this[front]; if (this.dispatchesRangeChanges) { - this.dispatchBeforeRangeChange([], [result], 0); + this.dispatchRangeWillChange([], [result], 0); } this[front] = void 0; @@ -134,7 +136,7 @@ Deque.prototype.unshift = function (value /*, ...values */) { if (this.dispatchesRangeChanges) { var plus = Array.prototype.slice.call(arguments); var minus = []; - this.dispatchBeforeRangeChange(plus, minus, 0); + this.dispatchRangeWillChange(plus, minus, 0); } if (argsLength > 1) { diff --git a/spec/deque-spec.js b/spec/deque-spec.js index f410ba0..ca5775d 100644 --- a/spec/deque-spec.js +++ b/spec/deque-spec.js @@ -61,13 +61,13 @@ describe("Deque", function () { spy(plus, minus, value); // ignore last arg }; var deque = Deque(); - deque.addRangeChangeListener(handler); + var observer = deque.observeRangeChange(handler); deque.push(1); deque.push(2, 3); deque.pop(); deque.shift(); deque.unshift(4, 5); - deque.removeRangeChangeListener(handler); + observer.cancel(); deque.shift(); expect(spy.argsForCall).toEqual([ [[1], [], 0], From 9e15bec04ab40a03c0018f30cb4fd568e30af06d Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Mon, 3 Feb 2014 11:17:08 -0800 Subject: [PATCH 23/83] Introduce iterator recount Also, factor this facility out of `filter`, so that recounting is an orthogonal concern. Recounting enforces sequential indexes on a wrapped iterator. --- iterator.js | 32 +++++++++++++++++++++++--------- spec/iterator-spec.js | 39 +++++++++++++++++++++++++++++++++++---- 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/iterator.js b/iterator.js index 8fe4899..ff2f5f1 100644 --- a/iterator.js +++ b/iterator.js @@ -112,27 +112,20 @@ function FilterIterator(iterator, callback, thisp) { this.iterator = iterator; this.callback = callback; this.thisp = thisp; - this.index = 0; } FilterIterator.prototype.next = function () { var iteration; while (true) { iteration = this.iterator.next(); - if (iteration.done) { - return iteration; - } else if (this.callback.call( + if (iteration.done || this.callback.call( this.thisp, iteration.value, iteration.index, this.iteration )) { - return new Iteration( - iteration.value, - this.index++ - ); + return iteration; } - } }; @@ -256,6 +249,27 @@ Iterator.prototype.iterateFlatten = function () { return Iterator.flatten(this); }; +Iterator.prototype.recount = function (start) { + return new Iterator(new RecountIterator(this, start)); +}; + +function RecountIterator(iterator, start) { + this.iterator = iterator; + this.index = start || 0; +} + +RecountIterator.prototype.next = function () { + var iteration = this.iterator.next(); + if (iteration.done) { + return iteration; + } else { + return new Iteration( + iteration.value, + this.index++ + ); + } +}; + // creates an iterator for Array and String function IndexIterator(iterable, start, stop, step) { if (step == null) { diff --git a/spec/iterator-spec.js b/spec/iterator-spec.js index 6a73f1c..d6a5a7a 100644 --- a/spec/iterator-spec.js +++ b/spec/iterator-spec.js @@ -126,14 +126,45 @@ function describeIterator(Iterator) { describe("iterateFilter", function () { - it("maps an iterator", function () { - var iterator = Iterator([1, 2, 3]).iterateMap(function (n, i) { + it("filters an iterator", function () { + var iterator = Iterator([1, 2, 3]).iterateFilter(function (n, i) { expect(i).toBe(n - 1); - return n * 2; + return n % 2 === 0; }); + expect(iterator.next()).toEqual({value: 2, index: 1, done: false}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + }); + + }); + + describe("recount", function () { + + it("recounts a filtered iterator", function () { + var iterator = Iterator([1, 2, 3, 4]).iterateFilter(function (n, i) { + expect(i).toBe(n - 1); + return n % 2 === 0; + }).recount(); expect(iterator.next()).toEqual({value: 2, index: 0, done: false}); expect(iterator.next()).toEqual({value: 4, index: 1, done: false}); - expect(iterator.next()).toEqual({value: 6, index: 2, done: false}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + }); + + it("recounts a sparse array iterator", function () { + var iterator = Iterator([1,, 2,, 3]).recount(); + expect(iterator.next()).toEqual({value: 1, index: 0, done: false}); + expect(iterator.next()).toEqual({value: 2, index: 1, done: false}); + expect(iterator.next()).toEqual({value: 3, index: 2, done: false}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + }); + + it("recounts from one", function () { + var iterator = Iterator([1,, 2,, 3]).recount(1); + expect(iterator.next()).toEqual({value: 1, index: 1, done: false}); + expect(iterator.next()).toEqual({value: 2, index: 2, done: false}); + expect(iterator.next()).toEqual({value: 3, index: 3, done: false}); expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); }); From 24049a92768a79f9d675477c2dc0e6af64a28929 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Mon, 3 Feb 2014 11:19:11 -0800 Subject: [PATCH 24/83] Revise iterator documentation --- README.md | 124 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 108 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 3f442d6..19fbc44 100644 --- a/README.md +++ b/README.md @@ -214,6 +214,7 @@ var Iterator = require("collections/iterator"); A wrapper for any iterable that implements `iterate` or iterator the implements `next`, providing a rich lazy traversal interface. + ### Array ```javascript @@ -927,62 +928,153 @@ the representation to an array instead, they can be `array.push` and (SortedSet) -### Iterator +### Iterator(iterable, start, stop, step) + +*Redesigned in version 2.* + +Creates an iterator from an iterable. Iterables include: + +- instances of Iterator, in which case the “iterable” will be simply returned + instead of a new iterator. +- objects with an `iterate(start, stop, step)` method, in which case the + optional `start`, `stop`, and `step` arguments are forwarded. Collections + implement this iterface, including Array. +- objects with a `next()` method, which is to say existing iterators, + though this iterator will only depend on the `next()` method and provide + the much richer Iterator interface using it. +- `next()` functions. + +Iterators are defined by the upcoming version of ECMAScript. The `next()` method +returns what I am calling an “iteration”, an object that has a `value` property +and an optional `done` flag. When `done` is true, the iteration signals the end +of the iterator and may be accompanied by a “return value” instead of a “yield +value”. + +In addition, Iterator produces iterations with an optional `index` property. The +indexes produced by an array are the positions of each value, which are +non-contiguous for sparse arrays. + +*In version 1, iterators followed the old, Pythonic protocol established in +Mozilla’s SpiderMonkey, where iterators yielded values directly and threw +`StopIteration` to terminate.* #### dropWhile(callback(value, index, iterator), thisp) +Returns an iterator that begins with the first iteration from this iterator to +fail the given test. + #### takeWhile(callback(value, index, iterator), thisp) -#### mapIterator(callback(value, index, iterator)) +Returns an iterator that ends before the first iteration from this iterator to +fail the given test. + +#### iterateMap(callback(value, index, iterator)) + +*Renamed in version 2 from `mapIterator` in version 1.* Returns an iterator for a mapping on the source values. Values are consumed on demand. -#### filterIterator(callback(value, index, iterator)) +#### iterateFilter(callback(value, index, iterator)) + +*Renamed in version 2 from `filterIterator` in version 1.* Returns an iterator for those values from the source that pass the given guard. Values are consumed on demand. -#### zipIterator(...iterables) +#### iterateZip(...iterables) + +*Introduced in version 2* Returns an iterator that incrementally combines the respective -values of the given iterations. +values of the given iterations, first including itself. + +#### iterateUnzip() + +*Introduced in version 2* + +Assuming that this is an iterator that produces iterables, produces an iteration +of the reslective values from each iterable. + +#### iterateConcat(...iterables) -#### enumerateIterator(start = 0) +*Renamed in version 2 from `concat` in version 1.* + +Creates an iteration that first produces all the values from this iteration, +then from each subsequent iterable in order. + +#### iterateFlatten() + +*Renamed in version 2 from `flattenIterator` in version 1.* + +Assuming that this is an iterator that produces iterables, creates an iterator +that yields all of the values from each of those iterables in order. + +#### iterateEnumerate(start = 0) + +*Renamed in version 2 from `enumerateIterator` in version 1.* Returns an iterator that provides [index, value] pairs from the source iteration. +#### recount(start=0) + +*Introduced in version 2.* + +Produces a new version of this iteration where the indexes are recounted. The +indexes for sparse arrays are not contiguous, as well as the iterators produced +by `filter`, `cycle`, `flatten`, and `concat` that pass iterations through +without alteration from various sources. `recount` smoothes out the sequence. + ### Iterator utilities -#### cycle(iterable, times) +#### cycle(iterable, times=Infinity) + +Produces an iterator that will cycle through the values from a given iterable +some number of times, indefinitely by default. The given iterable must be able +to produce the sequence of values each time it is iterated, which is to say, not +an iterator, but any collection would do. -#### concat(iterables) +#### unzip(iterables) -#### transpose(iterables) +Transposes a two dimensional iterable, which is to say, produces an iterator +that will yield a tuple of the respective values from each of the given +iterables. #### zip(...iterables) -Variadic transpose. +Transposes a two dimensional iterable, which is to say, produces an iterator +that will yield a tuple of the respective values from each of the given +iterables. `zip` differs from `unzip` only in that the arguments are variadic. + +#### flatten(iterables) + +Returns an iterator that will produce all the values from each of the given +iterators in order. -#### chain(...iterables) +#### concat(...iterables) -Variadic concat. +Returns an iterator that will produce all the values from each of the given +iterators in order. Differs from `flatten` only in that the arguments are +variadic. -#### range(start, stop, step) +#### range(length) -Iterates from start to stop by step. +Iterates `length` numbers starting from 0. + +#### range(start, stop, step=1) + +Iterates numbers from start to stop by step. #### count(start, step) -Iterates from start by step, indefinitely. +Iterates numbers from start by step, indefinitely. #### repeat(value, times) Repeats the given value either finite times or indefinitely. - ## Change Listeners All collections support change listeners. There are three types of From 7e2c9b66b7fcdd031e8aa632c301d52acbbf9ced Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Mon, 3 Feb 2014 13:18:34 -0800 Subject: [PATCH 25/83] Add change observer documentation --- README.md | 307 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 307 insertions(+) diff --git a/README.md b/README.md index 279fcf2..196d997 100644 --- a/README.md +++ b/README.md @@ -996,9 +996,316 @@ Iterates from start by step, indefinitely. Repeats the given value either finite times or indefinitely. +## Change Observers + +*Introduced in Version 2.* + +All collections support change observers. There are three types of changes. +Property changes, range changes, and map changes. Whether a collection supports +a kind of change can be inferred by the existence of an `observe*` method +appropriate to the kind of change, `observeRangeChange` for example. + +#### Observers + +The `observe*` methods all return “observer” objects with some overlapping +interface. Most importantly, an “observer” has a `cancel()` method that will +remove the observer from the queue of observers for its corresponding object and +property name. To reduce garbage collection churn, the observer may be reused, +but if the observer is canceled during change dispatch, it will not be recycled +until all changes have been handled. Also, if an observer is cancelled during +change dispatch, it will be passed over. If an observer is created during change +dispatch, it will also be passed over, to be informed of any subsequent changes. + +The observer will have a `handlerMethodName` property, based on a convention +that take into account all the parameters of the change observer and what +methods are available on the handler, such as `handleFooPropertyWillChange` or +simply null if the handler is a function. This method name can be overridden if +you need to break from convention. + +The observer will also have a `dispatch` method that you can use to manually +force a change notification. + +The observer will have a `note` property, as provided as an argument to the +observe method. This value is not used by the change observer system, but is +left for the user, for example to provide helpful information for inspecting why +the observer exists and what systems it participates in. + +The observer will have a `childObserver` property. Handlers have the option of +returning an observer, if that observer needs to be canceled when this observer +notices a change. This facility allows observers to “stack”. + +All kinds of changes have a `get*ChangeObservers` method that will return an +array of change observers. This function will consistently return the same array +for the same arguments, and the content of the array is itself observable. + +#### Handlers + +A handler may be an object with a handler method or a function. Either way, the +change observer will dispatch an argument pattern to the observer including both +the new and old values associated with the change and other parameters that +allow generic handlers to multiplex changes from multiple sources. See the +specific change observer documentation for details about the argument pattern +and handler method name convention. + +Again, a handler has the option of returning an observer. This observer will be +canceled if there is a subsequent change. So for example, if you are observing +the "children" property of the "root" property of a tree, you would be able to +stack the "children" property observer on top of the "root" property observer, +ensuring that the children property observer does not linger on old roots. + +If a handler throws an exception, it will not corrupt the state of the change +notification system, but it may corrupt the state of the observed object and the +assuptions of the rest of the program. Such errors are annotated by the change +dispatch system to increase awareness that all such errors are irrecoverable +programmer errors. + +#### Capture + +All observers support “change” and “will change” (“capture”) phases. Both phases +receive both the old and new values, but in the capture phase, the direct +interrogation of the object being observed will show that the change has not +taken effect, though change observers do not provide a facility for preventing a +change and throwing exceptions can corrupt the state of involved collections. +All “will change” methods exist to increase the readability of the program but +simply forward a true “capture” argument to the corresponding “change” method. +For example, `map.observeMapWillChange(handler)` just calls +`map.observeMapChange(handler, null, null, true)`, eliding the `name` and `note` +arguments not provided. + +### Property Changes + +The `observable-object` module provides facilities for observing changes to +properties on arbitrary objects, as well as a mix-in prototype that allows any +collection to support the property change observer interface directly. The +`observable-array` module alters the `Array` in this context to support the +property observer interface for its `"length"` property and indexed properties +by number, as long as those properties are altered by a method of the array +(which is to say, *caveat emptor: direct assignment to a property of an array is +not observable*). This shim does not introduce any overhead to arrays that are +not observed. + +```javascript +var ObservableObject = require("collections/observable-object"); +ObservableObject.observePropertyChange(object, name, handler, note, capture); +ObservableObject.observePropertyWillChange(object, name, handler, note, capture); +ObservableObject.dispatchPropertyChange(object, plus, minus); +ObservableObject.dispatchPropertyWillChange(object, plus, minus); +ObservableObject.getPropertyChangeObservers(object, name, capture) +ObservableObject.getPropertyWillChangeObservers(object, name, capture) +ObservableObject.makePropertyObservable(object, name); +ObservableObject.preventPropertyObserver(object, name); +``` + +All of these methods delegate to methods of the same name on an object if one +exists, making it possible to use these on arbitrary objects as well as objects +with custom property observer behavior. The property change observer interface +can be imbued on arbitrary objects. + +```javascript +Object.addEach(Constructor.prototype, ObservableObject.prototype); +var object = new Constructor(); + +object.observePropertyChange(name, handler, note, capture); +object.observePropertyWillChange(name, handler, note); +object.dispatchPropertyChange(plus, minus, capture); +object.dispatchPropertyWillChange(plus, minus); +object.getPropertyChangeObservers(name, capture) +object.getPropertyWillChangeObservers(name) +object.makePropertyObservable(name); +object.preventPropertyObserver(name); +``` + +`observePropertyChange` and `observePropertyWillChange` accept a property +`name` and a `handler` and returns an `observer`. + +#### Handlers + +The arguments to a property change handler are: + +- `plus`: the new value +- `minus`: the old value +- `name` (`observer.propertyName`, the `name` argument to + `observePropertyChange`) +- `object` (`observer.object`, the `object` given to `observePropertyChange`) +- `this` is the `handler` or undefined if the handler is a callable. + +The prefereed handler method name for a property change observer is composed: + +- `"handle"` +- `name`, with the first character capitalized +- `"Property"` +- `"Will"` if `capture` +- `"Change"` + +*The specific handler method name differs from those constructed by Version 1, +in that it includes the term, `"Property"`. Thus, all observer handler method +names now receive a complete description of the kind of change, at the expense +of some verbosity.* + +If this method does not exist, the method name falls back to the generic without +the property name: + +- `"handle"` +- `"Property"` +- `"Will"` if `capture` +- `"Change"` + +Otherwise, the handler must be callable, implementing `handler.call(this, plus, +minus, name, object)`, but not necessarily a function. + +#### Observers + +A property change observer has properties in addition to those common to all +observers. + +- `propertyName` +- `value` the last value dispatched. This will be used if `minus` is not given + when a change is dispatched and is otherwise is useful for inspecting + observers. + +#### Observability + +Property change observers use various means to make properties observable. In +general, they install a “thunk” property on the object that intercepts `get` and +`set` calls. A thunk will never be installed over an existing thunk. + +Observers take great care to do what makes sense with the underlying property +descriptor. For example, different kinds of thunks are installed for descriptors +with `get` and `set` methods than those with a simple `value`. If a property is +read-only, either indicated by `writable` being false or `get` being provided +without a matching `set`, no thunk is installed at all. + +If a property is ostensibly immutable, for lack of a `set` method, but the value +returned by `get` does in fact change in response to exogenous changes, those +changes may be rigged to dispatch a property change manually, using one of the +above `dispatch` methods. + +To avoid installing a thunk on every instance of particular constructor, +`makePropertyObservable` can be applied to a property of a prototype. To avoid +installing a thunk on a property at all, `preventPropertyObserver` can be +applied to either an instance or a prototype. + +Properties of an `Array` cannot be observed with thunks, so the +`observable-array` module adds methods to the Array prototype that allow it to +be transformed into an observed array on demand. The transformation involves +replacing all the methods that modify the content of the array with versions +that report the changes. The observable array interface is installed either by +subverting the prototype of the instance, or by redefining these methods +directly onto the instance if the prototype is not mutable. + +### Range Changes + +Many collections represent a contiguous range of values in a fixed order. For +these collections, range change observation is available. + +- `Array` with `require("collections/observable-array")` +- `List`† +- `Deque` +- `Set`† +- `SortedSet` +- `SortedArray` +- `SortedArraySet` +- `Heap` + +*†Note that with `List` and `Set`, observing range changes often +nullifies any performance improvment that might be gained using them instead of +an array, deque, or array-backed set.* + +`SortedSet` can grow to absurd proportions and still quickly dispatch range +change notifications at any position, owing to an algorithim that can +incrementally track the index of each node in time proportional to the logarithm +of the size of the collection. + +The `observe-range-changes` module exports a **mixin** that provides the range +change interface for a collection. + +```javascript +collection.observeRangeChange(handler, name, note, capture) +collection.observeRangeWillChange(handler, name, note) +collection.dispatchRangeChange(plus, minus, index, capture) +collection.dispatchRangeWillChange(plus, minus, index) +collection.getRangeChangeObservers(capture) +collection.getRangeWillChangeObservers() +collection.makeRangeChangeObservable() +``` + +The `name` is optional and only affects the handler method name computation. +The convention for the name of a range change handler method name is: + +- `"handle"` +- `name` with the first character capitalized, if given, and only if the + resulting method name is available on the handler. +- `"Range"` +- `"Will"` if `capture` +- `"Change"` + +The arguments of a range change are: + +- `plus`: values added at `index` +- `minus`: values removed at `index` before `plus` was added +- `index` +- `collection` + +The `makeRangeChangeObservable` method is overridable if a collection needs to +perform some operations apart from setting `dispatchesRangeChanges` in order to +become observable. For example, a `Set` has to establish observers on its own +`order` storage list. + +### Map Changes + +*Note: map change observers are very different than Version 1 map change +listeners.* + +Many collections represent a mapping from keys to values, irrespective of order. +For most of these collections, map change observation is available. + +- `Array` with `require("collections/observable-array")` +- `Map` +- `FastMap` +- `LruMap` +- `SortedMap` +- `SortedArrayMap` +- `Dict` +- `Heap` only for key 0 + +```javascript +collection.observeMapChange(handler, name, note, capture) +collection.observeMapWillChange(handler, name, note) +collection.dispatchMapChange(plus, minus, index, capture) +collection.dispatchMapWillChange(plus, minus, index) +collection.getMapChangeObservers(capture) +collection.getMapWillChangeObservers() +collection.makeMapChangeObservable() +``` + +The `name` is optional and only affects the handler method name computation. +The convention for the name of a range change handler method name is: + +- `"handle"` +- `name` with the first character capitalized, if given, and only if the + resulting method name is available on the handler. +- `"Map"` +- `"Will"` if `capture` +- `"Change"` + +The arguments of a range change are: + +- `plus`: the new value +- `minus`: the old value +- `key` +- `type`: one of `"create"`, `"update"`, or `"delete"` +- `collection` + +The `makeMapChangeObservable` method is overridable if a collection needs to +perform some operations apart from setting `dispatchesMapChanges` in order to +become observable. + ## Change Listeners +*The change listener interface exists in Version 1, but has been replaced with +Change Observers in Version 2.* + All collections support change listeners. There are three types of changes. Property changes, map changes, and range changes. From 3a2cbf1437bac127e7cd889f9b6d38aeedf9200a Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Mon, 3 Feb 2014 15:03:23 -0800 Subject: [PATCH 26/83] Remove script form --- README.md | 12 ++- collections.cat.js | 188 --------------------------------------------- collections.min.js | 114 --------------------------- 3 files changed, 5 insertions(+), 309 deletions(-) delete mode 100644 collections.cat.js delete mode 100644 collections.min.js diff --git a/README.md b/README.md index 7aa1204..0ea69e4 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,10 @@ This package contains JavaScript implementations of common data structures with idiomatic iterfaces, including extensions for Array and Object. -You can use these Node Packaged Modules with Node.js, [Browserify][], -[Mr][], or any compatible CommonJS module loader. Using a module loader -or bundler when using Collections in web browsers has the advantage of -only incorporating the modules you need. However, you can just embed -` + + + + diff --git a/spec/index.js b/spec/index.js new file mode 100644 index 0000000..c6db3f5 --- /dev/null +++ b/spec/index.js @@ -0,0 +1,37 @@ + +// "Isomprphic" test suite runner. This can be executed either: +// run node test/index.js +// or visit test/index.html (which loads this with Mr) + +var Suite = require("jasminum/suite"); + +var suite = new Suite("Q").describe(function () { + require("./shim-object-spec"); + require("./shim-functions-spec"); + require("./array-spec"); + require("./clone-spec"); + require("./deque-spec"); + require("./dict-spec"); + require("./fast-map-spec"); + require("./fast-set-spec"); + require("./heap-spec"); + require("./iterator-spec"); + require("./list-spec"); + require("./lru-map-spec"); + require("./lru-set-spec"); + require("./map-spec"); + require("./observable-array-spec"); + require("./observable-map-spec"); + require("./observable-object-spec"); + require("./observable-range-spec"); + require("./regexp-spec"); + require("./set-spec"); + require("./sorted-array-map-spec"); + require("./sorted-array-set-spec"); + require("./sorted-array-spec"); + require("./sorted-map-spec"); + require("./sorted-set-spec"); +}); + +suite.runAndReportSync(); + diff --git a/spec/iterator-spec.js b/spec/iterator-spec.js index d6a5a7a..042756b 100644 --- a/spec/iterator-spec.js +++ b/spec/iterator-spec.js @@ -12,11 +12,11 @@ describe("Iterator", function () { }); function expectCommonIterator(iterator) { - expect(iterator.next()).toEqual({value: 1, index: 0, done: false}); - expect(iterator.next()).toEqual({value: 2, index: 1, done: false}); - expect(iterator.next()).toEqual({value: 3, index: 2, done: false}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(Object.equals(iterator.next(), {value: 1, index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 2, index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 3, index: 2, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); } function describeIterator(Iterator) { @@ -28,20 +28,20 @@ function describeIterator(Iterator) { it("iterates a sparse array", function () { var iterator = Iterator([1,, 2,, 3]); - expect(iterator.next()).toEqual({value: 1, index: 0, done: false}); - expect(iterator.next()).toEqual({value: 2, index: 2, done: false}); - expect(iterator.next()).toEqual({value: 3, index: 4, done: false}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(Object.equals(iterator.next(), {value: 1, index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 2, index: 2, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 3, index: 4, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); }); it("iterates a string", function () { var iterator = Iterator("abc"); - expect(iterator.next()).toEqual({value: "a", index: 0, done: false}); - expect(iterator.next()).toEqual({value: "b", index: 1, done: false}); - expect(iterator.next()).toEqual({value: "c", index: 2, done: false}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(Object.equals(iterator.next(), {value: "a", index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: "b", index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: "c", index: 2, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); }); it("fails to iterate null", function () { @@ -115,11 +115,11 @@ function describeIterator(Iterator) { expect(i).toBe(n - 1); return n * 2; }); - expect(iterator.next()).toEqual({value: 2, index: 0, done: false}); - expect(iterator.next()).toEqual({value: 4, index: 1, done: false}); - expect(iterator.next()).toEqual({value: 6, index: 2, done: false}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(Object.equals(iterator.next(), {value: 2, index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 4, index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 6, index: 2, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); }); }); @@ -131,9 +131,9 @@ function describeIterator(Iterator) { expect(i).toBe(n - 1); return n % 2 === 0; }); - expect(iterator.next()).toEqual({value: 2, index: 1, done: false}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(Object.equals(iterator.next(), {value: 2, index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); }); }); @@ -145,28 +145,28 @@ function describeIterator(Iterator) { expect(i).toBe(n - 1); return n % 2 === 0; }).recount(); - expect(iterator.next()).toEqual({value: 2, index: 0, done: false}); - expect(iterator.next()).toEqual({value: 4, index: 1, done: false}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(Object.equals(iterator.next(), {value: 2, index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 4, index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); }); it("recounts a sparse array iterator", function () { var iterator = Iterator([1,, 2,, 3]).recount(); - expect(iterator.next()).toEqual({value: 1, index: 0, done: false}); - expect(iterator.next()).toEqual({value: 2, index: 1, done: false}); - expect(iterator.next()).toEqual({value: 3, index: 2, done: false}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(Object.equals(iterator.next(), {value: 1, index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 2, index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 3, index: 2, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); }); it("recounts from one", function () { var iterator = Iterator([1,, 2,, 3]).recount(1); - expect(iterator.next()).toEqual({value: 1, index: 1, done: false}); - expect(iterator.next()).toEqual({value: 2, index: 2, done: false}); - expect(iterator.next()).toEqual({value: 3, index: 3, done: false}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(Object.equals(iterator.next(), {value: 1, index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 2, index: 2, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 3, index: 3, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); }); }); @@ -204,23 +204,27 @@ function describeIterator(Iterator) { }); describe("dropWhile", function () { - var iterator = new Iterator([-1, -2, -3, 1, 2, 3]) - .dropWhile(function (n) { - return n < 0; + it("drops while the guard is true", function () { + var iterator = new Iterator([-1, -2, -3, 1, 2, 3]) + .dropWhile(function (n) { + return n < 0; + }); + expect(Object.equals(iterator.next(), {value: 1, index: 3, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 2, index: 4, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 3, index: 5, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); }); - expect(iterator.next()).toEqual({value: 1, index: 3, done: false}); - expect(iterator.next()).toEqual({value: 2, index: 4, done: false}); - expect(iterator.next()).toEqual({value: 3, index: 5, done: false}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); }); describe("takeWhile", function () { - var iterator = new Iterator([1, 2, 3, 4, 5, 6]) - .takeWhile(function (n) { - return n < 4; + it("takes while the guard is true", function () { + var iterator = new Iterator([1, 2, 3, 4, 5, 6]) + .takeWhile(function (n) { + return n < 4; + }); + expectCommonIterator(iterator); }); - expectCommonIterator(iterator); }); describe("iterateFlatten", function () { @@ -230,14 +234,14 @@ function describeIterator(Iterator) { Iterator([3, 4]), Iterator([5, 6]) ]).iterateFlatten(); - expect(iterator.next()).toEqual({value: 1, index: 0, done: false}); - expect(iterator.next()).toEqual({value: 2, index: 1, done: false}); - expect(iterator.next()).toEqual({value: 3, index: 0, done: false}); - expect(iterator.next()).toEqual({value: 4, index: 1, done: false}); - expect(iterator.next()).toEqual({value: 5, index: 0, done: false}); - expect(iterator.next()).toEqual({value: 6, index: 1, done: false}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(Object.equals(iterator.next(), {value: 1, index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 2, index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 3, index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 4, index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 5, index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 6, index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); }); }); @@ -247,16 +251,16 @@ function describeIterator(Iterator) { Iterator([1, 'B', 'y', 'I']), Iterator([2, 'C']) ); - expect(iterator.next()).toEqual({ + expect(Object.equals(iterator.next(), { value: [0, 1, 2], index: 0, done: false - }); - expect(iterator.next()).toEqual({ + })).toBe(true); + expect(Object.equals(iterator.next(), { value: ["A", "B", "C"], index: 1, done: false - }); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + })).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); }); }); @@ -267,26 +271,26 @@ function describeIterator(Iterator) { Iterator([1, 'B', 'y', 'I']), Iterator([2, 'C']) ]).iterateUnzip(); - expect(iterator.next()).toEqual({ + expect(Object.equals(iterator.next(), { value: [0, 1, 2], index: 0, done: false - }); - expect(iterator.next()).toEqual({ + })).toBe(true); + expect(Object.equals(iterator.next(), { value: ["A", "B", "C"], index: 1, done: false - }); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + })).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); }); }); describe("iterateEnumerate", function () { it("should enumerate an array", function () { var iterator = Iterator([1, 2, 3]).iterateEnumerate(); - expect(iterator.next()).toEqual({value: [0, 1], index: 0, done: false}); - expect(iterator.next()).toEqual({value: [1, 2], index: 1, done: false}); - expect(iterator.next()).toEqual({value: [2, 3], index: 2, done: false}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(Object.equals(iterator.next(), {value: [0, 1], index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: [1, 2], index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: [2, 3], index: 2, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); }); }); @@ -296,14 +300,14 @@ function describeIterator(Iterator) { Iterator([3, 4]), Iterator([5, 6]) ); - expect(iterator.next()).toEqual({value: 1, index: 0, done: false}); - expect(iterator.next()).toEqual({value: 2, index: 1, done: false}); - expect(iterator.next()).toEqual({value: 3, index: 0, done: false}); - expect(iterator.next()).toEqual({value: 4, index: 1, done: false}); - expect(iterator.next()).toEqual({value: 5, index: 0, done: false}); - expect(iterator.next()).toEqual({value: 6, index: 1, done: false}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(Object.equals(iterator.next(), {value: 1, index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 2, index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 3, index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 4, index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 5, index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 6, index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); }); }); @@ -313,27 +317,27 @@ describe("Iterator.cycle", function () { it("cycles an array", function () { var iterator = Iterator.cycle([1, 2, 3]); - expect(iterator.next()).toEqual({value: 1, index: 0, done: false}); - expect(iterator.next()).toEqual({value: 2, index: 1, done: false}); - expect(iterator.next()).toEqual({value: 3, index: 2, done: false}); - expect(iterator.next()).toEqual({value: 1, index: 0, done: false}); - expect(iterator.next()).toEqual({value: 2, index: 1, done: false}); - expect(iterator.next()).toEqual({value: 3, index: 2, done: false}); + expect(Object.equals(iterator.next(), {value: 1, index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 2, index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 3, index: 2, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 1, index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 2, index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 3, index: 2, done: false})).toBe(true); }); it("cycles an array twice", function () { var iterator = Iterator.cycle([1, 2], 2); - expect(iterator.next()).toEqual({value: 1, index: 0, done: false}); - expect(iterator.next()).toEqual({value: 2, index: 1, done: false}); - expect(iterator.next()).toEqual({value: 1, index: 0, done: false}); - expect(iterator.next()).toEqual({value: 2, index: 1, done: false}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(Object.equals(iterator.next(), {value: 1, index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 2, index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 1, index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 2, index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); }); it("cycles zero times", function () { var iterator = Iterator.cycle([1, 2, 3], 0); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); }); }); @@ -346,14 +350,14 @@ describe("Iterator.concat", function () { Iterator([3, 4]), Iterator([5, 6]) ); - expect(iterator.next()).toEqual({value: 1, index: 0, done: false}); - expect(iterator.next()).toEqual({value: 2, index: 1, done: false}); - expect(iterator.next()).toEqual({value: 3, index: 0, done: false}); - expect(iterator.next()).toEqual({value: 4, index: 1, done: false}); - expect(iterator.next()).toEqual({value: 5, index: 0, done: false}); - expect(iterator.next()).toEqual({value: 6, index: 1, done: false}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(Object.equals(iterator.next(), {value: 1, index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 2, index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 3, index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 4, index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 5, index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 6, index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); }); }); @@ -366,14 +370,14 @@ describe("Iterator.flatten", function () { Iterator([3, 4]), Iterator([5, 6]) ]); - expect(iterator.next()).toEqual({value: 1, index: 0, done: false}); - expect(iterator.next()).toEqual({value: 2, index: 1, done: false}); - expect(iterator.next()).toEqual({value: 3, index: 0, done: false}); - expect(iterator.next()).toEqual({value: 4, index: 1, done: false}); - expect(iterator.next()).toEqual({value: 5, index: 0, done: false}); - expect(iterator.next()).toEqual({value: 6, index: 1, done: false}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(Object.equals(iterator.next(), {value: 1, index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 2, index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 3, index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 4, index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 5, index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 6, index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); }); }); @@ -386,16 +390,16 @@ describe("Iterator.unzip", function () { Iterator([1, 'B', 'y', 'I']), Iterator([2, 'C']) ]); - expect(iterator.next()).toEqual({ + expect(Object.equals(iterator.next(), { value: [0, 1, 2], index: 0, done: false - }); - expect(iterator.next()).toEqual({ + })).toBe(true); + expect(Object.equals(iterator.next(), { value: ["A", "B", "C"], index: 1, done: false - }); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + })).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); }); }); @@ -408,16 +412,16 @@ describe("Iterator.zip", function () { Iterator([1, 'B', 'y', 'I']), Iterator([2, 'C']) ); - expect(iterator.next()).toEqual({ + expect(Object.equals(iterator.next(), { value: [0, 1, 2], index: 0, done: false - }); - expect(iterator.next()).toEqual({ + })).toBe(true); + expect(Object.equals(iterator.next(), { value: ["A", "B", "C"], index: 1, done: false - }); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + })).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); }); }); @@ -426,11 +430,11 @@ describe("Iterator.range", function () { it("iterates a range", function () { var iterator = new Iterator.range(3); - expect(iterator.next()).toEqual({value: 0, index: 0, done: false}); - expect(iterator.next()).toEqual({value: 1, index: 1, done: false}); - expect(iterator.next()).toEqual({value: 2, index: 2, done: false}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(Object.equals(iterator.next(), {value: 0, index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 1, index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 2, index: 2, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); }); it("iterates an offset range", function () { @@ -440,11 +444,11 @@ describe("Iterator.range", function () { it("iterates an offset, strided range", function () { var iterator = new Iterator.range(0, 5, 2); - expect(iterator.next()).toEqual({value: 0, index: 0, done: false}); - expect(iterator.next()).toEqual({value: 2, index: 1, done: false}); - expect(iterator.next()).toEqual({value: 4, index: 2, done: false}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(Object.equals(iterator.next(), {value: 0, index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 2, index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 4, index: 2, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); }); }); @@ -453,26 +457,26 @@ describe("Iterator.count", function () { it("iterates an open range", function () { var iterator = new Iterator.count(); - expect(iterator.next()).toEqual({value: 0, index: 0, done: false}); - expect(iterator.next()).toEqual({value: 1, index: 1, done: false}); - expect(iterator.next()).toEqual({value: 2, index: 2, done: false}); - expect(iterator.next()).toEqual({value: 3, index: 3, done: false}); + expect(Object.equals(iterator.next(), {value: 0, index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 1, index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 2, index: 2, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 3, index: 3, done: false})).toBe(true); }); it("iterates an open range starting with one", function () { var iterator = new Iterator.count(1); - expect(iterator.next()).toEqual({value: 1, index: 0, done: false}); - expect(iterator.next()).toEqual({value: 2, index: 1, done: false}); - expect(iterator.next()).toEqual({value: 3, index: 2, done: false}); - expect(iterator.next()).toEqual({value: 4, index: 3, done: false}); + expect(Object.equals(iterator.next(), {value: 1, index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 2, index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 3, index: 2, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 4, index: 3, done: false})).toBe(true); }); it("iterates an open range with stride", function () { var iterator = new Iterator.count(0, 2); - expect(iterator.next()).toEqual({value: 0, index: 0, done: false}); - expect(iterator.next()).toEqual({value: 2, index: 1, done: false}); - expect(iterator.next()).toEqual({value: 4, index: 2, done: false}); - expect(iterator.next()).toEqual({value: 6, index: 3, done: false}); + expect(Object.equals(iterator.next(), {value: 0, index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 2, index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 4, index: 2, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 6, index: 3, done: false})).toBe(true); }); }); @@ -482,18 +486,18 @@ describe("Iterator.repeat", function () { it("repeats a value indefinitely", function () { var iterator = Iterator.repeat(1); for (var index = 0; index < 10; index++) { - expect(iterator.next()).toEqual({value: 1, index: index, done: false}); + expect(Object.equals(iterator.next(), {value: 1, index: index, done: false})).toBe(true); } }); it("repeats a value some times", function () { var iterator = Iterator.repeat(1, 3); - expect(iterator.next()).toEqual({value: 1, index: 0, done: false}); - expect(iterator.next()).toEqual({value: 1, index: 1, done: false}); - expect(iterator.next()).toEqual({value: 1, index: 2, done: false}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(Object.equals(iterator.next(), {value: 1, index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 1, index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: 1, index: 2, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); }); }); @@ -502,10 +506,10 @@ describe("Iterator.enumerate", function () { it("should enumerate an array", function () { var iterator = Iterator.enumerate([1, 2, 3]); - expect(iterator.next()).toEqual({value: [0, 1], index: 0, done: false}); - expect(iterator.next()).toEqual({value: [1, 2], index: 1, done: false}); - expect(iterator.next()).toEqual({value: [2, 3], index: 2, done: false}); - expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(Object.equals(iterator.next(), {value: [0, 1], index: 0, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: [1, 2], index: 1, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: [2, 3], index: 2, done: false})).toBe(true); + expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); }); }); diff --git a/spec/lru-map-spec.js b/spec/lru-map-spec.js index 814ffc3..4b93db3 100644 --- a/spec/lru-map-spec.js +++ b/spec/lru-map-spec.js @@ -1,4 +1,5 @@ +var sinon = require("sinon"); var LruMap = require("../lru-map"); var describeDict = require("./dict"); var describeMap = require("./map"); @@ -62,12 +63,12 @@ describe("LruMap", function () { it("should dispatch deletion for stale entries", function () { var map = LruMap({a: 10, b: 20, c: 30}, 3); - var spy = jasmine.createSpy(); + var spy = sinon.spy(); map.observeMapChange(function (plus, minus, key, type) { spy(plus, minus, key, type); }); map.set('d', 40); - expect(spy.argsForCall).toEqual([ + expect(spy.args).toEqual([ [undefined, 10, "a", "delete"], // a pruned [40, undefined, "d", "create"] // d added ]); diff --git a/spec/lru-set-spec.js b/spec/lru-set-spec.js index b2bc9bf..b055146 100644 --- a/spec/lru-set-spec.js +++ b/spec/lru-set-spec.js @@ -1,4 +1,5 @@ +var sinon = require("sinon"); var LruSet = require("../lru-set"); var describeCollection = require("./collection"); var describeSet = require("./set"); @@ -38,7 +39,7 @@ describe("LruSet", function () { it("should dispatch LRU changes as singleton operation", function () { var set = LruSet([4, 3, 1, 2, 3], 3); - var spy = jasmine.createSpy(); + var spy = sinon.spy(); set.observeRangeWillChange(function (plus, minus) { spy('before-plus', plus); spy('before-minus', minus); @@ -48,7 +49,7 @@ describe("LruSet", function () { spy('after-minus', minus); }); expect(set.add(4)).toBe(false); - expect(spy.argsForCall).toEqual([ + expect(spy.args).toEqual([ ['before-plus', [4]], ['before-minus', [1]], ['after-plus', [4]], diff --git a/spec/map.js b/spec/map.js index 104d6e0..0c34a73 100644 --- a/spec/map.js +++ b/spec/map.js @@ -67,20 +67,24 @@ function describeMap(Map, values) { }); describe("equals", function () { - var map = Map({a: 10, b: 20}); - expect(Object.equals(map, map)).toBe(true); - expect(map.equals(map)).toBe(true); - expect(Map({a: 10, b: 20}).equals({b: 20, a: 10})).toBe(true); - expect(Object.equals({a: 10, b: 20}, Map({b: 20, a: 10}))).toBe(true); - expect(Object.equals(Map({b: 20, a: 10}), {a: 10, b: 20})).toBe(true); - expect(Object.equals(Map({b: 20, a: 10}), Map({a: 10, b: 20}))).toBe(true); + it("should compare maps", function () { + var map = Map({a: 10, b: 20}); + expect(Object.equals(map, map)).toBe(true); + expect(map.equals(map)).toBe(true); + expect(Map({a: 10, b: 20}).equals({b: 20, a: 10})).toBe(true); + expect(Object.equals({a: 10, b: 20}, Map({b: 20, a: 10}))).toBe(true); + expect(Object.equals(Map({b: 20, a: 10}), {a: 10, b: 20})).toBe(true); + expect(Object.equals(Map({b: 20, a: 10}), Map({a: 10, b: 20}))).toBe(true); + }); }); describe("clone", function () { - var map = Map({a: 10, b: 20}); - var clone = Object.clone(map); - expect(map).toNotBe(clone); - expect(map.equals(clone)).toBe(true); + it("clones a map", function () { + var map = Map({a: 10, b: 20}); + var clone = Object.clone(map); + expect(map).toNotBe(clone); + expect(map.equals(clone)).toBe(true); + }); }); describeObservableMap(Map); diff --git a/spec/observable-array-spec.js b/spec/observable-array-spec.js index d3ca114..8fb20f1 100644 --- a/spec/observable-array-spec.js +++ b/spec/observable-array-spec.js @@ -1,4 +1,6 @@ +var sinon = require("sinon"); +var extendSpyExpectation = require("./spy-expectation"); require("../observable-array"); // TODO var describeObservableRange = require("./observable-range"); // TODO make Array.from consistent with List @@ -55,9 +57,9 @@ describe("Array change dispatch with map observers", function () { }); it("push", function () { - spy = jasmine.createSpy(); + spy = sinon.spy(); array.push(1, 2, 3); - expect(spy.argsForCall).toEqual([ + expect(spy.args).toEqual([ ["length will change from", 0, "to", 3], ["range will change from", [], "to", [1, 2, 3], "at", 0], ["map will", "create", 0, "from", undefined, "to", 1], @@ -73,10 +75,10 @@ describe("Array change dispatch with map observers", function () { }); it("clear", function () { - spy = jasmine.createSpy(); + spy = sinon.spy(); array.clear(); expect(array).toEqual([]); - expect(spy.argsForCall).toEqual([ + expect(spy.args).toEqual([ ["length will change from", 3, "to", 0], ["range will change from", [1, 2, 3], "to", [], "at", 0], ["map will", "delete", 0, "from", 1, "to", undefined], @@ -92,9 +94,9 @@ describe("Array change dispatch with map observers", function () { it("pop one value from an array", function () { array.push(1, 2, 3); - spy = jasmine.createSpy(); + spy = sinon.spy(); array.pop(); - expect(spy.argsForCall).toEqual([ + expect(spy.args).toEqual([ ["length will change from", 3, "to", 2], ["range will change from", [3], "to", [], "at", 2], ["map will", "delete", 2, "from", 3, "to", undefined], @@ -106,9 +108,9 @@ describe("Array change dispatch with map observers", function () { it("shift one value off an array", function () { array.push(1, 2, 3); - spy = jasmine.createSpy(); + spy = sinon.spy(); array.shift(); - expect(spy.argsForCall).toEqual([ + expect(spy.args).toEqual([ ["length will change from", 3, "to", 2], ["range will change from", [1], "to", [], "at", 0], ["map will", "update", 0, "from", 1, "to", 2], @@ -124,9 +126,9 @@ describe("Array change dispatch with map observers", function () { it("replaces values into the midst of an array", function () { array.push(1, 3, 2, 4); - spy = jasmine.createSpy(); + spy = sinon.spy(); array.splice(1, 2, 2, 3); - expect(spy.argsForCall).toEqual([ + expect(spy.args).toEqual([ ["range will change from", [3, 2], "to", [2, 3], "at", 1], ["map will", "update", 1, "from", 3, "to", 2], ["map will", "update", 2, "from", 2, "to", 3], @@ -138,9 +140,9 @@ describe("Array change dispatch with map observers", function () { it("replaces values into the midst of an array, from a negative index", function () { array.push(1, 3, 2, 4); - spy = jasmine.createSpy(); + spy = sinon.spy(); array.splice(-3, 2, 2, 3); - expect(spy.argsForCall).toEqual([ + expect(spy.args).toEqual([ ["range will change from", [3, 2], "to", [2, 3], "at", 1], ["map will", "update", 1, "from", 3, "to", 2], ["map will", "update", 2, "from", 2, "to", 3], @@ -152,9 +154,9 @@ describe("Array change dispatch with map observers", function () { it("splices values into the midst of an array", function () { array.push(1, 4); - spy = jasmine.createSpy(); + spy = sinon.spy(); array.splice(1, 0, 2, 3); - expect(spy.argsForCall).toEqual([ + expect(spy.args).toEqual([ ["length will change from", 2, "to", 4], ["range will change from", [], "to", [2, 3], "at", 1], ["map will", "update", 1, "from", 4, "to", 2], @@ -170,9 +172,9 @@ describe("Array change dispatch with map observers", function () { it("splices values into the midst of an array, with a negative length", function () { array.push(1, 4); - spy = jasmine.createSpy(); + spy = sinon.spy(); array.splice(1, -1, 2, 3); - expect(spy.argsForCall).toEqual([ + expect(spy.args).toEqual([ ["length will change from", 2, "to", 4], ["range will change from", [], "to", [2, 3], "at", 1], ["map will", "update", 1, "from", 4, "to", 2], @@ -188,9 +190,9 @@ describe("Array change dispatch with map observers", function () { it("set at end", function () { array.push(1, 2, 3); - spy = jasmine.createSpy(); + spy = sinon.spy(); array.set(3, 4); - expect(spy.argsForCall).toEqual([ + expect(spy.args).toEqual([ ["length will change from", 3, "to", 4], ["range will change from", [], "to", [4], "at", 3], ["map will", "create", 3, "from", undefined, "to", 4], @@ -202,9 +204,9 @@ describe("Array change dispatch with map observers", function () { it("set at beginning", function () { array.push(1, 2, 3); - spy = jasmine.createSpy(); + spy = sinon.spy(); array.set(0, 3); - expect(spy.argsForCall).toEqual([ + expect(spy.args).toEqual([ ["range will change from", [1], "to", [3], "at", 0], ["map will", "update", 0, "from", 1, "to", 3], ["map", "update", 0, "from", 1, "to", 3], @@ -214,9 +216,9 @@ describe("Array change dispatch with map observers", function () { it("unshifts", function () { array.push(3, 4); - spy = jasmine.createSpy(); + spy = sinon.spy(); array.unshift(1, 2); - expect(spy.argsForCall).toEqual([ + expect(spy.args).toEqual([ ["length will change from", 2, "to", 4], ["range will change from", [], "to", [1, 2], "at", 0], ["map will", "update", 0, "from", 3, "to", 1], @@ -234,9 +236,9 @@ describe("Array change dispatch with map observers", function () { it("reverses in place", function () { array.push(10, 20, 30); - spy = jasmine.createSpy(); + spy = sinon.spy(); array.reverse(); - expect(spy.argsForCall).toEqual([ + expect(spy.args).toEqual([ ["range will change from", [10, 20, 30], "to", [30, 20, 10], "at", 0], ["map will", "update", 0, "from", 10, "to", 30], ["map will", "update", 2, "from", 30, "to", 10], @@ -248,9 +250,9 @@ describe("Array change dispatch with map observers", function () { it("sorts in place", function () { array.push(30, 20, 10); - spy = jasmine.createSpy(); + spy = sinon.spy(); array.sort(); - expect(spy.argsForCall).toEqual([ + expect(spy.args).toEqual([ ["range will change from", [30, 20, 10], "to", [10, 20, 30], "at", 0], ["map will", "update", 0, "from", 30, "to", 10], ["map will", "update", 2, "from", 10, "to", 30], @@ -266,9 +268,11 @@ describe("Array change dispatch with map observers", function () { describe("Array changes", function () { + extendSpyExpectation(); + it("observes range changes on arrays that are not otherwised observed", function () { var array = [1, 2, 3]; - var spy = jasmine.createSpy(); + var spy = sinon.spy(); array.observeRangeChange(spy); array.push(4); expect(spy).toHaveBeenCalledWith([4], [], 3, array); @@ -276,7 +280,7 @@ describe("Array changes", function () { it("observes length changes on arrays that are not otherwised observed", function () { var array = [1, 2, 3]; - var spy = jasmine.createSpy(); + var spy = sinon.spy(); array.observePropertyChange("length", spy); array.push(4); expect(spy).toHaveBeenCalledWith(4, 3, "length", array); @@ -284,7 +288,7 @@ describe("Array changes", function () { it("observes map changes on arrays that are not otherwised observed", function () { var array = [1, 2, 3]; - var spy = jasmine.createSpy(); + var spy = sinon.spy(); array.observeMapChange(spy); array.push(4); expect(spy).toHaveBeenCalledWith(4, undefined, 3, "create", array); @@ -292,7 +296,7 @@ describe("Array changes", function () { it("observes index changes on arrays that are not otherwised observed", function () { var array = [1, 2, 3]; - var spy = jasmine.createSpy(); + var spy = sinon.spy(); array.observePropertyChange(3, spy); array.push(4); expect(spy).toHaveBeenCalledWith(4, undefined, 3, array); diff --git a/spec/observable-map-spec.js b/spec/observable-map-spec.js index fb7a74c..ea5d8d5 100644 --- a/spec/observable-map-spec.js +++ b/spec/observable-map-spec.js @@ -1,9 +1,13 @@ "use strict"; +var sinon = require("sinon"); +var extendSpyExpectation = require("./spy-expectation"); var ObservableMap = require("../observable-map"); describe("ObservableMap", function () { + extendSpyExpectation(); + describe("observeMapChange", function () { it("observe, dispatch", function () { @@ -14,7 +18,7 @@ describe("ObservableMap", function () { spy(plus, minus, key, type, object); }); - spy = jasmine.createSpy(); + spy = sinon.spy(); map.dispatchMapChange("create", "foo", 10, undefined); expect(spy).toHaveBeenCalledWith(10, undefined, "foo", "create", map); @@ -29,7 +33,7 @@ describe("ObservableMap", function () { spy(plus, minus, key, type, object); }); - spy = jasmine.createSpy(); + spy = sinon.spy(); observer.cancel(); map.dispatchMapChange("create", "foo", 10, undefined); expect(spy).not.toHaveBeenCalled(); @@ -45,12 +49,12 @@ describe("ObservableMap", function () { spy(plus, minus, key, type, object); }); - spy = jasmine.createSpy(); + spy = sinon.spy(); map.dispatchMapChange("create", "foo", 10, undefined); expect(spy).toHaveBeenCalledWith(10, undefined, "foo", "create", map); observer.cancel(); - spy = jasmine.createSpy(); + spy = sinon.spy(); map.dispatchMapChange("create", "foo", 10, undefined); expect(spy).not.toHaveBeenCalled(); }); diff --git a/spec/observable-map.js b/spec/observable-map.js index 9e0126f..9373fab 100644 --- a/spec/observable-map.js +++ b/spec/observable-map.js @@ -1,9 +1,15 @@ +var sinon = require("sinon"); +var extendSpyExpectation = require("./spy-expectation"); + module.exports = describeObservableMap; function describeObservableMap(Map) { + + extendSpyExpectation(); + it("create, update, delete", function () { var map = new Map(); - var spy = jasmine.createSpy(); + var spy = sinon.spy(); var observer = map.observeMapChange(function (plus, minus, key, type) { spy(plus, minus, key, type); }); @@ -15,17 +21,18 @@ function describeObservableMap(Map) { map.delete("a"); expect(spy).toHaveBeenCalledWith(undefined, 20, "a", "delete"); - spy = jasmine.createSpy(); + spy = sinon.spy(); map.set("a", 30); expect(spy).toHaveBeenCalledWith(30, undefined, "a", "create"); map.set("a", undefined); expect(spy).toHaveBeenCalledWith(undefined, 30, "a", "update"); - spy = jasmine.createSpy(); + spy = sinon.spy(); observer.cancel(); map.set("b", 20); expect(spy).not.toHaveBeenCalled(); }); + } diff --git a/spec/observable-object-spec.js b/spec/observable-object-spec.js index be94579..4e1b061 100644 --- a/spec/observable-object-spec.js +++ b/spec/observable-object-spec.js @@ -9,6 +9,8 @@ // TODO observePropertyWillChange // TODO access observer notes +var sinon = require("sinon"); +var extendSpyExpectation = require("./spy-expectation"); var ObservableObject = require("../observable-object"); var observePropertyChange = ObservableObject.observePropertyChange; var makePropertyObservable = ObservableObject.makePropertyObservable; @@ -17,11 +19,13 @@ var dispatchPropertyChange = ObservableObject.dispatchPropertyChange; describe("ObservableObject", function () { + extendSpyExpectation(); + describe("observePropertyChange", function () { it("property change", function () { var object = {}; - var spy = jasmine.createSpy(); + var spy = sinon.spy(); var observer = observePropertyChange(object, "foo", function (plus, minus, name, object) { spy(plus, minus, name, object); }); @@ -31,7 +35,7 @@ describe("ObservableObject", function () { it("property non-change", function () { var object = {foo: 10}; - var spy = jasmine.createSpy(); + var spy = sinon.spy(); var observer = observePropertyChange(object, "foo", function (plus, minus, name, object) { spy(plus, minus, name, object); }); @@ -41,28 +45,28 @@ describe("ObservableObject", function () { it("property change, property non-change", function () { var object = {}; - var spy = jasmine.createSpy(); + var spy = sinon.spy(); var observer = observePropertyChange(object, "foo", function (plus, minus, name, object) { spy(plus, minus, name, object); }); object.foo = 10; expect(spy).toHaveBeenCalledWith(10, undefined, "foo", object); - spy = jasmine.createSpy(); + spy = sinon.spy(); object.foo = 10; expect(spy).not.toHaveBeenCalled(); }); it("property change, observer", function () { var object = {}; - var spy = jasmine.createSpy(); + var spy = sinon.spy(); var observer = observePropertyChange(object, "foo", function (plus, minus, name, object) { spy(plus, minus, name, object); }); object.foo = 10; observer.cancel(); - spy = jasmine.createSpy(); + spy = sinon.spy(); object.foo = 20; expect(spy).not.toHaveBeenCalled(); }); @@ -74,18 +78,18 @@ describe("ObservableObject", function () { }); observer.cancel(); - spy = jasmine.createSpy(); + spy = sinon.spy(); object.foo = 20; expect(spy).not.toHaveBeenCalled(); }); it("multiple observers", function () { var object = {}; - var spy1 = jasmine.createSpy(); + var spy1 = sinon.spy(); var observer1 = observePropertyChange(object, "foo", function (plus, minus, name, object) { spy1(plus, minus, name, object); }); - var spy2 = jasmine.createSpy(); + var spy2 = sinon.spy(); var observer2 = observePropertyChange(object, "foo", function (plus, minus, name, object) { spy2(plus, minus, name, object); }); @@ -96,11 +100,11 @@ describe("ObservableObject", function () { it("multiple observers, one observered", function () { var object = {}; - var spy1 = jasmine.createSpy(); + var spy1 = sinon.spy(); var observer1 = observePropertyChange(object, "foo", function (plus, minus, name, object) { spy1(plus, minus, name, object); }); - var spy2 = jasmine.createSpy(); + var spy2 = sinon.spy(); var observer2 = observePropertyChange(object, "foo", function (plus, minus, name, object) { spy2(plus, minus, name, object); }); @@ -112,11 +116,11 @@ describe("ObservableObject", function () { it("multiple observers, other observered", function () { var object = {}; - var spy1 = jasmine.createSpy(); + var spy1 = sinon.spy(); var observer1 = observePropertyChange(object, "foo", function (plus, minus, name, object) { spy1(plus, minus, name, object); }); - var spy2 = jasmine.createSpy(); + var spy2 = sinon.spy(); var observer2 = observePropertyChange(object, "foo", function (plus, minus, name, object) { spy2(plus, minus, name, object); }); @@ -128,11 +132,11 @@ describe("ObservableObject", function () { it("multiple observers, both observered", function () { var object = {}; - var spy1 = jasmine.createSpy(); + var spy1 = sinon.spy(); var observer1 = observePropertyChange(object, "foo", function (plus, minus, name, object) { spy1(plus, minus, name, object); }); - var spy2 = jasmine.createSpy(); + var spy2 = sinon.spy(); var observer2 = observePropertyChange(object, "foo", function (plus, minus, name, object) { spy2(plus, minus, name, object); }); @@ -145,12 +149,12 @@ describe("ObservableObject", function () { it("observe, observer, observe", function () { var object = {}; - var spy1 = jasmine.createSpy(); + var spy1 = sinon.spy(); var observer1 = observePropertyChange(object, "foo", function (plus, minus, name, object) { spy1(plus, minus, name, object); }); observer1.cancel(); - var spy2 = jasmine.createSpy(); + var spy2 = sinon.spy(); var observer2 = observePropertyChange(object, "foo", function (plus, minus, name, object) { spy2(plus, minus, name, object); }); @@ -167,7 +171,7 @@ describe("ObservableObject", function () { } }; var observer = observePropertyChange(object, "foo", object); - var spy = jasmine.createSpy(); + var spy = sinon.spy(); object.foo = 20; expect(spy).toHaveBeenCalledWith(20, 10, "foo", object); }); @@ -180,23 +184,23 @@ describe("ObservableObject", function () { } }; var observer = observePropertyChange(object, "foo", object); - var spy = jasmine.createSpy(); + var spy = sinon.spy(); object.foo = 20; expect(spy).toHaveBeenCalledWith(20, 10, "foo", object); }); it("is robust against observeration of an intermediate observer", function () { var object = {foo: 10}; - var spy1 = jasmine.createSpy(); + var spy1 = sinon.spy(); var observer1 = observePropertyChange(object, "foo", function (plus, minus, name, object) { spy1(plus, minus, name, object); if (observer2) observer2.cancel(); }); - var spy2 = jasmine.createSpy(); + var spy2 = sinon.spy(); var observer2 = observePropertyChange(object, "foo", spy2); - var spy3 = jasmine.createSpy(); + var spy3 = sinon.spy(); var observer3 = observePropertyChange(object, "foo", spy3); - var spy4 = jasmine.createSpy(); + var spy4 = sinon.spy(); var observer4 = observePropertyChange(object, "foo", spy4); expect(spy1.callCount).toBe(0); expect(spy2.callCount).toBe(0); @@ -209,7 +213,7 @@ describe("ObservableObject", function () { it("is robust against property changes during dispatch of a property change", function () { var object = {}; - var spy = jasmine.createSpy(); + var spy = sinon.spy(); var observer = observePropertyChange(object, "foo", function (plus, minus, name, object) { if (object.foo >= 10) { return observer.cancel(); @@ -223,8 +227,8 @@ describe("ObservableObject", function () { it("should observe nested observer", function () { var object = {}; - var spy = jasmine.createSpy(); - var innerCancel = jasmine.createSpy(); + var spy = sinon.spy(); + var innerCancel = sinon.spy(); var observer = observePropertyChange(object, "foo", function () { spy(); return {cancel: innerCancel}; @@ -251,7 +255,7 @@ describe("ObservableObject", function () { var observer = observePropertyChange(object, "foo", function (child) { throw new Error("X"); }); - var spy = jasmine.createSpy(); + var spy = sinon.spy(); observePropertyChange(object, "foo", spy); var error; try { @@ -266,7 +270,7 @@ describe("ObservableObject", function () { it("handles manual dispatch", function () { var object = {}; - var spy = jasmine.createSpy(); + var spy = sinon.spy(); var observer = observePropertyChange(object, "foo", function (value) { spy.apply(this, arguments); if (value === 2) { @@ -300,7 +304,7 @@ describe("ObservableObject", function () { var object = new Foo(); expect(object.foo).toBe(0); - var spy = jasmine.createSpy(); + var spy = sinon.spy(); var observer = observePropertyChange(object, "foo", function (plus, minus, name, object) { spy(plus, minus); }); @@ -325,7 +329,7 @@ describe("ObservableObject", function () { var object = new Foo(); expect(object._foo).toBe(0); - var spy = jasmine.createSpy(); + var spy = sinon.spy(); var observer = observePropertyChange(object, "foo", function (plus, minus, name, object) { spy(plus, minus); }); @@ -344,9 +348,9 @@ describe("ObservableObject", function () { it("observes changes to different properties", function () { var object = {}; - var fooSpy = jasmine.createSpy(); + var fooSpy = sinon.spy(); var fooObserver = observePropertyChange(object, "foo", fooSpy); - var barSpy = jasmine.createSpy(); + var barSpy = sinon.spy(); var barObserver = observePropertyChange(object, "bar", barSpy); object.foo = 10; @@ -390,7 +394,7 @@ describe("ObservableObject", function () { enumerable: false, configurable: true }); - var spy = jasmine.createSpy(); + var spy = sinon.spy(); var observer = observePropertyChange(object, "foo", spy); object.foo = 10; expect(spy).not.toHaveBeenCalled(); @@ -404,7 +408,7 @@ describe("ObservableObject", function () { preventPropertyObserver(Foo.prototype, "foo"); var object = new Foo(); expect(object.hasOwnProperty("foo")).toBe(false); - var spy = jasmine.createSpy(); + var spy = sinon.spy(); var observer = observePropertyChange(object, "foo", spy); expect(object.hasOwnProperty("foo")).toBe(false); expect(object.foo).toBe(undefined); diff --git a/spec/observable-range-spec.js b/spec/observable-range-spec.js index 2178e0c..422e6ba 100644 --- a/spec/observable-range-spec.js +++ b/spec/observable-range-spec.js @@ -1,10 +1,15 @@ "use strict"; +var sinon = require("sinon"); +var extendSpyExpectation = require("./spy-expectation"); var ObservableRange = require("../observable-range"); describe("ObservableRange", function () { describe("observeRangeChange", function () { + + extendSpyExpectation(); + it("observe, dispatch", function () { var range = Object.create(ObservableRange.prototype); var spy; @@ -13,7 +18,7 @@ describe("ObservableRange", function () { spy(plus, minus, index); }); - spy = jasmine.createSpy(); + spy = sinon.spy(); range.dispatchRangeChange([1, 2, 3], [], 0); expect(spy).toHaveBeenCalledWith([1, 2, 3], [], 0); }); diff --git a/spec/order.js b/spec/order.js index f192710..a6762e7 100644 --- a/spec/order.js +++ b/spec/order.js @@ -51,7 +51,8 @@ function describeOrder(Collection) { // contains 10, 20, 30 it("a fake array should be equal to collection", function () { - expect(Object.compare(fakeArray, Collection([10, 20, 30]))).toEqual(0); + // The oddest thing happens here because of a negative zero + expect(-Object.compare(fakeArray, Collection([10, 20, 30]))).toEqual(0); }); it("a fake array should be less than a collection", function () { diff --git a/spec/set-spec.js b/spec/set-spec.js index 72f0b80..1020bb7 100644 --- a/spec/set-spec.js +++ b/spec/set-spec.js @@ -1,10 +1,14 @@ +var sinon = require("sinon"); +var extendSpyExpectation = require("./spy-expectation"); var Set = require("../set"); var describeCollection = require("./collection"); var describeSet = require("./set"); describe("Set", function () { + extendSpyExpectation(); + function newSet(values) { return new Set(values); } @@ -30,7 +34,7 @@ describe("Set", function () { it("should dispatch range change on clear", function () { var set = Set([1, 2, 3]); - var spy = jasmine.createSpy(); + var spy = sinon.spy(); set.observeRangeChange(function (plus, minus, index, _set) { spy(plus, minus, index); expect(_set).toBe(set); @@ -41,7 +45,7 @@ describe("Set", function () { it("should dispatch range change on add", function () { var set = Set([1, 3]); - var spy = jasmine.createSpy(); + var spy = sinon.spy(); set.observeRangeChange(function (plus, minus, index, _set) { spy(plus, minus, index); expect(_set).toBe(set); @@ -53,7 +57,7 @@ describe("Set", function () { it("should dispatch range change on delete", function () { var set = Set([1, 2, 3]); - var spy = jasmine.createSpy(); + var spy = sinon.spy(); set.observeRangeChange(function (plus, minus, index, _set) { spy(plus, minus, index); expect(_set).toBe(set); @@ -65,7 +69,7 @@ describe("Set", function () { it("should dispatch range change on pop", function () { var set = Set([1, 3, 2]); - var spy = jasmine.createSpy(); + var spy = sinon.spy(); set.observeRangeChange(function (plus, minus, index, _set) { spy(plus, minus, index); expect(_set).toBe(set); @@ -77,7 +81,7 @@ describe("Set", function () { it("should dispatch range change on shift", function () { var set = Set([1, 3, 2]); - var spy = jasmine.createSpy(); + var spy = sinon.spy(); set.observeRangeChange(function (plus, minus, index, _set) { spy(plus, minus, index); expect(_set).toBe(set); @@ -96,22 +100,22 @@ describe("Set", function () { expect(_set).toBe(set); }); - var spy = jasmine.createSpy(); + var spy = sinon.spy(); expect(set.add(2)).toEqual(true); expect(set.toArray()).toEqual([1, 3, 2]); expect(spy).toHaveBeenCalledWith([2], [], 2); - var spy = jasmine.createSpy(); + var spy = sinon.spy(); expect(set.shift()).toEqual(1); expect(set.toArray()).toEqual([3, 2]); expect(spy).toHaveBeenCalledWith([], [1], 0); - var spy = jasmine.createSpy(); + var spy = sinon.spy(); expect(set.pop()).toEqual(2); expect(set.toArray()).toEqual([3]); expect(spy).toHaveBeenCalledWith([], [2], 1); - var spy = jasmine.createSpy(); + var spy = sinon.spy(); expect(set.delete(3)).toEqual(true); expect(set.toArray()).toEqual([]); expect(spy).toHaveBeenCalledWith([], [3], 0); diff --git a/spec/set.js b/spec/set.js index dcba1ca..86cfc22 100644 --- a/spec/set.js +++ b/spec/set.js @@ -4,9 +4,11 @@ var Iterator = require("../iterator"); module.exports = describeSet; function describeSet(Set, sorted) { - describe("uniqueness", function () { - var set = Set([1, 2, 3, 1, 2, 3]); - expect(set.toArray().sort()).toEqual([1, 2, 3]); + describe("Set constructor", function () { + it("should establish uniqueness of values", function () { + var set = Set([1, 2, 3, 1, 2, 3]); + expect(set.toArray().sort()).toEqual([1, 2, 3]); + }); }); describe("forEach", function () { diff --git a/spec/shim-object-spec.js b/spec/shim-object-spec.js index 3b74854..90fe0f2 100644 --- a/spec/shim-object-spec.js +++ b/spec/shim-object-spec.js @@ -7,11 +7,15 @@ https://github.com/motorola-mobility/montage/blob/master/LICENSE.md */ +var sinon = require("sinon"); +var extendSpyExpectation = require("./spy-expectation"); require("../shim"); var Dict = require("../dict"); describe("Object", function () { + extendSpyExpectation(); + it("should have no enumerable properties", function () { expect(Object.keys(Object.prototype)).toEqual([]); }); @@ -193,7 +197,7 @@ describe("Object", function () { }); it("should delegate to a 'set' method", function () { - var spy = jasmine.createSpy(); + var spy = sinon.spy(); var Type = Object.create(Object.prototype, { set: { value: spy @@ -201,7 +205,7 @@ describe("Object", function () { }); var instance = Object.create(Type); Object.set(instance, "a", 10); - expect(spy.argsForCall).toEqual([ + expect(spy.args).toEqual([ ["a", 10] ]); }); @@ -211,10 +215,10 @@ describe("Object", function () { describe("forEach", function () { it("should iterate the owned properties of an object", function () { - var spy = jasmine.createSpy(); + var spy = sinon.spy(); var object = {a: 10, b: 20, c: 30}; Object.forEach(object, spy); - expect(spy.argsForCall).toEqual([ + expect(spy.args).toEqual([ [10, "a", object], [20, "b", object], [30, "c", object] @@ -315,7 +319,7 @@ describe("Object", function () { return this; }, equals: function (n) { - return n === 10 || typeof n === "object" && n.value === 10; + return n === 10 || !!n && n.value === 10; } }; @@ -361,9 +365,12 @@ describe("Object", function () { // within each pair of class, test exhaustive combinations to cover // the commutative property Object.forEach(equivalenceClass, function (a, ai) { - Object.forEach(equivalenceClass, function (b, bi) { - it(": " + ai + " equals " + bi, function () { - expect(Object.equals(a, b)).toBe(true); + describe(ai, function () { + Object.forEach(equivalenceClass, function (b, bi) { + it("equals " + bi, function () { + expect(Object.equals(a, b)).toBe(true); + //expect(a).toEqual(b); + }); }); }); }); @@ -494,9 +501,10 @@ describe("Object", function () { graph.cycle = graph; graph.arrayWithHoles[10] = 10; - graph.typedObject = Object.create(null); - graph.typedObject.a = 10; - graph.typedObject.b = 10; + // Not reflexively equal, not equal to clone + //graph.typedObject = Object.create(null); + //graph.typedObject.a = 10; + //graph.typedObject.b = 10; Object.forEach(graph, function (value, name) { it(name + " cloned equals self", function () { @@ -526,8 +534,6 @@ describe("Object", function () { it("should clone array at two levels of depth", function () { var clone = Object.clone(graph, 2); expect(clone).toEqual(graph); - expect(clone.array).toNotBe(graph.array); - expect(clone.array).toEqual(graph.array); }); it("should clone identical values at least once", function () { diff --git a/spec/sorted-array-set-spec.js b/spec/sorted-array-set-spec.js index 50e3762..afa2da3 100644 --- a/spec/sorted-array-set-spec.js +++ b/spec/sorted-array-set-spec.js @@ -18,9 +18,11 @@ describe("SortedArraySet", function () { describeSet(SortedArraySet); }); - describe("uniqueness", function () { - var set = SortedArraySet([1, 2, 3, 1, 2, 3]); - expect(set.slice()).toEqual([1, 2, 3]); + describe("constructor", function () { + it("only allows unique values", function () { + var set = SortedArraySet([1, 2, 3, 1, 2, 3]); + expect(set.slice()).toEqual([1, 2, 3]); + }); }); }); diff --git a/spec/sorted-array-spec.js b/spec/sorted-array-spec.js index f3f5ae7..6b02713 100644 --- a/spec/sorted-array-spec.js +++ b/spec/sorted-array-spec.js @@ -19,8 +19,10 @@ describe("SortedArray", function () { }); describe("non-uniqueness", function () { - var array = SortedArray([1, 2, 3, 1, 2, 3]); - expect(array.slice()).toEqual([1, 1, 2, 2, 3, 3]); + it("should sort non-unique values", function () { + var array = SortedArray([1, 2, 3, 1, 2, 3]); + expect(array.slice()).toEqual([1, 1, 2, 2, 3, 3]); + }); }); // TODO test stability diff --git a/spec/sorted-map-spec.js b/spec/sorted-map-spec.js index c38fd1c..8064476 100644 --- a/spec/sorted-map-spec.js +++ b/spec/sorted-map-spec.js @@ -6,15 +6,17 @@ describe("SortedMap", function () { describeDict(SortedMap); describe("reduceRight", function () { - var map = SortedMap([ - [1, 2], - [2, 4], - [3, 6], - [4, 8] - ]); - expect(map.reduceRight(function (valid, value, key) { - return valid && key * 2 == value; - }, true)).toBe(true); + it("should reduce entries from right to left", function () { + var map = SortedMap([ + [1, 2], + [2, 4], + [3, 6], + [4, 8] + ]); + expect(map.reduceRight(function (valid, value, key) { + return valid && key * 2 == value; + }, true)).toBe(true); + }); }); }); diff --git a/spec/spy-expectation.js b/spec/spy-expectation.js new file mode 100644 index 0000000..3c7b5c2 --- /dev/null +++ b/spec/spy-expectation.js @@ -0,0 +1,25 @@ + +module.exports = extendSpyExpectation; +function extendSpyExpectation() { + var Expectation = getCurrentSuite().Expectation; + + Expectation.prototype.toHaveBeenCalledWith = function () { + var args = Array.prototype.slice.call(arguments); + this.assert(Object.has(this.value.args, args), [ + "expected spy [not] to have been called with", + "but calls were" + ], [ + args, + this.value.args + ]); + }; + + Expectation.prototype.toHaveBeenCalled = function () { + this.assert(!!this.value.args.length, [ + "expected spy [not] to have been called" + ], [ + ]); + }; + +}; + From d40e942e555285d2228c16b40d5ab7ba5360d8b5 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Mon, 17 Mar 2014 15:55:23 -0700 Subject: [PATCH 37/83] Migrate expect toNotBe to not.toBe --- spec/array-spec.js | 10 +++++----- spec/clone-spec.js | 4 ++-- spec/map.js | 2 +- spec/order.js | 8 ++++---- spec/set.js | 2 +- spec/shim-object-spec.js | 14 +++++++------- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/spec/array-spec.js b/spec/array-spec.js index 213d4c6..915ad4e 100644 --- a/spec/array-spec.js +++ b/spec/array-spec.js @@ -133,7 +133,7 @@ describe("Array", function () { }); it("should not be an in-place sort", function () { - expect(unsorted.sorted()).toNotBe(unsorted); + expect(unsorted.sorted()).not.toBe(unsorted); }); it("should sort objects by a property array", function () { @@ -153,7 +153,7 @@ describe("Array", function () { var array = [[[]]]; var clone = array.clone(); expect(clone).toEqual(array); - expect(clone).toNotBe(array); + expect(clone).not.toBe(array); }); it("should clone with depth 0", function () { @@ -163,14 +163,14 @@ describe("Array", function () { it("should clone with depth 1", function () { var array = [{}]; - expect(array.clone(1)).toNotBe(array); + expect(array.clone(1)).not.toBe(array); expect(array.clone(1)[0]).toBe(array[0]); }); it("should clone with depth 2", function () { var array = [{a: 10}]; - expect(array.clone(2)).toNotBe(array); - expect(array.clone(2)[0]).toNotBe(array[0]); + expect(array.clone(2)).not.toBe(array); + expect(array.clone(2)[0]).not.toBe(array[0]); expect(array.clone(2)[0]).toEqual(array[0]); }); diff --git a/spec/clone-spec.js b/spec/clone-spec.js index 1971c97..7810250 100644 --- a/spec/clone-spec.js +++ b/spec/clone-spec.js @@ -12,9 +12,9 @@ describe("clone", function () { expect(Object.equals(a, b)).toBe(false); expect(a.equals(b)).toBe(false); - expect(a.one()).toNotBe(b.one()); + expect(a.one()).not.toBe(b.one()); expect(a.one().equals(b.one())).toBe(true); - expect(a.one().get('a')).toNotBe(b.one().get('a')); + expect(a.one().get('a')).not.toBe(b.one().get('a')); expect(a.one().get('a')).toEqual(b.one().get('a')); }); diff --git a/spec/map.js b/spec/map.js index 0c34a73..f3d7bc1 100644 --- a/spec/map.js +++ b/spec/map.js @@ -82,7 +82,7 @@ function describeMap(Map, values) { it("clones a map", function () { var map = Map({a: 10, b: 20}); var clone = Object.clone(map); - expect(map).toNotBe(clone); + expect(map).not.toBe(clone); expect(map.equals(clone)).toBe(true); }); }); diff --git a/spec/order.js b/spec/order.js index a6762e7..5ead802 100644 --- a/spec/order.js +++ b/spec/order.js @@ -333,7 +333,7 @@ function describeOrder(Collection) { var collection = Collection([[[]]]); var clone = collection.clone(); expect(clone).toEqual(collection); - expect(clone).toNotBe(collection); + expect(clone).not.toBe(collection); }); it("should clone with depth 0", function () { @@ -343,14 +343,14 @@ function describeOrder(Collection) { it("should clone with depth 1", function () { var collection = [Collection({})]; - expect(collection.clone(1)).toNotBe(collection); + expect(collection.clone(1)).not.toBe(collection); expect(collection.clone(1).one()).toBe(collection.one()); }); it("should clone with depth 2", function () { var collection = Collection([{a: 10}]); - expect(collection.clone(2)).toNotBe(collection); - expect(collection.clone(2).one()).toNotBe(collection.one()); + expect(collection.clone(2)).not.toBe(collection); + expect(collection.clone(2).one()).not.toBe(collection.one()); expect(collection.clone(2).one()).toEqual(collection.one()); }); diff --git a/spec/set.js b/spec/set.js index 86cfc22..eb4aee2 100644 --- a/spec/set.js +++ b/spec/set.js @@ -96,7 +96,7 @@ function describeSet(Set, sorted) { expect(set.average()).toBe(1.5); expect(set.map(function (n) { return n + 1; - }).indexOf(3)).toNotBe(-1); + }).indexOf(3)).not.toBe(-1); }); it("is iterable", function () { diff --git a/spec/shim-object-spec.js b/spec/shim-object-spec.js index 90fe0f2..1cd66cb 100644 --- a/spec/shim-object-spec.js +++ b/spec/shim-object-spec.js @@ -520,13 +520,13 @@ describe("Object", function () { it("should clone object at one level of depth", function () { var clone = Object.clone(graph, 1); expect(clone).toEqual(graph); - expect(clone).toNotBe(graph); + expect(clone).not.toBe(graph); }); it("should clone object at two levels of depth", function () { var clone = Object.clone(graph, 2); expect(clone).toEqual(graph); - expect(clone.object).toNotBe(graph.object); + expect(clone.object).not.toBe(graph.object); expect(clone.object).toEqual(graph.object); expect(clone.nestedObject.a).toBe(graph.nestedObject.a); }); @@ -538,7 +538,7 @@ describe("Object", function () { it("should clone identical values at least once", function () { var clone = Object.clone(graph); - expect(clone.cycle).toNotBe(graph.cycle); + expect(clone.cycle).not.toBe(graph.cycle); }); it("should clone identical values only once", function () { @@ -563,15 +563,15 @@ describe("Object", function () { it("should clone one level", function () { var clone = Object.clone(object, 1); expect(clone).toEqual(object); - expect(clone).toNotBe(object); + expect(clone).not.toBe(object); expect(clone.a).toBe(object.a); }); it("should clone two levels", function () { var clone = Object.clone(object, 2); expect(clone).toEqual(object); - expect(clone).toNotBe(object); - expect(clone.a).toNotBe(object.a); + expect(clone).not.toBe(object); + expect(clone.a).not.toBe(object.a); }); it("should clone with reference cycles", function () { @@ -579,7 +579,7 @@ describe("Object", function () { cycle.cycle = cycle; var clone = Object.clone(cycle); expect(clone).toEqual(cycle); - expect(clone).toNotBe(cycle); + expect(clone).not.toBe(cycle); expect(clone.cycle).toBe(clone); }); From b8fd60352532e7c3739e238b7265ca73ac0e08c2 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Mon, 17 Mar 2014 20:33:32 -0700 Subject: [PATCH 38/83] Use Jasminum PhantomJS to for browser tests Remove use of function bind --- fast-set.js | 8 +++++++- package.json | 2 +- sorted-set.js | 9 ++++++++- spec/index.js | 37 ------------------------------------- 4 files changed, 16 insertions(+), 40 deletions(-) delete mode 100644 spec/index.js diff --git a/fast-set.js b/fast-set.js index 8e5eefb..a03d4ae 100644 --- a/fast-set.js +++ b/fast-set.js @@ -123,7 +123,13 @@ FastSet.prototype.log = function (charmap, logNode, callback, thisp) { callback = console.log; thisp = console; } - callback = callback.bind(thisp); + + // Bind is unavailable in PhantomJS, the only environment of consequence + // that does not implement it yet. + var originalCallback = callback; + callback = function () { + return originalCallback.apply(thisp, arguments); + }; var buckets = this.buckets; var hashes = buckets.keys(); diff --git a/package.json b/package.json index 8be29b0..06c0bdf 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "sinon": "^1.9.0" }, "scripts": { - "test": "node spec/index.js", + "test": "jasminum spec && jasminum-phantom spec", "cover": "istanbul cover spec/index.js spec && istanbul report html && opener coverage/index.html" } } diff --git a/sorted-set.js b/sorted-set.js index 96a034a..2cad56d 100644 --- a/sorted-set.js +++ b/sorted-set.js @@ -530,7 +530,14 @@ SortedSet.prototype.log = function (charmap, logNode, callback, thisp) { callback = console.log; thisp = console; } - callback = callback.bind(thisp); + + // Bind is unavailable in PhantomJS, the only environment of consequence + // that does not implement it yet. + var originalCallback = callback; + callback = function () { + return originalCallback.apply(thisp, arguments); + }; + if (this.root) { this.root.log(charmap, logNode, callback, callback); } diff --git a/spec/index.js b/spec/index.js deleted file mode 100644 index c6db3f5..0000000 --- a/spec/index.js +++ /dev/null @@ -1,37 +0,0 @@ - -// "Isomprphic" test suite runner. This can be executed either: -// run node test/index.js -// or visit test/index.html (which loads this with Mr) - -var Suite = require("jasminum/suite"); - -var suite = new Suite("Q").describe(function () { - require("./shim-object-spec"); - require("./shim-functions-spec"); - require("./array-spec"); - require("./clone-spec"); - require("./deque-spec"); - require("./dict-spec"); - require("./fast-map-spec"); - require("./fast-set-spec"); - require("./heap-spec"); - require("./iterator-spec"); - require("./list-spec"); - require("./lru-map-spec"); - require("./lru-set-spec"); - require("./map-spec"); - require("./observable-array-spec"); - require("./observable-map-spec"); - require("./observable-object-spec"); - require("./observable-range-spec"); - require("./regexp-spec"); - require("./set-spec"); - require("./sorted-array-map-spec"); - require("./sorted-array-set-spec"); - require("./sorted-array-spec"); - require("./sorted-map-spec"); - require("./sorted-set-spec"); -}); - -suite.runAndReportSync(); - From 05d2c8c87462a8ab569f9a7c18d78fe64f8a63bc Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Wed, 5 Mar 2014 11:37:53 -0800 Subject: [PATCH 39/83] Fix generic map filter The `add` method needed to pass the key through. --- generic-collection.js | 2 +- spec/map.js | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/generic-collection.js b/generic-collection.js index a73bc4c..52cbd77 100644 --- a/generic-collection.js +++ b/generic-collection.js @@ -104,7 +104,7 @@ GenericCollection.prototype.filter = function (callback /*, thisp*/) { var result = this.constructClone(); this.reduce(function (undefined, value, key, object, depth) { if (callback.call(thisp, value, key, object, depth)) { - result.add(value); + result.add(value, key); } }, undefined); return result; diff --git a/spec/map.js b/spec/map.js index f3d7bc1..481fff2 100644 --- a/spec/map.js +++ b/spec/map.js @@ -47,6 +47,16 @@ function describeMap(Map, values) { shouldHaveTheUsualContent(map); }); + it("should support filter", function () { + var map = Map({a: 10, b: 20, c: 30}); + expect(map.filter(function (value, key) { + return key === "a" || value === 30; + }).entries()).toEqual([ + ["a", 10], + ["c", 30] + ]); + }); + describe("delete", function () { it("should remove one entry", function () { var map = Map([[a, 10], [b, 20], [c, 30]]); From f5568a4703f517c692ab1b6059da6c02942d26e5 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Mon, 17 Mar 2014 20:06:54 -0700 Subject: [PATCH 40/83] Add failing test for dictionary map change observers --- spec/dict-spec.js | 5 +++++ spec/observable-map.js | 30 ++++++++++++++++++------------ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/spec/dict-spec.js b/spec/dict-spec.js index 33f2e61..3b977b5 100644 --- a/spec/dict-spec.js +++ b/spec/dict-spec.js @@ -1,11 +1,15 @@ var Dict = require("../dict"); var describeDict = require("./dict"); +var describeObservableMap = require("./observable-map"); describe("Dict", function () { + describeDict(Dict); + describeObservableMap(Dict); it("should throw errors for non-string keys", function () { + var dict = Dict(); expect(function () { dict.get(0); @@ -19,6 +23,7 @@ describe("Dict", function () { expect(function () { dict.delete(0); }).toThrow(); + }); }); diff --git a/spec/observable-map.js b/spec/observable-map.js index 9373fab..7b5cd18 100644 --- a/spec/observable-map.js +++ b/spec/observable-map.js @@ -9,29 +9,35 @@ function describeObservableMap(Map) { it("create, update, delete", function () { var map = new Map(); - var spy = sinon.spy(); - var observer = map.observeMapChange(function (plus, minus, key, type) { - spy(plus, minus, key, type); + var changeSpy = sinon.spy(); + var willChangeSpy = sinon.spy(); + var willChangeObserver = map.observeMapWillChange(function (plus, minus, key, type) { + willChangeSpy(plus, minus, key, type); + }); + var changeObserver = map.observeMapChange(function (plus, minus, key, type) { + changeSpy(plus, minus, key, type); + expect(willChangeSpy.args[willChangeSpy.args.length - 1]).toEqual([plus, minus, key, type]); }); map.set("a", 10); - expect(spy).toHaveBeenCalledWith(10, undefined, "a", "create"); + expect(changeSpy).toHaveBeenCalledWith(10, undefined, "a", "create"); map.set("a", 20); - expect(spy).toHaveBeenCalledWith(20, 10, "a", "update"); + expect(changeSpy).toHaveBeenCalledWith(20, 10, "a", "update"); map.delete("a"); - expect(spy).toHaveBeenCalledWith(undefined, 20, "a", "delete"); + expect(changeSpy).toHaveBeenCalledWith(undefined, 20, "a", "delete"); - spy = sinon.spy(); + changeSpy = sinon.spy(); map.set("a", 30); - expect(spy).toHaveBeenCalledWith(30, undefined, "a", "create"); + expect(changeSpy).toHaveBeenCalledWith(30, undefined, "a", "create"); map.set("a", undefined); - expect(spy).toHaveBeenCalledWith(undefined, 30, "a", "update"); + expect(changeSpy).toHaveBeenCalledWith(undefined, 30, "a", "update"); - spy = sinon.spy(); - observer.cancel(); + changeSpy = sinon.spy(); + changeObserver.cancel(); + willChangeObserver.cancel(); map.set("b", 20); - expect(spy).not.toHaveBeenCalled(); + expect(changeSpy).not.toHaveBeenCalled(); }); } From 63fe35f58e597285e13787fade29ac415cc8b3ca Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Mon, 17 Mar 2014 20:08:48 -0700 Subject: [PATCH 41/83] Fix map change dispatch on Dict --- dict.js | 13 ++++++++----- generic-map.js | 14 +++++++------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/dict.js b/dict.js index 6808f1a..8b5255b 100644 --- a/dict.js +++ b/dict.js @@ -91,14 +91,16 @@ Dict.prototype.has = function (key) { Dict.prototype["delete"] = function (key) { this.assertString(key); var mangled = mangle(key); + var from; if (mangled in this.store) { if (this.dispatchesMapChanges) { - this.dispatchBeforeMapChange(key, this.store[mangled]); + from = this.store[mangled]; + this.dispatchMapWillChange("delete", key, void 0, from); } delete this.store[mangle(key)]; this.length--; if (this.dispatchesMapChanges) { - this.dispatchMapChange(key, undefined); + this.dispatchMapChange("delete", key, void 0, from); } return true; } @@ -106,15 +108,16 @@ Dict.prototype["delete"] = function (key) { }; Dict.prototype.clear = function () { - var key, mangled; + var key, mangled, from; for (mangled in this.store) { key = unmangle(mangled); if (this.dispatchesMapChanges) { - this.dispatchBeforeMapChange(key, this.store[mangled]); + from = this.store[mangled]; + this.dispatchMapWillChange("delete", key, void 0, from); } delete this.store[mangled]; if (this.dispatchesMapChanges) { - this.dispatchMapChange(key, undefined); + this.dispatchMapChange("delete", key, void 0, from); } } this.length = 0; diff --git a/generic-map.js b/generic-map.js index 74cefeb..d7facf7 100644 --- a/generic-map.js +++ b/generic-map.js @@ -95,12 +95,12 @@ GenericMap.prototype['delete'] = function (key) { var from; if (this.dispatchesMapChanges) { from = this.store.get(item).value; - this.dispatchMapWillChange("delete", key, undefined, from); + this.dispatchMapWillChange("delete", key, void 0, from); } this.store["delete"](item); this.length--; if (this.dispatchesMapChanges) { - this.dispatchMapChange("delete", key, undefined, from); + this.dispatchMapChange("delete", key, void 0, from); } return true; } @@ -108,18 +108,18 @@ GenericMap.prototype['delete'] = function (key) { }; GenericMap.prototype.clear = function () { - var keys; + var from; if (this.dispatchesMapChanges) { this.forEach(function (value, key) { - this.dispatchBeforeMapChange(key, value); + this.dispatchMapWillChange("delete", key, void 0, value); }, this); - keys = this.keys(); + from = this.constructClone(this); } this.store.clear(); this.length = 0; if (this.dispatchesMapChanges) { - keys.forEach(function (key) { - this.dispatchMapChange(key); + from.forEach(function (value, key) { + this.dispatchMapChange("delete", key, void 0, value); }, this); } }; From 44905ef2621907c287217ad36465f22c4651691c Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Mon, 17 Mar 2014 15:48:11 -0700 Subject: [PATCH 42/83] Rename maxLength to capacity --- lru-map.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lru-map.js b/lru-map.js index fee012a..fc0c8be 100644 --- a/lru-map.js +++ b/lru-map.js @@ -8,19 +8,20 @@ var ObservableObject = require("./observable-object"); module.exports = LruMap; -function LruMap(values, maxLength, equals, hash, getDefault) { +function LruMap(values, capacity, equals, hash, getDefault) { if (!(this instanceof LruMap)) { - return new LruMap(values, maxLength, equals, hash, getDefault); + return new LruMap(values, capacity, equals, hash, getDefault); } equals = equals || Object.equals; hash = hash || Object.hash; getDefault = getDefault || Function.noop; + this.capacity = capacity || Infinity; this.contentEquals = equals; this.contentHash = hash; this.getDefault = getDefault; this.store = new LruSet( undefined, - maxLength, + capacity, function keysEqual(a, b) { return equals(a.key, b.key); }, @@ -41,7 +42,7 @@ Object.addEach(LruMap.prototype, ObservableObject.prototype); LruMap.prototype.constructClone = function (values) { return new this.constructor( values, - this.maxLength, + this.capacity, this.contentEquals, this.contentHash, this.getDefault From d1438a7c25c81e32a849b721fd1eebbc15476975 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Mon, 17 Mar 2014 20:58:18 -0700 Subject: [PATCH 43/83] Let getDefault to be overridden on the prototype Of any map-like collection. --- dict.js | 4 +++- fast-map.js | 2 +- generic-map.js | 3 +++ lru-map.js | 2 +- map.js | 2 +- sorted-array-map.js | 2 +- sorted-map.js | 2 +- spec/dict.js | 33 +++++++++++++++++++++++++++++++++ spec/map-spec.js | 4 +--- spec/map.js | 8 +++++--- 10 files changed, 50 insertions(+), 12 deletions(-) diff --git a/dict.js b/dict.js index 8b5255b..805dcc6 100644 --- a/dict.js +++ b/dict.js @@ -12,7 +12,7 @@ function Dict(values, getDefault) { if (!(this instanceof Dict)) { return new Dict(values, getDefault); } - getDefault = getDefault || Function.noop; + getDefault = getDefault || this.getDefault; this.getDefault = getDefault; this.store = {}; this.length = 0; @@ -33,6 +33,8 @@ Object.addEach(Dict.prototype, GenericCollection.prototype); Object.addEach(Dict.prototype, GenericMap.prototype); Object.addEach(Dict.prototype, ObservableObject.prototype); +Dict.prototype.isDict = true; + Dict.prototype.constructClone = function (values) { return new this.constructor(values, this.mangle, this.getDefault); }; diff --git a/fast-map.js b/fast-map.js index ebb7d50..4d28aa8 100644 --- a/fast-map.js +++ b/fast-map.js @@ -14,7 +14,7 @@ function FastMap(values, equals, hash, getDefault) { } equals = equals || Object.equals; hash = hash || Object.hash; - getDefault = getDefault || Function.noop; + getDefault = getDefault || this.getDefault; this.contentEquals = equals; this.contentHash = hash; this.getDefault = getDefault; diff --git a/generic-map.js b/generic-map.js index d7facf7..5369db6 100644 --- a/generic-map.js +++ b/generic-map.js @@ -52,6 +52,9 @@ GenericMap.prototype.get = function (key, defaultValue) { } }; +GenericMap.prototype.getDefault = function () { +}; + GenericMap.prototype.set = function (key, value) { var item = new this.Item(key, value); var found = this.store.get(item); diff --git a/lru-map.js b/lru-map.js index fc0c8be..0062182 100644 --- a/lru-map.js +++ b/lru-map.js @@ -14,7 +14,7 @@ function LruMap(values, capacity, equals, hash, getDefault) { } equals = equals || Object.equals; hash = hash || Object.hash; - getDefault = getDefault || Function.noop; + getDefault = getDefault || this.getDefault; this.capacity = capacity || Infinity; this.contentEquals = equals; this.contentHash = hash; diff --git a/map.js b/map.js index 5c2ca64..1b89980 100644 --- a/map.js +++ b/map.js @@ -14,7 +14,7 @@ function Map(values, equals, hash, getDefault) { } equals = equals || Object.equals; hash = hash || Object.hash; - getDefault = getDefault || Function.noop; + getDefault = getDefault || this.getDefault; this.contentEquals = equals; this.contentHash = hash; this.getDefault = getDefault; diff --git a/sorted-array-map.js b/sorted-array-map.js index f614120..55ba226 100644 --- a/sorted-array-map.js +++ b/sorted-array-map.js @@ -14,7 +14,7 @@ function SortedArrayMap(values, equals, compare, getDefault) { } equals = equals || Object.equals; compare = compare || Object.compare; - getDefault = getDefault || Function.noop; + getDefault = getDefault || this.getDefault; this.contentEquals = equals; this.contentCompare = compare; this.getDefault = getDefault; diff --git a/sorted-map.js b/sorted-map.js index 2b3a6f1..388776c 100644 --- a/sorted-map.js +++ b/sorted-map.js @@ -14,7 +14,7 @@ function SortedMap(values, equals, compare, getDefault) { } equals = equals || Object.equals; compare = compare || Object.compare; - getDefault = getDefault || Function.noop; + getDefault = getDefault || this.getDefault; this.contentEquals = equals; this.contentCompare = compare; this.getDefault = getDefault; diff --git a/spec/dict.js b/spec/dict.js index 73e67bc..fa2a03b 100644 --- a/spec/dict.js +++ b/spec/dict.js @@ -45,6 +45,39 @@ function describeDict(Dict) { expect(dict.delete("__proto__")).toBe(false); }); + describe("getDefault", function () { + + it("can be overridden on the prototype", function () { + + var called = false; + + function Memo() { + Dict.call(this); + } + + Memo.prototype = Object.create(Dict.prototype); + Memo.prototype.constructor = Memo; + + Memo.prototype.getDefault = function (key) { + called = true; + this.set(key, key + "!"); + return this.get(key); + }; + + var memo = new Memo(); + + called = false; + expect(memo.get("hi")).toBe("hi!"); + expect(called).toBe(true); + + called = false; + expect(memo.get("hi")).toBe("hi!"); + expect(called).toBe(false); + + }); + + }); + } function shouldHaveTheUsualContent(dict) { diff --git a/spec/map-spec.js b/spec/map-spec.js index 9709635..2585722 100644 --- a/spec/map-spec.js +++ b/spec/map-spec.js @@ -1,11 +1,9 @@ // TODO test insertion order var Map = require("../map"); -var describeDict = require("./dict"); var describeMap = require("./map"); describe("Map", function () { - describeDict(Map); - describeMap(Map); + describeMap(Map); // Subsumes describeDict }); diff --git a/spec/map.js b/spec/map.js index 481fff2..dfa6ed7 100644 --- a/spec/map.js +++ b/spec/map.js @@ -2,6 +2,7 @@ // These do not apply to SortedMap since keys are not comparable. var describeObservableMap = require("./observable-map"); +var describeDict = require("./dict"); module.exports = describeMap; function describeMap(Map, values) { @@ -58,7 +59,7 @@ function describeMap(Map, values) { }); describe("delete", function () { - it("should remove one entry", function () { + it("removes one entry", function () { var map = Map([[a, 10], [b, 20], [c, 30]]); expect(map.delete(c)).toBe(true); shouldHaveTheUsualContent(map); @@ -66,7 +67,7 @@ function describeMap(Map, values) { }); describe("clear", function () { - it("should be able to delete all content", function () { + it("deletes all content", function () { var map = Map({a: 10, b: 20, c: 30}); map.clear(); expect(map.length).toBe(0); @@ -77,7 +78,7 @@ function describeMap(Map, values) { }); describe("equals", function () { - it("should compare maps", function () { + it("compares maps", function () { var map = Map({a: 10, b: 20}); expect(Object.equals(map, map)).toBe(true); expect(map.equals(map)).toBe(true); @@ -98,6 +99,7 @@ function describeMap(Map, values) { }); describeObservableMap(Map); + describeDict(Map); } From d5ba9d2c86ef8192874800e5f6025acaa861b4fe Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Mon, 17 Mar 2014 15:47:18 -0700 Subject: [PATCH 44/83] Support iteration of null or undefined Returning a null (empty) iterator. --- iterator.js | 4 +++- spec/iterator-spec.js | 27 +++++++++++++++------------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/iterator.js b/iterator.js index b48c261..7419ff8 100644 --- a/iterator.js +++ b/iterator.js @@ -7,7 +7,9 @@ var GenericCollection = require("./generic-collection"); // upgrades an iterable to a Iterator function Iterator(iterable, start, stop, step) { - if (iterable instanceof Iterator) { + if (!iterable) { + return Iterator.empty; + } else if (iterable instanceof Iterator) { return iterable; } else if (!(this instanceof Iterator)) { return new Iterator(iterable, start, stop, step); diff --git a/spec/iterator-spec.js b/spec/iterator-spec.js index 042756b..2f763f7 100644 --- a/spec/iterator-spec.js +++ b/spec/iterator-spec.js @@ -21,6 +21,21 @@ function expectCommonIterator(iterator) { function describeIterator(Iterator) { + it("iterates undefined (empty) iteration", function () { + var iterator = Iterator(); + expect(iterator.next()).toEqual({value: undefined, done: true}); + }); + + it("iterates null (empty) iteration", function () { + var iterator = Iterator(null); + expect(iterator.next()).toEqual({value: undefined, done: true}); + }); + + it("iterates empty array iteration", function () { + var iterator = Iterator([]); + expect(iterator.next()).toEqual({value: undefined, done: true}); + }); + it("iterates an array", function () { var iterator = Iterator([1, 2, 3]); expectCommonIterator(iterator); @@ -44,18 +59,6 @@ function describeIterator(Iterator) { expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); }); - it("fails to iterate null", function () { - expect(function () { - Iterator(null); - }).toThrow(); - }); - - it("fails to iterate undefined", function () { - expect(function () { - Iterator(); - }).toThrow(); - }); - it("fails to iterate a number", function () { expect(function () { Iterator(42); From 5debcec847369902536ff3712aa725ca708d3052 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Mon, 17 Mar 2014 15:50:51 -0700 Subject: [PATCH 45/83] Eliminate use of closure for Lru Map observer The new map dispatch code can delegate to a method on the LRU map prototype instead of a closure. Already tested by virtue of LRU map spec. Just a change of implementation. --- lru-map.js | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/lru-map.js b/lru-map.js index 0062182..b7641f1 100644 --- a/lru-map.js +++ b/lru-map.js @@ -63,18 +63,21 @@ LruMap.prototype.observeMapChange = function () { // Detect LRU deletions in the LruSet and emit as MapChanges. // Array and Heap have no store. // Dict and FastMap define no listeners on their store. - var self = this; - this.store.observeRangeWillChange(function(plus, minus) { - if (plus.length && minus.length) { // LRU item pruned - self.dispatchMapWillChange("delete", minus[0].key, undefined, minus[0].value); - } - }); - this.store.observeRangeChange(function(plus, minus) { - if (plus.length && minus.length) { - self.dispatchMapChange("delete", minus[0].key, undefined, minus[0].value); - } - }); + this.store.observeRangeWillChange(this, "store"); + this.store.observeRangeChange(this, "store"); } return GenericMap.prototype.observeMapChange.apply(this, arguments); }; +LruMap.prototype.handleStoreRangeWillChange = function (plus, minus, index) { + if (plus.length && minus.length) { // LRU item pruned + this.dispatchMapWillChange("delete", minus[0].key, undefined, minus[0].value); + } +}; + +LruMap.prototype.handleStoreRangeChange = function (plus, minus, index) { + if (plus.length && minus.length) { + this.dispatchMapChange("delete", minus[0].key, undefined, minus[0].value); + } +}; + From 453ed4f8ebd5c559baacc320e18f71291ce465ee Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Tue, 18 Mar 2014 12:12:30 -0700 Subject: [PATCH 46/83] Fix Object.equals for structurally similar cycles --- shim-object.js | 4 ++-- spec/shim-object-spec.js | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/shim-object.js b/shim-object.js index bea7b56..7349eaa 100644 --- a/shim-object.js +++ b/shim-object.js @@ -347,9 +347,9 @@ Object.equals = function (a, b, equals, memo) { if (Object.isObject(a)) { memo = memo || new WeakMap(); if (memo.has(a)) { - return memo.get(a) === b; + return true; } - memo.set(a, b); + memo.set(a, true); } if (Object.isObject(a) && typeof a.equals === "function") { return a.equals(b, equals, memo); diff --git a/spec/shim-object-spec.js b/spec/shim-object-spec.js index 1cd66cb..75144a9 100644 --- a/spec/shim-object-spec.js +++ b/spec/shim-object-spec.js @@ -308,6 +308,7 @@ describe("Object", function () { }); describe("equals", function () { + var fakeNumber = { valueOf: function () { return 10; @@ -399,6 +400,21 @@ describe("Object", function () { }); }); + it("recognizes deep structural similarity", function () { + var a = []; + a.push(a); + expect(Object.equals(a, [[a]])).toBe(true); + }); + + it("recognizes deep structural dissimilarity", function () { + function Foo() {} + var foo = new Foo(); + expect(Object.equals( + [foo], + [[[foo]]] + )).toBe(false); + }); + }); describe("compare", function () { From beb9fb8bd74878f5a1feb8263228c1edd120108e Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Tue, 18 Mar 2014 12:15:44 -0700 Subject: [PATCH 47/83] Fix Object.clone of functions Allow function objects to be cloned as values. --- shim-object.js | 4 +++- spec/shim-object-spec.js | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/shim-object.js b/shim-object.js index 7349eaa..5923f26 100644 --- a/shim-object.js +++ b/shim-object.js @@ -474,7 +474,9 @@ Object.clone = function (value, depth, memo) { } else if (depth === 0) { return value; } - if (Object.isObject(value)) { + if (typeof value === "function") { + return value; + } else if (Object.isObject(value)) { if (!memo.has(value)) { if (value && typeof value.clone === "function") { memo.set(value, value.clone(depth, memo)); diff --git a/spec/shim-object-spec.js b/spec/shim-object-spec.js index 75144a9..4ddaf4f 100644 --- a/spec/shim-object-spec.js +++ b/spec/shim-object-spec.js @@ -567,6 +567,13 @@ describe("Object", function () { expect(clone.clonable).toBe(graph.clonable); }); + it("should clone an object with a function property", function () { + var original = {foo: function () {}}; + var clone = Object.clone(original); + expect(clone.foo).toBe(original.foo); + expect(Object.equals(clone, original)).toBe(true); + }); + }); describe("clone", function () { From 296721e7b75cbb6b380cda9c12a37cf1b61e26b3 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Tue, 18 Mar 2014 12:18:56 -0700 Subject: [PATCH 48/83] Fix splice start truncation for observed arrays Per ECMAScript, the start must be truncated to the array length. `swap` is not bound by this restriction, which is important for supporting `set(index, value)` for indicies beyond the length of the array and being analogous to `array[index] = value`. --- observable-array.js | 3 +++ spec/observable-array-spec.js | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/observable-array.js b/observable-array.js index 1894064..f0d26e0 100644 --- a/observable-array.js +++ b/observable-array.js @@ -200,6 +200,9 @@ var observableArrayProperties = { splice: { value: function splice(start, length) { + if (start > this.length) { + start = this.length; + } return this.swap.call(this, start, length, array_slice.call(arguments, 2)); }, writable: true, diff --git a/spec/observable-array-spec.js b/spec/observable-array-spec.js index 8fb20f1..3818c5c 100644 --- a/spec/observable-array-spec.js +++ b/spec/observable-array-spec.js @@ -318,3 +318,20 @@ describe("Array changes", function () { }); +describe("splice", function () { + + it("truncates start to length", function () { + var array = []; + array.splice(1000, 0, 1, 2, 3); + expect(array).toEqual([1, 2, 3]); + }); + + it("truncates start to length on an observed array", function () { + var array = []; + array.makeRangeChangesObservable(); + array.splice(1000, 0, 1, 2, 3); + expect(array).toEqual([1, 2, 3]); + }); + +}); + From 3ca52f7021cb3fe137d9a35458c1489e74ff238c Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Tue, 18 Mar 2014 16:42:50 -0700 Subject: [PATCH 49/83] Various swap and splice fixes This change reimplements swap in plain JavaScript so that it can avoid the problems with splice, including the large array problem previously solved, preserves holes in the plus array, and extends the array if the starting position is beyond the length. Splice truncates the start index to the length. As such, splice is not a suitable primitive for the set method, since setting the value at an index should extend the array and leave holes behind. Splice is also not good at copying array holes. Fix array set and swap for indexes beyond the initial length. :warning: Additionally, I have removed the behavior of returning the array of removed values. This is seldom useful and usually generates unnecessary garbage. Fix redundant property change on array indexes Add failing test for redundant property dispatch on unchanged properties of an array during swap. Add test verifying that map changes only dispatch for value change on update. Fix problem with observing the last index of an array. Off by one error in the book keeping logic. Reconcile observed and unobserved array swap. --- observable-array.js | 116 +++++++++++++++++++----------- shim-array.js | 131 ++++++++++++++++++++++++---------- spec/array-spec.js | 60 +++++++++++++++- spec/observable-array-spec.js | 68 +++++++++++++++++- spec/spy-expectation.js | 3 +- 5 files changed, 295 insertions(+), 83 deletions(-) diff --git a/observable-array.js b/observable-array.js index f0d26e0..dbbea34 100644 --- a/observable-array.js +++ b/observable-array.js @@ -47,7 +47,7 @@ var observableArrayProperties = { makePropertyObservable: { value: function (index) { // Is a valid array index: - if (~~index === index && index > 0) { // Note: NaN !== NaN, ~~"foo" !== "foo" + if (~~index === index && index >= 0) { // Note: NaN !== NaN, ~~"foo" !== "foo" this.makeIndexObservable(index); } // Does not call through to super because property dispatch on @@ -61,7 +61,7 @@ var observableArrayProperties = { makeIndexObservable: { value: function (index) { var maxObservedIndex = observedLengthForObject.get(this) || 0; - if (index > maxObservedIndex) { + if (index >= maxObservedIndex) { observedLengthForObject.set(this, index + 1); } }, @@ -70,7 +70,7 @@ var observableArrayProperties = { }, swap: { - value: function swap(start, length, plus) { + value: function swap(start, minusLength, plus) { if (plus) { if (!Array.isArray(plus)) { plus = array_slice.call(plus); @@ -81,9 +81,28 @@ var observableArrayProperties = { if (start < 0) { start = this.length + start; + } else if (start > this.length) { + var holes = start - this.length; + var newPlus = Array(holes + plus.length); + for (var i = 0, j = holes; i < plus.length; i++, j++) { + if (i in plus) { + newPlus[j] = plus[i]; + } + } + plus = newPlus; + start = this.length; + } + + if (start + minusLength > this.length) { + // Truncate minus length if it extends beyond the length + minusLength = this.length - start; + } else if (minusLength < 0) { + // It is the JavaScript way. + minusLength = 0; } + var minus; - if (length === 0) { + if (minusLength === 0) { // minus will be empty if (plus.length === 0) { // at this point if plus is empty there is nothing to do. @@ -91,8 +110,9 @@ var observableArrayProperties = { } minus = Array.empty; } else { - minus = array_slice.call(this, start, start + length); + minus = array_slice.call(this, start, start + minusLength); } + var diff = plus.length - minus.length; var oldLength = this.length; var newLength = Math.max(this.length + diff, start + plus.length); @@ -107,8 +127,10 @@ var observableArrayProperties = { if (diff === 0) { // Substring replacement for (var i = start, j = 0; i < start + plus.length; i++, j++) { - this.dispatchPropertyWillChange(i, plus[j], minus[j]); - this.dispatchMapWillChange("update", i, plus[j], minus[j]); + if (plus[j] !== minus[j]) { + this.dispatchPropertyWillChange(i, plus[j], minus[j]); + this.dispatchMapWillChange("update", i, plus[j], minus[j]); + } } } else { // All subsequent values changed or shifted. @@ -117,22 +139,32 @@ var observableArrayProperties = { for (var i = start, j = 0; i < observedLength; i++, j++) { if (i < oldLength && i < newLength) { // update if (j < plus.length) { - this.dispatchPropertyWillChange(i, plus[j], this[i]); - this.dispatchMapWillChange("update", i, plus[j], this[i]); + if (plus[j] !== this[i]) { + this.dispatchPropertyWillChange(i, plus[j], this[i]); + this.dispatchMapWillChange("update", i, plus[j], this[i]); + } } else { - this.dispatchPropertyWillChange(i, this[i - diff], this[i]); - this.dispatchMapWillChange("update", i, this[i - diff], this[i]); + if (this[i - diff] !== this[i]) { + this.dispatchPropertyWillChange(i, this[i - diff], this[i]); + this.dispatchMapWillChange("update", i, this[i - diff], this[i]); + } } } else if (i < newLength) { // but i >= oldLength, create if (j < plus.length) { - this.dispatchPropertyWillChange(i, plus[j]); + if (plus[j] !== void 0) { + this.dispatchPropertyWillChange(i, plus[j]); + } this.dispatchMapWillChange("create", i, plus[j]); } else { - this.dispatchPropertyWillChange(i, this[i - diff]); + if (this[i - diff] !== void 0) { + this.dispatchPropertyWillChange(i, this[i - diff]); + } this.dispatchMapWillChange("create", i, this[i - diff]); } } else if (i < oldLength) { // but i >= newLength, delete - this.dispatchPropertyWillChange(i, void 0, this[i]); + if (this[i] !== void 0) { + this.dispatchPropertyWillChange(i, void 0, this[i]); + } this.dispatchMapWillChange("delete", i, void 0, this[i]); } else { throw new Error("assertion error"); @@ -141,16 +173,15 @@ var observableArrayProperties = { } // actual work - if (start > oldLength) { - this.length = start; - } - var result = array_swap.call(this, start, length, plus); + array_swap.call(this, start, minusLength, plus); // dispatch after change events if (diff === 0) { // substring replacement for (var i = start, j = 0; i < start + plus.length; i++, j++) { - this.dispatchPropertyChange(i, plus[j], minus[j]); - this.dispatchMapChange("update", i, plus[j], minus[j]); + if (plus[j] !== minus[j]) { + this.dispatchPropertyChange(i, plus[j], minus[j]); + this.dispatchMapChange("update", i, plus[j], minus[j]); + } } } else { // All subsequent values changed or shifted. @@ -159,26 +190,38 @@ var observableArrayProperties = { for (var i = start, j = 0; i < observedLength; i++, j++) { if (i < oldLength && i < newLength) { // update if (j < minus.length) { - this.dispatchPropertyChange(i, this[i], minus[j]); - this.dispatchMapChange("update", i, this[i], minus[j]); + if (this[i] !== minus[j]) { + this.dispatchPropertyChange(i, this[i], minus[j]); + this.dispatchMapChange("update", i, this[i], minus[j]); + } } else { - this.dispatchPropertyChange(i, this[i], this[i + diff]); - this.dispatchMapChange("update", i, this[i], this[i + diff]); + if (this[i] !== this[i + diff]) { + this.dispatchPropertyChange(i, this[i], this[i + diff]); + this.dispatchMapChange("update", i, this[i], this[i + diff]); + } } } else if (i < newLength) { // but i >= oldLength, create if (j < minus.length) { - this.dispatchPropertyChange(i, this[i], minus[j]); + if (this[i] !== minus[j]) { + this.dispatchPropertyChange(i, this[i], minus[j]); + } this.dispatchMapChange("create", i, this[i], minus[j]); } else { - this.dispatchPropertyChange(i, this[i], this[i + diff]); + if (this[i] !== this[i + diff]) { + this.dispatchPropertyChange(i, this[i], this[i + diff]); + } this.dispatchMapChange("create", i, this[i], this[i + diff]); } } else if (i < oldLength) { // but i >= newLength, delete if (j < minus.length) { - this.dispatchPropertyChange(i, void 0, minus[j]); + if (minus[j] !== void 0) { + this.dispatchPropertyChange(i, void 0, minus[j]); + } this.dispatchMapChange("delete", i, void 0, minus[j]); } else { - this.dispatchPropertyChange(i, void 0, this[i + diff]); + if (this[i + diff] !== void 0) { + this.dispatchPropertyChange(i, void 0, this[i + diff]); + } this.dispatchMapChange("delete", i, void 0, this[i + diff]); } } else { @@ -191,19 +234,19 @@ var observableArrayProperties = { if (diff) { this.dispatchPropertyChange("length", newLength, oldLength); } - - return result; }, writable: true, configurable: true }, splice: { - value: function splice(start, length) { + value: function splice(start, minusLength) { if (start > this.length) { start = this.length; } - return this.swap.call(this, start, length, array_slice.call(arguments, 2)); + var result = this.slice(start, start + minusLength); + this.swap.call(this, start, minusLength, array_slice.call(arguments, 2)); + return result; }, writable: true, configurable: true @@ -235,15 +278,6 @@ var observableArrayProperties = { configurable: true }, - set: { - value: function set(index, value) { - this.splice(index, 1, value); - return this; - }, - writable: true, - configurable: true - }, - shift: { value: function shift() { return this.splice(0, 1)[0]; diff --git a/shim-array.js b/shim-array.js index 131d18a..04fe23d 100644 --- a/shim-array.js +++ b/shim-array.js @@ -99,8 +99,14 @@ define("get", function (index, defaultValue) { }); define("set", function (index, value) { - this.splice(index, 1, value); - return true; + if (index < this.length) { + this.splice(index, 1, value); + } else { + // Must use swap instead of splice, dispite the unfortunate array + // argument, because splice would truncate index to length. + this.swap(index, 1, [value]); + } + return this; }); define("add", function (value) { @@ -139,48 +145,101 @@ define("findLastValue", function (value, equals) { return -1; }); -define("swap", function (start, length, plus) { - var args, plusLength, i, j, returnValue; - if (typeof plus !== "undefined") { - args = [start, length]; +define("swap", function (start, minusLength, plus) { + // Unrolled implementation into JavaScript for a couple reasons. + // Calling splice can cause large stack sizes for large swaps. Also, + // splice cannot handle array holes. + if (plus) { if (!Array.isArray(plus)) { plus = array_slice.call(plus); } - i = 0; - plusLength = plus.length; - // 1000 is a magic number, presumed to be smaller than the remaining - // stack length. For swaps this small, we take the fast path and just - // use the underlying Array splice. We could measure the exact size of - // the remaining stack using a try/catch around an unbounded recursive - // function, but this would defeat the purpose of short-circuiting in - // the common case. - if (plusLength < 1000) { - for (i; i < plusLength; i++) { - args[i+2] = plus[i]; + } else { + plus = Array.empty; + } + + if (start < 0) { + start = this.length + start; + } else if (start > this.length) { + this.length = start; + } + + if (start + minusLength > this.length) { + // Truncate minus length if it extends beyond the length + minusLength = this.length - start; + } else if (minusLength < 0) { + // It is the JavaScript way. + minusLength = 0; + } + + var diff = plus.length - minusLength; + var oldLength = this.length; + var newLength = this.length + diff; + + if (diff > 0) { + // Head Tail Plus Minus + // H H H H M M T T T T + // H H H H P P P P T T T T + // ^ start + // ^-^ minus.length + // ^ --> diff + // ^-----^ plus.length + // ^------^ tail before + // ^------^ tail after + // ^ start iteration + // ^ start iteration offset + // ^ end iteration + // ^ end iteration offset + // ^ start + minus.length + // ^ length + // ^ length - 1 + for (var index = oldLength - 1; index >= start + minusLength; index--) { + var offset = index + diff; + if (index in this) { + this[offset] = this[index]; + } else { + // Oddly, PhantomJS complains about deleting array + // properties, unless you assign undefined first. + this[offset] = void 0; + delete this[offset]; } - return array_splice.apply(this, args); + } + } + for (var index = 0; index < plus.length; index++) { + if (index in plus) { + this[start + index] = plus[index]; } else { - // Avoid maximum call stack error. - // First delete the desired entries. - returnValue = array_splice.apply(this, args); - // Second batch in 1000s. - for (i; i < plusLength;) { - args = [start+i, 0]; - for (j = 2; j < 1002 && i < plusLength; j++, i++) { - args[j] = plus[i]; - } - array_splice.apply(this, args); + this[start + index] = void 0; + delete this[start + index]; + } + } + if (diff < 0) { + // Head Tail Plus Minus + // H H H H M M M M T T T T + // H H H H P P T T T T + // ^ start + // ^-----^ length + // ^-^ plus.length + // ^ start iteration + // ^ offset start iteration + // ^ end + // ^ offset end + // ^ start + minus.length - plus.length + // ^ start - diff + // ^------^ tail before + // ^------^ tail after + // ^ length - diff + // ^ newLength + for (var index = start + plus.length; index < oldLength - diff; index++) { + var offset = index - diff; + if (offset in this) { + this[index] = this[offset]; + } else { + this[index] = void 0; + delete this[index]; } - return returnValue; } - // using call rather than apply to cut down on transient objects - } else if (typeof length !== "undefined") { - return array_splice.call(this, start, length); - } else if (typeof start !== "undefined") { - return array_splice.call(this, start); - } else { - return []; } + this.length = newLength; }); define("peek", function () { diff --git a/spec/array-spec.js b/spec/array-spec.js index 915ad4e..b1f900a 100644 --- a/spec/array-spec.js +++ b/spec/array-spec.js @@ -212,7 +212,58 @@ describe("Array", function () { beforeEach(function () { array = [1, 2, 3]; }); - it("should be able to replace content with content of another arraylike", function () { + + it("grows", function () { + array.swap(3, 0, [4, 5, 6]); + expect(array).toEqual([1, 2, 3, 4, 5, 6]); + }); + + it("grows with some removed", function () { + array.swap(1, 2, [4, 5, 6]); + expect(array).toEqual([1, 4, 5, 6]); + }); + + it("grows from beyond length", function () { + array.swap(4, 0, [1]); + expect(array).toEqual([1, 2, 3, , 1]); + }); + + it("grows from beyond length truncating removal", function () { + array.swap(4, 1, [1]); + expect(array).toEqual([1, 2, 3, , 1]); + }); + + it("shrinks", function () { + array.swap(1, 1); + expect(array).toEqual([1, 3]); + }); + + it("shrinks with some added", function () { + array.swap(1, 2, [4]); + expect(array).toEqual([1, 4]); + }); + + it("copies a hole", function () { + array.swap(1, 1, [,]); + expect(array).toEqual([1, , 3]); + }); + + it("copies holes", function () { + array.swap(1, 1, [,,]); + expect(array).toEqual([1, , , 3]); + }); + + it("sets within", function () { + array.set(1, 4); + expect(array).toEqual([1, 4, 3]); + }); + + it("sets without", function () { + array.set(4, 4); + expect(array).toEqual([1, 2, 3, , 4]); + }); + + it("can replace content with content of another arraylike", function () { otherArray = { __proto__ : Array.prototype }; otherArray[0] = 4; otherArray[1] = 5; @@ -220,23 +271,26 @@ describe("Array", function () { array.swap(0, array.length, otherArray); expect(array).toEqual([4, 5]); }); + it("should ignore non array like plus value", function () { array.swap(0, array.length, 4); expect(array).toEqual([]); }); + it("should ignore extra arguments", function () { array.swap(0, array.length, 4, 5, 6); expect(array).toEqual([]); - }); - it("should work with large arrays", function () { + + it("works with large arrays", function () { otherArray = new Array(200000); expect(function () { array.swap(0, array.length, otherArray); }).not.toThrow(); expect(array.length).toEqual(200000); }); + }); }); diff --git a/spec/observable-array-spec.js b/spec/observable-array-spec.js index 3818c5c..eef6da3 100644 --- a/spec/observable-array-spec.js +++ b/spec/observable-array-spec.js @@ -5,6 +5,8 @@ require("../observable-array"); // TODO var describeObservableRange = require("./observable-range"); // TODO make Array.from consistent with List +extendSpyExpectation(); + describe("Array", function () { it("change dispatch properties should not be enumerable", function () { // this verifies that dispatchesRangeChanges and dispatchesMapChanges @@ -262,14 +264,37 @@ describe("Array change dispatch with map observers", function () { ]); }); + it("does not dispatch redundant map changes", function () { + array.length = 3; + spy = sinon.spy(); + array.set(0, void 0); + array.set(0, void 0); + array.set(0, 1); + expect(spy.args).toEqual([ + + // opens holes + ["range will change from", [,], "to", [void 0], "at", 0], + ["range change from", [,], "to", [void 0], "at", 0], + + // no real change + ["range will change from", [void 0], "to", [void 0], "at", 0], + ["range change from", [void 0], "to", [void 0], "at", 0], + + // map change + ["range will change from", [void 0], "to", [1], "at", 0], + ["map will", "update", 0, "from", void 0, "to", 1], + ["map", "update", 0, "from", void 0, "to", 1], + ["range change from", [void 0], "to", [1], "at", 0] + + ]); + }); + // TODO cancel observers }); describe("Array changes", function () { - extendSpyExpectation(); - it("observes range changes on arrays that are not otherwised observed", function () { var array = [1, 2, 3]; var spy = sinon.spy(); @@ -302,6 +327,19 @@ describe("Array changes", function () { expect(spy).toHaveBeenCalledWith(4, undefined, 3, array); }); + it("does not observe redundant property changes", function () { + var array = []; + var spy = sinon.spy(); + array.observePropertyChange(0, spy); + array.set(0, 1); + expect(array).toEqual([1]); + expect(spy).toHaveBeenCalledWith(1, void 0, 0, array); + + spy.args.clear(); + array.set(0, 1); + expect(spy).not.toHaveBeenCalled(); + }); + describe("swap", function () { it("works with large arrays", function () { var array = []; @@ -335,3 +373,29 @@ describe("splice", function () { }); +describe("swap", function () { + it("grows the array if start beyond length", function () { + var array = []; + var spy = sinon.spy(); + array.observeRangeChange(function (plus, minus, index) { + spy(plus, minus, index); + }); + array.swap(4, 0, [1, 2, 3]); + expect(spy).toHaveBeenCalledWith([ , , , , 1, 2, 3], [], 0); + expect(array).toEqual([ , , , , 1, 2, 3]); + }); +}); + +describe("set", function () { + it("grows the array if start beyond length", function () { + var array = []; + var spy = sinon.spy(); + array.observeRangeChange(function (plus, minus, index) { + spy(plus, minus, index); + }); + array.set(4, 1); + expect(spy).toHaveBeenCalledWith([ , , , , 1], [], 0); + expect(array).toEqual([ , , , , 1]); + }); +}); + diff --git a/spec/spy-expectation.js b/spec/spy-expectation.js index 3c7b5c2..bd5f9c6 100644 --- a/spec/spy-expectation.js +++ b/spec/spy-expectation.js @@ -16,8 +16,9 @@ function extendSpyExpectation() { Expectation.prototype.toHaveBeenCalled = function () { this.assert(!!this.value.args.length, [ - "expected spy [not] to have been called" + "expected spy [not] to have been called but calls were" ], [ + this.value.args ]); }; From a9185ae23ea3b26c4e8d41f26916c45b20586511 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Tue, 18 Mar 2014 21:20:40 -0700 Subject: [PATCH 50/83] Refine compare Use +/-Infinity to compare values that vary by indeterminate scale, like strings, such that the magnitude of a comparison is only useful for numbers. Allow null to be compared to objects that implement compare. --- shim-object.js | 14 +++++--------- spec/shim-object-spec.js | 2 +- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/shim-object.js b/shim-object.js index 5923f26..2d368a9 100644 --- a/shim-object.js +++ b/shim-object.js @@ -432,19 +432,15 @@ Object.compare = function (a, b) { return 0; var aType = typeof a; var bType = typeof b; - if (aType !== bType) - return 0; - if (a == null) - return b == null ? 0 : -1; - if (aType === "number") + if (aType === "number" && bType === "number") return a - b; - if (aType === "string") - return a < b ? -1 : 1; + if (aType === "string" && bType === "string") + return a < b ? -Infinity : Infinity; // the possibility of equality elimiated above - if (typeof a.compare === "function") + if (a && typeof a.compare === "function") return a.compare(b); // not commutative, the relationship is reversed - if (typeof b.compare === "function") + if (b && typeof b.compare === "function") return -b.compare(a); return 0; }; diff --git a/spec/shim-object-spec.js b/spec/shim-object-spec.js index 4ddaf4f..cc34d78 100644 --- a/spec/shim-object-spec.js +++ b/spec/shim-object-spec.js @@ -445,7 +445,7 @@ describe("Object", function () { [[10], [10], 0], [[10], [20], -10], [[100, 10], [100, 0], 10], - ["a", "b", -1], + ["a", "b", -Infinity], [now, now, 0, "now to itself"], [ comparable.create(function () { From 30f4aee6bd4461c1d73083a7b64032d2dacd4728 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Tue, 18 Mar 2014 21:23:34 -0700 Subject: [PATCH 51/83] Update dependencies --- package.json | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 06c0bdf..8f9943d 100644 --- a/package.json +++ b/package.json @@ -28,14 +28,13 @@ "url": "http://github.com/montagejs/collections.git" }, "dependencies": { - "weak-map": "1.0.0" + "weak-map": "^1.0.0" }, "devDependencies": { - "jasminum": "~1.0.0", - "istanbul": "~0.2.4", - "opener": "~1.3.0", - "mr": "~0.15.1", - "sinon": "^1.9.0" + "jasminum": "^2.0.0", + "sinon": "^1.9.0", + "istanbul": "^0.2.4", + "opener": "^1.3.0" }, "scripts": { "test": "jasminum spec && jasminum-phantom spec", From 9c1b083add0ffc91b25453b852499c310220dca6 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Wed, 19 Mar 2014 18:19:31 -0700 Subject: [PATCH 52/83] Version 2.0.0 Introduces backward incompatibilities. Recommended only for adventurers. --- CHANGES.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 7351ddb..cb29d72 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,8 @@ +## v2.0.0 :warning: BACKWARD INCOMPATIBLE + +- Iterators have been reimplemented with an ES6 alike interface. +- Change observers have been reimplemented with an FRB alike interface. - Removes support for `any` and `all`. Use `some(Boolean)` or `every(Boolean)`. diff --git a/package.json b/package.json index 8f9943d..826625b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "collections", - "version": "0.2.2", + "version": "2.0.0", "description": "data structures with idiomatic JavaScript collection interfaces", "homepage": "http://github.com/montagejs/collections", "author": "Kris Kowal (http://github.com/kriskowal)", From da230ef12757bdddb2a4a32c947c9e811e4846db Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Thu, 20 Mar 2014 12:57:09 -0700 Subject: [PATCH 53/83] Version 2.0.1 Synchronize dependencies --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 826625b..4b90793 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "collections", - "version": "2.0.0", + "version": "2.0.1", "description": "data structures with idiomatic JavaScript collection interfaces", "homepage": "http://github.com/montagejs/collections", "author": "Kris Kowal (http://github.com/kriskowal)", @@ -28,10 +28,10 @@ "url": "http://github.com/montagejs/collections.git" }, "dependencies": { - "weak-map": "^1.0.0" + "weak-map": "^1.0.4" }, "devDependencies": { - "jasminum": "^2.0.0", + "jasminum": "^2.0.1", "sinon": "^1.9.0", "istanbul": "^0.2.4", "opener": "^1.3.0" From bb143234722ed5bf2949892c1a1090ae5591338e Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Thu, 3 Apr 2014 14:00:18 -0700 Subject: [PATCH 54/83] Fix SortedArray length update on push pop Fixes #64 --- sorted-array.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sorted-array.js b/sorted-array.js index f817de0..5e88516 100644 --- a/sorted-array.js +++ b/sorted-array.js @@ -175,11 +175,15 @@ SortedArray.prototype.unshift = function () { }; SortedArray.prototype.pop = function () { - return this.array.pop(); + var val = this.array.pop(); + this.length = this.array.length; + return val; }; SortedArray.prototype.shift = function () { - return this.array.shift(); + var val = this.array.shift(); + this.length = this.array.length; + return val; }; SortedArray.prototype.slice = function () { From f63e028bb7f3015c7fbbb054e96aa36a19c30a2e Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Thu, 15 May 2014 22:40:14 -0700 Subject: [PATCH 55/83] Iterate maps and objects properly --- dict.js | 23 +++++++++++++++++++++++ generic-map.js | 12 ++++++------ iterator.js | 20 ++++++++++++++++++-- spec/dict.js | 19 +++++++++++++++++++ spec/iterator-spec.js | 14 ++++++++++++++ 5 files changed, 80 insertions(+), 8 deletions(-) diff --git a/dict.js b/dict.js index 805dcc6..6fa5148 100644 --- a/dict.js +++ b/dict.js @@ -4,6 +4,7 @@ var Shim = require("./shim"); var GenericCollection = require("./generic-collection"); var GenericMap = require("./generic-map"); var ObservableObject = require("./observable-object"); +var Iterator = require("./iterator"); // Burgled from https://github.com/domenic/dict @@ -147,3 +148,25 @@ Dict.prototype.one = function () { } }; +Dict.prototype.iterate = function () { + return new this.Iterator(new Iterator(this.store)); +}; + +Dict.prototype.Iterator = DictIterator; + +function DictIterator(storeIterator) { + this.storeIterator = storeIterator; +} + +DictIterator.prototype.next = function () { + var iteration = this.storeIterator.next(); + if (iteration.done) { + return iteration; + } else { + return new Iterator.Iteration( + iteration.value, + unmangle(iteration.index) + ); + } +}; + diff --git a/generic-map.js b/generic-map.js index 5369db6..27c2fb7 100644 --- a/generic-map.js +++ b/generic-map.js @@ -128,7 +128,7 @@ GenericMap.prototype.clear = function () { }; GenericMap.prototype.iterate = function () { - return new GenericMapIterator(this); + return new this.Iterator(this); }; GenericMap.prototype.reduce = function (callback, basis, thisp) { @@ -181,6 +181,7 @@ GenericMap.prototype.equals = function (that, equals) { }; GenericMap.prototype.Item = Item; +GenericMap.prototype.Iterator = GenericMapIterator; function Item(key, value) { this.key = key; @@ -196,21 +197,20 @@ Item.prototype.compare = function (that) { }; function GenericMapIterator(map) { - this.map = map; - this.iterator = map.store.iterate(); + this.storeIterator = new Iterator(map.store); } GenericMapIterator.prototype = Object.create(Iterator.prototype); GenericMapIterator.prototype.constructor = GenericMapIterator; GenericMapIterator.prototype.next = function () { - var iteration = this.iterator.next(); + var iteration = this.storeIterator.next(); if (iteration.done) { return iteration; } else { return new Iterator.Iteration( - iteration.value[1], - iteration.value[0] + iteration.value.value, + iteration.value.key ); } }; diff --git a/iterator.js b/iterator.js index 7419ff8..59e20a0 100644 --- a/iterator.js +++ b/iterator.js @@ -24,6 +24,8 @@ function Iterator(iterable, start, stop, step) { iterators.set(this, iterable.iterate(start, stop, step)); } else if (Object.prototype.toString.call(iterable) === "[object Function]") { this.next = iterable; + } else if (Object.getPrototypeOf(iterable) === Object.prototype) { + iterators.set(this, new ObjectIterator(iterable)); } else { throw new TypeError("Can't iterate " + iterable); } @@ -60,8 +62,7 @@ Iterator.prototype.constructClone = function (values) { }; // A level of indirection so a full-interface iterator can proxy for a simple -// nextable iterator, and to allow the child iterator to replace its governing -// iterator, as with drop-while iterators. +// nextable iterator. Iterator.prototype.next = function () { var nextable = iterators.get(this); if (nextable) { @@ -333,6 +334,21 @@ IndexIterator.prototype.next = function () { return iteration; }; +function ObjectIterator(object) { + this.object = object; + this.iterator = new Iterator(Object.keys(object)); +} + +ObjectIterator.prototype.next = function () { + var iteration = this.iterator.next(); + if (iteration.done) { + return iteration; + } else { + var key = iteration.value; + return new Iteration(this.object[key], key); + } +}; + Iterator.cycle = function (cycle, times) { if (arguments.length < 2) { times = Infinity; diff --git a/spec/dict.js b/spec/dict.js index fa2a03b..22ddffd 100644 --- a/spec/dict.js +++ b/spec/dict.js @@ -78,6 +78,25 @@ function describeDict(Dict) { }); + describe("iterate", function () { + it("should iterate a dictionary", function () { + var dict = new Dict({a: 10, b: 20, c: 30}); + var iterator = dict.iterate(); + expect(iterator.next()).toEqual({value: 10, index: "a", done: false}); + }); + }); + + describe("some", function () { + it("can enumerate the content of a dict", function () { + var dict = new Dict({only: 10}); + expect(dict.some(function (value, key) { + expect(key).toBe("only"); + expect(value).toBe(10); + return value === 10; + })).toBe(true); + }); + }); + } function shouldHaveTheUsualContent(dict) { diff --git a/spec/iterator-spec.js b/spec/iterator-spec.js index 2f763f7..cce9f33 100644 --- a/spec/iterator-spec.js +++ b/spec/iterator-spec.js @@ -36,6 +36,11 @@ function describeIterator(Iterator) { expect(iterator.next()).toEqual({value: undefined, done: true}); }); + it("iterates empty object iteration", function () { + var iterator = Iterator({}); + expect(iterator.next()).toEqual({value: undefined, done: true}); + }); + it("iterates an array", function () { var iterator = Iterator([1, 2, 3]); expectCommonIterator(iterator); @@ -50,6 +55,15 @@ function describeIterator(Iterator) { expect(Object.equals(iterator.next(), {value: undefined, index: undefined, done: true})).toBe(true); }); + it("iterates an object", function () { + var iterator = Iterator({a: 10, b: 20, c: 30}); + expect(iterator.next()).toEqual({value: 10, index: "a", done: false}); + expect(iterator.next()).toEqual({value: 20, index: "b", done: false}); + expect(iterator.next()).toEqual({value: 30, index: "c", done: false}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + expect(iterator.next()).toEqual({value: undefined, index: undefined, done: true}); + }); + it("iterates a string", function () { var iterator = Iterator("abc"); expect(Object.equals(iterator.next(), {value: "a", index: 0, done: false})).toBe(true); From 717ab528e3f7c485d6fd4b523577993d780c6268 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Thu, 15 May 2014 22:40:39 -0700 Subject: [PATCH 56/83] Remove deprecated map items method --- generic-map.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/generic-map.js b/generic-map.js index 27c2fb7..155b0b6 100644 --- a/generic-map.js +++ b/generic-map.js @@ -159,11 +159,6 @@ GenericMap.prototype.entries = function () { }); }; -// XXX deprecated -GenericMap.prototype.items = function () { - return this.entries(); -}; - GenericMap.prototype.equals = function (that, equals) { equals = equals || Object.equals; if (this === that) { From 8cc1ad4a8370fdb807d174656fe9d5056e9793e6 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Thu, 15 May 2014 22:41:10 -0700 Subject: [PATCH 57/83] Take beter care to use swap instead of splice In observable arrays, to avoid unnecessary allocation. --- observable-array.js | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/observable-array.js b/observable-array.js index dbbea34..87e04ca 100644 --- a/observable-array.js +++ b/observable-array.js @@ -280,7 +280,11 @@ var observableArrayProperties = { shift: { value: function shift() { - return this.splice(0, 1)[0]; + if (this.length) { + var result = this[0]; + this.swap(0, 1); + return result; + } }, writable: true, configurable: true @@ -289,7 +293,9 @@ var observableArrayProperties = { pop: { value: function pop() { if (this.length) { - return this.splice(this.length - 1, 1)[0]; + var result = this[this.length - 1]; + this.swap(this.length - 1, 1); + return result; } }, writable: true, @@ -297,26 +303,18 @@ var observableArrayProperties = { }, push: { - value: function push(arg) { - if (arguments.length === 1) { - return this.splice(this.length, 0, arg); - } else { - var args = array_slice.call(arguments); - return this.swap(this.length, 0, args); - } + value: function push(value) { + this.swap(this.length, 0, arguments); + return this.length; }, writable: true, configurable: true }, unshift: { - value: function unshift(arg) { - if (arguments.length === 1) { - return this.splice(0, 0, arg); - } else { - var args = array_slice.call(arguments); - return this.swap(0, 0, args); - } + value: function unshift(value) { + this.swap(0, 0, arguments); + return this.length; }, writable: true, configurable: true @@ -324,7 +322,7 @@ var observableArrayProperties = { clear: { value: function clear() { - return this.splice(0, this.length); + this.swap(0, this.length); }, writable: true, configurable: true From dd3a303738fdf285d2b9ce782698f51a696eb7d6 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Thu, 15 May 2014 22:43:17 -0700 Subject: [PATCH 58/83] Fix initial state check for property observers --- observable-object.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/observable-object.js b/observable-object.js index c922e89..9b088ba 100644 --- a/observable-object.js +++ b/observable-object.js @@ -479,11 +479,14 @@ function makeValuePropertyThunk(name, wrappedDescriptor) { // upward in the prototype chain. if (this.__state__ === void 0 || this.__state__.__this__ !== this) { initState(this); - // Get the initial value from up the prototype chain - this.__state__[name] = wrappedDescriptor.value; } var state = this.__state__; + if (!(name in state)) { + // Get the initial value from up the prototype chain + state[name] = wrappedDescriptor.value; + } + return state[name]; }, set: function (plus) { @@ -495,6 +498,11 @@ function makeValuePropertyThunk(name, wrappedDescriptor) { } var state = this.__state__; + if (!(name in state)) { + // Get the initial value from up the prototype chain + state[name] = wrappedDescriptor.value; + } + if (plus === state[name]) { return plus; } From 7d5e2782f256aaa04ee7fc103b8c434e9f7a6371 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Thu, 15 May 2014 22:43:39 -0700 Subject: [PATCH 59/83] Ensure that v2 does not get published to npm as latest --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 4b90793..ab2478e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,9 @@ { "name": "collections", "version": "2.0.1", + "publishConfig": { + "tag": "future" + }, "description": "data structures with idiomatic JavaScript collection interfaces", "homepage": "http://github.com/montagejs/collections", "author": "Kris Kowal (http://github.com/kriskowal)", From 63df92fbf639e117842a36bba11bbd6f18e7d36d Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Thu, 15 May 2014 22:44:03 -0700 Subject: [PATCH 60/83] Allow object holes to be treated as undefined For the purpose of Object.equals tests on object literals. Previously, {x: undefined} and {} were not considered equivalent. --- observable-range.js | 1 + shim-object.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/observable-range.js b/observable-range.js index 093ec03..d90f5aa 100644 --- a/observable-range.js +++ b/observable-range.js @@ -216,6 +216,7 @@ RangeChangeObserver.prototype.dispatch = function (plus, minus, index) { } this.childObserver = childObserver; + return this; }; diff --git a/shim-object.js b/shim-object.js index 2d368a9..924280d 100644 --- a/shim-object.js +++ b/shim-object.js @@ -366,7 +366,7 @@ Object.equals = function (a, b, equals, memo) { } } for (var name in b) { - if (!(name in a) || !equals(b[name], a[name], equals, memo)) { + if (!equals(b[name], a[name], equals, memo)) { return false; } } From 705d553681377b8ed0b083c3b2c63af170d4cea6 Mon Sep 17 00:00:00 2001 From: Chris Barrick Date: Thu, 3 Apr 2014 16:38:40 -0400 Subject: [PATCH 61/83] Fix #64 --- sorted-array.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sorted-array.js b/sorted-array.js index 5e88516..c37ff1e 100644 --- a/sorted-array.js +++ b/sorted-array.js @@ -175,15 +175,15 @@ SortedArray.prototype.unshift = function () { }; SortedArray.prototype.pop = function () { - var val = this.array.pop(); + var value = this.array.pop(); this.length = this.array.length; - return val; + return value; }; SortedArray.prototype.shift = function () { - var val = this.array.shift(); + var value = this.array.shift(); this.length = this.array.length; - return val; + return value; }; SortedArray.prototype.slice = function () { From d77cc2421405509697ac1ccf1da5fb2982010ee7 Mon Sep 17 00:00:00 2001 From: Chris Barrick Date: Tue, 8 Apr 2014 11:32:49 -0400 Subject: [PATCH 62/83] Make heaps update length on delete --- heap.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/heap.js b/heap.js index 61984c6..f3704f9 100644 --- a/heap.js +++ b/heap.js @@ -86,6 +86,7 @@ Heap.prototype.delete = function (value) { if (index === -1) return false; var top = this.content.pop(); + this.length--; if (index === this.content.length) return true; this.content.set(index, top); @@ -95,7 +96,6 @@ Heap.prototype.delete = function (value) { } else if (comparison < 0) { this.sink(index); } - this.length--; return true; }; @@ -235,4 +235,3 @@ Heap.prototype.handleContentMapChange = function (plus, minus, key, type) { Heap.prototype.handleContentMapWillChange = function (plus, minus, key, type) { this.dispatchMapWillChange(type, key, plus, minus); }; - From e72ade15553373eaeac402299d2c5382d3e3f803 Mon Sep 17 00:00:00 2001 From: Chris Barrick Date: Wed, 9 Apr 2014 15:45:53 -0400 Subject: [PATCH 63/83] Fix length getting out of sync when deleting from an empty heap --- heap.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/heap.js b/heap.js index f3704f9..94aecda 100644 --- a/heap.js +++ b/heap.js @@ -86,7 +86,7 @@ Heap.prototype.delete = function (value) { if (index === -1) return false; var top = this.content.pop(); - this.length--; + this.length = this.content.length; if (index === this.content.length) return true; this.content.set(index, top); From 1b6a02c3a579b6eebd3bc7ce2049c9a6d549207a Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Thu, 15 May 2014 22:52:37 -0700 Subject: [PATCH 64/83] Always use swap for splice Existing logic was more complex than necessary. --- shim-array.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/shim-array.js b/shim-array.js index 04fe23d..0e77ca8 100644 --- a/shim-array.js +++ b/shim-array.js @@ -99,13 +99,11 @@ define("get", function (index, defaultValue) { }); define("set", function (index, value) { - if (index < this.length) { - this.splice(index, 1, value); - } else { - // Must use swap instead of splice, dispite the unfortunate array - // argument, because splice would truncate index to length. - this.swap(index, 1, [value]); - } + // Whether we use splice or swap, we're going to allocate an + // unnecessary array. "swap" works in cases where the index + // exceeds the length of the array, whereas splice would + // truncate. + this.swap(index, 1, [value]); return this; }); From ec42c0a244d05bb606e8ddb88514614b85d5be3f Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Thu, 15 May 2014 22:53:15 -0700 Subject: [PATCH 65/83] Phantom is not yet ready for Jasminum. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ab2478e..2c97e14 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "opener": "^1.3.0" }, "scripts": { - "test": "jasminum spec && jasminum-phantom spec", + "test": "jasminum spec", "cover": "istanbul cover spec/index.js spec && istanbul report html && opener coverage/index.html" } } From b3463a2f75e9bc0adf76cb30338b277c0e4d4151 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Thu, 10 Apr 2014 13:52:35 -0700 Subject: [PATCH 66/83] Update change log --- CHANGES.md | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index cb29d72..edb1e66 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,79 @@ - Removes support for `any` and `all`. Use `some(Boolean)` or `every(Boolean)`. +## v1.0.3 + +- Fixes array `set` and `swap` for indexes outside the bounds of the existing + array, for both observed and unobserved arrays. + +## v1.0.2 + +- Refinements on `Object.equals` and `Object.compare`. These are not + necessarily backward compatible, but should be a strict improvement: +- `Object.compare` will now return +/- Infinity for inequal strings, + rather than +/- 1 which imply that the distance between any two inequal + strings is always 1. `Object.compare` for numbers is suitable for finding + the magnitude of the difference as well as the direction. +- `Object.compare` and `Object.equals` will now delegate to either non-null, + non-undefined argument if the other argument is null or undefined. + This allows objects to be constructed that will identify themselves + as equivalent to null or undefined, for example `Any` types, useful for + testing. +- `Object.equals` will only compare object literals derrived directly from the + `Object.prototype`. All other objects that do not implement `compare` are + incomparable. +- First attempt at fixing `set`, `swap`, and `splice`, later fixed in v1.0.3. + `splice` must truncate the `start` index to the array length. `swap` and + `set` should not. + +## v1.0.1 + +- Bug fix for filter on map-like collections. + +## v1.0.0 :cake: + +- Adds a Deque type based on a circular buffer of exponential + capacity. (@petkaantonov) +- Implements `peek`, `peekBack`, `poke`, and `pokeBack` on array + shim for Deque “isomorphism”. +- Fixes the cases where a change listener is added or removed during + change dispatch. Neither listener will be informed until the next + change. (@asolove) +- The property change listener system has been altered such that + once a thunk has been installed on an object, it will not be + removed, in order to avoid churn. Once a property has been + observed, it is likely to be observed again. +- Fixes `Object.equals` for comparing NaN to itself, which should + report `true` such that collections that use `Object.equals` to + identify values are able to find `NaN`. Previously, `NaN` could + get stuck in a collection permanently. +- In abstract, Collections previously identified duck types by + looking only at the prototype chain, ignoring owned properties. + Thus, an object could distinguish a property name that was being + used as a key of a record, from the same property name that was + being used as a method name. To improve performance and to face + the reality that oftentimes an owned property is in fact a method, + Collections no longer observe this distinction. That is, if an + object has a function by the appropriate name, either by ownership + or inheritance, it will be recognized as a method of a duck type. + This particularly affects `Object.equals`, which should be much + faster now. +- Fixes `Object.equals` such that property for property comparison + between objects only happens if they both descend directly from + `Object.prototype`. Previously, objects would be thus compared if + they both descended from the same prototype. +- Accommodate *very* large arrays with the `swap` shim. Previously, + the size of an array swap was limited by the size of the + JavaScript run-time stack. (@francoisfrisch) +- Fixes `splice` on an array when given a negative start index. + (@stuk) +- Some methods accept an optional `equals` or `index` argument + that may or may not be supported by certain collections, like + `find` on a `SortedSet` versus a `List`. Collections that do not + support this argument will now throw an error instead of silently + ignoring the argument. +- Fixes `Array#clone` cycle detection. + ## v0.2.2 - `one` now returns a consistent value between changes of a sorted From 1bafc92b34accde8de2e2ee4e2c444bae8c9184b Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Thu, 10 Apr 2014 13:52:45 -0700 Subject: [PATCH 67/83] Disable Node.js v0.8 integration tests Travis now errors because of a bad npm certificate. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 45a0c7e..908589d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: node_js node_js: - - "0.8" - "0.10" notifications: irc: From b3fb53269a86ca11f3adc0ff737fd8c91b56433a Mon Sep 17 00:00:00 2001 From: Chris Barrick Date: Fri, 11 Apr 2014 20:47:26 -0400 Subject: [PATCH 68/83] Add test case for #66 --- spec/heap-spec.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/spec/heap-spec.js b/spec/heap-spec.js index 52dbfbb..b848dde 100644 --- a/spec/heap-spec.js +++ b/spec/heap-spec.js @@ -76,22 +76,29 @@ describe("Heap", function () { it("should delete properly", function () { var heap = new Heap([1, 2, 3, 4, 5, 6]); + expect(heap.length).toEqual(6); heap.delete(3); expect(heap.sorted()).toEqual([1, 2, 4, 5, 6]); + expect(heap.length).toEqual(5); heap.delete(6); expect(heap.sorted()).toEqual([1, 2, 4, 5]); + expect(heap.length).toEqual(4); heap.delete(1); expect(heap.sorted()).toEqual([2, 4, 5]); + expect(heap.length).toEqual(3); heap.delete(4); expect(heap.sorted()).toEqual([2, 5]); + expect(heap.length).toEqual(2); heap.delete(2); expect(heap.sorted()).toEqual([5]); + expect(heap.length).toEqual(1); heap.delete(5); expect(heap.sorted()).toEqual([]); + expect(heap.length).toEqual(0); expect(heap.delete(null)).toBe(false); expect(heap.sorted()).toEqual([]); + expect(heap.length).toEqual(0); }); }); - From 743a264bbe9e3805ee3eb5ff3cea38a66b97f4b8 Mon Sep 17 00:00:00 2001 From: Stuart Knightley Date: Mon, 21 Apr 2014 17:48:39 -0700 Subject: [PATCH 69/83] Add initial LFU --- lfu-set.js | 245 +++++++++++++++++++++++++++++++++++++++++++ spec/lfu-set-spec.js | 62 +++++++++++ 2 files changed, 307 insertions(+) create mode 100644 lfu-set.js create mode 100644 spec/lfu-set-spec.js diff --git a/lfu-set.js b/lfu-set.js new file mode 100644 index 0000000..2fdf902 --- /dev/null +++ b/lfu-set.js @@ -0,0 +1,245 @@ +"use strict"; + +// Based on http://dhruvbird.com/lfu.pdf + +var Shim = require("./shim"); +var Set = require("./set"); +var GenericCollection = require("./generic-collection"); +var GenericSet = require("./generic-set"); +var PropertyChanges = require("./listen/property-changes"); +var RangeChanges = require("./listen/range-changes"); + +module.exports = LfuSet; + +function LfuSet(values, capacity, equals, hash, getDefault) { + if (!(this instanceof LfuSet)) { + return new LfuSet(values, capacity, equals, hash, getDefault); + } + capacity = capacity || Infinity; + equals = equals || Object.equals; + hash = hash || Object.hash; + getDefault = getDefault || Function.noop; + + // TODO + this.store = new Set( + undefined, + function valueEqual(a, b) { + return equals(a.value, b.value); + }, + function valueHash(node) { + return hash(node.value); + } + ); + this.frequencyHead = new this.FrequencyNode(0); + + this.contentEquals = equals; + this.contentHash = hash; + this.getDefault = getDefault; + this.capacity = capacity; + this.length = 0; + this.addEach(values); +} + +LfuSet.LfuSet = LfuSet; // hack so require("lfu-set").LfuSet will work in MontageJS + +Object.addEach(LfuSet.prototype, GenericCollection.prototype); +Object.addEach(LfuSet.prototype, GenericSet.prototype); +Object.addEach(LfuSet.prototype, PropertyChanges.prototype); +Object.addEach(LfuSet.prototype, RangeChanges.prototype); + +LfuSet.prototype.constructClone = function (values) { + return new this.constructor( + values, + this.capacity, + this.contentEquals, + this.contentHash, + this.getDefault + ); +}; + +LfuSet.prototype.has = function (value) { + return this.store.has(new this.Node(value)); +}; + +LfuSet.prototype.get = function (value, equals) { + if (equals) { + throw new Error("LfuSet#get does not support second argument: equals"); + } + + var node = this.store.get(new this.Node(value)); + if (node !== undefined) { + var freq = node.parent; + var nextFreq = node.parent.next; + if (nextFreq.value !== freq.value + 1) { + nextFreq = new this.FrequencyNode(freq.value + 1, freq, nextFreq); + } + + nextFreq.items.add(node); + node.parent = nextFreq; + freq.items["delete"](node); + + if (freq.items.length === 0) { + freq.prev.next = freq.next; + freq.next.prev = freq.prev; + } + + return node.value; + } else { + return this.getDefault(value); + } +}; + +LfuSet.prototype.add = function (value) { + // if the value already exists, get it so that its frequency increases + if (this.has(value)) { + this.get(value); + return false; + } + + var plus = [], minus = [], leastFrequentNode, leastFrequent; + if (this.capacity > 0) { + plus.push(value); + if (this.length + 1 > this.capacity) { + leastFrequentNode = this.frequencyHead.next; + leastFrequent = leastFrequentNode.items.order.head.next.value; + minus.push(leastFrequent.value); + } + if (this.dispatchesRangeChanges) { + this.dispatchBeforeRangeChange(plus, minus, 0); + } + + // removal must happen before addition, otherwise we could remove + // the value we are about to add + if (minus.length > 0) { + this.store["delete"](leastFrequent); + leastFrequentNode.items["delete"](leastFrequent); + // Don't remove the frequencyNode with value of 1, because we + // are about to use it again in the addition. + if (leastFrequentNode.value !== 1 && leastFrequentNode.items.length === 0) { + this.frequencyHead.next = leastFrequentNode.next; + leastFrequentNode.next.prev = this.frequencyHead; + } + } + + var node = new this.Node(value); + var freq = this.frequencyHead.next; + if (freq.value !== 1) { + freq = new this.FrequencyNode(1, this.frequencyHead, freq); + } + this.store.add(node); + freq.items.add(node); + node.parent = freq; + + this.length = this.length + plus.length - minus.length; + + if (this.dispatchesRangeChanges) { + this.dispatchRangeChange(plus, minus, 0); + } + } + + // whether it grew + return plus.length !== minus.length; +}; + +LfuSet.prototype["delete"] = function (value, equals) { + if (equals) { + throw new Error("LfuSet#delete does not support second argument: equals"); + } + + var node = this.store.get(new this.Node(value)); + var found = !!node; + if (found) { + if (this.dispatchesRangeChanges) { + this.dispatchBeforeRangeChange([], [value], 0); + } + var freq = node.parent; + + this.store["delete"](node); + freq.items["delete"](node); + if (freq.items.length === 0) { + freq.prev.next = freq.next; + freq.next.prev = freq.prev; + } + this.length--; + + if (this.dispatchesRangeChanges) { + this.dispatchRangeChange([], [value], 0); + } + } + + return found; +}; + +LfuSet.prototype.one = function () { + if (this.length > 0) { + return this.frequencyHead.next.items.one().value; + } +}; + +LfuSet.prototype.clear = function () { + this.store.clear(); + this.frequencyHead.next = this.frequencyHead; + this.length = 0; +}; + +LfuSet.prototype.reduce = function (callback, basis /*, thisp*/) { + var thisp = arguments[2]; + var index = 0; + var freq = this.frequencyHead.next; + + while (freq.value !== 0) { + var set = freq.items; + basis = set.reduce(function (basis, node) { + return callback.call(thisp, basis, node.value, index++, this); + }, basis, this); + + freq = freq.next; + } + + return basis; +}; + +LfuSet.prototype.reduceRight = function (callback, basis /*, thisp*/) { + var thisp = arguments[2]; + var index = this.length - 1; + var freq = this.frequencyHead.prev; + + while (freq.value !== 0) { + var set = freq.items; + basis = set.reduceRight(function (basis, node) { + return callback.call(thisp, basis, node.value, index--, this); + }, basis, this); + + freq = freq.prev; + } + + return basis; +}; + +LfuSet.prototype.iterate = function () { + return this.store.map(function (node) { + return node.value; + }).iterate(); +}; + +LfuSet.prototype.Node = Node; + +function Node(value, parent) { + this.value = value; + this.parent = parent; +} + +LfuSet.prototype.FrequencyNode = FrequencyNode; + +function FrequencyNode(value, prev, next) { + this.value = value; + this.items = new Set(); + this.prev = prev || this; + this.next = next || this; + if (prev) { + prev.next = this; + } + if (next) { + next.prev = this; + } +} diff --git a/spec/lfu-set-spec.js b/spec/lfu-set-spec.js new file mode 100644 index 0000000..611090c --- /dev/null +++ b/spec/lfu-set-spec.js @@ -0,0 +1,62 @@ +var LfuSet = require("../lfu-set"); +var describeCollection = require("./collection"); +var describeSet = require("./set"); + +describe("LfuSet", function () { + + // construction, has, add, get, delete + function newLfuSet(values) { + return new LfuSet(values); + } + + [LfuSet, newLfuSet].forEach(function (LfuSet) { + describeCollection(LfuSet, [1, 2, 3, 4], true); + describeCollection(LfuSet, [{id: 0}, {id: 1}, {id: 2}, {id: 3}], true); + describeSet(LfuSet); + }); + + it("should handle many repeated values", function () { + var set = new LfuSet([1, 1, 1, 2, 2, 2, 1, 2]); + expect(set.toArray()).toEqual([1, 2]); + }); + + it("should remove stale entries", function () { + var set = LfuSet([3, 4, 1, 3, 2], 3); + + expect(set.length).toBe(3); + expect(set.toArray()).toEqual([1, 2, 3]); + set.add(4); + expect(set.toArray()).toEqual([2, 4, 3]); + }); + + it("should emit LFU changes as singleton operation", function () { + var a = 1, b = 2, c = 3, d = 4; + var lfuset = LfuSet([d, c, a, b, c], 3); + lfuset.addRangeChangeListener(function(plus, minus) { + expect(plus).toEqual([d]); + expect(minus).toEqual([a]); + }); + expect(lfuset.add(d)).toBe(false); + }); + + it("should dispatch LRU changes as singleton operation", function () { + var set = LfuSet([4, 3, 1, 2, 3], 3); + var spy = jasmine.createSpy(); + set.addBeforeRangeChangeListener(function (plus, minus) { + spy('before-plus', plus); + spy('before-minus', minus); + }); + set.addRangeChangeListener(function (plus, minus) { + spy('after-plus', plus); + spy('after-minus', minus); + }); + expect(set.add(4)).toBe(false); + expect(spy.argsForCall).toEqual([ + ['before-plus', [4]], + ['before-minus', [1]], + ['after-plus', [4]], + ['after-minus', [1]] + ]); + }) +}); + From 19aea8924f6abd8aa97cdb8679dd41d8432f6474 Mon Sep 17 00:00:00 2001 From: Stuart Knightley Date: Mon, 21 Apr 2014 18:59:04 -0700 Subject: [PATCH 70/83] Add LfuMap --- lfu-map.js | 79 ++++++++++++++++++++++++++++++++++++++++++ spec/lfu-map-spec.js | 82 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 lfu-map.js create mode 100644 spec/lfu-map-spec.js diff --git a/lfu-map.js b/lfu-map.js new file mode 100644 index 0000000..3b707f4 --- /dev/null +++ b/lfu-map.js @@ -0,0 +1,79 @@ +"use strict"; + +var Shim = require("./shim"); +var LfuSet = require("./lfu-set"); +var GenericCollection = require("./generic-collection"); +var GenericMap = require("./generic-map"); +var PropertyChanges = require("./listen/property-changes"); + +module.exports = LfuMap; + +function LfuMap(values, maxLength, equals, hash, getDefault) { + if (!(this instanceof LfuMap)) { + return new LfuMap(values, maxLength, equals, hash, getDefault); + } + equals = equals || Object.equals; + hash = hash || Object.hash; + getDefault = getDefault || Function.noop; + this.contentEquals = equals; + this.contentHash = hash; + this.getDefault = getDefault; + this.store = new LfuSet( + undefined, + maxLength, + function keysEqual(a, b) { + return equals(a.key, b.key); + }, + function keyHash(item) { + return hash(item.key); + } + ); + this.length = 0; + this.addEach(values); +} + +LfuMap.LfuMap = LfuMap; // hack so require("lfu-map").LfuMap will work in MontageJS + +Object.addEach(LfuMap.prototype, GenericCollection.prototype); +Object.addEach(LfuMap.prototype, GenericMap.prototype); +Object.addEach(LfuMap.prototype, PropertyChanges.prototype); + +LfuMap.prototype.constructClone = function (values) { + return new this.constructor( + values, + this.maxLength, + this.contentEquals, + this.contentHash, + this.getDefault + ); +}; + +LfuMap.prototype.log = function (charmap, stringify) { + stringify = stringify || this.stringify; + this.store.log(charmap, stringify); +}; + +LfuMap.prototype.stringify = function (item, leader) { + return leader + JSON.stringify(item.key) + ": " + JSON.stringify(item.value); +}; + +LfuMap.prototype.addMapChangeListener = function () { + if (!this.dispatchesMapChanges) { + // Detect LFU deletions in the LfuSet and emit as MapChanges. + // Array and Heap have no store. + // Dict and FastMap define no listeners on their store. + var self = this; + this.store.addBeforeRangeChangeListener(function(plus, minus) { + if (plus.length && minus.length) { // LFU item pruned + self.dispatchBeforeMapChange(minus[0].key, undefined); + } + }); + this.store.addRangeChangeListener(function(plus, minus) { + if (plus.length && minus.length) { + self.dispatchMapChange(minus[0].key, undefined); + } + }); + } + GenericMap.prototype.addMapChangeListener.apply(this, arguments); +}; + diff --git a/spec/lfu-map-spec.js b/spec/lfu-map-spec.js new file mode 100644 index 0000000..d6183c0 --- /dev/null +++ b/spec/lfu-map-spec.js @@ -0,0 +1,82 @@ + +var LfuMap = require("../lfu-map"); +var describeDict = require("./dict"); +var describeMap = require("./map"); + +describe("LfuMap", function () { + + describeDict(LfuMap); + describeMap(LfuMap); + + it("should remove stale entries", function () { + var map = LfuMap({a: 10, b: 20, c: 30}, 3); + map.get("a"); + map.get("b"); + map.set("d", 40); + expect(map.keys()).toEqual(['d', 'a', 'b']); + expect(map.length).toBe(3); + }); + + it("should not grow when re-adding", function () { + var map = LfuMap({a: 10, b: 20, c: 30}, 3); + + expect(map.keys()).toEqual(['a', 'b', 'c']); + expect(map.length).toBe(3); + + map.get("b"); + expect(map.keys()).toEqual(['a', 'c', 'b']); + expect(map.length).toBe(3); + + map.set("c", 40); + expect(map.keys()).toEqual(['a', 'b', 'c']); + expect(map.length).toBe(3); + }); + + it("should grow when adding new values", function () { + var map = LfuMap({}, 3); + expect(map.length).toBe(0); + + map.set("a", 10); + expect(map.length).toBe(1); + map.set("a", 10); + expect(map.length).toBe(1); + + map.set("b", 20); + expect(map.length).toBe(2); + map.set("b", 20); + expect(map.length).toBe(2); + + map.set("c", 30); + expect(map.length).toBe(3); + map.set("c", 30); + expect(map.length).toBe(3); + + // stops growing + map.set("d", 40); + expect(map.length).toBe(3); + map.set("d", 40); + expect(map.length).toBe(3); + + map.set("e", 50); + expect(map.length).toBe(3); + }); + + it("should dispatch deletion for stale entries", function () { + var map = LfuMap({a: 10, b: 20, c: 30}, 3); + var spy = jasmine.createSpy(); + map.addBeforeMapChangeListener(function (value, key) { + spy('before', key, value); + }); + map.addMapChangeListener(function (value, key) { + spy('after', key, value); + }); + map.set('d', 40); + expect(spy.argsForCall).toEqual([ + ['before', 'd', undefined], // d will be added + ['before', 'a', undefined], // then a is pruned (stale) + ['after', 'a', undefined], // afterwards a is still pruned + ['after', 'd', 40] // and now d has a value + ]); + }); +}); + From 0ae10d2bd6491962e96fd1728c4bae268851c86c Mon Sep 17 00:00:00 2001 From: Stuart Knightley Date: Wed, 23 Apr 2014 16:07:03 -0700 Subject: [PATCH 71/83] Make LfuSet variables more descriptive --- lfu-set.js | 74 +++++++++++++++++++++++++++--------------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/lfu-set.js b/lfu-set.js index 2fdf902..f042bca 100644 --- a/lfu-set.js +++ b/lfu-set.js @@ -68,19 +68,19 @@ LfuSet.prototype.get = function (value, equals) { var node = this.store.get(new this.Node(value)); if (node !== undefined) { - var freq = node.parent; - var nextFreq = node.parent.next; - if (nextFreq.value !== freq.value + 1) { - nextFreq = new this.FrequencyNode(freq.value + 1, freq, nextFreq); + var frequencyNode = node.frequencyNode; + var nextFrequencyNode = frequencyNode.next; + if (nextFrequencyNode.frequency !== frequencyNode.frequency + 1) { + nextFrequencyNode = new this.FrequencyNode(frequencyNode.frequency + 1, frequencyNode, nextFrequencyNode); } - nextFreq.items.add(node); - node.parent = nextFreq; - freq.items["delete"](node); + nextFrequencyNode.values.add(node); + node.frequencyNode = nextFrequencyNode; + frequencyNode.values["delete"](node); - if (freq.items.length === 0) { - freq.prev.next = freq.next; - freq.next.prev = freq.prev; + if (frequencyNode.values.length === 0) { + frequencyNode.prev.next = frequencyNode.next; + frequencyNode.next.prev = frequencyNode.prev; } return node.value; @@ -101,7 +101,7 @@ LfuSet.prototype.add = function (value) { plus.push(value); if (this.length + 1 > this.capacity) { leastFrequentNode = this.frequencyHead.next; - leastFrequent = leastFrequentNode.items.order.head.next.value; + leastFrequent = leastFrequentNode.values.order.head.next.value; minus.push(leastFrequent.value); } if (this.dispatchesRangeChanges) { @@ -112,23 +112,23 @@ LfuSet.prototype.add = function (value) { // the value we are about to add if (minus.length > 0) { this.store["delete"](leastFrequent); - leastFrequentNode.items["delete"](leastFrequent); + leastFrequentNode.values["delete"](leastFrequent); // Don't remove the frequencyNode with value of 1, because we // are about to use it again in the addition. - if (leastFrequentNode.value !== 1 && leastFrequentNode.items.length === 0) { + if (leastFrequentNode.value !== 1 && leastFrequentNode.values.length === 0) { this.frequencyHead.next = leastFrequentNode.next; leastFrequentNode.next.prev = this.frequencyHead; } } var node = new this.Node(value); - var freq = this.frequencyHead.next; - if (freq.value !== 1) { - freq = new this.FrequencyNode(1, this.frequencyHead, freq); + var frequencyNode = this.frequencyHead.next; + if (frequencyNode.frequency !== 1) { + frequencyNode = new this.FrequencyNode(1, this.frequencyHead, frequencyNode); } this.store.add(node); - freq.items.add(node); - node.parent = freq; + frequencyNode.values.add(node); + node.frequencyNode = frequencyNode; this.length = this.length + plus.length - minus.length; @@ -152,13 +152,13 @@ LfuSet.prototype["delete"] = function (value, equals) { if (this.dispatchesRangeChanges) { this.dispatchBeforeRangeChange([], [value], 0); } - var freq = node.parent; + var frequencyNode = node.frequencyNode; this.store["delete"](node); - freq.items["delete"](node); - if (freq.items.length === 0) { - freq.prev.next = freq.next; - freq.next.prev = freq.prev; + frequencyNode.values["delete"](node); + if (frequencyNode.values.length === 0) { + frequencyNode.prev.next = frequencyNode.next; + frequencyNode.next.prev = frequencyNode.prev; } this.length--; @@ -172,7 +172,7 @@ LfuSet.prototype["delete"] = function (value, equals) { LfuSet.prototype.one = function () { if (this.length > 0) { - return this.frequencyHead.next.items.one().value; + return this.frequencyHead.next.values.one().value; } }; @@ -185,15 +185,15 @@ LfuSet.prototype.clear = function () { LfuSet.prototype.reduce = function (callback, basis /*, thisp*/) { var thisp = arguments[2]; var index = 0; - var freq = this.frequencyHead.next; + var frequencyNode = this.frequencyHead.next; - while (freq.value !== 0) { - var set = freq.items; + while (frequencyNode.frequency !== 0) { + var set = frequencyNode.values; basis = set.reduce(function (basis, node) { return callback.call(thisp, basis, node.value, index++, this); }, basis, this); - freq = freq.next; + frequencyNode = frequencyNode.next; } return basis; @@ -202,15 +202,15 @@ LfuSet.prototype.reduce = function (callback, basis /*, thisp*/) { LfuSet.prototype.reduceRight = function (callback, basis /*, thisp*/) { var thisp = arguments[2]; var index = this.length - 1; - var freq = this.frequencyHead.prev; + var frequencyNode = this.frequencyHead.prev; - while (freq.value !== 0) { - var set = freq.items; + while (frequencyNode.frequency !== 0) { + var set = frequencyNode.values; basis = set.reduceRight(function (basis, node) { return callback.call(thisp, basis, node.value, index--, this); }, basis, this); - freq = freq.prev; + frequencyNode = frequencyNode.prev; } return basis; @@ -224,16 +224,16 @@ LfuSet.prototype.iterate = function () { LfuSet.prototype.Node = Node; -function Node(value, parent) { +function Node(value, frequencyNode) { this.value = value; - this.parent = parent; + this.frequencyNode = frequencyNode; } LfuSet.prototype.FrequencyNode = FrequencyNode; -function FrequencyNode(value, prev, next) { - this.value = value; - this.items = new Set(); +function FrequencyNode(frequency, prev, next) { + this.frequency = frequency; + this.values = new Set(); this.prev = prev || this; this.next = next || this; if (prev) { From 4323b8eefa43422c7fa997520901fc63154d16fc Mon Sep 17 00:00:00 2001 From: Stuart Knightley Date: Wed, 23 Apr 2014 16:45:27 -0700 Subject: [PATCH 72/83] Update readme to contain Lfu{Set, Map} --- README.md | 143 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 82 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 9b50cbe..f219386 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,27 @@ var LruMap = require("collections/lru-map"); A cache of entries backed by an `LruSet`. +### LfuSet(values, capacity, equals, hash, getDefault) + +```javascript +var LfuSet = require("collections/lfu-set"); +``` + +A cache with the Least-Frequently-Used strategy for truncating its +content when it’s length exceeds `capacity`. It maintains the +eqivalent of a LRU for each frequency, so that the oldest, least +frequently used value is evicted first. Both getting and setting a +value constitute usage, but checking whether the set has a value and +iterating values do not. + +### LfuMap(map, capacity, equals, hash, getDefault) + +```javascript +var LfuMap = require("collections/lfu-map"); +``` + +A cache of entries backed by an `LfuSet`. + ### SortedArray(values, equals, compare, getDefault) ```javascript @@ -319,8 +340,8 @@ should exist but has not yet been made (Send a pull request!). These are all of the collections: (Array, Array+, Object+, Iterator, List, Set, Map, MultiMap, WeakMap, -SortedSet, SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, -SortedArrayMap, FastSet, FastMap, Dict) +SortedSet, SortedMap, LruSet, LruMap, LfuSet, LfuMap, SortedArray, +SortedArraySet, SortedArrayMap, FastSet, FastMap, Dict) ### has @@ -425,8 +446,8 @@ Deletes the equivalent value. Returns whether the value was found. Deletes every value or every value for each key. (Array+, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, -LruSet, LruMap, SortedArray, SortedArraySet, SortedArrayMap, FastSet, -FastMap, Dict, Heap) +LruSet, LruMap, LfuSet, LfuMap, SortedArray, SortedArraySet, SortedArrayMap, +FastSet, FastMap, Dict, Heap) ### indexOf(value) @@ -599,7 +620,7 @@ Performs a splice without variadic arguments. Deletes the all values. (Array+, Object+, List, Deque, Set, Map, MultiMap, SortedSet, -SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, +SortedMap, LruSet, LruMap, LfuSet, LfuMap, SortedArray, SortedArraySet, SortedArrayMap, FastSet, FastMap, Dict, Heap) ### sort(compare) @@ -634,9 +655,9 @@ element from the collection is placed into an equivalence class if they have the same corresponding return value from the given callback. -(Array+, Object+, List, Deque, Set, Map, MultiMap, SortedSet, -SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, -SortedArrayMap, FastSet, FastMap, Dict, Heap, Iterator) +(Array+, Object+, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, LruSet, +LruMap, LfuSet, LfuMap, SortedArray, SortedArraySet, SortedArrayMap, FastSet, +FastMap, Dict, Heap, Iterator) ### reverse() @@ -672,9 +693,9 @@ iterables are treated as map-like objects and each successively updates the result. Array is like a map from index to value. List, Set, and SortedSet are like maps from nodes to values. -(Array, ~~Object+~~, Iterator, List, Set, Map, MultiMap, -SortedSet, SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, -SortedArrayMap, FastSet, FastMap, Dict, Heap) +(Array, ~~Object+~~, Iterator, List, Set, Map, MultiMap, SortedSet, SortedMap, +LruSet, LruMap, LfuSet, LfuMap, SortedArray, SortedArraySet, SortedArrayMap, +FastSet, FastMap, Dict, Heap) ### keys() @@ -699,9 +720,9 @@ Dict) ### reduce(callback(result, value, key, object, depth), basis, thisp) -(Array, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, -SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, -SortedArrayMap, FastSet, FastMap, Dict, Heap) +(Array, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, LruSet, +LruMap, LfuSet, LfuMap, SortedArray, SortedArraySet, SortedArrayMap, FastSet, +FastMap, Dict, Heap) ### reduceRight(callback(result, value, key, object, depth), basis, thisp) @@ -716,20 +737,20 @@ added after the current node will be visited and nodes added before the current node will be ignored, and no node will be visited twice. (Array, Object+, Iterator, List, Deque, Set, Map, MultiMap, WeakMap, -SortedSet, SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, +SortedSet, SortedMap, LruSet, LruMap, LfuSet, LfuMap, SortedArray, SortedArraySet, SortedArrayMap, FastSet, FastMap, Dict, Heap) ### map(callback(value, key, object, depth), thisp) -(Array, Object+, Iterator, List, Deque, Set, Map, MultiMap, WeakMap, -SortedSet, SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, +(Array, Object+, Iterator, List, Deque, Set, Map, MultiMap, WeakMap, SortedSet, +SortedMap, LruSet, LruMap, LfuSet, LfuMap, SortedArray, SortedArraySet, SortedArrayMap, FastSet, FastMap, Dict, Heap) ### toArray() -(Array+, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, -SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, -SortedArrayMap, FastSet, FastMap, Dict, Heap) +(Array+, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, +LruSet, LruMap, LfuSet, LfuMap, SortedArray, SortedArraySet, SortedArrayMap, +FastSet, FastMap, Dict, Heap) ### toObject() @@ -741,9 +762,9 @@ SortedArrayMap, FastMap, Dict, Heap) ### filter(callback(value, key, object, depth), thisp) -(Array, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, -SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, -SortedArrayMap, FastSet, FastMap, Dict, Heap) +(Array, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, LruSet, +LruMap, LfuSet, LfuMap, SortedArray, SortedArraySet, SortedArrayMap, FastSet, +FastMap, Dict, Heap) ### every(callback(value, key, object, depth), thisp) @@ -751,9 +772,9 @@ Whether every value passes a given guard. Stops evaluating the guard after the first failure. Iterators stop consuming after the the first failure. -(Array, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, -SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, -SortedArrayMap, FastSet, FastMap, Dict, Heap) +(Array, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, LruSet, +LruMap, LfuSet, LfuMap, SortedArray, SortedArraySet, SortedArrayMap, FastSet, +FastMap, Dict, Heap) *The method `all` from version 1 was removed in version 2 in favor of the idiom `every(Boolean)`.* @@ -764,9 +785,9 @@ Whether there is a value that passes a given guard. Stops evaluating the guard after the first success. Iterators stop consuming after the first success. -(Array, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, -SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, -SortedArrayMap, FastSet, FastMap, Dict, Heap) +(Array, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, LruSet, +LruMap, LfuSet, LfuMap, SortedArray, SortedArraySet, SortedArrayMap, FastSet, +FastMap, Dict, Heap) *The method `any` from version 1 was removed in version 2 in favor of the idiom `some(Boolean)`.* @@ -777,9 +798,9 @@ The smallest value. This is fast for sorted collections (logarithic for SortedSet, constant for SortedArray, SortedArraySet, and SortedArrayMap), but slow for everything else (linear). -(Array+, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, -SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, -SortedArrayMap, FastSet, FastMap, Dict) +(Array+, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, +LruSet, LruMap, LfuSet, LfuMap, SortedArray, SortedArraySet, SortedArrayMap, +FastSet, FastMap, Dict) ### max() @@ -788,7 +809,7 @@ for SortedSet, constant for SortedArray, SortedArraySet, and SortedArrayMap), but slow for everything else (linear). (Array+, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, -SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, +SortedMap, LruSet, LruMap, LfuSet, LfuMap, SortedArray, SortedArraySet, SortedArrayMap, FastSet, FastMap, Dict, Heap) ### one() @@ -800,9 +821,9 @@ is very fast (constant time) for most collections. For a sorted set, being consistent across accesses, and only changing in response to mutation, `one` returns the `min` of the set in logarithmic time. -(Array+, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, -LruSet, LruMap, SortedArray, SortedArray, SortedArraySet, -SortedArrayMap, FastSet, FastMap, Dict, Heap) +(Array+, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, LruSet, LruMap, +LfuSet, LfuMap, SortedArray, SortedArray, SortedArraySet, SortedArrayMap, +FastSet, FastMap, Dict, Heap) ### only() @@ -810,38 +831,38 @@ The one and only value, or throws an exception if there are no values or more than one value. (Array+, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, -LruSet, LruMap, SortedArray, SortedArray, SortedArraySet, +LruSet, LruMap, LfuSet, LfuMap, SortedArray, SortedArray, SortedArraySet, SortedArrayMap, FastSet, FastMap, Dict, Heap) ### sum() -(Array+, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, -SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, -SortedArrayMap, FastSet, FastMap, Dict) +(Array+, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, +LruSet, LruMap, LfuSet, LfuMap, SortedArray, SortedArraySet, SortedArrayMap, +FastSet, FastMap, Dict) ### average() -(Array+, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, -SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, -SortedArrayMap, FastSet, FastMap, Dict) +(Array+, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, +LruSet, LruMap, LfuSet, LfuMap, SortedArray, SortedArraySet, SortedArrayMap, +FastSet, FastMap, Dict) ### flatten() -(Array+, Iterator, List, Set, Map, MultiMap, SortedSet, -SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, -SortedArrayMap, FastSet, FastMap, Dict, Heap) +(Array+, Iterator, List, Set, Map, MultiMap, SortedSet, SortedMap, LruSet, +LruMap, LfuSet, LfuMap, SortedArray, SortedArraySet, SortedArrayMap, FastSet, +FastMap, Dict, Heap) ### zip(...collections) -(Array+, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, -SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, -SortedArrayMap, FastSet, FastMap, Dict, Heap) +(Array+, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, +LruSet, LruMap, LfuSet, LfuMap, SortedArray, SortedArraySet, SortedArrayMap, +FastSet, FastMap, Dict, Heap) ### enumerate(zero) -(Array+, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, -SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, -SortedArrayMap, FastSet, FastMap, Dict, Heap) +(Array+, Iterator, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, +LruSet, LruMap, LfuSet, LfuMap, SortedArray, SortedArraySet, SortedArrayMap, +FastSet, FastMap, Dict, Heap) ### clone(depth, memo) @@ -860,9 +881,9 @@ The `clone` method on any other objects is not intended to be used directly since they do not necessarily supply a default depth and memo. -(Array+, Object+, List, Deque, Set, Map, MultiMap, SortedSet, -SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, -SortedArrayMap, FastSet, FastMap, Dict, Heap) +(Array+, Object+, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, LruSet, +LruMap, LfuSet, LfuMap, SortedArray, SortedArraySet, SortedArrayMap, FastSet, +FastMap, Dict, Heap) ### constructClone(values) @@ -872,15 +893,15 @@ same options (`equals`, `compare`, `hash` options), but it leaves the job of deeply cloning the values to the more general `clone` method. -(Array+, Object+, List, Deque, Set, Map, MultiMap, SortedSet, -SortedMap, LruSet, LruMap, SortedArray, SortedArraySet, -SortedArrayMap, FastSet, FastMap, Dict, Heap) +(Array+, Object+, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, LruSet, +LruMap, LfuSet, LfuMap, SortedArray, SortedArraySet, SortedArrayMap, FastSet, +FastMap, Dict, Heap) ### equals(that, equals) -(Array+, Object+, List, Deque, Set, Map, MultiMap, SortedSet, -SortedMap, LruSet, LruMap, ~~SortedArray~~, SortedArraySet, -SortedArrayMap, FastSet, FastMap, Dict) +(Array+, Object+, List, Deque, Set, Map, MultiMap, SortedSet, SortedMap, LruSet, +LruMap, LfuSet, LfuMap, ~~SortedArray~~, SortedArraySet, SortedArrayMap, +FastSet, FastMap, Dict) ### compare(that) From 600781a7fbd947c824e26a76353a6fe586752fa6 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Fri, 16 May 2014 09:53:29 -0700 Subject: [PATCH 73/83] Port LFU to v2 branch conventions --- lfu-map.js | 37 ++++---- lfu-set.js | 12 +-- spec/dict.js | 144 ++++++++++++++++--------------- spec/lfu-map-spec.js | 16 ++-- spec/lfu-set-spec.js | 14 +-- spec/map.js | 158 +++++++++++++++++----------------- spec/set-spec.js | 197 ++++++++++++++++++++++--------------------- 7 files changed, 297 insertions(+), 281 deletions(-) diff --git a/lfu-map.js b/lfu-map.js index 3b707f4..cbe4794 100644 --- a/lfu-map.js +++ b/lfu-map.js @@ -4,7 +4,7 @@ var Shim = require("./shim"); var LfuSet = require("./lfu-set"); var GenericCollection = require("./generic-collection"); var GenericMap = require("./generic-map"); -var PropertyChanges = require("./listen/property-changes"); +var ObservableObject = require("./observable-object"); module.exports = LfuMap; @@ -14,7 +14,7 @@ function LfuMap(values, maxLength, equals, hash, getDefault) { } equals = equals || Object.equals; hash = hash || Object.hash; - getDefault = getDefault || Function.noop; + getDefault = getDefault || this.getDefault; this.contentEquals = equals; this.contentHash = hash; this.getDefault = getDefault; @@ -36,7 +36,7 @@ LfuMap.LfuMap = LfuMap; // hack so require("lfu-map").LfuMap will work in Montag Object.addEach(LfuMap.prototype, GenericCollection.prototype); Object.addEach(LfuMap.prototype, GenericMap.prototype); -Object.addEach(LfuMap.prototype, PropertyChanges.prototype); +Object.addEach(LfuMap.prototype, ObservableObject.prototype); LfuMap.prototype.constructClone = function (values) { return new this.constructor( @@ -57,23 +57,26 @@ LfuMap.prototype.stringify = function (item, leader) { return leader + JSON.stringify(item.key) + ": " + JSON.stringify(item.value); }; -LfuMap.prototype.addMapChangeListener = function () { +LfuMap.prototype.observeMapChange = function () { if (!this.dispatchesMapChanges) { - // Detect LFU deletions in the LfuSet and emit as MapChanges. + // Detect LRU deletions in the LfuSet and emit as MapChanges. // Array and Heap have no store. // Dict and FastMap define no listeners on their store. - var self = this; - this.store.addBeforeRangeChangeListener(function(plus, minus) { - if (plus.length && minus.length) { // LFU item pruned - self.dispatchBeforeMapChange(minus[0].key, undefined); - } - }); - this.store.addRangeChangeListener(function(plus, minus) { - if (plus.length && minus.length) { - self.dispatchMapChange(minus[0].key, undefined); - } - }); + this.store.observeRangeWillChange(this, "store"); + this.store.observeRangeChange(this, "store"); + } + return GenericMap.prototype.observeMapChange.apply(this, arguments); +}; + +LfuMap.prototype.handleStoreRangeWillChange = function (plus, minus, index) { + if (plus.length && minus.length) { // LRU item pruned + this.dispatchMapWillChange("delete", minus[0].key, undefined, minus[0].value); + } +}; + +LfuMap.prototype.handleStoreRangeChange = function (plus, minus, index) { + if (plus.length && minus.length) { + this.dispatchMapChange("delete", minus[0].key, undefined, minus[0].value); } - GenericMap.prototype.addMapChangeListener.apply(this, arguments); }; diff --git a/lfu-set.js b/lfu-set.js index f042bca..17fcbf4 100644 --- a/lfu-set.js +++ b/lfu-set.js @@ -6,8 +6,8 @@ var Shim = require("./shim"); var Set = require("./set"); var GenericCollection = require("./generic-collection"); var GenericSet = require("./generic-set"); -var PropertyChanges = require("./listen/property-changes"); -var RangeChanges = require("./listen/range-changes"); +var ObservableRange = require("./observable-range"); +var ObservableObject = require("./observable-object"); module.exports = LfuSet; @@ -44,8 +44,8 @@ LfuSet.LfuSet = LfuSet; // hack so require("lfu-set").LfuSet will work in Montag Object.addEach(LfuSet.prototype, GenericCollection.prototype); Object.addEach(LfuSet.prototype, GenericSet.prototype); -Object.addEach(LfuSet.prototype, PropertyChanges.prototype); -Object.addEach(LfuSet.prototype, RangeChanges.prototype); +Object.addEach(LfuSet.prototype, ObservableRange.prototype); +Object.addEach(LfuSet.prototype, ObservableObject.prototype); LfuSet.prototype.constructClone = function (values) { return new this.constructor( @@ -105,7 +105,7 @@ LfuSet.prototype.add = function (value) { minus.push(leastFrequent.value); } if (this.dispatchesRangeChanges) { - this.dispatchBeforeRangeChange(plus, minus, 0); + this.dispatchRangeWillChange(plus, minus, 0); } // removal must happen before addition, otherwise we could remove @@ -150,7 +150,7 @@ LfuSet.prototype["delete"] = function (value, equals) { var found = !!node; if (found) { if (this.dispatchesRangeChanges) { - this.dispatchBeforeRangeChange([], [value], 0); + this.dispatchRangeWillChange([], [value], 0); } var frequencyNode = node.frequencyNode; diff --git a/spec/dict.js b/spec/dict.js index 22ddffd..97614a0 100644 --- a/spec/dict.js +++ b/spec/dict.js @@ -3,98 +3,102 @@ module.exports = describeDict; function describeDict(Dict) { - it("should be constructable from entry duples", function () { - var dict = Dict([['a', 10], ['b', 20]]); - shouldHaveTheUsualContent(dict); - }); + describe("as Dict", function () { - it("should be constructable from objects", function () { - var dict = Dict({a: 10, b: 20}); - shouldHaveTheUsualContent(dict); - }); + it("should be constructable from entry duples", function () { + var dict = Dict([['a', 10], ['b', 20]]); + shouldHaveTheUsualContent(dict); + }); - it("should be constructable from dicts", function () { - var dict = Dict(Dict({a: 10, b: 20})); - shouldHaveTheUsualContent(dict); - }); + it("should be constructable from objects", function () { + var dict = Dict({a: 10, b: 20}); + shouldHaveTheUsualContent(dict); + }); - describe("delete", function () { - it("should be able to delete keys", function () { - var dict = Dict({a: 10, b: 20, c: 30}); - expect(dict.delete('c')).toBe(true); - expect(dict.delete('c')).toBe(false); + it("should be constructable from dicts", function () { + var dict = Dict(Dict({a: 10, b: 20})); shouldHaveTheUsualContent(dict); }); - }); - it("should be able to contain hasOwnProperty", function () { - var dict = Dict(); - expect(dict.set("hasOwnProperty", 10)).toBe(true); - expect(dict.get("hasOwnProperty")).toBe(10); - expect(dict.delete("hasOwnProperty")).toBe(true); - expect(dict.length).toBe(0); - expect(dict.delete("hasOwnProperty")).toBe(false); - }); + describe("delete", function () { + it("should be able to delete keys", function () { + var dict = Dict({a: 10, b: 20, c: 30}); + expect(dict.delete('c')).toBe(true); + expect(dict.delete('c')).toBe(false); + shouldHaveTheUsualContent(dict); + }); + }); - it("should be able to contain __proto__", function () { - var dict = Dict(); - expect(dict.set("__proto__", 10)).toBe(true); - expect(dict.get("__proto__")).toBe(10); - expect(dict.delete("__proto__")).toBe(true); - expect(dict.length).toBe(0); - expect(dict.delete("__proto__")).toBe(false); - }); + it("should be able to contain hasOwnProperty", function () { + var dict = Dict(); + expect(dict.set("hasOwnProperty", 10)).toBe(true); + expect(dict.get("hasOwnProperty")).toBe(10); + expect(dict.delete("hasOwnProperty")).toBe(true); + expect(dict.length).toBe(0); + expect(dict.delete("hasOwnProperty")).toBe(false); + }); - describe("getDefault", function () { + it("should be able to contain __proto__", function () { + var dict = Dict(); + expect(dict.set("__proto__", 10)).toBe(true); + expect(dict.get("__proto__")).toBe(10); + expect(dict.delete("__proto__")).toBe(true); + expect(dict.length).toBe(0); + expect(dict.delete("__proto__")).toBe(false); + }); - it("can be overridden on the prototype", function () { + describe("getDefault", function () { - var called = false; + it("can be overridden on the prototype", function () { - function Memo() { - Dict.call(this); - } + var called = false; - Memo.prototype = Object.create(Dict.prototype); - Memo.prototype.constructor = Memo; + function Memo() { + Dict.call(this); + } - Memo.prototype.getDefault = function (key) { - called = true; - this.set(key, key + "!"); - return this.get(key); - }; + Memo.prototype = Object.create(Dict.prototype); + Memo.prototype.constructor = Memo; - var memo = new Memo(); + Memo.prototype.getDefault = function (key) { + called = true; + this.set(key, key + "!"); + return this.get(key); + }; - called = false; - expect(memo.get("hi")).toBe("hi!"); - expect(called).toBe(true); + var memo = new Memo(); - called = false; - expect(memo.get("hi")).toBe("hi!"); - expect(called).toBe(false); + called = false; + expect(memo.get("hi")).toBe("hi!"); + expect(called).toBe(true); - }); + called = false; + expect(memo.get("hi")).toBe("hi!"); + expect(called).toBe(false); - }); + }); - describe("iterate", function () { - it("should iterate a dictionary", function () { - var dict = new Dict({a: 10, b: 20, c: 30}); - var iterator = dict.iterate(); - expect(iterator.next()).toEqual({value: 10, index: "a", done: false}); }); - }); - describe("some", function () { - it("can enumerate the content of a dict", function () { - var dict = new Dict({only: 10}); - expect(dict.some(function (value, key) { - expect(key).toBe("only"); - expect(value).toBe(10); - return value === 10; - })).toBe(true); + describe("iterate", function () { + it("should iterate a dictionary", function () { + var dict = new Dict({a: 10, b: 20, c: 30}); + var iterator = dict.iterate(); + expect(iterator.next()).toEqual({value: 10, index: "a", done: false}); + }); + }); + + describe("some", function () { + it("can enumerate the content of a dict", function () { + var dict = new Dict({only: 10}); + expect(dict.some(function (value, key) { + expect(key).toBe("only"); + expect(value).toBe(10); + return value === 10; + })).toBe(true); + }); }); + }); } diff --git a/spec/lfu-map-spec.js b/spec/lfu-map-spec.js index d6183c0..d08bc5d 100644 --- a/spec/lfu-map-spec.js +++ b/spec/lfu-map-spec.js @@ -1,11 +1,11 @@ +var sinon = require("sinon"); var LfuMap = require("../lfu-map"); var describeDict = require("./dict"); var describeMap = require("./map"); describe("LfuMap", function () { - describeDict(LfuMap); describeMap(LfuMap); it("should remove stale entries", function () { @@ -63,17 +63,17 @@ describe("LfuMap", function () { it("should dispatch deletion for stale entries", function () { var map = LfuMap({a: 10, b: 20, c: 30}, 3); - var spy = jasmine.createSpy(); - map.addBeforeMapChangeListener(function (value, key) { - spy('before', key, value); + var spy = sinon.spy(); + map.observeMapWillChange(function (plus, minus, key) { + spy('before', key, minus); }); - map.addMapChangeListener(function (value, key) { - spy('after', key, value); + map.observeMapChange(function (plus, minus, key) { + spy('after', key, plus); }); map.set('d', 40); - expect(spy.argsForCall).toEqual([ + expect(spy.args).toEqual([ ['before', 'd', undefined], // d will be added - ['before', 'a', undefined], // then a is pruned (stale) + ['before', 'a', 10], // then a is pruned (stale) ['after', 'a', undefined], // afterwards a is still pruned ['after', 'd', 40] // and now d has a value ]); diff --git a/spec/lfu-set-spec.js b/spec/lfu-set-spec.js index 611090c..e81ea1d 100644 --- a/spec/lfu-set-spec.js +++ b/spec/lfu-set-spec.js @@ -1,3 +1,5 @@ + +var sinon = require("sinon"); var LfuSet = require("../lfu-set"); var describeCollection = require("./collection"); var describeSet = require("./set"); @@ -12,7 +14,7 @@ describe("LfuSet", function () { [LfuSet, newLfuSet].forEach(function (LfuSet) { describeCollection(LfuSet, [1, 2, 3, 4], true); describeCollection(LfuSet, [{id: 0}, {id: 1}, {id: 2}, {id: 3}], true); - describeSet(LfuSet); + //describeSet(LfuSet); }); it("should handle many repeated values", function () { @@ -32,7 +34,7 @@ describe("LfuSet", function () { it("should emit LFU changes as singleton operation", function () { var a = 1, b = 2, c = 3, d = 4; var lfuset = LfuSet([d, c, a, b, c], 3); - lfuset.addRangeChangeListener(function(plus, minus) { + lfuset.observeRangeChange(function(plus, minus) { expect(plus).toEqual([d]); expect(minus).toEqual([a]); }); @@ -41,17 +43,17 @@ describe("LfuSet", function () { it("should dispatch LRU changes as singleton operation", function () { var set = LfuSet([4, 3, 1, 2, 3], 3); - var spy = jasmine.createSpy(); - set.addBeforeRangeChangeListener(function (plus, minus) { + var spy = sinon.spy(); + set.observeRangeChange(function (plus, minus) { spy('before-plus', plus); spy('before-minus', minus); }); - set.addRangeChangeListener(function (plus, minus) { + set.observeRangeChange(function (plus, minus) { spy('after-plus', plus); spy('after-minus', minus); }); expect(set.add(4)).toBe(false); - expect(spy.argsForCall).toEqual([ + expect(spy.args).toEqual([ ['before-plus', [4]], ['before-minus', [1]], ['after-plus', [4]], diff --git a/spec/map.js b/spec/map.js index dfa6ed7..5c0f935 100644 --- a/spec/map.js +++ b/spec/map.js @@ -7,95 +7,99 @@ var describeDict = require("./dict"); module.exports = describeMap; function describeMap(Map, values) { - values = values || []; - var a = values[0] || {}; - var b = values[1] || {}; - var c = values[2] || {}; - - function shouldHaveTheUsualContent(map) { - expect(map.has(a)).toBe(true); - expect(map.has(b)).toBe(true); - expect(map.has(c)).toBe(false); - expect(map.get(a)).toBe(10); - expect(map.get(b)).toBe(20); - expect(map.get(c)).toBe(undefined); - expect(map.get(c, 30)).toBe(30); - expect(map.length).toBe(2); - expect(map.keys()).toEqual([a, b]); - expect(map.values()).toEqual([10, 20]); - expect(map.entries()).toEqual([[a, 10], [b, 20]]); - expect(map.reduce(function (basis, value, key) { - basis.push([this, key, value]); - return basis; - }, [], map)).toEqual([ - [map, a, 10], - [map, b, 20] - ]);; - } - - it("should be constructable from entry duples with object keys", function () { - var map = Map([[a, 10], [b, 20]]); - shouldHaveTheUsualContent(map); - }); + describe("as Map", function () { - it("should be constructable from an interable", function () { - var map = Map({ - forEach: function (callback, thisp) { - callback.call(thisp, [a, 10]); - callback.call(thisp, [b, 20]); - } - }); - shouldHaveTheUsualContent(map); - }); + values = values || []; + var a = values[0] || {}; + var b = values[1] || {}; + var c = values[2] || {}; - it("should support filter", function () { - var map = Map({a: 10, b: 20, c: 30}); - expect(map.filter(function (value, key) { - return key === "a" || value === 30; - }).entries()).toEqual([ - ["a", 10], - ["c", 30] - ]); - }); + function shouldHaveTheUsualContent(map) { + expect(map.has(a)).toBe(true); + expect(map.has(b)).toBe(true); + expect(map.has(c)).toBe(false); + expect(map.get(a)).toBe(10); + expect(map.get(b)).toBe(20); + expect(map.get(c)).toBe(undefined); + expect(map.get(c, 30)).toBe(30); + expect(map.length).toBe(2); + expect(map.keys()).toEqual([a, b]); + expect(map.values()).toEqual([10, 20]); + expect(map.entries()).toEqual([[a, 10], [b, 20]]); + expect(map.reduce(function (basis, value, key) { + basis.push([this, key, value]); + return basis; + }, [], map)).toEqual([ + [map, a, 10], + [map, b, 20] + ]);; + } - describe("delete", function () { - it("removes one entry", function () { - var map = Map([[a, 10], [b, 20], [c, 30]]); - expect(map.delete(c)).toBe(true); + it("should be constructable from entry duples with object keys", function () { + var map = Map([[a, 10], [b, 20]]); + shouldHaveTheUsualContent(map); + }); + + it("should be constructable from an interable", function () { + var map = Map({ + forEach: function (callback, thisp) { + callback.call(thisp, [a, 10]); + callback.call(thisp, [b, 20]); + } + }); shouldHaveTheUsualContent(map); }); - }); - describe("clear", function () { - it("deletes all content", function () { + it("should support filter", function () { var map = Map({a: 10, b: 20, c: 30}); - map.clear(); - expect(map.length).toBe(0); - expect(map.keys()).toEqual([]); - expect(map.values()).toEqual([]); - expect(map.entries()).toEqual([]); + expect(map.filter(function (value, key) { + return key === "a" || value === 30; + }).entries()).toEqual([ + ["a", 10], + ["c", 30] + ]); }); - }); - describe("equals", function () { - it("compares maps", function () { - var map = Map({a: 10, b: 20}); - expect(Object.equals(map, map)).toBe(true); - expect(map.equals(map)).toBe(true); - expect(Map({a: 10, b: 20}).equals({b: 20, a: 10})).toBe(true); - expect(Object.equals({a: 10, b: 20}, Map({b: 20, a: 10}))).toBe(true); - expect(Object.equals(Map({b: 20, a: 10}), {a: 10, b: 20})).toBe(true); - expect(Object.equals(Map({b: 20, a: 10}), Map({a: 10, b: 20}))).toBe(true); + describe("delete", function () { + it("removes one entry", function () { + var map = Map([[a, 10], [b, 20], [c, 30]]); + expect(map.delete(c)).toBe(true); + shouldHaveTheUsualContent(map); + }); + }); + + describe("clear", function () { + it("deletes all content", function () { + var map = Map({a: 10, b: 20, c: 30}); + map.clear(); + expect(map.length).toBe(0); + expect(map.keys()).toEqual([]); + expect(map.values()).toEqual([]); + expect(map.entries()).toEqual([]); + }); }); - }); - describe("clone", function () { - it("clones a map", function () { - var map = Map({a: 10, b: 20}); - var clone = Object.clone(map); - expect(map).not.toBe(clone); - expect(map.equals(clone)).toBe(true); + describe("equals", function () { + it("compares maps", function () { + var map = Map({a: 10, b: 20}); + expect(Object.equals(map, map)).toBe(true); + expect(map.equals(map)).toBe(true); + expect(Map({a: 10, b: 20}).equals({b: 20, a: 10})).toBe(true); + expect(Object.equals({a: 10, b: 20}, Map({b: 20, a: 10}))).toBe(true); + expect(Object.equals(Map({b: 20, a: 10}), {a: 10, b: 20})).toBe(true); + expect(Object.equals(Map({b: 20, a: 10}), Map({a: 10, b: 20}))).toBe(true); + }); }); + + describe("clone", function () { + it("clones a map", function () { + var map = Map({a: 10, b: 20}); + var clone = Object.clone(map); + expect(map).not.toBe(clone); + expect(map.equals(clone)).toBe(true); + }); + }); + }); describeObservableMap(Map); diff --git a/spec/set-spec.js b/spec/set-spec.js index 1020bb7..0b36ea3 100644 --- a/spec/set-spec.js +++ b/spec/set-spec.js @@ -7,119 +7,122 @@ var describeSet = require("./set"); describe("Set", function () { - extendSpyExpectation(); + describe("as Set", function () { - function newSet(values) { - return new Set(values); - } + extendSpyExpectation(); - [Set, newSet].forEach(function (Set) { - describeCollection(Set, [1, 2, 3, 4], true); - describeCollection(Set, [{id: 0}, {id: 1}, {id: 2}, {id: 3}], true); - describeSet(Set); - }); + function newSet(values) { + return new Set(values); + } - describeCollection(function (values) { - return Set(values, Object.is); - }, [{}, {}, {}, {}], true); - - it("should pop and shift", function () { - var a = {i: 2}; - var b = {i: 1}; - var c = {i: 0}; - var set = Set([a, b, c], Object.is); - expect(set.pop()).toBe(c); - expect(set.shift()).toBe(a); - }); + [Set, newSet].forEach(function (Set) { + describeCollection(Set, [1, 2, 3, 4], true); + describeCollection(Set, [{id: 0}, {id: 1}, {id: 2}, {id: 3}], true); + describeSet(Set); + }); - it("should dispatch range change on clear", function () { - var set = Set([1, 2, 3]); - var spy = sinon.spy(); - set.observeRangeChange(function (plus, minus, index, _set) { - spy(plus, minus, index); - expect(_set).toBe(set); + describeCollection(function (values) { + return Set(values, Object.is); + }, [{}, {}, {}, {}], true); + + it("should pop and shift", function () { + var a = {i: 2}; + var b = {i: 1}; + var c = {i: 0}; + var set = Set([a, b, c], Object.is); + expect(set.pop()).toBe(c); + expect(set.shift()).toBe(a); }); - set.clear(); - expect(spy).toHaveBeenCalledWith([], [1, 2, 3], 0); - }); - it("should dispatch range change on add", function () { - var set = Set([1, 3]); - var spy = sinon.spy(); - set.observeRangeChange(function (plus, minus, index, _set) { - spy(plus, minus, index); - expect(_set).toBe(set); + it("should dispatch range change on clear", function () { + var set = Set([1, 2, 3]); + var spy = sinon.spy(); + set.observeRangeChange(function (plus, minus, index, _set) { + spy(plus, minus, index); + expect(_set).toBe(set); + }); + set.clear(); + expect(spy).toHaveBeenCalledWith([], [1, 2, 3], 0); }); - set.add(2); - expect(set.toArray()).toEqual([1, 3, 2]); - expect(spy).toHaveBeenCalledWith([2], [], 2); - }); - it("should dispatch range change on delete", function () { - var set = Set([1, 2, 3]); - var spy = sinon.spy(); - set.observeRangeChange(function (plus, minus, index, _set) { - spy(plus, minus, index); - expect(_set).toBe(set); + it("should dispatch range change on add", function () { + var set = Set([1, 3]); + var spy = sinon.spy(); + set.observeRangeChange(function (plus, minus, index, _set) { + spy(plus, minus, index); + expect(_set).toBe(set); + }); + set.add(2); + expect(set.toArray()).toEqual([1, 3, 2]); + expect(spy).toHaveBeenCalledWith([2], [], 2); }); - set["delete"](2); - expect(set.toArray()).toEqual([1, 3]); - expect(spy).toHaveBeenCalledWith([], [2], 1); - }); - it("should dispatch range change on pop", function () { - var set = Set([1, 3, 2]); - var spy = sinon.spy(); - set.observeRangeChange(function (plus, minus, index, _set) { - spy(plus, minus, index); - expect(_set).toBe(set); + it("should dispatch range change on delete", function () { + var set = Set([1, 2, 3]); + var spy = sinon.spy(); + set.observeRangeChange(function (plus, minus, index, _set) { + spy(plus, minus, index); + expect(_set).toBe(set); + }); + set["delete"](2); + expect(set.toArray()).toEqual([1, 3]); + expect(spy).toHaveBeenCalledWith([], [2], 1); }); - expect(set.pop()).toEqual(2); - expect(set.toArray()).toEqual([1, 3]); - expect(spy).toHaveBeenCalledWith([], [2], 2); - }); - it("should dispatch range change on shift", function () { - var set = Set([1, 3, 2]); - var spy = sinon.spy(); - set.observeRangeChange(function (plus, minus, index, _set) { - spy(plus, minus, index); - expect(_set).toBe(set); + it("should dispatch range change on pop", function () { + var set = Set([1, 3, 2]); + var spy = sinon.spy(); + set.observeRangeChange(function (plus, minus, index, _set) { + spy(plus, minus, index); + expect(_set).toBe(set); + }); + expect(set.pop()).toEqual(2); + expect(set.toArray()).toEqual([1, 3]); + expect(spy).toHaveBeenCalledWith([], [2], 2); }); - expect(set.shift()).toEqual(1); - expect(set.toArray()).toEqual([3, 2]); - expect(spy).toHaveBeenCalledWith([], [1], 0); - }); - // Need to reevaluate whether sets fully support range changes, or whether - // they support merely set changes (no index). - it("should dispatch range change on shift then pop", function () { - var set = Set([1, 3]); - set.observeRangeChange(function (plus, minus, index, _set) { - spy(plus, minus, index); - expect(_set).toBe(set); + it("should dispatch range change on shift", function () { + var set = Set([1, 3, 2]); + var spy = sinon.spy(); + set.observeRangeChange(function (plus, minus, index, _set) { + spy(plus, minus, index); + expect(_set).toBe(set); + }); + expect(set.shift()).toEqual(1); + expect(set.toArray()).toEqual([3, 2]); + expect(spy).toHaveBeenCalledWith([], [1], 0); }); - var spy = sinon.spy(); - expect(set.add(2)).toEqual(true); - expect(set.toArray()).toEqual([1, 3, 2]); - expect(spy).toHaveBeenCalledWith([2], [], 2); - - var spy = sinon.spy(); - expect(set.shift()).toEqual(1); - expect(set.toArray()).toEqual([3, 2]); - expect(spy).toHaveBeenCalledWith([], [1], 0); - - var spy = sinon.spy(); - expect(set.pop()).toEqual(2); - expect(set.toArray()).toEqual([3]); - expect(spy).toHaveBeenCalledWith([], [2], 1); - - var spy = sinon.spy(); - expect(set.delete(3)).toEqual(true); - expect(set.toArray()).toEqual([]); - expect(spy).toHaveBeenCalledWith([], [3], 0); - }); + // Need to reevaluate whether sets fully support range changes, or whether + // they support merely set changes (no index). + it("should dispatch range change on shift then pop", function () { + var set = Set([1, 3]); + set.observeRangeChange(function (plus, minus, index, _set) { + spy(plus, minus, index); + expect(_set).toBe(set); + }); + + var spy = sinon.spy(); + expect(set.add(2)).toEqual(true); + expect(set.toArray()).toEqual([1, 3, 2]); + expect(spy).toHaveBeenCalledWith([2], [], 2); + + var spy = sinon.spy(); + expect(set.shift()).toEqual(1); + expect(set.toArray()).toEqual([3, 2]); + expect(spy).toHaveBeenCalledWith([], [1], 0); + + var spy = sinon.spy(); + expect(set.pop()).toEqual(2); + expect(set.toArray()).toEqual([3]); + expect(spy).toHaveBeenCalledWith([], [2], 1); + + var spy = sinon.spy(); + expect(set.delete(3)).toEqual(true); + expect(set.toArray()).toEqual([]); + expect(spy).toHaveBeenCalledWith([], [3], 0); + }); + }); }); From a41067b35a078d1353c70bf9bbd20ae94e0bbbf4 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Wed, 23 Apr 2014 21:04:02 -0700 Subject: [PATCH 74/83] Generalize Set specs for alternate iteration order Because LFU sets iterate in order of frequency instead of insertion. --- spec/set.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/set.js b/spec/set.js index eb4aee2..58b0379 100644 --- a/spec/set.js +++ b/spec/set.js @@ -112,20 +112,20 @@ function describeSet(Set, sorted) { }); it("should compute unions", function () { - expect(Set([1, 2, 3]).union([2, 3, 4]).toArray()).toEqual([1, 2, 3, 4]); + expect(Set([1, 2, 3]).union([2, 3, 4]).sorted()).toEqual([1, 2, 3, 4]); expect(Set([1, 2, 3]).union([2, 3, 4]).equals([1, 2, 3, 4])).toBe(true); }); it("should compute intersections", function () { - expect(Set([1, 2, 3]).intersection([2, 3, 4]).toArray()).toEqual([2, 3]); + expect(Set([1, 2, 3]).intersection([2, 3, 4]).sorted()).toEqual([2, 3]); }); it("should compute differences", function () { - expect(Set([1, 2, 3]).difference([2, 3, 4]).toArray()).toEqual([1]); + expect(Set([1, 2, 3]).difference([2, 3, 4]).sorted()).toEqual([1]); }); it("should compute symmetric differences", function () { - expect(Set([1, 2, 3]).symmetricDifference([2, 3, 4]).toArray()).toEqual([1, 4]); + expect(Set([1, 2, 3]).symmetricDifference([2, 3, 4]).sorted()).toEqual([1, 4]); }); } From aa0d3f05a69ef5ee47e656a0bed38d7ec6b5bd98 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Fri, 16 May 2014 10:39:09 -0700 Subject: [PATCH 75/83] Re-enable LfuSet Set specs --- spec/lfu-set-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lfu-set-spec.js b/spec/lfu-set-spec.js index e81ea1d..2be272c 100644 --- a/spec/lfu-set-spec.js +++ b/spec/lfu-set-spec.js @@ -14,7 +14,7 @@ describe("LfuSet", function () { [LfuSet, newLfuSet].forEach(function (LfuSet) { describeCollection(LfuSet, [1, 2, 3, 4], true); describeCollection(LfuSet, [{id: 0}, {id: 1}, {id: 2}, {id: 3}], true); - //describeSet(LfuSet); + describeSet(LfuSet); }); it("should handle many repeated values", function () { From 996876252e379ca347ac65cbc63fe112a1d8d80d Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Wed, 23 Apr 2014 21:12:15 -0700 Subject: [PATCH 76/83] Update change log and checklist for 1.1.0 --- CHANGES.md | 4 +- checklist.csv | 189 +++++++++++++++++++++++++------------------------- lfu-set.js | 1 + 3 files changed, 100 insertions(+), 94 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index edb1e66..57ebd02 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,8 +6,10 @@ - Removes support for `any` and `all`. Use `some(Boolean)` or `every(Boolean)`. -## v1.0.3 +## v1.1.0 +- Adds an LfuSet, a set useful as a cache with a least-frequently-used + eviction strategy. - Fixes array `set` and `swap` for indexes outside the bounds of the existing array, for both observed and unobserved arrays. diff --git a/checklist.csv b/checklist.csv index 4504ab3..cb5eb7c 100644 --- a/checklist.csv +++ b/checklist.csv @@ -1,93 +1,96 @@ -(na),(na),,Sets,,,,,,Maps,,,,,,,,Orders,,,,,Shims,,Generics,,, -Order,Method,Interface,Set,SortedSet,LruSet,SortedArraySet,FastSet,ArraySet,Map,MultiMap,SortedMap,LruMap,SortedArrayMap,FastMap,Dict,WeakMap,List,Deque,Heap,SortedArray,Iterator,Array,Object (not prototype),GenericCollection,GenericSet,GenericMap,GenericOrder -0a1,has(value),collection,Set,SortedSet,LruSet,SortedArray,FastSet,(todo),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(todo),(alt),SortedArray,(alt),(alt),(alt),,,, -0a2,"has(value, equals=)",order,(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),List,(todo),(maybe todo),(alt),(maybe todo),Array,(alt),,,, -0a3,has(key),map,(alt),(alt),(alt),(alt),(alt),(alt),GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,Dict,WeakMap,(alt),(na),(alt),(alt),(alt),(alt),Object,,,GenericMap, -0b1a,get(value),collection,Set,SortedSet,LruSet,SortedArray,FastSet,(todo),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),SortedArray,(maybe todo),(alt),(alt),,,, -0b1b,"get(value, equals=)",order,(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),List,(alt),(alt),(alt),(alt),(alt),(alt),,,, -0b2a,get(key),(na),(na),(na),(na),(na),(na),(alt),(na),(na),(na),(na),(na),(na),(na),WeakMap,(na),Deque,(na),(na),(alt),(na),(na),(na),(na),(na),(na) -0b2b,"get(key or index, defaultValue)",map,(alt),(alt),(alt),(alt),(alt),(alt),GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,Dict,(todo maybe),(alt),(na),(alt),(alt),(alt),Array,Object,,,GenericMap, -0c1,"set(key or index, value)","map, array",(na),(na),(na),(na),(na),(na),GenericMap,MultiMap,GenericMap,GenericMap,GenericMap,GenericMap,Dict,WeakMap,(na),(na),(na),(na),(na),Array,Object,,,GenericMap, -0d1a1,add(value),collection,Set,SortedSet,LruSet,SortedArraySet,FastSet,(todo),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),List,Queue,Heap,SortedArray,(na),(alt),(alt),,,, -0d1a2,"add(value, key)",map,(na),(na),(na),(na),(na),(alt),GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,Dict,(todo maybe),(na),(na),(na),(na),(na),(todo maybe),(todo maybe for the property change),,,GenericMap, -0d1b1,addEach(values),collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),(alt),(alt),GenericCollection,GenericCollection,GenericCollection,GenericCollection,(alt),(alt),GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,(alt),GenericCollection,,, -0d1b2,addEach(map),map,(alt),(alt),(alt),(alt),(alt),(na),GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,(todo maybe),(alt),(alt),(alt),(alt),(alt),(alt),Object,,,GenericMap, -0d2a1a,delete(value),collection,Set,SortedSet,LruSet,SortedArray,FastSet,(todo),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(na),Heap,SortedArray,(na),(alt),(alt),,,, -0d2a1b,"delete(value, equals)",order,(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),List,(na),(alt),(alt),(na),Array,(alt),,,, -0d2a2,delete(key or index),map,(alt),(alt),(alt),(alt),(alt),(alt),GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,Dict,WeakMap,(alt),(na),(alt),(alt),(na),(alt),(todo maybe for the property change),,,GenericMap, -0d2b1,"deleteEach(keys or values, optional equals)",collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(alt),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo maybe),GenericCollection,(na),GenericCollection,GenericCollection,(na),GenericCollection,(todo maybe),GenericCollection,,, -1a1,"indexOf(value, index)",array,(na),SortedSet,(na),SortedArray O(log length),(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),(todo),Deque,(na),SortedArray O(log length),(todo),(spec),(na),,,, -1a2,"lastIndexOf(value, index)",order,(na),(na because uniqueness guarantees equivalence to indexOf),(na),SortedArray O(log length),(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),(todo),Deque,(na),SortedArray O(log length),(todo),(spec),(na),,,, -1b1,"find(callback, thisp, index)",order,(todo),(todo),(na),(todo),(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),(todo),(todo),(na),(todo),(todo),ES6,(na),,,, -1b2,"findLast(callback, thisp, index)",order,(na),(na),(na),(todo),(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),(todo),(todo),(na),(todo),(todo),(todo),(na),,,, -1c1,"findIndex(callback, thisp, index)",order,(na),(todo),(na),(todo),(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),(todo),(todo),(na),(todo),(todo),ES6,(na),,,, -1c2,"findLastIndex(callback, thisp, index)",order,(na),(na),(na),(todo),(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),(todo),(todo),(na),(todo),(todo),(todo),(na),,,, -1d1,"findValue(value, equals=, index) nee find",order,(na),SortedSet,(na),SortedArray O(log length),(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),List,Deque,(na),SortedArray O(log length),(na),(todo),(na),,,, -1d2,"findLastValue(value, equals=, index) nee findLast",order,(na),SortedSet,(na),SortedArray O(log length),(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),List,Deque,(na),SortedArray O(log length),(na),(todo),(na),,,, -1e,findLeast(),sorted collection,(na),SortedSet,(na),(todo) SortedArray O(1),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(todo) SortedArray O(1),(na),(na),(na),,,, -1e1,findLeastGreaterThan(value),sorted collection,(na),SortedSet,(na),(todo) SortedArray O(log n),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(todo) SortedArray O(log n),(na),(na),(na),,,, -1e2,findLeastGreaterThanOrEqual(value),sorted collection,(na),SortedSet,(na),(todo) SortedArray O(log n),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(todo) SortedArray O(log n),(na),(na),(na),,,, -1f,findGreatest(),sorted collection,(na),SortedSet,(na),(todo) SortedArray O(1),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(todo) SortedArray O(1),(na),(na),(na),,,, -1f1,findGreatestLessThan(value),sorted collection,(na),SortedSet,(na),(todo) SortedArray O(log n),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(todo) SortedArray O(log n),(na),(na),(na),,,, -1f2,findGreatestLessThanOrEqual(value),sorted collection,(na),SortedSet,(na),(todo) SortedArray O(log n),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(todo) SortedArray O(log n),(na),(na),(na),,,, -2-dequeue-1a,push(...values),dequeue,(maybe todo) GenericCollection,SortedSet,(maybe todo) GenericCollection,SortedArray O(log length),(maybe todo) GenericCollection,(na),(na),(na),(na),(na),(na),(na),(na),(na),List,Queue,Heap,SortedArray O(log length),(na),(spec),(na),,,, -2-dequeue-1b,unshift(...values),dequeue,(maybe todo) GenericCollection,SortedSet,(maybe todo) GenericCollection,SortedArray O(log length),(maybe todo) GenericCollection,(na),(na),(na),(na),(na),(na),(na),(na),(na),List,(todo),(na),SortedArray O(log length),(na),(spec),(na),,,, -2-dequeue-2a,pop(),dequeue,Set,SortedSet,(maybe todo),SortedArray O(1),(maybe todo),(na),(na),(na),(na),(na),(na),(na),(na),(na),List,(todo),Heap,SortedArray O(1),(na),(spec),(na),,,, -2-dequeue-2b,shift(),dequeue,Set,SortedSet,(maybe todo),SortedArray O(1),(maybe todo),(na),(na),(na),(na),(na),(na),(na),(na),(na),List,Queue,(na),SortedArray O(1),(na),(spec),(na),,,, -2-dequeue-3a,peek(),dequeue,(na),(maybe todo),(na),(maybe todo),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),List (doc),(todo),Heap,(maybe todo),(na),(maybe todo),(na),,,, -2-dequeue-3b,poke(value),dequeue,(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),List (doc),(todo),(na),(na),(na),(maybe todo),(na),,,, -3a,"slice(start, end)",array,(na),SortedSet,(na),SortedArray,(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),List,(maybe todo),(na),SortedArray,(na),(spec),(na),,,, -3b,"splice(start, length, ...values)",array,(na),"SortedSet (removes in place, adds to proper positions)",(na),SortedArray,(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),List,(na),(na),SortedArray,(na),(spec),(na),,,, -3c,"swap(start, length, values)",array,(na),(maybe todo),(na),SortedArray,(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),List,(na),(na),SortedArray,(na),Array,(na),,,, -3d,clear(),collection,Set,SortedSet,LruSet,SortedArray,FastSet,(todo),GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,Dict,(maybe todo),List,Deque,Heap,SortedArray,(na),Array,Object,,,GenericMap, -4a1,sort(compare=),collection,(na),(na),(na),(na),(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),(wont),(na),(na),(na),(na),(spec),(na),,,, -4a2,sorted(compare=),collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(wont),GenericCollection,,, -4a3,"group(cb, thisp, equals)",collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(wont),GenericCollection,,, -4b1,reverse(),collection,(na),(wont),(na),(na),(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),List,(todo),(na),(na),(na),(spec),(wont),,,, -4b2,reversed(),collection,(todo),(todo),(todo),(todo),(todo),(todo),(na),(na),(na),(na),(na),(na),(na),(na),List,GenericCollection,(na),(wont),GenericCollection,GenericCollection,(wont),GenericCollection,,, -5a,keys(),map,(na),(na),(na),(na),(na),(na),GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,(na),(na),(na),(na),(na),(na),(wont),(spec),,,GenericMap, -5b,values(),"map, array",(na),(na),(na),(na),(na),(na),GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,(na),(na),(na),(na),(na),(na),(wont),Object,,,GenericMap, -5c,items(),map,(na),(na),(na),(na),(na),(na),GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,(na),(na),(na),(na),(na),(na),(wont),(wont),,,GenericMap, -6a1,"reduce(cb, basis, thisp)",collection,Set,SortedSet,LruSet,SortedArraySet,FastSet,(todo),GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,Dict,(na),List,Deque,Heap,SortedArray,Iterator,(spec),(wont),,,GenericMap, -6a2,"reduceRight(cb, basis, thisp)",collection,Set,SortedSet,LruSet,SortedArraySet,(fast set is not ordered),(todo),GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,Dict,(na),List,Deque,Heap,SortedArray,(na),(spec),(wont),,,GenericMap, -6b,"forEach(cb, thisp)",collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(spec),Object,GenericCollection,,, -6c,"map(cb, thisp)",collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(spec),Object,GenericCollection,,, -6d,"filter(cb, thisp)",collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(spec),(wont),GenericCollection,,, -6e1a,"every(cb, thisp)",collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(spec),(wont),GenericCollection,,, -6e1b,"some(cb, thisp)",collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(spec),(wont),GenericCollection,,, -6e2a,any(),collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(wont),GenericCollection,,, -6e2b,all(),collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(wont),GenericCollection,,, -6f1,min(),collection,GenericCollection,SortedSet,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(wont),GenericCollection,,, -6f2,max(),collection,GenericCollection,SortedSet,GenericCollection,SortedArray,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,Heap O(1),SortedArray,GenericCollection,GenericCollection,(wont),GenericCollection,,, -6g,sum(),collection,GenericCollection,GenericCollection,GenericCollection,SortedArray,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,SortedArray,GenericCollection,GenericCollection,(wont),GenericCollection,,, -6h,average(),collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(wont),GenericCollection,,, -7a,one(),collection,Set,SortedSet,LruSet,SortedArray,FastSet,(todo),(todo),(todo),(todo),(todo),(todo),(todo),Dict,(na),List,Deque,Heap O(1),SortedArray,(maybe todo),Array,(wont),,,, -7b,only(),collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,(wont),GenericCollection,,, -8a,concat(...iterables),collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,Iterator,(spec),Object,GenericCollection,,, -8b,flatten(),collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(wont),GenericCollection,,, -8c,zip(...collections),collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(wont),GenericCollection,,, -8d,enumerate(zero=0),collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(wont),GenericCollection,,, -8e,join(delimiter),collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(wont),GenericCollection,,, -9a1,equals(that),collection,GenericSet,GenericSet,GenericSet,(alt),GenericSet,(todo),GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,(na),(alt),(alt),(na),(alt),(na),(alt),(alt),(na),GenericSet,GenericMap,(alt) -9a2,"equals(that, equals)",order,(alt),(alt),(alt),SortedArray,(alt),(todo),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(na),GenericOrder,GenericOrder,(na),SortedArray,(na),Array,Object,(na),(alt),(alt),GenericOrder -9b,"compare(that, compare)",order,(na),(na),(na),SortedArray,(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),GenericOrder,GenericOrder,(na),SortedArray,(na),Array,(na),,,,GenericOrder -a1,toArray(),collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(alt) Array.from,GenericCollection,,, -a2,toObject(),collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(alt) Object.from,GenericCollection,,, -b1,"clone(depth, memo)",collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),Array,Object,GenericCollection,,, -b2,constructClone(values),collection,Set,SortedSet,LruSet,SortedArray,FastSet,(todo),Map,MultiMap,SortedMap,LruMap,SortedArrayMap,FastMap,Dict,(na),List,Deque,Heap,SortedArray,Iterator,Array,(na),,,, -c1,iterate(),collection,Set,SortedSet,LruSet,SortedArray,FastSet,(todo),(na),(na),(na),(na),(na),(na),(na),(na),List,(todo),(maybe todo),SortedArray,Iterator,Array,(na),,,, -c2,"iterate(start, end)",array,(todo),SortedSet,(na),(todo),(na),(todo),(todo),(todo),(todo),(todo),(todo),(todo),(todo),(na),(todo),(todo),(na),(todo),(todo),Array,(na),,,, -d1,union(that),set,GenericSet,GenericSet,GenericSet,(maybe todo) GenericMap,GenericSet,(todo),(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(na),(na),(na),(na),(na),(na),(na),(na),,GenericSet,, -d2,intersection(that),set,GenericSet,GenericSet,GenericSet,(maybe todo) GenericMap,GenericSet,(todo),(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(na),(na),(na),(na),(na),(na),(na),(na),,GenericSet,, -d3,difference(that),set,GenericSet,GenericSet,GenericSet,(maybe todo) GenericMap,GenericSet,(todo),(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(na),(na),(na),(na),(na),(na),(na),(na),,GenericSet,, -d4,symmetricDifference(that),set,GenericSet,GenericSet,GenericSet,(maybe todo) GenericMap,GenericSet,(todo),(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(na),(na),(na),(na),(na),(na),(na),(na),,GenericSet,, -f1,"dropWhile(cb, thisp)",iterator,(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),Iterator,(na),(na),,,, -f2,"takeWhile(cb, thisp)",iterator,(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),Iterator,(na),(na),,,, -f3,"mapIterator(cb, thisp)",iterator,(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),Iterator,(na),(na),,,, -f4,"filterIterator(cb, thisp)",iterator,(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),Iterator,(na),(na),,,, -f5,zipIterator(...iterators),iterator,(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),Iterator,(na),(na),,,, -f6,enumerateIterator(zero=0),iterator,(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),Iterator,(na),(na),,,, -z1,Observable Object,object,X,X,X,X,X,(todo),X,X,X,X,X,X,X,(wont),X,X,X,X,(wont),X-,X-,,,, -z2,Observable Range,array,X,X,X,X,(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),X,X,X,X,(na),X-,(na),,,, -z3,Observable Map,map,(na),(na),(na),(na),(na),(na),X,X,X,X,X,X,X,(na),(na),(na),X,(na),(na),X-,(na),,,, -z4,Observable Set,set,(todo),(todo),(todo),(todo),(todo),(todo),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na) \ No newline at end of file +(na),(na),,Sets,,,,,,,Maps,,,,,,,,Orders,,,,,Shims,,Generics,,, +Order,Method,Interface,Set,SortedSet,LruSet,LfuSet,SortedArraySet,FastSet,ArraySet,Map,MultiMap,SortedMap,LruMap,SortedArrayMap,FastMap,Dict,WeakMap,List,Deque,Heap,SortedArray,Iterator,Array,Object (not prototype),GenericCollection,GenericSet,GenericMap,GenericOrder +0a1,has(value),collection,Set,SortedSet,LruSet,LfuSet,SortedArray,FastSet,(todo),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(todo),(alt),SortedArray,(alt),(alt),(alt),,,, +0a2,"has(value, equals=)",order,(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),List,(todo),(maybe todo),(alt),(maybe todo),Array,(alt),,,, +0a3,has(key),map,(alt),(alt),(alt),(alt),(alt),(alt),(alt),GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,Dict,WeakMap,(alt),(na),(alt),(alt),(alt),(alt),Object,,,GenericMap, +0b1a,get(value),collection,Set,SortedSet,LruSet,LfuSet,SortedArray,FastSet,(todo),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),SortedArray,(maybe todo),(alt),(alt),,,, +0b1b,"get(value, equals=)",order,(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),List,(alt),(alt),(alt),(alt),(alt),(alt),,,, +0b2a,get(key),(na),(na),(na),(na),(na),(na),(na),(alt),(na),(na),(na),(na),(na),(na),(na),WeakMap,(na),Deque,(na),(na),(alt),(na),(na),(na),(na),(na),(na) +0b2b,"get(key or index, defaultValue)",map,(alt),(alt),(alt),(alt),(alt),(alt),(alt),GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,Dict,(todo maybe),(alt),(na),(alt),(alt),(alt),Array,Object,,,GenericMap, +0c1,"set(key or index, value)","map, array",(na),(na),(na),(na),(na),(na),(na),GenericMap,MultiMap,GenericMap,GenericMap,GenericMap,GenericMap,Dict,WeakMap,(na),(na),(na),(na),(na),Array,Object,,,GenericMap, +0d1a1,add(value),collection,Set,SortedSet,LruSet,LfuSet,SortedArraySet,FastSet,(todo),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),List,Queue,Heap,SortedArray,(na),(alt),(alt),,,, +0d1a2,"add(value, key)",map,(na),(na),(na),(na),(na),(na),(alt),GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,Dict,(todo maybe),(na),(na),(na),(na),(na),(todo maybe),(todo maybe for the property change),,,GenericMap, +0d1b1,addEach(values),collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),(alt),(alt),GenericCollection,GenericCollection,GenericCollection,GenericCollection,(alt),(alt),GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,(alt),GenericCollection,,, +0d1b2,addEach(map),map,(alt),(alt),(alt),(alt),(alt),(alt),(na),GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,(todo maybe),(alt),(alt),(alt),(alt),(alt),(alt),Object,,,GenericMap, +0d2a1a,delete(value),collection,Set,SortedSet,LruSet,LfuSet,SortedArray,FastSet,(todo),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(na),Heap,SortedArray,(na),(alt),(alt),,,, +0d2a1b,"delete(value, equals)",order,(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(alt),List,(na),(alt),(alt),(na),Array,(alt),,,, +0d2a2,delete(key or index),map,(alt),(alt),(alt),(alt),(alt),(alt),(alt),GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,Dict,WeakMap,(alt),(na),(alt),(alt),(na),(alt),(todo maybe for the property change),,,GenericMap, +0d2b1,"deleteEach(keys or values, optional equals)",collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(alt),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo maybe),GenericCollection,(na),GenericCollection,GenericCollection,(na),GenericCollection,(todo maybe),GenericCollection,,, +1a1,"indexOf(value, index)",array,(na),SortedSet,(na),(na),SortedArray O(log length),(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),(todo),Deque,(na),SortedArray O(log length),(todo),(spec),(na),,,, +1a2,"lastIndexOf(value, index)",order,(na),(na because uniqueness guarantees equivalence to indexOf),(na),(na),SortedArray O(log length),(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),(todo),Deque,(na),SortedArray O(log length),(todo),(spec),(na),,,, +1b1,"find(callback, thisp, index)",order,(todo),(todo),(na),(na),(todo),(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),(todo),(todo),(na),(todo),(todo),ES6,(na),,,, +1b2,"findLast(callback, thisp, index)",order,(na),(na),(na),(na),(todo),(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),(todo),(todo),(na),(todo),(todo),(todo),(na),,,, +1c1,"findIndex(callback, thisp, index)",order,(na),(todo),(na),(na),(todo),(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),(todo),(todo),(na),(todo),(todo),ES6,(na),,,, +1c2,"findLastIndex(callback, thisp, index)",order,(na),(na),(na),(na),(todo),(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),(todo),(todo),(na),(todo),(todo),(todo),(na),,,, +1d1,"findValue(value, equals=, index) nee find",order,(na),SortedSet,(na),(na),SortedArray O(log length),(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),List,Deque,(na),SortedArray O(log length),(na),(todo),(na),,,, +1d2,"findLastValue(value, equals=, index) nee findLast",order,(na),SortedSet,(na),(na),SortedArray O(log length),(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),List,Deque,(na),SortedArray O(log length),(na),(todo),(na),,,, +1e,findLeast(),sorted collection,(na),SortedSet,(na),(na),(todo) SortedArray O(1),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(todo) SortedArray O(1),(na),(na),(na),,,, +1e1,findLeastGreaterThan(value),sorted collection,(na),SortedSet,(na),(na),(todo) SortedArray O(log n),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(todo) SortedArray O(log n),(na),(na),(na),,,, +1e2,findLeastGreaterThanOrEqual(value),sorted collection,(na),SortedSet,(na),(na),(todo) SortedArray O(log n),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(todo) SortedArray O(log n),(na),(na),(na),,,, +1f,findGreatest(),sorted collection,(na),SortedSet,(na),(na),(todo) SortedArray O(1),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(todo) SortedArray O(1),(na),(na),(na),,,, +1f1,findGreatestLessThan(value),sorted collection,(na),SortedSet,(na),(na),(todo) SortedArray O(log n),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(todo) SortedArray O(log n),(na),(na),(na),,,, +1f2,findGreatestLessThanOrEqual(value),sorted collection,(na),SortedSet,(na),(na),(todo) SortedArray O(log n),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(todo) SortedArray O(log n),(na),(na),(na),,,, +2-dequeue-1a,push(...values),dequeue,(maybe todo) GenericCollection,SortedSet,(maybe todo) GenericCollection,(maybe todo) GenericCollection,SortedArray O(log length),(maybe todo) GenericCollection,(na),(na),(na),(na),(na),(na),(na),(na),(na),List,Queue,Heap,SortedArray O(log length),(na),(spec),(na),,,, +2-dequeue-1b,unshift(...values),dequeue,(maybe todo) GenericCollection,SortedSet,(maybe todo) GenericCollection,(maybe todo) GenericCollection,SortedArray O(log length),(maybe todo) GenericCollection,(na),(na),(na),(na),(na),(na),(na),(na),(na),List,(todo),(na),SortedArray O(log length),(na),(spec),(na),,,, +2-dequeue-2a,pop(),dequeue,Set,SortedSet,(maybe todo),(maybe todo),SortedArray O(1),(maybe todo),(na),(na),(na),(na),(na),(na),(na),(na),(na),List,(todo),Heap,SortedArray O(1),(na),(spec),(na),,,, +2-dequeue-2b,shift(),dequeue,Set,SortedSet,(maybe todo),(maybe todo),SortedArray O(1),(maybe todo),(na),(na),(na),(na),(na),(na),(na),(na),(na),List,Queue,(na),SortedArray O(1),(na),(spec),(na),,,, +2-dequeue-3a,peek(),dequeue,(na),(maybe todo),(na),(na),(maybe todo),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),List (doc),(todo),Heap,(maybe todo),(na),(maybe todo),(na),,,, +2-dequeue-3b,poke(value),dequeue,(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),List (doc),(todo),(na),(na),(na),(maybe todo),(na),,,, +3a,"slice(start, end)",array,(na),SortedSet,(na),(na),SortedArray,(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),List,(maybe todo),(na),SortedArray,(na),(spec),(na),,,, +3b,"splice(start, length, ...values)",array,(na),"SortedSet (removes in place, adds to proper positions)",(na),(na),SortedArray,(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),List,(na),(na),SortedArray,(na),(spec),(na),,,, +3c,"swap(start, length, values)",array,(na),(maybe todo),(na),(na),SortedArray,(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),List,(na),(na),SortedArray,(na),Array,(na),,,, +3d,clear(),collection,Set,SortedSet,LruSet,LfuSet,SortedArray,FastSet,(todo),GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,Dict,(maybe todo),List,Deque,Heap,SortedArray,(na),Array,Object,,,GenericMap, +4a1,sort(compare=),collection,(na),(na),(na),(na),(na),(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),(wont),(na),(na),(na),(na),(spec),(na),,,, +4a2,sorted(compare=),collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(wont),GenericCollection,,, +4a3,"group(cb, thisp, equals)",collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(wont),GenericCollection,,, +4b1,reverse(),collection,(na),(wont),(na),(na),(na),(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),List,(todo),(na),(na),(na),(spec),(wont),,,, +4b2,reversed(),collection,(todo),(todo),(todo),(todo),(todo),(todo),(todo),(na),(na),(na),(na),(na),(na),(na),(na),List,GenericCollection,(na),(wont),GenericCollection,GenericCollection,(wont),GenericCollection,,, +5a,keys(),map,(na),(na),(na),(na),(na),(na),(na),GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,(na),(na),(na),(na),(na),(na),(wont),(spec),,,GenericMap, +5b,values(),"map, array",(na),(na),(na),(na),(na),(na),(na),GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,(na),(na),(na),(na),(na),(na),(wont),Object,,,GenericMap, +5c,items(),map,(na),(na),(na),(na),(na),(na),(na),GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,(na),(na),(na),(na),(na),(na),(wont),(wont),,,GenericMap, +6a1,"reduce(cb, basis, thisp)",collection,Set,SortedSet,LruSet,LfuSet,SortedArraySet,FastSet,(todo),GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,Dict,(na),List,Deque,Heap,SortedArray,Iterator,(spec),(wont),,,GenericMap, +6a2,"reduceRight(cb, basis, thisp)",collection,Set,SortedSet,LruSet,LfuSet,SortedArraySet,(fast set is not ordered),(todo),GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,Dict,(na),List,Deque,Heap,SortedArray,(na),(spec),(wont),,,GenericMap, +6b,"forEach(cb, thisp)",collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(spec),Object,GenericCollection,,, +6c,"map(cb, thisp)",collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(spec),Object,GenericCollection,,, +6d,"filter(cb, thisp)",collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(spec),(wont),GenericCollection,,, +6e1a,"every(cb, thisp)",collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(spec),(wont),GenericCollection,,, +6e1b,"some(cb, thisp)",collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(spec),(wont),GenericCollection,,, +6e2a,any(),collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(wont),GenericCollection,,, +6e2b,all(),collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(wont),GenericCollection,,, +6f1,min(),collection,GenericCollection,SortedSet,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(wont),GenericCollection,,, +6f2,max(),collection,GenericCollection,SortedSet,GenericCollection,GenericCollection,SortedArray,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,Heap O(1),SortedArray,GenericCollection,GenericCollection,(wont),GenericCollection,,, +6g,sum(),collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,SortedArray,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,SortedArray,GenericCollection,GenericCollection,(wont),GenericCollection,,, +6h,average(),collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(wont),GenericCollection,,, +7a,one(),collection,Set,SortedSet,LruSet,LfuSet,SortedArray,FastSet,(todo),(todo),(todo),(todo),(todo),(todo),(todo),Dict,(na),List,Deque,Heap O(1),SortedArray,(maybe todo),Array,(wont),,,, +7b,only(),collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,(wont),GenericCollection,,, +8a,concat(...iterables),collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,Iterator,(spec),Object,GenericCollection,,, +8b,flatten(),collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(wont),GenericCollection,,, +8c,zip(...collections),collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(wont),GenericCollection,,, +8d,enumerate(zero=0),collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(wont),GenericCollection,,, +8e,join(delimiter),collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(wont),GenericCollection,,, +9a1,equals(that),collection,GenericSet,GenericSet,GenericSet,GenericSet,(alt),GenericSet,(todo),GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,GenericMap,(na),(alt),(alt),(na),(alt),(na),(alt),(alt),(na),GenericSet,GenericMap,(alt) +9a2,"equals(that, equals)",order,(alt),(alt),(alt),(alt),SortedArray,(alt),(todo),(alt),(alt),(alt),(alt),(alt),(alt),(alt),(na),GenericOrder,GenericOrder,(na),SortedArray,(na),Array,Object,(na),(alt),(alt),GenericOrder +9b,"compare(that, compare)",order,(na),(na),(na),(na),SortedArray,(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),GenericOrder,GenericOrder,(na),SortedArray,(na),Array,(na),,,,GenericOrder +a1,toArray(),collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(alt) Array.from,GenericCollection,,, +a2,toObject(),collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(alt) Object.from,GenericCollection,,, +b1,"clone(depth, memo)",collection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(todo),GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),GenericCollection,GenericCollection,GenericCollection,GenericCollection,(na),Array,Object,GenericCollection,,, +b2,constructClone(values),collection,Set,SortedSet,LruSet,LfuSet,SortedArray,FastSet,(todo),Map,MultiMap,SortedMap,LruMap,SortedArrayMap,FastMap,Dict,(na),List,Deque,Heap,SortedArray,Iterator,Array,(na),,,, +d1,union(that),set,GenericSet,GenericSet,GenericSet,GenericSet,(maybe todo) GenericMap,GenericSet,(todo),(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(na),(na),(na),(na),(na),(na),(na),(na),,GenericSet,, +d2,intersection(that),set,GenericSet,GenericSet,GenericSet,GenericSet,(maybe todo) GenericMap,GenericSet,(todo),(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(na),(na),(na),(na),(na),(na),(na),(na),,GenericSet,, +d3,difference(that),set,GenericSet,GenericSet,GenericSet,GenericSet,(maybe todo) GenericMap,GenericSet,(todo),(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(na),(na),(na),(na),(na),(na),(na),(na),,GenericSet,, +d4,symmetricDifference(that),set,GenericSet,GenericSet,GenericSet,GenericSet,(maybe todo) GenericMap,GenericSet,(todo),(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(maybe todo) GenericMap,(na),(na),(na),(na),(na),(na),(na),(na),,GenericSet,, +c1,iterate(),collection,Set,SortedSet,LruSet,LfuSet,SortedArray,FastSet,(todo),(na),(na),(na),(na),(na),(na),(na),(na),List,(todo),(maybe todo),SortedArray,Iterator,Array,(na),,,, +c2,"iterate(start, end, stride)",array,(todo),SortedSet,(na),(na),(todo),(na),(todo),(todo),(todo),(todo),(todo),(todo),(todo),(todo),(na),(todo),(todo),(na),(todo),(todo),Array,(na),,,, +f1,"dropWhile(cb, thisp)",iterator,(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),Iterator,(na),(na),,,, +f2,"takeWhile(cb, thisp)",iterator,(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),Iterator,(na),(na),,,, +f3,"iterateMap(cb, thisp)",iterator,(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),Iterator,(na),(na),,,, +f4,"iterateFilter(cb, thisp)",iterator,(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),Iterator,(na),(na),,,, +f5,iterateZip(...iterators),iterator,(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),Iterator,(na),(na),,,, +f6,iterateUnzip(),,,,,,,,,,,,,,,,,,,,,,,,,,, +f7,iterateEnumerate(zero=0),iterator,(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),Iterator,(na),(na),,,, +f8,iterateConcat(...iterators),,,,,,,,,,,,,,,,,,,,,,,,,,, +f9,iterateFlatten(),,,,,,,,,,,,,,,,,,,,,,,,,,, +z1,Observable Object,object,X,X,X,X,X,X,(todo),X,X,X,X,X,X,X,(wont),X,X,X,X,(wont),X-,X-,,,, +z2,Observable Range,array,X,X,X,X,X,(na),(todo),(na),(na),(na),(na),(na),(na),(na),(na),X,X,X,X,(na),X-,(na),,,, +z3,Observable Map,map,(na),(na),(na),(na),(na),(na),(na),X,X,X,X,X,X,X,(na),(na),(na),X,(na),(na),X-,(na),,,, +z4,Observable Set,set,(todo),(todo),(todo),(todo),(todo),(todo),(todo),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na),(na) \ No newline at end of file diff --git a/lfu-set.js b/lfu-set.js index 17fcbf4..2bc3b6c 100644 --- a/lfu-set.js +++ b/lfu-set.js @@ -243,3 +243,4 @@ function FrequencyNode(frequency, prev, next) { next.prev = this; } } + From 455e7958c05cdbd011fbc16bdcfe586df4c8f9e2 Mon Sep 17 00:00:00 2001 From: Trevor Dixon Date: Thu, 15 May 2014 17:03:32 -0600 Subject: [PATCH 77/83] Update find methods to account for wrong assumption about splay splay does not guarantee that root.value <= value if value doesn't exist in the tree. Update comments and find methods to handle the case where root.value > value after splaying. --- sorted-set.js | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/sorted-set.js b/sorted-set.js index 2cad56d..8c490dd 100644 --- a/sorted-set.js +++ b/sorted-set.js @@ -195,25 +195,29 @@ SortedSet.prototype.findLeast = function (at) { SortedSet.prototype.findGreatestLessThanOrEqual = function (value) { if (this.root) { this.splay(value); - // assert root.value <= value - return this.root; + if (this.contentCompare(this.root.value, value) > 0) { + return this.root.getPrevious(); + } else { + return this.root; + } } }; SortedSet.prototype.findGreatestLessThan = function (value) { if (this.root) { this.splay(value); - // assert root.value <= value - return this.root.getPrevious(); + if (this.contentCompare(this.root.value, value) >= 0) { + return this.root.getPrevious(); + } else { + return this.root; + } } }; SortedSet.prototype.findLeastGreaterThanOrEqual = function (value) { if (this.root) { this.splay(value); - // assert root.value <= value - var comparison = this.contentCompare(value, this.root.value); - if (comparison === 0) { + if (this.contentCompare(this.root.value, value) >= 0) { return this.root; } else { return this.root.getNext(); @@ -224,9 +228,11 @@ SortedSet.prototype.findLeastGreaterThanOrEqual = function (value) { SortedSet.prototype.findLeastGreaterThan = function (value) { if (this.root) { this.splay(value); - // assert root.value <= value - var comparison = this.contentCompare(value, this.root.value); - return this.root.getNext(); + if (this.contentCompare(this.root.value, value) <= 0) { + return this.root.getNext(); + } else { + return this.root; + } } }; @@ -319,8 +325,9 @@ SortedSet.prototype.swap = function (start, length, plus) { }; // This is the simplified top-down splaying algorithm from: "Self-adjusting -// Binary Search Trees" by Sleator and Tarjan guarantees that the root.value <= -// value if root exists +// Binary Search Trees" by Sleator and Tarjan. Guarantees that root.value +// equals value if value exists. If value does not exist, then root will be +// the node whose value either immediately preceeds or immediately follows value. // - as described in https://github.com/hij1nx/forest SortedSet.prototype.splay = function (value) { var stub, left, right, temp, root, history; From cda098a45214e2721c303c86c1515bb3b8e6d1cd Mon Sep 17 00:00:00 2001 From: Trevor Dixon Date: Thu, 15 May 2014 21:57:20 -0600 Subject: [PATCH 78/83] Add tests for SortedSet find* methods Ported from v1 branch. `find` has been renamed `findValue. --- spec/sorted-set-spec.js | 135 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/spec/sorted-set-spec.js b/spec/sorted-set-spec.js index 6ec05bd..078fd72 100644 --- a/spec/sorted-set-spec.js +++ b/spec/sorted-set-spec.js @@ -197,6 +197,141 @@ describe("SortedSet", function () { } }); + describe("find methods", function () { + var set = new SortedSet([22, 23, 1, 34, 19, 5, 26, 12, 27, 30, 21, + 20, 6, 7, 2, 32, 10, 9, 33, 3, 11, 17, 28, 15]); + + describe("find", function() { + + it("should find the node for existing values", function() { + expect(set.findValue(1).value).toBe(1); + expect(set.findValue(5).value).toBe(5); + expect(set.findValue(9).value).toBe(9); + expect(set.findValue(30).value).toBe(30); + expect(set.findValue(34).value).toBe(34); + }); + + it("should return undefined for non-existent values", function() { + expect(set.findValue(4)).toBe(undefined); + expect(set.findValue(13)).toBe(undefined); + expect(set.findValue(31)).toBe(undefined); + }); + + }); + + describe("findGreatest", function () { + + it("should return the highest value in the set", function() { + expect(set.findGreatest().value).toBe(34); + }); + + }); + + describe("findLeast", function () { + + it("should return the lowest value in the set", function() { + expect(set.findLeast().value).toBe(1); + }); + + }); + + describe("findGreatestLessThanOrEqual", function () { + + it("should return values that exist in the set", function() { + expect(set.findGreatestLessThanOrEqual(5).value).toBe(5); + expect(set.findGreatestLessThanOrEqual(7).value).toBe(7); + expect(set.findGreatestLessThanOrEqual(9).value).toBe(9); + }); + + it("should return the next highest value", function() { + expect(set.findGreatestLessThanOrEqual(14).value).toBe(12); + expect(set.findGreatestLessThanOrEqual(24).value).toBe(23); + expect(set.findGreatestLessThanOrEqual(31).value).toBe(30); + expect(set.findGreatestLessThanOrEqual(4).value).toBe(3); + expect(set.findGreatestLessThanOrEqual(29).value).toBe(28); + expect(set.findGreatestLessThanOrEqual(25).value).toBe(23); + }); + + it("should return undefined for values out of range", function() { + expect(set.findGreatestLessThanOrEqual(0)).toBe(undefined); + }); + + }); + + describe("findGreatestLessThan", function () { + + it("should return next highest for values that exist in the set", function() { + expect(set.findGreatestLessThan(5).value).toBe(3); + expect(set.findGreatestLessThan(7).value).toBe(6); + expect(set.findGreatestLessThan(9).value).toBe(7); + expect(set.findGreatestLessThan(26).value).toBe(23); + }); + + it("should return the next highest value", function() { + expect(set.findGreatestLessThan(14).value).toBe(12); + expect(set.findGreatestLessThan(24).value).toBe(23); + expect(set.findGreatestLessThan(31).value).toBe(30); + expect(set.findGreatestLessThan(4).value).toBe(3); + expect(set.findGreatestLessThan(29).value).toBe(28); + expect(set.findGreatestLessThan(25).value).toBe(23); + }); + + + it("should return undefined for value at bottom of range", function() { + expect(set.findGreatestLessThan(1)).toBe(undefined); + }); + + }); + + describe("findLeastGreaterThanOrEqual", function () { + + it("should return values that exist in the set", function() { + expect(set.findLeastGreaterThanOrEqual(5).value).toBe(5); + expect(set.findLeastGreaterThanOrEqual(7).value).toBe(7); + expect(set.findLeastGreaterThanOrEqual(9).value).toBe(9); + }); + + it("should return the next value", function() { + expect(set.findLeastGreaterThanOrEqual(13).value).toBe(15); + expect(set.findLeastGreaterThanOrEqual(24).value).toBe(26); + expect(set.findLeastGreaterThanOrEqual(31).value).toBe(32); + expect(set.findLeastGreaterThanOrEqual(4).value).toBe(5); + expect(set.findLeastGreaterThanOrEqual(29).value).toBe(30); + expect(set.findLeastGreaterThanOrEqual(25).value).toBe(26); + }); + + it("should return undefined for values out of range", function() { + expect(set.findLeastGreaterThanOrEqual(36)).toBe(undefined); + }); + + }); + + describe("findLeastGreaterThan", function () { + + it("should return next value for values that exist in the set", function() { + expect(set.findLeastGreaterThan(5).value).toBe(6); + expect(set.findLeastGreaterThan(7).value).toBe(9); + expect(set.findLeastGreaterThan(9).value).toBe(10); + expect(set.findLeastGreaterThan(26).value).toBe(27); + }); + + it("should return the next value", function() { + expect(set.findLeastGreaterThan(14).value).toBe(15); + expect(set.findLeastGreaterThan(24).value).toBe(26); + expect(set.findLeastGreaterThan(31).value).toBe(32); + expect(set.findLeastGreaterThan(4).value).toBe(5); + expect(set.findLeastGreaterThan(29).value).toBe(30); + expect(set.findLeastGreaterThan(25).value).toBe(26); + }); + + it("should return undefined for value at top of range", function() { + expect(set.findLeastGreaterThan(34)).toBe(undefined); + }); + + }); + + }); + describe("observeRangeChange", function () { // fuzz cases for (var seed = 0; seed < 20; seed++) { From ac7dd38b760dd72be778a38cb11d0882781d421e Mon Sep 17 00:00:00 2001 From: Stuart Knightley Date: Mon, 9 Jun 2014 14:38:16 -0700 Subject: [PATCH 79/83] Add analytics --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f219386..ec5c8ec 100644 --- a/README.md +++ b/README.md @@ -1721,3 +1721,4 @@ More possible collections - array-set (a set, for fast lookup, backed by an array for meaningful range changes) +[![Analytics](https://ga-beacon.appspot.com/UA-51771141-2/collections/readme)](https://github.com/igrigorik/ga-beacon) From 5574d0ba0758dccfbf57a9a39e6b82ff31060db3 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Sat, 12 Jul 2014 22:46:45 -0700 Subject: [PATCH 80/83] Only patch Array clear if it doesn't exist Fixes #88 --- shim-array.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/shim-array.js b/shim-array.js index 0e77ca8..a834d3a 100644 --- a/shim-array.js +++ b/shim-array.js @@ -270,10 +270,12 @@ define("one", function () { } }); -define("clear", function () { - this.length = 0; - return this; -}); +if (!Array.prototype.clear) { + define("clear", function () { + this.length = 0; + return this; + }); +} define("compare", function (that, compare) { compare = compare || Object.compare; From 7c674d49c04955f01bbd2839f90936e15aceea2f Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Sun, 7 Sep 2014 22:45:56 -0400 Subject: [PATCH 81/83] Relax coupling to global shims. --- deque.js | 17 +-- dict.js | 8 +- fast-map.js | 14 ++- fast-set.js | 17 +-- generic-collection.js | 25 ++-- generic-map.js | 14 ++- generic-order.js | 7 +- heap.js | 17 +-- iterator.js | 2 +- lfu-map.js | 14 ++- lfu-set.js | 16 +-- list.js | 16 +-- lru-map.js | 14 ++- lru-set.js | 16 +-- map.js | 14 ++- observable-array.js | 6 +- observable-map.js | 1 - operators.js | 225 +++++++++++++++++++++++++++++++++ operators/add-each.js | 25 ++++ operators/clear.js | 23 ++++ operators/clone.js | 54 ++++++++ operators/compare.js | 57 +++++++++ operators/equals.js | 104 ++++++++++++++++ operators/escape.js | 15 +++ operators/hash.js | 19 +++ operators/noop.js | 5 + operators/swap.js | 102 +++++++++++++++ package.json | 2 +- set.js | 19 +-- shim-array.js | 152 +++++------------------ shim-object.js | 280 ++---------------------------------------- shim-regexp.js | 10 +- sorted-array-map.js | 14 ++- sorted-array-set.js | 6 +- sorted-array.js | 17 +-- sorted-map.js | 14 ++- sorted-set.js | 16 +-- 37 files changed, 843 insertions(+), 534 deletions(-) create mode 100644 operators.js create mode 100644 operators/add-each.js create mode 100644 operators/clear.js create mode 100644 operators/clone.js create mode 100644 operators/compare.js create mode 100644 operators/equals.js create mode 100644 operators/escape.js create mode 100644 operators/hash.js create mode 100644 operators/noop.js create mode 100644 operators/swap.js diff --git a/deque.js b/deque.js index 277fa1d..7c22d71 100644 --- a/deque.js +++ b/deque.js @@ -1,12 +1,13 @@ "use strict"; -require("./shim-object"); var GenericCollection = require("./generic-collection"); var GenericOrder = require("./generic-order"); var GenericOrder = require("./generic-order"); var ObservableRange = require("./observable-range"); var ObservableObject = require("./observable-object"); var Iterator = require("./iterator"); +var addEach = require("./operators/add-each"); +var equalsOperator = require("./operators/equals"); // by Petka Antonov // https://github.com/petkaantonov/deque/blob/master/js/deque.js @@ -27,10 +28,10 @@ function Deque(values, capacity) { this.addEach(values); } -Object.addEach(Deque.prototype, GenericCollection.prototype); -Object.addEach(Deque.prototype, GenericOrder.prototype); -Object.addEach(Deque.prototype, ObservableRange.prototype); -Object.addEach(Deque.prototype, ObservableObject.prototype); +addEach(Deque.prototype, GenericCollection.prototype); +addEach(Deque.prototype, GenericOrder.prototype); +addEach(Deque.prototype, ObservableRange.prototype); +addEach(Deque.prototype, ObservableObject.prototype); Deque.prototype.maxCapacity = (1 << 30) | 0; Deque.prototype.minCapacity = 16; @@ -350,7 +351,7 @@ Deque.prototype.lastIndexOf = function (value, index) { } Deque.prototype.findValue = function (value, equals, index) { - equals = equals || Object.equals; + equals = equals || equalsOperator; // Default start index at beginning if (index == null) { index = 0; @@ -371,7 +372,7 @@ Deque.prototype.findValue = function (value, equals, index) { }; Deque.prototype.findLastValue = function (value, equals, index) { - equals = equals || Object.equals; + equals = equals || equalsOperator; // Default start position at the end if (index == null) { index = this.length - 1; @@ -392,7 +393,7 @@ Deque.prototype.findLastValue = function (value, equals, index) { }; Deque.prototype.has = function (value, equals) { - equals = equals || Object.equals; + equals = equals || equalsOperator; // Left to right walk var mask = this.capacity - 1; for (var index = 0; index < this.length; index++) { diff --git a/dict.js b/dict.js index 6fa5148..f7a76c1 100644 --- a/dict.js +++ b/dict.js @@ -1,10 +1,10 @@ "use strict"; -var Shim = require("./shim"); var GenericCollection = require("./generic-collection"); var GenericMap = require("./generic-map"); var ObservableObject = require("./observable-object"); var Iterator = require("./iterator"); +var addEach = require("./operators/add-each"); // Burgled from https://github.com/domenic/dict @@ -30,9 +30,9 @@ function unmangle(mangled) { return mangled.slice(1); } -Object.addEach(Dict.prototype, GenericCollection.prototype); -Object.addEach(Dict.prototype, GenericMap.prototype); -Object.addEach(Dict.prototype, ObservableObject.prototype); +addEach(Dict.prototype, GenericCollection.prototype); +addEach(Dict.prototype, GenericMap.prototype); +addEach(Dict.prototype, ObservableObject.prototype); Dict.prototype.isDict = true; diff --git a/fast-map.js b/fast-map.js index 4d28aa8..efeb173 100644 --- a/fast-map.js +++ b/fast-map.js @@ -1,10 +1,12 @@ "use strict"; -var Shim = require("./shim"); var Set = require("./fast-set"); var GenericCollection = require("./generic-collection"); var GenericMap = require("./generic-map"); var ObservableObject = require("./observable-object"); +var equalsOperator = require("./operators/equals"); +var hashOperator = require("./operators/hash"); +var addEach = require("./operators/add-each"); module.exports = FastMap; @@ -12,8 +14,8 @@ function FastMap(values, equals, hash, getDefault) { if (!(this instanceof FastMap)) { return new FastMap(values, equals, hash, getDefault); } - equals = equals || Object.equals; - hash = hash || Object.hash; + equals = equals || equalsOperator; + hash = hash || hashOperator; getDefault = getDefault || this.getDefault; this.contentEquals = equals; this.contentHash = hash; @@ -33,9 +35,9 @@ function FastMap(values, equals, hash, getDefault) { FastMap.FastMap = FastMap; // hack so require("fast-map").FastMap will work in MontageJS -Object.addEach(FastMap.prototype, GenericCollection.prototype); -Object.addEach(FastMap.prototype, GenericMap.prototype); -Object.addEach(FastMap.prototype, ObservableObject.prototype); +addEach(FastMap.prototype, GenericCollection.prototype); +addEach(FastMap.prototype, GenericMap.prototype); +addEach(FastMap.prototype, ObservableObject.prototype); FastMap.prototype.constructClone = function (values) { return new this.constructor( diff --git a/fast-set.js b/fast-set.js index a03d4ae..99b5657 100644 --- a/fast-set.js +++ b/fast-set.js @@ -1,12 +1,15 @@ "use strict"; -var Shim = require("./shim"); var Dict = require("./dict"); var List = require("./list"); var GenericCollection = require("./generic-collection"); var GenericSet = require("./generic-set"); var TreeLog = require("./tree-log"); var ObservableObject = require("./observable-object"); +var noop = require("./operators/noop"); +var hashOperator = require("./operators/hash"); +var equalsOperator = require("./operators/equals"); +var addEach = require("./operators/add-each"); var object_has = Object.prototype.hasOwnProperty; @@ -16,9 +19,9 @@ function FastSet(values, equals, hash, getDefault) { if (!(this instanceof FastSet)) { return new FastSet(values, equals, hash, getDefault); } - equals = equals || Object.equals; - hash = hash || Object.hash; - getDefault = getDefault || Function.noop; + equals = equals || equalsOperator; + hash = hash || hashOperator; + getDefault = getDefault || noop; this.contentEquals = equals; this.contentHash = hash; this.getDefault = getDefault; @@ -29,9 +32,9 @@ function FastSet(values, equals, hash, getDefault) { FastSet.FastSet = FastSet; // hack so require("fast-set").FastSet will work in MontageJS -Object.addEach(FastSet.prototype, GenericCollection.prototype); -Object.addEach(FastSet.prototype, GenericSet.prototype); -Object.addEach(FastSet.prototype, ObservableObject.prototype); +addEach(FastSet.prototype, GenericCollection.prototype); +addEach(FastSet.prototype, GenericSet.prototype); +addEach(FastSet.prototype, ObservableObject.prototype); FastSet.prototype.Buckets = Dict; FastSet.prototype.Bucket = List; diff --git a/generic-collection.js b/generic-collection.js index 52cbd77..2dc2db9 100644 --- a/generic-collection.js +++ b/generic-collection.js @@ -1,5 +1,9 @@ "use strict"; +var equalsOperator = require("./operators/equals"); +var compareOperator = require("./operators/compare"); +var cloneOperator = require("./operators/clone"); + module.exports = GenericCollection; function GenericCollection() { throw new Error("Can't construct. GenericCollection is a mixin."); @@ -64,7 +68,7 @@ GenericCollection.prototype.enumerate = function (start) { }; GenericCollection.prototype.group = function (callback, thisp, equals) { - equals = equals || Object.equals; + equals = equals || equalsOperator; var groups = []; var keys = []; this.forEach(function (value, key, object) { @@ -137,7 +141,7 @@ GenericCollection.prototype.some = function (callback /*, thisp*/) { }; GenericCollection.prototype.min = function (compare) { - compare = compare || this.contentCompare || Object.compare; + compare = compare || this.contentCompare || compareOperator; var first = true; return this.reduce(function (result, value) { if (first) { @@ -150,7 +154,7 @@ GenericCollection.prototype.min = function (compare) { }; GenericCollection.prototype.max = function (compare) { - compare = compare || this.contentCompare || Object.compare; + compare = compare || this.contentCompare || compareOperator; var first = true; return this.reduce(function (result, value) { if (first) { @@ -210,11 +214,11 @@ GenericCollection.prototype.join = function (delimiter) { }; GenericCollection.prototype.sorted = function (compare, by, order) { - compare = compare || this.contentCompare || Object.compare; + compare = compare || this.contentCompare || compareOperator; // account for comparators generated by Function.by if (compare.by) { by = compare.by; - compare = compare.compare || this.contentCompare || Object.compare; + compare = compare.compare || this.contentCompare || compareOperator; } else { by = by || Function.identity; } @@ -238,17 +242,18 @@ GenericCollection.prototype.reversed = function () { return this.constructClone(this).reverse(); }; -GenericCollection.prototype.clone = function (depth, memo) { +GenericCollection.prototype.clone = function (depth, memo, clone) { if (depth === undefined) { depth = Infinity; } else if (depth === 0) { return this; } - var clone = this.constructClone(); + clone = clone || cloneOperator; + var collection = this.constructClone(); this.forEach(function (value, key) { - clone.add(Object.clone(value, depth - 1, memo), key); + collection.add(clone(value, depth - 1, memo), key); }, this); - return clone; + return collection; }; GenericCollection.prototype.only = function () { @@ -257,5 +262,3 @@ GenericCollection.prototype.only = function () { } }; -require("./shim-array"); - diff --git a/generic-map.js b/generic-map.js index 155b0b6..28fd873 100644 --- a/generic-map.js +++ b/generic-map.js @@ -1,17 +1,19 @@ "use strict"; -var Object = require("./shim-object"); var ObservableMap = require("./observable-map"); var ObservableObject = require("./observable-object"); var Iterator = require("./iterator"); +var equalsOperator = require("./operators/equals"); +var compareOperator = require("./operators/compare"); +var addEach = require("./operators/add-each"); module.exports = GenericMap; function GenericMap() { throw new Error("Can't construct. GenericMap is a mixin."); } -Object.addEach(GenericMap.prototype, ObservableMap.prototype); -Object.addEach(GenericMap.prototype, ObservableObject.prototype); +addEach(GenericMap.prototype, ObservableMap.prototype); +addEach(GenericMap.prototype, ObservableObject.prototype); // all of these methods depend on the constructor providing a `store` set @@ -160,7 +162,7 @@ GenericMap.prototype.entries = function () { }; GenericMap.prototype.equals = function (that, equals) { - equals = equals || Object.equals; + equals = equals || equalsOperator; if (this === that) { return true; } else if (that && typeof that.every === "function") { @@ -184,11 +186,11 @@ function Item(key, value) { } Item.prototype.equals = function (that) { - return Object.equals(this.key, that.key) && Object.equals(this.value, that.value); + return equalsOperator(this.key, that.key) && equalsOperator(this.value, that.value); }; Item.prototype.compare = function (that) { - return Object.compare(this.key, that.key); + return compareOperator(this.key, that.key); }; function GenericMapIterator(map) { diff --git a/generic-order.js b/generic-order.js index a5f30a0..3bc6399 100644 --- a/generic-order.js +++ b/generic-order.js @@ -1,5 +1,6 @@ -var Object = require("./shim-object"); +var equalsOperator = require("./operators/equals"); +var compareOperator = require("./operators/compare"); module.exports = GenericOrder; function GenericOrder() { @@ -7,7 +8,7 @@ function GenericOrder() { } GenericOrder.prototype.equals = function (that, equals) { - equals = equals || this.contentEquals || Object.equals; + equals = equals || this.contentEquals || equalsOperator; if (this === that) { return true; @@ -26,7 +27,7 @@ GenericOrder.prototype.equals = function (that, equals) { }; GenericOrder.prototype.compare = function (that, compare) { - compare = compare || this.contentCompare || Object.compare; + compare = compare || this.contentCompare || compareOperator; if (this === that) { return 0; diff --git a/heap.js b/heap.js index 94aecda..52b86a1 100644 --- a/heap.js +++ b/heap.js @@ -3,11 +3,13 @@ // http://eloquentjavascript.net/appendix2.html require("./observable-array"); -require("./shim"); var GenericCollection = require("./generic-collection"); var ObservableObject = require("./observable-object"); var ObservableRange = require("./observable-range"); var ObservableMap = require("./observable-map"); +var equalsOperator = require("./operators/equals"); +var compareOperator = require("./operators/compare"); +var addEach = require("./operators/add-each"); // Max Heap by default. Comparison can be reversed to produce a Min Heap. @@ -17,8 +19,8 @@ function Heap(values, equals, compare) { if (!(this instanceof Heap)) { return new Heap(values, equals, compare); } - this.contentEquals = equals || Object.equals; - this.contentCompare = compare || Object.compare; + this.contentEquals = equals || equalsOperator; + this.contentCompare = compare || compareOperator; this.content = []; this.length = 0; this.addEach(values); @@ -26,10 +28,10 @@ function Heap(values, equals, compare) { Heap.Heap = Heap; // hack so require("heap").Heap will work in MontageJS -Object.addEach(Heap.prototype, GenericCollection.prototype); -Object.addEach(Heap.prototype, ObservableObject.prototype); -Object.addEach(Heap.prototype, ObservableRange.prototype); -Object.addEach(Heap.prototype, ObservableMap.prototype); +addEach(Heap.prototype, GenericCollection.prototype); +addEach(Heap.prototype, ObservableObject.prototype); +addEach(Heap.prototype, ObservableRange.prototype); +addEach(Heap.prototype, ObservableMap.prototype); Heap.prototype.constructClone = function (values) { return new this.constructor( @@ -235,3 +237,4 @@ Heap.prototype.handleContentMapChange = function (plus, minus, key, type) { Heap.prototype.handleContentMapWillChange = function (plus, minus, key, type) { this.dispatchMapWillChange(type, key, plus, minus); }; + diff --git a/iterator.js b/iterator.js index 59e20a0..0c7bfd8 100644 --- a/iterator.js +++ b/iterator.js @@ -2,7 +2,7 @@ module.exports = Iterator; -var WeakMap = require("./weak-map"); +var WeakMap = require("weak-map"); var GenericCollection = require("./generic-collection"); // upgrades an iterable to a Iterator diff --git a/lfu-map.js b/lfu-map.js index cbe4794..8df70c4 100644 --- a/lfu-map.js +++ b/lfu-map.js @@ -1,10 +1,12 @@ "use strict"; -var Shim = require("./shim"); var LfuSet = require("./lfu-set"); var GenericCollection = require("./generic-collection"); var GenericMap = require("./generic-map"); var ObservableObject = require("./observable-object"); +var equalsOperator = require("./operators/equals"); +var hashOperator = require("./operators/hash"); +var addEach = require("./operators/add-each"); module.exports = LfuMap; @@ -12,8 +14,8 @@ function LfuMap(values, maxLength, equals, hash, getDefault) { if (!(this instanceof LfuMap)) { return new LfuMap(values, maxLength, equals, hash, getDefault); } - equals = equals || Object.equals; - hash = hash || Object.hash; + equals = equals || equalsOperator; + hash = hash || hashOperator; getDefault = getDefault || this.getDefault; this.contentEquals = equals; this.contentHash = hash; @@ -34,9 +36,9 @@ function LfuMap(values, maxLength, equals, hash, getDefault) { LfuMap.LfuMap = LfuMap; // hack so require("lfu-map").LfuMap will work in MontageJS -Object.addEach(LfuMap.prototype, GenericCollection.prototype); -Object.addEach(LfuMap.prototype, GenericMap.prototype); -Object.addEach(LfuMap.prototype, ObservableObject.prototype); +addEach(LfuMap.prototype, GenericCollection.prototype); +addEach(LfuMap.prototype, GenericMap.prototype); +addEach(LfuMap.prototype, ObservableObject.prototype); LfuMap.prototype.constructClone = function (values) { return new this.constructor( diff --git a/lfu-set.js b/lfu-set.js index 2bc3b6c..9a10e80 100644 --- a/lfu-set.js +++ b/lfu-set.js @@ -2,12 +2,14 @@ // Based on http://dhruvbird.com/lfu.pdf -var Shim = require("./shim"); var Set = require("./set"); var GenericCollection = require("./generic-collection"); var GenericSet = require("./generic-set"); var ObservableRange = require("./observable-range"); var ObservableObject = require("./observable-object"); +var equalsOperator = require("./operators/equals"); +var hashOperator = require("./operators/hash"); +var addEach = require("./operators/add-each"); module.exports = LfuSet; @@ -16,8 +18,8 @@ function LfuSet(values, capacity, equals, hash, getDefault) { return new LfuSet(values, capacity, equals, hash, getDefault); } capacity = capacity || Infinity; - equals = equals || Object.equals; - hash = hash || Object.hash; + equals = equals || equalsOperator; + hash = hash || hashOperator; getDefault = getDefault || Function.noop; // TODO @@ -42,10 +44,10 @@ function LfuSet(values, capacity, equals, hash, getDefault) { LfuSet.LfuSet = LfuSet; // hack so require("lfu-set").LfuSet will work in MontageJS -Object.addEach(LfuSet.prototype, GenericCollection.prototype); -Object.addEach(LfuSet.prototype, GenericSet.prototype); -Object.addEach(LfuSet.prototype, ObservableRange.prototype); -Object.addEach(LfuSet.prototype, ObservableObject.prototype); +addEach(LfuSet.prototype, GenericCollection.prototype); +addEach(LfuSet.prototype, GenericSet.prototype); +addEach(LfuSet.prototype, ObservableRange.prototype); +addEach(LfuSet.prototype, ObservableObject.prototype); LfuSet.prototype.constructClone = function (values) { return new this.constructor( diff --git a/list.js b/list.js index 18441c9..78d0268 100644 --- a/list.js +++ b/list.js @@ -2,12 +2,14 @@ module.exports = List; -var Shim = require("./shim"); var GenericCollection = require("./generic-collection"); var GenericOrder = require("./generic-order"); var ObservableObject = require("./observable-object"); var ObservableRange = require("./observable-range"); var Iterator = require("./iterator"); +var equalsOperator = require("./operators/equals"); +var noop = require("./operators/noop"); +var addEach = require("./operators/add-each"); function List(values, equals, getDefault) { if (!(this instanceof List)) { @@ -16,18 +18,18 @@ function List(values, equals, getDefault) { var head = this.head = new this.Node(); head.next = head; head.prev = head; - this.contentEquals = equals || Object.equals; - this.getDefault = getDefault || Function.noop; + this.contentEquals = equals || equalsOperator; + this.getDefault = getDefault || noop; this.length = 0; this.addEach(values); } List.List = List; // hack so require("list").List will work in MontageJS -Object.addEach(List.prototype, GenericCollection.prototype); -Object.addEach(List.prototype, GenericOrder.prototype); -Object.addEach(List.prototype, ObservableObject.prototype); -Object.addEach(List.prototype, ObservableRange.prototype); +addEach(List.prototype, GenericCollection.prototype); +addEach(List.prototype, GenericOrder.prototype); +addEach(List.prototype, ObservableObject.prototype); +addEach(List.prototype, ObservableRange.prototype); List.prototype.constructClone = function (values) { return new this.constructor(values, this.contentEquals, this.getDefault); diff --git a/lru-map.js b/lru-map.js index b7641f1..315682c 100644 --- a/lru-map.js +++ b/lru-map.js @@ -1,10 +1,12 @@ "use strict"; -var Shim = require("./shim"); var LruSet = require("./lru-set"); var GenericCollection = require("./generic-collection"); var GenericMap = require("./generic-map"); var ObservableObject = require("./observable-object"); +var equalsOperator = require("./operators/equals"); +var hashOperator = require("./operators/hash"); +var addEach = require("./operators/add-each"); module.exports = LruMap; @@ -12,8 +14,8 @@ function LruMap(values, capacity, equals, hash, getDefault) { if (!(this instanceof LruMap)) { return new LruMap(values, capacity, equals, hash, getDefault); } - equals = equals || Object.equals; - hash = hash || Object.hash; + equals = equals || equalsOperator; + hash = hash || hashOperator; getDefault = getDefault || this.getDefault; this.capacity = capacity || Infinity; this.contentEquals = equals; @@ -35,9 +37,9 @@ function LruMap(values, capacity, equals, hash, getDefault) { LruMap.LruMap = LruMap; // hack so require("lru-map").LruMap will work in MontageJS -Object.addEach(LruMap.prototype, GenericCollection.prototype); -Object.addEach(LruMap.prototype, GenericMap.prototype); -Object.addEach(LruMap.prototype, ObservableObject.prototype); +addEach(LruMap.prototype, GenericCollection.prototype); +addEach(LruMap.prototype, GenericMap.prototype); +addEach(LruMap.prototype, ObservableObject.prototype); LruMap.prototype.constructClone = function (values) { return new this.constructor( diff --git a/lru-set.js b/lru-set.js index 3a030f3..9dbf263 100644 --- a/lru-set.js +++ b/lru-set.js @@ -1,11 +1,13 @@ "use strict"; -var Shim = require("./shim"); var Set = require("./set"); var GenericCollection = require("./generic-collection"); var GenericSet = require("./generic-set"); var ObservableObject = require("./observable-object"); var ObservableRange = require("./observable-range"); +var equalsOperator = require("./operators/equals"); +var hashOperator = require("./operators/hash"); +var addEach = require("./operators/add-each"); module.exports = LruSet; @@ -14,8 +16,8 @@ function LruSet(values, maxLength, equals, hash, getDefault) { return new LruSet(values, maxLength, equals, hash, getDefault); } maxLength = maxLength || Infinity; - equals = equals || Object.equals; - hash = hash || Object.hash; + equals = equals || equalsOperator; + hash = hash || hashOperator; getDefault = getDefault || Function.noop; this.store = new Set(undefined, equals, hash); this.contentEquals = equals; @@ -28,10 +30,10 @@ function LruSet(values, maxLength, equals, hash, getDefault) { LruSet.LruSet = LruSet; // hack so require("lru-set").LruSet will work in MontageJS -Object.addEach(LruSet.prototype, GenericCollection.prototype); -Object.addEach(LruSet.prototype, GenericSet.prototype); -Object.addEach(LruSet.prototype, ObservableObject.prototype); -Object.addEach(LruSet.prototype, ObservableRange.prototype); +addEach(LruSet.prototype, GenericCollection.prototype); +addEach(LruSet.prototype, GenericSet.prototype); +addEach(LruSet.prototype, ObservableObject.prototype); +addEach(LruSet.prototype, ObservableRange.prototype); LruSet.prototype.constructClone = function (values) { return new this.constructor( diff --git a/map.js b/map.js index 1b89980..81318b8 100644 --- a/map.js +++ b/map.js @@ -1,10 +1,12 @@ "use strict"; -var Shim = require("./shim"); var Set = require("./set"); var GenericCollection = require("./generic-collection"); var GenericMap = require("./generic-map"); var ObservableObject = require("./observable-object"); +var equalsOperator = require("./operators/equals"); +var hashOperator = require("./operators/hash"); +var addEach = require("./operators/add-each"); module.exports = Map; @@ -12,8 +14,8 @@ function Map(values, equals, hash, getDefault) { if (!(this instanceof Map)) { return new Map(values, equals, hash, getDefault); } - equals = equals || Object.equals; - hash = hash || Object.hash; + equals = equals || equalsOperator; + hash = hash || hashOperator; getDefault = getDefault || this.getDefault; this.contentEquals = equals; this.contentHash = hash; @@ -33,9 +35,9 @@ function Map(values, equals, hash, getDefault) { Map.Map = Map; // hack so require("map").Map will work in MontageJS -Object.addEach(Map.prototype, GenericCollection.prototype); -Object.addEach(Map.prototype, GenericMap.prototype); // overrides GenericCollection -Object.addEach(Map.prototype, ObservableObject.prototype); +addEach(Map.prototype, GenericCollection.prototype); +addEach(Map.prototype, GenericMap.prototype); // overrides GenericCollection +addEach(Map.prototype, ObservableObject.prototype); Map.prototype.constructClone = function (values) { return new this.constructor( diff --git a/observable-array.js b/observable-array.js index 87e04ca..87b5d8e 100644 --- a/observable-array.js +++ b/observable-array.js @@ -13,7 +13,7 @@ * necessary for any collection with observable content. */ -require("./shim"); +require("./shim-array"); var WeakMap = require("weak-map"); var observedLengthForObject = new WeakMap(); @@ -22,7 +22,7 @@ var ObservableObject = require("./observable-object"); var ObservableRange = require("./observable-range"); var ObservableMap = require("./observable-map"); -var array_swap = Array.prototype.swap; +var array_swap = require("./operators/swap"); var array_splice = Array.prototype.splice; var array_slice = Array.prototype.slice; var array_reverse = Array.prototype.reverse; @@ -173,7 +173,7 @@ var observableArrayProperties = { } // actual work - array_swap.call(this, start, minusLength, plus); + array_swap(this, start, minusLength, plus); // dispatch after change events if (diff === 0) { // substring replacement diff --git a/observable-map.js b/observable-map.js index da60060..64527c0 100644 --- a/observable-map.js +++ b/observable-map.js @@ -1,7 +1,6 @@ /*global -WeakMap*/ "use strict"; -require("./shim-array"); var WeakMap = require("weak-map"); var changeObserversByObject = new WeakMap(); diff --git a/operators.js b/operators.js new file mode 100644 index 0000000..5900174 --- /dev/null +++ b/operators.js @@ -0,0 +1,225 @@ + +var Map = require("./map"); +var Set = require("./set"); +var equals = require("./operators/equals"); +var compare = require("./operators/compare"); +var escape = require("./operators/escape"); + +// from highest to lowest precedence + +exports.toNumber = toNumber; +function toNumber(value) { + if (typeof value === "string" || typeof value === "number") { + return +value; + } +} + +exports.toString = toString; +function toString(value) { + if (typeof value === "string" || typeof value === "number") { + return "" + value; + } +} + +exports.toArray = Array.from; + +exports.toMap = Map; + +exports.toSet = Set; + +exports.not = not; +function not(value) { + return !value; +}; + +exports.neg = neg; +function neg(n) { + return -n; +} + +exports.pow = pow; +function pow(a, b) { + return Math.pow(a, b); +} + +exports.root = root; +function root(a, b) { + return Math.pow(a, 1 / b); +} + +exports.log = log; +function log(a, b) { + return Math.log(a) / Math.log(b); +} + +exports.mul = mul; +function mul(a, b) { + return a * b; +} + +exports.div = div; +function div(a, b) { + return a / b; +} + +exports.mod = mod; +function mod(a, b) { + return ((a % b) + b) % b; +} + +exports.rem = rem; +function rem(a, b) { + return a % b; +} + +exports.add = add; +function add(a, b) { + return a + b; +} + +exports.sub = sub; +function sub(a, b) { + return a - b; +} + +exports.ceil = ceil; +function ceil(n) { + if (typeof n === "number") { + return Math.ceil(n); + } +} + +exports.floor = floor; +function floor(n) { + if (typeof n === "number") { + return Math.floor(n); + } +} + +exports.round = round; +function round(n) { + if (typeof n === "number") { + return Math.round(n); + } +} + +exports.lessThan = lessThan; +function lessThan(a, b) { + return compare(a, b) < 0; +} + +exports.greaterThan = greaterThan; +function greaterThan(a, b) { + return compare(a, b) > 0; +} + +exports.lessThanOrEqual = lessThanOrEqual; +function lessThanOrEqual(a, b) { + return compare(a, b) <= 0; +} + +exports.greaterThanOrEqual = greaterThanOrEqual; +function greaterThanOrEqual(a, b) { + return compare(a, b) >= 0; +} + +/** + Returns whether two values are identical. Any value is identical to itself + and only itself. This is much more restictive than equivalence and subtly + different than strict equality, === because of edge cases + including negative zero and NaN. Identity is useful for + resolving collisions among keys in a mapping where the domain is any value. + This method does not delgate to any method on an object and cannot be + overridden. + @see http://wiki.ecmascript.org/doku.php?id=harmony:egal + @param {Any} this + @param {Any} that + @returns {Boolean} whether this and that are identical +*/ +exports.is = is; +function is(a, b) { + if (a === b) { + // 0 === -0, but they are not identical + return a !== 0 || 1 / a === 1 / b; + } + // NaN !== NaN, but they are identical. + // NaNs are the only non-reflexive value, i.e., if a !== a, + // then a is a NaN. + // isNaN is broken: it converts its argument to number, so + // isNaN("foo") => true + return a !== a && b !== b; +} + +exports.equals = equals; + +exports.compare = compare; + +exports.and = and; +function and(a, b) { + return a && b; +} + +exports.or = or; +function or(a, b) { + return a || b; +} + +exports.defined = defined; +function defined(value) { + return value != null; +} + +// "startsWith", "endsWith", and "contains" are overridden in +// complile-observer so they can precompile the regular expression and reuse it +// in each reaction. + +exports.startsWith = startsWith; +function startsWith(a, b) { + var expression = new RegExp("^" + escape(b)); + return expression.test(a); +} + +exports.endsWith = endsWith; +function endsWith(a, b) { + var expression = new RegExp(escape(b) + "$"); + return expression.test(a); +} + +exports.contains = contains; +function contains(a, b) { + var expression = new RegExp(escape(b)); + return expression.test(a); +} + +exports.join = join; +function join(a, b) { + return a.join(b || ""); +} + +exports.split = split; +function split(a, b) { + return a.split(b || ""); +} + +exports.range = range; +function range(stop) { + var range = []; + for (var start = 0; start < stop; start++) { + range.push(start); + } + return range; +} + +exports.clone = require("./operators/clone"); + +function isObject(object) { + return Object(object) === object; +} + +function valueOf(value) { + if (value && typeof value.valueOf === "function") { + value = value.valueOf(); + } + return value; +} + diff --git a/operators/add-each.js b/operators/add-each.js new file mode 100644 index 0000000..ef94663 --- /dev/null +++ b/operators/add-each.js @@ -0,0 +1,25 @@ + +module.exports = addEach; +function addEach(target, source) { + if (!source) { + } else if (typeof source.forEach === "function" && !source.hasOwnProperty("forEach")) { + // copy map-alikes + if (typeof source.keys === "function") { + source.forEach(function (value, key) { + target[key] = value; + }); + // iterate key value pairs of other iterables + } else { + source.forEach(function (pair) { + target[pair[0]] = pair[1]; + }); + } + } else { + // copy other objects as map-alikes + Object.keys(source).forEach(function (key) { + target[key] = source[key]; + }); + } + return target; +} + diff --git a/operators/clear.js b/operators/clear.js new file mode 100644 index 0000000..69ea583 --- /dev/null +++ b/operators/clear.js @@ -0,0 +1,23 @@ + +/** + Removes all properties owned by this object making the object suitable for + reuse. + + @function external:Object.clear + @returns this +*/ +module.exports = clear; +function clear(object) { + if (object && typeof object.clear === "function") { + object.clear(); + } else { + var keys = Object.keys(object), + i = keys.length; + while (i) { + i--; + delete object[keys[i]]; + } + } + return object; +} + diff --git a/operators/clone.js b/operators/clone.js new file mode 100644 index 0000000..bfb7332 --- /dev/null +++ b/operators/clone.js @@ -0,0 +1,54 @@ + +var WeakMap = require("weak-map"); + +/** + * Creates a deep copy of any value. Values, being immutable, are returned + * without alternation. Forwards to clone on objects and arrays. + * + * @function external:Object.clone + * @param {Any} value a value to clone + * @param {Number} depth an optional traversal depth, defaults to infinity. A + * value of 0 means to make no clone and return the value + * directly. + * @param {Map} memo an optional memo of already visited objects to preserve + * reference cycles. The cloned object will have the exact same shape as the + * original, but no identical objects. Te map may be later used to associate + * all objects in the original object graph with their corresponding member of + * the cloned graph. + * @returns a copy of the value + */ +module.exports = clone; +function clone(value, depth, memo) { + if (value && value.valueOf) { + value = value.valueOf(); + } + memo = memo || new WeakMap(); + if (depth === undefined) { + depth = Infinity; + } else if (depth === 0) { + return value; + } + if (typeof value === "function") { + return value; + } else if (value && typeof value === "object") { + if (!memo.has(value)) { + if (value && typeof value.clone === "function") { + memo.set(value, value.clone(depth, memo)); + } else { + var prototype = Object.getPrototypeOf(value); + if (prototype === null || prototype === Object.prototype) { + var clone = Object.create(prototype); + memo.set(value, clone); + for (var key in value) { + clone[key] = module.exports(value[key], depth - 1, memo); + } + } else { + throw new Error("Can't clone " + value); + } + } + } + return memo.get(value); + } + return value; +} + diff --git a/operators/compare.js b/operators/compare.js new file mode 100644 index 0000000..30d161d --- /dev/null +++ b/operators/compare.js @@ -0,0 +1,57 @@ + +/** + Determines the order in which any two objects should be sorted by returning + a number that has an analogous relationship to zero as the left value to + the right. That is, if the left is "less than" the right, the returned + value will be "less than" zero, where "less than" may be any other + transitive relationship. + +

Arrays are compared by the first diverging values, or by length. + +

Any two values that are incomparable return zero. As such, + equals should not be implemented with compare + since incomparability is indistinguishable from equality. + +

Sorts strings lexicographically. This is not suitable for any + particular international setting. Different locales sort their phone books + in very different ways, particularly regarding diacritics and ligatures. + +

If the given object is an instance of a type that implements a method + named "compare", this function defers to the instance. The method does not + need to be an owned property to distinguish it from an object literal since + object literals are incomparable. Unlike Object however, + Array implements compare. + + @param {Any} left + @param {Any} right + @returns {Number} a value having the same transitive relationship to zero + as the left and right values. +*/ +module.exports = compare; +function compare(a, b, compare) { + // unbox objects + // mercifully handles the Date case + if (a && typeof a.valueOf === "function") { + a = a.valueOf(); + } + if (b && typeof b.valueOf === "function") { + b = b.valueOf(); + } + if (a === b) + return 0; + var aType = typeof a; + var bType = typeof b; + if (aType === "number" && bType === "number") + return a - b; + if (aType === "string" && bType === "string") + return a < b ? -Infinity : Infinity; + // the possibility of equality elimiated above + compare = compare || module.exports; + if (a && typeof a.compare === "function") + return a.compare(b, compare); + // not commutative, the relationship is reversed + if (b && typeof b.compare === "function") + return -b.compare(a, compare); + return 0; +} + diff --git a/operators/equals.js b/operators/equals.js new file mode 100644 index 0000000..0e26f41 --- /dev/null +++ b/operators/equals.js @@ -0,0 +1,104 @@ +"use strict"; + +/** + Performs a polymorphic, type-sensitive deep equivalence comparison of any + two values. + +

As a basic principle, any value is equivalent to itself (as in + identity), any boxed version of itself (as a new Number(10) is + to 10), and any deep clone of itself. + +

Equivalence has the following properties: + +

    +
  • polymorphic: + If the given object is an instance of a type that implements a + methods named "equals", this function defers to the method. So, + this function can safely compare any values regardless of type, + including undefined, null, numbers, strings, any pair of objects + where either implements "equals", or object literals that may even + contain an "equals" key. +
  • type-sensitive: + Incomparable types are not equal. No object is equivalent to any + array. No string is equal to any other number. +
  • deep: + Collections with equivalent content are equivalent, recursively. +
  • equivalence: + Identical values and objects are equivalent, but so are collections + that contain equivalent content. Whether order is important varies + by type. For Arrays and lists, order is important. For Objects, + maps, and sets, order is not important. Boxed objects are mutally + equivalent with their unboxed values, by virtue of the standard + valueOf method. +
+ @param this + @param that + @returns {Boolean} whether the values are deeply equivalent +*/ +module.exports = equals; +function equals(a, b, equals, memo) { + equals = equals || module.exports; + // unbox objects + if (a && typeof a.valueOf === "function") { + a = a.valueOf(); + } + if (b && typeof b.valueOf === "function") { + b = b.valueOf(); + } + if (a === b) + return true; + if (isObject(a)) { + var Map = require("../map"); + memo = memo || new Map(); + if (memo.has(a)) { + return true; + } + memo.set(a, true); + } + if (isObject(a) && typeof a.equals === "function") { + return a.equals(b, equals, memo); + } + // commutative + if (isObject(b) && typeof b.equals === "function") { + return b.equals(a, equals, memo); + } + if (isObject(a) && isObject(b)) { + if (Object.getPrototypeOf(a) === Object.prototype && Object.getPrototypeOf(b) === Object.prototype) { + for (var name in a) { + if (!equals(a[name], b[name], equals, memo)) { + return false; + } + } + for (var name in b) { + if (!equals(b[name], a[name], equals, memo)) { + return false; + } + } + return true; + } + } + // NaN !== NaN, but they are equal. + // NaNs are the only non-reflexive value, i.e., if x !== x, + // then x is a NaN. + // isNaN is broken: it converts its argument to number, so + // isNaN("foo") => true + // We have established that a !== b, but if a !== a && b !== b, they are + // both NaN. + if (a !== a && b !== b) + return true; + if (!a || !b) + return a === b; + return false; +} + +// Because a return value of 0 from a `compare` function may mean either +// "equals" or "is incomparable", `equals` cannot be defined in terms of +// `compare`. However, `compare` *can* be defined in terms of `equals` and +// `lessThan`. Again however, more often it would be desirable to implement +// all of the comparison functions in terms of compare rather than the other +// way around. + +function isObject(object) { + return Object(object) === object; +} + diff --git a/operators/escape.js b/operators/escape.js new file mode 100644 index 0000000..6642c4f --- /dev/null +++ b/operators/escape.js @@ -0,0 +1,15 @@ + +var special = /[-[\]{}()*+?.\\^$|,#\s]/g; + +module.exports = escape; + +/** + * accepts a string; returns the string with regex metacharacters escaped. the + * returned string can safely be used within a regex to match a literal string. + * escaped characters are [, ], {, }, (, ), -, *, +, ?, ., \, ^, $, |, #, + * [comma], and whitespace. + */ +function escape(string) { + return string.replace(special, "\\$&"); +} + diff --git a/operators/hash.js b/operators/hash.js new file mode 100644 index 0000000..449a25c --- /dev/null +++ b/operators/hash.js @@ -0,0 +1,19 @@ + +var WeakMap = require("weak-map"); + +var hashMap = new WeakMap(); + +module.exports = hash; +function hash(object) { + if (object && typeof object.hash === "function") { + return "" + object.hash(); + } else if (Object.isObject(object)) { + if (!hashMap.has(object)) { + hashMap.set(object, Math.random().toString(36).slice(2)); + } + return hashMap.get(object); + } else { + return "" + object; + } +} + diff --git a/operators/noop.js b/operators/noop.js new file mode 100644 index 0000000..33d1cb8 --- /dev/null +++ b/operators/noop.js @@ -0,0 +1,5 @@ + +module.exports = noop; +function noop() { +} + diff --git a/operators/swap.js b/operators/swap.js new file mode 100644 index 0000000..bdd3128 --- /dev/null +++ b/operators/swap.js @@ -0,0 +1,102 @@ +"use strict"; + +var array_slice = Array.prototype.slice; + +module.exports = swap; +function swap(array, start, minusLength, plus) { + // Unrolled implementation into JavaScript for a couple reasons. + // Calling splice can cause large stack sizes for large swaps. Also, + // splice cannot handle array holes. + if (plus) { + if (!Array.isArray(plus)) { + plus = array_slice.call(plus); + } + } else { + plus = Array.empty; + } + + if (start < 0) { + start = array.length + start; + } else if (start > array.length) { + array.length = start; + } + + if (start + minusLength > array.length) { + // Truncate minus length if it extends beyond the length + minusLength = array.length - start; + } else if (minusLength < 0) { + // It is the JavaScript way. + minusLength = 0; + } + + var diff = plus.length - minusLength; + var oldLength = array.length; + var newLength = array.length + diff; + + if (diff > 0) { + // Head Tail Plus Minus + // H H H H M M T T T T + // H H H H P P P P T T T T + // ^ start + // ^-^ minus.length + // ^ --> diff + // ^-----^ plus.length + // ^------^ tail before + // ^------^ tail after + // ^ start iteration + // ^ start iteration offset + // ^ end iteration + // ^ end iteration offset + // ^ start + minus.length + // ^ length + // ^ length - 1 + for (var index = oldLength - 1; index >= start + minusLength; index--) { + var offset = index + diff; + if (index in array) { + array[offset] = array[index]; + } else { + // Oddly, PhantomJS complains about deleting array + // properties, unless you assign undefined first. + array[offset] = void 0; + delete array[offset]; + } + } + } + for (var index = 0; index < plus.length; index++) { + if (index in plus) { + array[start + index] = plus[index]; + } else { + array[start + index] = void 0; + delete array[start + index]; + } + } + if (diff < 0) { + // Head Tail Plus Minus + // H H H H M M M M T T T T + // H H H H P P T T T T + // ^ start + // ^-----^ length + // ^-^ plus.length + // ^ start iteration + // ^ offset start iteration + // ^ end + // ^ offset end + // ^ start + minus.length - plus.length + // ^ start - diff + // ^------^ tail before + // ^------^ tail after + // ^ length - diff + // ^ newLength + for (var index = start + plus.length; index < oldLength - diff; index++) { + var offset = index - diff; + if (offset in array) { + array[index] = array[offset]; + } else { + array[index] = void 0; + delete array[index]; + } + } + } + array.length = newLength; +} + diff --git a/package.json b/package.json index 2c97e14..d286b5a 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "weak-map": "^1.0.4" }, "devDependencies": { - "jasminum": "^2.0.1", + "jasminum": "^2.0.4", "sinon": "^1.9.0", "istanbul": "^0.2.4", "opener": "^1.3.0" diff --git a/set.js b/set.js index b973edb..4be0901 100644 --- a/set.js +++ b/set.js @@ -1,12 +1,15 @@ "use strict"; -var Shim = require("./shim"); var List = require("./list"); var FastSet = require("./fast-set"); var GenericCollection = require("./generic-collection"); var GenericSet = require("./generic-set"); var ObservableObject = require("./observable-object"); var ObservableRange = require("./observable-range"); +var equalsOperator = require("./operators/equals"); +var hashOperator = require("./operators/hash"); +var noop = require("./operators/noop"); +var addEach = require("./operators/add-each"); module.exports = Set; @@ -14,9 +17,9 @@ function Set(values, equals, hash, getDefault) { if (!(this instanceof Set)) { return new Set(values, equals, hash, getDefault); } - equals = equals || Object.equals; - hash = hash || Object.hash; - getDefault = getDefault || Function.noop; + equals = equals || equalsOperator; + hash = hash || hashOperator; + getDefault = getDefault || noop; this.contentEquals = equals; this.contentHash = hash; this.getDefault = getDefault; @@ -40,10 +43,10 @@ function Set(values, equals, hash, getDefault) { Set.Set = Set; // hack so require("set").Set will work in MontageJS -Object.addEach(Set.prototype, GenericCollection.prototype); -Object.addEach(Set.prototype, GenericSet.prototype); -Object.addEach(Set.prototype, ObservableObject.prototype); -Object.addEach(Set.prototype, ObservableRange.prototype); +addEach(Set.prototype, GenericCollection.prototype); +addEach(Set.prototype, GenericSet.prototype); +addEach(Set.prototype, ObservableObject.prototype); +addEach(Set.prototype, ObservableRange.prototype); Set.prototype.Order = List; Set.prototype.Store = FastSet; diff --git a/shim-array.js b/shim-array.js index a834d3a..fd6ad61 100644 --- a/shim-array.js +++ b/shim-array.js @@ -7,11 +7,11 @@ https://github.com/motorola-mobility/montage/blob/master/LICENSE.md */ -var Function = require("./shim-function"); var GenericCollection = require("./generic-collection"); var GenericOrder = require("./generic-order"); var Iterator = require("./iterator"); var WeakMap = require("weak-map"); +var swap = require("./operators/swap"); module.exports = Array; @@ -24,34 +24,38 @@ if (Object.freeze) { Object.freeze(Array.empty); } -Array.from = function (values) { - var array = []; - array.addEach(values); - return array; -}; - -Array.unzip = function (table) { - var transpose = []; - var length = Infinity; - // compute shortest row - for (var i = 0; i < table.length; i++) { - var row = table[i]; - table[i] = row.toArray(); - if (row.length < length) { - length = row.length; +if (!Array.from) { + Array.from = function (values) { + var array = []; + array.addEach(values); + return array; + }; +} + +if (!Array.unzip) { + Array.unzip = function (table) { + var transpose = []; + var length = Infinity; + // compute shortest row + for (var i = 0; i < table.length; i++) { + var row = table[i]; + table[i] = row.toArray(); + if (row.length < length) { + length = row.length; + } } - } - for (var i = 0; i < table.length; i++) { - var row = table[i]; - for (var j = 0; j < row.length; j++) { - if (j < length && j in row) { - transpose[j] = transpose[j] || []; - transpose[j][i] = row[j]; + for (var i = 0; i < table.length; i++) { + var row = table[i]; + for (var j = 0; j < row.length; j++) { + if (j < length && j in row) { + transpose[j] = transpose[j] || []; + transpose[j][i] = row[j]; + } } } - } - return transpose; -}; + return transpose; + }; +} function define(key, value) { Object.defineProperty(Array.prototype, key, { @@ -103,6 +107,7 @@ define("set", function (index, value) { // unnecessary array. "swap" works in cases where the index // exceeds the length of the array, whereas splice would // truncate. + // The swap implementation is overridden by observable arrays. this.swap(index, 1, [value]); return this; }); @@ -144,100 +149,7 @@ define("findLastValue", function (value, equals) { }); define("swap", function (start, minusLength, plus) { - // Unrolled implementation into JavaScript for a couple reasons. - // Calling splice can cause large stack sizes for large swaps. Also, - // splice cannot handle array holes. - if (plus) { - if (!Array.isArray(plus)) { - plus = array_slice.call(plus); - } - } else { - plus = Array.empty; - } - - if (start < 0) { - start = this.length + start; - } else if (start > this.length) { - this.length = start; - } - - if (start + minusLength > this.length) { - // Truncate minus length if it extends beyond the length - minusLength = this.length - start; - } else if (minusLength < 0) { - // It is the JavaScript way. - minusLength = 0; - } - - var diff = plus.length - minusLength; - var oldLength = this.length; - var newLength = this.length + diff; - - if (diff > 0) { - // Head Tail Plus Minus - // H H H H M M T T T T - // H H H H P P P P T T T T - // ^ start - // ^-^ minus.length - // ^ --> diff - // ^-----^ plus.length - // ^------^ tail before - // ^------^ tail after - // ^ start iteration - // ^ start iteration offset - // ^ end iteration - // ^ end iteration offset - // ^ start + minus.length - // ^ length - // ^ length - 1 - for (var index = oldLength - 1; index >= start + minusLength; index--) { - var offset = index + diff; - if (index in this) { - this[offset] = this[index]; - } else { - // Oddly, PhantomJS complains about deleting array - // properties, unless you assign undefined first. - this[offset] = void 0; - delete this[offset]; - } - } - } - for (var index = 0; index < plus.length; index++) { - if (index in plus) { - this[start + index] = plus[index]; - } else { - this[start + index] = void 0; - delete this[start + index]; - } - } - if (diff < 0) { - // Head Tail Plus Minus - // H H H H M M M M T T T T - // H H H H P P T T T T - // ^ start - // ^-----^ length - // ^-^ plus.length - // ^ start iteration - // ^ offset start iteration - // ^ end - // ^ offset end - // ^ start + minus.length - plus.length - // ^ start - diff - // ^------^ tail before - // ^------^ tail after - // ^ length - diff - // ^ newLength - for (var index = start + plus.length; index < oldLength - diff; index++) { - var offset = index - diff; - if (offset in this) { - this[index] = this[offset]; - } else { - this[index] = void 0; - delete this[index]; - } - } - } - this.length = newLength; + return swap(this, start, minusLength, plus); }); define("peek", function () { diff --git a/shim-object.js b/shim-object.js index 924280d..e9444c1 100644 --- a/shim-object.js +++ b/shim-object.js @@ -1,6 +1,8 @@ "use strict"; var WeakMap = require("weak-map"); +var Operators = require("./operators"); +var hash = require("./operators/hash"); module.exports = Object; @@ -64,19 +66,7 @@ Object.getValueOf = function (value) { return value; }; -var hashMap = new WeakMap(); -Object.hash = function (object) { - if (object && typeof object.hash === "function") { - return "" + object.hash(); - } else if (Object.isObject(object)) { - if (!hashMap.has(object)) { - hashMap.set(object, Math.random().toString(36).slice(2)); - } - return hashMap.get(object); - } else { - return "" + object; - } -}; +Object.hash = hash; /** A shorthand for Object.prototype.hasOwnProperty.call(object, @@ -191,28 +181,7 @@ Object.set = function (object, key, value) { } }; -Object.addEach = function (target, source) { - if (!source) { - } else if (typeof source.forEach === "function" && !source.hasOwnProperty("forEach")) { - // copy map-alikes - if (typeof source.keys === "function") { - source.forEach(function (value, key) { - target[key] = value; - }); - // iterate key value pairs of other iterables - } else { - source.forEach(function (pair) { - target[pair[0]] = pair[1]; - }); - } - } else { - // copy other objects as map-alikes - Object.keys(source).forEach(function (key) { - target[key] = source[key]; - }); - } - return target; -}; +Object.addEach = require("./operators/add-each"); /** Iterates over the owned properties of an object. @@ -274,244 +243,13 @@ Object.concat = function () { Object.from = Object.concat; -/** - Returns whether two values are identical. Any value is identical to itself - and only itself. This is much more restictive than equivalence and subtly - different than strict equality, === because of edge cases - including negative zero and NaN. Identity is useful for - resolving collisions among keys in a mapping where the domain is any value. - This method does not delgate to any method on an object and cannot be - overridden. - @see http://wiki.ecmascript.org/doku.php?id=harmony:egal - @param {Any} this - @param {Any} that - @returns {Boolean} whether this and that are identical - @function external:Object.is -*/ -Object.is = function (x, y) { - if (x === y) { - // 0 === -0, but they are not identical - return x !== 0 || 1 / x === 1 / y; - } - // NaN !== NaN, but they are identical. - // NaNs are the only non-reflexive value, i.e., if x !== x, - // then x is a NaN. - // isNaN is broken: it converts its argument to number, so - // isNaN("foo") => true - return x !== x && y !== y; -}; - -/** - Performs a polymorphic, type-sensitive deep equivalence comparison of any - two values. - -

As a basic principle, any value is equivalent to itself (as in - identity), any boxed version of itself (as a new Number(10) is - to 10), and any deep clone of itself. - -

Equivalence has the following properties: - -

    -
  • polymorphic: - If the given object is an instance of a type that implements a - methods named "equals", this function defers to the method. So, - this function can safely compare any values regardless of type, - including undefined, null, numbers, strings, any pair of objects - where either implements "equals", or object literals that may even - contain an "equals" key. -
  • type-sensitive: - Incomparable types are not equal. No object is equivalent to any - array. No string is equal to any other number. -
  • deep: - Collections with equivalent content are equivalent, recursively. -
  • equivalence: - Identical values and objects are equivalent, but so are collections - that contain equivalent content. Whether order is important varies - by type. For Arrays and lists, order is important. For Objects, - maps, and sets, order is not important. Boxed objects are mutally - equivalent with their unboxed values, by virtue of the standard - valueOf method. -
- @param this - @param that - @returns {Boolean} whether the values are deeply equivalent - @function external:Object.equals -*/ -Object.equals = function (a, b, equals, memo) { - equals = equals || Object.equals; - // unbox objects, but do not confuse object literals - a = Object.getValueOf(a); - b = Object.getValueOf(b); - if (a === b) - return true; - if (Object.isObject(a)) { - memo = memo || new WeakMap(); - if (memo.has(a)) { - return true; - } - memo.set(a, true); - } - if (Object.isObject(a) && typeof a.equals === "function") { - return a.equals(b, equals, memo); - } - // commutative - if (Object.isObject(b) && typeof b.equals === "function") { - return b.equals(a, equals, memo); - } - if (Object.isObject(a) && Object.isObject(b)) { - if (Object.getPrototypeOf(a) === Object.prototype && Object.getPrototypeOf(b) === Object.prototype) { - for (var name in a) { - if (!equals(a[name], b[name], equals, memo)) { - return false; - } - } - for (var name in b) { - if (!equals(b[name], a[name], equals, memo)) { - return false; - } - } - return true; - } - } - // NaN !== NaN, but they are equal. - // NaNs are the only non-reflexive value, i.e., if x !== x, - // then x is a NaN. - // isNaN is broken: it converts its argument to number, so - // isNaN("foo") => true - // We have established that a !== b, but if a !== a && b !== b, they are - // both NaN. - if (a !== a && b !== b) - return true; - if (!a || !b) - return a === b; - return false; -}; - -// Because a return value of 0 from a `compare` function may mean either -// "equals" or "is incomparable", `equals` cannot be defined in terms of -// `compare`. However, `compare` *can* be defined in terms of `equals` and -// `lessThan`. Again however, more often it would be desirable to implement -// all of the comparison functions in terms of compare rather than the other -// way around. - -/** - Determines the order in which any two objects should be sorted by returning - a number that has an analogous relationship to zero as the left value to - the right. That is, if the left is "less than" the right, the returned - value will be "less than" zero, where "less than" may be any other - transitive relationship. +Object.is = Operators.is; -

Arrays are compared by the first diverging values, or by length. +Object.equals = Operators.equals; -

Any two values that are incomparable return zero. As such, - equals should not be implemented with compare - since incomparability is indistinguishable from equality. +Object.compare = Operators.compare; -

Sorts strings lexicographically. This is not suitable for any - particular international setting. Different locales sort their phone books - in very different ways, particularly regarding diacritics and ligatures. +Object.clone = Operators.clone; -

If the given object is an instance of a type that implements a method - named "compare", this function defers to the instance. The method does not - need to be an owned property to distinguish it from an object literal since - object literals are incomparable. Unlike Object however, - Array implements compare. - - @param {Any} left - @param {Any} right - @returns {Number} a value having the same transitive relationship to zero - as the left and right values. - @function external:Object.compare -*/ -Object.compare = function (a, b) { - // unbox objects, but do not confuse object literals - // mercifully handles the Date case - a = Object.getValueOf(a); - b = Object.getValueOf(b); - if (a === b) - return 0; - var aType = typeof a; - var bType = typeof b; - if (aType === "number" && bType === "number") - return a - b; - if (aType === "string" && bType === "string") - return a < b ? -Infinity : Infinity; - // the possibility of equality elimiated above - if (a && typeof a.compare === "function") - return a.compare(b); - // not commutative, the relationship is reversed - if (b && typeof b.compare === "function") - return -b.compare(a); - return 0; -}; - -/** - Creates a deep copy of any value. Values, being immutable, are - returned without alternation. Forwards to clone on - objects and arrays. - - @function external:Object.clone - @param {Any} value a value to clone - @param {Number} depth an optional traversal depth, defaults to infinity. - A value of 0 means to make no clone and return the value - directly. - @param {Map} memo an optional memo of already visited objects to preserve - reference cycles. The cloned object will have the exact same shape as the - original, but no identical objects. Te map may be later used to associate - all objects in the original object graph with their corresponding member of - the cloned graph. - @returns a copy of the value -*/ -Object.clone = function (value, depth, memo) { - value = Object.getValueOf(value); - memo = memo || new WeakMap(); - if (depth === undefined) { - depth = Infinity; - } else if (depth === 0) { - return value; - } - if (typeof value === "function") { - return value; - } else if (Object.isObject(value)) { - if (!memo.has(value)) { - if (value && typeof value.clone === "function") { - memo.set(value, value.clone(depth, memo)); - } else { - var prototype = Object.getPrototypeOf(value); - if (prototype === null || prototype === Object.prototype) { - var clone = Object.create(prototype); - memo.set(value, clone); - for (var key in value) { - clone[key] = Object.clone(value[key], depth - 1, memo); - } - } else { - throw new Error("Can't clone " + value); - } - } - } - return memo.get(value); - } - return value; -}; - -/** - Removes all properties owned by this object making the object suitable for - reuse. - - @function external:Object.clear - @returns this -*/ -Object.clear = function (object) { - if (object && typeof object.clear === "function") { - object.clear(); - } else { - var keys = Object.keys(object), - i = keys.length; - while (i) { - i--; - delete object[keys[i]]; - } - } - return object; -}; +Object.clear = require("./operators/clear"); diff --git a/shim-regexp.js b/shim-regexp.js index e30b9b8..5c345f0 100644 --- a/shim-regexp.js +++ b/shim-regexp.js @@ -1,14 +1,6 @@ -/** - accepts a string; returns the string with regex metacharacters escaped. - the returned string can safely be used within a regex to match a literal - string. escaped characters are [, ], {, }, (, ), -, *, +, ?, ., \, ^, $, - |, #, [comma], and whitespace. -*/ if (!RegExp.escape) { var special = /[-[\]{}()*+?.\\^$|,#\s]/g; - RegExp.escape = function (string) { - return string.replace(special, "\\$&"); - }; + RegExp.escape = require("./operators/escape"); } diff --git a/sorted-array-map.js b/sorted-array-map.js index 55ba226..710c56d 100644 --- a/sorted-array-map.js +++ b/sorted-array-map.js @@ -1,10 +1,12 @@ "use strict"; -var Shim = require("./shim"); var SortedArraySet = require("./sorted-array-set"); var GenericCollection = require("./generic-collection"); var GenericMap = require("./generic-map"); var ObservableObject = require("./observable-object"); +var equalsOperator = require("./operators/equals"); +var compareOperator = require("./operators/compare"); +var addEach = require("./operators/add-each"); module.exports = SortedArrayMap; @@ -12,8 +14,8 @@ function SortedArrayMap(values, equals, compare, getDefault) { if (!(this instanceof SortedArrayMap)) { return new SortedArrayMap(values, equals, compare, getDefault); } - equals = equals || Object.equals; - compare = compare || Object.compare; + equals = equals || equalsOperator; + compare = compare || compareOperator; getDefault = getDefault || this.getDefault; this.contentEquals = equals; this.contentCompare = compare; @@ -34,9 +36,9 @@ function SortedArrayMap(values, equals, compare, getDefault) { // hack so require("sorted-array-map").SortedArrayMap will work in MontageJS SortedArrayMap.SortedArrayMap = SortedArrayMap; -Object.addEach(SortedArrayMap.prototype, GenericCollection.prototype); -Object.addEach(SortedArrayMap.prototype, GenericMap.prototype); -Object.addEach(SortedArrayMap.prototype, ObservableObject.prototype); +addEach(SortedArrayMap.prototype, GenericCollection.prototype); +addEach(SortedArrayMap.prototype, GenericMap.prototype); +addEach(SortedArrayMap.prototype, ObservableObject.prototype); SortedArrayMap.prototype.constructClone = function (values) { return new this.constructor( diff --git a/sorted-array-set.js b/sorted-array-set.js index c0bb25f..0015a45 100644 --- a/sorted-array-set.js +++ b/sorted-array-set.js @@ -1,9 +1,9 @@ "use strict"; -var Shim = require("./shim"); var SortedArray = require("./sorted-array"); var GenericSet = require("./generic-set"); var ObservableObject = require("./observable-object"); +var addEach = require("./operators/add-each"); module.exports = SortedArraySet; @@ -21,8 +21,8 @@ SortedArraySet.prototype = Object.create(SortedArray.prototype); SortedArraySet.prototype.constructor = SortedArraySet; -Object.addEach(SortedArraySet.prototype, GenericSet.prototype); -Object.addEach(SortedArraySet.prototype, ObservableObject.prototype); +addEach(SortedArraySet.prototype, GenericSet.prototype); +addEach(SortedArraySet.prototype, ObservableObject.prototype); SortedArraySet.prototype.isSorted = true; diff --git a/sorted-array.js b/sorted-array.js index c37ff1e..a75a927 100644 --- a/sorted-array.js +++ b/sorted-array.js @@ -2,11 +2,14 @@ module.exports = SortedArray; -var Shim = require("./shim"); var GenericCollection = require("./generic-collection"); var ObservableObject = require("./observable-object"); var ObservableRange = require("./observable-range"); var Iterator = require("./iterator"); +var equalsOperator = require("./operators/equals"); +var compareOperator = require("./operators/compare"); +var noop = require("./operators/noop"); +var addEach = require("./operators/add-each"); function SortedArray(values, equals, compare, getDefault) { if (!(this instanceof SortedArray)) { @@ -18,9 +21,9 @@ function SortedArray(values, equals, compare, getDefault) { } else { this.array = []; } - this.contentEquals = equals || Object.equals; - this.contentCompare = compare || Object.compare; - this.getDefault = getDefault || Function.noop; + this.contentEquals = equals || equalsOperator; + this.contentCompare = compare || compareOperator; + this.getDefault = getDefault || noop; this.length = 0; this.addEach(values); @@ -29,9 +32,9 @@ function SortedArray(values, equals, compare, getDefault) { // hack so require("sorted-array").SortedArray will work in MontageJS SortedArray.SortedArray = SortedArray; -Object.addEach(SortedArray.prototype, GenericCollection.prototype); -Object.addEach(SortedArray.prototype, ObservableObject.prototype); -Object.addEach(SortedArray.prototype, ObservableRange.prototype); +addEach(SortedArray.prototype, GenericCollection.prototype); +addEach(SortedArray.prototype, ObservableObject.prototype); +addEach(SortedArray.prototype, ObservableRange.prototype); SortedArray.prototype.isSorted = true; diff --git a/sorted-map.js b/sorted-map.js index 388776c..bae2ed8 100644 --- a/sorted-map.js +++ b/sorted-map.js @@ -1,10 +1,12 @@ "use strict"; -var Shim = require("./shim"); var SortedSet = require("./sorted-set"); var GenericCollection = require("./generic-collection"); var GenericMap = require("./generic-map"); var ObservableObject = require("./observable-object"); +var equalsOperator = require("./operators/equals"); +var compareOperator = require("./operators/compare"); +var addEach = require("./operators/add-each"); module.exports = SortedMap; @@ -12,8 +14,8 @@ function SortedMap(values, equals, compare, getDefault) { if (!(this instanceof SortedMap)) { return new SortedMap(values, equals, compare, getDefault); } - equals = equals || Object.equals; - compare = compare || Object.compare; + equals = equals || equalsOperator; + compare = compare || compareOperator; getDefault = getDefault || this.getDefault; this.contentEquals = equals; this.contentCompare = compare; @@ -34,9 +36,9 @@ function SortedMap(values, equals, compare, getDefault) { // hack so require("sorted-map").SortedMap will work in MontageJS SortedMap.SortedMap = SortedMap; -Object.addEach(SortedMap.prototype, GenericCollection.prototype); -Object.addEach(SortedMap.prototype, GenericMap.prototype); -Object.addEach(SortedMap.prototype, ObservableObject.prototype); +addEach(SortedMap.prototype, GenericCollection.prototype); +addEach(SortedMap.prototype, GenericMap.prototype); +addEach(SortedMap.prototype, ObservableObject.prototype); SortedMap.prototype.constructClone = function (values) { return new this.constructor( diff --git a/sorted-set.js b/sorted-set.js index 8c490dd..2083fd9 100644 --- a/sorted-set.js +++ b/sorted-set.js @@ -2,20 +2,22 @@ module.exports = SortedSet; -var Shim = require("./shim"); var GenericCollection = require("./generic-collection"); var GenericSet = require("./generic-set"); var ObservableObject = require("./observable-object"); var ObservableRange = require("./observable-range"); var Iterator = require("./iterator"); var TreeLog = require("./tree-log"); +var equalsOperator = require("./operators/equals"); +var compareOperator = require("./operators/compare"); +var addEach = require("./operators/add-each"); function SortedSet(values, equals, compare, getDefault) { if (!(this instanceof SortedSet)) { return new SortedSet(values, equals, compare, getDefault); } - this.contentEquals = equals || Object.equals; - this.contentCompare = compare || Object.compare; + this.contentEquals = equals || equalsOperator; + this.contentCompare = compare || compareOperator; this.getDefault = getDefault || Function.noop; this.root = null; this.length = 0; @@ -25,10 +27,10 @@ function SortedSet(values, equals, compare, getDefault) { // hack so require("sorted-set").SortedSet will work in MontageJS SortedSet.SortedSet = SortedSet; -Object.addEach(SortedSet.prototype, GenericCollection.prototype); -Object.addEach(SortedSet.prototype, GenericSet.prototype); -Object.addEach(SortedSet.prototype, ObservableObject.prototype); -Object.addEach(SortedSet.prototype, ObservableRange.prototype); +addEach(SortedSet.prototype, GenericCollection.prototype); +addEach(SortedSet.prototype, GenericSet.prototype); +addEach(SortedSet.prototype, ObservableObject.prototype); +addEach(SortedSet.prototype, ObservableRange.prototype); SortedSet.prototype.isSorted = true; From cf168456896dc71138a90d85f1189888dfc606ae Mon Sep 17 00:00:00 2001 From: Harold Thetiot Date: Wed, 6 Dec 2017 18:55:36 -0800 Subject: [PATCH 82/83] merge .travis and add npm run lint --- .travis.yml | 9 ++++----- package.json | 14 ++++++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0267a85..585ff74 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,12 @@ language: node_js node_js: -<<<<<<< HEAD - - "4.8.0" + - "4." + - "6" + - "8" script: npm run $COMMAND env: + - COMMAND=lint - COMMAND=test -======= - - "0.10" ->>>>>>> v2 notifications: irc: channels: diff --git a/package.json b/package.json index 73e6c6b..ef731e8 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "collections", "version": "6.0.0", "publishConfig": { - "tag": "future" + "tag": "future" }, "description": "data structures with idiomatic JavaScript collection interfaces", "homepage": "http://www.collectionsjs.com", @@ -40,22 +40,24 @@ "weak-map": "~1.0.x" }, "devDependencies": { - "montage-testing": "git://github.com/montagejs/montage-testing#master", + "concurrently": "^3.4.0", + "http-server": "^0.9.0", + "istanbul": "^0.2.4", "jasmine-console-reporter": "^1.2.7", "jasmine-core": "^2.5.2", + "jshint": "^2.9.5", "karma": "^1.5.0", "karma-chrome-launcher": "^2.0.0", "karma-coverage": "^1.1.1", "karma-firefox-launcher": "^1.0.1", "karma-jasmine": "^1.1.0", "karma-phantomjs-launcher": "^1.0.2", + "montage-testing": "git://github.com/montagejs/montage-testing#master", "mop-integration": "git://github.com/montagejs/mop-integration.git#master", - "concurrently": "^3.4.0", - "http-server": "^0.9.0", - "open": "0.0.5", - "istanbul": "^0.2.4" + "open": "0.0.5" }, "scripts": { + "lint": "jshint .", "test": "node test/run-node.js", "integration": "mop-integration", "cover": "istanbul cover spec/index.js spec && istanbul report html && open coverage/index.html", From bbf9df05a3693add0a10096a8bd25d992eb1c4c2 Mon Sep 17 00:00:00 2001 From: Harold Thetiot Date: Wed, 6 Dec 2017 19:25:32 -0800 Subject: [PATCH 83/83] attempt at fix this.iterate undefined --- generic-map.js | 4 ---- iterator.js | 5 +++++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/generic-map.js b/generic-map.js index 5f80c08..4758391 100644 --- a/generic-map.js +++ b/generic-map.js @@ -142,10 +142,6 @@ GenericMap.prototype.clear = function () { } }; -GenericMap.prototype.iterate = function () { - return new this.Iterator(this); -}; - GenericMap.prototype.reduce = function (callback, basis, thisp) { return this.store.reduce(function (basis, item) { return callback.call(thisp, basis, item.value, item.key, this); diff --git a/iterator.js b/iterator.js index 66e811b..99a2652 100644 --- a/iterator.js +++ b/iterator.js @@ -138,6 +138,11 @@ FilterIterator.prototype.next = function () { } }; + +Iterator.prototype.iterate = function () { + return Iterator(this); +}; + Iterator.prototype.mapIterator = function (callback /*, thisp*/) { throw new Error('TODO'); return;