Source: js/library.prototypes.js

/** @class   Function
    @desc    Native function prototype.
    @borrows Function#context as Function#bind
*/

/** @class   String
    @desc    Native string prototype.
*/

/** @class   Array
    @desc    Native array prototype.
*/

/** @class   Object
    @desc    Native object prototype.
*/

/** @class   Boolean
    @desc    Native boolean prototype.
*/

/** @class   Number
    @desc    Native number prototype.
*/

/** @class   Date
    @desc    Native date prototype.
*/

/** @class   RegExp
    @desc    Native regular expression prototype.
*/

/** @method   Function#context
    @readonly
    @desc     Binds "this" context of function to a specific object.
    
    @arg      {Object}   context - Object to act as parent (this) of function call.
    @returns  {Function} Wrapper function.
    
    @see      {@link Function#bind}
*/
!Function.prototype.context && (Function.prototype.context = function (context)
{
    var fn = this;
    
    return function () { return fn.apply(context, arguments); };
});

!Function.prototype.bind && (Function.prototype.bind = Function.prototype.context);



/** @method    Array#keySort
    @readonly
    @chainable
    @desc      Sort a list of objects by a specific property.
    
    @arg       {String} property  - Value of each object to compare.
    @arg       {Number} [order=1] - Positive for ascending, negative for descending.
    @returns   {Array}              Sorted list.
    
    @see       {@link http://en.wikipedia.org/wiki/Schwartzian_transform}
    @see       {@link http://blog.rodneyrehm.de/archives/14-Sorting-Were-Doing-It-Wrong.html}
    @todo      Use a schwartzian transform to cache values, preventing accesses for every comparison.
               Will have varying speed improvements depending on which comparison method the runtime uses,
               some make vastly more comparisons.
*/
!Array.prototype.keySort && (Array.prototype.keySort = function (property, order)
{
    if (property)
    {
        // sanitize
        
        typeof order === 'string' && (order = Number(order));
        (!order || isNaN(order))  && (order = 1); // default to ascending
        
        this.sort(function (a, b)
        {
            if (order < 0) // descending? swap a & b
            {
                var temp = a;
                
                a = b;
                b = temp;
            }
            
            // init
            
            var aValue = a ? a[property] : null,
                bValue = b ? b[property] : null;
            
            // sanitize
            
            if (!exists(aValue) && !exists(bValue)) { return 0; }
            
            // compare
            
            if (aValue)
            {
                if (typeof aValue === 'number' && typeof bValue === 'number')
                {
                    return aValue == bValue ? 0 : (aValue > bValue ? 1 : -1);
                }
                
                return aValue.localeCompare(bValue);
            }
            else { return bValue.localeCompare(aValue); }
        });
    }
    else { return this.sort(); }
    
    return this;
});



/** @method    Array#elementAttributeSort
    @readonly
    @chainable
    @desc      Sort a list of elements by attribute(s).
    
    @arg       {String}   properties - Properties of each object to compare.
    @arg       {Number}   [order=1]  - Positive for ascending, negative for descending.
    @returns   {Array}                 Sorted list.
    
    @see       {@link http://en.wikipedia.org/wiki/Schwartzian_transform}
    @see       {@link http://blog.rodneyrehm.de/archives/14-Sorting-Were-Doing-It-Wrong.html}
    @todo      Concatenating won't work with property values of different lengths:
               
               foo.a = 'ab';
               foo.b = 'z';
               
               bar.a = 'abc';
               bar.b = 'y';
               
               foo's a + b -> 'abz'
               bar's a + b -> 'abcy'
               
               ...foo will sort after bar though that's not what we want.
               
               Either:
               
               - Calculate the longest value (.toString() if it's not) length and right-pad them all with the appropriate
                 number of spaces to make them equal.
               - (Faster) enforce the primary sort first because in most cases a sub-comparison isn't necessary.
               
               Also use a schwartzian transform to cache values, preventing accesses for every comparison.
               Will have varying speed improvements depending on which comparison method the runtime uses,
               some make vastly more comparisons.
*/
!Array.prototype.elementAttributeSort && (Array.prototype.elementAttributeSort = function (properties, order)
{
    if (properties)
    {
        // sanitize
        
        typeof order === 'string' && (order = Number(order));
        (!order || isNaN(order))  && (order = 1); // default to ascending
        
        properties = properties.split(/\s*,\s*/);
        
        this.sort(function (a, b)
        {
            // descending? swap a & b
            
            if (order < 0)
            {
                var temp = a;
                
                a = b;
                b = temp;
            }
            
            // init
            
            var aValue = '',
                bValue = '';
            
            // concatenate values
            
            for (var i = 0; i < properties.length ; i++)
            {
                var aAttribute = a.attributes ? a.attributes[properties[i]] : null,
                    bAttribute = b.attributes ? b.attributes[properties[i]] : null;
                
                aValue += aAttribute ? aAttribute.value : '';
                bValue += bAttribute ? bAttribute.value : '';
            }
            
            return aValue.localeCompare(bValue);
        });
    }
    
    return this;
});



