Pourover Test

Benchmark created by Pourover Test on


Description

Test Pourover Speeds

Preparation HTML

<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.6.0/underscore-min.js"></script>

<script>
var PourOver = (function(){

    PourOver = {
      // Utility functions. Skip down to "Collections" for the real meat of PourOver.
      //
      // # The basic sorted set operations
      //
      union_sorted: function(a,b){
        // Make more efficient by just copying at Infinity
        var lowa = 0, lowb = 0, higha = a.length, highb = b.length, result=[], la, lb;
        while (higha > lowa || highb > lowb){
          la = a[lowa];
          lb = b[lowb];
          if(_.isUndefined(la)) la = Infinity;
          if(_.isUndefined(lb)) lb = Infinity;
          if(lowa == higha){
            return result.concat(b.slice(lowb,highb));
          }
          if(lowb == highb){
            return result.concat(a.slice(lowa,higha));
          }
          if(la == lb){
            result.push(la);
            lowa++;lowb++;
          } else if (la < lb){
            result.push(la);
            lowa++;
          } else {
            result.push(lb);
            lowb++;
          }
        }
        return result;
      },
      intersect_sorted: function(a,b){
        var lowa = 0, lowb = 0, higha = a.length, highb = b.length, result=[], la, lb;
        while (higha > lowa && highb > lowb){
          la = a[lowa];
          lb = b[lowb];


          if(la == lb){
            result.push(la);
            lowa++;lowb++;
          } else if (la < lb){
            lowa++;
          } else {
            lowb++;
          }
        }
        return result;
      },
      subtract_sorted: function(a,b){
        var lowa = 0, lowb = 0, higha = a.length, highb = b.length, result=[], la, lb;
        while (higha > lowa || highb > lowb){
          la = a[lowa];
          lb = b[lowb];
          if(higha == lowa){
            return result;
          }
          if(highb == lowb){
            return result.concat(a.slice(lowa,higha));
          }
          if(la == lb){
            lowa++;lowb++;
          } else if (la < lb){
            result.push(la);
            lowa++;
          } else {
            lowb++;
          }
        }
        return result;
      },
      insert_sorted: function(set,element){
        var length = set.length,
          i = 0,
          last_elem = set[length - 1];
        if(element > last_elem){
          set.push(element);
          return set;
        }
        while(i < length){
          if(element < set[i]){
            return set.slice(0,i).concat([element]).concat(set.slice(i,length));
          } else {
            i++;
          }
        }
        set.push(element);
        return set;
      },

      //
      // # Sort support
      //

      // Sort the set according to some function and then store an array of the translations
      // of the indicies. So if the first item went to index 2 after being sorted, put 2 in
      // the first spot of the permutation array.
      build_permutation_array: function(set,sort){
        var sorted_set = _(set).clone(),perm=[];
        if(typeof(sort) === "function"){
          sorted_set.sort(sort);
        } else {
          sorted_set.sort(function(a,b){return sort.fn.call(sort,a,b);});
        }
        _(sorted_set).each(function(m,i){perm[m.cid] = i;});
        return perm;
      },
      // Use a permutation array to resort a subset of a collection.
      permute_from_array: function(collection,perm){
        var output = [];
        if(typeof(collection[0]) === "number"){
          _(collection).each(function(i){ output[perm[i]] = i ;});
        } else {
          _(collection).each(function(i){ output[perm[i.cid]] = i ;});
        }
        return _(output).without(undefined);
      },
      // Remove an element from a sorted set.
      remove_sorted: function(set,element){
        var length = set.length,
            i = 0;
        while(i < length){
          if(element == set[i]){
            return set.slice(0,i).concat(set.slice(i+1,length));
          } else {
            i++;
          }
        }
        return set;
      },
      bisect_by: function(f) {
        // Thanks to crossfilter (https://github.com/square/crossfilter) for this implementation.
        function bisectLeft(a, x, lo, hi) {
          while (lo < hi) {
            var mid = lo + hi >>> 1;
            if (f(a[mid]) < x) lo = mid + 1;
            else hi = mid;
          }
          return lo;
        }

        function bisectRight(a, x, lo, hi) {
          while (lo < hi) {
            var mid = lo + hi >>> 1;
            if (x < f(a[mid])) hi = mid;
            else lo = mid + 1;
          }
          return lo;
        }

        bisectRight.right = bisectRight;
        bisectRight.left = bisectLeft;
        return bisectRight;
      },
      // # Pre-defined cache methods
      // Caching is really the raison d'etre of Pourover. Every filter has two cache methods: one for rebuilding the whole filter from scratch
      // and one for adding new items. As Pourover grows it will gain more pre-defined cache methods that correlate with common UI and data patterns.
      cacheMethods: {
        // ### Default: the dumb caches.
        // Just goes through each possible value for the filter and tests every item in the collection against it. As expensive as
        // possibile, but simple.
        defaultCache: function(items){
          var that = this;
          _(that.possibilities).each(function(p){
            var matching_items = _(items).filter(function(i){return that.fn(p,i);}),
                matching_cids = _(matching_items).map(function(i){return i.cid;});
            p.matching_cids = matching_cids;
          });
        },
        defaultAddCache: function(items){
          var that = this;
          _(that.possibilities).each(function(p){
            var matching_items = _(items).filter(function(i){return that.fn(p,i);}),
                matching_cids = _(matching_items).map(function(i){return i.cid;});
            p.matching_cids = PourOver.union_sorted(p.matching_cids,matching_cids);
          });
        },
        // ### Exact: the fastest caches.
        // For filters that evaluate by strict equality (this property === this value). The name of the filter must
        // match the name of the property for exact cache to work.
        exactCache: function(items){
          var that = this,
              attr = this.attr || this.name;
          _(items).each(function(i){
            var p = that.possibilities[i[attr]];
            if (p) {
              p.matching_cids = PourOver.insert_sorted(p.matching_cids,i.cid);
            }
          });
        },
        exactAddCache: function(items){
          PourOver.cacheMethods.exactCache.call(this,items);
        },
        inclusionCache: function(items){
          var that = this,
              attr = this.attr || this.name;
          _(items).each(function(i){
            _(i[attr]).each(function(v){
              var p = that.possibilities[v];
              if(p){
                p.matching_cids = PourOver.insert_sorted(p.matching_cids,i.cid);
              }
            });
          });
        },
        inclusionAddCache: function(items){
          PourOver.cacheMethods.inclusionCache.call(this,items);
        }
      }
    };
        // Copied from Backbone
        var array = [];
        var push = array.push;
        var slice = array.slice;
        var splice = array.splice;
        var Events = PourOver.Events = {

        // Bind an event to a `callback` function. Passing `"all"` will bind
        // the callback to all events fired.
        on: function(name, callback, context) {
          if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this;
          this._events || (this._events = {});
          var events = this._events[name] || (this._events[name] = []);
          events.push({callback: callback, context: context, ctx: context || this});
          return this;
        },

        // Bind an event to only be triggered a single time. After the first time
        // the callback is invoked, it will be removed.
        once: function(name, callback, context) {
          if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this;
          var self = this;
          var once = _.once(function() {
            self.off(name, once);
            callback.apply(this, arguments);
          });
          once._callback = callback;
          return this.on(name, once, context);
        },

        // Remove one or many callbacks. If `context` is null, removes all
        // callbacks with that function. If `callback` is null, removes all
        // callbacks for the event. If `name` is null, removes all bound
        // callbacks for all events.
        off: function(name, callback, context) {
          var retain, ev, events, names, i, l, j, k;
          if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
          if (!name && !callback && !context) {
            this._events = void 0;
            return this;
          }
          names = name ? [name] : _.keys(this._events);
          for (i = 0, l = names.length; i < l; i++) {
            name = names[i];
            if (events = this._events[name]) {
              this._events[name] = retain = [];
              if (callback || context) {
                for (j = 0, k = events.length; j < k; j++) {
                  ev = events[j];
                  if ((callback && callback !== ev.callback && callback !== ev.callback._callback) ||
                      (context && context !== ev.context)) {
                    retain.push(ev);
                  }
                }
              }
              if (!retain.length) delete this._events[name];
            }
          }

          return this;
        },

        // Trigger one or many events, firing all bound callbacks. Callbacks are
        // passed the same arguments as `trigger` is, apart from the event name
        // (unless you're listening on `"all"`, which will cause your callback to
        // receive the true name of the event as the first argument).
        trigger: function(name) {
          if (!this._events) return this;
          var args = slice.call(arguments, 1);
          if (!eventsApi(this, 'trigger', name, args)) return this;
          var events = this._events[name];
          var allEvents = this._events.all;
          if (events) triggerEvents(events, args);
          if (allEvents) triggerEvents(allEvents, arguments);
          return this;
        },

        // Tell this object to stop listening to either specific events ... or
        // to every object it's currently listening to.
        stopListening: function(obj, name, callback) {
          var listeningTo = this._listeningTo;
          if (!listeningTo) return this;
          var remove = !name && !callback;
          if (!callback && typeof name === 'object') callback = this;
          if (obj) (listeningTo = {})[obj._listenId] = obj;
          for (var id in listeningTo) {
            obj = listeningTo[id];
            obj.off(name, callback, this);
            if (remove || _.isEmpty(obj._events)) delete this._listeningTo[id];
          }
          return this;
        }

      };

      // Regular expression used to split event strings.
      var eventSplitter = /\s+/;

      // Implement fancy features of the Events API such as multiple event
      // names `"change blur"` and jQuery-style event maps `{change: action}`
      // in terms of the existing API.
      var eventsApi = function(obj, action, name, rest) {
        if (!name) return true;

        // Handle event maps.
        if (typeof name === 'object') {
          for (var key in name) {
            obj[action].apply(obj, [key, name[key]].concat(rest));
          }
          return false;
        }

        // Handle space separated event names.
        if (eventSplitter.test(name)) {
          var names = name.split(eventSplitter);
          for (var i = 0, l = names.length; i < l; i++) {
            obj[action].apply(obj, [names[i]].concat(rest));
          }
          return false;
        }

        return true;
      };

      // A difficult-to-believe, but optimized internal dispatch function for
      // triggering events. Tries to keep the usual cases speedy (most internal
      // PourOver events have 3 arguments).
      var triggerEvents = function(events, args) {
        var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];
        switch (args.length) {
          case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return;
          case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
          case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
          case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;
          default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return;
        }
      };

      var listenMethods = {listenTo: 'on', listenToOnce: 'once'};

      // Inversion-of-control versions of `on` and `once`. Tell *this* object to
      // listen to an event in another object ... keeping track of what it's
      // listening to.
      _.each(listenMethods, function(implementation, method) {
        Events[method] = function(obj, name, callback) {
          var listeningTo = this._listeningTo || (this._listeningTo = {});
          var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
          listeningTo[id] = obj;
          if (!callback && typeof name === 'object') callback = this;
          obj[implementation](name, callback, this);
          return this;
        };
      });

      // Aliases for backwards compatibility.
      Events.bind   = Events.on;
      Events.unbind = Events.off;

      // Allow the `PourOver` object to serve as a global event bus, for folks who
      // want global "pubsub" in a convenient place.
      _.extend(PourOver, Events);
      // #Collections
      //The main kind of object in Pourover. A collection is basically a wrapper around an array of objects.
      //It adds collection ids to its members and has support for various ways of retrieving all or a part of
      //its members.

      PourOver.Collection = function(items,opts){
        if(typeof(items) == "undefined"){items = [];}
        this.items = [];
        this.filters = {};
        this.sorts = {};
        this.addItems(items);
        this.on("change",function(){
          _(this.filters).each(function(f){ if(f.current_query){f.current_query.refresh();} });
        });
        this.initialize.apply(this, arguments);
      };

      _.extend(PourOver.Collection.prototype,PourOver.Events,{
          initialize: function(){},
          // Force the filters and sorts of a collection to refresh. Generally most useful if you have batched
          // up a bunch of silented actions and you want to refresh once at the end.
          refresh: function(){
            this.trigger("queryChange");
          },

          // Retrive the objects associated with an array of cids. Like everything in Pourover, the cids must be sorted.
          // This is not ususally an issue as you generally will not be calling `collection.get` with an array you
          // manually create. You will probably be using the output of some function that keeps it sorted for you.
          get: function(cids){
            return PourOver.Collection.prototype.getBy.call(this,"cid",cids,true);
          },

          // Similar to get, except -- rather than getting items by cid -- you are getting them by [attr_name].
          // Here vals is an array of [attr_names]s.
          getBy: function(attr_name,vals,sorted){
            if(! _.isArray(vals)){ var vals = [vals] }
            if(typeof(sorted) == "undefined"){sorted = false;}
            var low = 0, high = this.items.length,lc = 0, hc = vals.length, output = [],items = this.items,i;
            if(sorted == true){
              while (low < high && lc < hc){
                if (vals[lc] == (i=items[low])[attr_name]){
                  output.push(i);
                  low++;
                  lc++;
                } else if (vals[lc] < i[attr_name]){
                  lc++;
                } else{
                  low++;
                }
              }
            } else if (sorted == "reverse"){
              while (low < high && lc < hc){
                if (vals[lc] == (i=items[low])[attr_name]){
                  output.push(i);
                  low++;
                  lc++;
                } else if (vals[lc] > i[attr_name]){
                  lc++;
                } else{
                  low++;
                }
              }
            } else {
              while (low < high && lc < hc){
                if ( _(vals).include((i=items[low])[attr_name])){
                  output.push(i);
                  vals = _(vals).without(i[attr_name]);
                  low++;
                  lc++;
                } else {
                  low++;
                }
              }
            }
            return output;
          },

          // Add items to the collection, triggering the appropriate events to keep all dependent sort and filter sets up-to-date.
          addItems: function(i){
            this.trigger("will_change");
            if(! _.isArray(i)){ var i = [i] }
            var last_id = this.items.length > 0 ? _(this.items).last().cid + 1 : 0,new_items;
            new_items = _(i).map(function(c){var n = PourOver.Item(c); n.cid = last_id++; return n;});
            this.items = this.items.concat(new_items);
            this.regenerateFilterSets(new_items);
            this.trigger("change");
          },

          // Remove items from the collection, triggering the appropriate events to keep all dependent sort and filter sets up-to-date.
          // This functionality is only included begrudgingly. Pourover is best for collections that rarely remove members.
          // TODO: Optimize
          removeItems: function(i,isSorted){
            this.trigger("will_change");
            if(typeof(isSorted) === "undefined"){var isSorted = false}
            if(! _.isArray(i)){ var i = [i] }
            if(isSorted){
              i = i.sort(function(a,b){return a.cid - b.cid ;});
              var new_items = [],old_items = this.items,new_length = i.length,old_length = this.items.length,newi = 0, oldi = 0;
              while(oldi < old_length){
                if(! newi < new_length){
                  new_items = new_items.concat(old_items.slice(oldi));
                  break;
                } else if(old_items[oldi].cid === i[newi].cid){
                  newi++;
                  oldi++;
                } else {
                  new_items.push(old_items[oldi]);
                  oldi++;
                }
              }
            } else {
              var new_items = [], old_items = this.items,old_length = this.items.length, oldi = 0,delete_cids = _(i).pluck("cid");
              while(oldi < old_length && delete_cids.length > 0){
                if(_(delete_cids).include(old_items[oldi].cid)){

                } else {
                  new_items.push(old_items[oldi]);
                }
                oldi++;
              }
            }
            this.items = new_items;
            this.regenerateFilterSets();
            this.trigger("change");
          },

          // # Collection filter functions
          // All filters are associated to collections rather than views. This allows for multiple views to share the same filter.
          // This is especially useful for modal situations in which you can set filters on a grid view that are reflected in the
          // one up view as well.
          addFilters: function(f){
            var that = this,new_filters;
            if(! _.isArray(f)){ var f = [f] }
            new_filters = _(f).reduce(function(m,i){ m[i.name] = _.clone(i); m[i.name].collection = that; return m; },{});
            this.filters = _(this.filters).extend(new_filters);
            // Bubble all query change events up from the individual filters to the collection. This allows a developers to
            // specify events that should be triggered whenever any filter's query is changed.
            _(new_filters).each(function(f){
              f.on("queryChange",function(){
                that.trigger("queryChange");
              });
              // All filters precache the result of their filtering. This is the source of pourover's speed optimizations.
              f.cacheResults(that.items);
              // If a user passes in an `associated_attrs` property on a filter, that filter will re-cache its result whenever
              // any object in the collection has an attribute changed. Setting `associated_attrs` is essential for admins or
              // other uses in which filterable values can change.
              if(f.associated_attrs){
                _(f.associated_attrs).each(function(a){
                  that.on("change:"+a,function(objs){
                    f.removeFromCache(objs);
                    f.addCacheResults(objs);
                    if(f.current_query){f.current_query.refresh();}
                  });
                });
              }
            });
          },

          // A shortcut to re-calculate the results of every filter. This is expensive if you do not pass in `new_items`, in which cases
          // only the new_items will be cached and the filters updated.
          regenerateFilterSets: function(new_items){
            var that = this;
            // If no new items are passed in, regenerate filters for all items in the collection
            if(typeof(new_items) == "undefined"){
              _(this.filters).each(function(f){
                f.cacheResults(that.items);
              });
            } else {
              _(this.filters).each(function(f){
                f.addCacheResults(new_items);
              });
            }
          },

          // A shortcut for returning a match object containing all the items in a collection. More on matches below.
          getAllItems: function(){
            var cids = _(this.items).map(function(i){return i.cid;});
            return new PourOver.MatchSet(cids,this,["all"]);
          },

          // Get the currently cached results for the last stateful query on a filter (the last time a `setQuery` was called on that filter.)
          // If `empty_default` is set to true, the function will return no items if the filter does not have a current query set. Otherwise,
          // the function will return all items in the collection. The former `empty_default` setting is useful when OR-ing filters together, when
          // you want an unset filter to represent an unselected dimension. The latter is useful when AND-ing filters together, when you
          // want an unset filter to comprise all objects in the collection.
          getCurrentFilteredItems: function(filter_name,empty_default){
            if(typeof(empty_default) === "undefined"){empty_default = false;}
            if(this.filters[filter_name].current_query && this.filters[filter_name].current_query.stack.length > 0){
              return this.filters[filter_name].current_query;
            } else {
              if(empty_default){
                return new PourOver.MatchSet([],this,[]);
              } else {
                return this.getAllItems();
              }
            }
          },

          // The non-stateful way to query a filter. Simply returns the result of the query but does not store the query on the filter.
          getFilteredItems: function(filter_name,query){
            var filter = this.filters[filter_name],possibility;
            if (_.isUndefined(filter) ) throw "The filter " + filter_name + " does not exist.";
            return filter.getFn(query);
          },

          // # Sort functions
          // Sorts, like filters, are generally stored on collections for the same reason that filters are stored on the collection rather than the view.
          // However, whereas filters keep track of their own state and this is shared between views, the state of which sort is enabled is stored on the view.

          addSort: function(sort){
            var that = this;
            this.sorts[sort.name] = sort;
            sort.collection = this;
            sort.rebuild_sort();
            this.on("change",function(){ sort.rebuild_sort(); });
            // Like filters, if you set `associated_attrs` on a sort, they will rebuild themselves whenever any item in the collection undergoes a change
            // on that attribute.
            // TODO: Consider cloning on add. Also, bring in line with addFilter (events or not!?)
            if(sort.associated_attrs){
              _(sort.associated_attrs).each(function(a){
                that.on("change:"+a,function(objs){
                  sort.rebuild_sort();
                });
              });
            }
          },

          // Add multiple sorts.
          addSorts: function(sorts){
            if(typeof(opts) === "undefined"){ opts = {};}
            if(! _(sorts).isArray()){sorts = [sorts];}
            var that = this;
            _(sorts).each(function(s){
              that.addSort(s);
            });
          },

          // The non-stateful way to retrieve all the items in the collection, sorted.
          getSortedItems: function(sort_name){
            var s = this.sorts[sort_name],that = this,output;
            return s.sort(this.items);
          },

          // A silly shortcut, pass in a cid and an attribute, retrieve its value. Useful for template helpers.
          getItemValue: function(cid,attribute){
            var item = _(this.items).find(function(i){return i.cid === Number(cid);});
            return item[attribute];
          },

          // Update the value of one attribute of one item in the collection.
          updateItem: function(cid,attribute,value){
            this.trigger("will_incremental_change");
            var item = _(this.items).find(function(i){return i.cid === Number(cid);});
            item[attribute] = value;
            this.trigger("change:"+attribute,[item]);
            this.trigger("incremental_change",[attribute]);
            this.trigger("update","updateItem");
            return item.guid;
          },

          // Delete an attribute of one item in the collection.
          removeItemAttribute: function(cid,attribute,value){
            this.trigger("will_incremental_change");
            var item = _(this.items).find(function(i){return i.cid === Number(cid);});
            delete item[attribute];
            this.trigger("change:"+attribute,[item]);
            this.trigger("incremental_change",[attribute]);
            this.trigger("update","updateItem");
            return item.guid;
          },

          // Change the value of one attribute of many items to the same value.
          batchUpdateItems: function(cids,attribute,value){
            this.trigger("will_incremental_change");
            var items = this.get(cids,true);
            _(items).each(function(i){
              i[attribute] = value;
            });
            this.trigger("change:"+attribute,items);
            this.trigger("incremental_change",[attribute]);
            this.trigger("update","batchUpdate");
            return _(items).pluck("guid");
          },

          // Change the value of several attributes of a single item in the collection.
          updateAttributes: function(cid,updates){
            this.trigger("will_incremental_change");
            var item = _(this.items).find(function(i){return i.cid === Number(cid);});
            var that = this;
            _(updates).each(function(v,k){
              item[k] = v;
              that.trigger("change:"+k,[item]);
            });
            this.trigger("incremental_change",_(updates).keys());
            this.trigger("update","updateAttribute");
            return item.guid;
          },

          // Change the value of several attributes of several items in the collection. Here 'updates'
          // is a hash of attributes -> new values.
          batchUpdateAttributes: function(cids,updates){
            this.trigger("will_incremental_change");
            var items = this.get(cids,true);
            var that = this;
            _(items).each(function(item){
              _(updates).each(function(v,k){
                item[k] = v;
              });
            });
            _(updates).each(function(v,k){
              that.trigger("change:"+k,items);
            });
            this.trigger("incremental_change",_(updates).keys());
            this.trigger("update","batchUpdate");
            this.trigger("batchUpdateAttribute");
            return _(items).pluck("guid");
          },

          batchLoadItems: function(data){
            this.trigger("will_incremental_change");

            _(data).each(_.bind(function(d){
                var item = this.getBy("guid",d.guid),
                    last_id = this.items.length > 0 ? _(this.items).last().cid + 1 : 0,
                    current_item;
                if (item && item[0]){
                    current_item = item[0];
                  _(d).each(function(v,k){
                    current_item[k] = v;
                  });
                } else {
                    item = PourOver.Item(d); 
                    item.cid = last_id++;
                    this.items = this.items.concat([item]);
                }
            },this))

            this.regenerateFilterSets();
            this.trigger("incremental_change","*");
            this.trigger("change");
            this.trigger("update","batchLoad");
            this.trigger("batchLoadItems");
          }
      });

      // #Items
      // If we ever need to add properties to items in a collection, the code would go here.
      PourOver.Item = function(i){
        return i;
      },

      // #Filters

      // A filter is basically a rule for mapping items of a collection into groups based on attribute
      // values. It caches the results and can be queried either statefully or non-statefully, depending
      // on developer preference.
      PourOver.Filter = function(name,values,opts){
        if(typeof(opts) === "undefined"){opts = {};}
        this.name = name;
        this.possibilities = this.create_possibilities(values);
        this.values = _(values).map(function(v){return v.value;});
        _.extend(this,opts);
        this.initialize.apply(this, arguments);
      }

      _.extend(PourOver.Filter.prototype,PourOver.Events,{

        // Initialize is a no-op by default.
        initialize: function(){},

        // Given an array of possible values, initializes the object that will store the cached results
        // of querying for that possibility.
        create_possibilities: function(vs){
          var o = {};
          _(vs).each(function(v){
            var name = v.name || String(v.value);
            o[name] = v;
            o[name].matching_cids = [];
          });
          return o;
         },

         // cacheResults and addCacheResults are generic methods that are must be overridden before instantiating a filter.
         // The preset filters included below provide good examples of how these functions should be written. cacheResults
         // should cache all the items in the collection, whereas addCacheResults incrementally adds new items to already
         // cached, filtered results.
         cacheResults: function(items){
           throw "No cache function has been defined for this filter '" + this.name + "'.";
         },
         addCacheResults: function(items){
           throw "No add cache function has been defined for this filter '" + this.name + "'.";
         },

         makeQueryMatchSet: function(cids,query){
            return new PourOver.MatchSet(cids, this.getCollection(), [[this,query]]);
         },

         // Generally only used when removing items from a collection or when an item changes value. This will remove the item from
         // the cache so that it can either be recached with its new value or thrown away.
         removeFromCache: function(items){
          var cids = _(items).map(function(i){return i.cid;}).sort(function(a,b){return a-b;});
          _(this.possibilities).each(function(p){
            p.matching_cids = PourOver.subtract_sorted(p.matching_cids,cids);
          });
         },

         // The stateful way to query a filter. Delegates the retrieval of a MatchSet to the filter's getFn and caches the results on the filter.
         query: function(q,silent){
           if(typeof(silent) === "undefined"){var silent = false;}
           var match_set = this.getFn(q);
           this.setQuery(match_set,silent);
         },

         // Assigns a MatchSet to a filter (caches the result) and triggers the appropriate events.
         setQuery: function(q,silent){
           if(typeof(silent) === "undefined"){var silent = false;}
           this.current_query = q;
           if(!silent){
             this.trigger("queryChange");
           }
         },

         // Removes a cached result from a filter.
         clearQuery: function(silent){
           if(typeof(silent) === "undefined"){var silent = false;}
           this.current_query = false;
           if(!silent){
             this.trigger("queryChange");
           }
         },

         // Unions a cached result with another result (both being MatchSets) and produces a new MatchSet.
         unionQuery: function(q,silent){
           if(typeof(silent) === "undefined"){var silent = false;}
           if(typeof(q) === "string" || typeof(q) === "number" || _.isArray(q)){
             var q = this.getFn(q);
           }
           if(this.current_query){
             this.current_query = this.current_query.or(q);
           } else {
             this.current_query = q;
           }
           if(!silent){
             this.trigger("queryChange");
           }
         },

         // Intersects a cached result with another result (both being MatchSets) and produces a new MatchSet.
         intersectQuery: function(q,silent){
           if(typeof(silent) === "undefined"){var silent = false;}
           if(typeof(q) === "string" || typeof(q) === "number" || _.isArray(q)){
             var q = this.getFn(q);
           }
           if(this.current_query){
             this.current_query = this.current_query.and(q);
           } else {
             this.current_query = q;
           }
           if(!silent){
             this.trigger("queryChange");
           }
         },

         // Subtracts a cached result with another result (both being MatchSets) and produces a new MatchSet.
         subtractQuery: function(q,silent){
           if(typeof(silent) === "undefined"){var silent = false;}
           if(typeof(q) === "string" || typeof(q) === "number" || _.isArray(q)){
             var q = this.getFn(q);
           }
           if(this.current_query){
             this.current_query = this.current_query.not(q);
           } else {
             this.current_query = q;
           }
           if(!silent){
             this.trigger("queryChange");
           }
         },

         // This is the inverse of the three functions above. Removes a query from a compound, cached MatchSet on a filter.
         // This is useful when you have a UI in which subsequent selections union together. It is faster on a toggle to remove
         // the deselected possibility rather than re-union the remaining selected ones.
         // TODO: Test
         removeSingleQuery: function(q,silent){
           if(! this.current_query){return false;}
           if(typeof(silent) === "undefined"){var silent = false;}
           if(typeof(q) === "string" || typeof(q) === "number" || _.isArray(q)){
             var q = this.getFn(q);
           }
           var s = [],
               stack = this.current_query.stack,new_stack,
               is_compound = function(c){return _.isString(c) && c.match(/^(or|and|not)$/);};
            new_stack = _(stack).reduce(function(m,i){
              if(i[1] === q.stack[0][1]){
                return m;
              } else if(is_compound(i[0]) && i[1][0][1] === q.stack[0][1]){
                return m;
              } else {m.push(i); return m;}
            },s);
           if(new_stack[0] && (new_stack[0][0] == "and" || new_stack[0][0] == "or" || new_stack[0][0] == "not")){
             new_stack[0] = new_stack [0][1][0];
           }
           this.current_query.stack = new_stack;
           this.current_query.refresh();
           if(!silent){
             this.trigger("queryChange");
           }
         },

         // Convenice method for getting the collection attached to a filter.
         // Just an aesthetic thing. I like the explicit "getCollection" calls in
         // the rest of the code.
         getCollection: function(){
           return this.collection;
          },

         getByPossibilityGroups: function(){
           var collection = this.collection;
           return _(this.possibilities).reduce(function(m,p,k){m[k] = collection.get(p.matching_cids); return m;},{});
         }
      });

      // #Sorts
      //
      // Sorts cache different orderings of collection items and subsets thereof. Sorts generally belong to collections,
      // but they can belong to views as well for optimization concerns.
      PourOver.Sort = function(name,opts){
        this.name = name;
        _.extend(this,opts);
        this.initialize.apply(this, arguments);
      };

      _.extend(PourOver.Sort.prototype,PourOver.Events,{
        initialize: function(){},

        // By default, sorts are not view sorts. A view sort is attached to a specific view and only updates when that
        // view undergoes a queryChange.
        view: false,

        // Use a sort to order an array of cids
        sort: function(set){return PourOver.permute_from_array(set,this.permutation_array);},

        // Recache the results of sorting the collection.
        rebuild_sort: function(){
          if(this.view){
            var items = this.view.match_set.all();
          } else {
            var items = this.collection.items;
          }
          this.permutation_array = PourOver.build_permutation_array(items,this);
          this.trigger("resort");
        }
      });

      // #Views
      //
      // Views store a state of collection and are generally what should be rendered. There can be many views per collection.
      // Views can be paged. Moreover, a view has a selection function which tells the view how to compose its various filters to produce the current set.
      PourOver.View = function(name,collection,opts){
        var that = this;
        this.name = name;
        if(typeof(opts) === "undefined"){ opts = {};}
        this.collection = collection;
        this.match_set = new PourOver.MatchSet(_(this.collection.items).map(function(i){return i.cid;}),this.collection,["all"]);
        if(opts.template){this.template = opts.template;}

        // Whenever the collection gains or loses members, recache the MatchSet saved on the view.
        this.collection.on("will_change will_incremental_change",function(){
            that.storeViewPosition();
        });

        this.collection.on("change",function(){
          that.match_set.refresh();
          that.setNaturalSelection();
          that.resetPage();
          that.trigger("collection-change");
        });

        // Whenever an item in the collection is changed, recache the MatchSet saved on the view.
        this.collection.on("incremental_change",function(attrs){
          that.match_set.refresh();
          that.setNaturalSelection(attrs);
          that.resetPage();
          that.trigger("collection-incremental-change");
        });

        // Bubble all collection update events through.
        this.collection.on("update",function(f){
          that.trigger("update",f);
        });

        // Whenever any filter is queried statefully, reset the view's MatchSet; We don't have to refresh the match_set here. That is only necessary
        // when it's possible that a filter has stale information as a result of a change in the underlying data.
        this.collection.on("queryChange",function(){
          that.setNaturalSelection();
          that.trigger("update","query");
        });

        // Bubble up sortChange events as updates
        this.on("sortChange",function(){
          this.trigger("update","sort");
        });

        // Bubble up pageChange events as updates
        this.on("pageChange",function(){
          this.trigger("update","page");
        });
        this.view_sorts = [];
        _.extend(this,opts);
        this.initialize.apply(this, arguments);
      };

      _.extend(PourOver.View.prototype,PourOver.Events,{
        initialize: function(){},
        current_page: 0,

        // By default, return all items in the view.
        page_size: Infinity,
        current_sort: false,

        // Changes a view from being sorted to no longer being sorted.
        removeSort: function(){
          if(this.current_sort.off){this.current_sort.off("resort");}
          this.current_sort = false;
          this.trigger("sortChange");
        },

        // Sets a sort on a view and fires all appropriate events.
        setSort: function(sort_name,view_sort,silent){
          if(typeof(view_sort) === "undefined"){view_sort = false;}
          if(typeof(silent) === "undefined"){silent = false;}
          var that = this;
          if(this.current_sort.off){this.current_sort.off("resort");}
          if(sort_name && view_sort){
            this.current_sort = this.view_sorts[sort_name];
            this.current_sort.on("resort",function(){that.trigger("sortChange");});
          } else if(sort_name){
            this.current_sort = this.collection.sorts[sort_name];
            this.current_sort.on("resort",function(){that.trigger("sortChange");});
          } else {
            this.current_sort = false;

          }
          if(! silent){
            this.trigger("sortChange");
          }
        },

        // Return the name of the current sort of the view.
        getSort: function(){
          if (!this.current_sort){
            return false;
          } else {
            return this.current_sort.name;
          }
        },

        // Add a sort to the view. The difference between this and a collection sort is that this sort will
        // only change if the view receives a selectionChange.
        addViewSorts: function(sorts){
            if(typeof(opts) === "undefined"){ opts = {};}
            if(! _(sorts).isArray()){sorts = [sorts];}
            var that = this;
            _(sorts).each(function(sort){
              that.view_sorts[sort.name] = sort;
              sort.collection = that.collection;
              sort.view = that;
              sort.rebuild_sort();
              that.on("selectionChange",function(attrs){
                if(sort.associated_attrs == undefined || attrs === "*"){
                  sort.rebuild_sort();
                }
                if(sort.associated_attrs && _.intersection(sort.associated_attrs,attrs).length > 0){
                  sort.rebuild_sort();
                }
              });
            });
        },

        // IMPORTANT: This determines how a view composes the filters on a collection to generate results. Here, by default,
        // every filter on the collection is intersected. This is often the desired behavior. However, this must be overridden
        // if you want your view to do fancier things such as union some filters, difference others, and intersect the rest.
        selectionFn: function(){
          var collection = this.collection;
          var output = _(collection.filters).reduce(function(m,i){
            var q = i.current_query;
            if(m && (!q || _.isEmpty(q.stack))){ return m;}
            if(!m && (!q || _.isEmpty(q.stack))){return collection.getAllItems();}

            if(m){
              return m.and(q);
            } else {
              return q;
            }
          },false);
          return output;
        },

        // Caches a MatchSet on the view as the current match_set;
        setSelection: function(match_set,attrs){
          this.match_set = match_set;
          this.trigger("selectionChange",attrs);
        },

        // Delegates to the views selectionFn to generate an array of valid cids given the current filters.
        setNaturalSelection: function(attrs){
          var selection;
          selection = this.selectionFn();
          this.setSelection(selection,attrs);
        },

        // Removes a MatchSet from a view and replaces it with the universe of possible items.
        clearSelection: function(){this.match_set = this.collection.getAllItems();},

        // IMPORTANT: This is the function you will call most often on views. This returns the cached, filtered items and
        // then sorts them and pages them as appropriate.
        getCurrentItems: function(page){
          if(! this.match_set){return [];}
          if(typeof(page) === "undefined"){
            var page = this.current_page;
          }
          if(this.page_size == Infinity){
            if(this.current_sort){
              var items = this.match_set.all_sorted(this.current_sort);
            } else {
              var items = this.match_set.all();
            }
          } else {
          // TODO: Slice cids before reassociating
            if(this.current_sort){
              var items = this.match_set.all_sorted_cids(this.current_sort);
              items = items.slice(this.page_size * page,this.page_size * (page + 1));
              var ordered_cids = _(items).clone().sort(function(a,b){return a-b;});
              var unsorted_items = this.collection.get(ordered_cids);
              items = _(items).map(function(i){return _(unsorted_items).find(function(o){return o.cid === i;});});
            } else {
              var items = this.match_set.cids;
              items = items.slice(this.page_size * page,this.page_size * (page + 1));
              items = this.collection.get(items);
            }
          }
          return items;
        },

        storeViewPosition: function(){
            var head_item =  this.getCurrentItems()[0];
            if(head_item){
                this.last_head_cid = head_item.cid;
            }
        },

        resetPage: function(){
            if(this.last_head_cid){
                if(this.current_sort){
                    this.current_sort.rebuild_sort();
                }
                this.pageTo(this.last_head_cid,true);
            }
            this.last_head_cid = undefined;
        },

        // Change the page of the view by [dir] pages. Negative values to page back.
        page: function(dir){
          var new_dir = dir + this.current_page;
          if(new_dir < 0) new_dir = 0;
          if(new_dir > Math.ceil(this.match_set.length()/this.page_size - 1)) new_dir = Math.ceil(this.match_set.length()/this.page_size - 1);
          this.current_page = new_dir;
          this.trigger("pageChange");
        },

        // Page to a specific cid.
        pageTo: function(cid,silent){
          if(typeof(silent) == "undefined"){
            var silent = false;
          }
          if(this.current_sort){
            var index = _(this.match_set.all_sorted_cids(this.current_sort)).indexOf(cid),
                len = this.match_set.cids.length,
                page = Math.floor(index/this.page_size);
          } else {
            var index = _(this.match_set.cids).indexOf(cid),
                len = this.match_set.cids.length,
                page = Math.floor(index/this.page_size);
          }
          if(index >= 0){
              this.current_page = page;
              if(! silent){
                this.trigger("pageChange");
              }
          }
        },

        // Change the page of the view to a specific page.
        setPage: function(page) {
          if(page < 0) page = 0;
          if(page > Math.ceil(this.match_set.length()/this.page_size - 1)) page = Math.ceil(this.match_set.length()/this.page_size - 1);
          this.current_page = page;
          this.trigger("pageChange");
        },

        // Set the page size.
        setPageSize: function(size){
          this.page_size = size;
          this.trigger("pageChange");
        },
        render: function(){}
      });

      // #MatchSets
      //
      // These are what are returned from queries on filters. They can be chained together with ands, or, & nots.
      // They also keep a "stack" to remember how they were created (after chaining) so that they can refresh themselves.
      PourOver.MatchSet = function(cids,collection,stack){
        this.cids = cids;
        this.collection = collection;
        this.stack = stack;
        this.initialize.apply(this, arguments);
      };
      _.extend(PourOver.MatchSet.prototype,PourOver.Events,{
        initialize: function(){},

        // When the underlying data has changed re-evaluate which items are included in this possibily compound result.
        refresh: function(s,match_set){
         if(typeof(s) === "undefined"){var s = this.stack || []}
         if(s.length < 1 && match_set){
           this.cids = match_set.cids;
           return this;
         } else if (s.length < 1){
           this.cids = false;
           return this;
         }

         var step = s[0],
             operation = step[0],
             is_compound = function(c){return _.isString(c) && c.match(/^(or|and|not)$/);};
         if(typeof(operation) === "object"){
           var match_set = operation.getFn(step[1]);
           return this.refresh(_(s).rest(),match_set);
         } else if(operation === "all" || step === "all") {
           var cids = _(this.collection.items).map(function(i){return i.cid;});
           var match_set = new PourOver.MatchSet(cids,this,["all"]);
           return this.refresh(_(s).rest(),match_set)
         } else if(is_compound(operation)) {
             var m = match_set[operation](this.refresh(step[1]));
         } else {
             var m = this.refresh(step[1]);
         }
         return this.refresh(_(s).rest(),m);
        },

        // Intersect this MatchSet with another MatchSet.
        and: function(other_matches){
          if(this.stack.length < 1 && other_matches){
            return other_matches;
          } else if (!other_matches){
            return this;
          } else {
            var set = PourOver.intersect_sorted(this.cids,other_matches.cids);
            return new PourOver.MatchSet(set,this.collection,this.stack.concat([["and",other_matches.stack]]))
          }
        },

        // Union this MatchSet with another MatchSet.
        or: function(other_matches){
          if(this.stack.length < 1 && other_matches){
            return other_matches;
          } else if (!other_matches){
            return this;
          } else {
            var set = PourOver.union_sorted(this.cids,other_matches.cids);
            return new PourOver.MatchSet(set,this.collection,this.stack.concat([["or",other_matches.stack]]));
          }
        },

        // Difference this MatchSet with another MatchSet.
        not: function(other_matches){
          if(this.stack.length < 1 || ! other_matches){
            return this;
          } else {
            var set = PourOver.subtract_sorted(this.cids,other_matches.cids);
            return new PourOver.MatchSet(set,this.collection,this.stack.concat([["not",other_matches.stack]]));
          }
        },

        // Return all the items corresponding to the cids cached on the MatchSet.
        all: function(){ return this.collection.get(this.cids);},

        // Return a slice of the items corresponding to the cids cached on the MatchSet.
        slice: function(s,e){ return this.collection.get(this.cids.slice(s,e)) },

        // Return all the items corresponding to the cids cached on the MatchSet AND sorted by sort s.
        all_sorted: function(s){
          var c = this.all();
          if(s){
            return s.sort(c);
          } else {
            return c;
          }
        },

        // Sort the cached cids.
        all_sorted_cids: function(s){
          var c = this.cids;
          if(s){
            return s.sort(c);
          } else {
            return c;
          }
        },

        // Return how many items comprise this MatchSet.
        length: function(){return this.cids.length}
      });


    // #PourOver.UI
    // PourOver.UI is a simple add-on for creating objects to be rendered as UI elements controlling the
    // state of filters and views.
    PourOver.UI = {}
    PourOver.UI.Element = function(opts){
        if(typeof(opts) === "undefined"){var opts = {}}
        _.extend(this,opts)
        this.initialize.apply(this, arguments);
    }

    _.extend(PourOver.UI.Element.prototype,PourOver.Events,{
      initialize: function(){},
      getMatchSet: function(){
        throw "No get match set function specified"
      },
      getFilterState: function(){
        throw "No get filter state specified";
      },
      template: function(){
        throw "No template specified"
      },
      render: function(){
        var filter_state = this.getFilterState(),
            output = this.template({state:filter_state});
        return output
      },
      // Pass in a MatchSet that only has a single query of a chain of OR'ed queried and receive
      // an array of possibility names that have been selected.
      getSimpleSelectState: function(match_set,s,output){
          if(typeof(match_set) === "undefined" || !match_set || !match_set.stack){return false}
          if(typeof(s) === "undefined"){var s = match_set.stack}
          if(typeof(output) === "undefined"){var output = []}
          if(s.length < 1){
            return output;
          } else if (typeof(s[0][0]) === "object"){
            output.push(s[0][1]);
            return this.getSimpleSelectState(match_set,_(s).rest(),output);
          } else if (s[0][0] === "or"){
            output = output.concat(this.getSimpleSelectState(match_set,s[0][1]));
            return this.getSimpleSelectState(match_set,_(s).rest(),output);
          } else {
            throw "This does not appear to be a valid, simple selectElement stack."
          }
      },
      getIntersectedSelectState: function(match_set,s,output){
          if(typeof(match_set) === "undefined" || !match_set || !match_set.stack){return false}
          if(typeof(s) === "undefined"){var s = match_set.stack}
          if(typeof(output) === "undefined"){var output = []}
          if(s.length < 1){
            return output;
          } else if (typeof(s[0][0]) === "object"){
            output.push(s[0][1]);
            return this.getIntersectedSelectState(match_set,_(s).rest(),output);
          } else if (s[0][0] === "and"){
            output = output.concat(this.getIntersectedSelectState(match_set,s[0][1]));
            return this.getIntersectedSelectState(match_set,_(s).rest(),output);
          } else {
            throw "This does not appear to be a valid, simple selectElement stack."
          }
      },

      // Pass in a MatchSet that is the result of a single, non-compounded range and receive the
      // value of that range.
      getSimpleRangeState: function(match_set){
        if(typeof(match_set) === "undefined" || !match_set || !match_set.stack){return false}
        stack = match_set.stack;
        if(stack.length !== 1 || stack[0][1].length !== 2){throw "The filter specified does not appear to have a simple range stack."}
        return stack[0][1];
      }

      // TODO: Added more UI gets.
    });


    // From Backbone
    // Helper function to correctly set up the prototype chain, for subclasses.
      // Similar to `goog.inherits`, but uses a hash of prototype properties and
      // class properties to be extended.
      PourOver.extend = function(protoProps, staticProps) {
          var parent = this;
          var child;

          // The constructor function for the new subclass is either defined by you
          // (the "constructor" property in your `extend` definition), or defaulted
          // by us to simply call the parent's constructor.
          if (protoProps && _.has(protoProps, 'constructor')) {
            child = protoProps.constructor;
          } else {
            child = function() {
              return parent.apply(this, arguments);
            };
          }

          // Add static properties to the constructor function, if supplied.
          _.extend(child, parent, staticProps);

          // Set the prototype chain to inherit from `parent`, without calling
          // `parent`'s constructor function.
          var Surrogate = function() {
              this.constructor = child;
            };
          Surrogate.prototype = parent.prototype;
          child.prototype = new Surrogate;

          // Add prototype properties (instance properties) to the subclass,
          // if supplied.
          if (protoProps) _.extend(child.prototype, protoProps);

          // Set a convenience property in case the parent's prototype is needed
          // later.
          child.__super__ = parent.prototype;

          return child;
        };


      PourOver.Collection.extend = PourOver.View.extend = PourOver.Filter.extend = PourOver.Sort.extend = PourOver.MatchSet.extend = PourOver.UI.Element.extend = PourOver.extend

      // #Presets

      // A PourOver buffered collection is one that stores some or all of its data as a promise. This is useful in conjunction with a
      // large data set in which you don't want to load all the data at page open.
      PourOver.BufferedCollection = PourOver.Collection.extend({
        buffered_items: {},
        stripFutures: function(item){
          return _(item).reduce(function(m,v,k){if(typeof(v) != "undefined"){m[k] = v} return m},{});
        },

        // Overrides the base get function with one that buffers in whole values from the server
        get: function(cids,raw){
          if(typeof(raw) === "undefined"){var raw = false}
          var items = PourOver.Collection.prototype.get.call(this,cids),
              that = this;
          if(raw){return items;}
          return _(items).map(function(i){
            var guid = i.guid, new_item;
            if(that.buffered_items.hasOwnProperty(guid)){
              return _(that.buffered_items[guid]).extend(that.stripFutures(i));
            } else {
              return i;
            }
          });
        },
        getBy: function(attr_name,vals,sorted,raw){
          if(typeof(raw) === "undefined"){var raw = false}
          var items = PourOver.Collection.prototype.getBy.call(this,attr_name,vals,sorted),
              that = this;
          if(raw){return items;}
          return _(items).map(function(i){
            var guid = i.guid, new_item;
            if(that.buffered_items.hasOwnProperty(guid)){
              return _(that.buffered_items[guid]).extend(that.stripFutures(i));
            } else {
              return i;
            }
          });
        },

        // Retrieve a specific attr of a specific item from the buffer.
        getBufferedValue: function(guid,attr){
          if(this.buffered_items.hasOwnProperty(guid)){
            return this.buffered_items[guid][attr] || false;
          } else {
            return false;
          }
        },

        // Delete all buffered values for items in the collection.
        clearBufferedItems: function (){
          var buffered_items = this.buffered_items;
          for (var p in buffered_items){
            if (buffered_items.hasOwnProperty(p)){
                delete buffered_items[p];
            }
          }
        },

        // When instantiating a buffered collection you must provide this method. This is how a buffered collection
        // knows what URL to fetch new data from.
        getBufferUrl: function(guids){
          throw "You must override getBufferUrl;"
        },
        preprocessItem: function(item){
          return [item["guid"],item]
        },

        // Pull down new data for an array of guids from the server at the URL returned by getBufferUrl. When the request returns,
        // push the new values into the buffer. The deferred object is returned from this method so you can chain additional callbacks
        // onto the resolution such as a render action.
        bufferGuids: function(guids){
          var that = this,
              guids = _(guids).select(function(g){ return g &&  ! that.buffered_items.hasOwnProperty(g);}),
              buffurl = this.getBufferUrl(guids),
              url = buffurl[0],
              jsonpCallback = buffurl[1];
          if(guids.length > 0){
            return $.ajax({
              url: url,
              dataType:'jsonp',
              cache: true
            }).always(function(d){
              if(_.isArray(d)){
                items = _(d).map(_.bind(that.preprocessItem,that));
                _(items).each(function(i){
                  that.buffered_items[i[0]] = i[1];
                });
              }
            });
          } else {
            return $.Deferred().resolve(false);
          }
        }
      });

      // A buffered view is the pair to a buffered collection. It calls the appropriate buffering methods of the buffered collection
      // so that you automatically get the benefits of buffering as you are paging through the view. If you use a buffered view with a buffered collection
      // you shouldn't need to call the buffering methods of the collection explicitly.
      PourOver.BufferedView = PourOver.View.extend({
        buffer_pages: 1,
        bufferAroundCurrentPage: function(){
          var current_page = this.current_page,
              low_bound = current_page - this.buffer_pages > 0 ? current_page - this.buffer_pages : 0,
              high_bound = current_page + this.buffer_pages,
              range = _.range(low_bound,high_bound + 1),
              that = this;
          range = _(range).map(function(page){
            return _(that.getCurrentItems(page)).pluck("guid");
          });
          var guids = _.flatten(range);
          buffer_deferred = this.collection.bufferGuids(guids);
          buffer_deferred.done(function(d){
              if(d){
                that.render();
              }
          })
        },
        bufferRender: function(){
          var guids = _(this.getCurrentItems()).pluck('guid'),
              buffer_deferred = this.collection.bufferGuids(guids);
          buffer_deferred.done(_(function(){
            this.render()
          }).bind(this));
        },
        page: function(dir){
          PourOver.View.prototype.page.call(this,dir);
          this.bufferAroundCurrentPage();
        },
        pageTo: function(cid,silent){
          if(typeof(silent) === "undefined"){
            var silent = false;
          }
          PourOver.View.prototype.pageTo.call(this,cid,silent);
          this.bufferAroundCurrentPage();
        }
      })

      // ## Filter defaults
      //
      // A strange filter that selects items based on an explicit list of cids. This is useful when you want to use PourOver in association
      // with, say, an editorially composed list of items or any mechanic in which you can "select" items to be included in a filter independent of
      // any attribute.
      PourOver.manualFilter = PourOver.Filter.extend({
        cacheResults: function(){return false},
        addCacheResults: function(){return false},
        getFn: function(query){
          if(_(query).isArray()){
            query = query.sort(function(a,b){return a - b})
            return new PourOver.MatchSet(query,this.getCollection(),[[this,query]]);
          } else if (typeof(query) === "number") {
            return new PourOver.MatchSet([query],this.getCollection(),[[this,query]]);
          } else {
            throw "Manual filters only support querying by one or more cids"
          }
        },
        addItems: function(cids){
          if(! _(cids).isArray()){cids = [cids]}
          cids = cids.sort(function(a,b){return a - b});
          if(this.current_query){
            var current_query = this.current_query.cids,
                new_query = PourOver.union_sorted(current_query,cids);
          } else {
            var new_query = cids;
          }
          this.query(new_query);
        },
        removeItems: function(cids){
          if(! _(cids).isArray()){cids = [cids]}
          cids = cids.sort(function(a,b){return a - b});
          var current_query = this.current_query.cids,
              new_query = PourOver.subtract_sorted(current_query,cids);
          this.query(new_query);
        }
      });

      // The convenience constructor for manual filters.
      PourOver.makeManualFilter = function(name){
        var filter = new PourOver.manualFilter(name,[]);
        return filter;
      }

      // An exact filter is the most commonly used filter. Given an attribute and a list of possibilities, an exact filter will bucket the items
      // into those satisfying each of the possibilities. This also has the fastest peformance as far as creating and updating.
      PourOver.exactFilter = PourOver.Filter.extend({
        cacheResults: PourOver.cacheMethods.exactCache,
        addCacheResults: PourOver.cacheMethods.exactAddCache,
        getFn: function(query){
          var that = this;
          if(_(query).isArray()){
            var match_set = _(query).reduce(function(m,i){
              if(!m){
                return that.getFn(i);
              } else {
                return m.or(that.getFn(i));
              }
            },false);
            return match_set;
          } else {
            var possibility = this.possibilities[query];
            if (_.isUndefined(possibility) ) throw "The filter " + this.name + " does not have a match for the query '" + query + "'.";
            return new PourOver.MatchSet(possibility.matching_cids,this.getCollection(),[[this,query]]);
          }
        }
      });

      // The convenience constructor for exact filters.
      PourOver.makeExactFilter = function(name,values,opts){
        if(typeof(opts) === "undefined"){opts = {}}
        var values = _(values).map(function(i){return {value:i}}),
            attr = opts.attr || name,
            opts = _.extend({associated_attrs: [attr]},opts),
            filter = new PourOver.exactFilter(name,values,opts);
        return filter;
      }

      PourOver.inclusionFilter = PourOver.exactFilter.extend({
        cacheResults: PourOver.cacheMethods.inclusionCache,
        addCacheResults: PourOver.cacheMethods.inclusionAddCache
      });

      PourOver.makeInclusionFilter = function(name,values,opts){
        if(typeof(opts) === "undefined"){opts = {}}
        var values = _(values).map(function(i){return {value:i}}),
            attr = opts.attr || name,
            opts = _.extend({associated_attrs: [attr]},opts),
            filter = new PourOver.inclusionFilter(name,values,opts);
        return filter;
      };

      // A range filter is for dividing items into buckets of ranges based on a specific attribute. A good example is, say, each item as 0-1000 "friends", then you can
      // supply a range filter with the possibilities: [[0,10],[11,100],[101,1000]] and it will create buckets for 0-10, 11-100, and 101 + friends.
      PourOver.rangeFilter = PourOver.Filter.extend({
        cacheResults: PourOver.cacheMethods.defaultCache,
        addCacheResults: PourOver.cacheMethods.defaultAddCache,
        fn: function(possibility,item){
          var attr = this.attr || this.name;
          return possibility.low <= item[attr] && possibility.high >= item[attr]
        },
        getFn: function(query){
          var possibility = this.possibilities[query.join("-")];
          if (_.isUndefined(possibility) ) throw "The filter " + this.name + " does not have a match for the query '" + query + "'.";
          return new PourOver.MatchSet(possibility.matching_cids,this.getCollection(),[[this,query]]);
        }
      });

      // The convenience constructor for range filters.
      PourOver.makeRangeFilter = function(name,ranges,opts){
        if(typeof(opts) === "undefined"){opts = {}}
        var values = _(ranges).map(function(r){return {low: r[0], high: r[1], value: r.join("-")}}),
            attr = opts.attr || name,
            newopts = _.extend({associated_attrs: [attr]},opts),
            filter = new PourOver.rangeFilter(name,values,newopts);
        return filter;
      }

      // The inverse of a range filter. Again each item has single value for a certain attribute, but the possibilities you provide are every value of that attribute.
      // Then, you query by a range. So, if a person can have 1-10 hats, you would feed a dv range filter the possibilities [1,2,3,4,5,6,7,8,9,10] and then make
      // queries such as [2,5] for 2-5 hats. Do not use this for huge ranges like 1-100. Use crossfilter or write some optimized way of doing this. PourOver is not optimized
      // for that kind of continuous query.
      PourOver.dvrangeFilter = PourOver.Filter.extend({
        cacheResults:  PourOver.cacheMethods.exactCache,
        addCacheResults: PourOver.cacheMethods.exactAddCache,
        getFn: function(query){
          if(! query[0] || ! query[1]){
            return new PourOver.MatchSet([],this.getCollection(),[[this,query]]);
          }
          var li,hi,that,possibilities,cids;
          li = _(this.values).indexOf(query[0]);
          hi = _(this.values).indexOf(query[1]);
          that = this;
          possibilities = _(this.values.slice(li,hi+1)).map(function(p){return that.possibilities[p]});
          cids = _(possibilities).reduce(function(m,i){ return PourOver.union_sorted(m,i.matching_cids) },[]);
          return new PourOver.MatchSet(cids,this.getCollection(),[[this,query]]);
        }
      });

      // The convenience constructor for dv range filters.
      PourOver.makeDVrangeFilter = function(name,v,opts){
        if(typeof(opts) === "undefined"){opts = {}}
        var values = _(v).map(function(i){return {value:i}}),
            attr = opts.attr || name,
            newopts = _.extend({associated_attrs: [attr]},opts),
            filter = new PourOver.dvrangeFilter(name,values,newopts);
        return filter
      }

      // Filter for data with a continuous range or many possible values, such as dates, floats, etc.
      // Query with a scalar to query by exact value, or query with a length-2 array to
      // query a range (as in dvrangeFilter) such that the value is greater than or equal 
      // to range[0] and less than range[1].
      PourOver.continuousRangeFilter = PourOver.Filter.extend({
        cacheResults: function(items){
          this.values = _.map(items, function(i) { return {cid: i.cid, val: i[this.name]}; }, this);
          this.values.sort(function(a,b) { return a.val-b.val });
        },
        addCacheResults: function(items){
          this.values = this.values.concat(items);
          this.values.sort(function(a,b) { return a.val-b.val });
        },
        getFn: function(query){
          var li,hi;
          var n = this.values.length;

          var bisect = PourOver.bisect_by( function(a) { return a.val });
          
          if(_.isArray(query)){
            // range filter
            if(_.isUndefined(query[0]) || _.isUndefined(query[1])){
              return new PourOver.MatchSet([],this.getCollection(),[[this,query]]);
            }
            li = bisect.left(this.values, query[0], 0, n);
            hi = bisect.left(this.values, query[1], 0, n);
          } else {
            // exact filter
            if(_.isUndefined(query)){
              return new PourOver.MatchSet([],this.getCollection(),[[this,query]]);
            }

            li = bisect.left(this.values, query, 0, n);
            hi = bisect.right(this.values, query, 0, n);
          }

          var cids = [];
          var i=li;
          while(i<hi) {
            cids.push(this.values[i].cid);
            ++i;
          }
          cids.sort(function(a,b) { return a-b });
          return new PourOver.MatchSet(cids,this.getCollection(),[[this,query]]);
        }
      });

      // The convenience constructor for continuous range filters.
      PourOver.makeContinuousRangeFilter = function(name,opts){
        if(typeof(opts) === "undefined"){opts = {}}
        var attr = opts.attr || name,
            newopts = _.extend({associated_attrs: [attr]},opts),
            filter = new PourOver.continuousRangeFilter(name,newopts);
        return filter;
      }

      // ## Preset sorts
      //
      // Sorts items based on an explicit ordering of values. This would be useful for, say, a slideshow in which
      // the order of items has nothing to do with any of their filterable attributes. Comes with methods to reorganize
      // the items in the sort.
      PourOver.explicitSort = PourOver.Sort.extend({
        fn: function(a,b){
          var a_index = _(this.order).indexOf(a[this.attr]),
              b_index = _(this.order).indexOf(b[this.attr]);
          if(a_index === -1) {a_index = 1/0}
          if(b_index === -1) {b_index = 1/0}
          return a_index - b_index;
        },
        reset: function(items){
          this.order = _(items).pluck(this.attr);
          this.rebuild_sort();
        },

        // Insert an item into the sort.
        insert: function(items,index){
          if(typeof(index) === "undefined"){var index = this.order.length}
          if(! _(items).isArray()){items = [items]}
          var new_order = _(items).pluck(this.attr),
              args = [index,0].concat(new_order);
          this.order.splice.apply(this.order,args);
          this.rebuild_sort();
        },

        // Remove an item from the sort.
        remove: function(items){
          if(! _(items).isArray()){items = [items]}
          var attrs = _(items).pluck(this.attr);
          this.order = _(this.order).difference(attrs);
          this.rebuild_sort();
        },

        // Move an item from one place to another in the sort.
        move: function(items,index){
          if(! _(items).isArray()){items = [items]}
          var attrs = _(items).pluck(this.attr);
          this.order = _(this.order).map(function(o){ return _(attrs).include(o) ? null : o });
          this.insert(items,index);
          this.order = _(this.order).compact();
        }
      });

      // The convenience constructor for an explicit sort.
      PourOver.makeExplicitSort = function(name,collection,attr,order,opts){
        var sort = new PourOver.explicitSort(name,opts);
        sort.attr = attr;
        sort.order = order;
        return sort;
      }

      PourOver.reverseCidSort = PourOver.Sort.extend({
        fn: function(a,b){
            return b.cid - a.cid;
        }
      })

      PourOver.makeReverseCidSort = function(name,collection){
        var sort = new PourOver.reverseCidSort(name);
        sort.attr = "cid";
        return sort;
      }

      // ## Preset UI elements
      //
      // A simple select element is roughly equavalent to elements such as a checklist or a radio list.
      // Items can only be unioned together. One or more selected.
      PourOver.UI.SimpleSelectElement = PourOver.UI.Element.extend({
        initialize: function(opts){
          if(!opts.filter){throw "A simple select element must have a filter specified"}
          this.filter = opts.filter;
        },
        getMatchSet: function(){
          return this.filter.current_query;
        },
        getFilterState: function(){
          var match_set = this.getMatchSet();
          return this.getSimpleSelectState(match_set)
        }
      });

      // A dv range element can be used for a slider in which you set the low and high and the query
      // corresponds to everything in between.
      PourOver.UI.SimpleDVRangeElement = PourOver.UI.Element.extend({
        initialize: function(opts){
          if(!opts.filter){throw "A simple dv range element must have a filter specified"}
          this.filter = opts.filter;
        },
        getMatchSet: function(){
          return this.filter.current_query;
        },
        getFilterState: function(){
          var match_set = this.getMatchSet();
          return this.getSimpleRangeState(match_set)
        }
      });

    return PourOver;
})();


