Source: trellinator-libs/IterableCollection.js

/**
* @class IterableCollection
* @memberof module:TrellinatorCore
* @constructor
* @param obj {Object} an object or Array that you would
*                     like to iterate over
* @classdesc The IterableCollection is used as the primary
* collection class for returning groups of objects or
* entities. Whenever you load, for example, an array of
* Card objects, you will get an IterableCollection
* back as the return value.
* @example
* new Trellinator().board("My Board").list("My List")
* .cards().each(function(card)
* {
*     card.postComment("I am card "+card.id());
* });
*/
var IterableCollection = function(obj)
{
    this.obj = obj;

    /**
    * Return a new IterableCollection containing
    * all objects returned from a callback. The
    * callback can modify the object if required,
    * 
    * @memberof module:TrellinatorCore.IterableCollection
    * @param comparator {Function} a callback
    * function to use in order to identify what
    * objects you're looking for
    * @example
    * try
    * {
    *     //using find() instead of transform
    *     //means we will have cached the list of
    *     //cards and won't need to reload if we 
    *     //do another find function later
    *     notif.board().cards().find(function(elem)
    *     {
    *         if(new RegExp("Search.*").test(elem.description()))
    *             return elem;
    *         else
    *             return false;
    *     }).first().postComment("Twinsies!");
    * }
    * 
    * catch(e)
    * {
    *     Notification.expectException(InvalidDataException,e);
    *     Trellinator.log("Looks like: "+my_card.name()+" wasn't found");
    * }
    */
    this.find = function(callback)
    {
        var new_obj = {};

        for(var key in this.obj)
        {
            if((transformed = callback(this.obj[key],key)) !== false)
                new_obj[key] = transformed;
        }
        
        return new IterableCollection(new_obj);
    }

    /**
    * Return a concatenated string of the values
    * in this collection separated by a
    * common separator, optionally with each
    * value being augmented by a callback, similar to
    * Array.join()
    * @memberof module:TrellinatorCore.IterableCollection
    * @param separator {string} (optional) the string to
    *        separate each value in the return string, 
    *        defaults to &
    * @param callback {Function} (optional) a function
    *        to augment each value before concatenating
    * @example
    * //prints "one","two"
    * console.log('"'+new IterableCollection({one: "one",two: "two"})
    * .implodeValues(",",function(elem)
    * {
    *     return '"'+elem+'"';
    * })+'"');
    * @example
    * //prints a semi-colon separated list of card names
    * console.log(notif.board().cards().implode(";",function(card)
    * {
    *     return card.name();
    * }));
    */
    this.implodeValues = function(separator,callback)
    {
        if(!separator)
            separator = "&";

        if(!callback)
        {
            callback = function(elem,key)
            {
                return elem;
            };
        }

        var ret = "";

        for(var key in this.obj)
        {
            var called = callback(this.obj[key],key);
            
            if(!ret)
                ret = called;
            else
                ret += called ? separator+callback(this.obj[key],key):called;
        }
        
        return ret;
    }

    /**
    * Concatenate all the items in a collection in
    * key=value pairs separated by a separator,
    * optionally with each value being augmented
    * by a callback (basically for creating query strings)
    * @memberof module:TrellinatorCore.IterableCollection
    * @param separator {string} (optional) the separator
    *        to be included between each key=value pair
    *        defaults to &
    * @param callback {Function} (optional) a callback to 
    *        be used to augment the value in each key=value
    *        pair
    * @example
    * var params = {name: "Kieth",
    *               strengths: "Accounts",
    *               weaknesses: "Eczema"};
    * HttpApi.call("post",base_url+params.implode("&",function(elem)
    * {
    *     return encodeURIComponent(elem);
    * }));
    */
    this.implode = function(separator,callback)
    {
        if(!separator)
            separator = "&";

        if(!callback)
        {
            callback = function(elem,key)
            {
                return elem;
            };
        }

        var ret = "";

        for(var key in this.obj)
        {
            if(!ret)
                ret = key+"="+callback(this.obj[key],key);
            else
                ret += separator+key+"="+callback(this.obj[key],key);
        }
        
        return ret;
    }

    /**
    * Return the item in this collection that appears
    * before a given item, identified by an expression,
    * optionally passing in a callback function used
    * to compare the element with the expression.
    * 
    * By default the expression can be a string or a 
    * RegExp and the comparison will be done based
    * on calling the name() method of the element.
    * 
    * A common use case is to find the previous list
    * in a Trello board that appears after a list
    * with a given name.
    * 
    * @memberof module:TrellinatorCore.IterableCollection
    * @param expression {string|RegExp} a string or RegExp
    * indicating the element before which you'd like to find
    * the previous item.
    * @param inspector {Function} (optional) a callback 
    * used to compare the items to the expression. This
    * should accept 2 parameters:
    *  - test {string|RegExp} The expression that you passed in
    *    to the original function
    *  - elem {Object} the item from the collection to be
    *    compared to the test expression
    * the function should return true if the element matches
    * the expression.
    * @example
    * var prev_list = notif.board().lists().itemBefore(new RegExp("Inbox.*"));
    * @example
    * var prev_card = notif.board().list("Test")
    * .cards().itemBefore(my_card.id(),function(test,elem)
    * {
    *     return test == elem.id();
    * });
    * @throws InvalidDataException
    */
    this.itemBefore = function(expression,inspector)
    {
      var ret         = null;
      var prev = null;
      var next = null;
      var return_prev = false;
      
      if(!inspector)
      {
        inspector = function(test,elem)
        {
          return TrelloApi.nameTest(TrelloApi.nameTestData(test),elem.name());
        };
      }
      
      if(expression)
      {
        for(var key in this.obj)
        {
          if(return_prev)
          {
            ret = prev;
            prev = next;
            return_prev = false;
          }
          
          if(inspector(expression,this.obj[key]))
          {
            return_prev = true;
            next = this.obj[key];
          }
          
          else
          {
            prev = this.obj[key];
          }
        }
      }
      
      if(return_prev)
      {
        ret = prev;
        return_prev = false;
      }
      
      if(!ret)
        throw new InvalidDataException("There was no item before: "+expression);
      
      return ret;
    }
    
    /**
    * Return the item in this collection that appears
    * after a given item, identified by an expression,
    * optionally passing in a callback function used
    * to compare the element with the expression.
    * 
    * By default the expression can be a string or a 
    * RegExp and the comparison will be done based
    * on calling the name() method of the element.
    * 
    * A common use case is to find the next list
    * in a Trello board that appears after a list
    * with a given name.
    * 
    * @memberof module:TrellinatorCore.IterableCollection
    * @param expression {string|RegExp} a string or RegExp
    * indicating the element after which you'd like to find
    * the next item.
    * @param inspector {Function} (optional) a callback 
    * used to compare the items to the expression. This
    * should accept 2 parameters:
    *  - test {string} The expression that you passed in
    *    to the original function
    *  - elem {Object} the item from the collection to be
    *    compared to the test expression
    * the function should return true if the element matches
    * the expression.
    * @example
    * var next_list = notif.board().lists().itemAfter(new RegExp("Inbox.*"));
    * @example
    * var next_card = notif.board().list("Test")
    * .cards().itemAfter(my_card.id(),function(test,elem)
    * {
    *     return test == elem.id();
    * });
    * @throws InvalidDataException
    */
    this.itemAfter = function(expression,inspector)
    {
        var ret         = null;
        var return_next = false;
        
        if(!inspector)
        {
            inspector = function(test,elem)
            {
                return TrelloApi.nameTest(TrelloApi.nameTestData(test),elem.name());
            };
        }
      
        if(expression)
        {
            for(var key in this.obj)
            {
                if(return_next)
                {
                    return_next = false;
                    ret = this.obj[key];
                }

                else if(inspector(expression,this.obj[key]))
                    return_next = true;
            }
        }
        
        if(!ret)
            throw new InvalidDataException("There was no item after: "+expression);
            
        return ret;
    }

    /**
    * Return a random element from this collection
    * @memberof module:TrellinatorCore.IterableCollection
    * @param include {Function} (optional) a callback that returns true if an object should be considered, false if not
    * @throws InvalidDataException
    */
    this.random = function(include)
    {
        if(!this.length())
            throw new InvalidDataException("No items in IterableCollection when selecting a random element");

        if(include)
        {
            var ret = false;
            
            while(!ret)
            {
                var keys = Object.keys(this.obj)
                var randkey = keys[ keys.length * Math.random() << 0];
                var rand = this.obj[randkey];
                
                if(include(rand))
                   ret = rand;
            }
            
        }
        
        else
        {
            var keys = Object.keys(this.obj)
            var randkey = keys[ keys.length * Math.random() << 0];
            var ret = this.obj[randkey];
        }

        return ret;
    }

    /**
    * Remove and return a random element from this collection
    * @memberof module:TrellinatorCore.IterableCollection
    * @param include {Function} (optional) a callback that returns true if an object should be considered, false if not
    * @throws InvalidDataException
    */
    this.removeRandom = function(include)
    {
        if(!this.length())
            throw new InvalidDataException("No items in IterableCollection when selecting a random element");

        if(include)
        {
            var ret = false;
            
            while(!ret)
            {
                var keys = Object.keys(this.obj)
                var randkey = keys[ keys.length * Math.random() << 0];
                var rand = this.obj[randkey];
                
                if(include(rand))
                   ret = rand;
            }
            
        }
        
        else
        {
            var keys = Object.keys(this.obj)
            var randkey = keys[ keys.length * Math.random() << 0];
            var ret = this.obj[randkey];
        }

        delete this.obj[randkey];
        return ret;
    }

    /**
    * Return the first element from this collection
    * @memberof module:TrellinatorCore.IterableCollection
    * @throws InvalidDataException
    */
    this.first = function()
    {
        var ret = null;

        for(var key in this.obj)
        {
            if(ret === null)
                ret = this.obj[key];
        }
        
        if(ret === null)
            throw new InvalidDataException("No data in IterableCollection: "+this.obj);
        
        return ret;
    }
    
    /**
    * Reverse the order of this collection
    * @memberof module:TrellinatorCore.IterableCollection
    * @throws InvalidDataException
    */
    this.reverse = function()
    {
        return new IterableCollection(this.asArray().reverse());
    }

    /**
    * Return the last element from this collection
    * @memberof module:TrellinatorCore.IterableCollection
    * @throws InvalidDataException
    */
    this.last = function()
    {
        return new IterableCollection(this.asArray().reverse()).first();
    }

    /**
    * Iterate over this collection, passing
    * each element into a callback function
    * @memberof module:TrellinatorCore.IterableCollection
    * @param callback {Function} the function
    * into which each element will be passed.
    * The first parameter passed in is the element
    * but you can optionally accept a second
    * parameter which is the key
    * @example
    * notif.board().list("List").cards().each(function(card)
    * {
    *     card.postComment("@board Hi! My name is: "+card.name());
    * });
    * @example
    * var params = {name: "Milton",
    * hobbies: "Listening to music from 9-11 at a reasonable volume"};
    * new IterableCollection(params).each(function(elem,key)
    * {
    *     console.log(key+": "+elem);
    * });
    */
    this.each = function(callback)
    {
        for(var key in this.obj)
            callback(this.obj[key],key);
    
        return this;
    }

    /**
    * Apply a callback to each item in this collection,
    * modifying this collection.
    *
    * Whatever is returned from the callback will 
    * replace the original object, keys are preserved.
    *
    * If the callback returns false, that key/element
    * pair will be removed.
    *
    * If you want to find an item in a collection 
    * without modifying the collection, use find()
    * instead of transform().
    * 
    * @memberof module:TrellinatorCore.IterableCollection
    * @param callback {Function} a call back that
    * accepts an element of this collection and
    * optionally a second argument which is the key
    * and returns an object to be included in the
    * modified collection, or false and that 
    * key/element pair will be removed
    * @example
    * notif.board().cards().transform(function(elem,key))
    * {
    *     if((key < 10) && (elem.name() == "ohai"))
    *         return elem;
    *     else
    *         return false;
    * }).first().postComment("Yep, ohai alright");
    */
    this.transform = function(callback)
    {
        this.obj = this.find(callback).obj;
        return this;
    }
    
    /**
    * Return the number of items in this collection
    * @memberof module:TrellinatorCore.IterableCollection
    */
    this.length = function()
    {
        return Object.keys(this.obj).length;
    }

    /**
    * Convenience function to find an object
    * by name() method, compared with a string
    * or RegExp, or object with a "name" 
    * parameter
    * @memberof module:TrellinatorCore.IterableCollection
    * @param expression {string|RegExp} the expression
    * to compare to the name of each element to find
    */
    this.findByName = function(expression)    
    {
        var ret = this;
      
        if(expression)
        {
            ret = this.find(function(elem)
            {
                if(TrelloApi.nameTest(TrelloApi.nameTestData(expression),elem.name()))
                    return elem;
                else
                    return false;
            });
        }
            
        return ret;
    }
    
    /**
    * Return the object as an Array, preserving
    * keys
    * @memberof module:TrellinatorCore.IterableCollection
    */
    this.asArray = function()
    {
      ret = new Array();
      
      for(var key in this.obj)
        ret.push(this.obj[key]);
      
      return ret;
    }

    //DEPRECATED
    this.clone = function()
    {
        var new_obj = [];

        for(var key in this.obj)
          new_obj[key] = this.obj[key];

        return new IterableCollection(new_obj);
    }
}