/** @method    Array#distinct
    @readonly
    @chainable
    @desc      Filter a list to contain only unique values.
    
    @returns   {Array} List of unique values.
    
    @todo      Use [].filter(). As it iterates the array, items can be excluded if they match (strict, ===)
               any previous indexes. May or may not be more efficient, but would provide finer matching control
               than a plain indexOf lookup.
*/
!Array.prototype.distinct && (Array.prototype.distinct = function ()
{
    var distinct = [];
    
    for (var i = 0; i < this.length; i++)
    {
        var value = this[i];
        
        !~distinct.indexOf(value) && distinct.push(value);
    }
    
    return distinct;
});



/** @method   Array#pushEach
    @readonly
    @desc     Concatenates this list with another.
    
    @arg      {Array}  other - List to concatenate to this one.
    @returns  {Number}         Count of all values.
    
    @todo     It would be more useful to return this (the array we're operating on) because the method could be chained.
              Length is easy to grab at any time. This method would be no different than native [...].concat([...]).
*/
!Array.prototype.pushEach && (Array.prototype.pushEach = function (other)
{
    if (!Array.isArray(other)) { return this.length; }
    
    for (var i = 0; i < other.length; i++) { this.push(other[i]); }
    
    return this.length;
});



/** @method   String#format
    @readonly
    @desc     Replaces numbered placeholders in a string with arguments provided. Placeholders are of the form "{#}"
              where "#" is the zero-based index of the argument to substitute.
    
    @arg      {...Mixed} arguments - Any number of values referenced by placeholders in this string.
    @returns  {String}               Formatted string.
*/
!String.prototype.format && (String.prototype.format = function ()
{
    var args = arguments;
    
    return this.replace(/{(\d+)}/g, function(match, number)
    {
        return exists(args[number]) ? args[number] : match;
    });
});



/** @method   Array#njoin
    @readonly
    @desc     {@link Array#join}, skipping falsy values.
    @example  ['a', '', 'c'].njoin(', ') -> 'a, c'
    
    @arg      {String} separator - Characters to use as delimiters.
    @returns  {String}             Delimited string.
    
    @todo     This will exclude ANY falsy values, e.g. '' or 0, not just null as originally described. Is this desired?
              Also use [].filter() to exclude, followed by a native join.
*/
!Array.prototype.njoin && (Array.prototype.njoin = function (separator)
{
    !separator && (separator = '');
    
    var output = '';
    
    for (var i = 0; i < this.length; i++)
    {
        var value = this[i];
        
        value && (output += value.toString() + separator);
    }
    
    return output.length
        ? output.substring(0, output.length - separator.length)
        : output;
});



/** @method   Array#njoin
    @readonly
    @desc     {@link Array#split}, skipping resulting empty strings.
    @example  'a, , c'.nsplit(', ') -> ['a', 'c']
    
    @arg      {String}   separator - Characters to detect as delimiters.
    @arg      {Function} callback  - Function to process indexes.
    @returns  {Array}                Resulting strings.
    
    @todo     Use native filter after native split. Also callback functionality can be accomplished with filter on the
              returned array and needn't be duplicated here.
*/
!String.prototype.nsplit && (String.prototype.nsplit = function (separator, eachFunction)
{
    var splitString = this.split(separator);
    
    for (var i = 0; i < splitString.length;)
    {
        if (!splitString[i])
        {
            splitString.splice(i, 1);
        }
        else
        {
            eachFunction && (splitString[i] = eachFunction(splitString[i]));
            
            i++;
        }
    }
    
    return splitString;
});