// Functions

function generate(maxEntries, maxAttrs, vocab) {
  var test = [];

  while (maxEntries--) {
    test.push(generateEntry(maxAttrs, vocab))
  }

  return test;
}

function generateEntry(maxAttrs, vocab) {
  var obj = {},
    key;

  function insertJunk(obj, vocab) {
    var key = '$$KEY:' + choose(vocab);
    if (obj[key]) {
      insertJunk(obj, vocab);
      return;
    } else {
      obj[key] = '##DATA:' + choose(vocab);
    }
  }

  while (maxAttrs--) {
    insertJunk(obj, vocab);
  }

  return obj;
}

function choose(vocab) {
  var index;
  index = Math.floor(Math.random() * vocab.length);
  return vocab[index];
}

function makeWord(i) {
  return i;
}

function generateVocab(vocabSize) {
  var vocab = [];
  while (vocabSize--) {
    vocab.push(vocabSize);
  }
  return vocab;
}

var vocabSize = 1000, maxAttrs = 200, maxEntries = 3000;
  vocab = generateVocab(vocabSize);

var testSet1 = generate(3000, maxAttrs, vocab);
var testSet2 = generate(10000, maxAttrs, vocab);
var testSet3 = generate(30000, maxAttrs, vocab);

var first = function() {
    collection = new PourOver.Collection(testSet1);
}

