sugar
Version:
A Javascript library for working with native objects.
1,561 lines (1,425 loc) • 101 kB
JavaScript
// Google Closure Compiler will output a wrapping function here.
(function() {
// A few optimizations for Google Closure Compiler will save us a couple kb in the release script.
var object = Object, array = Array, regexp = RegExp, date = Date, string = String, number = Number, Undefined;
// defineProperty exists in IE8 but will error when trying to define a property on
// native objects. IE8 does not have defineProperies, however, so this check saves a try/catch block.
var definePropertySupport = object.defineProperty && object.defineProperties;
function extend(klass, instance, override, methods) {
var extendee = instance ? klass.prototype : klass;
iterateOverObject(methods, function(name, method) {
if(typeof override === 'function') {
defineProperty(extendee, name, wrapNative(extendee[name], method, override));
} else if(override === true || !extendee[name]) {
defineProperty(extendee, name, method);
}
});
}
function wrapNative(nativeFn, extendedFn, condition) {
return function() {
if(nativeFn && (condition === true || condition.apply(this, arguments))) {
return nativeFn.apply(this, arguments);
} else {
return extendedFn.apply(this, arguments);
}
}
}
function defineProperty(target, name, method) {
if(definePropertySupport) {
object.defineProperty(target, name, { 'value': method, 'configurable': true, 'enumerable': false, 'writable': true });
} else {
target[name] = method;
}
}
function iterateOverObject(obj, fn) {
var count = 0;
for(var key in obj) {
if(!obj.hasOwnProperty(key)) continue;
fn.call(obj, key, obj[key], count);
count++;
}
}
function multiMatch(el, match, scope, params) {
if(el === match) {
// Match strictly equal values up front.
return true;
} else if(object.isRegExp(match)) {
// Match against a regexp
return regexp(match).test(el);
} else if(object.isFunction(match)) {
// Match against a filtering function
return match.apply(scope, [el].concat(params));
} else {
// Match against a hash or array.
return object.equal(match, el);
}
}
function transformArgument(el, map, context, mapArgs) {
if(isUndefined(map)) {
return el;
} else if(object.isFunction(map)) {
return map.apply(context, mapArgs || []);
} else if(object.isFunction(el[map])) {
return el[map].call(el);
} else {
return el[map];
}
}
function getArgs(args, index) {
return Array.prototype.slice.call(args, index);
}
function multiArgs(args, fn, flatten, index) {
args = getArgs(args);
if(flatten !== false) args = arrayFlatten(args);
arrayEach(args, fn, index);
}
/***
* Object module
*
* Much thanks to kangax for his informative aricle about how problems with instanceof and constructor
* http://perfectionkills.com/instanceof-considered-harmful-or-how-to-write-a-robust-isarray/
*
***/
function instanceOf(obj, str) {
return object.prototype.toString.call(obj) === '[object '+str+']';
}
function isObjectPrimitive(o) {
return typeof o == 'object';
}
function isNull(o) {
return o === null;
}
function isUndefined(o) {
return o === Undefined;
}
function isDefined(o) {
return o !== Undefined;
}
function mergeObject(target, source, deep, resolve) {
if(isObjectPrimitive(source)) {
iterateOverObject(source, function(key, val) {
var prop = target[key], conflict = isDefined(prop), isArray = object.isArray(val);
if(deep === true && (isArray || object.isObject(val))) {
if(!prop) prop = isArray ? [] : {};
mergeObject(prop, val, deep);
} else if(conflict && object.isFunction(resolve)) {
prop = resolve.call(source, key, target[key], source[key])
} else if(!conflict || (conflict && resolve !== false)) {
prop = source[key];
}
if(isDefined(prop)) target[key] = prop;
});
}
return target;
}
function Hash(obj) {
var self = this;
iterateOverObject(obj, function(key, value) {
self[key] = value;
});
}
Hash.prototype.constructor = object;
/***
* @method is[Type](<obj>)
* @returns Boolean
* @short Returns true if <obj> is an object of that type.
* @extra %isObject% will return false on anything that is not an object literal, including instances of inherited classes. Note also that %isNaN% will ONLY return true if the object IS %NaN%. It does not mean the same as browser native %isNaN%, which returns true for anything that is "not a number". Type methods are available as instance methods on extended objects and when using Object.extend().
* @example
*
* Object.isArray([1,2,3]) -> true
* Object.isDate(3) -> false
* Object.isRegExp(/wasabi/) -> true
* Object.isObject({ broken:'wear' }) -> true
*
***
* @method isArray()
* @set isType
***
* @method isBoolean()
* @set isType
***
* @method isDate()
* @set isType
***
* @method isFunction()
* @set isType
***
* @method isNumber()
* @set isType
***
* @method isString()
* @set isType
***
* @method isRegExp()
* @set isType
***/
function buildTypeMethods() {
var methods = {};
arrayEach(['Array','Boolean','Date','Function','Number','String','RegExp'], function(type) {
methods['is' + type] = function(obj) {
return instanceOf(obj, type);
}
});
extend(Object, false, false, methods);
}
function buildObject() {
buildTypeMethods();
}
extend(object, false, false, {
/***
* @method isObject()
* @set isType
***/
'isObject': function(obj) {
if(isNull(obj) || isUndefined(obj)) {
return false;
} else {
return instanceOf(obj, 'Object') && obj.constructor === object;
}
},
/***
* @method each(<obj>, [fn])
* @returns Object
* @short Iterates over each property in <obj> calling [fn] on each iteration.
* @extra %each% is available as an instance method on extended objects and when using Object.extend().
* @example
*
* Object.each({ broken:'wear' }, function(key, value) {
* // Iterates over each key/value pair.
* });
* Object.extended({ broken:'wear' }).each(function(key, value) {
* // Iterates over each key/value pair.
* });
*
***/
'each': function(obj, fn) {
if(fn) {
iterateOverObject(obj, function(k,v) {
fn.call(obj, k, v, obj);
});
}
return obj;
},
/***
* @method merge(<target>, <source>, [resolve] = true)
* @returns Merged object
* @short Merges all the properties of <source> into <target>.
* @extra Properties of <source> will win in the case of conflicts, unless [resolve] is %false%. [resolve] can also be a function that resolves the conflict. In this case it will be passed 3 arguments, %key%, %targetVal%, and %sourceVal%, with the context set to <source>. This will allow you to solve conflict any way you want, ie. adding two numbers together, etc. %merge% is available as an instance method on extended objects and when using Object.extend().
* @example
*
* Object.merge({a:1},{b:2}) -> { a:1, b:2 }
* Object.merge({a:1},{a:2}, false) -> { a:1 }
+ Object.merge({a:1},{a:2}, function(key, a, b) {
* return a + b;
* }); -> { a:3 }
* Object.extended({a:1}).merge({b:2}) -> { a:1, b:2 }
*
***/
'merge': function(target, merge, resolve) {
return mergeObject(target, merge, true, resolve);
}
});
/***
* Array module
*
***/
// Basic array internal methods
function arrayEach(arr, fn, startIndex, loop) {
var length, index, i;
checkCallback(fn);
if(startIndex < 0) startIndex = arr.length + startIndex;
i = toIntegerWithDefault(startIndex, 0);
length = loop === true ? arr.length + i : arr.length;
while(i < length) {
index = i % arr.length;
if(fn.call(arr, arr[index], index, arr) === false) {
break;
}
i++;
}
}
function arrayFind(arr, f, startIndex, loop, returnIndex) {
var result, index;
arrayEach(arr, function(el, i, arr) {
if(multiMatch(el, f, arr, [i, arr])) {
result = el;
index = i;
return false;
}
}, startIndex, loop);
return returnIndex ? index : result;
}
function arrayFlatten(arr, level, current) {
level = level || Infinity;
current = current || 0;
var result = [];
arrayEach(arr, function(el) {
if(object.isArray(el) && current < level) {
result = result.concat(arrayFlatten(el, level, current + 1));
} else {
result.push(el);
}
});
return result;
}
// ECMA5 methods
function arrayIndexOf(arr, search, fromIndex, increment) {
var length = arr.length,
fromRight = increment == -1,
start = fromRight ? length - 1 : 0,
index = toIntegerWithDefault(fromIndex, start);
if(index < 0) {
index = length + index;
}
if((!fromRight && index < 0) || (fromRight && index >= length)) {
index = start;
}
while((fromRight && index >= 0) || (!fromRight && index < length)) {
if(arr[index] === search) {
return index;
}
index += increment;
}
return -1;
}
function toIntegerWithDefault(i, d) {
if(isNaN(i)) {
return d;
} else {
return parseInt(i >> 0);
}
}
function isArrayIndex(arr, i) {
return i in arr && toUInt32(i) == i && i != 0xffffffff;
}
function toUInt32(i) {
return i >>> 0;
}
function checkCallback(fn) {
if(!fn || !fn.call) {
throw new TypeError('Callback is not callable');
}
}
function checkFirstArgumentExists(args) {
if(args.length === 0) {
throw new TypeError('First argument must be defined');
}
}
extend(array, true, function() { var a = arguments; return a.length === 0 || object.isFunction(a[0]); }, {
/***
* @method every(<f>, [scope])
* @returns Boolean
* @short Returns true if all elements in the array match <f>.
* @extra [scope] is the %this% object. In addition to providing this method for browsers that don't support it natively, this enhanced method also directly accepts strings, numbers, deep objects, and arrays for <f>. %all% is provided an alias.
* @example
*
+ ['a','a','a'].every(function(n) {
* return n == 'a';
* });
* ['a','a','a'].every('a') -> true
* [{a:2},{a:2}].every({a:2}) -> true
*
***/
'every': function(f, scope) {
var length = this.length, index = 0;
checkFirstArgumentExists(arguments);
while(index < length) {
if(index in this && !multiMatch(this[index], f, scope, [index, this])) {
return false;
}
index++;
}
return true;
},
/***
* @method some(<f>, [scope])
* @returns Boolean
* @short Returns true if any element in the array matches <f>.
* @extra [scope] is the %this% object. In addition to providing this method for browsers that don't support it natively, this enhanced method also directly accepts strings, numbers, deep objects, and arrays for <f>. %any% and %has% are provided as aliases.
* @example
*
+ ['a','b','c'].some(function(n) {
* return n == 'a';
* });
+ ['a','b','c'].some(function(n) {
* return n == 'd';
* });
* ['a','b','c'].some('a') -> true
* [{a:2},{b:5}].some({a:2}) -> true
*
***/
'some': function(f, scope) {
var length = this.length, index = 0;
checkFirstArgumentExists(arguments);
while(index < length) {
if(index in this && multiMatch(this[index], f, scope, [index, this])) {
return true;
}
index++;
}
return false;
},
/***
* @method map(<map>, [scope])
* @returns Array
* @short Maps the array to another array containing the values that are the result of calling <map> on each element.
* @extra [scope] is the %this% object. In addition to providing this method for browsers that don't support it natively, this enhanced method also directly accepts a string, which is a shortcut for a function that gets that property (or invokes a function) on each element. %collect% is provided as an alias.
* @example
*
+ [1,2,3].map(function(n) {
* return n * 3;
* }); -> [3,6,9]
* ['one','two','three'].map(function(n) {
* return n.length;
* }); -> [3,3,5]
* ['one','two','three'].map('length') -> [3,3,5]
*
***/
'map': function(map, scope) {
var length = this.length, index = 0, el, result = new Array(length);
checkFirstArgumentExists(arguments);
while(index < length) {
if(index in this) {
el = this[index];
result[index] = transformArgument(el, map, scope, [el, index, this]);
}
index++;
}
return result;
},
/***
* @method filter(<f>, [scope])
* @returns Array
* @short Returns any elements in the array that match <f>.
* @extra [scope] is the %this% object. In addition to providing this method for browsers that don't support it natively, this enhanced method also directly accepts strings, numbers, deep objects, and arrays for <f>.
* @example
*
+ [1,2,3].filter(function(n) {
* return n > 1;
* });
* [1,2,2,4].filter(2) -> 2
*
***/
'filter': function(f, scope) {
var length = this.length, index = 0, result = [];
checkFirstArgumentExists(arguments);
while(index < length) {
if(index in this && multiMatch(this[index], f, scope, [index, this])) {
result.push(this[index]);
}
index++;
}
return result;
}
});
extend(array, true, false, {
/***
* @method indexOf(<search>, [fromIndex])
* @returns Number
* @short Searches the array and returns the first index where <search> occurs, or -1 if the element is not found.
* @extra [fromIndex] is the index from which to begin the search. This method performs a simple strict equality comparison on <search>. It does not support enhanced functionality such as searching the contents against a regex, callback, or deep comparison of objects. For such functionality, use the %find% method instead.
* @example
*
* [1,2,3].indexOf(3) -> 1
* [1,2,3].indexOf(7) -> -1
*
***/
'indexOf': function(search, fromIndex) {
if(object.isString(this)) return this.indexOf(search, fromIndex);
return arrayIndexOf(this, search, fromIndex, 1);
},
/***
* @method lastIndexOf(<search>, [fromIndex])
* @returns Number
* @short Searches the array and returns the last index where <search> occurs, or -1 if the element is not found.
* @extra [fromIndex] is the index from which to begin the search. This method performs a simple strict equality comparison on <search>.
* @example
*
* [1,2,1].lastIndexOf(1) -> 2
* [1,2,1].lastIndexOf(7) -> -1
*
***/
'lastIndexOf': function(search, fromIndex) {
if(object.isString(this)) return this.lastIndexOf(search, fromIndex);
return arrayIndexOf(this, search, fromIndex, -1);
},
/***
* @method forEach([fn], [scope])
* @returns Nothing
* @short Iterates over the array, calling [fn] on each loop.
* @extra This method is only provided for those browsers that do not support it natively. [scope] becomes the %this% object.
* @example
*
* ['a','b','c'].forEach(function(a) {
* // Called 3 times: 'a','b','c'
* });
*
***/
'forEach': function(fn, scope) {
var length = this.length, index = 0;
checkCallback(fn);
while(index < length) {
if(index in this) {
fn.call(scope, this[index], index, this);
}
index++;
}
},
/***
* @method reduce([fn], [init])
* @returns Mixed
* @short Reduces the array to a single result.
* @extra By default this method calls [fn] n - 1 times, where n is the length of the array. On the first call it is passed the first and second elements in the array. The result of that callback will then be passed into the next iteration until it reaches the end, where the accumulated value will be returned as the final result. If [init] is passed, it will call [fn] one extra time in the beginning passing in [init] along with the first element. This method is only provided for those browsers that do not support it natively.
* @example
*
+ [1,2,3,4].reduce(function(a, b) {
* return a + b;
* });
+ [1,2,3,4].reduce(function(a, b) {
* return a + b;
* }, 100);
*
***/
'reduce': function(fn, init) {
return arrayReduce(this, fn, init);
},
/***
* @method reduceRight([fn], [init])
* @returns Mixed
* @short Reduces the array to a single result by stepping through it from the right.
* @extra By default this method calls [fn] n - 1 times, where n is the length of the array. On the first call it is passed the last and second to last elements in the array. The result of that callback will then be passed into the next iteration until it reaches the beginning, where the accumulated value will be returned as the final result. If [init] is passed, it will call [fn] one extra time in the beginning passing in [init] along with the last element. This method is only provided for those browsers that do not support it natively.
* @example
*
+ [1,2,3,4].reduceRight(function(a, b) {
* return a - b;
* });
*
***/
'reduceRight': function(fn, init) {
return arrayReduce(this, fn, init, true);
},
/***
* @method groupBy(<map>, [fn])
* @returns Object
* @short Groups the array by <map>.
* @extra Will return an object with keys equal to the grouped values. <map> may be a mapping function, or a string acting as a shortcut. Optionally calls [fn] for each group.
* @example
*
* ['fee','fi','fum'].groupBy('length') -> { 2: ['fi'], 3: ['fee','fum'] }
+ [{age:35,name:'ken'},{age:15,name:'bob'}].groupBy(function(n) {
* return n.age;
* }); -> { 35: [{age:35,name:'ken'}], 15: [{age:15,name:'bob'}] }
*
***/
'groupBy': function(map, fn) {
var arr = this, result = {}, key;
arrayEach(arr, function(el, index) {
key = transformArgument(el, map, arr, [el, index, arr]);
if(!result[key]) result[key] = [];
result[key].push(el);
});
return object.each(result, fn);
},
/***
* @method compact([all] = false)
* @returns Array
* @short Removes all instances of %undefined%, %null%, and %NaN% from the array.
* @extra If [all] is %true%, all "falsy" elements will be removed. This includes empty strings, 0, and false.
* @example
*
* [1,null,2,undefined,3].compact() -> [1,2,3]
* [1,'',2,false,3].compact() -> [1,'',2,false,3]
* [1,'',2,false,3].compact(true) -> [1,2,3]
*
***/
'compact': function(all) {
var result = [];
arrayEach(this, function(el, i) {
if(object.isArray(el)) {
result.push(el.compact());
} else if(all && el) {
result.push(el);
} else if(!all && isDefined(el) && !isNull(el) && (!object.isNumber(el) || !isNaN(el))) {
result.push(el);
}
});
return result;
}
});
/***
* Number module
*
***/
function round(val, precision, method) {
var fn = Math[method || 'round'];
var multiplier = Math.abs(Math.pow(10, (precision || 0)));
if(precision < 0) multiplier = 1 / multiplier;
return fn(val * multiplier) / multiplier;
}
function getRange(start, stop, fn, step) {
var arr = [], i = parseInt(start), up = step > 0;
while((up && i <= stop) || (!up && i >= stop)) {
arr.push(i);
if(fn) fn.call(this, i);
i += step;
}
return arr;
}
extend(number, true, false, {
/***
* @method toNumber()
* @returns Number
* @short Returns a number. This is mostly for compatibility reasons.
* @example
*
* (420).toNumber() -> 420
*
***/
'toNumber': function() {
return parseFloat(this, 10);
},
/***
* @method ordinalize()
* @returns String
* @short Returns an ordinalized (English) string, i.e. "1st", "2nd", etc.
* @example
*
* (1).ordinalize() -> '1st';
* (2).ordinalize() -> '2nd';
* (8).ordinalize() -> '8th';
*
***/
'ordinalize': function() {
var suffix;
if(this >= 11 && this <= 13) {
suffix = 'th';
} else {
switch(this % 10) {
case 1: suffix = 'st'; break;
case 2: suffix = 'nd'; break;
case 3: suffix = 'rd'; break;
default: suffix = 'th';
}
}
return this.toString() + suffix;
},
/***
* @method pad(<place> = 0, [sign] = false, [base] = 10)
* @returns String
* @short Pads a number with "0" to <place>.
* @extra [sign] allows you to force the sign as well (+05, etc). [base] can change the base for numeral conversion.
* @example
*
* (5).pad(2) -> '05'
* (-5).pad(4) -> '-0005'
* (82).pad(3, true) -> '+082'
*
***/
'pad': function(place, sign, base) {
base = base || 10;
var str = this.toNumber() === 0 ? '' : this.toString(base).replace(/^-/, '');
str = padString(str, '0', place - str.replace(/\.\d+$/, '').length, 0);
if(sign || this < 0) {
str = (this < 0 ? '-' : '+') + str;
}
return str;
}
});
/***
* @method [unit]()
* @returns Number
* @short Takes the number as a corresponding unit of time and converts to milliseconds.
* @extra Method names can be both singular and plural. Note that as "a month" is ambiguous as a unit of time, %months% will be equivalent to 30.4375 days, the average number in a month. Be careful using %months% if you need exact precision.
* @example
*
* (5).milliseconds() -> 5
* (10).hours() -> 36000000
* (1).day() -> 86400000
*
***
* @method millisecond()
* @set unit
***
* @method milliseconds()
* @set unit
***
* @method second()
* @set unit
***
* @method seconds()
* @set unit
***
* @method minute()
* @set unit
***
* @method minutes()
* @set unit
***
* @method hour()
* @set unit
***
* @method hours()
* @set unit
***
* @method day()
* @set unit
***
* @method days()
* @set unit
***
* @method week()
* @set unit
***
* @method weeks()
* @set unit
***
* @method month()
* @set unit
***
* @method months()
* @set unit
***
* @method year()
* @set unit
***
* @method years()
* @set unit
***
* @method [unit]Before([d], [locale] = currentLocale)
* @returns Date
* @short Returns a date that is <n> units before [d], where <n> is the number.
* @extra [d] will accept a date object, timestamp, or text format. Note that "months" is ambiguous as a unit of time. If the target date falls on a day that does not exist (ie. August 31 -> February 31), the date will be shifted to the last day of the month. Be careful using %monthsBefore% if you need exact precision. See @date_format for more information.
* @example
*
* (5).daysBefore('tuesday') -> 5 days before tuesday of this week
* (1).yearBefore('January 23, 1997') -> January 23, 1996
*
***
* @method millisecondBefore()
* @set unitBefore
***
* @method millisecondsBefore()
* @set unitBefore
***
* @method secondBefore()
* @set unitBefore
***
* @method secondsBefore()
* @set unitBefore
***
* @method minuteBefore()
* @set unitBefore
***
* @method minutesBefore()
* @set unitBefore
***
* @method hourBefore()
* @set unitBefore
***
* @method hoursBefore()
* @set unitBefore
***
* @method dayBefore()
* @set unitBefore
***
* @method daysBefore()
* @set unitBefore
***
* @method weekBefore()
* @set unitBefore
***
* @method weeksBefore()
* @set unitBefore
***
* @method monthBefore()
* @set unitBefore
***
* @method monthsBefore()
* @set unitBefore
***
* @method yearBefore()
* @set unitBefore
***
* @method yearsBefore()
* @set unitBefore
***
* @method [unit]Ago()
* @returns Date
* @short Returns a date that is <n> units ago.
* @extra Note that "months" is ambiguous as a unit of time. If the target date falls on a day that does not exist (ie. August 31 -> February 31), the date will be shifted to the last day of the month. Be careful using %monthsAgo% if you need exact precision.
* @example
*
* (5).weeksAgo() -> 5 weeks ago
* (1).yearAgo() -> January 23, 1996
*
***
* @method millisecondAgo()
* @set unitAgo
***
* @method millisecondsAgo()
* @set unitAgo
***
* @method secondAgo()
* @set unitAgo
***
* @method secondsAgo()
* @set unitAgo
***
* @method minuteAgo()
* @set unitAgo
***
* @method minutesAgo()
* @set unitAgo
***
* @method hourAgo()
* @set unitAgo
***
* @method hoursAgo()
* @set unitAgo
***
* @method dayAgo()
* @set unitAgo
***
* @method daysAgo()
* @set unitAgo
***
* @method weekAgo()
* @set unitAgo
***
* @method weeksAgo()
* @set unitAgo
***
* @method monthAgo()
* @set unitAgo
***
* @method monthsAgo()
* @set unitAgo
***
* @method yearAgo()
* @set unitAgo
***
* @method yearsAgo()
* @set unitAgo
***
* @method [unit]After([d], [locale] = currentLocale)
* @returns Date
* @short Returns a date <n> units after [d], where <n> is the number.
* @extra [d] will accept a date object, timestamp, or text format. Note that "months" is ambiguous as a unit of time. If the target date falls on a day that does not exist (ie. August 31 -> February 31), the date will be shifted to the last day of the month. Be careful using %monthsAfter% if you need exact precision. See @date_format for more information.
* @example
*
* (5).daysAfter('tuesday') -> 5 days after tuesday of this week
* (1).yearAfter('January 23, 1997') -> January 23, 1998
*
***
* @method millisecondAfter()
* @set unitAfter
***
* @method millisecondsAfter()
* @set unitAfter
***
* @method secondAfter()
* @set unitAfter
***
* @method secondsAfter()
* @set unitAfter
***
* @method minuteAfter()
* @set unitAfter
***
* @method minutesAfter()
* @set unitAfter
***
* @method hourAfter()
* @set unitAfter
***
* @method hoursAfter()
* @set unitAfter
***
* @method dayAfter()
* @set unitAfter
***
* @method daysAfter()
* @set unitAfter
***
* @method weekAfter()
* @set unitAfter
***
* @method weeksAfter()
* @set unitAfter
***
* @method monthAfter()
* @set unitAfter
***
* @method monthsAfter()
* @set unitAfter
***
* @method yearAfter()
* @set unitAfter
***
* @method yearsAfter()
* @set unitAfter
***
* @method [unit]FromNow()
* @returns Date
* @short Returns a date <n> units from now.
* @extra Note that "months" is ambiguous as a unit of time. If the target date falls on a day that does not exist (ie. August 31 -> February 31), the date will be shifted to the last day of the month. Be careful using %monthsFromNow% if you need exact precision.
* @example
*
* (5).weeksFromNow() -> 5 weeks ago
* (1).yearFromNow() -> January 23, 1998
*
***
* @method millisecondFromNow()
* @set unitFromNow
***
* @method millisecondsFromNow()
* @set unitFromNow
***
* @method secondFromNow()
* @set unitFromNow
***
* @method secondsFromNow()
* @set unitFromNow
***
* @method minuteFromNow()
* @set unitFromNow
***
* @method minutesFromNow()
* @set unitFromNow
***
* @method hourFromNow()
* @set unitFromNow
***
* @method hoursFromNow()
* @set unitFromNow
***
* @method dayFromNow()
* @set unitFromNow
***
* @method daysFromNow()
* @set unitFromNow
***
* @method weekFromNow()
* @set unitFromNow
***
* @method weeksFromNow()
* @set unitFromNow
***
* @method monthFromNow()
* @set unitFromNow
***
* @method monthsFromNow()
* @set unitFromNow
***
* @method yearFromNow()
* @set unitFromNow
***
* @method yearsFromNow()
* @set unitFromNow
***/
function buildNumberToDateAlias(unit, multiplier) {
var add = 'add' + unit.capitalize() + 's';
function base() { return round(this * multiplier); }
function after() { return createDate(arguments)[add](this); }
function before() { return createDate(arguments)[add](-this); }
defineProperty(number.prototype, unit, base);
defineProperty(number.prototype, unit + 's', base);
defineProperty(number.prototype, unit + 'Before', before);
defineProperty(number.prototype, unit + 'sBefore', before);
defineProperty(number.prototype, unit + 'Ago', before);
defineProperty(number.prototype, unit + 'sAgo', before);
defineProperty(number.prototype, unit + 'After', after);
defineProperty(number.prototype, unit + 'sAfter', after);
defineProperty(number.prototype, unit + 'FromNow', after);
defineProperty(number.prototype, unit + 'sFromNow', after);
}
/***
* String module
*
***/
// WhiteSpace/LineTerminator as defined in ES5.1 plus Unicode characters in the Space, Separator category.
var getTrimmableCharacters = function() {
return '\u0009\u000A\u000B\u000C\u000D\u0020\u00A0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u2028\u2029\u3000\uFEFF';
}
function padString(str, p, left, right) {
var padding = String(p);
if(padding != p) {
padding = '';
}
if(!object.isNumber(left)) left = 1;
if(!object.isNumber(right)) right = 1;
return padding.repeat(left) + str + padding.repeat(right);
}
function buildTrim() {
var support = getTrimmableCharacters().match(/^\s+$/);
try { string.prototype.trim.call([1]); } catch(e) { support = false; }
var trimL = regexp('^['+getTrimmableCharacters()+']+');
var trimR = regexp('['+getTrimmableCharacters()+']+$');
extend(string, true, !support, {
/***
* @method trim[Side]()
* @returns String
* @short Removes leading and/or trailing whitespace from the string.
* @extra Whitespace is defined as line breaks, tabs, and any character in the "Space, Separator" Unicode category, conforming to the the ES5 spec. The standard %trim% method is only added when not fully supported natively.
* @example
*
* ' wasabi '.trim() -> 'wasabi'
* ' wasabi '.trimLeft() -> 'wasabi '
* ' wasabi '.trimRight() -> ' wasabi'
*
***
* @method trim()
* @set trimSide
***/
'trim': function() {
return this.toString().trimLeft().trimRight();
},
/***
* @method trimLeft()
* @set trimSide
***/
'trimLeft': function() {
return this.replace(trimL, '');
},
/***
* @method trimRight()
* @set trimSide
***/
'trimRight': function() {
return this.replace(trimR, '');
}
});
}
function buildString() {
buildTrim();
}
extend(string, true, false, {
/***
* @method capitalize([all] = false)
* @returns String
* @short Capitalizes the first character in the string.
* @extra If [all] is true, all words in the string will be capitalized.
* @example
*
* 'hello'.capitalize() -> 'hello'
* 'hello kitty'.capitalize() -> 'hello kitty'
* 'hello kitty'.capitalize(true) -> 'hello kitty'
*
*
***/
'capitalize': function(all) {
var reg = all ? /\b[a-z]/g : /^[a-z]/;
return this.toLowerCase().replace(reg, function(letter) {
return letter.toUpperCase();
});
},
/***
* @method repeat([num] = 0)
* @returns String
* @short Returns the string repeated [num] times.
* @example
*
* 'jumpy'.repeat(2) -> 'jumpyjumpy'
* 'a'.repeat(5) -> 'aaaaa'
*
***/
'repeat': function(num) {
var str = '', i = 0;
if(object.isNumber(num) && num > 0) {
while(i < num) {
str += this;
i++;
}
}
return str;
},
/***
* @method toNumber([base] = 10)
* @returns Number
* @short Converts the string into a number.
* @extra Any value with a "." fill be converted to a floating point value, otherwise an integer.
* @example
*
* '153'.toNumber() -> 153
* '12,000'.toNumber() -> 12000
* '10px'.toNumber() -> 10
* 'ff'.toNumber(16) -> 255
*
***/
'toNumber': function(base) {
var str = this.replace(/,/g, '');
return str.match(/\./) ? parseFloat(str) : parseInt(str, base || 10);
},
/***
* @method first([n] = 1)
* @returns String
* @short Returns the first [n] characters of the string.
* @example
*
* 'lucky charms'.first() -> 'l'
* 'lucky charms'.first(3) -> 'luc'
*
***/
'first': function(num) {
num = isUndefined(num) ? 1 : num;
return this.substr(0, num);
},
/***
* @method last([n] = 1)
* @returns String
* @short Returns the last [n] characters of the string.
* @example
*
* 'lucky charms'.last() -> 's'
* 'lucky charms'.last(3) -> 'rms'
*
***/
'last': function(num) {
num = isUndefined(num) ? 1 : num;
var start = this.length - num < 0 ? 0 : this.length - num;
return this.substr(start);
},
/***
* @method to([index] = end)
* @returns String
* @short Returns a section of the string ending at [index].
* @example
*
* 'lucky charms'.to() -> 'lucky charms'
* 'lucky charms'.to(7) -> 'lucky ch'
*
***/
'to': function(num) {
if(isUndefined(num)) num = this.length;
return this.slice(0, num);
},
/***
* @method toDate([locale])
* @returns Date
* @short Creates a date from the string.
* @extra Accepts a wide range of input. [locale] allows you to specify a locale code. See @date_format for more information.
* @example
*
* 'January 25, 2015'.toDate() -> same as Date.create('January 25, 2015')
* 'yesterday'.toDate() -> same as Date.create('yesterday')
* 'next Monday'.toDate() -> same as Date.create('next Monday')
*
***/
'toDate': function(locale) {
return createDate([this.toString(), locale]);
},
/***
* @method assign(<obj1>, <obj2>, ...)
* @returns String
* @short Assigns variables to tokens in a string.
* @extra If an object is passed, it's properties can be assigned using the object's keys. If a non-object (string, number, etc.) is passed it can be accessed by the argument number beginning with 1 (as with regex tokens). Multiple objects can be passed and will be merged together.
* @example
*
* 'Welcome, Mr. {name}.'.assign({ name: 'Franklin' }) -> 'Welcome, Mr. Franklin.'
* 'You are {1} years old today.'.assign(14) -> 'You are 14 years old today.'
* '{n} and {r}'.assign({ n: 'Cheech' }, { r: 'Chong' }) -> 'Cheech and Chong'
*
***/
'assign': function() {
var assign = {};
multiArgs(arguments, function(a, i) {
if(object.isObject(a)) {
object.merge(assign, a);
} else {
assign[i + 1] = a;
}
});
return this.replace(/\{(.+?)\}/g, function(m, key) {
return assign.hasOwnProperty(key) ? assign[key] : m;
});
}
});
/***
* Date module
*
***/
var TimeFormat = ['hour','minute','second','millisecond','meridian','utc','offset_sign','offset_hours','offset_minutes']
var RequiredTime = '(\\d{1,2}):?(\\d{2})?:?(\\d{2})?(?:\\.(\\d{1,6}))?(am|pm)?(?:(Z)|(?:([+-])(\\d{2})(?::?(\\d{2}))?)?)?';
var OptionalTime = '\\s*(?:(?:t|at |\\s+)' + RequiredTime + ')?';
var LowerAsianDigits = '一二三四五六七八九';
var UpperAsianDigits = '十百千万';
var AsianDigitReg = regexp('[' + LowerAsianDigits + UpperAsianDigits + ']', 'g');
var DateInputFormats = [];
var DateArgumentUnits;
var DateUnitsReversed;
var StaticInputFormats = [
// @date_format 2010
{ src: '(\\d{4})', to: ['year'] },
// @date_format 2010-05
// @date_format 2010.05
// @date_format 2010/05
// @date_format 2010-05-25 (ISO8601)
// @date_format 2010-05-25T12:30:40.299Z (ISO8601)
// @date_format 2010-05-25T12:30:40.299+01:00 (ISO8601)
// @date_format 2010.05.25
// @date_format 2010/05/25
{ src: '([+-])?(\\d{4})[-.]?({month})[-.]?(\\d{1,2})?', to: ['year_sign','year','month','date'] },
// @date_format 05-25
// @date_format 05/25
// @date_format 05.25
// @date_format 05-25-2010
// @date_format 05/25/2010
// @date_format 05.25.2010
{ src: '(\\d{1,2})[-.\\/]({month})[-.\\/]?(\\d{2,4})?', to: ['month','date','year'], variant: true },
// @date_format Date(628318530718)
{ src: '\\/Date\\((\\d+(?:\\+\\d{4})?)\\)\\/', to: ['timestamp'], time: false }
];
var DateOutputFormats = [
{
token: 'f{1,4}|ms|milliseconds',
format: function(d) {
return d.getMilliseconds();
}
},
{
token: 'ss?|seconds',
format: function(d, len) {
return d.getSeconds();
}
},
{
token: 'mm?|minutes',
format: function(d, len) {
return d.getMinutes();
}
},
{
token: 'hh?|hours|12hr',
format: function(d) {
return getShortHour(d);
}
},
{
token: 'HH?|24hr',
format: function(d) {
return d.getHours();
}
},
{
token: 'dd?|date|day',
format: function(d) {
return d.getDate();
}
},
{
token: 'dow|weekday',
word: true,
format: function(d, loc, n, t) {
return loc['weekdays'][d.getDay() + (n - 1) * 7];
}
},
{
token: 'MM?',
format: function(d) {
return d.getMonth() + 1;
}
},
{
token: 'mon|month',
word: true,
format: function(d, loc, n, len) {
return loc['months'][d.getMonth() + (n - 1) * 12];
}
},
{
token: 'y{2,4}|year',
format: function(d) {
return d.getFullYear();
}
},
{
token: '[Tt]{1,2}',
format: function(d, loc, n, format) {
var m = getMeridian(d);
if(format.length === 1) m = m.first();
if(format.first() === 'T') m = m.toUpperCase();
return m;
}
},
{
token: 'z{1,4}|tz|timezone',
text: true,
format: function(d, loc, n, format) {
var tz = d.getUTCOffset();
if(format == 'z' || format == 'zz') {
tz = tz.replace(/(\d{2})(\d{2})/, function(f,h,m) {
return h.toNumber().pad(format.length);
});
}
return tz;
}
},
{
token: 'iso(tz|timezone)',
format: function(d) {
return d.getUTCOffset(true);
}
},
{
token: 'ord',
format: function(d) {
return d.getDate().ordinalize();
}
}
];
var DateUnits = [
{
unit: 'year',
method: 'FullYear',
multiplier: function(d) {
var adjust = d ? (d.isLeapYear() ? 1 : 0) : 0.25;
return (365 + adjust) * 24 * 60 * 60 * 1000;
}
},
{
unit: 'month',
method: 'Month',
multiplier: function(d, ms) {
var days = 30.4375, inMonth;
if(d) {
inMonth = d.daysInMonth();
if(ms <= inMonth.days()) {
days = inMonth;
}
}
return days * 24 * 60 * 60 * 1000;
}
},
{
unit: 'week',
method: 'Week',
multiplier: function() {
return 7 * 24 * 60 * 60 * 1000;
}
},
{
unit: 'day',
method: 'Date',
multiplier: function() {
return 24 * 60 * 60 * 1000;
}
},
{
unit: 'hour',
method: 'Hours',
multiplier: function() {
return 60 * 60 * 1000;
}
},
{
unit: 'minute',
method: 'Minutes',
multiplier: function() {
return 60 * 1000;
}
},
{
unit: 'second',
method: 'Seconds',
multiplier: function() {
return 1000;
}
},
{
unit: 'millisecond',
method: 'Milliseconds',
multiplier: function() {
return 1;
}
}
];
// Date Localization
var Localizations = {};
var CommonLocales = {
'en': '2;;January,February,March,April,May,June,July,August,September,October,November,December;Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday;millisecond:|s,second:|s,minute:|s,hour:|s,day:|s,week:|s,month:|s,year:|s;one,two,three,four,five,six,seven,eight,nine,ten;a,an,the;the,st|nd|rd|th,of;{num} {unit} {sign},{num} {unit=4-5} {sign} {day},{weekday?} {month} {date}{2} {year?} {time?},{date} {month} {year},{month} {year},{shift?} {weekday} {time?},{shift} week {weekday} {time?},{shift} {unit=5-7},{1} {edge} of {shift?} {unit=4-7?}{month?}{year?},{weekday} {3} {shift} week,{1} {date}{2} of {month},{1}{month?} {date?}{2} of {shift} {unit=6-7},{day} at {time?},{time} {day};{Month} {d}, {yyyy};,yesterday,today,tomorrow;,ago|before,,from now|after|from;,last,the|this,next;last day,end,,first day|beginning',
'ja': '1;月;;日曜日,月曜日,火曜日,水曜日,木曜日,金曜日,土曜日;ミリ秒,秒,分,時間,日,週間|週,ヶ月|ヵ月|月,年;;;;{num}{unit}{sign},{shift}{unit=5-7}{weekday?},{year}年{month?}月?{date?}日?,{month}月{date?}日?,{date}日;{yyyy}年{M}月{d}日;一昨日,昨日,今日,明日,明後日;,前,,後;,去|先,,来',
'ko': '1;월;;일요일,월요일,화요일,수요일,목요일,금요일,토요일;밀리초,초,분,시간,일,주,개월|달,년;일|한,이,삼,사,오,육,칠,팔,구,십;;;{num}{unit} {sign},{shift} {unit=5-7},{shift} {unit=5?} {weekday},{year}년{month?}월?{date?}일?,{month}월{date?}일?,{date}일;{yyyy}년{M}월{d}일;그저께,어제,오늘,내일,모레;,전,,후;,지난|작,이번,다음|내',
'ru': '4;;Январ:я|ь,Феврал:я|ь,Март:а|,Апрел:я|ь,Ма:я|й,Июн:я|ь,Июл:я|ь,Август:а|,Сентябр:я|ь,Октябр:я|ь,Ноябр:я|ь,Декабр:я|ь;Воскресенье,Понедельник,Вторник,Среда,Четверг,Пятница,Суббота;миллисекунд:а|у|ы|,секунд:а|у|ы|,минут:а|у|ы|,час:||а|ов,день|день|дня|дней,недел:я|ю|и|ь|е,месяц:||а|ев|е,год|год|года|лет|году;од:ин|ну,дв:а|е,три,четыре,пять,шесть,семь,восемь,девять,десять;;в|на,года;{num} {unit} {sign},{sign} {num} {unit},{date} {month} {year?} {2},{month} {year},{1} {shift} {unit=5-7};{d} {month} {yyyy} года;позавчера,вчера,сегодня,завтра,послезавтра;,назад,,через;,прошло:й|м,,следующе:й|м',
'es': '6;;enero,febrero,marzo,abril,mayo,junio,julio,agosto,septiembre,octubre,noviembre,diciembre;domingo,lunes,martes,miércoles|miercoles,jueves,viernes,sábado|sabado;milisegundo:|s,segundo:|s,minuto:|s,hora:|s,día|días|dia|dias,semana:|s,mes:|es,año|años|ano|anos;uno,dos,tres,cuatro,cinco,seis,siete,ocho,nueve,diez;;el,de;{sign} {num} {unit},{num} {unit} {sign},{date?} {2} {month} {2} {year?},{1} {unit=5-7} {shift},{1} {shift} {unit=5-7};{d} de {month} de {yyyy};anteayer,ayer,hoy,mañana|manana;,hace,,de ahora;,pasad:o|a,,próximo|próxima|proximo|proxima',
'pt': '6;;janeiro,fevereiro,março,abril,maio,junho,julho,agosto,setembro,outubro,novembro,dezembro;domingo,segunda-feira,terça-feira,quarta-feira,quinta-feira,sexta-feira,sábado|sabado;milisegundo:|s,segundo:|s,minuto:|s,hora:|s,dia:|s,semana:|s,mês|mêses|mes|meses,ano:|s;um,dois,três|tres,quatro,cinco,seis,sete,oito,nove,dez,uma,duas;;a,de;{num} {unit} {sign},{sign} {num} {unit},{date?} {2} {month} {2} {year?},{1} {unit=5-7} {shift},{1} {shift} {unit=5-7};{d} de {month} de {yyyy};anteontem,ontem,hoje,amanh:ã|a;,atrás|atras|há|ha,,daqui a;,passad:o|a,,próximo|próxima|proximo|proxima',
'fr': '2;;janvier,février|fevrier,mars,avril,mai,juin,juillet,août,septembre,octobre,novembre,décembre|decembre;dimanche,lundi,mardi,mercredi,jeudi,vendredi,samedi;milliseconde:|s,seconde:|s,minute:|s,heure:|s,jour:|s,semaine:|s,mois,an:|s|née|nee;un:|e,deux,trois,quatre,cinq,six,sept,huit,neuf,dix;;l\'|la|le;{sign} {num} {unit},{sign} {num} {unit},{1} {date?} {month} {year?},{1} {unit=5-7} {shift};{d} {month} {yyyy};,hier,aujourd\'hui,demain;,il y a,,dans|d\'ici;,derni:er|ère|ere,,prochain:|e',
'it': '2;;Gennaio,Febbraio,Marzo,Aprile,Maggio,Giugno,Luglio,Agosto,Settembre,Ottobre,Novembre,Dicembre;Domenica,Luned:ì|i,Marted:ì|i,Mercoled:ì|i,Gioved:ì|i,Venerd:ì|i,Sabato;millisecond:o|i,second:o|i,minut:o|i,or:a|e,giorn:o|i,settiman:a|e,mes:e|i,ann:o|i;un:|\'|a|o,due,tre,quattro,cinque,sei,sette,otto,nove,dieci;;l\'|la|il;{num} {unit} {sign},{weekday?} {date?} {month} {year?},{1} {unit=5-7} {shift},{1} {shift} {unit=5-7};{d} {month} {yyyy};,ieri,oggi,domani,dopodomani;,fa,,da adesso;,scors:o|a,,prossim:o|a',
'de': '2;;Januar,Februar,März|Marz,April,Mai,Juni,Juli,August,September,Oktober,November,Dezember;Sonntag,Montag,Dienstag,Mittwoch,Donnerstag,Freitag,Samstag;Millisekunde:|n,Sekunde:|n,Minute:|n,Stunde:|n,Tag:|en,Woche:|n,Monat:|en,Jahr:|en;ein:|e|er|em|en,zwei,drei,vier,fuenf,sechs,sieben,acht,neun,zehn;;der;{sign} {num} {unit},{num} {unit} {sign},{num} {unit} {sign},{sign} {num} {unit},{weekday?} {date?} {month} {year?},{shift} {unit=5-7};{d}. {Month} {yyyy};vorgestern,gestern,heute,morgen,übermorgen|ubermorgen|uebermorgen;,vor:|her,,in;,letzte:|r|n|s,,nächste:|r|n|s+naechste:|r|n|s',
'zh-TW': '1;月;;日,一,二,三,四,五,六;毫秒,秒鐘,分鐘,小時,天,個星期|週,個月,年;;;日|號;{num}{unit}{sign},星期{weekday},{shift}{unit=5-7},{shift}{unit=5}{weekday},{year}年{month?}月?{date?}{1},{month}月{date?}{1},{date}{1};{yyyy}年{M}月{d}日;前天,昨天,今天,明天,後天;,前,,後;,上|去,這,下|明',
'zh-CN': '1;月;;日,一,二,三,四,五,六;毫秒,秒钟,分钟,小时,天,个星期|周,个月,年;;;日|号;{num}{unit}{sign},星期{weekday},{shift}{unit=5-7},{shift}{unit=5}{weekday},{year}年{month?}月?{date?}{1},{month}月{date?}{1},{date}{1};{yyyy}年{M}月{d}日;前天,昨天,今天,明天,后天;,前,,后;,上|去,这,下|明'
}
function checkLocaleFormatsAdded(loc) {
var addFormat = date.addFormat, code = loc['code'];
if(loc.formatsAdded) return;
addFormat('(' + loc['months'].compact().join('|') + ')', ['month'], code);
addFormat('(' + loc['weekdays'].compact().join('|') + ')', ['weekday'], code);
addFormat('(' + loc['modifiers'].filter(function(m){ return m.name === 'day'; }).map('text').join('|') + ')', ['day'], code);
arrayEach(loc['formats'], function(src) {
var to = [];
src = src.replace(/\s+/g, '[-,. ]*');
src = src.replace(/\{(.+?)\}/g, function(all, k) {
var opt = k.match(/\?$/), slice = k.match(/(\d)(?:-(\d))?/), nc = k.match(/^\d+$/), key = k.replace(/[^a-z]+$/, ''), value, arr;
if(key === 'time') {
to = to.concat(TimeFormat);
return opt ? OptionalTime : RequiredTime;
}
if(nc) {
value = loc['optionals'][nc[0] - 1];
} else if(loc[key]) {
value = loc[key];
} else if(loc[key + 's']) {
value = loc[key + 's'];
if(slice) {
// Can't use filter here as Prototype hijacks the method and doesn't
// pass an index, so use a simple loop instead!
arr = [];
arrayEach(value, function(m,i) {
var mod = i % (loc['units'] ? 8 : value.length);
if(mod >= slice[1] && mod <= (slice[2] || slice[1])) {
arr.push(m);
}
});
value = arr;
}
value = value.compact().join('|');
}
if(nc) {
return '(?:' + value + ')?';
} else {
to.push(key);
return '(' + value + ')' + (opt ? '?' : '');
}
});
addFormat(src, to, code);
});
loc.formatsAdded = true;
}
function getLocalization(code, fallback, set) {
if(fallback && (!object.isString(code) || !code)) code = Date['currentLocale'];
if(code && !Localizations[code]) initializeLocalization(code, set);
return Localizations[code];
}
function initializeLocalization(code, set) {
set = set || getCommonLocalization(code);
if(!set) {
throw new Error('Invalid locale.');
}
function eachAlternate(str, fn) {
str = str.split('+').map(function(split) {
return split.replace(/(.+):(.+)$/, function(full, base, suffixes) {
return suffixes.split('|').map(function(suffix) {
return base + suffix;
}).join('|');
});
}).join('|');
return arrayEach(str.split('|'), fn);
}
function setArray(name, abbreviate, multiple) {
var arr = [];
if(!set[name]) return;
arrayEach(set[name], function(el, i) {
eachAlternate(el, function(str, j) {
arr[j * multiple + i] = str.toLowerCase();
});
});
if(abbreviate) arr = arr.concat(set[name].map(function(str)