/** @method   Object.getOwnValues
    @readonly
    @desc     Queries object for non-inherited property values and returns a list of them.
    
    @arg      {Object} object - Item to query for non-inherited property values.
    @returns  {Array}           Object's own values.
    
    @todo     Change to instance method.
*/
!Object.getOwnValues && (Object.getOwnValues = function (object)
{
    var keys   = Object.getOwnPropertyNames(object),
        values = [];
    
    for (var i = 0; i < keys.length; i++)
    {
        var value = object[keys[i]];
        
        typeof value !== 'function' && values.push(value);
    }
    
    return values;
});



/** @method   Date#format
    @readonly
    @desc     Recreation of PHP's date function.
    
    @arg      {String} format - Sequence of symbols to be replaced with corresponding date/time values.
    @returns  {String}          Formetted date/time string.
    
    @see      {@link Date~replaceChars}
    @see      {@link http://php.net/manual/en/function.date.php}
*/
!Date.prototype.format && (Date.prototype.format = function(format)
{
    var returnStr = '',
        replace   = Date.replaceChars;
    
    for (var i = 0; i < format.length; i++)
    {
        var curChar = format.charAt(i);
        
             if (i - 1 >= 0 && format.charAt(i - 1) == "\\") { returnStr += curChar; }
        else if (replace[curChar])                           { returnStr += replace[curChar].call(this); }
        else if (curChar != "\\")                            { returnStr += curChar; }
    }
    
    return returnStr;
});



/** @member   {Object} Date~replaceChars
    @readonly
    @desc     Index of symbols and corresponding replacement functions.
    
    @see      {@link Date#format}
    @see      {@link http://php.net/manual/en/function.date.php}
*/
!Date.replaceChars && (Date.replaceChars =
{
    // display names
    'shortMonths': ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
    'longMonths' : ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
    'shortDays'  : ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
    'longDays'   : ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
    
    // day
    'd': function() { return (this.getDate() < 10 ? '0' : '') + this.getDate(); },
    'D': function() { return Date.replaceChars.shortDays[this.getDay()]; },
    'j': function() { return this.getDate(); },
    'l': function() { return Date.replaceChars.longDays[this.getDay()]; },
    'N': function() { return this.getDay() + 1; },
    'S': function() { return (this.getDate() % 10 == 1 && this.getDate() != 11 ? 'st' : (this.getDate() % 10 == 2 && this.getDate() != 12 ? 'nd' : (this.getDate() % 10 == 3 && this.getDate() != 13 ? 'rd' : 'th'))); },
    'w': function() { return this.getDay(); },
    'z': function() { var d = new Date(this.getFullYear(),0,1); return Math.ceil((this - d) / 86400000); }, // Fixed now
    
    // week
    'W': function() { var d = new Date(this.getFullYear(), 0, 1); return Math.ceil((((this - d) / 86400000) + d.getDay() + 1) / 7); }, // Fixed now
    
    // month
    'F': function() { return Date.replaceChars.longMonths[this.getMonth()]; },
    'm': function() { return (this.getMonth() < 9 ? '0' : '') + (this.getMonth() + 1); },
    'M': function() { return Date.replaceChars.shortMonths[this.getMonth()]; },
    'n': function() { return this.getMonth() + 1; },
    't': function() { var d = new Date(); return new Date(d.getFullYear(), d.getMonth(), 0).getDate() }, // Fixed now, gets #days of date
    
    // year
    'L': function() { var year = this.getFullYear(); return (year % 400 == 0 || (year % 100 != 0 && year % 4 == 0)); },   // Fixed now
    'o': function() { var d = new Date(this.valueOf());  d.setDate(d.getDate() - ((this.getDay() + 6) % 7) + 3); return d.getFullYear();}, //Fixed now
    'Y': function() { return this.getFullYear(); },
    'y': function() { return ('' + this.getFullYear()).substr(2); },
    
    // time
    'a': function() { return this.getHours() < 12 ? 'am' : 'pm'; },
    'A': function() { return this.getHours() < 12 ? 'AM' : 'PM'; },
    'B': function() { return Math.floor((((this.getUTCHours() + 1) % 24) + this.getUTCMinutes() / 60 + this.getUTCSeconds() / 3600) * 1000 / 24); }, // Fixed now
    'g': function() { return this.getHours() % 12 || 12; },
    'G': function() { return this.getHours(); },
    'h': function() { return ((this.getHours() % 12 || 12) < 10 ? '0' : '') + (this.getHours() % 12 || 12); },
    'H': function() { return (this.getHours() < 10 ? '0' : '') + this.getHours(); },
    'i': function() { return (this.getMinutes() < 10 ? '0' : '') + this.getMinutes(); },
    's': function() { return (this.getSeconds() < 10 ? '0' : '') + this.getSeconds(); },
    'u': function() { var m = this.getMilliseconds(); return (m < 10 ? '00' : (m < 100 ? '0' : '')) + m; },
    
    // zone
    'e': function() { return 'Not Yet Supported'; },
    'I': function()
    {
        var DST = null;
        
        for (var i = 0; i < 12; ++i)
        {
            var d = new Date(this.getFullYear(), i, 1),
                offset = d.getTimezoneOffset();
            
                 if (DST === null) { DST = offset; }
            else if (offset < DST) { DST = offset; break; }
            else if (offset > DST) { break; }
        }
        
        return (this.getTimezoneOffset() == DST) | 0;
    },
    'O': function() { return (-this.getTimezoneOffset() < 0 ? '-' : '+') + (Math.abs(this.getTimezoneOffset() / 60) < 10 ? '0' : '') + (Math.abs(this.getTimezoneOffset() / 60)) + '00'; },
    'P': function() { return (-this.getTimezoneOffset() < 0 ? '-' : '+') + (Math.abs(this.getTimezoneOffset() / 60) < 10 ? '0' : '') + (Math.abs(this.getTimezoneOffset() / 60)) + ':00'; }, // fixed now
    'T': function() { var m = this.getMonth(); this.setMonth(0); var result = this.toTimeString().replace(/^.+ \(?([^\)]+)\)?$/, '$1'); this.setMonth(m); return result;},
    'Z': function() { return -this.getTimezoneOffset() * 60; },
    
    // full date/time
    'c': function() { return this.format('Y-m-d\\TH:i:sP'); }, // fixed now
    'r': function() { return this.toString(); },
    'U': function() { return this.getTime() / 1000; }
});