var second = function() {
    collection = new PourOver.Collection(testSet2);
}

var third = function() {
    collection = new PourOver.Collection(testSet3);
}


ui.benchmarks[0].setup = first
ui.benchmarks[1].setup = first
ui.benchmarks[2].setup = first
ui.benchmarks[3].setup = first
ui.benchmarks[4].setup = first

///10000
ui.benchmarks[5].setup = second
ui.benchmarks[6].setup = second
ui.benchmarks[7].setup = second
ui.benchmarks[8].setup = second
ui.benchmarks[9].setup = second


//30000
ui.benchmarks[11].setup = third
ui.benchmarks[10].setup = third
ui.benchmarks[12].setup = third
ui.benchmarks[13].setup = third
ui.benchmarks[14].setup = third

</script>

Teardown


    collection = {};
    testSet = {};
    
  

Test runner

Ready to run.

Testing in
TestOps/sec
Update Item (3000)
collection.updateItem(Math.floor(Math.random() * maxEntries), vocab[0], "margot");

collection.removeItems(Math.floor(Math.random() * maxEntries));
ready
Filter (3000)
// Test 2:
var view = new PourOver.View("default_view", collection);

var exact_filter = PourOver.makeExactFilter("$$KEY:4", ['##DATA:6', '##DATA:1', '##DATA:2']);
var inclusion_filter = PourOver.makeInclusionFilter("$$KEY:10",["##DATA:6", "##DATA:1", "##DATA:2", "##DATA:3", "##DATA:4" ]);
collection.addFilters([exact_filter, inclusion_filter]);

