baja/sys/structures/FilterCursor.js

/**
 * @copyright 2015 Tridium, Inc. All Rights Reserved.
 * @author Gareth Johnson
 */

/**
 * Defines {@link baja.FilterCursor}.
 * @module baja/sys/structures/FilterCursor
 */
define([ "bajaScript/baja/sys/bajaUtils",
        "bajaScript/baja/sys/inherit",
        "bajaScript/baja/sys/structures/SyncCursor" ], function (
         bajaUtils,
         inherit,
         SyncCursor) {
  
  "use strict";
  
  var subclass = inherit.subclass,
      callSuper = inherit.callSuper,
      strictArg = bajaUtils.strictArg;
  
  /** 
   * A filtered cursor used for iteration.
   * 
   * This Cursor is a generic Cursor used for iteration in a {@link baja.OrderedMap}.
   *
   * @class
   * @alias baja.FilterCursor
   * @extends baja.SyncCursor
   * @see baja.OrderedMap
   * 
   * @param {Object} context the context to bind to `this` in cursor operations.
   * @param {baja.OrderedMap} orderedMap the ordered map to iterate over
   */  
  var FilterCursor = function FilterCursor(context, orderedMap) {
    callSuper(FilterCursor, this, arguments);
    this.$context = context;
    this.$orderedMap = orderedMap;
    this.$keys = orderedMap && orderedMap.getKeys();
    this.$filter = null;
    this.$index = -1;
  };
  
  subclass(FilterCursor, SyncCursor);

  function filterNext(cursor) {
    if (cursor.$index < cursor.$keys.length) {
      ++cursor.$index;
      return cursor.$index !== cursor.$keys.length;
    }
    
    return false;
  }
  
  /**
   * Advance cursor and return true if successful.
   *
   * @returns {Boolean}
   */
  FilterCursor.prototype.next = function () {
    var that = this;
  
    if (!that.$filter) {
      return filterNext(this);
    }
    // If a Constructor has been passed in then keep iterating
    // until we find a matching element
    do {
      if (!filterNext(this)) {
        return false;
      }
    } while (!that.$filter.call(that.$context, that.get()));
    
    return true;
  };

  /**
   * Return the current item. If this is a 
   * {@link module:baja/comp/SlotCursor SlotCursor}, this will return a 
   * {@link baja.Slot Slot}.
   * 
   * @returns the cursor value (null if none available).
   */
  FilterCursor.prototype.get = function () {
    var x = this.$index,
        keys = this.$keys;
    
    if (x === -1 || x >= keys.length) {
      return null;
    }
    
    return this.$orderedMap.get(keys[x]);
  };    

  /**
   * Return the current key.
   * 
   * This is a private method and shouldn't be used by non-Tridium developers.
   *
   * @private
   * 
   * @returns {String} the cursor key (null if none available).
   */
  FilterCursor.prototype.getKey = function () {
    var x = this.$index,
        keys = this.$keys;
    
    if (x === -1 || x >= keys.length) {
      return null;
    }
    
    return keys[x];
  }; 

  /**
   * Return the current index.
   * 
   * This is a private method and shouldn't be used by non-Tridium developers.
   *
   * @private
   * 
   * @returns {Number} the cursor index (null if none available).
   */
  FilterCursor.prototype.getIndex = function () {
    return this.$index;
  }; 

  /**
   * Iterate through the Cursor and call a function on every item.
   * 
   * When the function is called, `this` refers to the `context` that
   * was passed in when the Cursor was created.
   * 
   * @param {Function} func function called on every iteration with the 'value' being used as an argument.
   *                        If this is a Slot Cursor the 'value' will be a Slot.
   */
  FilterCursor.prototype.each = function (func) {
    strictArg(func, Function);

    var result;
    while (this.next()) {
      result = func.call(this.$context, this.get(), this.getIndex());

      if (result) {
        return result;
      }
    }
  };
      
  /**
   * Return true if the Cursor is completely empty (regardless of iterative state).
   *
   * @returns {Boolean}
   */
  FilterCursor.prototype.isEmpty = function () {
    // Note the old cursor index
    var oldX = this.$index;
    
    // Set the cursor back to the start
    this.$index = -1;
    
    // See if we have any valid entries from the start
    var res = this.next(); 
    
    // Restore the old cursor index
    this.$index = oldX;
    
    // Return the result of our search
    return !res;
  };
      
  /**
   * Return an array of the cursor results (regardless of iterative state).
   * If this is a Slot Cursor, this will be an array of Slots.
   *
   * @returns {Array}
   */
  FilterCursor.prototype.toArray = function () {
    // Note the old cursor index
    var oldX = this.$index,
        a = [];
        
    this.$index = -1;
    
    // Iterate through and fill up the array
    while (this.next()) {
      a.push(this.get());
    }
    
    // Restore the old cursor index
    this.$index = oldX;
    
    return a;
  };
  
  /**
   * Return an Object Map of keys with their corresponding values. 
   * If this is a Slot Cursor, this will be a Map of Slot names with their 
   * corresponding Slots (regardless of iterative state).
   *
   * @returns {Object}
   */  
  FilterCursor.prototype.toMap = function () {
    var slots = this.toArray(),
        map = {},
        s,
        i;
    
    for (i = 0; i < slots.length; ++i) {
      s = slots[i];
      map[s.getName()] = s;
    }
            
    return map;
  };
      
  /**
   * Return the size of the cursor (regardless of iterative state).
   *
   * @returns {Number}
   */
  FilterCursor.prototype.getSize = function () {
    // Note the old cursor index
    var oldX = this.$index,
        count = 0;
    
    this.$index = -1;
    
    // Iterate through and fill up the array
    while (this.next()) {
      ++count;
    }
    
    // Restore the old cursor index
    this.$index = oldX;
    
    return count;
  };
  
  /**
   * Add a filter function to the Cursor.
   *
   * @param {Function} filter used to filter the results of the Cursor.
   *                          When invoked, the first argument will be the item to filter (i.e. a Slot).
   *                          This function must return a true value for the item to be kept.                 
   * @returns {baja.FilterCursor} itself.
   */
  FilterCursor.prototype.filter = function (filter) {
    if (!this.$filter) {
      this.$filter = filter;
    } else {
      var oldFilter = this.$filter;
      
      // Merge the filter functions together
      this.$filter = function (val) {
        return oldFilter.call(this, val) && filter.call(this, val);
      };
    }
    return this;
  };
  
  /**
   * Return the first item in the cursor (regardless of iterative state).
   * 
   * If this is being used as a Slot Cursor, the Slot will be returned.
   *
   * @returns first item found in the Cursor (or null if nothing found).
   */
  FilterCursor.prototype.first = function () {
    // Note the old cursor index
    var oldX = this.$index,
        val = null;
        
    this.$index = -1;
    
    // Iterate through and fill up the array
    if (this.next()) {
      val = this.get();
    }
     
    // Restore the old cursor index
    this.$index = oldX;
    
    return val;
  };
  
  /**
   * Return the last item in the cursor (regardless of iterative state).
   * 
   * If this is being used as a Slot Cursor, the Slot will be returned.
   *
   * @returns last item found in the Cursor (or null if nothing found).
   */
  FilterCursor.prototype.last = function () {
    // Note the old cursor index
    var oldX = this.$index,
        val = null,
        index = this.$keys.length - 2;

    while (val === null && index >= -1) {
      this.$index = index;
      if (this.next()) {
        val = this.get();
      }
      index = index - 2;
    }
     
    // Restore the old cursor index
    this.$index = oldX;
    
    return val;
  };  

  return FilterCursor;
});