/** @method   String#parseUrl
    @readonly
    @desc     Break URL into components.
    
    @returns  {Object} Index of URL components.
*/
!String.prototype.parseUrl && (String.prototype.parseUrl = function ()
{
    var anchor     = $('<a></a>').attr('href', this)[0],
        properties = ['href', 'protocol', 'host', 'hostname', 'port', 'pathname', 'search', 'hash', 'origin'],
        renames    = {'href': 'url', 'pathname': 'path', 'search': 'query'},
        parsed     = {},
        query      = {};
    
    // retrieve properties
    
    properties.forEach(function (key) { parsed[renames[key] || key] = anchor[key]; });
    
    // split query
    
    parsed.query.substr(1).split('&').forEach(function (param)
    {
        param           = param.split('=');
        query[param[0]] = param[1];
    });
    
    // reformat
    
    $.extend(parsed,
    {
        'protocol': parsed.protocol.substr(0, parsed.protocol.length - 1),
        'port'    : Number(parsed.port) || null,
        'path'    : parsed.path.substr(1),
        'query'   : query,
        'hash'    : parsed.hash.substr(1)
    });
    
    return parsed;
});



/** @method   String#capitalize
    @readonly
    @desc     Capitalizes the first character in a string.
    
    @returns  {String} Capitalized string.
*/
!String.prototype.capitalize && (String.prototype.capitalize = function ()
{
    return this.charAt(0).toUpperCase() + this.slice(1);
});



/** @method   Object#toArray
    @readonly
    @desc     Lists property values of an object. Useful for converting indexed data to a loopable list.
    
    @returns  {Object} Index of URL components.
    
    @see      {@link dataObjectToArray}
*/
!Object.prototype.toArray && Object.defineProperty(Object.prototype, 'toArray',
{
    'enumerable'  : false,
    'configurable': false,
    'writable'    : false,
    'value'       : function ()
    {
        var arr = [];
        
        for (prop in this) { this.hasOwnProperty(prop) && arr.push(this[prop]); }
        
        return arr;
    }
});