view.getCurrentItems();

 
ready
Sort (3000)
var RevKeySort = PourOver.Sort.extend({
    attr: "$$KEY:4",
    fn: function(a,b){
        if (b < a){
          return -1;
        } else if (b > a){
          return 1;
        } else {
          return 0;
        }
    }
});
var rev_key_sort = new RevKeySort("rev_key");
 
collection.addSorts([rev_key_sort]);

var view = new PourOver.View("default_view", collection);
view.setSort("rev_key");
view.getCurrentItems();
ready
Custom View (3000)
var exact_filter = PourOver.makeExactFilter("$$KEY:4", ['##DATA:6', '##DATA:1', '##DATA:2']);
var inclusion_filter = PourOver.makeInclusionFilter("$$KEY:10",["##DATA:6", "##DATA:1", "##DATA:2", "##DATA:3", "##DATA:4" ]);
collection.addFilters([exact_filter, inclusion_filter]);

var CustomView = PourOver.View.extend({
    selectionFn: function(){
        var exact_query = collection.filters.exact_filter.current_query,
           inclusion_query  = collection.filters.inclusion_filter.current_query;
        return inclusion_query.not(exact_query);
    },
    render: function(){
        current_items = this.getCurrentItems();
    }
})
var custom_view = new CustomView("custom_view",collection);
custom_view.render();
ready
Attach 50 Filters, Render 51 Views (3000)
var filters = [];

for (var i = 0; i < 50; i++) {
   filters.push(PourOver.makeExactFilter(("$$KEY:" + i), ['##DATA:1', '##DATA:2', '##DATA:3', '##DATA:4', '##DATA:5', '##DATA:6']));
}

collection.addFilters(filters);
var view = new PourOver.View("default_view", collection);

for (var i = 0; i < 50; i++) {
   collection.filters['$$KEY:'+i].query("##DATA:1");
   view.getCurrentItems();
}

view.getCurrentItems();

 
ready
Insert (10000)
collection.updateItem(Math.floor(Math.random() * maxEntries), vocab[0], "margot");

collection.removeItems(Math.floor(Math.random() * maxEntries));
ready
Filter (10000)
// Test 2:
var view = new PourOver.View("default_view", collection);

var exact_filter = PourOver.makeExactFilter("$$KEY:4", ['##DATA:6', '##DATA:1', '##DATA:2']);
var inclusion_filter = PourOver.makeInclusionFilter("$$KEY:10",["##DATA:6", "##DATA:1", "##DATA:2", "##DATA:3", "##DATA:4" ]);
collection.addFilters([exact_filter, inclusion_filter]);

view.getCurrentItems();

 
ready
Sort (10000)
var RevKeySort = PourOver.Sort.extend({
    attr: "$$KEY:4",
    fn: function(a,b){
        if (b < a){
          return -1;
        } else if (b > a){
          return 1;
        } else {
          return 0;
        }
    }
});
var rev_key_sort = new RevKeySort("rev_key");
 