/** @method   Object#extend
    @readonly
    @desc     Merge contents of one or more objects into this object.
    
    @arg      {...Object} arguments - Objects to merge into this one.
    @returns  {Object}                Merge result.
    
    @see      {@link Object#extend(2)}
    @see      {@link http://api.jquery.com/jQuery.extend/}
*/
/** @method   Object#extend(2)
    @readonly
    @desc     Recursively merge contents of one or more objects into this object.
    
    @arg      {Boolean}   deep      - Recursive flag.
    @arg      {...Object} arguments - Objects to merge into this one.
    @returns  {Object}                Merge result.
    
    @see      {@link Object#extend}
    @see      {@link http://api.jquery.com/jQuery.extend/}
*/
!Object.prototype.extend && Object.defineProperty(Object.prototype, 'extend',
{
    'enumerable'  : false,
    'configurable': false,
    'writable'    : false,
    'value'       : function ()
    {
        var deep = arguments[0] === true,
            objs = Array.prototype.slice.call(arguments).splice(deep ? 1 : 0, 0, this);
        
        return $.extend.apply($, objs);
    }
});



/** @method   Object#renameProperty
    @readonly
    @desc     Changes the name of a property.
    
    @arg      {String} keyOld - Current name of property.
    @arg      {String} keyNew - Desired name of property.
    @returns  {Object}          Altered object.
*/
!Object.prototype.renameProperty && Object.defineProperty(Object.prototype, 'renameProperty',
{
    'enumerable'  : false,
    'configurable': false,
    'writable'    : false,
    'value'       : function (keyOld, keyNew)
    {
        if (this.hasOwnProperty(keyOld) && keyOld !== keyNew)
        {
            this[keyNew] = this[keyOld];
            delete this[keyOld];
        }
        
        return this;
    }
});



// Number = function ()
// {
//     //$.extend(this, Number.apply(window, arguments));
    
//     this.prototype = Number.prototype;
    
//     var hz = null;
    
//     Object.defineProperty(this.prototype, 'hz',
//     {
//         'enumerable'  : true,
//         'configurable': true,
//         //'writable'    : true,
//         'get'         : function () { console.log(this, arguments); return 5; },
//         'set'         : function (rate) { console.log(this, arguments, rate); hz = Math.round(1000 / rate); }
//     });
// };



/** @method   String#toBoolean
    @readonly
    @desc     Detects if a string === 'true'. Useful for working with attribute or query string values.
    
    @arg      {String} prop - Name of property value to retrieve from each object.
    @returns  {Array}         List of property values.
*/
!String.prototype.toBoolean && (String.prototype.toBoolean = function ()
{
    return /^true|yes|on$/i.test(this);
});



/** @method   Array#pluck
    @readonly
    @desc     Queries an array of objects for a specific property and returns a list of those values.
    
    @arg      {String} prop - Name of property value to retrieve from each object.
    @returns  {Array}         List of property values.
    
    @see      {@link http://documentcloud.github.io/underscore/#pluck}
*/
!Array.prototype.pluck && (Array.prototype.pluck = function (prop)
{
    return this.map(function (obj, index) { return obj[prop] });
});



// in development
!Object.prototype.pluck && Object.defineProperty(Object.prototype, 'pluck',
{
    'enumerable'  : false,
    'configurable': false,
    'writable'    : false,
    'value'       : function ()
    {
        // init
        
        var properties       = [].concat.apply([], [].slice.call(arguments)),
            propertiesLength = properties.length,
            obj              = {};
        
        // validate
        
        if (!propertiesLength)
        {
            console.log(L_WARN, 'Object.prototype.pluck', 'no arguments supplied');
            return {};
        }
        
        for (var index = 0; index < propertiesLength; index++)
        {
            var property = properties[index];
            
            if (typeof property !== 'string')
            {
                console.log(L_WARN, 'Object.prototype.pluck', 'invalid argument found', property);
                return {};
            }
            else { obj[property] = this[property]; } // add
        }
        
        return obj;
    }
});



// in development
!Array.prototype.flatten && (Array.prototype.flatten = function (prop)
{
    return this.map(function (obj, index) { return obj[prop] });
});