collection.addSorts([rev_key_sort]);

var view = new PourOver.View("default_view", collection);
view.setSort("rev_key");
view.getCurrentItems();
 
ready
Custom View (10000)
var exact_filter = PourOver.makeExactFilter("$$KEY:4", ['##DATA:6', '##DATA:1', '##DATA:2']);
var inclusion_filter = PourOver.makeInclusionFilter("$$KEY:10",["##DATA:6", "##DATA:1", "##DATA:2", "##DATA:3", "##DATA:4" ]);
collection.addFilters([exact_filter, inclusion_filter]);

var CustomView = PourOver.View.extend({
    selectionFn: function(){
        var exact_query = collection.filters.exact_filter.current_query,
           inclusion_query  = collection.filters.inclusion_filter.current_query;
        return inclusion_query.not(exact_query);
    },
    render: function(){
        current_items = this.getCurrentItems();
    }
})
var custom_view = new CustomView("custom_view",collection);
custom_view.render();
ready
Attach 50 Filters, Render 51 Views (10000)
var filters = [];

for (var i = 0; i < 50; i++) {
   filters.push(PourOver.makeExactFilter(("$$KEY:" + i), ['##DATA:1', '##DATA:2', '##DATA:3', '##DATA:4', '##DATA:5', '##DATA:6']));
}

collection.addFilters(filters);
var view = new PourOver.View("default_view", collection);

for (var i = 0; i < 50; i++) {
   collection.filters['$$KEY:'+i].query("##DATA:1");
   view.getCurrentItems();
}

view.getCurrentItems();
 
ready
Insert (30,000)
collection.updateItem(Math.floor(Math.random() * maxEntries), vocab[0], "margot");

collection.removeItems(Math.floor(Math.random() * maxEntries));
ready
Filter (30,000)
// Test 2:
var view = new PourOver.View("default_view", collection);

var exact_filter = PourOver.makeExactFilter("$$KEY:4", ['##DATA:6', '##DATA:1', '##DATA:2']);
var inclusion_filter = PourOver.makeInclusionFilter("$$KEY:10",["##DATA:6", "##DATA:1", "##DATA:2", "##DATA:3", "##DATA:4" ]);
collection.addFilters([exact_filter, inclusion_filter]);

view.getCurrentItems();
 
ready
Sort (30,000)
var RevKeySort = PourOver.Sort.extend({
    attr: "$$KEY:4",
    fn: function(a,b){
        if (b < a){
          return -1;
        } else if (b > a){
          return 1;
        } else {
          return 0;
        }
    }
});
var rev_key_sort = new RevKeySort("rev_key");
 
collection.addSorts([rev_key_sort]);

var view = new PourOver.View("default_view", collection);
view.setSort("rev_key");
view.getCurrentItems();
 
ready
Custom View (30,000)
var exact_filter = PourOver.makeExactFilter("$$KEY:4", ['##DATA:6', '##DATA:1', '##DATA:2']);
var inclusion_filter = PourOver.makeInclusionFilter("$$KEY:10",["##DATA:6", "##DATA:1", "##DATA:2", "##DATA:3", "##DATA:4" ]);
collection.addFilters([exact_filter, inclusion_filter]);

var CustomView = PourOver.View.extend({
    selectionFn: function(){
        var exact_query = collection.filters.exact_filter.current_query,
           inclusion_query  = collection.filters.inclusion_filter.current_query;
        return inclusion_query.not(exact_query);
    },
    render: function(){
        current_items = this.getCurrentItems();
    }
})
var custom_view = new CustomView("custom_view",collection);
custom_view.render();
ready
Attach 50 Filters, Render 51 Views (30,000)
var filters = [];

for (var i = 0; i < 50; i++) {
   filters.push(PourOver.makeExactFilter(("$$KEY:" + i), ['##DATA:1', '##DATA:2', '##DATA:3', '##DATA:4', '##DATA:5', '##DATA:6']));
}

collection.addFilters(filters);
var view = new PourOver.View("default_view", collection);

for (var i = 0; i < 50; i++) {
   collection.filters['$$KEY:'+i].query("##DATA:1");
   view.getCurrentItems();
}

view.getCurrentItems();
 
ready

Revisions

You can edit these tests or add more tests to this page by appending /edit to the URL.

  • Revision 1: published by Pourover Test on