mdot_server/app/lib/sugar.js

12628 lines
373 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Sugar v2.0.0
*
* Freely distributable and licensed under the MIT-style license.
* Copyright (c) Andrew Plummer
* https://sugarjs.com/
*
* ---------------------------- */
(function() {
'use strict';
/***
* @module Core
* @description Core functionality including the ability to define methods and
* extend onto natives.
*
***/
// The global to export.
var Sugar;
// The name of Sugar in the global namespace.
var SUGAR_GLOBAL = 'Sugar';
// Natives available on initialization. Letting Object go first to ensure its
// global is set by the time the rest are checking for chainable Object methods.
var NATIVE_NAMES = 'Object Number String Array Date RegExp Function';
// Static method flag
var STATIC = 0x1;
// Instance method flag
var INSTANCE = 0x2;
// IE8 has a broken defineProperty but no defineProperties so this saves a try/catch.
var PROPERTY_DESCRIPTOR_SUPPORT = !!(Object.defineProperty && Object.defineProperties);
// The global context. Rhino uses a different "global" keyword so
// do an extra check to be sure that it's actually the global context.
var globalContext = typeof global !== 'undefined' && global.Object === Object ? global : this;
// Is the environment node?
var hasExports = typeof module !== 'undefined' && module.exports;
// Whether object instance methods can be mapped to the prototype.
var allowObjectPrototype = false;
// A map from Array to SugarArray.
var namespacesByName = {};
// A map from [object Object] to namespace.
var namespacesByClassString = {};
// Defining properties.
var defineProperty = PROPERTY_DESCRIPTOR_SUPPORT ? Object.defineProperty : definePropertyShim;
// A default chainable class for unknown types.
var DefaultChainable = getNewChainableClass('Chainable');
// Global methods
function setupGlobal() {
Sugar = globalContext[SUGAR_GLOBAL];
if (Sugar) {
// Reuse already defined Sugar global object.
return;
}
Sugar = function(arg) {
forEachProperty(Sugar, function(sugarNamespace, name) {
// Although only the only enumerable properties on the global
// object are Sugar namespaces, environments that can't set
// non-enumerable properties will step through the utility methods
// as well here, so use this check to only allow true namespaces.
if (hasOwn(namespacesByName, name)) {
sugarNamespace.extend(arg);
}
});
return Sugar;
};
if (hasExports) {
module.exports = Sugar;
} else {
try {
globalContext[SUGAR_GLOBAL] = Sugar;
} catch (e) {
// Contexts such as QML have a read-only global context.
}
}
forEachProperty(NATIVE_NAMES.split(' '), function(name) {
createNamespace(name);
});
setGlobalProperties();
}
/***
* @method createNamespace(<name>)
* @returns Namespace
* @global
* @short Creates a new Sugar namespace.
* @extra This method is for plugin developers who want to define methods to be
* used with natives that Sugar does not handle by default. The new
* namespace will appear on the `Sugar` global with all the methods of
* normal namespaces, including the ability to define new methods. When
* extended, any defined methods will be mapped to `name` in the global
* context.
*
* @example
*
* Sugar.createNamespace('Boolean');
*
***/
function createNamespace(name) {
// Is the current namespace Object?
var isObject = name === 'Object';
// A Sugar namespace is also a chainable class: Sugar.Array, etc.
var sugarNamespace = getNewChainableClass(name, true);
/***
* @method extend([options])
* @returns Sugar
* @global
* @namespace
* @short Extends Sugar defined methods onto natives.
* @extra This method can be called on individual namespaces like
* `Sugar.Array` or on the `Sugar` global itself, in which case
* [options] will be forwarded to each `extend` call. For more,
* see `extending`.
*
* @options
*
* methods An array of method names to explicitly extend.
*
* except An array of method names or global namespaces (`Array`,
* `String`) to explicitly exclude. Namespaces should be the
* actual global objects, not strings.
*
* namespaces An array of global namespaces (`Array`, `String`) to
* explicitly extend. Namespaces should be the actual
* global objects, not strings.
*
* enhance A shortcut to disallow all "enhance" flags at once
* (flags listed below). For more, see `enhanced methods`.
* Default is `true`.
*
* enhanceString A boolean allowing String enhancements. Default is `true`.
*
* enhanceArray A boolean allowing Array enhancements. Default is `true`.
*
* objectPrototype A boolean allowing Sugar to extend Object.prototype
* with instance methods. This option is off by default
* and should generally not be used except with caution.
* For more, see `object methods`.
*
* @example
*
* Sugar.Array.extend();
* Sugar.extend();
*
***/
var extend = function (opts) {
var nativeClass = globalContext[name], nativeProto = nativeClass.prototype;
var staticMethods = {}, instanceMethods = {}, methodsByName;
function objectRestricted(name, target) {
return isObject && target === nativeProto &&
(!allowObjectPrototype || name === 'get' || name === 'set');
}
function arrayOptionExists(field, val) {
var arr = opts[field];
if (arr) {
for (var i = 0, el; el = arr[i]; i++) {
if (el === val) {
return true;
}
}
}
return false;
}
function arrayOptionExcludes(field, val) {
return opts[field] && !arrayOptionExists(field, val);
}
function disallowedByFlags(methodName, target, flags) {
// Disallowing methods by flag currently only applies if methods already
// exist to avoid enhancing native methods, as aliases should still be
// extended (i.e. Array#all should still be extended even if Array#every
// is being disallowed by a flag).
if (!target[methodName] || !flags) {
return false;
}
for (var i = 0; i < flags.length; i++) {
if (opts[flags[i]] === false) {
return true;
}
}
}
function namespaceIsExcepted() {
return arrayOptionExists('except', nativeClass) ||
arrayOptionExcludes('namespaces', nativeClass);
}
function methodIsExcepted(methodName) {
return arrayOptionExists('except', methodName);
}
function canExtend(methodName, method, target) {
return !objectRestricted(methodName, target) &&
!disallowedByFlags(methodName, target, method.flags) &&
!methodIsExcepted(methodName);
}
opts = opts || {};
methodsByName = opts.methods;
if (namespaceIsExcepted()) {
return;
} else if (isObject && typeof opts.objectPrototype === 'boolean') {
// Store "objectPrototype" flag for future reference.
allowObjectPrototype = opts.objectPrototype;
}
forEachProperty(methodsByName || sugarNamespace, function(method, methodName) {
if (methodsByName) {
// If we have method names passed in an array,
// then we need to flip the key and value here
// and find the method in the Sugar namespace.
methodName = method;
method = sugarNamespace[methodName];
}
if (hasOwn(method, 'instance') && canExtend(methodName, method, nativeProto)) {
instanceMethods[methodName] = method.instance;
}
if(hasOwn(method, 'static') && canExtend(methodName, method, nativeClass)) {
staticMethods[methodName] = method;
}
});
// Accessing the extend target each time instead of holding a reference as
// it may have been overwritten (for example Date by Sinon). Also need to
// access through the global to allow extension of user-defined namespaces.
extendNative(nativeClass, staticMethods);
extendNative(nativeProto, instanceMethods);
if (!methodsByName) {
// If there are no method names passed, then
// all methods in the namespace will be extended
// to the native. This includes all future defined
// methods, so add a flag here to check later.
setProperty(sugarNamespace, 'active', true);
}
return Sugar;
};
function defineWithOptionCollect(methodName, instance, args) {
setProperty(sugarNamespace, methodName, function(arg1, arg2, arg3) {
var opts = collectDefineOptions(arg1, arg2, arg3);
defineMethods(sugarNamespace, opts.methods, instance, args, opts.last);
return sugarNamespace;
});
}
/***
* @method defineStatic(...)
* @returns Namespace
* @namespace
* @short Defines static methods on the namespace that can later be extended
* onto the native globals.
* @extra Accepts either a single object mapping names to functions, or name
* and function as two arguments. If `extend` was previously called
* with no arguments, the method will be immediately mapped to its
* native when defined.
*
* @example
*
* Sugar.Number.defineStatic({
* isOdd: function (num) {
* return num % 2 === 1;
* }
* });
*
***/
defineWithOptionCollect('defineStatic', STATIC);
/***
* @method defineInstance(...)
* @returns Namespace
* @namespace
* @short Defines methods on the namespace that can later be extended as
* instance methods onto the native prototype.
* @extra Accepts either a single object mapping names to functions, or name
* and function as two arguments. All functions should accept the
* native for which they are mapped as their first argument, and should
* never refer to `this`. If `extend` was previously called with no
* arguments, the method will be immediately mapped to its native when
* defined.
*
* Methods cannot accept more than 4 arguments in addition to the
* native (5 arguments total). Any additional arguments will not be
* mapped. If the method needs to accept unlimited arguments, use
* `defineInstanceWithArguments`. Otherwise if more options are
* required, use an options object instead.
*
* @example
*
* Sugar.Number.defineInstance({
* square: function (num) {
* return num * num;
* }
* });
*
***/
defineWithOptionCollect('defineInstance', INSTANCE);
/***
* @method defineInstanceAndStatic(...)
* @returns Namespace
* @namespace
* @short A shortcut to define both static and instance methods on the namespace.
* @extra This method is intended for use with `Object` instance methods. Sugar
* will not map any methods to `Object.prototype` by default, so defining
* instance methods as static helps facilitate their proper use.
*
* @example
*
* Sugar.Object.defineInstanceAndStatic({
* isAwesome: function (obj) {
* // check if obj is awesome!
* }
* });
*
***/
defineWithOptionCollect('defineInstanceAndStatic', INSTANCE | STATIC);
/***
* @method defineStaticWithArguments(...)
* @returns Namespace
* @namespace
* @short Defines static methods that collect arguments.
* @extra This method is identical to `defineStatic`, except that when defined
* methods are called, they will collect any arguments past `n - 1`,
* where `n` is the number of arguments that the method accepts.
* Collected arguments will be passed to the method in an array
* as the last argument defined on the function.
*
* @example
*
* Sugar.Number.defineStaticWithArguments({
* addAll: function (num, args) {
* for (var i = 0; i < args.length; i++) {
* num += args[i];
* }
* return num;
* }
* });
*
***/
defineWithOptionCollect('defineStaticWithArguments', STATIC, true);
/***
* @method defineInstanceWithArguments(...)
* @returns Namespace
* @namespace
* @short Defines instance methods that collect arguments.
* @extra This method is identical to `defineInstance`, except that when
* defined methods are called, they will collect any arguments past
* `n - 1`, where `n` is the number of arguments that the method
* accepts. Collected arguments will be passed to the method as the
* last argument defined on the function.
*
* @example
*
* Sugar.Number.defineInstanceWithArguments({
* addAll: function (num, args) {
* for (var i = 0; i < args.length; i++) {
* num += args[i];
* }
* return num;
* }
* });
*
***/
defineWithOptionCollect('defineInstanceWithArguments', INSTANCE, true);
/***
* @method defineStaticPolyfill(...)
* @returns Namespace
* @namespace
* @short Defines static methods that are mapped onto the native if they do
* not already exist.
* @extra Intended only for use creating polyfills that follow the ECMAScript
* spec. Accepts either a single object mapping names to functions, or
* name and function as two arguments.
*
* @example
*
* Sugar.Object.defineStaticPolyfill({
* keys: function (obj) {
* // get keys!
* }
* });
*
***/
setProperty(sugarNamespace, 'defineStaticPolyfill', function(arg1, arg2, arg3) {
var opts = collectDefineOptions(arg1, arg2, arg3);
extendNative(globalContext[name], opts.methods, true, opts.last);
});
/***
* @method defineInstancePolyfill(...)
* @returns Namespace
* @namespace
* @short Defines instance methods that are mapped onto the native prototype
* if they do not already exist.
* @extra Intended only for use creating polyfills that follow the ECMAScript
* spec. Accepts either a single object mapping names to functions, or
* name and function as two arguments. This method differs from
* `defineInstance` as there is no static signature (as the method
* is mapped as-is to the native), so it should refer to its `this`
* object.
*
* @example
*
* Sugar.Array.defineInstancePolyfill({
* indexOf: function (arr, el) {
* // index finding code here!
* }
* });
*
***/
setProperty(sugarNamespace, 'defineInstancePolyfill', function(arg1, arg2, arg3) {
var opts = collectDefineOptions(arg1, arg2, arg3);
extendNative(globalContext[name].prototype, opts.methods, true, opts.last);
// Map instance polyfills to chainable as well.
forEachProperty(opts.methods, function(fn, methodName) {
defineChainableMethod(sugarNamespace, methodName, fn);
});
});
/***
* @method alias(<toName>, <fromName>)
* @returns Namespace
* @namespace
* @short Aliases one Sugar method to another.
*
* @example
*
* Sugar.Array.alias('all', 'every');
*
***/
setProperty(sugarNamespace, 'alias', function(name, source) {
var method = typeof source === 'string' ? sugarNamespace[source] : source;
setMethod(sugarNamespace, name, method);
});
// Each namespace can extend only itself through its .extend method.
setProperty(sugarNamespace, 'extend', extend);
// Cache the class to namespace relationship for later use.
namespacesByName[name] = sugarNamespace;
namespacesByClassString['[object ' + name + ']'] = sugarNamespace;
mapNativeToChainable(name);
mapObjectChainablesToNamespace(sugarNamespace);
// Export
return Sugar[name] = sugarNamespace;
}
function setGlobalProperties() {
setProperty(Sugar, 'extend', Sugar);
setProperty(Sugar, 'toString', toString);
setProperty(Sugar, 'createNamespace', createNamespace);
setProperty(Sugar, 'util', {
'hasOwn': hasOwn,
'getOwn': getOwn,
'setProperty': setProperty,
'classToString': classToString,
'defineProperty': defineProperty,
'forEachProperty': forEachProperty,
'mapNativeToChainable': mapNativeToChainable
});
}
function toString() {
return SUGAR_GLOBAL;
}
// Defining Methods
function defineMethods(sugarNamespace, methods, type, args, flags) {
forEachProperty(methods, function(method, methodName) {
var instanceMethod, staticMethod = method;
if (args) {
staticMethod = wrapMethodWithArguments(method);
}
if (flags) {
staticMethod.flags = flags;
}
// A method may define its own custom implementation, so
// make sure that's not the case before creating one.
if (type & INSTANCE && !method.instance) {
instanceMethod = wrapInstanceMethod(method, args);
setProperty(staticMethod, 'instance', instanceMethod);
}
if (type & STATIC) {
setProperty(staticMethod, 'static', true);
}
setMethod(sugarNamespace, methodName, staticMethod);
if (sugarNamespace.active) {
// If the namespace has been activated (.extend has been called),
// then map this method as well.
sugarNamespace.extend(methodName);
}
});
}
function collectDefineOptions(arg1, arg2, arg3) {
var methods, last;
if (typeof arg1 === 'string') {
methods = {};
methods[arg1] = arg2;
last = arg3;
} else {
methods = arg1;
last = arg2;
}
return {
last: last,
methods: methods
};
}
function wrapInstanceMethod(fn, args) {
return args ? wrapMethodWithArguments(fn, true) : wrapInstanceMethodFixed(fn);
}
function wrapMethodWithArguments(fn, instance) {
// Functions accepting enumerated arguments will always have "args" as the
// last argument, so subtract one from the function length to get the point
// at which to start collecting arguments. If this is an instance method on
// a prototype, then "this" will be pushed into the arguments array so start
// collecting 1 argument earlier.
var startCollect = fn.length - 1 - (instance ? 1 : 0);
return function() {
var args = [], collectedArgs = [], len;
if (instance) {
args.push(this);
}
len = Math.max(arguments.length, startCollect);
// Optimized: no leaking arguments
for (var i = 0; i < len; i++) {
if (i < startCollect) {
args.push(arguments[i]);
} else {
collectedArgs.push(arguments[i]);
}
}
args.push(collectedArgs);
return fn.apply(this, args);
};
}
function wrapInstanceMethodFixed(fn) {
switch(fn.length) {
// Wrapped instance methods will always be passed the instance
// as the first argument, but requiring the argument to be defined
// may cause confusion here, so return the same wrapped function regardless.
case 0:
case 1:
return function() {
return fn(this);
};
case 2:
return function(a) {
return fn(this, a);
};
case 3:
return function(a, b) {
return fn(this, a, b);
};
case 4:
return function(a, b, c) {
return fn(this, a, b, c);
};
case 5:
return function(a, b, c, d) {
return fn(this, a, b, c, d);
};
}
}
// Method helpers
function extendNative(target, source, polyfill, override) {
forEachProperty(source, function(method, name) {
if (polyfill && !override && target[name]) {
// Method exists, so bail.
return;
}
setProperty(target, name, method);
});
}
function setMethod(sugarNamespace, methodName, method) {
sugarNamespace[methodName] = method;
if (method.instance) {
defineChainableMethod(sugarNamespace, methodName, method.instance, true);
}
}
// Chainables
function getNewChainableClass(name) {
var fn = function SugarChainable(obj, arg) {
if (!(this instanceof fn)) {
return new fn(obj, arg);
}
if (this.constructor !== fn) {
// Allow modules to define their own constructors.
obj = this.constructor.apply(obj, arguments);
}
this.raw = obj;
};
setProperty(fn, 'toString', function() {
return SUGAR_GLOBAL + name;
});
setProperty(fn.prototype, 'valueOf', function() {
return this.raw;
});
return fn;
}
function defineChainableMethod(sugarNamespace, methodName, fn) {
var wrapped = wrapWithChainableResult(fn), existing, collision, dcp;
dcp = DefaultChainable.prototype;
existing = dcp[methodName];
// If the method was previously defined on the default chainable, then a
// collision exists, so set the method to a disambiguation function that will
// lazily evaluate the object and find it's associated chainable. An extra
// check is required to avoid false positives from Object inherited methods.
collision = existing && existing !== Object.prototype[methodName];
// The disambiguation function is only required once.
if (!existing || !existing.disambiguate) {
dcp[methodName] = collision ? disambiguateMethod(methodName) : wrapped;
}
// The target chainable always receives the wrapped method. Additionally,
// if the target chainable is Sugar.Object, then map the wrapped method
// to all other namespaces as well if they do not define their own method
// of the same name. This way, a Sugar.Number will have methods like
// isEqual that can be called on any object without having to traverse up
// the prototype chain and perform disambiguation, which costs cycles.
// Note that the "if" block below actually does nothing on init as Object
// goes first and no other namespaces exist yet. However it needs to be
// here as Object instance methods defined later also need to be mapped
// back onto existing namespaces.
sugarNamespace.prototype[methodName] = wrapped;
if (sugarNamespace === Sugar.Object) {
mapObjectChainableToAllNamespaces(methodName, wrapped);
}
}
function mapObjectChainablesToNamespace(sugarNamespace) {
forEachProperty(Sugar.Object && Sugar.Object.prototype, function(val, methodName) {
if (typeof val === 'function') {
setObjectChainableOnNamespace(sugarNamespace, methodName, val);
}
});
}
function mapObjectChainableToAllNamespaces(methodName, fn) {
forEachProperty(namespacesByName, function(sugarNamespace) {
setObjectChainableOnNamespace(sugarNamespace, methodName, fn);
});
}
function setObjectChainableOnNamespace(sugarNamespace, methodName, fn) {
var proto = sugarNamespace.prototype;
if (!hasOwn(proto, methodName)) {
proto[methodName] = fn;
}
}
function wrapWithChainableResult(fn) {
return function() {
return new DefaultChainable(fn.apply(this.raw, arguments));
};
}
function disambiguateMethod(methodName) {
var fn = function() {
var raw = this.raw, sugarNamespace, fn;
if (raw != null) {
// Find the Sugar namespace for this unknown.
sugarNamespace = namespacesByClassString[classToString(raw)];
}
if (!sugarNamespace) {
// If no sugarNamespace can be resolved, then default
// back to Sugar.Object so that undefined and other
// non-supported types can still have basic object
// methods called on them, such as type checks.
sugarNamespace = Sugar.Object;
}
fn = new sugarNamespace(raw)[methodName];
if (fn.disambiguate) {
// If the method about to be called on this chainable is
// itself a disambiguation method, then throw an error to
// prevent infinite recursion.
throw new TypeError('Cannot resolve namespace for ' + raw);
}
return fn.apply(this, arguments);
};
fn.disambiguate = true;
return fn;
}
function mapNativeToChainable(name, methodNames) {
var sugarNamespace = namespacesByName[name],
nativeProto = globalContext[name].prototype;
if (!methodNames && ownPropertyNames) {
methodNames = ownPropertyNames(nativeProto);
}
forEachProperty(methodNames, function(methodName) {
if (nativeMethodProhibited(methodName)) {
// Sugar chainables have their own constructors as well as "valueOf"
// methods, so exclude them here. The __proto__ argument should be trapped
// by the function check below, however simply accessing this property on
// Object.prototype causes QML to segfault, so pre-emptively excluding it.
return;
}
try {
var fn = nativeProto[methodName];
if (typeof fn !== 'function') {
// Bail on anything not a function.
return;
}
} catch (e) {
// Function.prototype has properties that
// will throw errors when accessed.
return;
}
defineChainableMethod(sugarNamespace, methodName, fn);
});
}
function nativeMethodProhibited(methodName) {
return methodName === 'constructor' ||
methodName === 'valueOf' ||
methodName === '__proto__';
}
// Util
// Internal references
var ownPropertyNames = Object.getOwnPropertyNames,
internalToString = Object.prototype.toString,
internalHasOwnProperty = Object.prototype.hasOwnProperty;
// Defining this as a variable here as the ES5 module
// overwrites it to patch DONTENUM.
var forEachProperty = function (obj, fn) {
for(var key in obj) {
if (!hasOwn(obj, key)) continue;
if (fn.call(obj, obj[key], key, obj) === false) break;
}
};
function definePropertyShim(obj, prop, descriptor) {
obj[prop] = descriptor.value;
}
function setProperty(target, name, value, enumerable) {
defineProperty(target, name, {
value: value,
enumerable: !!enumerable,
configurable: true,
writable: true
});
}
// PERF: Attempts to speed this method up get very Heisenbergy. Quickly
// returning based on typeof works for primitives, but slows down object
// types. Even === checks on null and undefined (no typeof) will end up
// basically breaking even. This seems to be as fast as it can go.
function classToString(obj) {
return internalToString.call(obj);
}
function hasOwn(obj, prop) {
return !!obj && internalHasOwnProperty.call(obj, prop);
}
function getOwn(obj, prop) {
if (hasOwn(obj, prop)) {
return obj[prop];
}
}
setupGlobal();
/***
* @module Common
* @description Internal utility and common methods.
***/
// Flag allowing native methods to be enhanced
var ENHANCEMENTS_FLAG = 'enhance';
// For type checking, etc. Excludes object as this is more nuanced.
var NATIVE_TYPES = 'Boolean Number String Date RegExp Function Array Error Set Map';
// Do strings have no keys?
var NO_KEYS_IN_STRING_OBJECTS = !('0' in Object('a'));
// Prefix for private properties
var PRIVATE_PROP_PREFIX = '_sugar_';
// Matches 1..2 style ranges in properties
var PROPERTY_RANGE_REG = /^(.*?)\[([-\d]*)\.\.([-\d]*)\](.*)$/;
// WhiteSpace/LineTerminator as defined in ES5.1 plus Unicode characters in the Space, Separator category.
var TRIM_CHARS = '\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';
// Regex for matching a formatted string
var STRING_FORMAT_REG = /([{}])\1|\{([^}]*)\}|(%)%|(%(\w*))/g;
// Common chars
var HALF_WIDTH_ZERO = 0x30,
FULL_WIDTH_ZERO = 0xff10,
HALF_WIDTH_PERIOD = '.',
FULL_WIDTH_PERIOD = '',
HALF_WIDTH_COMMA = ',',
OPEN_BRACE = '{',
CLOSE_BRACE = '}';
// Namespace aliases
var sugarObject = Sugar.Object,
sugarArray = Sugar.Array,
sugarDate = Sugar.Date,
sugarString = Sugar.String,
sugarNumber = Sugar.Number,
sugarFunction = Sugar.Function,
sugarRegExp = Sugar.RegExp;
// Class checks
var isSerializable,
isBoolean, isNumber, isString,
isDate, isRegExp, isFunction,
isArray, isSet, isMap, isError;
function buildClassChecks() {
var knownTypes = {};
function addCoreTypes() {
var names = spaceSplit(NATIVE_TYPES);
isBoolean = buildPrimitiveClassCheck(names[0]);
isNumber = buildPrimitiveClassCheck(names[1]);
isString = buildPrimitiveClassCheck(names[2]);
isDate = buildClassCheck(names[3]);
isRegExp = buildClassCheck(names[4]);
// Wanted to enhance performance here by using simply "typeof"
// but Firefox has two major issues that make this impossible,
// one fixed, the other not, so perform a full class check here.
//
// 1. Regexes can be typeof "function" in FF < 3
// https://bugzilla.mozilla.org/show_bug.cgi?id=61911 (fixed)
//
// 2. HTMLEmbedElement and HTMLObjectElement are be typeof "function"
// https://bugzilla.mozilla.org/show_bug.cgi?id=268945 (won't fix)
isFunction = buildClassCheck(names[5]);
isArray = Array.isArray || buildClassCheck(names[6]);
isError = buildClassCheck(names[7]);
isSet = buildClassCheck(names[8], typeof Set !== 'undefined' && Set);
isMap = buildClassCheck(names[9], typeof Map !== 'undefined' && Map);
// Add core types as known so that they can be checked by value below,
// notably excluding Functions and adding Arguments and Error.
addKnownType('Arguments');
addKnownType(names[0]);
addKnownType(names[1]);
addKnownType(names[2]);
addKnownType(names[3]);
addKnownType(names[4]);
addKnownType(names[6]);
}
function addArrayTypes() {
var types = 'Int8 Uint8 Uint8Clamped Int16 Uint16 Int32 Uint32 Float32 Float64';
forEach(spaceSplit(types), function(str) {
addKnownType(str + 'Array');
});
}
function addKnownType(className) {
var str = '[object '+ className +']';
knownTypes[str] = true;
}
function isKnownType(className) {
return knownTypes[className];
}
function buildClassCheck(className, globalObject) {
if (globalObject && isClass(new globalObject, 'Object')) {
return getConstructorClassCheck(globalObject);
} else {
return getToStringClassCheck(className);
}
}
function getConstructorClassCheck(obj) {
var ctorStr = String(obj);
return function(obj) {
return String(obj.constructor) === ctorStr;
};
}
function getToStringClassCheck(className) {
return function(obj, str) {
// perf: Returning up front on instanceof appears to be slower.
return isClass(obj, className, str);
};
}
function buildPrimitiveClassCheck(className) {
var type = className.toLowerCase();
return function(obj) {
var t = typeof obj;
return t === type || t === 'object' && isClass(obj, className);
};
}
addCoreTypes();
addArrayTypes();
isSerializable = function(obj, className) {
// Only known objects can be serialized. This notably excludes functions,
// host objects, Symbols (which are matched by reference), and instances
// of classes. The latter can arguably be matched by value, but
// distinguishing between these and host objects -- which should never be
// compared by value -- is very tricky so not dealing with it here.
className = className || classToString(obj);
return isKnownType(className) || isPlainObject(obj, className);
};
}
function isClass(obj, className, str) {
if (!str) {
str = classToString(obj);
}
return str === '[object '+ className +']';
}
// Wrapping the core's "define" methods to
// save a few bytes in the minified script.
function wrapNamespace(method) {
return function(sugarNamespace, arg1, arg2) {
sugarNamespace[method](arg1, arg2);
};
}
// Method define aliases
var alias = wrapNamespace('alias'),
defineStatic = wrapNamespace('defineStatic'),
defineInstance = wrapNamespace('defineInstance'),
defineStaticPolyfill = wrapNamespace('defineStaticPolyfill'),
defineInstancePolyfill = wrapNamespace('defineInstancePolyfill'),
defineInstanceAndStatic = wrapNamespace('defineInstanceAndStatic'),
defineInstanceWithArguments = wrapNamespace('defineInstanceWithArguments');
function defineInstanceSimilar(sugarNamespace, set, fn, flags) {
defineInstance(sugarNamespace, collectSimilarMethods(set, fn), flags);
}
function defineInstanceAndStaticSimilar(sugarNamespace, set, fn, flags) {
defineInstanceAndStatic(sugarNamespace, collectSimilarMethods(set, fn), flags);
}
function collectSimilarMethods(set, fn) {
var methods = {};
if (isString(set)) {
set = spaceSplit(set);
}
forEach(set, function(el, i) {
fn(methods, el, i);
});
return methods;
}
// This song and dance is to fix methods to a different length
// from what they actually accept in order to stay in line with
// spec. Additionally passing argument length, as some methods
// throw assertion errors based on this (undefined check is not
// enough). Fortunately for now spec is such that passing 3
// actual arguments covers all requirements. Note that passing
// the argument length also forces the compiler to not rewrite
// length of the compiled function.
function fixArgumentLength(fn) {
var staticFn = function(a) {
var args = arguments;
return fn(a, args[1], args[2], args.length - 1);
};
staticFn.instance = function(b) {
var args = arguments;
return fn(this, b, args[1], args.length);
};
return staticFn;
}
function defineAccessor(namespace, name, fn) {
setProperty(namespace, name, fn);
}
function defineOptionsAccessor(namespace, defaults) {
var obj = simpleClone(defaults);
function getOption(name) {
return obj[name];
}
function setOption(name, val) {
if (val === null) {
val = defaults[name];
}
obj[name] = val;
}
defineAccessor(namespace, 'getOption', getOption);
defineAccessor(namespace, 'setOption', setOption);
return getOption;
}
// For methods defined directly on the prototype like Range
function defineOnPrototype(ctor, methods) {
var proto = ctor.prototype;
forEachProperty(methods, function(val, key) {
proto[key] = val;
});
}
// Argument helpers
function assertArgument(exists) {
if (!exists) {
throw new TypeError('Argument required');
}
}
function assertCallable(obj) {
if (!isFunction(obj)) {
throw new TypeError('Function is not callable');
}
}
function assertArray(obj) {
if (!isArray(obj)) {
throw new TypeError('Array required');
}
}
function assertWritable(obj) {
if (isPrimitive(obj)) {
// If strict mode is active then primitives will throw an
// error when attempting to write properties. We can't be
// sure if strict mode is available, so pre-emptively
// throw an error here to ensure consistent behavior.
throw new TypeError('Property cannot be written');
}
}
// Coerces an object to a positive integer.
// Does not allow Infinity.
function coercePositiveInteger(n) {
n = +n || 0;
if (n < 0 || !isNumber(n) || !isFinite(n)) {
throw new RangeError('Invalid number');
}
return trunc(n);
}
// General helpers
function isDefined(o) {
return o !== undefined;
}
function isUndefined(o) {
return o === undefined;
}
function privatePropertyAccessor(key) {
var privateKey = PRIVATE_PROP_PREFIX + key;
return function(obj, val) {
if (arguments.length > 1) {
setProperty(obj, privateKey, val);
return obj;
}
return obj[privateKey];
};
}
function setChainableConstructor(sugarNamespace, createFn) {
sugarNamespace.prototype.constructor = function() {
return createFn.apply(this, arguments);
};
}
// Fuzzy matching helpers
function getMatcher(f) {
if (!isPrimitive(f)) {
var className = classToString(f);
if (isRegExp(f, className)) {
return regexMatcher(f);
} else if (isDate(f, className)) {
return dateMatcher(f);
} else if (isFunction(f, className)) {
return functionMatcher(f);
} else if (isPlainObject(f, className)) {
return fuzzyMatcher(f);
}
}
// Default is standard isEqual
return defaultMatcher(f);
}
function fuzzyMatcher(obj) {
var matchers = {};
return function(el, i, arr) {
var matched = true;
if (!isObjectType(el)) {
return false;
}
forEachProperty(obj, function(val, key) {
matchers[key] = getOwn(matchers, key) || getMatcher(val);
if (matchers[key].call(arr, el[key], i, arr) === false) {
matched = false;
}
return matched;
});
return matched;
};
}
function defaultMatcher(f) {
return function(el) {
return isEqual(el, f);
};
}
function regexMatcher(reg) {
reg = RegExp(reg);
return function(el) {
return reg.test(el);
};
}
function dateMatcher(d) {
var ms = d.getTime();
return function(el) {
return !!(el && el.getTime) && el.getTime() === ms;
};
}
function functionMatcher(fn) {
return function(el, i, arr) {
// Return true up front if match by reference
return el === fn || fn.call(arr, el, i, arr);
};
}
// Object helpers
function getKeys(obj) {
return Object.keys(obj);
}
function deepHasProperty(obj, key, any) {
return handleDeepProperty(obj, key, any, true);
}
function deepGetProperty(obj, key, any) {
return handleDeepProperty(obj, key, any, false);
}
function deepSetProperty(obj, key, val) {
handleDeepProperty(obj, key, false, false, true, false, val);
return obj;
}
function handleDeepProperty(obj, key, any, has, fill, fillLast, val) {
var ns, bs, ps, cbi, set, isLast, isPush, isIndex, nextIsIndex, exists;
ns = obj || undefined;
if (key == null) return;
if (isObjectType(key)) {
// Allow array and array-like accessors
bs = [key];
} else {
key = String(key);
if (key.indexOf('..') !== -1) {
return handleArrayIndexRange(obj, key, any, val);
}
bs = key.split('[');
}
set = isDefined(val);
for (var i = 0, blen = bs.length; i < blen; i++) {
ps = bs[i];
if (isString(ps)) {
ps = periodSplit(ps);
}
for (var j = 0, plen = ps.length; j < plen; j++) {
key = ps[j];
// Is this the last key?
isLast = i === blen - 1 && j === plen - 1;
// Index of the closing ]
cbi = key.indexOf(']');
// Is the key an array index?
isIndex = cbi !== -1;
// Is this array push syntax "[]"?
isPush = set && cbi === 0;
// If the bracket split was successful and this is the last element
// in the dot split, then we know the next key will be an array index.
nextIsIndex = blen > 1 && j === plen - 1;
if (isPush) {
// Set the index to the end of the array
key = ns.length;
} else if (isIndex) {
// Remove the closing ]
key = key.slice(0, -1);
}
// If the array index is less than 0, then
// add its length to allow negative indexes.
if (isIndex && key < 0) {
key = +key + ns.length;
}
// Bracket keys may look like users[5] or just [5], so the leading
// characters are optional. We can enter the namespace if this is the
// 2nd part, if there is only 1 part, or if there is an explicit key.
if (i || key || blen === 1) {
exists = any ? key in ns : hasOwn(ns, key);
// Non-existent namespaces are only filled if they are intermediate
// (not at the end) or explicitly filling the last.
if (fill && (!isLast || fillLast) && !exists) {
// For our purposes, last only needs to be an array.
ns = ns[key] = nextIsIndex || (fillLast && isLast) ? [] : {};
continue;
}
if (has) {
if (isLast || !exists) {
return exists;
}
} else if (set && isLast) {
assertWritable(ns);
ns[key] = val;
}
ns = exists ? ns[key] : undefined;
}
}
}
return ns;
}
// Get object property with support for 0..1 style range notation.
function handleArrayIndexRange(obj, key, any, val) {
var match, start, end, leading, trailing, arr, set;
match = key.match(PROPERTY_RANGE_REG);
if (!match) {
return;
}
set = isDefined(val);
leading = match[1];
if (leading) {
arr = handleDeepProperty(obj, leading, any, false, set ? true : false, true);
} else {
arr = obj;
}
assertArray(arr);
trailing = match[4];
start = match[2] ? +match[2] : 0;
end = match[3] ? +match[3] : arr.length;
// A range of 0..1 is inclusive, so we need to add 1 to the end. If this
// pushes the index from -1 to 0, then set it to the full length of the
// array, otherwise it will return nothing.
end = end === -1 ? arr.length : end + 1;
if (set) {
for (var i = start; i < end; i++) {
handleDeepProperty(arr, i + trailing, any, false, true, false, val);
}
} else {
arr = arr.slice(start, end);
// If there are trailing properties, then they need to be mapped for each
// element in the array.
if (trailing) {
if (trailing.charAt(0) === HALF_WIDTH_PERIOD) {
// Need to chomp the period if one is trailing after the range. We
// can't do this at the regex level because it will be required if
// we're setting the value as it needs to be concatentated together
// with the array index to be set.
trailing = trailing.slice(1);
}
return arr.map(function(el) {
return handleDeepProperty(el, trailing);
});
}
}
return arr;
}
function getOwnKey(obj, key) {
if (hasOwn(obj, key)) {
return key;
}
}
function hasProperty(obj, prop) {
return !isPrimitive(obj) && prop in obj;
}
function isObjectType(obj, type) {
return !!obj && (type || typeof obj) === 'object';
}
function isPrimitive(obj, type) {
type = type || typeof obj;
return obj == null || type === 'string' || type === 'number' || type === 'boolean';
}
function isPlainObject(obj, className) {
return isObjectType(obj) &&
isClass(obj, 'Object', className) &&
hasValidPlainObjectPrototype(obj) &&
hasOwnEnumeratedProperties(obj);
}
function hasValidPlainObjectPrototype(obj) {
var hasToString = 'toString' in obj;
var hasConstructor = 'constructor' in obj;
// An object created with Object.create(null) has no methods in the
// prototype chain, so check if any are missing. The additional hasToString
// check is for false positives on some host objects in old IE which have
// toString but no constructor. If the object has an inherited constructor,
// then check if it is Object (the "isPrototypeOf" tapdance here is a more
// robust way of ensuring this if the global has been hijacked). Note that
// accessing the constructor directly (without "in" or "hasOwnProperty")
// will throw a permissions error in IE8 on cross-domain windows.
return (!hasConstructor && !hasToString) ||
(hasConstructor && !hasOwn(obj, 'constructor') &&
hasOwn(obj.constructor.prototype, 'isPrototypeOf'));
}
function hasOwnEnumeratedProperties(obj) {
// Plain objects are generally defined as having enumerated properties
// all their own, however in early IE environments without defineProperty,
// there may also be enumerated methods in the prototype chain, so check
// for both of these cases.
var objectProto = Object.prototype;
for (var key in obj) {
var val = obj[key];
if (!hasOwn(obj, key) && val !== objectProto[key]) {
return false;
}
}
return true;
}
function simpleRepeat(n, fn) {
for (var i = 0; i < n; i++) {
fn(i);
}
}
function simpleClone(obj) {
return simpleMerge({}, obj);
}
function simpleMerge(target, source) {
forEachProperty(source, function(val, key) {
target[key] = val;
});
return target;
}
// Make primtives types like strings into objects.
function coercePrimitiveToObject(obj) {
if (isPrimitive(obj)) {
obj = Object(obj);
}
if (NO_KEYS_IN_STRING_OBJECTS && isString(obj)) {
forceStringCoercion(obj);
}
return obj;
}
// Force strings to have their indexes set in
// environments that don't do this automatically.
function forceStringCoercion(obj) {
var i = 0, chr;
while (chr = obj.charAt(i)) {
obj[i++] = chr;
}
}
// Equality helpers
function isEqual(a, b, stack) {
var aClass, bClass;
if (a === b) {
// Return quickly up front when matched by reference,
// but be careful about 0 !== -0.
return a !== 0 || 1 / a === 1 / b;
}
aClass = classToString(a);
bClass = classToString(b);
if (aClass !== bClass) {
return false;
}
if (isSerializable(a, aClass) && isSerializable(b, bClass)) {
return objectIsEqual(a, b, aClass, stack);
} else if (isSet(a, aClass) && isSet(b, bClass)) {
return a.size === b.size && isEqual(setToArray(a), setToArray(b), stack);
} else if (isMap(a, aClass) && isMap(b, bClass)) {
return a.size === b.size && isEqual(mapToArray(a), mapToArray(b), stack);
} else if (isError(a, aClass) && isError(b, bClass)) {
return a.toString() === b.toString();
}
return false;
}
function objectIsEqual(a, b, aClass, stack) {
var aType = typeof a, bType = typeof b, propsEqual, count;
if (aType !== bType) {
return false;
}
if (isObjectType(a.valueOf())) {
if (a.length !== b.length) {
// perf: Quickly returning up front for arrays.
return false;
}
count = 0;
propsEqual = true;
iterateWithCyclicCheck(a, false, stack, function(key, val, cyc, stack) {
if (!cyc && (!(key in b) || !isEqual(val, b[key], stack))) {
propsEqual = false;
}
count++;
return propsEqual;
});
if (!propsEqual || count !== getKeys(b).length) {
return false;
}
}
// Stringifying the value handles NaN, wrapped primitives, dates, and errors in one go.
return a.valueOf().toString() === b.valueOf().toString();
}
// Serializes an object in a way that will provide a token unique
// to the type, class, and value of an object. Host objects, class
// instances etc, are not serializable, and are held in an array
// of references that will return the index as a unique identifier
// for the object. This array is passed from outside so that the
// calling function can decide when to dispose of this array.
function serializeInternal(obj, refs, stack) {
var type = typeof obj, className, value, ref;
// Return quickly for primitives to save cycles
if (isPrimitive(obj, type) && !isRealNaN(obj)) {
return type + obj;
}
className = classToString(obj);
if (!isSerializable(obj, className)) {
ref = indexOf(refs, obj);
if (ref === -1) {
ref = refs.length;
refs.push(obj);
}
return ref;
} else if (isObjectType(obj)) {
value = serializeDeep(obj, refs, stack) + obj.toString();
} else if (1 / obj === -Infinity) {
value = '-0';
} else if (obj.valueOf) {
value = obj.valueOf();
}
return type + className + value;
}
function serializeDeep(obj, refs, stack) {
var result = '';
iterateWithCyclicCheck(obj, true, stack, function(key, val, cyc, stack) {
result += cyc ? 'CYC' : key + serializeInternal(val, refs, stack);
});
return result;
}
function iterateWithCyclicCheck(obj, sortedKeys, stack, fn) {
function next(val, key) {
var cyc = false;
// Allowing a step into the structure before triggering this check to save
// cycles on standard JSON structures and also to try as hard as possible to
// catch basic properties that may have been modified.
if (stack.length > 1) {
var i = stack.length;
while (i--) {
if (stack[i] === val) {
cyc = true;
}
}
}
stack.push(val);
fn(key, val, cyc, stack);
stack.pop();
}
function iterateWithSortedKeys() {
// Sorted keys is required for serialization, where object order
// does not matter but stringified order does.
var arr = getKeys(obj).sort(), key;
for (var i = 0; i < arr.length; i++) {
key = arr[i];
next(obj[key], arr[i]);
}
}
// This method for checking for cyclic structures was egregiously stolen from
// the ingenious method by @kitcambridge from the Underscore script:
// https://github.com/documentcloud/underscore/issues/240
if (!stack) {
stack = [];
}
if (sortedKeys) {
iterateWithSortedKeys();
} else {
forEachProperty(obj, next);
}
}
// Array helpers
function isArrayIndex(n) {
return n >>> 0 == n && n != 0xFFFFFFFF;
}
function iterateOverSparseArray(arr, fn, fromIndex, loop) {
var indexes = getSparseArrayIndexes(arr, fromIndex, loop), index;
for (var i = 0, len = indexes.length; i < len; i++) {
index = indexes[i];
fn.call(arr, arr[index], index, arr);
}
return arr;
}
// It's unclear whether or not sparse arrays qualify as "simple enumerables".
// If they are not, however, the wrapping function will be deoptimized, so
// isolate here (also to share between es5 and array modules).
function getSparseArrayIndexes(arr, fromIndex, loop, fromRight) {
var indexes = [], i;
for (i in arr) {
if (isArrayIndex(i) && (loop || (fromRight ? i <= fromIndex : i >= fromIndex))) {
indexes.push(+i);
}
}
indexes.sort(function(a, b) {
var aLoop = a > fromIndex;
var bLoop = b > fromIndex;
if (aLoop !== bLoop) {
return aLoop ? -1 : 1;
}
return a - b;
});
return indexes;
}
function getEntriesForIndexes(obj, find, loop, isString) {
var result, length = obj.length;
if (!isArray(find)) {
return entryAtIndex(obj, find, length, loop, isString);
}
result = new Array(find.length);
forEach(find, function(index, i) {
result[i] = entryAtIndex(obj, index, length, loop, isString);
});
return result;
}
function getNormalizedIndex(index, length, loop) {
if (index && loop) {
index = index % length;
}
if (index < 0) index = length + index;
return index;
}
function entryAtIndex(obj, index, length, loop, isString) {
index = getNormalizedIndex(index, length, loop);
return isString ? obj.charAt(index) : obj[index];
}
function mapWithShortcuts(el, f, context, mapArgs) {
if (!f) {
return el;
} else if (f.apply) {
return f.apply(context, mapArgs || []);
} else if (isArray(f)) {
return f.map(function(m) {
return mapWithShortcuts(el, m, context, mapArgs);
});
} else if (isFunction(el[f])) {
return el[f].call(el);
} else {
return deepGetProperty(el, f);
}
}
function spaceSplit(str) {
return str.split(' ');
}
function commaSplit(str) {
return str.split(HALF_WIDTH_COMMA);
}
function periodSplit(str) {
return str.split(HALF_WIDTH_PERIOD);
}
function forEach(arr, fn) {
for (var i = 0, len = arr.length; i < len; i++) {
if (!(i in arr)) {
return iterateOverSparseArray(arr, fn, i);
}
fn(arr[i], i);
}
}
function filter(arr, fn) {
var result = [];
for (var i = 0, len = arr.length; i < len; i++) {
var el = arr[i];
if (i in arr && fn(el, i)) {
result.push(el);
}
}
return result;
}
function map(arr, fn) {
// perf: Not using fixed array len here as it may be sparse.
var result = [];
for (var i = 0, len = arr.length; i < len; i++) {
if (i in arr) {
result.push(fn(arr[i], i));
}
}
return result;
}
function indexOf(arr, el) {
for (var i = 0, len = arr.length; i < len; i++) {
if (i in arr && arr[i] === el) return i;
}
return -1;
}
// Number helpers
var trunc = Math.trunc || function(n) {
if (n === 0 || !isFinite(n)) return n;
return n < 0 ? ceil(n) : floor(n);
};
function isRealNaN(obj) {
// This is only true of NaN
return obj != null && obj !== obj;
}
function withPrecision(val, precision, fn) {
var multiplier = pow(10, abs(precision || 0));
fn = fn || round;
if (precision < 0) multiplier = 1 / multiplier;
return fn(val * multiplier) / multiplier;
}
function padNumber(num, place, sign, base, replacement) {
var str = abs(num).toString(base || 10);
str = repeatString(replacement || '0', place - str.replace(/\.\d+/, '').length) + str;
if (sign || num < 0) {
str = (num < 0 ? '-' : '+') + str;
}
return str;
}
function getOrdinalSuffix(num) {
if (num >= 11 && num <= 13) {
return 'th';
} else {
switch(num % 10) {
case 1: return 'st';
case 2: return 'nd';
case 3: return 'rd';
default: return 'th';
}
}
}
// Fullwidth number helpers
var fullWidthNumberReg, fullWidthNumberMap, fullWidthNumbers;
function buildFullWidthNumber() {
var fwp = FULL_WIDTH_PERIOD, hwp = HALF_WIDTH_PERIOD, hwc = HALF_WIDTH_COMMA, fwn = '';
fullWidthNumberMap = {};
for (var i = 0, digit; i <= 9; i++) {
digit = chr(i + FULL_WIDTH_ZERO);
fwn += digit;
fullWidthNumberMap[digit] = chr(i + HALF_WIDTH_ZERO);
}
fullWidthNumberMap[hwc] = '';
fullWidthNumberMap[fwp] = hwp;
// Mapping this to itself to capture it easily
// in stringToNumber to detect decimals later.
fullWidthNumberMap[hwp] = hwp;
fullWidthNumberReg = allCharsReg(fwn + fwp + hwc + hwp);
fullWidthNumbers = fwn;
}
// Takes into account full-width characters, commas, and decimals.
function stringToNumber(str, base) {
var sanitized, isDecimal;
sanitized = str.replace(fullWidthNumberReg, function(chr) {
var replacement = getOwn(fullWidthNumberMap, chr);
if (replacement === HALF_WIDTH_PERIOD) {
isDecimal = true;
}
return replacement;
});
return isDecimal ? parseFloat(sanitized) : parseInt(sanitized, base || 10);
}
// Math aliases
var abs = Math.abs,
pow = Math.pow,
min = Math.min,
max = Math.max,
ceil = Math.ceil,
floor = Math.floor,
round = Math.round;
// String helpers
var chr = String.fromCharCode;
function trim(str) {
return str.trim();
}
function repeatString(str, num) {
var result = '';
str = str.toString();
while (num > 0) {
if (num & 1) {
result += str;
}
if (num >>= 1) {
str += str;
}
}
return result;
}
function simpleCapitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
function createFormatMatcher(bracketMatcher, percentMatcher, precheck) {
var reg = STRING_FORMAT_REG;
var compileMemoized = memoizeFunction(compile);
function getToken(format, match) {
var get, token, literal, fn;
var bKey = match[2];
var pLit = match[3];
var pKey = match[5];
if (match[4] && percentMatcher) {
token = pKey;
get = percentMatcher;
} else if (bKey) {
token = bKey;
get = bracketMatcher;
} else if (pLit && percentMatcher) {
literal = pLit;
} else {
literal = match[1] || match[0];
}
if (get) {
assertPassesPrecheck(precheck, bKey, pKey);
fn = function(obj, opt) {
return get(obj, token, opt);
};
}
format.push(fn || getLiteral(literal));
}
function getSubstring(format, str, start, end) {
if (end > start) {
var sub = str.slice(start, end);
assertNoUnmatched(sub, OPEN_BRACE);
assertNoUnmatched(sub, CLOSE_BRACE);
format.push(function() {
return sub;
});
}
}
function getLiteral(str) {
return function() {
return str;
};
}
function assertPassesPrecheck(precheck, bt, pt) {
if (precheck && !precheck(bt, pt)) {
throw new TypeError('Invalid token '+ (bt || pt) +' in format string');
}
}
function assertNoUnmatched(str, chr) {
if (str.indexOf(chr) !== -1) {
throw new TypeError('Unmatched '+ chr +' in format string');
}
}
function compile(str) {
var format = [], lastIndex = 0, match;
reg.lastIndex = 0;
while(match = reg.exec(str)) {
getSubstring(format, str, lastIndex, match.index);
getToken(format, match);
lastIndex = reg.lastIndex;
}
getSubstring(format, str, lastIndex, str.length);
return format;
}
return function(str, obj, opt) {
var format = compileMemoized(str), result = '';
for (var i = 0; i < format.length; i++) {
result += format[i](obj, opt);
}
return result;
};
}
// Inflection helper
var Inflections = {};
function getAcronym(str) {
return Inflections.acronyms && Inflections.acronyms.find(str);
}
function getHumanWord(str) {
return Inflections.human && Inflections.human.find(str);
}
function runHumanRules(str) {
return Inflections.human && Inflections.human.runRules(str) || str;
}
// RegExp helpers
function allCharsReg(src) {
return RegExp('[' + src + ']', 'g');
}
function getRegExpFlags(reg, add) {
var flags = '';
add = add || '';
function checkFlag(prop, flag) {
if (prop || add.indexOf(flag) > -1) {
flags += flag;
}
}
checkFlag(reg.global, 'g');
checkFlag(reg.ignoreCase, 'i');
checkFlag(reg.multiline, 'm');
checkFlag(reg.sticky, 'y');
return flags;
}
function escapeRegExp(str) {
if (!isString(str)) str = String(str);
return str.replace(/([\\\/\'*+?|()\[\]{}.^$-])/g,'\\$1');
}
// Date helpers
var _utc = privatePropertyAccessor('utc');
function callDateGet(d, method) {
return d['get' + (_utc(d) ? 'UTC' : '') + method]();
}
function callDateSet(d, method, value, safe) {
// "Safe" denotes not setting the date if the value is the same as what is
// currently set. In theory this should be a noop, however it will cause
// timezone shifts when in the middle of a DST fallback. This is unavoidable
// as the notation itself is ambiguous (i.e. there are two "1:00ams" on
// November 1st, 2015 in northern hemisphere timezones that follow DST),
// however when advancing or rewinding dates this can throw off calculations
// so avoiding this unintentional shifting on an opt-in basis.
if (safe && value === callDateGet(d, method, value)) {
return;
}
d['set' + (_utc(d) ? 'UTC' : '') + method](value);
}
// Memoization helpers
var INTERNAL_MEMOIZE_LIMIT = 1000;
// Note that attemps to consolidate this with Function#memoize
// ended up clunky as that is also serializing arguments. Separating
// these implementations turned out to be simpler.
function memoizeFunction(fn) {
var memo = {}, counter = 0;
return function(key) {
if (hasOwn(memo, key)) {
return memo[key];
}
if (counter === INTERNAL_MEMOIZE_LIMIT) {
memo = {};
counter = 0;
}
counter++;
return memo[key] = fn(key);
};
}
// ES6 helpers
function setToArray(set) {
var arr = new Array(set.size), i = 0;
set.forEach(function(val) {
arr[i++] = val;
});
return arr;
}
function mapToArray(map) {
var arr = new Array(map.size), i = 0;
map.forEach(function(val, key) {
arr[i++] = [key, val];
});
return arr;
}
buildClassChecks();
buildFullWidthNumber();
/***
* @module ES6
* @description Polyfills that provide basic ES6 compatibility. This module
* provides the base for Sugar functionality, but is not a full
* polyfill suite.
*
***/
/*** @namespace String ***/
function getCoercedStringSubject(obj) {
if (obj == null) {
throw new TypeError('String required.');
}
return String(obj);
}
function getCoercedSearchString(obj) {
if (isRegExp(obj)) {
throw new TypeError();
}
return String(obj);
}
defineInstancePolyfill(sugarString, {
/***
* @method includes(<search>, [pos] = 0)
* @returns Boolean
* @polyfill ES6
* @short Returns true if <search> is contained within the string.
* @extra Search begins at [pos], which defaults to the beginning of the
* string. Sugar enhances this method to allow matching a regex.
*
* @example
*
* 'jumpy'.includes('py') -> true
* 'broken'.includes('ken', 3) -> true
* 'broken'.includes('bro', 3) -> false
*
***/
'includes': function(searchString) {
// Force compiler to respect argument length.
var argLen = arguments.length, pos = arguments[1];
var str = getCoercedStringSubject(this);
searchString = getCoercedSearchString(searchString);
return str.indexOf(searchString, pos) !== -1;
},
/***
* @method startsWith(<search>, [pos] = 0)
* @returns Boolean
* @polyfill ES6
* @short Returns true if the string starts with substring <search>.
* @extra Search begins at [pos], which defaults to the entire string length.
*
* @example
*
* 'hello'.startsWith('hell') -> true
* 'hello'.startsWith('HELL') -> false
* 'hello'.startsWith('ell', 1) -> true
*
***/
'startsWith': function(searchString) {
// Force compiler to respect argument length.
var argLen = arguments.length, position = arguments[1];
var str, start, pos, len, searchLength;
str = getCoercedStringSubject(this);
searchString = getCoercedSearchString(searchString);
pos = +position || 0;
len = str.length;
start = min(max(pos, 0), len);
searchLength = searchString.length;
if (searchLength + start > len) {
return false;
}
if (str.substr(start, searchLength) === searchString) {
return true;
}
return false;
},
/***
* @method endsWith(<search>, [pos] = length)
* @returns Boolean
* @polyfill ES6
* @short Returns true if the string ends with substring <search>.
* @extra Search ends at [pos], which defaults to the entire string length.
*
* @example
*
* 'jumpy'.endsWith('py') -> true
* 'jumpy'.endsWith('MPY') -> false
* 'jumpy'.endsWith('mp', 4) -> false
*
***/
'endsWith': function(searchString) {
// Force compiler to respect argument length.
var argLen = arguments.length, endPosition = arguments[1];
var str, start, end, pos, len, searchLength;
str = getCoercedStringSubject(this);
searchString = getCoercedSearchString(searchString);
len = str.length;
pos = len;
if (isDefined(endPosition)) {
pos = +endPosition || 0;
}
end = min(max(pos, 0), len);
searchLength = searchString.length;
start = end - searchLength;
if (start < 0) {
return false;
}
if (str.substr(start, searchLength) === searchString) {
return true;
}
return false;
},
/***
* @method repeat([num] = 0)
* @returns String
* @polyfill ES6
* @short Returns the string repeated [num] times.
*
* @example
*
* 'jumpy'.repeat(2) -> 'jumpyjumpy'
* 'a'.repeat(5) -> 'aaaaa'
* 'a'.repeat(0) -> ''
*
***/
'repeat': function(num) {
num = coercePositiveInteger(num);
return repeatString(this, num);
}
});
/*** @namespace Number ***/
defineStaticPolyfill(sugarNumber, {
/***
* @method isNaN(<value>)
* @returns Boolean
* @polyfill ES6
* @static
* @short Returns true only if the number is `NaN`.
* @extra This is differs from the global `isNaN`, which returns true for
* anything that is not a number.
*
* @example
*
* Number.isNaN(NaN) -> true
* Number.isNaN('n') -> false
*
***/
'isNaN': function(obj) {
return isRealNaN(obj);
}
});
/*** @namespace Array ***/
function getCoercedObject(obj) {
if (obj == null) {
throw new TypeError('Object required.');
}
return coercePrimitiveToObject(obj);
}
defineStaticPolyfill(sugarArray, {
/***
* @method from(<a>, [map], [context])
* @returns Mixed
* @polyfill ES6
* @static
* @short Creates an array from an array-like object.
* @extra If a function is passed for [map], it will be map each element of
* the array. [context] is the `this` object if passed.
*
* @callback map
*
* el The element of the current iteration.
* i The index of the current iteration.
* arr A reference to the array.
*
* @example
*
* Array.from({0:'a',1:'b',length:2}); -> ['a','b']
*
***/
'from': function(a) {
// Force compiler to respect argument length.
var argLen = arguments.length, map = arguments[1], context = arguments[2];
var len, arr;
if (isDefined(map)) {
assertCallable(map);
}
a = getCoercedObject(a);
len = trunc(max(0, a.length || 0));
if (!isArrayIndex(len)) {
throw new RangeError('Invalid array length');
}
if (isFunction(this)) {
arr = new this(len);
arr.length = len;
} else {
arr = new Array(len);
}
for (var i = 0; i < len; i++) {
setProperty(arr, i, isDefined(map) ? map.call(context, a[i], i) : a[i], true);
}
return arr;
}
});
defineInstancePolyfill(sugarArray, {
'find': function(f) {
// Force compiler to respect argument length.
var argLen = arguments.length, context = arguments[1];
assertCallable(f);
for (var i = 0, len = this.length; i < len; i++) {
if (f.call(context, this[i], i, this)) {
return this[i];
}
}
},
'findIndex': function(f) {
// Force compiler to respect argument length.
var argLen = arguments.length, context = arguments[1];
assertCallable(f);
for (var i = 0, len = this.length; i < len; i++) {
if (f.call(context, this[i], i, this)) {
return i;
}
}
return -1;
}
});
/***
* @module ES7
* @description Polyfills that provide basic ES7 compatibility. This module
* provides the base for Sugar functionality, but is not a full
* polyfill suite.
*
***/
/*** @namespace Array ***/
function sameValueZero(a, b) {
if (isRealNaN(a)) {
return isRealNaN(b);
}
return a === b ? a !== 0 || 1 / a === 1 / b : false;
}
defineInstancePolyfill(sugarArray, {
/***
* @method includes(<search>, [fromIndex] = 0)
* @returns Boolean
* @polyfill ES7
* @short Returns true if <search> is contained within the array.
* @extra Search begins at [fromIndex], which defaults to the beginning of the
* array.
*
* @example
*
* [1,2,3].includes(2) -> true
* [1,2,3].includes(4) -> false
* [1,2,3].includes(2, 3) -> false
*
***/
'includes': function(search) {
// Force compiler to respect argument length.
var argLen = arguments.length, fromIndex = arguments[1];
var arr = this, len;
if (isString(arr)) return arr.includes(search, fromIndex);
fromIndex = fromIndex ? fromIndex.valueOf() : 0;
len = arr.length;
if (fromIndex < 0) {
fromIndex = max(0, fromIndex + len);
}
for (var i = fromIndex; i < len; i++) {
if (sameValueZero(search, arr[i])) {
return true;
}
}
return false;
}
});
/***
* @module Date
* @description Date parsing and formatting, relative formats, number shortcuts,
* and locale support with default English locales.
*
***/
var DATE_OPTIONS = {
'newDateInternal': defaultNewDate
};
var LOCALE_ARRAY_FIELDS = [
'months', 'weekdays', 'units', 'numerals', 'placeholders',
'articles', 'tokens', 'timeMarkers', 'ampm', 'timeSuffixes',
'parse', 'timeParse', 'timeFrontParse', 'modifiers'
];
// Regex for stripping Timezone Abbreviations
var TIMEZONE_ABBREVIATION_REG = /(\w{3})[()\s\d]*$/;
// One minute in milliseconds
var MINUTES = 60 * 1000;
// Date unit indexes
var HOURS_INDEX = 3,
DAY_INDEX = 4,
WEEK_INDEX = 5,
MONTH_INDEX = 6,
YEAR_INDEX = 7;
// ISO Defaults
var ISO_FIRST_DAY_OF_WEEK = 1,
ISO_FIRST_DAY_OF_WEEK_YEAR = 4;
var ParsingTokens = {
'yyyy': {
param: 'year',
src: '\\d{4}'
},
'MM': {
param: 'month',
src: '[01]?\\d'
},
'dd': {
param: 'date',
src: '[0123]?\\d'
},
'hh': {
param: 'hour',
src: '[0-2]?\\d'
},
'mm': {
param: 'minute',
src: '[0-5]\\d'
},
'ss': {
param: 'second',
src: '[0-5]\\d(?:[,.]\\d+)?'
},
'yy': {
param: 'year',
src: '\\d{2}'
},
'y': {
param: 'year',
src: '\\d'
},
'yearSign': {
src: '[+-]',
sign: true
},
'tzHour': {
src: '[0-1]\\d'
},
'tzMinute': {
src: '[0-5]\\d'
},
'tzSign': {
src: '[+-]',
sign: true
},
'ihh': {
param: 'hour',
src: '[0-2]?\\d(?:[,.]\\d+)?'
},
'imm': {
param: 'minute',
src: '[0-5]\\d(?:[,.]\\d+)?'
},
'GMT': {
param: 'utc',
src: 'GMT',
val: 1
},
'Z': {
param: 'utc',
src: 'Z',
val: 1
},
'timestamp': {
src: '\\d+'
}
};
var LocalizedParsingTokens = {
'year': {
base: 'yyyy',
requiresSuffix: true
},
'month': {
base: 'MM',
requiresSuffix: true
},
'date': {
base: 'dd',
requiresSuffix: true
},
'hour': {
base: 'hh',
requiresSuffixOr: ':'
},
'minute': {
base: 'mm'
},
'second': {
base: 'ss'
},
'num': {
src: '\\d+',
requiresNumerals: true
}
};
var CoreParsingFormats = [
{
// 12-1978
// 08-1978 (MDY)
src: '{MM}[-.\\/]{yyyy}'
},
{
// 12/08/1978
// 08/12/1978 (MDY)
time: true,
src: '{dd}[-.\\/]{MM}(?:[-.\\/]{yyyy|yy|y})?',
mdy: '{MM}[-.\\/]{dd}(?:[-.\\/]{yyyy|yy|y})?'
},
{
// 1975-08-25
time: true,
src: '{yyyy}[-.\\/]{MM}(?:[-.\\/]{dd})?'
},
{
// .NET JSON
src: '\\\\/Date\\({timestamp}(?:[+-]\\d{4,4})?\\)\\\\/'
},
{
// ISO-8601
src: '{yearSign?}{yyyy}(?:-?{MM}(?:-?{dd}(?:T{ihh}(?::?{imm}(?::?{ss})?)?)?)?)?{tzOffset?}'
}
];
var CoreOutputFormats = {
'ISO8601': '{yyyy}-{MM}-{dd}T{HH}:{mm}:{ss}.{SSS}{Z}',
'RFC1123': '{Dow}, {dd} {Mon} {yyyy} {HH}:{mm}:{ss} {ZZ}',
'RFC1036': '{Weekday}, {dd}-{Mon}-{yy} {HH}:{mm}:{ss} {ZZ}'
};
var FormatTokensBase = [
{
ldml: 'Dow',
strf: 'a',
lowerToken: 'dow',
get: function(d, localeCode) {
return localeManager.get(localeCode).getWeekdayName(getWeekday(d), 2);
}
},
{
ldml: 'Weekday',
strf: 'A',
lowerToken: 'weekday',
allowAlternates: true,
get: function(d, localeCode, alternate) {
return localeManager.get(localeCode).getWeekdayName(getWeekday(d), alternate);
}
},
{
ldml: 'Mon',
strf: 'b h',
lowerToken: 'mon',
get: function(d, localeCode) {
return localeManager.get(localeCode).getMonthName(getMonth(d), 2);
}
},
{
ldml: 'Month',
strf: 'B',
lowerToken: 'month',
allowAlternates: true,
get: function(d, localeCode, alternate) {
return localeManager.get(localeCode).getMonthName(getMonth(d), alternate);
}
},
{
strf: 'C',
get: function(d) {
return getYear(d).toString().slice(0, 2);
}
},
{
ldml: 'd date day',
strf: 'd',
strfPadding: 2,
ldmlPaddedToken: 'dd',
ordinalToken: 'do',
get: function(d) {
return getDate(d);
}
},
{
strf: 'e',
get: function(d) {
return padNumber(getDate(d), 2, false, 10, ' ');
}
},
{
ldml: 'H 24hr',
strf: 'H',
strfPadding: 2,
ldmlPaddedToken: 'HH',
get: function(d) {
return getHours(d);
}
},
{
ldml: 'h hours 12hr',
strf: 'I',
strfPadding: 2,
ldmlPaddedToken: 'hh',
get: function(d) {
return getHours(d) % 12 || 12;
}
},
{
ldml: 'D',
strf: 'j',
strfPadding: 3,
ldmlPaddedToken: 'DDD',
get: function(d) {
var s = setUnitAndLowerToEdge(cloneDate(d), MONTH_INDEX);
return getDaysSince(d, s) + 1;
}
},
{
ldml: 'M',
strf: 'm',
strfPadding: 2,
ordinalToken: 'Mo',
ldmlPaddedToken: 'MM',
get: function(d) {
return getMonth(d) + 1;
}
},
{
ldml: 'm minutes',
strf: 'M',
strfPadding: 2,
ldmlPaddedToken: 'mm',
get: function(d) {
return callDateGet(d, 'Minutes');
}
},
{
ldml: 'Q',
get: function(d) {
return ceil((getMonth(d) + 1) / 3);
}
},
{
ldml: 'TT',
strf: 'p',
get: function(d, localeCode) {
return getMeridiemToken(d, localeCode);
}
},
{
ldml: 'tt',
strf: 'P',
get: function(d, localeCode) {
return getMeridiemToken(d, localeCode).toLowerCase();
}
},
{
ldml: 'T',
lowerToken: 't',
get: function(d, localeCode) {
return getMeridiemToken(d, localeCode).charAt(0);
}
},
{
ldml: 's seconds',
strf: 'S',
strfPadding: 2,
ldmlPaddedToken: 'ss',
get: function(d) {
return callDateGet(d, 'Seconds');
}
},
{
ldml: 'S ms',
strfPadding: 3,
ldmlPaddedToken: 'SSS',
get: function(d) {
return callDateGet(d, 'Milliseconds');
}
},
{
ldml: 'e',
strf: 'u',
ordinalToken: 'eo',
get: function(d) {
return getWeekday(d) || 7;
}
},
{
strf: 'U',
strfPadding: 2,
get: function(d) {
// Sunday first, 0-53
return getWeekNumber(d, false, 0);
}
},
{
ldml: 'W',
strf: 'V',
strfPadding: 2,
ordinalToken: 'Wo',
ldmlPaddedToken: 'WW',
get: function(d) {
// Monday first, 1-53 (ISO8601)
return getWeekNumber(d, true);
}
},
{
strf: 'w',
get: function(d) {
return getWeekday(d);
}
},
{
ldml: 'w',
ordinalToken: 'wo',
ldmlPaddedToken: 'ww',
get: function(d, localeCode) {
// Locale dependent, 1-53
var loc = localeManager.get(localeCode),
dow = loc.getFirstDayOfWeek(localeCode),
doy = loc.getFirstDayOfWeekYear(localeCode);
return getWeekNumber(d, true, dow, doy);
}
},
{
strf: 'W',
strfPadding: 2,
get: function(d) {
// Monday first, 0-53
return getWeekNumber(d, false);
}
},
{
ldmlPaddedToken: 'gggg',
ldmlTwoDigitToken: 'gg',
get: function(d, localeCode) {
return getWeekYear(d, localeCode);
}
},
{
strf: 'G',
strfPadding: 4,
strfTwoDigitToken: 'g',
ldmlPaddedToken: 'GGGG',
ldmlTwoDigitToken: 'GG',
get: function(d, localeCode) {
return getWeekYear(d, localeCode, true);
}
},
{
ldml: 'year',
ldmlPaddedToken: 'yyyy',
ldmlTwoDigitToken: 'yy',
strf: 'Y',
strfPadding: 4,
strfTwoDigitToken: 'y',
get: function(d) {
return getYear(d);
}
},
{
ldml: 'ZZ',
strf: 'z',
get: function(d) {
return getUTCOffset(d);
}
},
{
ldml: 'X',
get: function(d) {
return trunc(d.getTime() / 1000);
}
},
{
ldml: 'x',
get: function(d) {
return d.getTime();
}
},
{
ldml: 'Z',
get: function(d) {
return getUTCOffset(d, true);
}
},
{
ldml: 'z',
strf: 'Z',
get: function(d) {
// Note that this is not accurate in all browsing environments!
// https://github.com/moment/moment/issues/162
// It will continue to be supported for Node and usage with the
// understanding that it may be blank.
var match = d.toString().match(TIMEZONE_ABBREVIATION_REG);
return match ? match[1]: '';
}
},
{
strf: 'D',
alias: '%m/%d/%y'
},
{
strf: 'F',
alias: '%Y-%m-%d'
},
{
strf: 'r',
alias: '%I:%M:%S %p'
},
{
strf: 'R',
alias: '%H:%M'
},
{
strf: 'T',
alias: '%H:%M:%S'
},
{
strf: 'x',
alias: '{short}'
},
{
strf: 'X',
alias: '{time}'
},
{
strf: 'c',
alias: '{stamp}'
}
];
var DateUnits = [
{
name: 'millisecond',
method: 'Milliseconds',
multiplier: 1,
start: 0,
end: 999
},
{
name: 'second',
method: 'Seconds',
multiplier: 1000,
start: 0,
end: 59
},
{
name: 'minute',
method: 'Minutes',
multiplier: 60 * 1000,
start: 0,
end: 59
},
{
name: 'hour',
method: 'Hours',
multiplier: 60 * 60 * 1000,
start: 0,
end: 23
},
{
name: 'day',
alias: 'date',
method: 'Date',
ambiguous: true,
multiplier: 24 * 60 * 60 * 1000,
start: 1,
end: function(d) {
return getDaysInMonth(d);
}
},
{
name: 'week',
method: 'ISOWeek',
ambiguous: true,
multiplier: 7 * 24 * 60 * 60 * 1000
},
{
name: 'month',
method: 'Month',
ambiguous: true,
multiplier: 30.4375 * 24 * 60 * 60 * 1000,
start: 0,
end: 11
},
{
name: 'year',
method: 'FullYear',
ambiguous: true,
multiplier: 365.25 * 24 * 60 * 60 * 1000,
start: 0
}
];
/***
* @method getOption(<name>)
* @returns Mixed
* @accessor
* @short Gets an option used interally by Date.
* @options
*
* newDateInternal Sugar's internal date constructor. By default this
* function simply returns a `new Date()`, however it can be
* overridden if needed.
*
* @example
*
* Sugar.Date.getOption('newDateInternal');
*
***
* @method setOption(<name>, <value>)
* @accessor
* @short Sets an option used interally by Date.
* @extra If <value> is `null`, the default value will be restored.
* @options
*
* newDateInternal Sugar's internal date constructor. Date methods often
* construct a `new Date()` internally as a reference point
* (`isToday`, relative formats like `tomorrow`, etc). This
* can be overridden if you need it to be something else.
* Most commonly, this allows you to return a shifted date
* to simulate a specific timezone, as dates in Javascript
* are always local.
*
* @example
*
* Sugar.Date.setOption('newDateInternal', function() {
* var d = new Date(), offset;
* offset = (d.getTimezoneOffset() - 600) * 60 * 1000; // Hawaii time!
* d.setTime(d.getTime() + offset);
* return d;
* });
*
***/
var _dateOptions = defineOptionsAccessor(sugarDate, DATE_OPTIONS);
function setDateChainableConstructor() {
setChainableConstructor(sugarDate, createDate);
}
// General helpers
function getNewDate() {
return _dateOptions('newDateInternal')();
}
function defaultNewDate() {
return new Date;
}
function cloneDate(d) {
// Rhino environments have a bug where new Date(d) truncates
// milliseconds so need to call getTime() here.
var clone = new Date(d.getTime());
_utc(clone, !!_utc(d));
return clone;
}
function dateIsValid(d) {
return !isNaN(d.getTime());
}
function assertDateIsValid(d) {
if (!dateIsValid(d)) {
throw new TypeError('Date is not valid');
}
}
function getHours(d) {
return callDateGet(d, 'Hours');
}
function getWeekday(d) {
return callDateGet(d, 'Day');
}
function getDate(d) {
return callDateGet(d, 'Date');
}
function getMonth(d) {
return callDateGet(d, 'Month');
}
function getYear(d) {
return callDateGet(d, 'FullYear');
}
function setDate(d, val) {
callDateSet(d, 'Date', val);
}
function setMonth(d, val) {
callDateSet(d, 'Month', val);
}
function setYear(d, val) {
callDateSet(d, 'FullYear', val);
}
function getDaysInMonth(d) {
return 32 - callDateGet(new Date(getYear(d), getMonth(d), 32), 'Date');
}
function setWeekday(d, dow, dir) {
if (!isNumber(dow)) return;
var currentWeekday = getWeekday(d);
if (dir) {
// Allow a "direction" parameter to determine whether a weekday can
// be set beyond the current weekday in either direction.
var ndir = dir > 0 ? 1 : -1;
var offset = dow % 7 - currentWeekday;
if (offset && offset / abs(offset) !== ndir) {
dow += 7 * ndir;
}
}
setDate(d, getDate(d) + dow - currentWeekday);
return d.getTime();
}
// Normal callDateSet method with ability
// to handle ISOWeek setting as well.
function callDateSetWithWeek(d, method, value, safe) {
if (method === 'ISOWeek') {
setISOWeekNumber(d, value);
} else {
callDateSet(d, method, value, safe);
}
}
// UTC helpers
function isUTC(d) {
return !!_utc(d) || tzOffset(d) === 0;
}
function getUTCOffset(d, iso) {
var offset = _utc(d) ? 0 : tzOffset(d), hours, mins, colon;
colon = iso === true ? ':' : '';
if (!offset && iso) return 'Z';
hours = padNumber(trunc(-offset / 60), 2, true);
mins = padNumber(abs(offset % 60), 2);
return hours + colon + mins;
}
function tzOffset(d) {
return d.getTimezoneOffset();
}
// Argument helpers
function collectDateArguments(args, allowDuration) {
var arg1 = args[0], arg2 = args[1];
if (allowDuration && isString(arg1)) {
arg1 = getDateParamsFromString(arg1);
} else if (isNumber(arg1) && isNumber(arg2)) {
arg1 = collectDateParamsFromArguments(args);
arg2 = null;
} else {
if (isObjectType(arg1)) {
arg1 = simpleClone(arg1);
}
}
return [arg1, arg2];
}
function collectDateParamsFromArguments(args) {
var params = {}, index = 0;
walkUnitDown(YEAR_INDEX, function(unit) {
var arg = args[index++];
if (isDefined(arg)) {
params[unit.name] = arg;
}
});
return params;
}
function getDateParamsFromString(str) {
var match, num, params = {};
match = str.match(/^(-?\d*[\d.]\d*)?\s?(\w+?)s?$/i);
if (match) {
if (isUndefined(num)) {
num = +match[1];
if (isNaN(num)) {
num = 1;
}
}
params[match[2].toLowerCase()] = num;
}
return params;
}
// Iteration helpers
// Years -> Milliseconds
function iterateOverDateUnits(fn, startIndex, endIndex) {
endIndex = endIndex || 0;
if (isUndefined(startIndex)) {
startIndex = YEAR_INDEX;
}
for (var index = startIndex; index >= endIndex; index--) {
if (fn(DateUnits[index], index) === false) {
break;
}
}
}
// Years -> Milliseconds using getLower/Higher methods
function walkUnitDown(unitIndex, fn) {
while (unitIndex >= 0) {
if (fn(DateUnits[unitIndex], unitIndex) === false) {
break;
}
unitIndex = getLowerUnitIndex(unitIndex);
}
}
// Moving lower from specific unit
function getLowerUnitIndex(index) {
if (index === MONTH_INDEX) {
return DAY_INDEX;
} else if (index === WEEK_INDEX) {
return HOURS_INDEX;
}
return index - 1;
}
// Moving higher from specific unit
function getHigherUnitIndex(index) {
return index === DAY_INDEX ? MONTH_INDEX : index + 1;
}
// Years -> Milliseconds checking all date params including "weekday"
function iterateOverDateParams(params, fn, startIndex, endIndex) {
function run(name, unit, i) {
var val = getDateParam(params, name);
if (isDefined(val)) {
fn(name, val, unit, i);
}
}
iterateOverDateUnits(function (unit, i) {
var result = run(unit.name, unit, i);
if (result !== false && i === DAY_INDEX) {
// Check for "weekday", which has a distinct meaning
// in the context of setting a date, but has the same
// meaning as "day" as a unit of time.
result = run('weekday', unit, i);
}
return result;
}, startIndex, endIndex);
}
// Years -> Days
function iterateOverHigherDateParams(params, fn) {
iterateOverDateParams(params, fn, YEAR_INDEX, DAY_INDEX);
}
// Advancing helpers
function advanceDate(d, unit, num, reset) {
var set = {};
set[unit] = num;
return updateDate(d, set, reset, 1);
}
function advanceDateWithArgs(d, args, dir) {
args = collectDateArguments(args, true);
return updateDate(d, args[0], args[1], dir);
}
// Edge helpers
function resetTime(d) {
return setUnitAndLowerToEdge(d, HOURS_INDEX);
}
function resetLowerUnits(d, unitIndex) {
return setUnitAndLowerToEdge(d, getLowerUnitIndex(unitIndex));
}
function moveToBeginningOfWeek(d, firstDayOfWeek) {
setWeekday(d, floor((getWeekday(d) - firstDayOfWeek) / 7) * 7 + firstDayOfWeek);
return d;
}
function moveToEndOfWeek(d, firstDayOfWeek) {
var target = firstDayOfWeek - 1;
setWeekday(d, ceil((getWeekday(d) - target) / 7) * 7 + target);
return d;
}
function moveToBeginningOfUnit(d, unitIndex, localeCode) {
if (unitIndex === WEEK_INDEX) {
moveToBeginningOfWeek(d, localeManager.get(localeCode).getFirstDayOfWeek());
}
return setUnitAndLowerToEdge(d, getLowerUnitIndex(unitIndex));
}
function moveToEndOfUnit(d, unitIndex, localeCode, stopIndex) {
if (unitIndex === WEEK_INDEX) {
moveToEndOfWeek(d, localeManager.get(localeCode).getFirstDayOfWeek());
}
return setUnitAndLowerToEdge(d, getLowerUnitIndex(unitIndex), stopIndex, true);
}
function setUnitAndLowerToEdge(d, startIndex, stopIndex, end) {
walkUnitDown(startIndex, function(unit, i) {
var val = end ? unit.end : unit.start;
if (isFunction(val)) {
val = val(d);
}
callDateSet(d, unit.method, val);
return !isDefined(stopIndex) || i > stopIndex;
});
return d;
}
// Param helpers
function getDateParamKey(params, key) {
return getOwnKey(params, key) ||
getOwnKey(params, key + 's') ||
(key === 'day' && getOwnKey(params, 'date'));
}
function getDateParam(params, key) {
return getOwn(params, getDateParamKey(params, key));
}
function deleteDateParam(params, key) {
delete params[getDateParamKey(params, key)];
}
function getUnitIndexForParamName(name) {
var params = {}, unitIndex;
params[name] = 1;
iterateOverDateParams(params, function(name, val, unit, i) {
unitIndex = i;
return false;
});
return unitIndex;
}
// Time distance helpers
function getDaysSince(d1, d2) {
return getTimeDistanceForUnit(d1, d2, DateUnits[DAY_INDEX]);
}
function getTimeDistanceForUnit(d1, d2, unit) {
var fwd = d2 > d1, num, tmp;
if (!fwd) {
tmp = d2;
d2 = d1;
d1 = tmp;
}
num = d2 - d1;
if (unit.multiplier > 1) {
num = trunc(num / unit.multiplier);
}
// For higher order with potential ambiguity, use the numeric calculation
// as a starting point, then iterate until we pass the target date.
if (unit.ambiguous) {
d1 = cloneDate(d1);
if (num) {
advanceDate(d1, unit.name, num);
}
while (d1 < d2) {
advanceDate(d1, unit.name, 1);
if (d1 > d2) {
break;
}
num += 1;
}
}
return fwd ? -num : num;
}
// Parsing helpers
function getParsingTokenValue(token, str) {
var val;
if (token.val) {
val = token.val;
} else if (token.sign) {
val = str === '+' ? 1 : -1;
} else if (token.bool) {
val = !!val;
} else {
val = +str.replace(/,/, '.');
}
if (token.param === 'month') {
val -= 1;
}
return val;
}
function getYearFromAbbreviation(str, d, prefer) {
// Following IETF here, adding 1900 or 2000 depending on the last two digits.
// Note that this makes no accordance for what should happen after 2050, but
// intentionally ignoring this for now. https://www.ietf.org/rfc/rfc2822.txt
var val = +str, delta;
val += val < 50 ? 2000 : 1900;
if (prefer) {
delta = val - getYear(d);
if (delta / abs(delta) !== prefer) {
val += prefer * 100;
}
}
return val;
}
// Week number helpers
function setISOWeekNumber(d, num) {
if (isNumber(num)) {
// Intentionally avoiding updateDate here to prevent circular dependencies.
var isoWeek = cloneDate(d), dow = getWeekday(d);
moveToFirstDayOfWeekYear(isoWeek, ISO_FIRST_DAY_OF_WEEK, ISO_FIRST_DAY_OF_WEEK_YEAR);
setDate(isoWeek, getDate(isoWeek) + 7 * (num - 1));
setYear(d, getYear(isoWeek));
setMonth(d, getMonth(isoWeek));
setDate(d, getDate(isoWeek));
setWeekday(d, dow || 7);
}
return d.getTime();
}
function getWeekNumber(d, allowPrevious, firstDayOfWeek, firstDayOfWeekYear) {
var isoWeek, n = 0;
if (isUndefined(firstDayOfWeek)) {
firstDayOfWeek = ISO_FIRST_DAY_OF_WEEK;
}
if (isUndefined(firstDayOfWeekYear)) {
firstDayOfWeekYear = ISO_FIRST_DAY_OF_WEEK_YEAR;
}
// Moving to the end of the week allows for forward year traversal, ie
// Dec 29 2014 is actually week 01 of 2015.
isoWeek = moveToEndOfWeek(cloneDate(d), firstDayOfWeek);
moveToFirstDayOfWeekYear(isoWeek, firstDayOfWeek, firstDayOfWeekYear);
if (allowPrevious && d < isoWeek) {
// If the date is still before the start of the year, then it should be
// the last week of the previous year, ie Jan 1 2016 is actually week 53
// of 2015, so move to the beginning of the week to traverse the year.
isoWeek = moveToBeginningOfWeek(cloneDate(d), firstDayOfWeek);
moveToFirstDayOfWeekYear(isoWeek, firstDayOfWeek, firstDayOfWeekYear);
}
while (isoWeek <= d) {
// Doing a very simple walk to get the week number.
setDate(isoWeek, getDate(isoWeek) + 7);
n++;
}
return n;
}
// Week year helpers
function getWeekYear(d, localeCode, iso) {
var year, month, firstDayOfWeek, firstDayOfWeekYear, week, loc;
year = getYear(d);
month = getMonth(d);
if (month === 0 || month === 11) {
if (!iso) {
loc = localeManager.get(localeCode);
firstDayOfWeek = loc.getFirstDayOfWeek(localeCode);
firstDayOfWeekYear = loc.getFirstDayOfWeekYear(localeCode);
}
week = getWeekNumber(d, false, firstDayOfWeek, firstDayOfWeekYear);
if (month === 0 && week === 0) {
year -= 1;
} else if (month === 11 && week === 1) {
year += 1;
}
}
return year;
}
function moveToFirstDayOfWeekYear(d, firstDayOfWeek, firstDayOfWeekYear) {
setUnitAndLowerToEdge(d, MONTH_INDEX);
setDate(d, firstDayOfWeekYear);
moveToBeginningOfWeek(d, firstDayOfWeek);
}
// Relative helpers
function dateRelative(d, dRelative, arg1, arg2) {
var adu, format, type, localeCode, fn;
assertDateIsValid(d);
if (isFunction(arg1)) {
fn = arg1;
} else {
localeCode = arg1;
fn = arg2;
}
adu = getAdjustedUnitForDate(d, dRelative);
if (fn) {
format = fn.apply(d, adu.concat(localeManager.get(localeCode)));
if (format) {
return dateFormat(d, format, localeCode);
}
}
// Adjust up if time is in ms, as this doesn't
// look very good for a standard relative date.
if (adu[1] === 0) {
adu[1] = 1;
adu[0] = 1;
}
if (dRelative) {
type = 'duration';
} else if (adu[2] > 0) {
type = 'future';
} else {
type = 'past';
}
return localeManager.get(localeCode).getRelativeFormat(adu, type);
}
// Gets an "adjusted date unit" which is a way of representing
// the largest possible meaningful unit. In other words, if passed
// 3600000, this will return an array which represents "1 hour".
function getAdjustedUnit(ms, fn) {
var unitIndex = 0, value = 0;
iterateOverDateUnits(function(unit, i) {
value = abs(fn(unit));
if (value >= 1) {
unitIndex = i;
return false;
}
});
return [value, unitIndex, ms];
}
// Gets the adjusted unit based on simple division by
// date unit multiplier.
function getAdjustedUnitForNumber(ms) {
return getAdjustedUnit(ms, function(unit) {
return trunc(withPrecision(ms / unit.multiplier, 1));
});
}
// Gets the adjusted unit using the unitsFromNow methods,
// which use internal date methods that neatly avoid vaguely
// defined units of time (days in month, leap years, etc).
// Reserving dRelative to allow another date to be relative to.
function getAdjustedUnitForDate(d, dRelative) {
var ms;
if (!dRelative) {
dRelative = getNewDate();
if (d > dRelative) {
// If our date is greater than the one that we got from getNewDate, it
// means that we are finding the unit for a date that is in the future
// relative to now. However, often the incoming date was created in
// the same cycle as our comparison, but our "now" date will have been
// created an instant after it, creating situations where "5 minutes from
// now" becomes "4 minutes from now" in the same tick. To prevent this,
// subtract a buffer of 10ms to compensate.
dRelative = new Date(dRelative.getTime() - 10);
}
}
ms = d - dRelative;
return getAdjustedUnit(ms, function(u) {
return abs(getTimeDistanceForUnit(d, dRelative, u));
});
}
// Foramtting helpers
// Formatting tokens
var ldmlTokens, strfTokens;
function dateFormat(d, format, localeCode) {
assertDateIsValid(d);
format = CoreOutputFormats[format] || format || '{long}';
return dateFormatMatcher(format, d, localeCode);
}
function getMeridiemToken(d, localeCode) {
var hours = getHours(d);
return localeManager.get(localeCode).ampm[trunc(hours / 12)] || '';
}
function buildDateFormatTokens() {
function addFormats(target, tokens, fn) {
if (tokens) {
forEach(spaceSplit(tokens), function(token) {
target[token] = fn;
});
}
}
function buildLowercase(get) {
return function(d, localeCode) {
return get(d, localeCode).toLowerCase();
};
}
function buildOrdinal(get) {
return function(d, localeCode) {
var n = get(d, localeCode);
return n + localeManager.get(localeCode).getOrdinal(n);
};
}
function buildPadded(get, padding) {
return function(d, localeCode) {
return padNumber(get(d, localeCode), padding);
};
}
function buildTwoDigits(get) {
return function(d, localeCode) {
return get(d, localeCode) % 100;
};
}
function buildAlias(alias) {
return function(d, localeCode) {
return dateFormatMatcher(alias, d, localeCode);
};
}
function buildAlternates(f) {
for (var n = 1; n <= 5; n++) {
buildAlternate(f, n);
}
}
function buildAlternate(f, n) {
var alternate = function(d, localeCode) {
return f.get(d, localeCode, n);
};
addFormats(ldmlTokens, f.ldml + n, alternate);
if (f.lowerToken) {
ldmlTokens[f.lowerToken + n] = buildLowercase(alternate);
}
}
function getIdentityFormat(name) {
return function(d, localeCode) {
var loc = localeManager.get(localeCode);
return dateFormatMatcher(loc[name], d, localeCode);
};
}
ldmlTokens = {};
strfTokens = {};
forEach(FormatTokensBase, function(f) {
var get = f.get, getPadded;
if (f.lowerToken) {
ldmlTokens[f.lowerToken] = buildLowercase(get);
}
if (f.ordinalToken) {
ldmlTokens[f.ordinalToken] = buildOrdinal(get, f);
}
if (f.ldmlPaddedToken) {
ldmlTokens[f.ldmlPaddedToken] = buildPadded(get, f.ldmlPaddedToken.length);
}
if (f.ldmlTwoDigitToken) {
ldmlTokens[f.ldmlTwoDigitToken] = buildPadded(buildTwoDigits(get), 2);
}
if (f.strfTwoDigitToken) {
strfTokens[f.strfTwoDigitToken] = buildPadded(buildTwoDigits(get), 2);
}
if (f.strfPadding) {
getPadded = buildPadded(get, f.strfPadding);
}
if (f.alias) {
get = buildAlias(f.alias);
}
if (f.allowAlternates) {
buildAlternates(f);
}
addFormats(ldmlTokens, f.ldml, get);
addFormats(strfTokens, f.strf, getPadded || get);
});
forEachProperty(CoreOutputFormats, function(src, name) {
addFormats(ldmlTokens, name, buildAlias(src));
});
defineInstanceSimilar(sugarDate, 'short medium long full', function(methods, name) {
var fn = getIdentityFormat(name);
addFormats(ldmlTokens, name, fn);
methods[name] = fn;
});
addFormats(ldmlTokens, 'time', getIdentityFormat('time'));
addFormats(ldmlTokens, 'stamp', getIdentityFormat('stamp'));
}
// Format matcher
var dateFormatMatcher;
function buildDateFormatMatcher() {
function getLdml(d, token, localeCode) {
return getOwn(ldmlTokens, token)(d, localeCode);
}
function getStrf(d, token, localeCode) {
return getOwn(strfTokens, token)(d, localeCode);
}
function checkDateToken(ldml, strf) {
return hasOwn(ldmlTokens, ldml) || hasOwn(strfTokens, strf);
}
// Format matcher for LDML or STRF tokens.
dateFormatMatcher = createFormatMatcher(getLdml, getStrf, checkDateToken);
}
// Comparison helpers
function fullCompareDate(date, d, margin) {
var tmp;
if (!dateIsValid(date)) return;
if (isString(d)) {
d = trim(d).toLowerCase();
switch(true) {
case d === 'future': return date.getTime() > getNewDate().getTime();
case d === 'past': return date.getTime() < getNewDate().getTime();
case d === 'today': return compareDay(date);
case d === 'tomorrow': return compareDay(date, 1);
case d === 'yesterday': return compareDay(date, -1);
case d === 'weekday': return getWeekday(date) > 0 && getWeekday(date) < 6;
case d === 'weekend': return getWeekday(date) === 0 || getWeekday(date) === 6;
case (isDefined(tmp = English.weekdayMap[d])):
return getWeekday(date) === tmp;
case (isDefined(tmp = English.monthMap[d])):
return getMonth(date) === tmp;
}
}
return compareDate(date, d, margin);
}
function compareDate(date, d, margin, localeCode, options) {
var loMargin = 0, hiMargin = 0, timezoneShift, compareEdges, override, min, max, p, t;
function getTimezoneShift() {
// If there is any specificity in the date then we're implicitly not
// checking absolute time, so ignore timezone shifts.
if (p.set && p.set.specificity) {
return 0;
}
return (tzOffset(p.date) - tzOffset(date)) * MINUTES;
}
function addSpecificUnit() {
var unit = DateUnits[p.set.specificity];
return advanceDate(cloneDate(p.date), unit.name, 1).getTime() - 1;
}
if (_utc(date)) {
options = options || {};
options.fromUTC = true;
options.setUTC = true;
}
p = getExtendedDate(null, d, options, true);
if (margin > 0) {
loMargin = hiMargin = margin;
override = true;
}
if (!dateIsValid(p.date)) return false;
if (p.set && p.set.specificity) {
if (isDefined(p.set.edge) || isDefined(p.set.shift)) {
compareEdges = true;
moveToBeginningOfUnit(p.date, p.set.specificity, localeCode);
}
if (compareEdges || p.set.specificity === MONTH_INDEX) {
max = moveToEndOfUnit(cloneDate(p.date), p.set.specificity, localeCode).getTime();
} else {
max = addSpecificUnit();
}
if (!override && isDefined(p.set.sign) && p.set.specificity) {
// If the time is relative, there can occasionally be an disparity between
// the relative date and "now", which it is being compared to, so set an
// extra margin to account for this.
loMargin = 50;
hiMargin = -50;
}
}
t = date.getTime();
min = p.date.getTime();
max = max || min;
timezoneShift = getTimezoneShift();
if (timezoneShift) {
min -= timezoneShift;
max -= timezoneShift;
}
return t >= (min - loMargin) && t <= (max + hiMargin);
}
function compareDay(d, shift) {
var comp = getNewDate();
if (shift) {
setDate(comp, getDate(comp) + shift);
}
return getYear(d) === getYear(comp) &&
getMonth(d) === getMonth(comp) &&
getDate(d) === getDate(comp);
}
// Create helpers
function createDate(d, options, forceClone) {
return getExtendedDate(null, d, options, forceClone).date;
}
function createDateWithContext(contextDate, d, options, forceClone) {
return getExtendedDate(contextDate, d, options, forceClone).date;
}
function getExtendedDate(contextDate, d, opt, forceClone) {
var date, set, loc, options, afterCallbacks, relative, weekdayDir;
afterCallbacks = [];
options = getDateOptions(opt);
function getDateOptions(opt) {
var options = isString(opt) ? { locale: opt } : opt || {};
options.prefer = +!!getOwn(options, 'future') - +!!getOwn(options, 'past');
return options;
}
function getFormatParams(match, dif) {
var set = getOwn(options, 'params') || {};
forEach(dif.to, function(field, i) {
var str = match[i + 1], token, val;
if (!str) return;
if (field === 'yy' || field === 'y') {
field = 'year';
val = getYearFromAbbreviation(str, date, getOwn(options, 'prefer'));
} else if (token = getOwn(ParsingTokens, field)) {
field = token.param || field;
val = getParsingTokenValue(token, str);
} else {
val = loc.getTokenValue(field, str);
}
set[field] = val;
});
return set;
}
// Clone date will set the utc flag, but it will
// be overriden later, so set option flags instead.
function cloneDateByFlag(d, clone) {
if (_utc(d) && !isDefined(getOwn(options, 'fromUTC'))) {
options.fromUTC = true;
}
if (_utc(d) && !isDefined(getOwn(options, 'setUTC'))) {
options.setUTC = true;
}
if (clone) {
d = new Date(d.getTime());
}
return d;
}
function afterDateSet(fn) {
afterCallbacks.push(fn);
}
function fireCallbacks() {
forEach(afterCallbacks, function(fn) {
fn.call();
});
}
function parseStringDate(str) {
str = str.toLowerCase();
// The act of getting the locale will initialize
// if it is missing and add the required formats.
loc = localeManager.get(getOwn(options, 'locale'));
for (var i = 0, dif, match; dif = loc.compiledFormats[i]; i++) {
match = str.match(dif.reg);
if (match) {
// Note that caching the format will modify the compiledFormats array
// which is not a good idea to do inside its for loop, however we
// know at this point that we have a matched format and that we will
// break out below, so simpler to do it here.
loc.cacheFormat(dif, i);
set = getFormatParams(match, dif);
if (isDefined(set.timestamp)) {
str = set.timestamp;
set = null;
break;
}
if (isDefined(set.ampm)) {
handleAmpm(set.ampm);
}
if (set.utc || isDefined(set.tzHour)) {
handleTimezoneOffset(set.tzHour, set.tzMinute, set.tzSign);
}
if (isDefined(set.shift) && isUndefined(set.unit)) {
// "next january", "next monday", etc
handleUnitlessShift();
}
if (isDefined(set.num) && isUndefined(set.unit)) {
// "the second of January", etc
handleUnitlessNum(set.num);
}
if (set.midday) {
// "noon" and "midnight"
handleMidday(set.midday);
}
if (isDefined(set.day)) {
// Relative day localizations such as "today" and "tomorrow".
handleRelativeDay(set.day);
}
if (isDefined(set.unit)) {
// "3 days ago", etc
handleRelativeUnit(set.unit);
}
if (set.edge) {
// "the end of January", etc
handleEdge(set.edge, set);
}
if (set.yearSign) {
set.year *= set.yearSign;
}
break;
}
}
if (!set) {
// Fall back to native parsing
date = new Date(str);
if (getOwn(options, 'fromUTC')) {
// Falling back to system date here which cannot be parsed as UTC,
// so if we're forcing UTC then simply add the offset.
date.setTime(date.getTime() + (tzOffset(date) * MINUTES));
}
} else if (relative) {
updateDate(date, set, false, 1);
} else {
if (_utc(date)) {
// UTC times can traverse into other days or even months,
// so preemtively reset the time here to prevent this.
resetTime(date);
}
updateDate(date, set, true, 0, getOwn(options, 'prefer'), weekdayDir);
}
fireCallbacks();
return date;
}
function handleAmpm(ampm) {
if (ampm === 1 && set.hour < 12) {
// If the time is 1pm-11pm advance the time by 12 hours.
set.hour += 12;
} else if (ampm === 0 && set.hour === 12) {
// If it is 12:00am then set the hour to 0.
set.hour = 0;
}
}
function handleTimezoneOffset(tzHour, tzMinute, tzSign) {
// Adjust for timezone offset
_utc(date, true);
var offset = (tzSign || 1) * ((tzHour || 0) * 60 + (tzMinute || 0));
if (offset) {
set.minute = (set.minute || 0) - offset;
}
}
function handleUnitlessShift() {
if (isDefined(set.month)) {
// "next January"
set.unit = YEAR_INDEX;
} else if (isDefined(set.weekday)) {
// "next Monday"
set.unit = WEEK_INDEX;
}
}
function handleUnitlessNum(num) {
if (isDefined(set.weekday)) {
// "The second Tuesday of March"
setOrdinalWeekday(num);
} else if (isDefined(set.month)) {
// "The second of March"
set.date = set.num;
}
}
function handleMidday(hour) {
set.hour = hour % 24;
if (hour > 23) {
// If the date has hours past 24, we need to prevent it from traversing
// into a new day as that would make it being part of a new week in
// ambiguous dates such as "Monday".
afterDateSet(function() {
advanceDate(date, 'date', trunc(hour / 24));
});
}
}
function handleRelativeDay() {
resetTime(date);
if (isUndefined(set.unit)) {
set.unit = DAY_INDEX;
set.num = set.day;
delete set.day;
}
}
function handleRelativeUnit(unitIndex) {
var num = isDefined(set.num) ? set.num : 1;
// If a weekday is defined, there are 3 possible formats being applied:
//
// 1. "the day after monday": unit is days
// 2. "next monday": short for "next week monday", unit is weeks
// 3. "the 2nd monday of next month": unit is months
//
// In the first case, we need to set the weekday up front, as the day is
// relative to it. The second case also needs to be handled up front for
// formats like "next monday at midnight" which will have its weekday reset
// if not set up front. The last case will set up the params necessary to
// shift the weekday and allow separateAbsoluteUnits below to handle setting
// it after the date has been shifted.
if(isDefined(set.weekday)) {
if(unitIndex === MONTH_INDEX) {
setOrdinalWeekday(num);
num = 1;
} else {
updateDate(date, { weekday: set.weekday }, true);
delete set.weekday;
}
}
if (set.half) {
// Allow localized "half" as a standalone colloquialism. Purposely avoiding
// the locale number system to reduce complexity. The units "month" and
// "week" are purposely excluded in the English date formats below, as
// "half a week" and "half a month" are meaningless as exact dates.
num *= set.half;
}
if (isDefined(set.shift)) {
// Shift and unit, ie "next month", "last week", etc.
num *= set.shift;
} else if (set.sign) {
// Unit and sign, ie "months ago", "weeks from now", etc.
num *= set.sign;
}
if (isDefined(set.day)) {
// "the day after tomorrow"
num += set.day;
delete set.day;
}
// Formats like "the 15th of last month" or "6:30pm of next week"
// contain absolute units in addition to relative ones, so separate
// them here, remove them from the params, and set up a callback to
// set them after the relative ones have been set.
separateAbsoluteUnits(unitIndex);
// Finally shift the unit.
set[English.units[unitIndex]] = num;
relative = true;
}
function handleEdge(edge, params) {
var edgeIndex = params.unit, weekdayOfMonth;
if (!edgeIndex) {
// If we have "the end of January", then we need to find the unit index.
iterateOverHigherDateParams(params, function(unitName, val, unit, i) {
if (unitName === 'weekday' && isDefined(params.month)) {
// If both a month and weekday exist, then we have a format like
// "the last tuesday in November, 2012", where the "last" is still
// relative to the end of the month, so prevent the unit "weekday"
// from taking over.
return;
}
edgeIndex = i;
});
}
if (edgeIndex === MONTH_INDEX && isDefined(params.weekday)) {
// If a weekday in a month exists (as described above),
// then set it up to be set after the date has been shifted.
weekdayOfMonth = params.weekday;
delete params.weekday;
}
afterDateSet(function() {
var stopIndex;
// "edge" values that are at the very edge are "2" so the beginning of the
// year is -2 and the end of the year is 2. Conversely, the "last day" is
// actually 00:00am so it is 1. -1 is reserved but unused for now.
if (edge < 0) {
moveToBeginningOfUnit(date, edgeIndex, getOwn(options, 'locale'));
} else if (edge > 0) {
if (edge === 1) {
stopIndex = DAY_INDEX;
moveToBeginningOfUnit(date, DAY_INDEX);
}
moveToEndOfUnit(date, edgeIndex, getOwn(options, 'locale'), stopIndex);
}
if (isDefined(weekdayOfMonth)) {
setWeekday(date, weekdayOfMonth, -edge);
resetTime(date);
}
});
if (edgeIndex === MONTH_INDEX) {
params.specificity = DAY_INDEX;
} else {
params.specificity = edgeIndex - 1;
}
}
function setOrdinalWeekday(num) {
// If we have "the 2nd Tuesday of June", then pass the "weekdayDir"
// flag along to updateDate so that the date does not accidentally traverse
// into the previous month. This needs to be independent of the "prefer"
// flag because we are only ensuring that the weekday is in the future, not
// the entire date.
set.weekday = 7 * (num - 1) + set.weekday;
set.date = 1;
weekdayDir = 1;
}
function separateAbsoluteUnits(unitIndex) {
var params;
iterateOverDateParams(set, function(name, val, unit, i) {
// If there is a time unit set that is more specific than
// the matched unit we have a string like "5:30am in 2 minutes",
// which is meaningless, so invalidate the date...
if (i >= unitIndex) {
date.setTime(NaN);
return false;
} else if (i < unitIndex) {
// ...otherwise set the params to set the absolute date
// as a callback after the relative date has been set.
params = params || {};
params[name] = val;
deleteDateParam(set, name);
}
});
if (params) {
afterDateSet(function() {
updateDate(date, params, true, false, getOwn(options, 'prefer'), weekdayDir);
});
if (set.edge) {
// "the end of March of next year"
handleEdge(set.edge, params);
delete set.edge;
}
}
}
if (contextDate && d) {
// If a context date is passed ("get" and "unitsFromNow"),
// then use it as the starting point.
date = cloneDateByFlag(contextDate, true);
} else {
date = getNewDate();
}
_utc(date, getOwn(options, 'fromUTC'));
if (isString(d)) {
date = parseStringDate(d);
} else if (isDate(d)) {
date = cloneDateByFlag(d, hasOwn(options, 'clone') || forceClone);
} else if (isObjectType(d)) {
set = simpleClone(d);
updateDate(date, set, true);
} else if (isNumber(d) || d === null) {
date.setTime(d);
}
// A date created by parsing a string presumes that the format *itself* is
// UTC, but not that the date, once created, should be manipulated as such. In
// other words, if you are creating a date object from a server time
// "2012-11-15T12:00:00Z", in the majority of cases you are using it to create
// a date that will, after creation, be manipulated as local, so reset the utc
// flag here unless "setUTC" is also set.
_utc(date, !!getOwn(options, 'setUTC'));
return {
set: set,
date: date
};
}
function updateDate(d, params, reset, advance, prefer, weekdayDir) {
var upperUnitIndex;
function setUpperUnit(unitName, unitIndex) {
if (prefer && !upperUnitIndex) {
if (unitName === 'weekday') {
upperUnitIndex = WEEK_INDEX;
} else {
upperUnitIndex = getHigherUnitIndex(unitIndex);
}
}
}
function setSpecificity(unitIndex) {
// Other functions may preemptively set the specificity before arriving
// here so concede to them if they have already set more specific units.
if (unitIndex > params.specificity) {
return;
}
params.specificity = unitIndex;
}
function canDisambiguate() {
if (!upperUnitIndex || upperUnitIndex > YEAR_INDEX) {
return;
}
switch(prefer) {
case -1: return d > getNewDate();
case 1: return d < getNewDate();
}
}
function disambiguateHigherUnit() {
var unit = DateUnits[upperUnitIndex];
advance = prefer;
setUnit(unit.name, 1, unit, upperUnitIndex);
}
function handleFraction(unit, unitIndex, fraction) {
if (unitIndex) {
var lowerUnit = DateUnits[getLowerUnitIndex(unitIndex)];
var val = round(unit.multiplier / lowerUnit.multiplier * fraction);
params[lowerUnit.name] = val;
}
}
function monthHasShifted(d, targetMonth) {
if (targetMonth < 0) {
targetMonth = targetMonth % 12 + 12;
}
return targetMonth % 12 !== getMonth(d);
}
function setUnit(unitName, value, unit, unitIndex) {
var method = unit.method, checkMonth, fraction;
setUpperUnit(unitName, unitIndex);
setSpecificity(unitIndex);
fraction = value % 1;
if (fraction) {
handleFraction(unit, unitIndex, fraction);
value = trunc(value);
}
if (unitName === 'weekday') {
if (!advance) {
// Weekdays are always considered absolute units so simply set them
// here even if it is an "advance" operation. This is to help avoid
// ambiguous meanings in "advance" as well as to neatly allow formats
// like "Wednesday of next week" without more complex logic.
setWeekday(d, value, weekdayDir);
}
return;
}
checkMonth = unitIndex === MONTH_INDEX && getDate(d) > 28;
// If we are advancing or rewinding, then we need we need to set the
// absolute time if the unit is "hours" or less. This is due to the fact
// that setting by method is ambiguous during DST shifts. For example,
// 1:00am on November 1st 2015 occurs twice in North American timezones
// with DST, the second time being after the clocks are rolled back at
// 2:00am. When springing forward this is automatically handled as there
// is no 2:00am so the date automatically jumps to 3:00am. However, when
// rolling back, setHours(2) will always choose the first "2am" even if
// the date is currently set to the second, causing unintended jumps.
// This ambiguity is unavoidable when setting dates as the notation is
// ambiguous. However when advancing, we clearly want the resulting date
// to be an acutal hour ahead, which can only be accomplished by setting
// the absolute time. Conversely, any unit higher than "hours" MUST use
// the internal set methods, as they are ambiguous as absolute units of
// time. Years may be 365 or 366 days depending on leap years, months are
// all over the place, and even days may be 23-25 hours depending on DST
// shifts. Finally, note that the kind of jumping described above will
// occur when calling ANY "set" method on the date and will occur even if
// the value being set is identical to the one currently set (i.e.
// setHours(2) on a date at 2am may not be a noop). This is precarious,
// so avoiding this situation in callDateSet by checking up front that
// the value is not the same before setting.
if (advance && !unit.ambiguous) {
d.setTime(d.getTime() + (value * advance * unit.multiplier));
return;
} else if (advance) {
if (unitIndex === WEEK_INDEX) {
value *= 7;
method = DateUnits[DAY_INDEX].method;
}
value = (value * advance) + callDateGet(d, method);
}
callDateSetWithWeek(d, method, value, advance);
if (checkMonth && monthHasShifted(d, value)) {
// As we are setting the units in reverse order, there is a chance that
// our date may accidentally traverse into a new month, such as setting
// { month: 1, date 15 } on January 31st. Check for this here and reset
// the date to the last day of the previous month if this has happened.
setDate(d, 0);
}
}
if (isNumber(params) && advance) {
// If param is a number and advancing, the number is in milliseconds.
params = { millisecond: params };
} else if (isNumber(params)) {
// Otherwise just set the timestamp and return.
d.setTime(params);
return d;
}
iterateOverDateParams(params, setUnit);
if (reset && params.specificity) {
resetLowerUnits(d, params.specificity);
}
// If past or future is preferred, then the process of "disambiguation" will
// ensure that an ambiguous time/date ("4pm", "thursday", "June", etc.) will
// be in the past or future. Weeks are only considered ambiguous if there is
// a weekday, i.e. "thursday" is an ambiguous week, but "the 4th" is an
// ambiguous month.
if (canDisambiguate()) {
disambiguateHigherUnit();
}
return d;
}
// Locales
// Locale helpers
var English, localeManager;
function getEnglishVariant(v) {
return simpleMerge(simpleClone(EnglishLocaleBaseDefinition), v);
}
function arrayToRegAlternates(arr) {
var joined = arr.join('');
if (!arr || !arr.length) {
return '';
}
if (joined.length === arr.length) {
return '[' + joined + ']';
}
// map handles sparse arrays so no need to compact the array here.
return map(arr, escapeRegExp).join('|');
}
function getRegNonCapturing(src, opt) {
if (src.length > 1) {
src = '(?:' + src + ')';
}
if (opt) {
src += '?';
}
return src;
}
function getParsingTokenWithSuffix(field, src, suffix) {
var token = LocalizedParsingTokens[field];
if (token.requiresSuffix) {
src = getRegNonCapturing(src + getRegNonCapturing(suffix));
} else if (token.requiresSuffixOr) {
src += getRegNonCapturing(token.requiresSuffixOr + '|' + suffix);
} else {
src += getRegNonCapturing(suffix, true);
}
return src;
}
function getArrayWithOffset(arr, n, alternate, offset) {
var val;
if (alternate > 1) {
val = arr[n + (alternate - 1) * offset];
}
return val || arr[n];
}
function buildLocales() {
function LocaleManager(loc) {
this.locales = {};
this.add(loc);
}
LocaleManager.prototype = {
get: function(code, fallback) {
var loc = this.locales[code];
if (!loc && LazyLoadedLocales[code]) {
loc = this.add(code, LazyLoadedLocales[code]);
} else if (!loc && code) {
loc = this.locales[code.slice(0, 2)];
}
return loc || fallback === false ? loc : this.current;
},
getAll: function() {
return this.locales;
},
set: function(code) {
var loc = this.get(code, false);
if (!loc) {
throw new TypeError('Invalid Locale: ' + code);
}
return this.current = loc;
},
add: function(code, def) {
if (!def) {
def = code;
code = def.code;
} else {
def.code = code;
}
var loc = def.compiledFormats ? def : getNewLocale(def);
this.locales[code] = loc;
if (!this.current) {
this.current = loc;
}
return loc;
},
remove: function(code) {
if (this.current.code === code) {
this.current = this.get('en');
}
return delete this.locales[code];
}
};
// Sorry about this guys...
English = getNewLocale(AmericanEnglishDefinition);
localeManager = new LocaleManager(English);
}
function getNewLocale(def) {
function Locale(def) {
this.init(def);
}
Locale.prototype = {
getMonthName: function(n, alternate) {
if (this.monthSuffix) {
return (n + 1) + this.monthSuffix;
}
return getArrayWithOffset(this.months, n, alternate, 12);
},
getWeekdayName: function(n, alternate) {
return getArrayWithOffset(this.weekdays, n, alternate, 7);
},
getTokenValue: function(field, str) {
var map = this[field + 'Map'], val;
if (map) {
val = map[str];
}
if (isUndefined(val)) {
val = this.getNumber(str);
if (field === 'month') {
// Months are the only numeric date field
// whose value is not the same as its number.
val -= 1;
}
}
return val;
},
getNumber: function(str) {
var num = this.numeralMap[str];
if (isDefined(num)) {
return num;
}
// The unary plus operator here show better performance and handles
// every format that parseFloat does with the exception of trailing
// characters, which are guaranteed not to be in our string at this point.
num = +str.replace(/,/, '.');
if (!isNaN(num)) {
return num;
}
num = this.getNumeralValue(str);
if (!isNaN(num)) {
this.numeralMap[str] = num;
return num;
}
return num;
},
getNumeralValue: function(str) {
var place = 1, num = 0, lastWasPlace, isPlace, numeral, digit, arr;
// Note that "numerals" that need to be converted through this method are
// all considered to be single characters in order to handle CJK. This
// method is by no means unique to CJK, but the complexity of handling
// inflections in non-CJK languages adds too much overhead for not enough
// value, so avoiding for now.
arr = str.split('');
for (var i = arr.length - 1; numeral = arr[i]; i--) {
digit = getOwn(this.numeralMap, numeral);
if (isUndefined(digit)) {
digit = getOwn(fullWidthNumberMap, numeral) || 0;
}
isPlace = digit > 0 && digit % 10 === 0;
if (isPlace) {
if (lastWasPlace) {
num += place;
}
if (i) {
place = digit;
} else {
num += digit;
}
} else {
num += digit * place;
place *= 10;
}
lastWasPlace = isPlace;
}
return num;
},
getOrdinal: function(n) {
var suffix = this.ordinalSuffix;
return suffix || getOrdinalSuffix(n);
},
getRelativeFormat: function(adu, type) {
return this.convertAdjustedToFormat(adu, type);
},
getDuration: function(ms) {
return this.convertAdjustedToFormat(getAdjustedUnitForNumber(max(0, ms)), 'duration');
},
getFirstDayOfWeek: function() {
var val = this.firstDayOfWeek;
return isDefined(val) ? val : ISO_FIRST_DAY_OF_WEEK;
},
getFirstDayOfWeekYear: function() {
return this.firstDayOfWeekYear || ISO_FIRST_DAY_OF_WEEK_YEAR;
},
convertAdjustedToFormat: function(adu, type) {
var sign, unit, mult,
num = adu[0],
u = adu[1],
ms = adu[2],
format = this[type] || this.relative;
if (isFunction(format)) {
return format.call(this, num, u, ms, type);
}
mult = !this.plural || num === 1 ? 0 : 1;
unit = this.units[mult * 8 + u] || this.units[u];
sign = this[ms > 0 ? 'fromNow' : 'ago'];
return format.replace(/\{(.*?)\}/g, function(full, match) {
switch(match) {
case 'num': return num;
case 'unit': return unit;
case 'sign': return sign;
}
});
},
cacheFormat: function(dif, i) {
this.compiledFormats.splice(i, 1);
this.compiledFormats.unshift(dif);
},
addFormat: function(src, to) {
var loc = this;
function getTokenSrc(str) {
var suffix, src, val,
opt = str.match(/\?$/),
nc = str.match(/^(\d+)\??$/),
slice = str.match(/(\d)(?:-(\d))?/),
key = str.replace(/[^a-z]+$/i, '');
// Allowing alias tokens such as {time}
if (val = getOwn(loc.parsingAliases, key)) {
src = replaceParsingTokens(val);
if (opt) {
src = getRegNonCapturing(src, true);
}
return src;
}
if (nc) {
src = loc.tokens[nc[1]];
} else if (val = getOwn(ParsingTokens, key)) {
src = val.src;
} else {
val = getOwn(loc.parsingTokens, key) || getOwn(loc, key);
// Both the "months" array and the "month" parsing token can be accessed
// by either {month} or {months}, falling back as necessary, however
// regardless of whether or not a fallback occurs, the final field to
// be passed to addRawFormat must be normalized as singular.
key = key.replace(/s$/, '');
if (!val) {
val = getOwn(loc.parsingTokens, key) || getOwn(loc, key + 's');
}
if (isString(val)) {
src = val;
suffix = loc[key + 'Suffix'];
} else {
if (slice) {
val = filter(val, function(m, i) {
var mod = i % (loc.units ? 8 : val.length);
return mod >= slice[1] && mod <= (slice[2] || slice[1]);
});
}
src = arrayToRegAlternates(val);
}
}
if (!src) {
return '';
}
if (nc) {
// Non-capturing tokens like {0}
src = getRegNonCapturing(src);
} else {
// Capturing group and add to parsed tokens
to.push(key);
src = '(' + src + ')';
}
if (suffix) {
// Date/time suffixes such as those in CJK
src = getParsingTokenWithSuffix(key, src, suffix);
}
if (opt) {
src += '?';
}
return src;
}
function replaceParsingTokens(str) {
// Make spaces optional
str = str.replace(/ /g, ' ?');
return str.replace(/\{([^,]+?)\}/g, function(match, token) {
var tokens = token.split('|'), src;
if (tokens.length > 1) {
src = getRegNonCapturing(map(tokens, getTokenSrc).join('|'));
} else {
src = getTokenSrc(token);
}
return src;
});
}
if (!to) {
to = [];
src = replaceParsingTokens(src);
}
loc.addRawFormat(src, to);
},
addRawFormat: function(format, to) {
this.compiledFormats.unshift({
reg: RegExp('^ *' + format + ' *$', 'i'),
to: to
});
},
init: function(def) {
var loc = this;
// -- Initialization helpers
function initFormats() {
loc.compiledFormats = [];
loc.parsingAliases = {};
loc.parsingTokens = {};
}
function initDefinition() {
simpleMerge(loc, def);
}
function initArrayFields() {
forEach(LOCALE_ARRAY_FIELDS, function(name) {
var val = loc[name];
if (isString(val)) {
loc[name] = commaSplit(val);
} else if (!val) {
loc[name] = [];
}
});
}
// -- Value array build helpers
function buildValueArray(name, mod, map, fn) {
var field = name, all = [], setMap;
if (!loc[field]) {
field += 's';
}
if (!map) {
map = {};
setMap = true;
}
forAllAlternates(field, function(alt, j, i) {
var idx = j * mod + i, val;
val = fn ? fn(i) : i;
map[alt] = val;
map[alt.toLowerCase()] = val;
all[idx] = alt;
});
loc[field] = all;
if (setMap) {
loc[name + 'Map'] = map;
}
}
function forAllAlternates(field, fn) {
forEach(loc[field], function(str, i) {
forEachAlternate(str, function(alt, j) {
fn(alt, j, i);
});
});
}
function forEachAlternate(str, fn) {
var arr = map(str.split('+'), function(split) {
return split.replace(/(.+):(.+)$/, function(full, base, suffixes) {
return map(suffixes.split('|'), function(suffix) {
return base + suffix;
}).join('|');
});
}).join('|');
forEach(arr.split('|'), fn);
}
function buildNumerals() {
var map = {};
buildValueArray('numeral', 10, map);
buildValueArray('article', 1, map, function() {
return 1;
});
buildValueArray('placeholder', 4, map, function(n) {
return pow(10, n + 1);
});
loc.numeralMap = map;
}
function buildTimeFormats() {
loc.parsingAliases['time'] = getTimeFormat();
loc.parsingAliases['tzOffset'] = getTZOffsetFormat();
}
function getTimeFormat() {
var src;
if (loc.ampmFront) {
// "ampmFront" exists mostly for CJK locales, which also presume that
// time suffixes exist, allowing this to be a simpler regex.
src = '{ampm?} {hour} (?:{minute} (?::?{second})?)?';
} else if(loc.ampm.length) {
src = '{hour}(?:[.:]{minute}(?:[.:]{second})? {ampm?}| {ampm})';
} else {
src = '{hour}(?:[.:]{minute}(?:[.:]{second})?)';
}
return src;
}
function getTZOffsetFormat() {
return '(?:{Z}|{GMT?}(?:{tzSign}{tzHour}(?::?{tzMinute}(?: \\([\\w\\s]+\\))?)?)?)?';
}
function buildParsingTokens() {
forEachProperty(LocalizedParsingTokens, function(token, name) {
var src, arr;
src = token.base ? ParsingTokens[token.base].src : token.src;
if (token.requiresNumerals || loc.numeralUnits) {
src += getNumeralSrc();
}
arr = loc[name + 's'];
if (arr && arr.length) {
src += '|' + arrayToRegAlternates(arr);
}
loc.parsingTokens[name] = src;
});
}
function getNumeralSrc() {
var all, src = '';
all = loc.numerals.concat(loc.placeholders).concat(loc.articles);
if (loc.allowsFullWidth) {
all = all.concat(fullWidthNumbers.split(''));
}
if (all.length) {
src = '|(?:' + arrayToRegAlternates(all) + ')+';
}
return src;
}
function buildTimeSuffixes() {
iterateOverDateUnits(function(unit, i) {
var token = loc.timeSuffixes[i];
if (token) {
loc[(unit.alias || unit.name) + 'Suffix'] = token;
}
});
}
function buildModifiers() {
forEach(loc.modifiers, function(modifier) {
var name = modifier.name, mapKey = name + 'Map', map;
map = loc[mapKey] || {};
forEachAlternate(modifier.src, function(alt, j) {
var token = getOwn(loc.parsingTokens, name), val = modifier.value;
map[alt] = val;
loc.parsingTokens[name] = token ? token + '|' + alt : alt;
if (modifier.name === 'sign' && j === 0) {
// Hooking in here to set the first "fromNow" or "ago" modifier
// directly on the locale, so that it can be reused in the
// relative format.
loc[val === 1 ? 'fromNow' : 'ago'] = alt;
}
});
loc[mapKey] = map;
});
}
// -- Format adding helpers
function addCoreFormats() {
forEach(CoreParsingFormats, function(df) {
var src = df.src;
if (df.mdy && loc.mdy) {
// Use the mm/dd/yyyy variant if it
// exists and the locale requires it
src = df.mdy;
}
if (df.time) {
// Core formats that allow time require the time
// reg on both sides, so add both versions here.
loc.addFormat(getFormatWithTime(src, true));
loc.addFormat(getFormatWithTime(src));
} else {
loc.addFormat(src);
}
});
loc.addFormat('{time}');
}
function addLocaleFormats() {
addFormatSet('parse');
addFormatSet('timeParse', true);
addFormatSet('timeFrontParse', true, true);
}
function addFormatSet(field, allowTime, timeFront) {
forEach(loc[field], function(format) {
if (allowTime) {
format = getFormatWithTime(format, timeFront);
}
loc.addFormat(format);
});
}
function getFormatWithTime(baseFormat, timeBefore) {
if (timeBefore) {
return getTimeBefore() + baseFormat;
}
return baseFormat + getTimeAfter();
}
function getTimeBefore() {
return getRegNonCapturing('{time}[,\\s\\u3000]', true);
}
function getTimeAfter() {
var markers = ',?[\\s\\u3000]', localized;
localized = arrayToRegAlternates(loc.timeMarkers);
if (localized) {
markers += '| (?:' + localized + ') ';
}
markers = getRegNonCapturing(markers, loc.timeMarkerOptional);
return getRegNonCapturing(markers + '{time}', true);
}
initFormats();
initDefinition();
initArrayFields();
buildValueArray('month', 12);
buildValueArray('weekday', 7);
buildValueArray('unit', 8);
buildValueArray('ampm', 2);
buildNumerals();
buildTimeFormats();
buildParsingTokens();
buildTimeSuffixes();
buildModifiers();
// The order of these formats is important. Order is reversed so formats
// that are initialized later will take precedence. Generally, this means
// that more specific formats should come later.
addCoreFormats();
addLocaleFormats();
}
};
return new Locale(def);
}
/***
* @method [units]Since([d], [options])
* @returns Number
* @short Returns the time since [d].
* @extra [d] will accept a date object, timestamp, or string. If not specified,
* [d] is assumed to be now. `unitsAgo` is provided as an alias to make
* this more readable when [d] is assumed to be the current date.
* [options] can be an object or a locale code as a string. See `create`
* for more.
*
* @set
* millisecondsSince
* secondsSince
* minutesSince
* hoursSince
* daysSince
* weeksSince
* monthsSince
* yearsSince
*
* @example
*
* new Date().millisecondsSince('1 hour ago') -> 3,600,000
* new Date().daysSince('1 week ago') -> 7
* new Date().yearsSince('15 years ago') -> 15
* lastYear.yearsAgo() -> 1
*
***
* @method [units]Ago()
* @returns Number
* @short Returns the time ago in the appropriate unit.
*
* @set
* millisecondsAgo
* secondsAgo
* minutesAgo
* hoursAgo
* daysAgo
* weeksAgo
* monthsAgo
* yearsAgo
*
* @example
*
* lastYear.millisecondsAgo() -> 3,600,000
* lastYear.daysAgo() -> 7
* lastYear.yearsAgo() -> 15
*
***
* @method [units]Until([d], [options])
* @returns Number
* @short Returns the time until [d].
* @extra [d] will accept a date object, timestamp, or string. If not specified,
* [d] is assumed to be now. `unitsFromNow` is provided as an alias to
* make this more readable when [d] is assumed to be the current date.
* [options] can be an object or a locale code as a string. See `create`
* for more.
*
*
* @set
* millisecondsUntil
* secondsUntil
* minutesUntil
* hoursUntil
* daysUntil
* weeksUntil
* monthsUntil
* yearsUntil
*
* @example
*
* new Date().millisecondsUntil('1 hour from now') -> 3,600,000
* new Date().daysUntil('1 week from now') -> 7
* new Date().yearsUntil('15 years from now') -> 15
* nextYear.yearsFromNow() -> 1
*
***
* @method [units]FromNow()
* @returns Number
* @short Returns the time from now in the appropriate unit.
*
* @set
* millisecondsFromNow
* secondsFromNow
* minutesFromNow
* hoursFromNow
* daysFromNow
* weeksFromNow
* monthsFromNow
* yearsFromNow
*
* @example
*
* nextYear.millisecondsFromNow() -> 3,600,000
* nextYear.daysFromNow() -> 7
* nextYear.yearsFromNow() -> 15
*
***
* @method add[Units](<n>, [reset] = false)
* @returns Date
* @short Adds <n> units to the date. If [reset] is true, all lower units will
* be reset.
* @extra This method modifies the date! Note that in the case of `addMonths`,
* the date may fall on a date that doesn't exist (i.e. February 30). In
* this case the date will be shifted to the last day of the month. Don't
* use `addMonths` if you need precision.
*
* @set
* addMilliseconds
* addSeconds
* addMinutes
* addHours
* addDays
* addWeeks
* addMonths
* addYears
*
* @example
*
* new Date().addYears(5) -> current time + 5 years
* new Date().addDays(5) -> current time + 5 days
* new Date().addDays(5, true) -> current time + 5 days (time reset)
*
***
* @method isLast[Unit]([locale])
* @returns Boolean
* @short Returns true if the date is last week, month, or year.
* @extra This method takes an optional locale code for `isLastWeek`, which is
* locale dependent. The default locale code is `en`, which places
* Sunday at the beginning of the week. You can pass `en-GB` as a quick
* way to force Monday as the beginning of the week.
*
* @set
* isLastWeek
* isLastMonth
* isLastYear
*
* @example
*
* yesterday.isLastWeek() -> true or false?
* yesterday.isLastMonth() -> probably not...
* yesterday.isLastYear() -> even less likely...
*
***
* @method isThis[Unit]([locale])
* @returns Boolean
* @short Returns true if the date is this week, month, or year.
* @extra This method takes an optional locale code for `isThisWeek`, which is
* locale dependent. The default locale code is `en`, which places
* Sunday at the beginning of the week. You can pass `en-GB` as a quick
* way to force Monday as the beginning of the week.
*
* @set
* isThisWeek
* isThisMonth
* isThisYear
*
* @example
*
* tomorrow.isThisWeek() -> true or false?
* tomorrow.isThisMonth() -> probably...
* tomorrow.isThisYear() -> signs point to yes...
*
***
* @method isNext[Unit]([locale])
* @returns Boolean
* @short Returns true if the date is next week, month, or year.
* @extra This method takes an optional locale code for `isNextWeek`, which is
* locale dependent. The default locale code is `en`, which places
* Sunday at the beginning of the week. You can pass `en-GB` as a quick
* way to force Monday as the beginning of the week.
*
* @set
* isNextWeek
* isNextMonth
* isNextYear
*
* @example
*
* tomorrow.isNextWeek() -> true or false?
* tomorrow.isNextMonth() -> probably not...
* tomorrow.isNextYear() -> even less likely...
*
***
* @method beginningOf[Unit]([locale])
* @returns Date
* @short Sets the date to the beginning of the appropriate unit.
* @extra This method modifies the date! A locale code can be passed for
* `beginningOfWeek`, which is locale dependent. If consistency is
* needed, use `beginningOfISOWeek` instead.
*
* @set
* beginningOfDay
* beginningOfWeek
* beginningOfMonth
* beginningOfYear
*
* @example
*
* new Date().beginningOfDay() -> the beginning of today (resets the time)
* new Date().beginningOfWeek() -> the beginning of the week
* new Date().beginningOfMonth() -> the beginning of the month
* new Date().beginningOfYear() -> the beginning of the year
*
***
* @method endOf[Unit]([locale])
* @returns Date
* @short Sets the date to the end of the appropriate unit.
* @extra This method modifies the date! A locale code can be passed for
* `endOfWeek`, which is locale dependent. If consistency is needed, use
* `endOfISOWeek` instead.
*
* @set
* endOfDay
* endOfWeek
* endOfMonth
* endOfYear
*
* @example
*
* new Date().endOfDay() -> the end of today (sets the time to 23:59:59.999)
* new Date().endOfWeek() -> the end of the week
* new Date().endOfMonth() -> the end of the month
* new Date().endOfYear() -> the end of the year
*
***/
function buildDateUnitMethods() {
defineInstanceSimilar(sugarDate, DateUnits, function(methods, unit, index) {
var name = unit.name, caps = simpleCapitalize(name);
if (index > DAY_INDEX) {
forEach(['Last','This','Next'], function(shift) {
methods['is' + shift + caps] = function(d, localeCode) {
return compareDate(d, shift + ' ' + name, 0, localeCode, { locale: 'en' });
};
});
}
if (index > HOURS_INDEX) {
methods['beginningOf' + caps] = function(d, localeCode) {
return moveToBeginningOfUnit(d, index, localeCode);
};
methods['endOf' + caps] = function(d, localeCode) {
return moveToEndOfUnit(d, index, localeCode);
};
}
methods['add' + caps + 's'] = function(d, num, reset) {
return advanceDate(d, name, num, reset);
};
var since = function(date, d, options) {
return getTimeDistanceForUnit(date, createDateWithContext(date, d, options, true), unit);
};
var until = function(date, d, options) {
return getTimeDistanceForUnit(createDateWithContext(date, d, options, true), date, unit);
};
methods[name + 'sAgo'] = methods[name + 'sUntil'] = until;
methods[name + 'sSince'] = methods[name + 'sFromNow'] = since;
});
}
/***
* @method is[Day]()
* @returns Boolean
* @short Returns true if the date falls on the specified day.
*
* @set
* isToday
* isYesterday
* isTomorrow
* isWeekday
* isWeekend
* isSunday
* isMonday
* isTuesday
* isWednesday
* isThursday
* isFriday
* isSaturday
*
* @example
*
* tomorrow.isToday() -> false
* thursday.isTomorrow() -> ?
* yesterday.isWednesday() -> ?
* today.isWeekend() -> ?
*
***
* @method isFuture()
* @returns Boolean
* @short Returns true if the date is in the future.
*
* @example
*
* lastWeek.isFuture() -> false
* nextWeek.isFuture() -> true
*
***
* @method isPast()
* @returns Boolean
* @short Returns true if the date is in the past.
*
* @example
*
* lastWeek.isPast() -> true
* nextWeek.isPast() -> false
*
***/
function buildRelativeAliases() {
var special = spaceSplit('Today Yesterday Tomorrow Weekday Weekend Future Past');
var weekdays = English.weekdays.slice(0, 7);
var months = English.months.slice(0, 12);
var together = special.concat(weekdays).concat(months);
defineInstanceSimilar(sugarDate, together, function(methods, name) {
methods['is'+ name] = function(d) {
return fullCompareDate(d, name);
};
});
}
defineStatic(sugarDate, {
/***
* @method create(<d>, [options])
* @returns Date
* @static
* @short Alternate date constructor which accepts text formats, a timestamp,
* objects, or another date.
* @extra If no argument is given, the date is assumed to be now. The second
* argument can either be an options object or a locale code as a
* shortcut. For more, see `date parsing`.
*
* @options
*
* locale A locale code to parse the date in. This can also be passed as
* the second argument to this method. Default is the current
* locale, which is `en` if none is set.
*
* past If `true`, ambiguous dates like `Sunday` will be parsed as
* `last Sunday`. Note that non-ambiguous dates are not
* guaranteed to be in the past.
* Default is `false`.
*
* future If `true`, ambiguous dates like `Sunday` will be parsed as
* `next Sunday`. Note that non-ambiguous dates are not
* guaranteed to be in the future.
* Default is `false`.
*
* fromUTC If `true`, dates with no timezone notation will be parsed as
* UTC (no timezone offset). This is useful for server
* timestamps, etc. Note that this flag is not required if the
* timezone is specified in the string, either as an explicit
* value (ex. +0900 or -09:00) or "Z", which is UTC time.
*
* setUTC If `true`, this will set a flag on the date that tells Sugar
* to internally use UTC methods like `getUTCHours` when handling
* it. This flag is the same as calling the `setUTC` method on
* the date after parsing is complete. Note that this is
* different from `fromUTC`, which parses a string as UTC, but
* does not set this flag.
*
* params An optional object that is populated with properties that are
* parsed from string input. This option is useful when parsed
* properties need to be retained.
*
* clone Clones <d> if it is a date.
*
* @example
*
* Date.create('July') -> July of this year
* Date.create('1776') -> 1776
* Date.create('today') -> today
* Date.create('Wednesday') -> This wednesday
* Date.create('next Friday') -> Next friday
* Date.create('July 4, 1776') -> July 4, 1776
* Date.create(-446806800000) -> November 5, 1955
* Date.create('1776年07月04日', 'ja') -> July 4, 1776
* Date.create('August', {past: true}) -> August of this or last year
* Date.create('August', {future: true}) -> August of this or next year
* Date.create('Thursday', {fromUTC: true}) -> Thursday at 12:00am UTC time
*
***/
'create': function(d, options) {
return createDate(d, options);
},
/***
* @method getLocale([code] = current)
* @returns Locale
* @static
* @short Gets the locale object for the given code, or the current locale.
* @extra The locale object has various properties that dictate how dates are
* parsed and formatted for that locale. The locale object is exposed
* here mostly for introspection - it should be uncommon to need to
* maniplate the object itself. For more, see `date locales`.
*
* @example
*
* Date.getLocale() -> Returns the current locale
* Date.getLocale('en') -> Returns the EN locale
*
***/
'getLocale': function(code) {
return localeManager.get(code, !code);
},
/***
* @method getAllLocales()
* @returns Object
* @static
* @short Returns all available locales as an object.
* @extra For more, see `date locales`.
* @example
*
* Date.getAllLocales()
*
***/
'getAllLocales': function() {
return localeManager.getAll();
},
/***
* @method getAllLocaleCodes()
* @returns Array
* @static
* @short Returns all available locale codes as an array of strings.
* @extra For more, see `date locales`.
* @example
*
* Date.getAllLocaleCodes()
*
***/
'getAllLocaleCodes': function() {
return getKeys(localeManager.getAll());
},
/***
* @method setLocale(<code>)
* @returns Locale
* @static
* @short Sets the current locale to be used with dates.
* @extra Sugar has native support for 17 major locales. In addition, you can
* define a new locale with `addLocale`. For more, see `date locales`.
* @example
*
* Date.setLocale('en')
*
***/
'setLocale': function(code) {
return localeManager.set(code);
},
/***
* @method addLocale(<code>, <def>)
* @returns Locale
* @static
* @short Adds a locale definition to the locales understood by Sugar.
* @extra This method should only be required for adding locale definitions
* that don't already exist. For more, see `date locales`.
* @example
*
* Date.addLocale('eo', {})
*
***/
'addLocale': function(code, set) {
return localeManager.add(code, set);
},
/***
* @method removeLocale(<code>)
* @returns Locale
* @static
* @short Deletes the the locale by <code> from Sugar's known locales.
* @extra For more, see `date locales`.
* @example
*
* Date.removeLocale('foo')
*
***/
'removeLocale': function(code) {
return localeManager.remove(code);
}
});
defineInstanceWithArguments(sugarDate, {
/***
* @method set(<set>, [reset] = false)
* @returns Date
* @short Sets the date object.
* @extra This method accepts multiple formats including a single number as
* a timestamp, an object, or enumerated arguments. If [reset] is
* `true`, any units more specific than those passed will be reset.
*
* @example
*
* new Date().set({year:2011,month:11,day:31}) -> December 31, 2011
* new Date().set(2011, 11, 31) -> December 31, 2011
* new Date().set(86400000) -> 1 day after Jan 1, 1970
* new Date().set({year:2004,month:6}, true) -> June 1, 2004, 00:00:00.000
*
***/
'set': function(d, args) {
args = collectDateArguments(args);
return updateDate(d, args[0], args[1]);
},
/***
* @method advance(<set>, [reset] = false)
* @returns Date
* @short Shifts the date forward.
* @extra <set> accepts multiple formats including an object, a string in the
* format "3 days", a single number as milliseconds, or enumerated
* parameters (as with the Date constructor). If [reset] is `true`, any
* units more specific than those passed will be reset. This method
* modifies the date!
*
* @example
*
* new Date().advance({ year: 2 }) -> 2 years in the future
* new Date().advance('2 hours') -> 2 hours in the future
* new Date().advance(0, 2, 3) -> 2 months 3 days in the future
* new Date().advance(86400000) -> 1 day in the future
*
***/
'advance': function(d, args) {
return advanceDateWithArgs(d, args, 1);
},
/***
* @method rewind(<set>, [reset] = false)
* @returns Date
* @short Shifts the date backward.
* @extra [set] accepts multiple formats including an object, a string in the
* format "3 days", a single number as milliseconds, or enumerated
* parameters (as with the Date constructor). If [reset] is `true`, any
* units more specific than those passed will be reset. This method
* modifies the date!
*
* @example
*
* new Date().rewind({ year: 2 }) -> 2 years in the past
* new Date().rewind('2 weeks') -> 2 weeks in the past
* new Date().rewind(0, 2, 3) -> 2 months 3 days in the past
* new Date().rewind(86400000) -> 1 day in the past
*
***/
'rewind': function(d, args) {
return advanceDateWithArgs(d, args, -1);
}
});
defineInstance(sugarDate, {
/***
* @method get(<d>, [options])
* @returns Date
* @short Gets a new date using the current one as a starting point.
* @extra This method is identical to `Date.create`, except that relative
* formats like `next month` are relative to the date instance rather
* than the current date. Accepts a locale code as a string in place
* of [options]. See `create` for more.
*
* @example
*
* nextYear.get('monday') -> monday of the week exactly 1 year from now
* millenium.get('2 years before') -> 2 years before Jan 1, 2000.
*
***/
'get': function(date, d, options) {
return createDateWithContext(date, d, options);
},
/***
* @method setWeekday(<dow>)
* @short Sets the weekday of the date, starting with Sunday at `0`.
* @extra This method modifies the date!
*
* @example
*
* d = new Date(); d.setWeekday(1); d; -> Monday of this week
* d = new Date(); d.setWeekday(6); d; -> Saturday of this week
*
***/
'setWeekday': function(date, dow) {
return setWeekday(date, dow);
},
/***
* @method setISOWeek(<num>)
* @short Sets the week (of the year) as defined by the ISO8601 standard.
* @extra Note that this standard places Sunday at the end of the week (day 7).
* This method modifies the date!
*
* @example
*
* d = new Date(); d.setISOWeek(15); d; -> 15th week of the year
*
***/
'setISOWeek': function(date, num) {
return setISOWeekNumber(date, num);
},
/***
* @method getISOWeek()
* @returns Number
* @short Gets the date's week (of the year) as defined by the ISO8601 standard.
* @extra Note that this standard places Sunday at the end of the week (day 7).
* If `utc` is set on the date, the week will be according to UTC time.
*
* @example
*
* new Date().getISOWeek() -> today's week of the year
*
***/
'getISOWeek': function(date) {
return getWeekNumber(date, true);
},
/***
* @method beginningOfISOWeek()
* @returns Date
* @short Set the date to the beginning of week as defined by ISO8601.
* @extra Note that this standard places Monday at the start of the week.
* This method modifies the date!
*
* @example
*
* new Date().beginningOfISOWeek() -> Monday
*
***/
'beginningOfISOWeek': function(date) {
var day = getWeekday(date);
if (day === 0) {
day = -6;
} else if (day !== 1) {
day = 1;
}
setWeekday(date, day);
return resetTime(date);
},
/***
* @method endOfISOWeek()
* @returns Date
* @short Set the date to the end of week as defined by this ISO8601 standard.
* @extra Note that this standard places Sunday at the end of the week.
* This method modifies the date!
*
* @example
*
* new Date().endOfISOWeek() -> Sunday
*
***/
'endOfISOWeek': function(date) {
if (getWeekday(date) !== 0) {
setWeekday(date, 7);
}
return moveToEndOfUnit(date, DAY_INDEX);
},
/***
* @method getUTCOffset([iso])
* @returns String
* @short Returns a string representation of the offset from UTC time. If [iso]
* is true the offset will be in ISO8601 format.
*
* @example
*
* new Date().getUTCOffset() -> "+0900"
* new Date().getUTCOffset(true) -> "+09:00"
*
***/
'getUTCOffset': function(date, iso) {
return getUTCOffset(date, iso);
},
/***
* @method setUTC([on] = false)
* @returns Date
* @short Controls a flag on the date that tells Sugar to internally use UTC
* methods like `getUTCHours`.
* @extra This flag is most commonly used for output in UTC time with the
* `format` method. Note that this flag only governs which methods are
* called internally date native methods like `setHours` will still
* return local non-UTC values. This method will modify the date!
*
* @example
*
* new Date().setUTC(true).long() -> formatted with UTC methods
* new Date().setUTC(false).long() -> formatted without UTC methods
*
***/
'setUTC': function(date, on) {
return _utc(date, on);
},
/***
* @method isUTC()
* @returns Boolean
* @short Returns true if the date has no timezone offset.
* @extra This will also return true for dates whose internal utc flag is set
* with `setUTC`. Even if the utc flag is set, `getTimezoneOffset`
* will always report the same thing as Javascript always reports that
* based on the environment's locale.
*
* @example
*
* new Date().isUTC() -> true or false (depends on the local offset)
* new Date().setUTC(true).isUTC() -> true
*
***/
'isUTC': function(date) {
return isUTC(date);
},
/***
* @method isValid()
* @returns Boolean
* @short Returns true if the date is valid.
*
* @example
*
* new Date().isValid() -> true
* new Date('flexor').isValid() -> false
*
***/
'isValid': function(date) {
return dateIsValid(date);
},
/***
* @method isAfter(<d>, [margin] = 0)
* @returns Boolean
* @short Returns true if the date is after <d>.
* @extra [margin] is to allow extra margin of error in ms. <d> will accept
* a date object, timestamp, or string. If not specified, <d> is
* assumed to be now. See `create` for formats.
*
* @example
*
* today.isAfter('tomorrow') -> false
* today.isAfter('yesterday') -> true
*
***/
'isAfter': function(date, d, margin) {
return date.getTime() > createDate(d).getTime() - (margin || 0);
},
/***
* @method isBefore(<d>, [margin] = 0)
* @returns Boolean
* @short Returns true if the date is before <d>.
* @extra [margin] is to allow extra margin of error in ms. <d> will accept
* a date object, timestamp, or text format. If not specified, <d> is
* assumed to be now. See `create` for formats.
*
* @example
*
* today.isBefore('tomorrow') -> true
* today.isBefore('yesterday') -> false
*
***/
'isBefore': function(date, d, margin) {
return date.getTime() < createDate(d).getTime() + (margin || 0);
},
/***
* @method isBetween(<d1>, <d2>, [margin] = 0)
* @returns Boolean
* @short Returns true if the date is later or equal to <d1> and before or
* equal to <d2>.
* @extra [margin] is to allow extra margin of error in ms. <d1> and <d2> will
* accept a date object, timestamp, or text format. If not specified,
* they are assumed to be now. See `create` for formats.
*
* @example
*
* new Date().isBetween('yesterday', 'tomorrow') -> true
* new Date().isBetween('last year', '2 years ago') -> false
*
***/
'isBetween': function(date, d1, d2, margin) {
var t = date.getTime();
var t1 = createDate(d1).getTime();
var t2 = createDate(d2).getTime();
var lo = min(t1, t2);
var hi = max(t1, t2);
margin = margin || 0;
return (lo - margin <= t) && (hi + margin >= t);
},
/***
* @method isLeapYear()
* @returns Boolean
* @short Returns true if the date is a leap year.
*
* @example
*
* millenium.isLeapYear() -> true
*
***/
'isLeapYear': function(date) {
var year = getYear(date);
return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
},
/***
* @method daysInMonth()
* @returns Number
* @short Returns the number of days in the date's month.
*
* @example
*
* may.daysInMonth() -> 31
* feb.daysInMonth() -> 28 or 29
*
***/
'daysInMonth': function(date) {
return getDaysInMonth(date);
},
/***
* @method format([f], [locale] = currentLocale)
* @returns String
* @short Returns the date as a string using the format <f>.
* @extra <f> is a string that contains tokens in either LDML format using
* curly braces, or "strftime" format using a percent sign. If <f> is
* not specified, the locale specific `{long}` format is used. [locale]
* is a locale code to use (if not specified the current locale is
* used). For more, see `date formatting`.
*
* @example
*
* new Date().format() -> ex. February 13, 2012 11:21 AM
* new Date().format('{Weekday} {d} {Month}') -> ex. Monday July 4
* new Date().format('{hh}:{mm}') -> ex. 15:57
* new Date().format('%H:%M') -> ex. 15:57
* new Date().format('{12hr}:{mm}{tt}') -> ex. 3:57pm
* new Date().format('ISO8601') -> ex. 2011-07-05 12:24:55.528Z
* new Date().format('{Weekday}', 'ja') -> ex. 先週
*
***
* @method short([locale] = currentLocale)
* @returns String
* @short Outputs the date in the short format for the current locale.
* @extra [locale] overrides the current locale if passed.
*
* @example
*
* new Date().short() -> ex. 02/13/2016
* new Date().short('fi') -> ex. 13.2.2016
*
***
* @method medium([locale] = currentLocale)
* @returns String
* @short Outputs the date in the medium format for the current locale.
* @extra [locale] overrides the current locale if passed.
*
* @example
*
* new Date().medium() -> ex. February 13, 2016
* new Date().medium('ja') -> ex. 2016年2月13日
*
***
* @method long([locale] = currentLocale)
* @returns String
* @short Outputs the date in the long format for the current locale.
* @extra [locale] overrides the current locale if passed.
*
* @example
*
* new Date().long() -> ex. February 13, 2016 6:22 PM
* new Date().long('es') -> ex. 13 de febrero de 2016 18:22
*
***
* @method full([locale] = currentLocale)
* @returns String
* @short Outputs the date in the full format for the current locale.
* @extra [locale] overrides the current locale if passed.
*
* @example
*
* new Date().full() -> ex. Saturday, February 13, 2016 6:23 PM
* new Date().full('ru') -> ex. суббота, 13 февраля 2016 г., 18:23
*
***/
'format': function(date, f, localeCode) {
return dateFormat(date, f, localeCode);
},
/***
* @method relative([locale] = currentLocale, [fn])
* @returns String
* @short Returns the date in a text format relative to the current time,
* such as "5 minutes ago".
* @extra [fn] is a function that can be passed to provide more granular
* control over the resulting string. Its return value will be passed
* to `format`. If nothing is returned, the relative format will be
* used. [fn] may be passed as the first argument in place of [locale].
* For more about formats, see `date formatting`.
*
* @callback fn
*
* num The offset number in `unit`.
* unit A numeric representation of the unit that `num` is in, starting at
* 0 for ms.
* ms The absolute offset in milliseconds.
* loc The locale object, either specified by [locale] or default.
*
* @example
*
* hourAgo.relative() -> 1 hour ago
* jan.relative() -> ex. 5 months ago
* jan.relative('ja') -> 3ヶ月前
* jan.relative(function(num, unit, ms, loc) {
* // Return an absolute date for anything over 6 months.
* if(unit == 6 && num > 6 || unit > 6) {
* return '{Month} {d}, {yyyy}';
* }
* }); -> ex. 5 months ago
*
***/
'relative': function(date, localeCode, fn) {
return dateRelative(date, null, localeCode, fn);
},
/***
* @method relativeTo(<d>, [locale] = currentLocale)
* @returns String
* @short Returns the date in a text format relative to <d>, such as
* "5 minutes".
* @extra <d> will accept a date object, timestamp, or string. [localeCode]
* applies to the method output, not <d>.
*
* @example
*
* jan.relativeTo(jul) -> 5 months
* yesterday.relativeTo('today', 'ja') -> 一日
*
***/
'relativeTo': function(date, d, localeCode) {
return dateRelative(date, createDate(d), localeCode);
},
/***
* @method is(<f>, [margin] = 0)
* @returns Boolean
* @short Returns true if the date matches <f>.
* @extra <f> will accept a date object, timestamp, or text format. In the
* case of objects and text formats, `is` will additionally compare
* based on the precision implied in the input. In the case of text
* formats <f> will use the currently set locale. [margin] allows an
* extra margin of error in milliseconds. See `create` for formats.
*
* @example
*
* new Date().is('July') -> true or false?
* new Date().is('1776') -> false
* new Date().is('today') -> true
* new Date().is('weekday') -> true or false?
* new Date().is('July 4, 1776') -> false
* new Date().is(-6106093200000) -> false
* new Date().is(new Date(1776, 6, 4)) -> false
*
***/
'is': function(date, d, margin) {
return fullCompareDate(date, d, margin);
},
/***
* @method reset([unit] = 'day', [localeCode])
* @returns Date
* @short Resets the date to the beginning of [unit].
* @extra This method effectively resets all smaller units, pushing the date
* to the beginning of [unit]. Default is `day`, which effectively
* resets the time. [localeCode] is provided for resetting weeks, which
* is locale dependent. This method modifies the date!
*
* @example
*
* new Date().reset('day') -> Beginning of the day
* new Date().reset('month') -> Beginning of the month
*
***/
'reset': function(date, unit, localeCode) {
var unitIndex = unit ? getUnitIndexForParamName(unit) : DAY_INDEX;
moveToBeginningOfUnit(date, unitIndex, localeCode);
return date;
},
/***
* @method clone()
* @returns Date
* @short Clones the date.
* @extra Note that the UTC flag will be preserved if set. This flag is
* set via the `setUTC` method or an option on `Date.create`.
*
* @example
*
* new Date().clone() -> Copy of now
*
***/
'clone': function(date) {
return cloneDate(date);
},
/***
* @method iso()
* @alias toISOString
*
***/
'iso': function(date) {
return date.toISOString();
},
/***
* @method getWeekday()
* @returns Number
* @short Alias for `getDay`.
*
* @example
*
* new Date().getWeekday(); -> (ex.) 3
*
***/
'getWeekday': function(date) {
return getWeekday(date);
},
/***
* @method getUTCWeekday()
* @returns Number
* @short Alias for `getUTCDay`.
*
* @example
*
* new Date().getUTCWeekday(); -> (ex.) 3
*
***/
'getUTCWeekday': function(date) {
return date.getUTCDay();
}
});
/*** @namespace Number ***/
/***
* @method [dateUnit]()
* @returns Number
* @short Takes the number as a unit of time and converts to milliseconds.
* @extra Method names can be singular or 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.
*
* @set
* millisecond
* milliseconds
* second
* seconds
* minute
* minutes
* hour
* hours
* day
* days
* week
* weeks
* month
* months
* year
* years
*
* @example
*
* (5).milliseconds() -> 5
* (10).hours() -> 36000000
* (1).day() -> 86400000
*
***
* @method [dateUnit]Before([d], [options])
* @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 (i.e. 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. [options] can be an object
* or a locale code as a string. See `create` for more.
*
*
* @set
* millisecondBefore
* millisecondsBefore
* secondBefore
* secondsBefore
* minuteBefore
* minutesBefore
* hourBefore
* hoursBefore
* dayBefore
* daysBefore
* weekBefore
* weeksBefore
* monthBefore
* monthsBefore
* yearBefore
* yearsBefore
*
* @example
*
* (5).daysBefore('tuesday') -> 5 days before tuesday of this week
* (1).yearBefore('January 23, 1997') -> January 23, 1996
*
***
* @method [dateUnit]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 (i.e. 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.
*
* @set
* millisecondAgo
* millisecondsAgo
* secondAgo
* secondsAgo
* minuteAgo
* minutesAgo
* hourAgo
* hoursAgo
* dayAgo
* daysAgo
* weekAgo
* weeksAgo
* monthAgo
* monthsAgo
* yearAgo
* yearsAgo
*
* @example
*
* (5).weeksAgo() -> 5 weeks ago
* (1).yearAgo() -> January 23, 1996
*
***
* @method [dateUnit]After([d], [options])
* @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 (i.e. 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. [options] can be an object
* or a locale code as a string. See `create` for more.
*
* @set
* millisecondAfter
* millisecondsAfter
* secondAfter
* secondsAfter
* minuteAfter
* minutesAfter
* hourAfter
* hoursAfter
* dayAfter
* daysAfter
* weekAfter
* weeksAfter
* monthAfter
* monthsAfter
* yearAfter
* yearsAfter
*
* @example
*
* (5).daysAfter('tuesday') -> 5 days after tuesday of this week
* (1).yearAfter('January 23, 1997') -> January 23, 1998
*
***
* @method [dateUnit]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 (i.e. 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.
*
* @set
* millisecondFromNow
* millisecondsFromNow
* secondFromNow
* secondsFromNow
* minuteFromNow
* minutesFromNow
* hourFromNow
* hoursFromNow
* dayFromNow
* daysFromNow
* weekFromNow
* weeksFromNow
* monthFromNow
* monthsFromNow
* yearFromNow
* yearsFromNow
*
* @example
*
* (5).weeksFromNow() -> 5 weeks ago
* (1).yearFromNow() -> January 23, 1998
*
***/
function buildNumberUnitMethods() {
defineInstanceSimilar(sugarNumber, DateUnits, function(methods, unit) {
var name = unit.name, base, after, before;
base = function(n) {
return round(n * unit.multiplier);
};
after = function(n, d, options) {
return advanceDate(createDate(d, options, true), name, n);
};
before = function(n, d, options) {
return advanceDate(createDate(d, options, true), name, -n);
};
methods[name] = base;
methods[name + 's'] = base;
methods[name + 'Before'] = before;
methods[name + 'sBefore'] = before;
methods[name + 'Ago'] = before;
methods[name + 'sAgo'] = before;
methods[name + 'After'] = after;
methods[name + 'sAfter'] = after;
methods[name + 'FromNow'] = after;
methods[name + 'sFromNow'] = after;
});
}
defineInstance(sugarNumber, {
/***
* @method duration([locale] = currentLocale)
* @returns String
* @short Takes the number as milliseconds and returns a localized string.
* @extra This method is the same as `Date#relative` without the localized
* equivalent of "from now" or "ago". [locale] can be passed as the
* first (and only) parameter. Note that this method is only available
* when the dates module is included.
*
* @example
*
* (500).duration() -> '500 milliseconds'
* (1200).duration() -> '1 second'
* (75).minutes().duration() -> '1 hour'
* (75).minutes().duration('es') -> '1 hora'
*
***/
'duration': function(n, localeCode) {
return localeManager.get(localeCode).getDuration(n);
}
});
var EnglishLocaleBaseDefinition = {
'code': 'en',
'plural': true,
'timeMarkers': 'at',
'ampm': 'AM|A.M.|a,PM|P.M.|p',
'units': 'millisecond:|s,second:|s,minute:|s,hour:|s,day:|s,week:|s,month:|s,year:|s',
'months': 'Jan:uary|,Feb:ruary|,Mar:ch|,Apr:il|,May,Jun:e|,Jul:y|,Aug:ust|,Sep:tember|t|,Oct:ober|,Nov:ember|,Dec:ember|',
'weekdays': 'Sun:day|,Mon:day|,Tue:sday|,Wed:nesday|,Thu:rsday|,Fri:day|,Sat:urday|+weekend',
'numerals': 'zero,one|first,two|second,three|third,four:|th,five|fifth,six:|th,seven:|th,eight:|h,nin:e|th,ten:|th',
'articles': 'a,an,the',
'tokens': 'the,st|nd|rd|th,of|in,a|an,on',
'time': '{H}:{mm}',
'past': '{num} {unit} {sign}',
'future': '{num} {unit} {sign}',
'duration': '{num} {unit}',
'modifiers': [
{ 'name': 'half', 'src': 'half', 'value': .5 },
{ 'name': 'midday', 'src': 'noon', 'value': 12 },
{ 'name': 'midday', 'src': 'midnight', 'value': 24 },
{ 'name': 'day', 'src': 'yesterday', 'value': -1 },
{ 'name': 'day', 'src': 'today|tonight', 'value': 0 },
{ 'name': 'day', 'src': 'tomorrow', 'value': 1 },
{ 'name': 'sign', 'src': 'ago|before', 'value': -1 },
{ 'name': 'sign', 'src': 'from now|after|from|in|later', 'value': 1 },
{ 'name': 'edge', 'src': 'first day|first|beginning', 'value': -2 },
{ 'name': 'edge', 'src': 'last day', 'value': 1 },
{ 'name': 'edge', 'src': 'end|last', 'value': 2 },
{ 'name': 'shift', 'src': 'last', 'value': -1 },
{ 'name': 'shift', 'src': 'the|this', 'value': 0 },
{ 'name': 'shift', 'src': 'next', 'value': 1 }
],
'parse': [
'(?:just)? now',
'{shift} {unit:5-7}',
"{months?} (?:{year}|'{yy})",
'{midday} {4?} {day|weekday}',
'{months},?(?:[-.\\/\\s]{year})?',
'{edge} of (?:day)? {day|weekday}',
'{0} {num}{1?} {weekday} {2} {months},? {year?}',
'{shift?} {day?} {weekday?} {timeMarker?} {midday}',
'{sign?} {3?} {half} {3?} {unit:3-4|unit:7} {sign?}',
'{0?} {edge} {weekday?} {2} {shift?} {unit:4-7?} {months?},? {year?}'
],
'timeParse': [
'{day|weekday}',
'{shift} {unit:5?} {weekday}',
'{0?} {date}{1?} {2?} {months?}',
'{weekday} {2?} {shift} {unit:5}',
'{0?} {num} {2?} {months}\\.?,? {year?}',
'{num?} {unit:4-5} {sign} {day|weekday}',
'{year}[-.\\/\\s]{months}[-.\\/\\s]{date}',
'{0|months} {date?}{1?} of {shift} {unit:6-7}',
'{0?} {num}{1?} {weekday} of {shift} {unit:6}',
"{date}[-.\\/\\s]{months}[-.\\/\\s](?:{year}|'?{yy})",
"{weekday?}\\.?,? {months}\\.?,? {date}{1?},? (?:{year}|'{yy})?"
],
'timeFrontParse': [
'{sign} {num} {unit}',
'{num} {unit} {sign}',
'{4?} {day|weekday}'
]
};
var AmericanEnglishDefinition = getEnglishVariant({
'mdy': true,
'firstDayOfWeek': 0,
'firstDayOfWeekYear': 1,
'short': '{MM}/{dd}/{yyyy}',
'medium': '{Month} {d}, {yyyy}',
'long': '{Month} {d}, {yyyy} {time}',
'full': '{Weekday}, {Month} {d}, {yyyy} {time}',
'stamp': '{Dow} {Mon} {d} {yyyy} {time}',
'time': '{h}:{mm} {TT}'
});
var BritishEnglishDefinition = getEnglishVariant({
'short': '{dd}/{MM}/{yyyy}',
'medium': '{d} {Month} {yyyy}',
'long': '{d} {Month} {yyyy} {H}:{mm}',
'full': '{Weekday}, {d} {Month}, {yyyy} {time}',
'stamp': '{Dow} {d} {Mon} {yyyy} {time}'
});
var CanadianEnglishDefinition = getEnglishVariant({
'short': '{yyyy}-{MM}-{dd}',
'medium': '{d} {Month}, {yyyy}',
'long': '{d} {Month}, {yyyy} {H}:{mm}',
'full': '{Weekday}, {d} {Month}, {yyyy} {time}',
'stamp': '{Dow} {d} {Mon} {yyyy} {time}'
});
var LazyLoadedLocales = {
'en-US': AmericanEnglishDefinition,
'en-GB': BritishEnglishDefinition,
'en-AU': BritishEnglishDefinition,
'en-CA': CanadianEnglishDefinition
};
buildLocales();
buildDateFormatTokens();
buildDateFormatMatcher();
buildDateUnitMethods();
buildNumberUnitMethods();
buildRelativeAliases();
setDateChainableConstructor();
/***
* @module String
* @description String manupulation, encoding, truncation, and formatting, and more.
*
***/
// Flag allowing native string methods to be enhanced
var STRING_ENHANCEMENTS_FLAG = 'enhanceString';
// Matches non-punctuation characters except apostrophe for capitalization.
var CAPITALIZE_REG = /[^\u0000-\u0040\u005B-\u0060\u007B-\u007F]+('s)?/g;
// Regex matching camelCase.
var CAMELIZE_REG = /(^|_)([^_]+)/g;
// Regex matching any HTML entity.
var HTML_ENTITY_REG = /&#?(x)?([\w\d]{0,5});/gi;
// Very basic HTML escaping regex.
var HTML_ESCAPE_REG = /[&<>]/g;
// Special HTML entities.
var HTMLFromEntityMap = {
'lt': '<',
'gt': '>',
'amp': '&',
'nbsp': ' ',
'quot': '"',
'apos': "'"
};
var HTMLToEntityMap;
// Words that should not be capitalized in titles
var DOWNCASED_WORDS = [
'and', 'or', 'nor', 'a', 'an', 'the', 'so', 'but', 'to', 'of', 'at',
'by', 'from', 'into', 'on', 'onto', 'off', 'out', 'in', 'over',
'with', 'for'
];
// HTML tags that do not have inner content.
var HTML_VOID_ELEMENTS = [
'area','base','br','col','command','embed','hr','img',
'input','keygen','link','meta','param','source','track','wbr'
];
var LEFT_TRIM_REG = RegExp('^['+ TRIM_CHARS +']+');
var RIGHT_TRIM_REG = RegExp('['+ TRIM_CHARS +']+$');
var TRUNC_REG = RegExp('(?=[' + TRIM_CHARS + '])');
// Reference to native String#includes to enhance later.
var nativeIncludes = String.prototype.includes;
// Base64
var encodeBase64, decodeBase64;
// Format matcher for String#format.
var stringFormatMatcher = createFormatMatcher(deepGetProperty);
function padString(num, padding) {
return repeatString(isDefined(padding) ? padding : ' ', num);
}
function truncateString(str, length, from, ellipsis, split) {
var str1, str2, len1, len2;
if (str.length <= length) {
return str.toString();
}
ellipsis = isUndefined(ellipsis) ? '...' : ellipsis;
switch(from) {
case 'left':
str2 = split ? truncateOnWord(str, length, true) : str.slice(str.length - length);
return ellipsis + str2;
case 'middle':
len1 = ceil(length / 2);
len2 = floor(length / 2);
str1 = split ? truncateOnWord(str, len1) : str.slice(0, len1);
str2 = split ? truncateOnWord(str, len2, true) : str.slice(str.length - len2);
return str1 + ellipsis + str2;
default:
str1 = split ? truncateOnWord(str, length) : str.slice(0, length);
return str1 + ellipsis;
}
}
function stringEach(str, search, fn) {
var chunks, chunk, reg, result = [];
if (isFunction(search)) {
fn = search;
reg = /[\s\S]/g;
} else if (!search) {
reg = /[\s\S]/g;
} else if (isString(search)) {
reg = RegExp(escapeRegExp(search), 'gi');
} else if (isRegExp(search)) {
reg = RegExp(search.source, getRegExpFlags(search, 'g'));
}
// Getting the entire array of chunks up front as we need to
// pass this into the callback function as an argument.
chunks = runGlobalMatch(str, reg);
if (chunks) {
for(var i = 0, len = chunks.length, r; i < len; i++) {
chunk = chunks[i];
result[i] = chunk;
if (fn) {
r = fn.call(str, chunk, i, chunks);
if (r === false) {
break;
} else if (isDefined(r)) {
result[i] = r;
}
}
}
}
return result;
}
// "match" in < IE9 has enumable properties that will confuse for..in
// loops, so ensure that the match is a normal array by manually running
// "exec". Note that this method is also slightly more performant.
function runGlobalMatch(str, reg) {
var result = [], match, lastLastIndex;
while ((match = reg.exec(str)) != null) {
if (reg.lastIndex === lastLastIndex) {
reg.lastIndex += 1;
} else {
result.push(match[0]);
}
lastLastIndex = reg.lastIndex;
}
return result;
}
function eachWord(str, fn) {
return stringEach(trim(str), /\S+/g, fn);
}
function stringCodes(str, fn) {
var codes = new Array(str.length), i, len;
for(i = 0, len = str.length; i < len; i++) {
var code = str.charCodeAt(i);
codes[i] = code;
if (fn) {
fn.call(str, code, i, str);
}
}
return codes;
}
function stringUnderscore(str) {
var areg = Inflections.acronyms && Inflections.acronyms.reg;
return str
.replace(/[-\s]+/g, '_')
.replace(areg, function(acronym, index) {
return (index > 0 ? '_' : '') + acronym.toLowerCase();
})
.replace(/([A-Z\d]+)([A-Z][a-z])/g,'$1_$2')
.replace(/([a-z\d])([A-Z])/g,'$1_$2')
.toLowerCase();
}
function stringCamelize(str, upper) {
str = stringUnderscore(str);
return str.replace(CAMELIZE_REG, function(match, pre, word, index) {
var cap = upper !== false || index > 0, acronym;
acronym = getAcronym(word);
if (acronym && cap) {
return acronym;
}
return cap ? stringCapitalize(word, true) : word;
});
}
function stringSpacify(str) {
return stringUnderscore(str).replace(/_/g, ' ');
}
function stringCapitalize(str, downcase, all) {
if (downcase) {
str = str.toLowerCase();
}
return all ? str.replace(CAPITALIZE_REG, simpleCapitalize) : simpleCapitalize(str);
}
function stringTitleize(str) {
var fullStopPunctuation = /[.:;!]$/, lastHadPunctuation;
str = runHumanRules(str);
str = stringSpacify(str);
return eachWord(str, function(word, index, words) {
word = getHumanWord(word) || word;
word = getAcronym(word) || word;
var hasPunctuation, isFirstOrLast;
var first = index == 0, last = index == words.length - 1;
hasPunctuation = fullStopPunctuation.test(word);
isFirstOrLast = first || last || hasPunctuation || lastHadPunctuation;
lastHadPunctuation = hasPunctuation;
if (isFirstOrLast || indexOf(DOWNCASED_WORDS, word) === -1) {
return stringCapitalize(word, false, true);
} else {
return word;
}
}).join(' ');
}
function stringParameterize(str, separator) {
if (separator === undefined) separator = '-';
str = str.replace(/[^a-z0-9\-_]+/gi, separator);
if (separator) {
var reg = RegExp('^{s}+|{s}+$|({s}){s}+'.split('{s}').join(escapeRegExp(separator)), 'g');
str = str.replace(reg, '$1');
}
return encodeURI(str.toLowerCase());
}
function reverseString(str) {
return str.split('').reverse().join('');
}
function truncateOnWord(str, limit, fromLeft) {
if (fromLeft) {
return reverseString(truncateOnWord(reverseString(str), limit));
}
var words = str.split(TRUNC_REG);
var count = 0;
return filter(words, function(word) {
count += word.length;
return count <= limit;
}).join('');
}
function unescapeHTML(str) {
return str.replace(HTML_ENTITY_REG, function(full, hex, code) {
var special = HTMLFromEntityMap[code];
return special || chr(hex ? parseInt(code, 16) : +code);
});
}
function tagIsVoid(tag) {
return indexOf(HTML_VOID_ELEMENTS, tag.toLowerCase()) !== -1;
}
function stringReplaceAll(str, f, replace) {
var i = 0, tokens;
if (isString(f)) {
f = RegExp(escapeRegExp(f), 'g');
} else if (f && !f.global) {
f = RegExp(f.source, getRegExpFlags(f, 'g'));
}
if (!replace) {
replace = '';
} else {
tokens = replace;
replace = function() {
var t = tokens[i++];
return t != null ? t : '';
};
}
return str.replace(f, replace);
}
function replaceTags(str, find, replacement, strip) {
var tags = isString(find) ? [find] : find, reg, src;
tags = map(tags || [], function(t) {
return escapeRegExp(t);
}).join('|');
src = tags.replace('all', '') || '[^\\s>]+';
src = '<(\\/)?(' + src + ')(\\s+[^<>]*?)?\\s*(\\/)?>';
reg = RegExp(src, 'gi');
return runTagReplacements(str.toString(), reg, strip, replacement);
}
function runTagReplacements(str, reg, strip, replacement, fullString) {
var match;
var result = '';
var currentIndex = 0;
var openTagName;
var openTagAttributes;
var openTagCount = 0;
function processTag(index, tagName, attributes, tagLength, isVoid) {
var content = str.slice(currentIndex, index), s = '', r = '';
if (isString(replacement)) {
r = replacement;
} else if (replacement) {
r = replacement.call(fullString, tagName, content, attributes, fullString) || '';
}
if (strip) {
s = r;
} else {
content = r;
}
if (content) {
content = runTagReplacements(content, reg, strip, replacement, fullString);
}
result += s + content + (isVoid ? '' : s);
currentIndex = index + (tagLength || 0);
}
fullString = fullString || str;
reg = RegExp(reg.source, 'gi');
while(match = reg.exec(str)) {
var tagName = match[2];
var attributes = (match[3]|| '').slice(1);
var isClosingTag = !!match[1];
var isSelfClosing = !!match[4];
var tagLength = match[0].length;
var isVoid = tagIsVoid(tagName);
var isOpeningTag = !isClosingTag && !isSelfClosing && !isVoid;
var isSameAsCurrent = tagName === openTagName;
if (!openTagName) {
result += str.slice(currentIndex, match.index);
currentIndex = match.index;
}
if (isOpeningTag) {
if (!openTagName) {
openTagName = tagName;
openTagAttributes = attributes;
openTagCount++;
currentIndex += tagLength;
} else if (isSameAsCurrent) {
openTagCount++;
}
} else if (isClosingTag && isSameAsCurrent) {
openTagCount--;
if (openTagCount === 0) {
processTag(match.index, openTagName, openTagAttributes, tagLength, isVoid);
openTagName = null;
openTagAttributes = null;
}
} else if (!openTagName) {
processTag(match.index, tagName, attributes, tagLength, isVoid);
}
}
if (openTagName) {
processTag(str.length, openTagName, openTagAttributes);
}
result += str.slice(currentIndex);
return result;
}
function numberOrIndex(str, n, from) {
if (isString(n)) {
n = str.indexOf(n);
if (n === -1) {
n = from ? str.length : 0;
}
}
return n;
}
function buildBase64() {
var encodeAscii, decodeAscii;
function catchEncodingError(fn) {
return function(str) {
try {
return fn(str);
} catch(e) {
return '';
}
};
}
if (typeof Buffer !== 'undefined') {
encodeBase64 = function(str) {
return new Buffer(str).toString('base64');
};
decodeBase64 = function(str) {
return new Buffer(str, 'base64').toString('utf8');
};
return;
}
if (typeof btoa !== 'undefined') {
encodeAscii = catchEncodingError(btoa);
decodeAscii = catchEncodingError(atob);
} else {
var key = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
var base64reg = /[^A-Za-z0-9\+\/\=]/g;
encodeAscii = function(str) {
var output = '';
var chr1, chr2, chr3;
var enc1, enc2, enc3, enc4;
var i = 0;
do {
chr1 = str.charCodeAt(i++);
chr2 = str.charCodeAt(i++);
chr3 = str.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output += key.charAt(enc1);
output += key.charAt(enc2);
output += key.charAt(enc3);
output += key.charAt(enc4);
chr1 = chr2 = chr3 = '';
enc1 = enc2 = enc3 = enc4 = '';
} while (i < str.length);
return output;
};
decodeAscii = function(input) {
var output = '';
var chr1, chr2, chr3;
var enc1, enc2, enc3, enc4;
var i = 0;
if (input.match(base64reg)) {
return '';
}
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, '');
do {
enc1 = key.indexOf(input.charAt(i++));
enc2 = key.indexOf(input.charAt(i++));
enc3 = key.indexOf(input.charAt(i++));
enc4 = key.indexOf(input.charAt(i++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
output = output + chr(chr1);
if (enc3 != 64) {
output = output + chr(chr2);
}
if (enc4 != 64) {
output = output + chr(chr3);
}
chr1 = chr2 = chr3 = '';
enc1 = enc2 = enc3 = enc4 = '';
} while (i < input.length);
return output;
};
}
encodeBase64 = function(str) {
return encodeAscii(unescape(encodeURIComponent(str)));
};
decodeBase64 = function(str) {
return decodeURIComponent(escape(decodeAscii(str)));
};
}
function buildEntities() {
HTMLToEntityMap = {};
forEachProperty(HTMLFromEntityMap, function(val, key) {
HTMLToEntityMap[val] = '&' + key + ';';
});
}
function callIncludesWithRegexSupport(str, search, position) {
if (!isRegExp(search)) {
return nativeIncludes.call(str, search, position);
}
if (position) {
str = str.slice(position);
}
return search.test(str);
}
defineInstance(sugarString, {
// Enhancment to String#includes to allow a regex.
'includes': fixArgumentLength(callIncludesWithRegexSupport)
}, [ENHANCEMENTS_FLAG, STRING_ENHANCEMENTS_FLAG]);
defineInstance(sugarString, {
/***
* @method at(<index>, [loop] = false)
* @returns Mixed
* @short Gets the character(s) at a given index.
* @extra When [loop] is true, overshooting the end of the string will begin
* counting from the other end. <index> may be negative. If <index> is
* an array, multiple elements will be returned.
* @example
*
* 'jumpy'.at(0) -> 'j'
* 'jumpy'.at(2) -> 'm'
* 'jumpy'.at(5) -> ''
* 'jumpy'.at(5, true) -> 'j'
* 'jumpy'.at(-1) -> 'y'
* 'lucky charms'.at([2, 4]) -> ['u','k']
*
***/
'at': function(str, index, loop) {
return getEntriesForIndexes(str, index, loop, true);
},
/***
* @method escapeURL([param] = false)
* @returns String
* @short Escapes characters in a string to make a valid URL.
* @extra If [param] is true, it will also escape valid URL characters. Use
* this when the entire string is meant for use in a query string.
*
* @example
*
* 'a, b, and c'.escapeURL() -> 'a,%20b,%20and%20c'
* 'http://foo.com/'.escapeURL(true) -> 'http%3A%2F%2Ffoo.com%2F'
*
***/
'escapeURL': function(str, param) {
return param ? encodeURIComponent(str) : encodeURI(str);
},
/***
* @method unescapeURL([partial] = false)
* @returns String
* @short Restores escaped characters in a URL escaped string.
* @extra If [partial] is true, it will only unescape non-valid URL tokens,
* and is included here for completeness, but should be rarely needed.
*
* @example
*
* 'http%3A%2F%2Ffoo.com%2F'.unescapeURL() -> 'http://foo.com/'
* 'http%3A%2F%2Ffoo.com%2F'.unescapeURL(true) -> 'http%3A%2F%2Ffoo.com%2F'
*
***/
'unescapeURL': function(str, param) {
return param ? decodeURI(str) : decodeURIComponent(str);
},
/***
* @method escapeHTML()
* @returns String
* @short Converts HTML characters to their entity equivalents.
*
* @example
*
* '<p>some text</p>'.escapeHTML() -> '&lt;p&gt;some text&lt;/p&gt;'
* 'one & two'.escapeHTML() -> 'one &amp; two'
*
***/
'escapeHTML': function(str) {
return str.replace(HTML_ESCAPE_REG, function(chr) {
return getOwn(HTMLToEntityMap, chr);
});
},
/***
* @method unescapeHTML()
* @returns String
* @short Restores escaped HTML characters.
*
* @example
*
* '&lt;p&gt;some text&lt;/p&gt;'.unescapeHTML() -> '<p>some text</p>'
* 'one &amp; two'.unescapeHTML() -> 'one & two'
*
***/
'unescapeHTML': function(str) {
return unescapeHTML(str);
},
/***
* @method stripTags([tag] = 'all', [replace])
* @returns String
* @short Strips HTML tags from the string.
* @extra [tag] may be an array of tags or 'all', in which case all tags will
* be stripped. [replace] will replace what was stripped, and may be a
* string or a function to handle replacements. If this function returns
* a string, then it will be used for the replacement. If it returns
* `undefined`, the tags will be stripped normally.
*
* @callback replace
*
* tag The tag name.
* inner The tag content.
* attr The attributes on the tag, if any, as a string.
* outer The entire matched tag string.
*
* @example
*
* '<p>just <b>some</b> text</p>'.stripTags() -> 'just some text'
* '<p>just <b>some</b> text</p>'.stripTags('p') -> 'just <b>some</b> text'
* '<p>hi!</p>'.stripTags('p', function(all, content) {
* return '|';
* }); -> '|hi!|'
*
***/
'stripTags': function(str, tag, replace) {
return replaceTags(str, tag, replace, true);
},
/***
* @method removeTags([tag] = 'all', [replace])
* @returns String
* @short Removes HTML tags and their contents from the string.
* @extra [tag] may be an array of tags or 'all', in which case all tags will
* be removed. [replace] will replace what was removed, and may be a
* string or a function to handle replacements. If this function returns
* a string, then it will be used for the replacement. If it returns
* `undefined`, the tags will be removed normally.
*
* @callback replace
*
* tag The tag name.
* inner The tag content.
* attr The attributes on the tag, if any, as a string.
* outer The entire matched tag string.
*
* @example
*
* '<p>just <b>some</b> text</p>'.removeTags() -> ''
* '<p>just <b>some</b> text</p>'.removeTags('b') -> '<p>just text</p>'
* '<p>hi!</p>'.removeTags('p', function(all, content) {
* return 'bye!';
* }); -> 'bye!'
*
***/
'removeTags': function(str, tag, replace) {
return replaceTags(str, tag, replace, false);
},
/***
* @method encodeBase64()
* @returns String
* @short Encodes the string into base64 encoding.
* @extra This method wraps native methods when available, and uses a custom
* implementation when not available. It can also handle Unicode
* string encodings.
*
* @example
*
* 'gonna get encoded!'.encodeBase64() -> 'Z29ubmEgZ2V0IGVuY29kZWQh'
* 'http://twitter.com/'.encodeBase64() -> 'aHR0cDovL3R3aXR0ZXIuY29tLw=='
*
***/
'encodeBase64': function(str) {
return encodeBase64(str);
},
/***
* @method decodeBase64()
* @returns String
* @short Decodes the string from base64 encoding.
* @extra This method wraps native methods when available, and uses a custom
* implementation when not available. It can also handle Unicode string
* encodings.
*
* @example
*
* 'aHR0cDovL3R3aXR0ZXIuY29tLw=='.decodeBase64() -> 'http://twitter.com/'
* 'anVzdCBnb3QgZGVjb2RlZA=='.decodeBase64() -> 'just got decoded!'
*
***/
'decodeBase64': function(str) {
return decodeBase64(str);
},
/***
* @method forEach([search], [fn])
* @returns Array
* @short Runs callback [fn] against every character in the string, or every
* every occurence of [search] if it is provided.
* @extra Returns an array of matches. [search] may be either a string or
* regex, and defaults to every character in the string. If [fn]
* returns false at any time it will break out of the loop.
*
* @callback fn
*
* match The current match.
* i The current index.
* arr An array of all matches.
*
* @example
*
* 'jumpy'.forEach(log) -> ['j','u','m','p','y']
* 'jumpy'.forEach(/[r-z]/) -> ['u','y']
* 'jumpy'.forEach(/mp/) -> ['mp']
* 'jumpy'.forEach(/[r-z]/, function(m) {
* // Called twice: "u", "y"
* });
*
***/
'forEach': function(str, search, fn) {
return stringEach(str, search, fn);
},
/***
* @method chars([fn])
* @returns Array
* @short Runs [fn] against each character in the string, and returns an array.
*
* @callback fn
*
* code The current character.
* i The current index.
* arr An array of all characters.
*
* @example
*
* 'jumpy'.chars() -> ['j','u','m','p','y']
* 'jumpy'.chars(function(c) {
* // Called 5 times: "j","u","m","p","y"
* });
*
***/
'chars': function(str, search, fn) {
return stringEach(str, search, fn);
},
/***
* @method words([fn])
* @returns Array
* @short Runs [fn] against each word in the string, and returns an array.
* @extra A "word" is defined as any sequence of non-whitespace characters.
*
* @callback fn
*
* word The current word.
* i The current index.
* arr An array of all words.
*
* @example
*
* 'broken wear'.words() -> ['broken','wear']
* 'broken wear'.words(function(w) {
* // Called twice: "broken", "wear"
* });
*
***/
'words': function(str, fn) {
return stringEach(trim(str), /\S+/g, fn);
},
/***
* @method lines([fn])
* @returns Array
* @short Runs [fn] against each line in the string, and returns an array.
*
* @callback fn
*
* line The current line.
* i The current index.
* arr An array of all lines.
*
* @example
*
* lineText.lines() -> array of lines
* lineText.lines(function(l) {
* // Called once per line
* });
*
***/
'lines': function(str, fn) {
return stringEach(trim(str), /^.*$/gm, fn);
},
/***
* @method codes([fn])
* @returns Array
* @short Runs callback [fn] against each character code in the string.
* Returns an array of character codes.
*
* @callback fn
*
* code The current character code.
* i The current index.
* str The string being operated on.
*
* @example
*
* 'jumpy'.codes() -> [106,117,109,112,121]
* 'jumpy'.codes(function(c) {
* // Called 5 times: 106, 117, 109, 112, 121
* });
*
***/
'codes': function(str, fn) {
return stringCodes(str, fn);
},
/***
* @method shift(<n>)
* @returns Array
* @short Shifts each character in the string <n> places in the character map.
*
* @example
*
* 'a'.shift(1) -> 'b'
* 'ク'.shift(1) -> 'グ'
*
***/
'shift': function(str, n) {
var result = '';
n = n || 0;
stringCodes(str, function(c) {
result += chr(c + n);
});
return result;
},
/***
* @method isBlank()
* @returns Boolean
* @short Returns true if the string has length 0 or contains only whitespace.
*
* @example
*
* ''.isBlank() -> true
* ' '.isBlank() -> true
* 'noway'.isBlank() -> false
*
***/
'isBlank': function(str) {
return trim(str).length === 0;
},
/***
* @method isEmpty()
* @returns Boolean
* @short Returns true if the string has length 0.
*
* @example
*
* ''.isEmpty() -> true
* 'a'.isBlank() -> false
* ' '.isBlank() -> false
*
***/
'isEmpty': function(str) {
return str.length === 0;
},
/***
* @method insert(<str>, [index] = length)
* @returns String
* @short Adds <str> at [index]. Allows negative values.
*
* @example
*
* 'dopamine'.insert('e', 3) -> dopeamine
* 'spelling eror'.insert('r', -3) -> spelling error
*
***/
'insert': function(str, substr, index) {
index = isUndefined(index) ? str.length : index;
return str.slice(0, index) + substr + str.slice(index);
},
/***
* @method remove(<f>)
* @returns String
* @short Removes the first occurrence of <f> in the string.
* @extra <f> can be a either case-sensitive string or a regex. In either case
* only the first match will be removed. To remove multiple occurrences,
* use `removeAll`.
*
* @example
*
* 'schfifty five'.remove('f') -> 'schifty five'
* 'schfifty five'.remove(/[a-f]/g) -> 'shfifty five'
*
***/
'remove': function(str, f) {
return str.replace(f, '');
},
/***
* @method removeAll(<f>)
* @returns String
* @short Removes any occurences of <f> in the string.
* @extra <f> can be either a case-sensitive string or a regex. In either case
* all matches will be removed. To remove only a single occurence, use
* `remove`.
*
* @example
*
* 'schfifty five'.removeAll('f') -> 'schity ive'
* 'schfifty five'.removeAll(/[a-f]/) -> 'shity iv'
*
***/
'removeAll': function(str, f) {
return stringReplaceAll(str, f);
},
/***
* @method reverse()
* @returns String
* @short Reverses the string.
*
* @example
*
* 'jumpy'.reverse() -> 'ypmuj'
* 'lucky charms'.reverse() -> 'smrahc ykcul'
*
***/
'reverse': function(str) {
return reverseString(str);
},
/***
* @method compact()
* @returns String
* @short Compacts whitespace in the string to a single space and trims the ends.
*
* @example
*
* 'too \n much \n space'.compact() -> 'too much space'
* 'enough \n '.compact() -> 'enought'
*
***/
'compact': function(str) {
return trim(str).replace(/([\r\n\s ])+/g, function(match, whitespace) {
return whitespace === ' ' ? whitespace : ' ';
});
},
/***
* @method from([index] = 0)
* @returns String
* @short Returns a section of the string starting from [index].
*
* @example
*
* 'lucky charms'.from() -> 'lucky charms'
* 'lucky charms'.from(7) -> 'harms'
*
***/
'from': function(str, from) {
return str.slice(numberOrIndex(str, from, true));
},
/***
* @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(str, to) {
if (isUndefined(to)) to = str.length;
return str.slice(0, numberOrIndex(str, to));
},
/***
* @method dasherize()
* @returns String
* @short Converts underscores and camel casing to hypens.
*
* @example
*
* 'a_farewell_to_arms'.dasherize() -> 'a-farewell-to-arms'
* 'capsLock'.dasherize() -> 'caps-lock'
*
***/
'dasherize': function(str) {
return stringUnderscore(str).replace(/_/g, '-');
},
/***
* @method underscore()
* @returns String
* @short Converts hyphens and camel casing to underscores.
*
* @example
*
* 'a-farewell-to-arms'.underscore() -> 'a_farewell_to_arms'
* 'capsLock'.underscore() -> 'caps_lock'
*
***/
'underscore': function(str) {
return stringUnderscore(str);
},
/***
* @method camelize([upper] = true)
* @returns String
* @short Converts underscores and hyphens to camel case.
* @extra If [upper] is true, the string will be UpperCamelCase. If the
* inflections module is included, acronyms can also be defined that
* will be used when camelizing.
*
* @example
*
* 'caps_lock'.camelize() -> 'CapsLock'
* 'moz-border-radius'.camelize() -> 'MozBorderRadius'
* 'moz-border-radius'.camelize(false) -> 'mozBorderRadius'
* 'http-method'.camelize() -> 'HTTPMethod'
*
***/
'camelize': function(str, upper) {
return stringCamelize(str, upper);
},
/***
* @method spacify()
* @returns String
* @short Converts camelcase, underscores, and hyphens to spaces.
*
* @example
*
* 'camelCase'.spacify() -> 'camel case'
* 'an-ugly-string'.spacify() -> 'an ugly string'
* 'oh-no_youDid-not'.spacify().capitalize(true) -> 'something else'
*
***/
'spacify': function(str) {
return stringSpacify(str);
},
/***
* @method titleize()
* @returns String
* @short Creates a title version of the string.
* @extra Capitalizes all the words and replaces some characters in the string
* to create a nicer looking title. String#titleize is meant for
* creating pretty output.
*
* @example
*
* 'man from the boondocks'.titleize() -> 'Man from the Boondocks'
* 'x-men: apocalypse'.titleize() -> 'X Men: Apocalypse'
* 'TheManWithoutAPast'.titleize() -> 'The Man Without a Past'
* 'raiders_of_the_lost_ark'.titleize() -> 'Raiders of the Lost Ark'
*
***/
'titleize': function(str) {
return stringTitleize(str);
},
/***
* @method parameterize()
* @returns String
* @short Replaces special characters in a string so that it may be used as
* part of a pretty URL.
*
* @example
*
* 'hell, no!'.parameterize() -> 'hell-no'
*
***/
'parameterize': function(str, separator) {
return stringParameterize(str, separator);
},
/***
* @method truncate(<length>, [from] = 'right', [ellipsis] = '...')
* @returns String
* @short Truncates a string.
* @extra [from] can be `'right'`, `'left'`, or `'middle'`. If the string is
* shorter than <length>, [ellipsis] will not be added.
*
* @example
*
* 'sittin on the dock'.truncate(10) -> 'sittin on ...'
* 'sittin on the dock'.truncate(10, 'left') -> '...n the dock'
* 'sittin on the dock'.truncate(10, 'middle') -> 'sitti... dock'
*
***/
'truncate': function(str, length, from, ellipsis) {
return truncateString(str, length, from, ellipsis);
},
/***
* @method truncateOnWord(<length>, [from] = 'right', [ellipsis] = '...')
* @returns String
* @short Truncates a string without splitting up words.
* @extra [from] can be `'right'`, `'left'`, or `'middle'`. If the string is
* shorter than <length>, [ellipsis] will not be added. A "word" is
* defined as any sequence of non-whitespace characters.
*
* @example
*
* 'here we go'.truncateOnWord(5) -> 'here...'
* 'here we go'.truncateOnWord(5, 'left') -> '...we go'
*
***/
'truncateOnWord': function(str, length, from, ellipsis) {
return truncateString(str, length, from, ellipsis, true);
},
/***
* @method pad(<num> = null, [padding] = ' ')
* @returns String
* @short Pads the string out with [padding] to be exactly <num> characters.
*
* @example
*
* 'wasabi'.pad(8) -> ' wasabi '
* 'wasabi'.pad(8, '-') -> '-wasabi-'
*
***/
'pad': function(str, num, padding) {
var half, front, back;
num = coercePositiveInteger(num);
half = max(0, num - str.length) / 2;
front = floor(half);
back = ceil(half);
return padString(front, padding) + str + padString(back, padding);
},
/***
* @method padLeft(<num> = null, [padding] = ' ')
* @returns String
* @short Pads the string out from the left with [padding] to be exactly
* <num> characters.
*
* @example
*
* 'wasabi'.padLeft(8) -> ' wasabi'
* 'wasabi'.padLeft(8, '-') -> '--wasabi'
*
***/
'padLeft': function(str, num, padding) {
num = coercePositiveInteger(num);
return padString(max(0, num - str.length), padding) + str;
},
/***
* @method padRight(<num> = null, [padding] = ' ')
* @returns String
* @short Pads the string out from the right with [padding] to be exactly
* <num> characters.
*
* @example
*
* 'wasabi'.padRight(8) -> 'wasabi '
* 'wasabi'.padRight(8, '-') -> 'wasabi--'
*
***/
'padRight': function(str, num, padding) {
num = coercePositiveInteger(num);
return str + padString(max(0, num - str.length), padding);
},
/***
* @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(str, num) {
if (isUndefined(num)) num = 1;
return str.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(str, num) {
if (isUndefined(num)) num = 1;
var start = str.length - num < 0 ? 0 : str.length - num;
return str.substr(start);
},
/***
* @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(str, base) {
return stringToNumber(str, base);
},
/***
* @method capitalize([lower] = false, [all] = false)
* @returns String
* @short Capitalizes the first character of the string.
* @extra If [lower] is true, the remainder of the string will be downcased.
* If [all] is true, all words in the string will be capitalized.
*
* @example
*
* 'hello'.capitalize() -> 'Hello'
* 'HELLO'.capitalize(true) -> 'Hello'
* 'hello kitty'.capitalize() -> 'Hello kitty'
* 'hEllO kItTy'.capitalize(true, true) -> 'Hello Kitty'
*
***/
'capitalize': function(str, lower, all) {
return stringCapitalize(str, lower, all);
},
/***
* @method trimLeft()
* @returns String
* @short Removes leading 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 `trim`
* spec.
*
* @example
*
* ' wasabi '.trimLeft() -> 'wasabi '
*
***/
'trimLeft': function(str) {
return str.replace(LEFT_TRIM_REG, '');
},
/***
* @method trimRight()
* @returns String
* @short Removes 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 `trim`
* spec.
*
* @example
*
* ' wasabi '.trimRight() -> ' wasabi'
*
***/
'trimRight': function(str) {
return str.replace(RIGHT_TRIM_REG, '');
}
});
defineInstanceWithArguments(sugarString, {
/***
* @method replaceAll(<f>, [str1], [str2], ...)
* @returns String
* @short Replaces all occurences of <f> with arguments passed.
* @extra This method is intended to be a quick way to perform multiple string
* replacements quickly when the replacement token differs depending on
* position. <f> can be either a case-sensitive string or a regex.
* In either case all matches will be replaced.
*
* @example
*
* '-x -y -z'.replaceAll('-', 1, 2, 3) -> '1x 2y 3z'
* 'one and two'.replaceAll(/one|two/, '1st', '2nd') -> '1st and 2nd'
*
***/
'replaceAll': function(str, f, args) {
return stringReplaceAll(str, f, args);
},
/***
* @method format(<obj1>, <obj2>, ...)
* @returns String
* @short Replaces `{}` tokens in the string with arguments or properties.
* @extra Tokens support `deep properties`. If a single object is passed, its
* properties can be accessed by keywords such as `{name}`. If multiple
* objects or a non-object are passed, they can be accessed by the
* argument position like `{0}`. Literal braces in the string can be
* escaped by repeating them.
*
* @example
*
* 'Welcome, {name}.'.format({ name: 'Bill' }) -> 'Welcome, Bill.'
* 'You are {0} years old today.'.format(5) -> 'You are 5 years old today.'
* '{0.name} and {1.name}'.format(users) -> logs first two users' names
* '${currencies.usd.balance}'.format(Harry) -> "$500"
* '{{Hello}}'.format('Hello') -> "{Hello}"
*
***/
'format': function(str, args) {
var arg1 = args[0] && args[0].valueOf();
// Unwrap if a single object is passed in.
if (args.length === 1 && isObjectType(arg1)) {
args = arg1;
}
return stringFormatMatcher(str, args);
}
});
buildBase64();
buildEntities();
/***
* @module Array
* @description Array manipulation and traversal, alphanumeric sorting and collation.
*
***/
var HALF_WIDTH_NINE = 0x39;
var FULL_WIDTH_NINE = 0xff19;
// Undefined array elements in < IE8 will not be visited by concat
// and so will not be copied. This means that non-sparse arrays will
// become sparse, so detect for this here.
var HAS_CONCAT_BUG = !('0' in [].concat(undefined).concat());
var ARRAY_OPTIONS = {
'sortIgnore': null,
'sortNatural': true,
'sortIgnoreCase': true,
'sortOrder': getSortOrder(),
'sortCollate': collateStrings,
'sortEquivalents': getSortEquivalents()
};
/***
* @method getOption(<name>)
* @returns Mixed
* @accessor
* @short Gets an option used interally by Array.
* @extra Options listed below. Current options are for sorting strings with
* `sortBy`.
*
* @options
*
* sortIgnore A regex to ignore when sorting. An example usage of this
* option would be to ignore numbers in a list to instead
* sort by the first text that appears. Default is `null`.
*
* sortIgnoreCase A boolean that ignores case when sorting.
* Default is `true`.
*
* sortNatural A boolean that turns on natural sorting. "Natural" means
* that numerals like "10" will be sorted after "9" instead
* of after "1". Default is `true`.
*
* sortOrder A string of characters to use as the base sort order. The
* default is an order natural to most major world languages.
*
* sortEquivalents A table of equivalent characters used when sorting. The
* default produces a natural sort order for most world
* languages, however can be modified for others. For
* example, setting "ä" and "ö" to `null` in the table would
* produce a Scandanavian sort order.
*
* sortCollate The collation function used when sorting strings. The
* default function produces a natural sort order that can
* be customized with the other "sort" options. Overriding
* the function directly here will also override these
* options.
*
* @example
*
* Sugar.Array.getOption('sortNatural')
*
***
* @method setOption(<name>, <value>)
* @accessor
* @short Sets an option used interally by Array.
* @extra Options listed below. Current options are for sorting strings with
* `sortBy`. If <value> is `null`, the default value will be restored.
*
* @options
*
* sortIgnore A regex to ignore when sorting. An example usage of this
* option would be to ignore numbers in a list to instead
* sort by the first text that appears. Default is `null`.
*
* sortIgnoreCase A boolean that ignores case when sorting.
* Default is `true`.
*
* sortNatural A boolean that turns on natural sorting. "Natural" means
* that numerals like "10" will be sorted after "9" instead
* of after "1". Default is `true`.
*
* sortOrder A string of characters to use as the base sort order. The
* default is an order natural to most major world languages.
*
* sortEquivalents A table of equivalent characters used when sorting. The
* default produces a natural sort order for most world
* languages, however can be modified for others. For
* example, setting "ä" and "ö" to `null` in the table would
* produce a Scandanavian sort order. Note that setting this
* option to `null` will restore the default table, but any
* mutations made to that table will persist.
*
* sortCollate The collation function used when sorting strings. The
* default function produces a natural sort order that can
* be customized with the other "sort" options. Overriding
* the function directly here will also override these
* options.
*
* @example
*
* Sugar.Array.setOption('sortIgnore', /^\d+\./)
* Sugar.Array.setOption('sortIgnoreCase', false)
*
***/
var _arrayOptions = defineOptionsAccessor(sugarArray, ARRAY_OPTIONS);
function setArrayChainableConstructor() {
setChainableConstructor(sugarArray, arrayCreate);
}
function isArrayOrInherited(obj) {
return obj && obj.constructor && isArray(obj.constructor.prototype);
}
function arrayCreate(obj, clone) {
var arr;
if (isArrayOrInherited(obj)) {
arr = clone ? arrayClone(obj) : obj;
} else if (isObjectType(obj) || isString(obj)) {
arr = Array.from(obj);
} else if (isDefined(obj)) {
arr = [obj];
}
return arr || [];
}
function arrayClone(arr) {
var clone = new Array(arr.length);
forEach(arr, function(el, i) {
clone[i] = el;
});
return clone;
}
function arrayConcat(arr1, arr2) {
if (HAS_CONCAT_BUG) {
return arraySafeConcat(arr1, arr2);
}
return arr1.concat(arr2);
}
// Avoids issues with [undefined] in < IE9
function arrayWrap(obj) {
var arr = [];
arr.push(obj);
return arr;
}
// Avoids issues with concat in < IE8
function arraySafeConcat(arr, arg) {
var result = arrayClone(arr), len = result.length, arr2;
arr2 = isArray(arg) ? arg : [arg];
result.length += arr2.length;
forEach(arr2, function(el, i) {
result[len + i] = el;
});
return result;
}
function arrayAppend(arr, el, index) {
var spliceArgs;
index = +index;
if (isNaN(index)) {
index = arr.length;
}
spliceArgs = [index, 0];
if (isDefined(el)) {
spliceArgs = spliceArgs.concat(el);
}
arr.splice.apply(arr, spliceArgs);
return arr;
}
function arrayRemove(arr, f) {
var matcher = getMatcher(f), i = 0;
while(i < arr.length) {
if (matcher(arr[i], i, arr)) {
arr.splice(i, 1);
} else {
i++;
}
}
return arr;
}
function arrayExclude(arr, f) {
var result = [], matcher = getMatcher(f);
for (var i = 0; i < arr.length; i++) {
if (!matcher(arr[i], i, arr)) {
result.push(arr[i]);
}
}
return result;
}
function arrayUnique(arr, map) {
var result = [], obj = {}, refs = [];
forEach(arr, function(el, i) {
var transformed = map ? mapWithShortcuts(el, map, arr, [el, i, arr]) : el;
var key = serializeInternal(transformed, refs);
if (!hasOwn(obj, key)) {
result.push(el);
obj[key] = true;
}
});
return result;
}
function arrayFlatten(arr, level, current) {
var result = [];
level = level || Infinity;
current = current || 0;
forEach(arr, function(el) {
if (isArray(el) && current < level) {
result = result.concat(arrayFlatten(el, level, current + 1));
} else {
result.push(el);
}
});
return result;
}
function arrayCompact(arr, all) {
return filter(arr, function(el) {
return el || (!all && el != null && el.valueOf() === el.valueOf());
});
}
function arrayShuffle(arr) {
arr = arrayClone(arr);
var i = arr.length, j, x;
while(i) {
j = (Math.random() * i) | 0;
x = arr[--i];
arr[i] = arr[j];
arr[j] = x;
}
return arr;
}
function arrayGroupBy(arr, map, fn) {
var result = {}, key;
forEach(arr, function(el, i) {
key = mapWithShortcuts(el, map, arr, [el, i, arr]);
if (!hasOwn(result, key)) {
result[key] = [];
}
result[key].push(el);
});
if (fn) {
forEachProperty(result, fn);
}
return result;
}
function arrayIntersectOrSubtract(arr1, arr2, subtract) {
var result = [], obj = {}, refs = [];
if (!isArray(arr2)) {
arr2 = arrayWrap(arr2);
}
forEach(arr2, function(el) {
obj[serializeInternal(el, refs)] = true;
});
forEach(arr1, function(el) {
var key = serializeInternal(el, refs);
if (hasOwn(obj, key) !== subtract) {
delete obj[key];
result.push(el);
}
});
return result;
}
// Collation helpers
function compareValue(aVal, bVal) {
var cmp, i, collate;
if (isString(aVal) && isString(bVal)) {
collate = _arrayOptions('sortCollate');
return collate(aVal, bVal);
} else if (isArray(aVal) && isArray(bVal)) {
if (aVal.length < bVal.length) {
return -1;
} else if (aVal.length > bVal.length) {
return 1;
} else {
for(i = 0; i < aVal.length; i++) {
cmp = compareValue(aVal[i], bVal[i]);
if (cmp !== 0) {
return cmp;
}
}
return 0;
}
}
return aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
}
function codeIsNumeral(code) {
return (code >= HALF_WIDTH_ZERO && code <= HALF_WIDTH_NINE) ||
(code >= FULL_WIDTH_ZERO && code <= FULL_WIDTH_NINE);
}
function collateStrings(a, b) {
var aValue, bValue, aChar, bChar, aEquiv, bEquiv, index = 0, tiebreaker = 0;
var sortOrder = _arrayOptions('sortOrder');
var sortIgnore = _arrayOptions('sortIgnore');
var sortNatural = _arrayOptions('sortNatural');
var sortIgnoreCase = _arrayOptions('sortIgnoreCase');
var sortEquivalents = _arrayOptions('sortEquivalents');
a = getCollationReadyString(a, sortIgnore, sortIgnoreCase);
b = getCollationReadyString(b, sortIgnore, sortIgnoreCase);
do {
aChar = getCollationCharacter(a, index, sortEquivalents);
bChar = getCollationCharacter(b, index, sortEquivalents);
aValue = getSortOrderIndex(aChar, sortOrder);
bValue = getSortOrderIndex(bChar, sortOrder);
if (aValue === -1 || bValue === -1) {
aValue = a.charCodeAt(index) || null;
bValue = b.charCodeAt(index) || null;
if (sortNatural && codeIsNumeral(aValue) && codeIsNumeral(bValue)) {
aValue = stringToNumber(a.slice(index));
bValue = stringToNumber(b.slice(index));
}
} else {
aEquiv = aChar !== a.charAt(index);
bEquiv = bChar !== b.charAt(index);
if (aEquiv !== bEquiv && tiebreaker === 0) {
tiebreaker = aEquiv - bEquiv;
}
}
index += 1;
} while(aValue != null && bValue != null && aValue === bValue);
if (aValue === bValue) return tiebreaker;
return aValue - bValue;
}
function getCollationReadyString(str, sortIgnore, sortIgnoreCase) {
if (!isString(str)) str = String(str);
if (sortIgnoreCase) {
str = str.toLowerCase();
}
if (sortIgnore) {
str = str.replace(sortIgnore, '');
}
return str;
}
function getCollationCharacter(str, index, sortEquivalents) {
var chr = str.charAt(index);
return getOwn(sortEquivalents, chr) || chr;
}
function getSortOrderIndex(chr, sortOrder) {
if (!chr) {
return null;
} else {
return sortOrder.indexOf(chr);
}
}
function getSortOrder() {
var order = 'AÁÀÂÃĄBCĆČÇDĎÐEÉÈĚÊËĘFGĞHıIÍÌİÎÏJKLŁMNŃŇÑOÓÒÔPQRŘSŚŠŞTŤUÚÙŮÛÜVWXYÝZŹŻŽÞÆŒØÕÅÄÖ';
return map(order.split(''), function(str) {
return str + str.toLowerCase();
}).join('');
}
function getSortEquivalents() {
var equivalents = {};
forEach(spaceSplit('AÁÀÂÃÄ CÇ EÉÈÊË IÍÌİÎÏ OÓÒÔÕÖ Sß UÚÙÛÜ'), function(set) {
var first = set.charAt(0);
forEach(set.slice(1).split(''), function(chr) {
equivalents[chr] = first;
equivalents[chr.toLowerCase()] = first.toLowerCase();
});
});
return equivalents;
}
defineStatic(sugarArray, {
/***
*
* @method create(<obj>, [clone] = false)
* @returns Array
* @static
* @short Creates an array from an unknown object.
* @extra This method is similar to native `Array.from` but is faster when
* <obj> is already an array. When [clone] is true, the array will be
* shallow cloned. Additionally, it will not fail on `undefined`,
* `null`, or numbers, producing an empty array in the case of
* `undefined` and wrapping <obj> otherwise.
*
* @example
*
* Array.create() -> []
* Array.create(8) -> [8]
* Array.create('abc') -> ['a','b','c']
* Array.create([1,2,3]) -> [1, 2, 3]
* Array.create(undefined) -> []
*
***/
'create': function(obj, clone) {
return arrayCreate(obj, clone);
},
/***
*
* @method construct(<n>, <fn>)
* @returns Array
* @static
* @short Constructs an array of <n> length from the values of <fn>.
* @extra This function is essentially a shortcut for using `Array.from` with
* `new Array(n)`.
*
* @callback fn
*
* i The index of the current iteration.
*
* @example
*
* Array.construct(4, function(i) {
* return i * i;
* }); -> [0, 1, 4]
*
***/
'construct': function(n, fn) {
n = coercePositiveInteger(n);
return Array.from(new Array(n), function(el, i) {
return fn && fn(i);
});
}
});
defineInstance(sugarArray, {
/***
* @method isEmpty()
* @returns Boolean
* @short Returns true if the array has a length of zero.
*
* @example
*
* [].isEmpty() -> true
* ['a'].isEmpty() -> false
*
***/
'isEmpty': function(arr) {
return arr.length === 0;
},
/***
* @method isEqual(<arr>)
* @returns Boolean
* @short Returns true if the array is equal to <arr>.
* @extra Objects in the array are considered equal if they are not obserably
* distinguishable. This method is an instance alias for
* `Object.isEqual()`.
*
* @example
*
* ['a','b'].isEqual(['a','b']) -> true
* ['a','b'].isEqual(['a','c']) -> false
* [{a:'a'}].isEqual([{a:'a'}]) -> true
* [5].isEqual([Object(5)]) -> false
*
***/
'isEqual': function(a, b) {
return isEqual(a, b);
},
/***
* @method clone()
* @returns Array
* @short Creates a shallow clone of the array.
*
* @example
*
* [1,2,3].clone() -> [1,2,3]
*
***/
'clone': function(arr) {
return arrayClone(arr);
},
/***
* @method at(<index>, [loop] = false)
* @returns Mixed
* @short Gets the element(s) at <index>.
* @extra When [loop] is true, overshooting the end of the array will begin
* counting from the other end. <index> may be negative. If <index> is
* an array, multiple elements will be returned.
*
* @example
*
* [1,2,3].at(0) -> 1
* [1,2,3].at(2) -> 3
* [1,2,3].at(4) -> undefined
* [1,2,3].at(4, true) -> 2
* [1,2,3].at(-1) -> 3
* [1,2,3].at([0, 1]) -> [1, 2]
*
***/
'at': function(arr, index, loop) {
return getEntriesForIndexes(arr, index, loop);
},
/***
* @method add(<item>, [index])
* @returns Array
* @short Adds <item> to the array and returns the result as a new array.
* @extra If <item> is also an array, it will be concatenated instead of
* inserted. [index] will control where <item> is added. Use `append`
* to modify the original array.
*
* @example
*
* [1,2,3,4].add(5) -> [1,2,3,4,5]
* [1,2,3,4].add(8, 1) -> [1,8,2,3,4]
* [1,2,3,4].add([5,6,7]) -> [1,2,3,4,5,6,7]
*
***/
'add': function(arr, item, index) {
return arrayAppend(arrayClone(arr), item, index);
},
/***
* @method subtract(<item>)
* @returns Array
* @short Subtracts <item> from the array and returns the result as a new array.
* @extra If <item> is also an array, all elements in it will be removed. In
* addition to primitives, this method will also deep-check objects for
* equality.
*
* @example
*
* [1,3,5].subtract([5,7,9]) -> [1,3]
* ['a','b'].subtract(['b','c']) -> ['a']
* [1,2,3].subtract(2) -> [1,3]
*
***/
'subtract': function(arr1, arr2) {
return arrayIntersectOrSubtract(arr1, arr2, true);
},
/***
* @method append(<item>, [index])
* @returns Array
* @short Appends <item> to the array.
* @extra If <item> is also an array, it will be concatenated instead of
* inserted. This method modifies the array! Use `add` to create a new
* array. Additionally, `insert` is provided as an alias that reads
* better when using an index.
*
* @example
*
* [1,2,3,4].append(5) -> [1,2,3,4,5]
* [1,2,3,4].append([5,6,7]) -> [1,2,3,4,5,6,7]
* [1,2,3,4].append(8, 1) -> [1,8,2,3,4]
*
***/
'append': function(arr, item, index) {
return arrayAppend(arr, item, index);
},
/***
* @method removeAt(<start>, [end])
* @returns Array
* @short Removes element at <start>. If [end] is specified, removes the range
* between <start> and [end]. This method will modify the array!
*
* @example
*
* ['a','b','c'].removeAt(0) -> ['b','c']
* [1,2,3,4].removeAt(1, 2) -> [1, 4]
*
***/
'removeAt': function(arr, start, end) {
if (isUndefined(start)) return arr;
if (isUndefined(end)) end = start;
arr.splice(start, end - start + 1);
return arr;
},
/***
* @method unique([map])
* @returns Array
* @short Removes all duplicate elements in the array.
* @extra [map] may be a function returning the value to be uniqued or a
* string acting as a shortcut. This is most commonly used when you
* only need to check a single field that can ensure the object's
* uniqueness (such as an `id` field). If [map] is not passed, then
* objects will be deep checked for equality. Supports
* `deep properties`.
*
* @callback map
*
* el The element of the current iteration.
* i The index of the current iteration.
* arr A reference to the array.
*
* @example
*
* [1,2,2,3].unique() -> [1,2,3]
* [{a:'a'},{a:'a'}].unique() -> [{a:'a'}]
*
* users.unique(function(user) {
* return user.id;
* }); -> users array uniqued by id
*
* users.unique('id') -> users array uniqued by id
*
***/
'unique': function(arr, map) {
return arrayUnique(arr, map);
},
/***
* @method flatten([limit] = Infinity)
* @returns Array
* @short Returns a flattened, one-dimensional copy of the array.
* @extra You can optionally specify a [limit], which will only flatten to
* that depth.
*
* @example
*
* [[1], 2, [3]].flatten() -> [1,2,3]
* [[1],[],2,3].flatten() -> [1,2,3]
*
***/
'flatten': function(arr, limit) {
return arrayFlatten(arr, limit);
},
/***
* @method first([num] = 1)
* @returns Mixed
* @short Returns the first element(s) in the array.
* @extra When <num> is passed, returns the first <num> elements in the array.
*
* @example
*
* [1,2,3].first() -> 1
* [1,2,3].first(2) -> [1,2]
*
***/
'first': function(arr, num) {
if (isUndefined(num)) return arr[0];
if (num < 0) num = 0;
return arr.slice(0, num);
},
/***
* @method last([num] = 1)
* @returns Mixed
* @short Returns the last element(s) in the array.
* @extra When <num> is passed, returns the last <num> elements in the array.
*
* @example
*
* [1,2,3].last() -> 3
* [1,2,3].last(2) -> [2,3]
*
***/
'last': function(arr, num) {
if (isUndefined(num)) return arr[arr.length - 1];
var start = arr.length - num < 0 ? 0 : arr.length - num;
return arr.slice(start);
},
/***
* @method from(<index>)
* @returns Array
* @short Returns a slice of the array from <index>.
*
* @example
*
* ['a','b','c'].from(1) -> ['b','c']
* ['a','b','c'].from(2) -> ['c']
*
***/
'from': function(arr, num) {
return arr.slice(num);
},
/***
* @method to(<index>)
* @returns Array
* @short Returns a slice of the array up to <index>.
*
* @example
*
* ['a','b','c'].to(1) -> ['a']
* ['a','b','c'].to(2) -> ['a','b']
*
***/
'to': function(arr, num) {
if (isUndefined(num)) num = arr.length;
return arr.slice(0, num);
},
/***
* @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]
* [null, [null, 'bye']].compact() -> ['hi', [null, 'bye']]
*
***/
'compact': function(arr, all) {
return arrayCompact(arr, all);
},
/***
* @method groupBy(<map>, [fn])
* @returns Object
* @short Groups the array by <map>.
* @extra Will return an object whose keys are the mapped from <map>, which
* may be a mapping function, or a string acting as a shortcut. <map>
* supports `deep properties`. Optionally calls [fn] for each group.
*
* @callback map
*
* el The element of the current iteration.
* i The index of the current iteration.
* arr A reference to the array.
*
* @callback fn
*
* arr The current group as an array.
* key The unique key of the current group.
* obj A reference to the object.
*
* @example
*
* ['a','aa','aaa'].groupBy('length') -> { 1: ['a'], 2: ['aa'], 3: ['aaa'] }
*
* users.groupBy(function(n) {
* return n.age;
* }); -> users array grouped by age
*
* users.groupBy('age', function(age, users) {
* // iterates each grouping
* });
*
***/
'groupBy': function(arr, map, fn) {
return arrayGroupBy(arr, map, fn);
},
/***
* @method inGroups(<num>, [padding])
* @returns Array
* @short Groups the array into <num> arrays.
* @extra If specified, [padding] will be added to the last array to be of
* equal length.
*
* @example
*
* [1,2,3,4,5,6,7].inGroups(3) -> [[1,2,3],[4,5,6],[7]]
* [1,2,3,4,5,6,7].inGroups(3, 0) -> [[1,2,3],[4,5,6],[7,0,0]]
*
***/
'inGroups': function(arr, num, padding) {
var pad = isDefined(padding);
var result = new Array(num);
var divisor = ceil(arr.length / num);
simpleRepeat(num, function(i) {
var index = i * divisor;
var group = arr.slice(index, index + divisor);
if (pad && group.length < divisor) {
simpleRepeat(divisor - group.length, function() {
group.push(padding);
});
}
result[i] = group;
});
return result;
},
/***
* @method inGroupsOf(<num>, [padding] = null)
* @returns Array
* @short Groups the array into arrays of <num> elements each.
* @extra [padding] will be added to the last array to be of equal length.
*
* @example
*
* [1,2,3,4,5,6,7].inGroupsOf(4) -> [ [1,2,3,4], [5,6,7] ]
* [1,2,3,4,5,6,7].inGroupsOf(4, 0) -> [ [1,2,3,4], [5,6,7,0] ]
*
***/
'inGroupsOf': function(arr, num, padding) {
var result = [], len = arr.length, group;
if (len === 0 || num === 0) return arr;
if (isUndefined(num)) num = 1;
if (isUndefined(padding)) padding = null;
simpleRepeat(ceil(len / num), function(i) {
group = arr.slice(num * i, num * i + num);
while(group.length < num) {
group.push(padding);
}
result.push(group);
});
return result;
},
/***
* @method shuffle()
* @returns Array
* @short Returns a copy of the array with the elements randomized.
* @extra Uses Fisher-Yates algorithm.
*
* @example
*
* [1,2,3,4].shuffle() -> [?,?,?,?]
*
***/
'shuffle': function(arr) {
return arrayShuffle(arr);
},
/***
* @method sample([num] = 1, [remove] = false)
* @returns Mixed
* @short Returns a random element from the array.
* @extra If [num] is passed, will return an array of [num] elements. If
* [remove] is true, sampled elements will also be removed from the
* array. [remove] can also be passed in place of [num].
*
* @example
*
* [1,2,3,4,5].sample() -> // Random element
* [1,2,3,4,5].sample(1) -> // Array of 1 random element
* [1,2,3,4,5].sample(3) -> // Array of 3 random elements
*
***/
'sample': function(arr, arg1, arg2) {
var result = [], num, remove, single;
if (isBoolean(arg1)) {
remove = arg1;
} else {
num = arg1;
remove = arg2;
}
if (isUndefined(num)) {
num = 1;
single = true;
}
if (!remove) {
arr = arrayClone(arr);
}
num = min(num, arr.length);
for (var i = 0, index; i < num; i++) {
index = trunc(Math.random() * arr.length);
result.push(arr[index]);
arr.splice(index, 1);
}
return single ? result[0] : result;
},
/***
* @method sortBy(<map>, [desc] = false)
* @returns Array
* @short Enhanced sorting function that will sort the array by <map>.
* @extra <map> may be a function, a string acting as a shortcut, an array
* (comparison by multiple values), or blank (direct comparison of
* array values). <map> supports `deep properties`. [desc] will sort
* the array in descending order. When the field being sorted on is
* a string, the resulting order will be determined by an internal
* collation algorithm that is optimized for major Western languages,
* but can be customized using sorting accessors such as `sortIgnore`.
* This method will modify the array!
*
* @callback map
*
* el An array element.
*
* @example
*
* ['world','a','new'].sortBy('length') -> ['a','new','world']
* ['world','a','new'].sortBy('length', true) -> ['world','new','a']
* users.sortBy(function(n) {
* return n.age;
* }); -> users array sorted by age
* users.sortBy('age') -> users array sorted by age
*
***/
'sortBy': function(arr, map, desc) {
arr.sort(function(a, b) {
var aProperty = mapWithShortcuts(a, map, arr, [a]);
var bProperty = mapWithShortcuts(b, map, arr, [b]);
return compareValue(aProperty, bProperty) * (desc ? -1 : 1);
});
return arr;
},
/***
* @method remove(<search>)
* @returns Array
* @short Removes any element in the array that matches <search>.
* @extra This method will modify the array! Use `exclude` for a
* non-destructive alias. This method implements `enhanced matching`.
*
* @callback search
*
* el The element of the current iteration.
* i The index of the current iteration.
* arr A reference to the array.
*
* @example
*
* [1,2,3].remove(3) -> [1,2]
* ['a','b','c'].remove(/b/) -> ['a','c']
* [{a:1},{b:2}].remove(function(n) {
* return n['a'] == 1;
* }); -> [{b:2}]
*
***/
'remove': function(arr, f) {
return arrayRemove(arr, f);
},
/***
* @method exclude(<search>)
* @returns Array
* @short Returns a new array with every element that does not match <search>.
* @extra This method can be thought of as the inverse of `Array#filter`. It
* will not modify the original array, Use `remove` to modify the
* array in place. Implements `enhanced matching`.
*
* @callback search
*
* el The element of the current iteration.
* i The index of the current iteration.
* arr A reference to the array.
*
* @example
*
* [1,2,3].exclude(3) -> [1,2]
* ['a','b','c'].exclude(/b/) -> ['a','c']
* [{a:1},{b:2}].exclude(function(n) {
* return n['a'] == 1;
* }); -> [{b:2}]
*
***/
'exclude': function(arr, f) {
return arrayExclude(arr, f);
},
/***
* @method union(<arr>)
* @returns Array
* @short Returns a new array containing elements in both arrays with
* duplicates removed.
* @extra In addition to primitives, this method will also deep-check objects
* for equality.
*
* @example
*
* [1,3,5].union([5,7,9]) -> [1,3,5,7,9]
* ['a','b'].union(['b','c']) -> ['a','b','c']
*
***/
'union': function(arr1, arr2) {
return arrayUnique(arrayConcat(arr1, arr2));
},
/***
* @method intersect(<arr>)
* @returns Array
* @short Returns a new array containing any elements that both arrays have in
* common.
* @extra In addition to primitives, this method will also deep-check objects
* for equality.
*
* @example
*
* [1,3,5].intersect([5,7,9]) -> [5]
* ['a','b'].intersect(['b','c']) -> ['b']
*
***/
'intersect': function(arr1, arr2) {
return arrayIntersectOrSubtract(arr1, arr2, false);
}
});
defineInstanceWithArguments(sugarArray, {
/***
* @method zip([arr1], [arr2], ...)
* @returns Array
* @short Merges multiple arrays together.
* @extra This method "zips up" smaller arrays into one large whose elements
* are "all elements at index 0", "all elements at index 1", etc.
* Useful when you have associated data that is split over separated
* arrays. If the arrays passed have more elements than the original
* array, they will be discarded. If they have fewer elements, the
* missing elements will filled with `null`.
*
* @example
*
* [1,2,3].zip([4,5,6]) -> [[1,2], [3,4], [5,6]]
*
***/
'zip': function(arr, args) {
return map(arr, function(el, i) {
return [el].concat(map(args, function(k) {
return (i in k) ? k[i] : null;
}));
});
}
});
/***
* @method insert(<item>, [index])
* @returns Array
* @short Appends <item> to the array at [index].
* @extra This method is simply a more readable alias for `append` when passing
* an index. If <el> is an array it will be joined. This method modifies
* the array! Use `add` as a non-destructive alias.
*
* @example
*
* [1,3,4,5].insert(2, 1) -> [1,2,3,4,5]
* [1,4,5,6].insert([2,3], 1) -> [1,2,3,4,5,6]
*
***/
alias(sugarArray, 'insert', 'append');
setArrayChainableConstructor();
/***
* @module Object
* @description Object creation, manipulation, comparison, type checking, and more.
*
* Much thanks to kangax for his informative aricle about how problems with
* instanceof and constructor: http://bit.ly/1Qds27W
*
***/
// Matches bracket-style query strings like user[name]
var DEEP_QUERY_STRING_REG = /^(.+?)(\[.*\])$/;
// Matches any character not allowed in a decimal number.
var NON_DECIMAL_REG = /[^\d.-]/;
// Native methods for merging by descriptor when available.
var getOwnPropertyNames = Object.getOwnPropertyNames;
var getOwnPropertySymbols = Object.getOwnPropertySymbols;
var getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
// Basic Helpers
function isArguments(obj, className) {
className = className || classToString(obj);
// .callee exists on Arguments objects in < IE8
return hasProperty(obj, 'length') && (className === '[object Arguments]' || !!obj.callee);
}
// Query Strings | Creating
function toQueryStringWithOptions(obj, opts) {
opts = opts || {};
if (isUndefined(opts.separator)) {
opts.separator = '_';
}
return toQueryString(obj, opts.deep, opts.transform, opts.prefix || '', opts.separator);
}
function toQueryString(obj, deep, transform, prefix, separator) {
if (isArray(obj)) {
return collectArrayAsQueryString(obj, deep, transform, prefix, separator);
} else if (isObjectType(obj) && obj.toString === internalToString) {
return collectObjectAsQueryString(obj, deep, transform, prefix, separator);
} else if (prefix) {
return getURIComponentValue(obj, prefix, transform);
}
return '';
}
function collectArrayAsQueryString(arr, deep, transform, prefix, separator) {
var el, qc, key, result = [];
// Intentionally treating sparse arrays as dense here by avoiding map,
// otherwise indexes will shift during the process of serialization.
for (var i = 0, len = arr.length; i < len; i++) {
el = arr[i];
key = prefix + (prefix && deep ? '[]' : '');
if (!key && !isObjectType(el)) {
// If there is no key, then the values of the array should be
// considered as null keys, so use them instead;
qc = sanitizeURIComponent(el);
} else {
qc = toQueryString(el, deep, transform, key, separator);
}
result.push(qc);
}
return result.join('&');
}
function collectObjectAsQueryString(obj, deep, transform, prefix, separator) {
var result = [];
forEachProperty(obj, function(val, key) {
var fullKey;
if (prefix && deep) {
fullKey = prefix + '[' + key + ']';
} else if (prefix) {
fullKey = prefix + separator + key;
} else {
fullKey = key;
}
result.push(toQueryString(val, deep, transform, fullKey, separator));
});
return result.join('&');
}
function getURIComponentValue(obj, prefix, transform) {
var value;
if (transform) {
value = transform(obj, prefix);
} else if (isDate(obj)) {
value = obj.getTime();
} else {
value = obj;
}
return sanitizeURIComponent(prefix) + '=' + sanitizeURIComponent(value);
}
function sanitizeURIComponent(obj) {
// undefined, null, and NaN are represented as a blank string,
// while false and 0 are stringified.
return !obj && obj !== false && obj !== 0 ? '' : encodeURIComponent(obj);
}
// Query Strings | Parsing
function fromQueryStringWithOptions(obj, opts) {
var str = String(obj || '').replace(/^.*?\?/, ''), result = {}, auto;
opts = opts || {};
if (str) {
forEach(str.split('&'), function(p) {
var split = p.split('=');
var key = decodeURIComponent(split[0]);
var val = split.length === 2 ? decodeURIComponent(split[1]) : '';
auto = opts.auto !== false;
parseQueryComponent(result, key, val, opts.deep, auto, opts.separator, opts.transform);
});
}
return result;
}
function parseQueryComponent(obj, key, val, deep, auto, separator, transform) {
var match;
if (separator) {
key = mapQuerySeparatorToKeys(key, separator);
deep = true;
}
if (deep === true && (match = key.match(DEEP_QUERY_STRING_REG))) {
parseDeepQueryComponent(obj, match, val, deep, auto, separator, transform);
} else {
setQueryProperty(obj, key, val, auto, transform);
}
}
function parseDeepQueryComponent(obj, match, val, deep, auto, separator, transform) {
var key = match[1];
var inner = match[2].slice(1, -1).split('][');
forEach(inner, function(k) {
if (!hasOwn(obj, key)) {
obj[key] = k ? {} : [];
}
obj = getOwn(obj, key);
key = k ? k : obj.length.toString();
});
setQueryProperty(obj, key, val, auto, transform);
}
function mapQuerySeparatorToKeys(key, separator) {
var split = key.split(separator), result = split[0];
for (var i = 1, len = split.length; i < len; i++) {
result += '[' + split[i] + ']';
}
return result;
}
function setQueryProperty(obj, key, val, auto, transform) {
var fnValue;
if (transform) {
fnValue = transform(val, key, obj);
}
if (isDefined(fnValue)) {
val = fnValue;
} else if (auto) {
val = getQueryValueAuto(obj, key, val);
}
obj[key] = val;
}
function getQueryValueAuto(obj, key, val) {
if (!val) {
return null;
} else if (val === 'true') {
return true;
} else if (val === 'false') {
return false;
}
var num = +val;
if (!isNaN(num) && stringIsDecimal(val)) {
return num;
}
var existing = getOwn(obj, key);
if (val && existing) {
return isArray(existing) ? existing.concat(val) : [existing, val];
}
return val;
}
function stringIsDecimal(str) {
return str !== '' && !NON_DECIMAL_REG.test(str);
}
// Object Merging
function mergeWithOptions(target, source, opts) {
opts = opts || {};
return objectMerge(target, source, opts.deep, opts.resolve, opts.hidden, opts.descriptor);
}
function defaults(target, sources, opts) {
opts = opts || {};
opts.resolve = opts.resolve || false;
return mergeAll(target, sources, opts);
}
function mergeAll(target, sources, opts) {
if (!isArray(sources)) {
sources = [sources];
}
forEach(sources, function(source) {
return mergeWithOptions(target, source, opts);
});
return target;
}
function iterateOverProperties(hidden, obj, fn) {
if (getOwnPropertyNames && hidden) {
iterateOverKeys(getOwnPropertyNames, obj, fn, hidden);
} else {
forEachProperty(obj, fn);
}
if (getOwnPropertySymbols) {
iterateOverKeys(getOwnPropertySymbols, obj, fn, hidden);
}
}
// "keys" may include symbols
function iterateOverKeys(getFn, obj, fn, hidden) {
var keys = getFn(obj), desc;
for (var i = 0, key; key = keys[i]; i++) {
desc = getOwnPropertyDescriptor(obj, key);
if (desc.enumerable || hidden) {
fn(obj[key], key);
}
}
}
function mergeByPropertyDescriptor(target, source, prop, sourceVal) {
var descriptor = getOwnPropertyDescriptor(source, prop);
if (isDefined(descriptor.value)) {
descriptor.value = sourceVal;
}
defineProperty(target, prop, descriptor);
}
function objectMerge(target, source, deep, resolve, hidden, descriptor) {
var resolveByFunction = isFunction(resolve), resolveConflicts = resolve !== false;
if (isUndefined(target)) {
target = getNewObjectForMerge(source);
} else if (resolveConflicts && isDate(target) && isDate(source)) {
// A date's timestamp is a property that can only be reached through its
// methods, so actively set it up front if both are dates.
target.setTime(source.getTime());
}
if (isPrimitive(target)) {
// Will not merge into a primitive type, so simply override.
return source;
}
// If the source object is a primitive
// type then coerce it into an object.
if (isPrimitive(source)) {
source = coercePrimitiveToObject(source);
}
iterateOverProperties(hidden, source, function(val, key) {
var sourceVal, targetVal, resolved, goDeep, result;
sourceVal = source[key];
// We are iterating over properties of the source, so hasOwnProperty on
// it is guaranteed to always be true. However, the target may happen to
// have properties in its prototype chain that should not be considered
// as conflicts.
targetVal = getOwn(target, key);
if (resolveByFunction) {
result = resolve(key, targetVal, sourceVal, target, source);
if (isUndefined(result)) {
// Result is undefined so do not merge this property.
return;
} else if (isDefined(result) && result !== Sugar) {
// If the source returns anything except undefined, then the conflict
// has been resolved, so don't continue traversing into the object. If
// the returned value is the Sugar global object, then allowing Sugar
// to resolve the conflict, so continue on.
sourceVal = result;
resolved = true;
}
} else if (isUndefined(sourceVal)) {
// Will not merge undefined.
return;
}
// Regex properties are read-only, so intentionally disallowing deep
// merging for now. Instead merge by reference even if deep.
goDeep = !resolved && deep && isObjectType(sourceVal) && !isRegExp(sourceVal);
if (!goDeep && !resolveConflicts && isDefined(targetVal)) {
return;
}
if (goDeep) {
sourceVal = objectMerge(targetVal, sourceVal, deep, resolve, hidden, descriptor);
}
// getOwnPropertyNames is standing in as
// a test for property descriptor support
if (getOwnPropertyNames && descriptor) {
mergeByPropertyDescriptor(target, source, key, sourceVal);
} else {
target[key] = sourceVal;
}
});
return target;
}
function getNewObjectForMerge(source) {
var klass = classToString(source);
// Primitive types, dates, and regexes have no "empty" state. If they exist
// at all, then they have an associated value. As we are only creating new
// objects when they don't exist in the target, these values can come alone
// for the ride when created.
if (isArray(source, klass)) {
return [];
} else if (isPlainObject(source, klass)) {
return {};
} else if (isDate(source, klass)) {
return new Date(source.getTime());
} else if (isRegExp(source, klass)) {
return RegExp(source.source, getRegExpFlags(source));
} else if (isPrimitive(source && source.valueOf())) {
return source;
}
// If the object is not of a known type, then simply merging its
// properties into a plain object will result in something different
// (it will not respond to instanceof operator etc). Similarly we don't
// want to call a constructor here as we can't know for sure what the
// original constructor was called with (Events etc), so throw an
// error here instead. Non-standard types can be handled if either they
// already exist and simply have their properties merged, if the merge
// is not deep so their references will simply be copied over, or if a
// resolve function is used to assist the merge.
throw new TypeError('Must be a basic data type');
}
function clone(source, deep) {
var target = getNewObjectForMerge(source);
return objectMerge(target, source, deep, true, true, true);
}
// Keys/Values
function objectSize(obj) {
return getKeysWithObjectCoercion(obj).length;
}
function getKeysWithObjectCoercion(obj) {
return getKeys(coercePrimitiveToObject(obj));
}
function getValues(obj) {
var values = [];
forEachProperty(obj, function(val) {
values.push(val);
});
return values;
}
function tap(obj, arg) {
var fn = arg;
if (!isFunction(arg)) {
fn = function() {
if (arg) obj[arg]();
};
}
fn.call(obj, obj);
return obj;
}
// Select/Reject
function objectSelect(obj, f) {
return selectFromObject(obj, f, true);
}
function objectReject(obj, f) {
return selectFromObject(obj, f, false);
}
function selectFromObject(obj, f, select) {
var match, result = {};
f = [].concat(f);
forEachProperty(obj, function(val, key) {
match = false;
for (var i = 0; i < f.length; i++) {
if (matchInObject(f[i], key)) {
match = true;
}
}
if (match === select) {
result[key] = val;
}
});
return result;
}
function matchInObject(match, key) {
if (isRegExp(match)) {
return match.test(key);
} else if (isObjectType(match)) {
return key in match;
} else {
return key === String(match);
}
}
// Remove/Exclude
function objectRemove(obj, f) {
var matcher = getMatcher(f);
forEachProperty(obj, function(val, key) {
if (matcher(val, key, obj)) {
delete obj[key];
}
});
return obj;
}
function objectExclude(obj, f) {
var result = {};
var matcher = getMatcher(f);
forEachProperty(obj, function(val, key) {
if (!matcher(val, key, obj)) {
result[key] = val;
}
});
return result;
}
function objectIntersectOrSubtract(obj1, obj2, subtract) {
if (!isObjectType(obj1)) {
return subtract ? obj1 : {};
}
obj2 = coercePrimitiveToObject(obj2);
function resolve(key, val, val1) {
var exists = key in obj2 && isEqual(val1, obj2[key]);
if (exists !== subtract) {
return val1;
}
}
return objectMerge({}, obj1, false, resolve);
}
/***
* @method is[Type](<obj>)
* @returns Boolean
* @short Returns true if <obj> is an object of that type.
*
* @set
* isArray
* isBoolean
* isDate
* isError
* isFunction
* isMap
* isNumber
* isRegExp
* isSet
* isString
*
* @example
*
* Object.isArray([3]) -> true
* Object.isNumber(3) -> true
* Object.isString(8) -> false
*
***/
function buildClassCheckMethods() {
var checks = [isBoolean, isNumber, isString, isDate, isRegExp, isFunction, isArray, isError, isSet, isMap];
defineInstanceAndStaticSimilar(sugarObject, NATIVE_TYPES, function(methods, name, i) {
methods['is' + name] = checks[i];
});
}
defineStatic(sugarObject, {
/***
* @method fromQueryString(<str>, [options])
* @returns Object
* @static
* @short Converts the query string of a URL into an object.
* @extra Options can be passed with [options] for more control over the result.
*
* @options
*
* deep If the string contains "deep" syntax `foo[]`, this will
* be automatically converted to an array. (Default `false`)
*
* auto If `true`, booleans `"true"` and `"false"`, numbers, and arrays
* (repeated keys) will be automatically cast to native
* values. (Default `true`)
*
* transform A function whose return value becomes the final value. If
* the function returns `undefined`, then the original value
* will be used. This allows the function to intercept only
* certain keys or values. (Default `undefined`)
*
* separator If passed, keys will be split on this string to extract
* deep values. (Default `''`)
*
* @callback transform
*
* key The key component of the query string (before `=`).
* val The value component of the query string (after `=`).
* obj A reference to the object being built.
*
* @example
*
* Object.fromQueryString('a=1&b=2') -> {a:1,b:2}
* Object.fromQueryString('a[]=1&a[]=2',{deep:true}) -> {a:['1','2']}
* Object.fromQueryString('a_b=c',{separator:'_'}) -> {a:{b:'c'}}
* Object.fromQueryString('id=123', {transform:idToNumber});
*
***/
'fromQueryString': function(obj, options) {
return fromQueryStringWithOptions(obj, options);
}
});
defineInstanceAndStatic(sugarObject, {
/***
* @method has(<obj>, <key>, [inherited] = false)
* @returns Boolean
* @short Checks if <obj> has property <key>.
* @extra Supports `deep properties`. If [inherited] is `true`,
* properties defined in the prototype chain will also return `true`.
* The default of `false` for this argument makes this method suited
* to working with objects as data stores by default.
*
* @example
*
* Object.has(usersByName, 'Harry') -> true
* Object.has(data, 'users[1].profile') -> true
* Object.has([], 'forEach') -> false
* Object.has([], 'forEach', true) -> true
*
***/
'has': function(obj, key, any) {
return deepHasProperty(obj, key, any);
},
/***
* @method get(<obj>, <key>, [inherited] = false)
* @returns Mixed
* @short Gets a property of <obj>.
* @extra Supports `deep properties`. If [inherited] is `true`,
* properties defined in the prototype chain will also be returned.
* The default of `false` for this argument makes this method suited
* to working with objects as data stores by default.
*
* @example
*
* Object.get(Harry, 'name'); -> 'Harry'
* Object.get(Harry, 'profile.likes'); -> Harry's likes
* Object.get(data, 'users[3].name') -> User 3's name
* Object.get(data, 'users[1..2]') -> Users 1 and 2
* Object.get(data, 'users[1..2].name') -> Names of users 1 and 2
* Object.get(data, 'users[-2..-1]') -> Last 2 users
*
***/
'get': function(obj, key, any) {
return deepGetProperty(obj, key, any);
},
/***
* @method set(<obj>, <key>, <val>)
* @returns Object
* @short Sets a property on <obj>.
* @extra Using a dot or square bracket in <key> is considered "deep" syntax,
* and will attempt to traverse into <obj> to set the property,
* creating properties that do not exist along the way. If the missing
* property is referenced using square brackets, an empty array will be
* created, otherwise an empty object. A special `[]` carries the
* meaning of "the last index + 1", and will effectively push <val>
* onto the end of the array. Lastly, a `..` separator inside the
* brackets is "range" notation, and will set properties on all
* elements in the specified range. Range members may be negative,
* which will be offset from the end of the array.
*
* @example
*
* Object.set({}, 'name', 'Harry'); -> {name:'Harry'}
* Object.set({}, 'user.name', 'Harry'); -> {user:{name:'Harry'}}
* Object.set({}, 'users[].name', 'Bob') -> {users:[{name:'Bob'}}
* Object.set({}, 'users[1].name','Bob') -> {users:[undefined, {name:'Bob'}]}
* Object.set({}, 'users[0..1].name','Bob') -> {users:[{name:'Bob'},{name:'Bob'}]}
*
***/
'set': function(obj, key, val) {
return deepSetProperty(obj, key, val);
},
/***
* @method size(<obj>)
* @returns Number
* @short Returns the number of properties in <obj>.
*
* @example
*
* Object.size({foo:'bar'}) -> 1
*
***/
'size': function(obj) {
return objectSize(obj);
},
/***
* @method isEmpty(<obj>)
* @returns Boolean
* @short Returns true if the number of properties in <obj> is zero.
*
* @example
*
* Object.isEmpty({}) -> true
* Object.isEmpty({a:1}) -> false
*
***/
'isEmpty': function(obj) {
return objectSize(obj) === 0;
},
/***
* @method toQueryString(<obj>, [options])
* @returns Object
* @short Converts the object into a query string.
* @extra Accepts deep objects and arrays. [options] can be passed for more
* control over the result.
*
* @options
*
* deep If `true`, non-standard "deep" syntax `foo[]` will be
* used for output. Note that `separator` will be ignored,
* as this option overrides shallow syntax. (Default `false`)
*
* prefix If passed, this string will be prefixed to all keys,
* separated by the `separator`. (Default `''`).
*
* transform A function whose return value becomes the final value
* in the string. (Default `undefined`)
*
* separator A string that is used to separate keys, either for deep
* objects, or when `prefix` is passed.(Default `_`).
*
* @callback transform
*
* key The key of the current iteration.
* val The value of the current iteration.
* obj A reference to the object.
*
* @example
*
* Object.toQueryString({foo:'bar'}) -> 'foo=bar'
* Object.toQueryString({foo:['a','b']}) -> 'foo=a&foo=b'
* Object.toQueryString({foo:['a','b']}, {deep:true}) -> 'foo[]=a&foo[]=b'
*
***/
'toQueryString': function(obj, options) {
return toQueryStringWithOptions(obj, options);
},
/***
* @method isEqual(<a>, <b>)
* @returns Boolean
* @short Returns true if <a> and <b> are equivalent.
* @extra If <a> and <b> are both built-in types, they will be considered
* equivalent if they are not "observably distinguishable". This means
* that primitives and object types, `0` and `-0`, and sparse and
* dense arrays are all not equal. Functions and non-built-ins like
* instances of user-defined classes and host objects like Element and
* Event are strictly compared `===`, and will only be equal if they
* are the same reference. Plain objects as well as Arrays will be
* traversed into and deeply checked by their non-inherited, enumerable
* properties. Other allowed types include Typed Arrays, Sets, Maps,
* Arguments, Dates, Regexes, and Errors.
*
* @example
*
* Object.isEqual({a:2}, {a:2}) -> true
* Object.isEqual({a:2}, {a:3}) -> false
* Object.isEqual(5, Object(5)) -> false
* Object.isEqual(Object(5), Object(5)) -> true
* Object.isEqual(NaN, NaN) -> false
*
***/
'isEqual': function(a, b) {
return isEqual(a, b);
},
/***
* @method merge(<target>, <source>, [options])
* @returns Merged object
* @short Merges properties from <source> into <target>.
* @extra This method will modify <target>! Use `add` for a non-destructive
* alias.
*
* @options
*
* deep If `true` deep properties are merged recursively.
* (Default `false`)
*
* hidden If `true`, non-enumerable properties will be merged as well.
* (Default `false`)
*
* descriptor If `true`, properties will be merged by property descriptor.
* Use this option to merge getters or setters, or to preserve
* `enumerable`, `configurable`, etc.
* (Default `false`)
*
* resolve Determines which property wins in the case of conflicts.
* If `true`, <source> wins. If `false`, <target> wins. If a
* function is passed, its return value will decide the result.
* Any non-undefined return value will resolve the conflict
* for that property (will not continue if `deep`). Returning
* `undefined` will do nothing (no merge). Finally, returning
* the global object `Sugar` will allow Sugar to handle the
* merge as normal. (Default `true`)
*
* @callback resolve
*
* key The key of the current iteration.
* targetVal The current value for the key in <target>.
* sourceVal The current value for the key in <source>.
* target The target object.
* source The source object.
*
* @example
*
* Object.merge({one:1},{two:2}) -> {one:1,two:2}
* Object.merge({one:1},{one:9,two:2}) -> {one:9,two:2}
* Object.merge({x:{a:1}},{x:{b:2}},{deep:true}) -> {x:{a:1,b:2}}
* Object.merge({a:1},{a:2},{resolve:mergeAdd}) -> {a:3}
*
***/
'merge': function(target, source, opts) {
return mergeWithOptions(target, source, opts);
},
/***
* @method mergeAll(<target>, <sources>, [options])
* @returns Merged object
* @short Merges properties from an array of <sources> into <target>.
* @extra This method will modify <target>! Use `addAll` for a non-destructive
* alias. See `merge` for options.
*
* @example
*
* Object.mergeAll({one:1},[{two:2},{three:3}]) -> {one:1,two:2,three:3}
* Object.mergeAll({x:{a:1}},[{x:{b:2}},{x:{c:3}}],{deep:true}) -> {x:{a:1,b:2,c:3}}
*
***/
'mergeAll': function(target, sources, opts) {
return mergeAll(target, sources, opts);
},
/***
* @method add(<obj1>, <obj2>, [options])
* @returns Object
* @short Adds properties in <obj2> to <obj1> and returns a new object.
* @extra This method will not modify <obj1>. See `merge` for options.
*
* @example
*
* Object.add({one:1},{two:2}) -> {one:1,two:2}
* Object.add({one:1},{one:9,two:2}) -> {one:9,two:2}
* Object.add({x:{a:1}},{x:{b:2}},{deep:true}) -> {x:{a:1,b:2}}
* Object.add({a:1},{a:2},{resolve:mergeAdd}) -> {a:3}
*
***/
'add': function(obj1, obj2, opts) {
return mergeWithOptions(clone(obj1), obj2, opts);
},
/***
* @method addAll(<obj>, <sources>, [options])
* @returns Merged object
* @short Adds properties from an array of <sources> to <obj> and returns a new object.
* @extra This method will not modify <obj>. See `merge` for options.
*
* @example
*
* Object.addAll({one:1},[{two:2},{three:3}]) -> {one:1,two:2,three:3}
* Object.addAll({x:{a:1}},[{x:{b:2}},{x:{c:3}}],{deep:true}) -> {x:{a:1,b:2,c:3}}
*
***/
'addAll': function(obj, sources, opts) {
return mergeAll(clone(obj), sources, opts);
},
/***
* @method intersect(<obj1>, <obj2>)
* @returns Object
* @short Returns a new object whose properties are those that both <obj1> and
* <obj2> have in common.
* @extra If both key and value do not match, then the property will not be included.
*
* @example
*
* Object.intersect({a:'a'},{b:'b'}) -> {}
* Object.intersect({a:'a'},{a:'b'}) -> {}
* Object.intersect({a:'a',b:'b'},{b:'b',z:'z'}) -> {b:'b'}
*
***/
'intersect': function(obj1, obj2) {
return objectIntersectOrSubtract(obj1, obj2, false);
},
/***
* @method subtract(<obj1>, <obj2>)
* @returns Object
* @short Returns a clone of <obj1> with any properties shared by <obj2> excluded.
* @extra If both key and value do not match, then the property will not be excluded.
*
* @example
*
* Object.subtract({a:'a',b:'b'},{b:'b'}) -> {a:'a'}
* Object.subtract({a:'a',b:'b'},{a:'b'}) -> {a:'a',b:'b'}
*
***/
'subtract': function(obj1, obj2) {
return objectIntersectOrSubtract(obj1, obj2, true);
},
/***
* @method defaults(<target>, <sources>, [options])
* @returns Merged object
* @short Merges properties from one or multiple <sources> into <target> while
* preserving <target>'s properties.
* @extra This method modifies <target>! See `merge` for options.
*
* @example
*
* Object.defaults({one:1},[{one:9},{two:2}]) -> {one:1,two:2}
* Object.defaults({x:{a:1}},[{x:{a:9}},{x:{b:2}}],{deep:true}) -> {x:{a:1,b:2}}
*
***/
'defaults': function(target, sources, opts) {
return defaults(target, sources, opts);
},
/***
* @method clone(<obj>, [deep] = false)
* @returns Cloned object
* @short Creates a clone of <obj>.
* @extra Default is a shallow clone, unless [deep] is true.
*
* @example
*
* Object.clone({foo:'bar'}) -> creates shallow clone
* Object.clone({foo:'bar'}, true) -> creates a deep clone
*
***/
'clone': function(obj, deep) {
return clone(obj, deep);
},
/***
* @method values(<obj>)
* @returns Array
* @short Returns an array containing the values in <obj>.
* @extra Values are in no particular order. Does not include inherited or
* non-enumerable properties.
*
* @example
*
* Object.values({a:'a',b:'b'}) -> ['a','b']
*
***/
'values': function(obj) {
return getValues(obj);
},
/***
* @method invert(<obj>, [multi] = false)
* @returns Object
* @short Creates a new object with the keys and values of <obj> swapped.
* @extra If [multi] is true, values will be an array of all keys, othewise
* collisions will be overwritten.
*
* @example
*
* Object.invert({foo:'bar'}) -> {bar:'foo'}
* Object.invert({a:1,b:1}, true) -> {1:['a','b']}
*
***/
'invert': function(obj, multi) {
var result = {};
multi = multi === true;
forEachProperty(obj, function(val, key) {
if (hasOwn(result, val) && multi) {
result[val].push(key);
} else if (multi) {
result[val] = [key];
} else {
result[val] = key;
}
});
return result;
},
/***
* @method tap(<obj>, <fn>)
* @returns Object
* @short Runs <fn> and returns <obj>.
* @extra A string can also be used as a shortcut to a method. This method is
* designed to run an intermediary function that "taps into" a method
* chain. As such, it is fairly useless as a static method. However it
* can be quite useful when combined with chainables.
*
* @callback
*
* obj A reference to <obj>.
*
* @example
*
* Sugar.Array([1,4,9]).map(Math.sqrt).tap('pop') -> [1,2]
* Sugar.Object({a:'a'}).tap(logArgs).merge({b:'b'}) -> {a:'a',b:'b'}
*
***/
'tap': function(obj, arg) {
return tap(obj, arg);
},
/***
* @method isArguments(<obj>)
* @returns Boolean
* @short Returns true if <obj> is an arguments object.
*
* @example
*
* Object.isArguments([1]) -> false
*
***/
'isArguments': function(obj) {
return isArguments(obj);
},
/***
* @method isObject(<obj>)
* @returns Boolean
* @short Returns true if <obj> is a "plain" object.
* @extra Plain objects do not include instances of classes or "host" objects,
* such as Elements, Events, etc.
*
* @example
*
* Object.isObject({ broken:'wear' }) -> true
*
***/
'isObject': function(obj) {
return isPlainObject(obj);
},
/***
* @method remove(<obj>, <search>)
* @returns Object
* @short Deletes all properties in <obj> matching <search>.
* @extra This method will modify <obj>!. Implements `enhanced matching`.
*
* @callback search
*
* key The key of the current iteration.
* val The value of the current iteration.
* obj A reference to the object.
*
* @example
*
* Object.remove({a:'a',b:'b'}, 'a'); -> {b:'b'}
* Object.remove({a:'a',b:'b',z:'z'}, /[a-f]/); -> {z:'z'}
*
***/
'remove': function(obj, f) {
return objectRemove(obj, f);
},
/***
* @method exclude(<obj>, <search>)
* @returns Object
* @short Returns a new object with all properties matching <search> removed.
* @extra This is a non-destructive version of `remove` and will not modify
* <obj>. Implements `enhanced matching`.
*
* @callback search
*
* key The key of the current iteration.
* val The value of the current iteration.
* obj A reference to the object.
*
* @example
*
* Object.exclude({a:'a',b:'b'}, 'a'); -> {b:'b'}
* Object.exclude({a:'a',b:'b',z:'z'}, /[a-f]/); -> {z:'z'}
*
***/
'exclude': function(obj, f) {
return objectExclude(obj, f);
},
/***
* @method select(<obj>, <find>)
* @returns Object
* @short Builds a new object containing the keys specified in <find>.
* @extra When <find> is a string, a single key will be selected. Arrays or
* objects match multiple keys, and a regex will match keys by regex.
*
* @example
*
* Object.select({a:1,b:2}, 'a') -> {a:1}
* Object.select({a:1,b:2}, ['a', 'b']) -> {a:1,b:2}
* Object.select({a:1,b:2}, /[a-z]/) -> {a:1,b:2}
* Object.select({a:1,b:2}, {a:'a',b:'b'}) -> {a:1,b:2}
*
***/
'select': function(obj, f) {
return objectSelect(obj, f);
},
/***
* @method reject(<obj>, <find>)
* @returns Object
* @short Builds a new object containing all keys except those in <find>.
* @extra When <find> is a string, a single key will be rejected. Arrays or
* objects match multiple keys, and a regex will match keys by regex.
*
* @example
*
* Object.reject({a:1,b:2}, 'a') -> {b:2}
* Object.reject({a:1,b:2}, /[a-z]/) -> {}
* Object.reject({a:1,b:2}, {a:'a'}) -> {b:2}
* Object.reject({a:1,b:2}, ['a', 'b']) -> {}
*
***/
'reject': function(obj, f) {
return objectReject(obj, f);
}
});
defineInstance(sugarObject, {
/***
* @method keys(<obj>)
* @returns Array
* @polyfill ES5
* @short Returns an array containing the keys of all of the non-inherited,
* enumerable properties of <obj>.
*
* @example
*
* Object.keys({a:'a',b:'b'}) -> ['a','b']
*
***/
'keys': function(obj) {
return getKeys(obj);
}
});
buildClassCheckMethods();
/***
* @module Enumerable
* @description Counting, mapping, and finding methods on both arrays and objects.
*
***/
function sum(obj, map) {
var sum = 0;
enumerateWithMapping(obj, map, function(val) {
sum += val;
});
return sum;
}
function average(obj, map) {
var sum = 0, count = 0;
enumerateWithMapping(obj, map, function(val) {
sum += val;
count++;
});
// Prevent divide by 0
return sum / (count || 1);
}
function median(obj, map) {
var result = [], middle, len;
enumerateWithMapping(obj, map, function(val) {
result.push(val);
});
len = result.length;
if (!len) return 0;
result.sort(function(a, b) {
// IE7 will throw errors on non-numbers!
return (a || 0) - (b || 0);
});
middle = trunc(len / 2);
return len % 2 ? result[middle] : (result[middle - 1] + result[middle]) / 2;
}
function getMinOrMax(obj, arg1, arg2, max, asObject) {
var result = [], pushVal, edge, all, map;
if (isBoolean(arg1)) {
all = arg1;
map = arg2;
} else {
map = arg1;
}
enumerateWithMapping(obj, map, function(val, key) {
if (isUndefined(val)) {
throw new TypeError('Cannot compare with undefined');
}
pushVal = asObject ? key : obj[key];
if (val === edge) {
result.push(pushVal);
} else if (isUndefined(edge) || (max && val > edge) || (!max && val < edge)) {
result = [pushVal];
edge = val;
}
});
return getReducedMinMaxResult(result, obj, all, asObject);
}
function getLeastOrMost(obj, arg1, arg2, most, asObject) {
var group = {}, refs = [], minMaxResult, result, all, map;
if (isBoolean(arg1)) {
all = arg1;
map = arg2;
} else {
map = arg1;
}
enumerateWithMapping(obj, map, function(val, key) {
var groupKey = serializeInternal(val, refs);
var arr = getOwn(group, groupKey) || [];
arr.push(asObject ? key : obj[key]);
group[groupKey] = arr;
});
minMaxResult = getMinOrMax(group, !!all, 'length', most, true);
if (all) {
result = [];
// Flatten result
forEachProperty(minMaxResult, function(val) {
result = result.concat(val);
});
} else {
result = getOwn(group, minMaxResult);
}
return getReducedMinMaxResult(result, obj, all, asObject);
}
// Support
function getReducedMinMaxResult(result, obj, all, asObject) {
if (asObject && all) {
// The method has returned an array of keys so use this array
// to build up the resulting object in the form we want it in.
return result.reduce(function(o, key) {
o[key] = obj[key];
return o;
}, {});
} else if (result && !all) {
result = result[0];
}
return result;
}
function enumerateWithMapping(obj, map, fn) {
var arrayIndexes = isArray(obj);
forEachProperty(obj, function(val, key) {
if (arrayIndexes) {
if (!isArrayIndex(key)) {
return;
}
key = +key;
}
var mapped = mapWithShortcuts(val, map, obj, [val, key, obj]);
fn(mapped, key);
});
}
/*** @namespace Array ***/
// Flag allowing native array methods to be enhanced
var ARRAY_ENHANCEMENTS_FLAG = 'enhanceArray';
// Enhanced map function
var enhancedMap = buildEnhancedMapping('map');
// Enhanced matcher methods
var enhancedFind = buildEnhancedMatching('find'),
enhancedSome = buildEnhancedMatching('some'),
enhancedEvery = buildEnhancedMatching('every'),
enhancedFilter = buildEnhancedMatching('filter'),
enhancedFindIndex = buildEnhancedMatching('findIndex');
function arrayNone() {
return !enhancedSome.apply(this, arguments);
}
function arrayCount(arr, f) {
if (isUndefined(f)) {
return arr.length;
}
return enhancedFilter.apply(this, arguments).length;
}
// Enhanced methods
function buildEnhancedMapping(name) {
return wrapNativeArrayMethod(name, enhancedMapping);
}
function buildEnhancedMatching(name) {
return wrapNativeArrayMethod(name, enhancedMatching);
}
function enhancedMapping(map, context) {
if (isFunction(map)) {
return map;
} else if (map) {
return function(el, i, arr) {
return mapWithShortcuts(el, map, context, [el, i, arr]);
};
}
}
function enhancedMatching(f) {
var matcher;
if (isFunction(f)) {
return f;
}
matcher = getMatcher(f);
return function(el, i, arr) {
return matcher(el, i, arr);
};
}
function wrapNativeArrayMethod(methodName, wrapper) {
var nativeFn = Array.prototype[methodName];
return function(arr, f, context, argsLen) {
var args = new Array(2);
assertArgument(argsLen > 0);
args[0] = wrapper(f, context);
args[1] = context;
return nativeFn.apply(arr, args);
};
}
/***
* @method [fn]FromIndex(<startIndex>, [loop], ...)
* @returns Mixed
* @short Runs native array functions beginning from <startIndex>.
* @extra If [loop] is `true`, once the end of the array has been reached,
* iteration will continue from the start of the array up to
* `startIndex - 1`. If [loop] is false it can be omitted. Standard
* arguments are then passed which will be forwarded to the native
* methods. When available, methods are always `enhanced`. This includes
* `deep properties` for `map`, and `enhanced matching` for `some`,
* `every`, `filter`, `find`, and `findIndex`. Note also that
* `forEachFromIndex` is optimized for sparse arrays and may be faster
* than native `forEach`.
*
* @set
* mapFromIndex
* forEachFromIndex
* filterFromIndex
* someFromIndex
* everyFromIndex
* reduceFromIndex
* reduceRightFromIndex
* findFromIndex
* findIndexFromIndex
*
* @example
*
* users.mapFromIndex(2, 'name');
* users.mapFromIndex(2, true, 'name');
* names.forEachFromIndex(10, log);
* names.everyFromIndex(15, /^[A-F]/);
*
***/
function buildFromIndexMethods() {
var methods = {
'forEach': {
base: forEachAsNative
},
'map': {
wrapper: enhancedMapping
},
'some every': {
wrapper: enhancedMatching
},
'findIndex': {
wrapper: enhancedMatching,
result: indexResult
},
'reduce': {
apply: applyReduce
},
'filter find': {
wrapper: enhancedMatching
},
'reduceRight': {
apply: applyReduce,
slice: sliceArrayFromRight,
clamp: clampStartIndexFromRight
}
};
forEachProperty(methods, function(opts, key) {
forEach(spaceSplit(key), function(baseName) {
var methodName = baseName + 'FromIndex';
var fn = createFromIndexWithOptions(baseName, opts);
defineInstanceWithArguments(sugarArray, methodName, fn);
});
});
function forEachAsNative(fn) {
forEach(this, fn);
}
// Methods like filter and find have a direct association between the value
// returned by the callback and the element of the current iteration. This
// means that when looping, array elements must match the actual index for
// which they are being called, so the array must be sliced. This is not the
// case for methods like forEach and map, which either do not use return
// values or use them in a way that simply getting the element at a shifted
// index will not affect the final return value. However, these methods will
// still fail on sparse arrays, so always slicing them here. For example, if
// "forEachFromIndex" were to be called on [1,,2] from index 1, although the
// actual index 1 would itself would be skipped, when the array loops back to
// index 0, shifting it by adding 1 would result in the element for that
// iteration being undefined. For shifting to work, all gaps in the array
// between the actual index and the shifted index would have to be accounted
// for. This is infeasible and is easily solved by simply slicing the actual
// array instead so that gaps align. Note also that in the case of forEach,
// we are using the internal function which handles sparse arrays in a way
// that does not increment the index, and so is highly optimized compared to
// the others here, which are simply going through the native implementation.
function sliceArrayFromLeft(arr, startIndex, loop) {
var result = arr;
if (startIndex) {
result = arr.slice(startIndex);
if (loop) {
result = result.concat(arr.slice(0, startIndex));
}
}
return result;
}
// When iterating from the right, indexes are effectively shifted by 1.
// For example, iterating from the right from index 2 in an array of 3
// should also include the last element in the array. This matches the
// "lastIndexOf" method which also iterates from the right.
function sliceArrayFromRight(arr, startIndex, loop) {
if (!loop) {
startIndex += 1;
arr = arr.slice(0, max(0, startIndex));
}
return arr;
}
function clampStartIndex(startIndex, len) {
return min(len, max(0, startIndex));
}
// As indexes are shifted by 1 when starting from the right, clamping has to
// go down to -1 to accommodate the full range of the sliced array.
function clampStartIndexFromRight(startIndex, len) {
return min(len, max(-1, startIndex));
}
function applyReduce(arr, startIndex, fn, context, len, loop) {
return function(acc, val, i) {
i = getNormalizedIndex(i + startIndex, len, loop);
return fn.call(arr, acc, val, i, arr);
};
}
function applyEach(arr, startIndex, fn, context, len, loop) {
return function(el, i) {
i = getNormalizedIndex(i + startIndex, len, loop);
return fn.call(context, arr[i], i, arr);
};
}
function indexResult(result, startIndex, len) {
if (result !== -1) {
result = (result + startIndex) % len;
}
return result;
}
function createFromIndexWithOptions(methodName, opts) {
var baseFn = opts.base || Array.prototype[methodName],
applyCallback = opts.apply || applyEach,
sliceArray = opts.slice || sliceArrayFromLeft,
clampIndex = opts.clamp || clampStartIndex,
getResult = opts.result,
wrapper = opts.wrapper;
return function(arr, startIndex, args) {
var callArgs = [], argIndex = 0, lastArg, result, len, loop, fn;
len = arr.length;
if (isBoolean(args[0])) {
loop = args[argIndex++];
}
fn = args[argIndex++];
lastArg = args[argIndex];
if (startIndex < 0) {
startIndex += len;
}
startIndex = clampIndex(startIndex, len);
assertArgument(args.length);
fn = wrapper ? wrapper(fn, lastArg) : fn;
callArgs.push(applyCallback(arr, startIndex, fn, lastArg, len, loop));
if (lastArg) {
callArgs.push(lastArg);
}
result = baseFn.apply(sliceArray(arr, startIndex, loop), callArgs);
if (getResult) {
result = getResult(result, startIndex, len);
}
return result;
};
}
}
defineInstance(sugarArray, {
/***
* @method map(<map>, [context])
* @returns Array
* @polyfill ES5
* @short Maps the array to another array whose elements are the values
* returned by the <map> callback.
* @extra [context] is the `this` object. Sugar enhances this method to accept
* a string for <map>, which is a shortcut for a function that gets
* a property or invokes a function on each element.
* Supports `deep properties`.
*
* @callback map
*
* el The element of the current iteration.
* i The index of the current iteration.
* arr A reference to the array.
*
* @example
*
* [1,2,3].map(function(n) {
* return n * 3;
* }); -> [3,6,9]
*
* ['a','aa','aaa'].map('length') -> [1,2,3]
* ['A','B','C'].map('toLowerCase') -> ['a','b','c']
* users.map('name') -> array of user names
*
***/
'map': fixArgumentLength(enhancedMap),
/***
* @method some(<search>, [context])
* @returns Boolean
* @polyfill ES5
* @short Returns true if <search> is true for any element in the array.
* @extra [context] is the `this` object. Implements `enhanced matching`.
*
* @callback search
*
* el The element of the current iteration.
* i The index of the current iteration.
* arr A reference to the array.
*
* @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
* users.some({ name: /^H/ }) -> true if any have a name starting with H
*
***/
'some': fixArgumentLength(enhancedSome),
/***
* @method every(<search>, [context])
* @returns Boolean
* @polyfill ES5
* @short Returns true if <search> is true for all elements of the array.
* @extra [context] is the `this` object. Implements `enhanced matching`.
*
* @callback search
*
* el The element of the current iteration.
* i The index of the current iteration.
* arr A reference to the array.
*
* @example
*
* ['a','a','a'].every(function(n) {
* return n == 'a';
* });
* ['a','a','a'].every('a') -> true
* [{a:2},{a:2}].every({a:2}) -> true
* users.every({ name: /^H/ }) -> true if all have a name starting with H
*
***/
'every': fixArgumentLength(enhancedEvery),
/***
* @method filter(<search>, [context])
* @returns Array
* @polyfill ES5
* @short Returns any elements in the array that match <search>.
* @extra [context] is the `this` object. Implements `enhanced matching`.
*
* @callback search
*
* el The element of the current iteration.
* i The index of the current iteration.
* arr A reference to the array.
*
* @example
*
* [1,2,3].filter(function(n) {
* return n > 1;
* });
* [1,2,2,4].filter(2) -> 2
* users.filter({ name: /^H/ }) -> all users with a name starting with H
*
***/
'filter': fixArgumentLength(enhancedFilter),
/***
* @method find(<search>, [context])
* @returns Mixed
* @polyfill ES6
* @short Returns the first element in the array that matches <search>.
* @extra Implements `enhanced matching`.
*
* @callback search
*
* el The element of the current iteration.
* i The index of the current iteration.
* arr A reference to the array.
*
* @example
*
* users.find(function(user) {
* return user.name = 'Harry';
* }); -> harry!
*
* users.find({ name: 'Harry' }); -> harry!
* users.find({ name: /^[A-H]/ }); -> First user with name starting with A-H
* users.find({ titles: ['Ms', 'Dr'] }); -> not harry!
*
*
***/
'find': fixArgumentLength(enhancedFind),
/***
* @method findIndex(<search>, [context])
* @returns Number
* @polyfill ES6
* @short Returns the index of the first element in the array that matches
* <search>, or `-1` if none.
* @extra [context] is the `this` object. Implements `enhanced matching`.
*
* @callback search
*
* el The element of the current iteration.
* i The index of the current iteration.
* arr A reference to the array.
*
* @example
*
* [1,2,3,4].findIndex(function(n) {
* return n % 2 == 0;
* }); -> 1
* ['a','b','c'].findIndex('c'); -> 2
* ['cuba','japan','canada'].find(/^c/) -> 0
*
***/
'findIndex': fixArgumentLength(enhancedFindIndex)
}, [ENHANCEMENTS_FLAG, ARRAY_ENHANCEMENTS_FLAG]);
defineInstance(sugarArray, {
/***
* @method none(<search>, [context])
*
* @returns Boolean
* @short Returns true if none of the elements in the array match <search>.
* @extra [context] is the `this` object. Implements `enhanced matching`.
*
* @callback search
*
* el The element of the current iteration.
* i The index of the current iteration.
* arr A reference to the array.
*
* @example
*
* [1,2,3].none(5) -> true
* ['a','b','c'].none(/b/) -> false
* users.none(function(user) {
* return user.name == 'Wolverine';
* }); -> probably true
* users.none({ name: 'Wolverine' }); -> same as above
*
***/
'none': fixArgumentLength(arrayNone),
/***
* @method count(<search>)
* @returns Number
* @short Counts all elements in the array that match <search>.
* @extra Implements `enhanced matching`.
*
* @callback search
*
* el The element of the current iteration.
* i The index of the current iteration.
* arr A reference to the array.
*
* @example
*
* ['a','b','a'].count('a') -> 2
* ['a','b','c'].count(/b/) -> 1
* users.count(function(user) {
* return user.age > 30;
* }); -> number of users older than 30
*
***/
'count': fixArgumentLength(arrayCount),
/***
* @method min([all] = false, [map])
* @returns Mixed
* @short Returns the element in the array with the lowest value.
* @extra [map] may be passed in place of [all], and is a function mapping the
* value to be checked or a string acting as a shortcut. If [all] is
* true, multiple elements will be returned. Supports `deep properties`.
*
* @callback map
*
* el The element of the current iteration.
* i The index of the current iteration.
* arr A reference to the array.
*
* @example
*
* [1,2,3].min() -> 1
* ['fee','fo','fum'].min('length') -> 'fo'
* ['fee','fo','fum'].min(true, 'length') -> ['fo']
* users.min('age') -> youngest guy!
*
* ['fee','fo','fum'].min(true, function(n) {
* return n.length;
* }); -> ['fo']
*
*
***/
'min': function(arr, all, map) {
return getMinOrMax(arr, all, map);
},
/***
* @method max([all] = false, [map])
* @returns Mixed
* @short Returns the element in the array with the greatest value.
* @extra [map] may be passed in place of [all], and is a function mapping the
* value to be checked or a string acting as a shortcut. If [all] is
* true, multiple elements will be returned. Supports `deep properties`.
*
* @callback map
*
* el The element of the current iteration.
* i The index of the current iteration.
* arr A reference to the array.
*
* @example
*
* [1,2,3].max() -> 3
* ['fee','fo','fum'].max('length') -> 'fee'
* ['fee','fo','fum'].max(true, 'length') -> ['fee','fum']
* users.max('age') -> oldest guy!
*
* ['fee','fo','fum'].max(true, function(n) {
* return n.length;
* }); -> ['fee', 'fum']
*
***/
'max': function(arr, all, map) {
return getMinOrMax(arr, all, map, true);
},
/***
* @method least([all] = false, [map])
* @returns Array
* @short Returns the elements in the array with the least commonly occuring value.
* @extra [map] may be passed in place of [all], and is a function mapping the
* value to be checked or a string acting as a shortcut. If [all] is
* true, will return multiple values in an array.
* Supports `deep properties`.
*
* @callback map
*
* el The element of the current iteration.
* i The index of the current iteration.
* arr A reference to the array.
*
* @example
*
* [3,2,2].least() -> 3
* ['fe','fo','fum'].least(true, 'length') -> ['fum']
* users.least('profile.type') -> (user with least commonly occurring type)
* users.least(true, 'profile.type') -> (users with least commonly occurring type)
*
***/
'least': function(arr, all, map) {
return getLeastOrMost(arr, all, map);
},
/***
* @method most([all] = false, [map])
* @returns Array
* @short Returns the elements in the array with the most commonly occuring value.
* @extra [map] may be passed in place of [all], and is a function mapping the
* value to be checked or a string acting as a shortcut. If [all] is
* true, will return multiple values in an array.
* Supports `deep properties`.
*
* @callback map
*
* el The element of the current iteration.
* i The index of the current iteration.
* arr A reference to the array.
*
* @example
*
* [3,2,2].most(2) -> 2
* ['fe','fo','fum'].most(true, 'length') -> ['fe','fo']
* users.most('profile.type') -> (user with most commonly occurring type)
* users.most(true, 'profile.type') -> (users with most commonly occurring type)
*
***/
'most': function(arr, all, map) {
return getLeastOrMost(arr, all, map, true);
},
/***
* @method sum([map])
* @returns Number
* @short Sums all values in the array.
* @extra [map] may be a function mapping the value to be summed or a string
* acting as a shortcut.
*
* @callback map
*
* el The element of the current iteration.
* i The index of the current iteration.
* arr A reference to the array.
*
* @example
*
* [1,2,2].sum() -> 5
* users.sum(function(user) {
* return user.votes;
* }); -> total votes!
* users.sum('votes') -> total votes!
*
***/
'sum': function(arr, map) {
return sum(arr, map);
},
/***
* @method average([map])
* @returns Number
* @short Gets the mean average for all values in the array.
* @extra [map] may be a function mapping the value to be averaged or a string
* acting as a shortcut. Supports `deep properties`.
*
* @callback map
*
* el The element of the current iteration.
* i The index of the current iteration.
* arr A reference to the array.
*
* @example
*
* [1,2,3,4].average() -> 2
* users.average(function(user) {
* return user.age;
* }); -> average user age
* users.average('age') -> average user age
* users.average('currencies.usd.balance') -> average USD balance
*
***/
'average': function(arr, map) {
return average(arr, map);
},
/***
* @method median([map])
* @returns Number
* @short Gets the median average for all values in the array.
* @extra [map] may be a function mapping the value to be averaged or a string
* acting as a shortcut.
*
* @callback map
*
* el The element of the current iteration.
* i The index of the current iteration.
* arr A reference to the array.
*
* @example
*
* [1,2,2].median() -> 2
* [{a:1},{a:2},{a:2}].median('a') -> 2
* users.median('age') -> median user age
* users.median('currencies.usd.balance') -> median USD balance
*
***/
'median': function(arr, map) {
return median(arr, map);
}
});
/*** @namespace Object ***/
// Object matchers
var objectSome = wrapObjectMatcher('some'),
objectFind = wrapObjectMatcher('find'),
objectEvery = wrapObjectMatcher('every');
function objectForEach(obj, fn) {
assertCallable(fn);
forEachProperty(obj, function(val, key) {
fn(val, key, obj);
});
return obj;
}
function objectMap(obj, map) {
var result = {};
forEachProperty(obj, function(val, key) {
result[key] = mapWithShortcuts(val, map, obj, [val, key, obj]);
});
return result;
}
function objectReduce(obj, fn, acc) {
var init = isDefined(acc);
forEachProperty(obj, function(val, key) {
if (!init) {
acc = val;
init = true;
return;
}
acc = fn(acc, val, key, obj);
});
return acc;
}
function objectNone(obj, f) {
return !objectSome(obj, f);
}
function objectFilter(obj, f) {
var matcher = getMatcher(f), result = {};
forEachProperty(obj, function(val, key) {
if (matcher(val, key, obj)) {
result[key] = val;
}
});
return result;
}
function objectCount(obj, f) {
var matcher = getMatcher(f), count = 0;
forEachProperty(obj, function(val, key) {
if (matcher(val, key, obj)) {
count++;
}
});
return count;
}
// Support
function wrapObjectMatcher(name) {
var nativeFn = Array.prototype[name];
return function(obj, f) {
var matcher = getMatcher(f);
return nativeFn.call(getKeys(obj), function(key) {
return matcher(obj[key], key, obj);
});
};
}
defineInstanceAndStatic(sugarObject, {
/***
* @method forEach(<obj>, <fn>)
* @returns Object
* @short Runs <fn> against each property in the object.
* @extra Does not iterate over inherited or non-enumerable properties.
*
* @callback fn
*
* val The value of the current iteration.
* key The key of the current iteration.
* obj A reference to the object.
*
* @example
*
* Object.forEach({a:'b'}, function(val, key) {
* // val = 'b', key = a
* });
*
***/
'forEach': function(obj, fn) {
return objectForEach(obj, fn);
},
/***
* @method map(<obj>, <map>)
* @returns Object
* @short Maps the object to another object whose properties are the values
* returned by <map>.
* @extra <map> can also be a string, which is a shortcut for a function that
* gets that property (or invokes a function) on each element.
* Supports `deep properties`.
*
* @callback map
*
* val The value of the current property.
* key The key of the current property.
* obj A reference to the object.
*
* @example
*
* Object.map({a:'b'}, function(val, key) {
* return key;
* }); -> {a:'b'}
* Object.map(usersByName, 'age');
*
***/
'map': function(obj, map) {
return objectMap(obj, map);
},
/***
* @method some(<obj>, <search>)
* @returns Boolean
* @short Returns true if <search> is true for any property in the object.
* @extra Implements `enhanced matching`.
*
* @callback search
*
* val The value of the current iteration.
* key The key of the current iteration.
* obj A reference to the object.
*
* @example
*
* Object.some({a:1,b:2}, function(val) {
* return val == 1;
* }); -> true
* Object.some({a:1,b:2}, 1); -> true
*
***/
'some': objectSome,
/***
* @method every(<obj>, <search>)
* @returns Boolean
* @short Returns true if <search> is true for all properties in the object.
* @extra Implements `enhanced matching`.
*
* @callback search
*
* val The value of the current iteration.
* key The key of the current iteration.
* obj A reference to the object.
*
* @example
*
* Object.every({a:1,b:2}, function(val) {
* return val > 0;
* }); -> true
* Object.every({a:'a',b:'b'}, /[a-z]/); -> true
*
***/
'every': objectEvery,
/***
* @method filter(<obj>, <search>)
* @returns Array
* @short Returns a new object with properties that match <search>.
* @extra Implements `enhanced matching`.
*
* @callback search
*
* val The value of the current iteration.
* key The key of the current iteration.
* obj A reference to the object.
*
* @example
*
* Object.filter({a:1,b:2}, function(val) {
* return val == 1;
* }); -> {a:1}
* Object.filter({a:'a',z:'z'}, /[a-f]/); -> {a:'a'}
* Object.filter(usersByName, /^H/); -> all users with names starting with H
*
***/
'filter': function(obj, f) {
return objectFilter(obj, f);
},
/***
* @method reduce(<obj>, <fn>, [init])
* @returns Mixed
* @short Reduces the object to a single result.
* @extra This operation is sometimes called "accumulation", as it takes the
* result of the last iteration of <fn> and passes it as the first
* argument to the next iteration, "accumulating" that value as it goes.
* The return value of this method will be the return value of the final
* iteration of <fn>. If [init] is passed, it will be the initial
* "accumulator" (the first argument). If [init] is not passed, then a
* property of the object will be used instead and <fn> will not be
* called for that property. Note that object properties have no order,
* and this may lead to bugs (for example if performing division or
* subtraction operations on a value). If order is important, use an
* array instead!
*
* @callback fn
*
* acc The "accumulator", either [init], the result of the last iteration
* of <fn>, or a property of <obj>.
* val The value of the current property called for <fn>.
* key The key of the current property called for <fn>.
* obj A reference to the object.
*
* @example
*
* Object.reduce({a:2,b:4}, function(a, b) {
* return a * b;
* }); -> 8
*
* Object.reduce({a:2,b:4}, function(a, b) {
* return a * b;
* }, 10); -> 80
*
*
***/
'reduce': function(obj, fn, init) {
return objectReduce(obj, fn, init);
},
/***
* @method find(<obj>, <search>)
* @returns Boolean
* @short Returns the first key whose value matches <search>.
* @extra Implements `enhanced matching`. Note that "first" is
* implementation-dependent. If order is important an array should be
* used instead.
*
* @callback search
*
* val The value of the current iteration.
* key The key of the current iteration.
* obj A reference to the object.
*
* @example
*
* Object.find({a:1,b:2}, function(val) {
* return val == 2;
* }); -> 'b'
* Object.find({a:'a',b:'b'}, /[a-z]/); -> 'a'
*
***/
'find': objectFind,
/***
* @method count(<obj>, <search>)
* @returns Number
* @short Counts all properties in the object that match <search>.
* @extra Implements `enhanced matching`.
*
* @callback search
*
* val The value of the current iteration.
* key The key of the current iteration.
* obj A reference to the object.
*
* @example
*
* Object.count({a:'a',b:'b',c:'a'}, 'a') -> 2
* Object.count(usersByName, function(user) {
* return user.age > 30;
* }); -> number of users older than 30
* Object.count(usersByName, { name: /^[H-Z]/ });
*
***/
'count': function(obj, f) {
return objectCount(obj, f);
},
/***
* @method none(<obj>, <search>)
* @returns Boolean
* @short Returns true if none of the properties in the object match <search>.
* @extra Implements `enhanced matching`.
*
* @callback search
*
* val The value of the current iteration.
* key The key of the current iteration.
* obj A reference to the object.
*
* @example
*
* Object.none({a:1,b:2}, 3); -> true
* Object.none(usersByName, function(user) {
* return user.name == 'Wolverine';
* }); -> probably true
*
***/
'none': function(obj, f) {
return objectNone(obj, f);
},
/***
* @method sum(<obj>, [map])
* @returns Number
* @short Sums all properties in the object.
* @extra [map] may be a function mapping the value to be summed or a string
* acting as a shortcut.
*
* @callback map
*
* val The value of the current iteration.
* key The key of the current iteration.
* obj A reference to the object.
*
* @example
*
* Object.sum({a:35,b:13}); -> 48
* Object.sum(usersByName, function(user) {
* return user.votes;
* }); -> total user votes
*
***/
'sum': function(obj, map) {
return sum(obj, map);
},
/***
* @method average(<obj>, [map])
* @returns Number
* @short Gets the mean average of all properties in the object.
* @extra [map] may be a function mapping the value to be averaged or a string
* acting as a shortcut.
*
* @callback map
*
* val The value of the current iteration.
* key The key of the current iteration.
* obj A reference to the object.
*
* @example
*
* Object.average({a:35,b:11}); -> 23
* Object.average(usersByName, 'age'); -> average user age
* Object.average(usersByName, 'currencies.usd.balance'); -> USD mean balance
*
***/
'average': function(obj, map) {
return average(obj, map);
},
/***
* @method median(<obj>, [map])
* @returns Number
* @short Gets the median average of all properties in the object.
* @extra [map] may be a function mapping the value to be averaged or a string
* acting as a shortcut.
*
* @callback map
*
* val The value of the current iteration.
* key The key of the current iteration.
* obj A reference to the object.
*
* @example
*
* Object.median({a:1,b:2,c:2}) -> 2
* Object.median(usersByName, 'age'); -> median user age
* Object.median(usersByName, 'currencies.usd.balance'); -> USD median balance
*
***/
'median': function(obj, map) {
return median(obj, map);
},
/***
* @method min(<obj>, [all] = false, [map])
* @returns Mixed
* @short Returns the key of the property in the object with the lowest value.
* @extra If [all] is true, will return an object with all properties in the
* object with the lowest value. [map] may be passed in place of [all]
* and is a function mapping the value to be checked or a string acting
* as a shortcut.
*
* @callback map
*
* val The value of the current iteration.
* key The key of the current iteration.
* obj A reference to the object.
*
* @example
*
* Object.min({a:1,b:2,c:3}) -> 'a'
* Object.min({a:'aaa',b:'bb',c:'c'}, 'length') -> 'c'
* Object.min({a:1,b:1,c:3}, true) -> {a:1,b:1}
*
***/
'min': function(obj, all, map) {
return getMinOrMax(obj, all, map, false, true);
},
/***
* @method max(<obj>, [all] = false, [map])
* @returns Mixed
* @short Returns the key of the property in the object with the highest value.
* @extra If [all] is true, will return an object with all properties in the
* object with the highest value. [map] may be passed in place of [all]
* and is a function mapping the value to be checked or a string acting
* as a shortcut.
*
* @callback map
*
* val The value of the current iteration.
* key The key of the current iteration.
* obj A reference to the object.
*
* @example
*
* Object.max({a:1,b:2,c:3}) -> 'c'
* Object.max({a:'aaa',b:'bb',c:'c'}, 'length') -> 'a'
* Object.max({a:1,b:3,c:3}, true) -> {b:3,c:3}
*
***/
'max': function(obj, all, map) {
return getMinOrMax(obj, all, map, true, true);
},
/***
* @method least(<obj>, [all] = false, [map])
* @returns Mixed
* @short Returns the key of the property in the object with the least commonly
* occuring value.
* @extra If [all] is true, will return an object with all properties in the
* object with the least common value. [map] may be passed in place of
* [all] and is a function mapping the value to be checked or a string
* acting as a shortcut.
*
* @callback map
*
* val The value of the current iteration.
* key The key of the current iteration.
* obj A reference to the object.
*
* @example
*
* Object.least({a:1,b:3,c:3}) -> 'a'
* Object.least({a:'aa',b:'bb',c:'c'}, 'length') -> 'c'
* Object.least({a:1,b:3,c:3}, true) -> {a:1}
*
***/
'least': function(obj, all, map) {
return getLeastOrMost(obj, all, map, false, true);
},
/***
* @method most(<obj>, [all] = false, [map])
* @returns Mixed
* @short Returns the key of the property in the object with the most commonly
* occuring value.
* @extra If [all] is true, will return an object with all properties in the
* object with the most common value. [map] may be passed in place of
* [all] and is a function mapping the value to be checked or a string
* acting as a shortcut.
*
* @callback map
*
* val The value of the current iteration.
* key The key of the current iteration.
* obj A reference to the object.
*
* @example
*
* Object.most({a:1,b:3,c:3}) -> 'b'
* Object.most({a:'aa',b:'bb',c:'c'}, 'length') -> 'a'
* Object.most({a:1,b:3,c:3}, true) -> {b:3,c:3}
*
***/
'most': function(obj, all, map) {
return getLeastOrMost(obj, all, map, true, true);
}
});
buildFromIndexMethods();
/***
* @module Number
* @description Number formatting, precision rounding, Math aliases, and more.
*
***/
var NUMBER_OPTIONS = {
'decimal': HALF_WIDTH_PERIOD,
'thousands': HALF_WIDTH_COMMA
};
// Abbreviation Units
var BASIC_UNITS = '|kmbt',
MEMORY_UNITS = '|KMGTPE',
MEMORY_BINARY_UNITS = '|,Ki,Mi,Gi,Ti,Pi,Ei',
METRIC_UNITS_SHORT = 'nμm|k',
METRIC_UNITS_FULL = 'yzafpnμm|KMGTPEZY';
/***
* @method getOption(<name>)
* @returns Mixed
* @accessor
* @short Gets an option used interally by Number.
* @options
*
* decimal A string used as the decimal marker by `format`, `abbr`,
* `metric`, and `bytes`. Default is `.`.
*
* thousands A string used as the thousands marker by `format`, `abbr`,
* `metric`, and `bytes`. Default is `,`.
*
* @example
*
* Sugar.Number.getOption('thousands');
*
***
* @method setOption(<name>, <value>)
* @accessor
* @short Sets an option used interally by Number.
* @extra If <value> is `null`, the default value will be restored.
* @options
*
* decimal A string used as the decimal marker by `format`, `abbr`,
* `metric`, and `bytes`. Default is `.`.
*
* thousands A string used as the thousands marker by `format`, `abbr`,
* `metric`, and `bytes`. Default is `,`.
*
*
* @example
*
* Sugar.Number.setOption('decimal', ',');
* Sugar.Number.setOption('thousands', ' ');
*
***/
var _numberOptions = defineOptionsAccessor(sugarNumber, NUMBER_OPTIONS);
function abbreviateNumber(num, precision, ustr, bytes) {
var fixed = num.toFixed(20),
decimalPlace = fixed.search(/\./),
numeralPlace = fixed.search(/[1-9]/),
significant = decimalPlace - numeralPlace,
units, unit, mid, i, divisor;
if (significant > 0) {
significant -= 1;
}
units = commaSplit(ustr);
if (units.length === 1) {
units = ustr.split('');
}
mid = units.indexOf('|');
if (mid === -1) {
// Skipping the placeholder means the units should start from zero,
// otherwise assume they end at zero.
mid = units[0] === '_' ? 0 : units.length;
}
i = max(min(floor(significant / 3), units.length - mid - 1), -mid);
unit = units[i + mid];
while (unit === '_') {
i += i < 0 ? -1 : 1;
unit = units[i + mid];
}
if (unit === '|') {
unit = '';
}
if (significant < -9) {
precision = abs(significant) - 9;
}
divisor = bytes ? pow(2, 10 * i) : pow(10, i * 3);
return numberFormat(withPrecision(num / divisor, precision || 0)) + unit;
}
function numberFormat(num, place) {
var result = '', thousands, decimal, fraction, integer, split, str;
decimal = _numberOptions('decimal');
thousands = _numberOptions('thousands');
if (isNumber(place)) {
str = withPrecision(num, place || 0).toFixed(max(place, 0));
} else {
str = num.toString();
}
str = str.replace(/^-/, '');
split = periodSplit(str);
integer = split[0];
fraction = split[1];
if (/e/.test(str)) {
result = str;
} else {
for(var i = integer.length; i > 0; i -= 3) {
if (i < integer.length) {
result = thousands + result;
}
result = integer.slice(max(0, i - 3), i) + result;
}
}
if (fraction) {
result += decimal + repeatString('0', (place || 0) - fraction.length) + fraction;
}
return (num < 0 ? '-' : '') + result;
}
function isInteger(n) {
return n % 1 === 0;
}
function isMultipleOf(n1, n2) {
return n1 % n2 === 0;
}
function createRoundingFunction(fn) {
return function(n, precision) {
return precision ? withPrecision(n, precision, fn) : fn(n);
};
}
defineStatic(sugarNumber, {
/***
* @method random([n1], [n2])
* @returns Number
* @static
* @short Returns a random integer from [n1] to [n2] (both inclusive).
* @extra If only 1 number is passed, the other will be 0. If none are passed,
* the number will be either 0 or 1.
*
* @example
*
* Number.random(50, 100) -> ex. 85
* Number.random(50) -> ex. 27
* Number.random() -> ex. 0
*
***/
'random': function(n1, n2) {
var minNum, maxNum;
if (arguments.length == 1) n2 = n1, n1 = 0;
minNum = min(n1 || 0, isUndefined(n2) ? 1 : n2);
maxNum = max(n1 || 0, isUndefined(n2) ? 1 : n2) + 1;
return trunc((Math.random() * (maxNum - minNum)) + minNum);
}
});
defineInstance(sugarNumber, {
/***
* @method isInteger()
* @returns Boolean
* @short Returns true if the number has no trailing decimal.
*
* @example
*
* (420).isInteger() -> true
* (4.5).isInteger() -> false
*
***/
'isInteger': function(n) {
return isInteger(n);
},
/***
* @method isOdd()
* @returns Boolean
* @short Returns true if the number is odd.
*
* @example
*
* (3).isOdd() -> true
* (18).isOdd() -> false
*
***/
'isOdd': function(n) {
return isInteger(n) && !isMultipleOf(n, 2);
},
/***
* @method isEven()
* @returns Boolean
* @short Returns true if the number is even.
*
* @example
*
* (6).isEven() -> true
* (17).isEven() -> false
*
***/
'isEven': function(n) {
return isMultipleOf(n, 2);
},
/***
* @method isMultipleOf(<num>)
* @returns Boolean
* @short Returns true if the number is a multiple of <num>.
*
* @example
*
* (6).isMultipleOf(2) -> true
* (17).isMultipleOf(2) -> false
* (32).isMultipleOf(4) -> true
* (34).isMultipleOf(4) -> false
*
***/
'isMultipleOf': function(n, num) {
return isMultipleOf(n, num);
},
/***
* @method log(<base> = Math.E)
* @returns Number
* @short Returns the logarithm of the number with <base>, or the natural
* logarithm of the number if <base> is undefined.
*
* @example
*
* (64).log(2) -> 6
* (9).log(3) -> 2
* (5).log() -> 1.6094379124341003
*
***/
'log': function(n, base) {
return Math.log(n) / (base ? Math.log(base) : 1);
},
/***
* @method abbr([precision] = 0)
* @returns String
* @short Returns an abbreviated form of the number ("k" for thousand, "m"
* for million, etc).
* @extra [precision] will round to the given precision. `thousands` and
* `decimal` allow custom separators to be used.
*
* @example
*
* (1000).abbr() -> "1k"
* (1000000).abbr() -> "1m"
* (1280).abbr(1) -> "1.3k"
*
***/
'abbr': function(n, precision) {
return abbreviateNumber(n, precision, BASIC_UNITS);
},
/***
* @method metric([precision] = 0, [units] = "nμm|k")
* @returns String
* @short Returns the number as a string in metric notation.
* @extra [precision] will round to the given precision (can be negative).
* [units] is a string that determines both the unit notation and the
* min/max unit allowed. The default is natural notation for common
* units (meters, grams, etc). "all" can be passed for [units] and is a
* shortcut to all standard SI units. The token `,` if present separates
* units, otherwise each character is a unit. The token `|` if present
* marks where fractional units end, otherwise no fractional units are
* used. Finally, the token `_` if present is a placeholder for no unit.
*
* @example
*
* (1000).metric() -> "1k"
* (1000000).metric() -> "1,000k"
* (1249).metric(2) + 'g' -> "1.25kg"
* (0.025).metric() + 'm' -> "25mm"
* (1000000).metric(0, 'nμm|kM') -> "1M"
*
***/
'metric': function(n, precision, units) {
if (units === 'all') {
units = METRIC_UNITS_FULL;
} else if (!units) {
units = METRIC_UNITS_SHORT;
}
return abbreviateNumber(n, precision, units);
},
/***
* @method bytes([precision] = 0, [binary] = false, [units] = 'si')
* @returns String
* @short Returns an abbreviated form of the number, with 'B' on the end for "bytes".
* @extra [precision] will round to the given precision. If [binary] is `true`,
* powers of 1024 will be used instead of 1000, and units will default
* to the binary units "KiB", "MiB", etc. Units can be overridden by
* passing "si" or "binary" for [units], or further customized by
* passing a unit string. See `metric` for more.
*
* @example
*
* (1000).bytes() -> "1KB"
* (1289).bytes(2) -> "1.29KB"
* (1000).bytes(2, true) -> "0.98KiB"
* (1000).bytes(2, true, 'si') -> "0.98KB"
*
***/
'bytes': function(n, precision, binary, units) {
if (units === 'binary' || (!units && binary)) {
units = MEMORY_BINARY_UNITS;
} else if(units === 'si' || !units) {
units = MEMORY_UNITS;
}
return abbreviateNumber(n, precision, units, binary) + 'B';
},
/***
* @method format([place] = 0)
* @returns String
* @short Formats the number to a readable string.
* @extra If [place] is `undefined`, the place will automatically be determined.
* `thousands` and `decimal` allow custom markers to be used.
*
* @example
*
* (56782).format() -> '56,782'
* (56782).format(2) -> '56,782.00'
* (4388.43).format(2) -> '4,388.43'
*
***/
'format': function(n, place) {
return numberFormat(n, place);
},
/***
* @method hex([pad] = 1)
* @returns String
* @short Converts the number to hexidecimal.
* @extra [pad] will pad the resulting string to that many places.
*
* @example
*
* (255).hex() -> 'ff';
* (255).hex(4) -> '00ff';
* (23654).hex() -> '5c66';
*
***/
'hex': function(n, pad) {
return padNumber(n, pad || 1, false, 16);
},
/***
* @method times(<fn>)
* @returns Mixed
* @short Calls <fn> a number of times equivalent to the number.
* @extra Any non-undefined return values of <fn> will be collected and
* returned in an array.
*
* @callback fn
*
* i The index of the current iteration.
*
* @example
*
* (8).times(logHello) -> logs "hello" 8 times
* (7).times(function(n) {
* return Math.pow(2, n);
* });
*
***/
'times': function(n, fn) {
var arr, result;
for(var i = 0; i < n; i++) {
result = fn.call(n, i);
if (isDefined(result)) {
if (!arr) {
arr = [];
}
arr.push(result);
}
}
return arr;
},
/***
* @method chr()
* @returns String
* @short Returns a string at the code point of the number.
*
* @example
*
* (65).chr() -> "A"
* (75).chr() -> "K"
*
***/
'chr': function(n) {
return chr(n);
},
/***
* @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(n, place, sign, base) {
return padNumber(n, place, sign, base);
},
/***
* @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(n) {
var num = abs(n), last = +num.toString().slice(-2);
return n + getOrdinalSuffix(last);
},
/***
* @method toNumber()
* @returns Number
* @short Identity function for compatibilty.
*
* @example
*
* (420).toNumber() -> 420
*
***/
'toNumber': function(n) {
return n.valueOf();
},
/***
* @method round(<precision> = 0)
* @returns Number
* @short Shortcut for `Math.round` that also allows a <precision>.
*
* @example
*
* (3.241).round() -> 3
* (-3.841).round() -> -4
* (3.241).round(2) -> 3.24
* (3748).round(-2) -> 3800
*
***/
'round': createRoundingFunction(round),
/***
* @method ceil(<precision> = 0)
* @returns Number
* @short Shortcut for `Math.ceil` that also allows a <precision>.
*
* @example
*
* (3.241).ceil() -> 4
* (-3.241).ceil() -> -3
* (3.241).ceil(2) -> 3.25
* (3748).ceil(-2) -> 3800
*
***/
'ceil': createRoundingFunction(ceil),
/***
* @method floor(<precision> = 0)
* @returns Number
* @short Shortcut for `Math.floor` that also allows a <precision>.
*
* @example
*
* (3.241).floor() -> 3
* (-3.841).floor() -> -4
* (3.241).floor(2) -> 3.24
* (3748).floor(-2) -> 3700
*
***/
'floor': createRoundingFunction(floor)
});
/***
* @method [math]()
* @returns Number
* @short Math related functions are mapped as shortcuts to numbers and are
* identical. Note that `log` provides some special defaults.
*
* @set
* abs
* sin
* asin
* cos
* acos
* tan
* atan
* sqrt
* exp
* pow
*
* @example
*
* (3).pow(3) -> 27
* (-3).abs() -> 3
* (1024).sqrt() -> 32
*
***/
function buildMathAliases() {
defineInstanceSimilar(sugarNumber, 'abs pow sin asin cos acos tan atan exp pow sqrt', function(methods, name) {
methods[name] = function(n, arg) {
// Note that .valueOf() here is only required due to a
// very strange bug in iOS7 that only occurs occasionally
// in which Math.abs() called on non-primitive numbers
// returns a completely different number (Issue #400)
return Math[name](n.valueOf(), arg);
};
});
}
buildMathAliases();
/***
* @module Function
* @description Lazy, throttled, and memoized functions, delayed functions and
* handling of timers, argument currying.
*
***/
var _lock = privatePropertyAccessor('lock');
var _timers = privatePropertyAccessor('timers');
var _partial = privatePropertyAccessor('partial');
var _canceled = privatePropertyAccessor('canceled');
var createInstanceFromPrototype = Object.create || function(prototype) {
var ctor = function() {};
ctor.prototype = prototype;
return new ctor;
};
function setDelay(fn, ms, after, scope, args) {
// Delay of infinity is never called of course...
ms = coercePositiveInteger(ms || 0);
if (!_timers(fn)) {
_timers(fn, []);
}
// This is a workaround for <= IE8, which apparently has the
// ability to call timeouts in the queue on the same tick (ms?)
// even if functionally they have already been cleared.
_canceled(fn, false);
_timers(fn).push(setTimeout(function() {
if (!_canceled(fn)) {
after.apply(scope, args || []);
}
}, ms));
}
function cancelFunction(fn) {
var timers = _timers(fn), timer;
if (isArray(timers)) {
while(timer = timers.shift()) {
clearTimeout(timer);
}
}
_canceled(fn, true);
return fn;
}
function createLazyFunction(fn, ms, immediate, limit) {
var queue = [], locked = false, execute, rounded, perExecution, result;
ms = ms || 1;
limit = limit || Infinity;
rounded = ceil(ms);
perExecution = round(rounded / ms) || 1;
execute = function() {
var queueLength = queue.length, maxPerRound;
if (queueLength == 0) return;
// Allow fractions of a millisecond by calling
// multiple times per actual timeout execution
maxPerRound = max(queueLength - perExecution, 0);
while(queueLength > maxPerRound) {
// Getting uber-meta here...
result = Function.prototype.apply.apply(fn, queue.shift());
queueLength--;
}
setDelay(lazy, rounded, function() {
locked = false;
execute();
});
};
function lazy() {
// If the execution has locked and it's immediate, then
// allow 1 less in the queue as 1 call has already taken place.
if (queue.length < limit - (locked && immediate ? 1 : 0)) {
// Optimized: no leaking arguments
var args = []; for(var $i = 0, $len = arguments.length; $i < $len; $i++) args.push(arguments[$i]);
queue.push([this, args]);
}
if (!locked) {
locked = true;
if (immediate) {
execute();
} else {
setDelay(lazy, rounded, execute);
}
}
// Return the memoized result
return result;
}
return lazy;
}
// Collecting arguments in an array instead of
// passing back the arguments object which will
// deopt this function in V8.
function collectArguments() {
var args = arguments, i = args.length, arr = new Array(i);
while (i--) {
arr[i] = args[i];
}
return arr;
}
function createHashedMemoizeFunction(fn, hashFn, limit) {
var map = {}, refs = [], counter = 0;
return function() {
var hashObj = hashFn.apply(this, arguments);
var key = serializeInternal(hashObj, refs);
if (hasOwn(map, key)) {
return getOwn(map, key);
}
if (counter === limit) {
map = {};
refs = [];
counter = 0;
}
counter++;
return map[key] = fn.apply(this, arguments);
};
}
defineInstance(sugarFunction, {
/***
* @method lazy([ms] = 1, [immediate] = false, [limit] = Infinity)
* @returns Function
* @short Creates a lazy function that, when called repeatedly, will queue
* execution and wait [ms] milliseconds to execute.
* @extra If [immediate] is `true`, first execution will happen immediately,
* then lock. If [limit] is a fininte number, calls past [limit] will
* be ignored while execution is locked. Compare this to `throttle`,
* which will execute only once per [ms] milliseconds. Note that [ms]
* can also be a fraction. Calling `cancel` on a lazy function will
* clear the entire queue.
*
* @example
*
* var fn = logHello.lazy(250);
* runTenTimes(fn); -> Logs 10 times each time 250ms later
*
* var fn = logHello.lazy(250, false, 5);
* runTenTimes(fn); -> Logs 5 times each time 250ms later
*
***/
'lazy': function(fn, ms, immediate, limit) {
return createLazyFunction(fn, ms, immediate, limit);
},
/***
* @method throttle([ms] = 1)
* @returns Function
* @short Creates a "throttled" version of the function that will only be
* executed once per <ms> milliseconds.
* @extra This is functionally equivalent to calling `lazy` with a [limit] of
* `1` and [immediate] as `true`. `throttle` is appropriate when you
* want to make sure a function is only executed at most once for a
* given duration.
*
* @example
*
* var fn = logHello.throttle(50);
* runTenTimes(fn);
*
***/
'throttle': function(fn, ms) {
return createLazyFunction(fn, ms, true, 1);
},
/***
* @method debounce([ms] = 1)
* @returns Function
* @short Creates a "debounced" function that postpones its execution until
* after <ms> milliseconds have passed.
* @extra This method is useful to execute a function after things have
* "settled down". A good example of this is when a user tabs quickly
* through form fields, execution of a heavy operation should happen
* after a few milliseconds when they have "settled" on a field.
*
* @example
*
* var fn = logHello.debounce(250)
* runTenTimes(fn); -> called once 250ms later
*
***/
'debounce': function(fn, ms) {
function debounced() {
// Optimized: no leaking arguments
var args = []; for(var $i = 0, $len = arguments.length; $i < $len; $i++) args.push(arguments[$i]);
cancelFunction(debounced);
setDelay(debounced, ms, fn, this, args);
}
return debounced;
},
/***
* @method cancel()
* @returns Function
* @short Cancels a delayed function scheduled to be run.
* @extra `delay`, `lazy`, `throttle`, and `debounce` can all set delays.
*
* @example
*
* logHello.delay(500).cancel() -> never logs
*
***/
'cancel': function(fn) {
return cancelFunction(fn);
},
/***
* @method after(<n>)
* @returns Function
* @short Creates a function that will execute after <n> calls.
* @extra `after` is useful for running a final callback after a specific
* number of operations, often when the order in which the operations
* will complete is unknown. The created function will be passed an
* array of the arguments that it has collected from each after <n>.
* Note that the function will execute on every call after <n>.
* Use `once` in conjunction with this method to prevent being
* triggered by subsequent calls.
*
* @example
*
* var fn = logHello.after(5);
* runTenTimes(fn); -> logs 6 times
*
* var fn = logHello.once().after(5)
* runTenTimes(fn); -> logs once
*
***/
'after': function(fn, num) {
var count = 0, collectedArgs = [];
num = coercePositiveInteger(num);
return function() {
// Optimized: no leaking arguments
var args = []; for(var $i = 0, $len = arguments.length; $i < $len; $i++) args.push(arguments[$i]);
collectedArgs.push(args);
count++;
if (count >= num) {
return fn.call(this, collectedArgs);
}
};
},
/***
* @method once()
* @returns Function
* @short Creates a function that will execute only once and store the result.
* @extra `once` is useful for creating functions that will cache the result
* of an expensive operation and use it on subsequent calls. Also it
* can be useful for creating initialization functions that only need
* to be run once.
*
* @example
*
* var fn = logHello.once();
* runTenTimes(fn); -> logs once
*
***/
'once': function(fn) {
var called = false, val;
return function() {
if (called) {
return val;
}
called = true;
return val = fn.apply(this, arguments);
};
},
/***
* @method memoize([hashFn], [limit])
* @returns Function
* @short Creates a function that will memoize results for unique calls.
* @extra `memoize` can be thought of as a more powerful `once`. Where `once`
* will only call a function once ever, memoized functions will be
* called once per unique call. A "unique call" is determined by the
* return value of [hashFn], which is passed the arguments of each call.
* If [hashFn] is undefined, it will deeply serialize all arguments,
* such that any different argument signature will result in a unique
* call. [hashFn] may be a string (allows `deep properties`) that acts
* as a shortcut to return a property of the first argument passed.
* [limit] sets an upper limit on memoized results. The default is no
* limit, meaning that unique calls will continue to memoize results.
* For most use cases this is fine, however [limit] is useful for more
* persistent (often server-side) applications for whom memory leaks
* are a concern.
*
* @example
*
* var fn = logHello.memoize();
* fn(1); fn(1); fn(2); -> logs twice, memoizing once
*
* var fn = calculateUserBalance.memoize('id');
* fn(Harry); fn(Mark); fn(Mark); -> logs twice, memoizing once
*
***/
'memoize': function(fn, arg1, arg2) {
var hashFn, limit, prop;
if (isNumber(arg1)) {
limit = arg1;
} else {
hashFn = arg1;
limit = arg2;
}
if (isString(hashFn)) {
prop = hashFn;
hashFn = function(obj) {
return deepGetProperty(obj, prop);
};
} else if (!hashFn) {
hashFn = collectArguments;
}
return createHashedMemoizeFunction(fn, hashFn, limit);
},
/***
* @method lock([n])
* @returns Function
* @short Locks the number of arguments accepted by the function.
* @extra If not passed, [n] will be the length of the function. This method
* can be called on functions created by `partial`, in which case it
* will lock the total arguments during execution.
*
* @example
*
* logArgs.lock(2)(1,2,3) -> logs 1,2
*
***/
'lock': function(fn, n) {
var lockedFn;
if (_partial(fn)) {
_lock(fn, isNumber(n) ? n : null);
return fn;
}
lockedFn = function() {
arguments.length = min(_lock(lockedFn), arguments.length);
return fn.apply(this, arguments);
};
_lock(lockedFn, isNumber(n) ? n : fn.length);
return lockedFn;
}
});
defineInstanceWithArguments(sugarFunction, {
/***
* @method partial(<arg1>, <arg2>, ...)
* @returns Function
* @short Returns a new version of the function which has part of its arguments
* pre-emptively filled in, also known as "currying".
* @extra `undefined` can be passed as any argument, and is a placeholder that
* will be replaced with arguments passed when the function is executed.
* This allows currying of arguments even when they occur toward the end
* of an argument list (the example demonstrates this more clearly).
*
* @example
*
* logArgs.partial(undefined, 'b')('a') -> logs a, b
*
***/
'partial': function(fn, curriedArgs) {
var curriedLen = curriedArgs.length;
var partialFn = function() {
var argIndex = 0, applyArgs = [], self = this, lock = _lock(partialFn), result, i;
for (i = 0; i < curriedLen; i++) {
var arg = curriedArgs[i];
if (isDefined(arg)) {
applyArgs[i] = arg;
} else {
applyArgs[i] = arguments[argIndex++];
}
}
for (i = argIndex; i < arguments.length; i++) {
applyArgs.push(arguments[i]);
}
if (lock === null) {
lock = curriedLen;
}
if (isNumber(lock)) {
applyArgs.length = min(applyArgs.length, lock);
}
// If the bound "this" object is an instance of the partialed
// function, then "new" was used, so preserve the prototype
// so that constructor functions can also be partialed.
if (self instanceof partialFn) {
self = createInstanceFromPrototype(fn.prototype);
result = fn.apply(self, applyArgs);
// An explicit return value is allowed from constructors
// as long as they are of "object" type, so return the
// correct result here accordingly.
return isObjectType(result) ? result : self;
}
return fn.apply(self, applyArgs);
};
_partial(partialFn, true);
return partialFn;
},
/***
* @method delay([ms] = 1, [arg1], ...)
* @returns Function
* @short Executes the function after <ms> milliseconds.
* @extra Returns a reference to itself. `delay` is also a way to execute non-
* blocking operations that will wait until the CPU is free. Delayed
* functions can be canceled using the `cancel` method. Can also curry
* arguments passed in after <ms>.
*
* @example
*
* logHello.delay(500) -> logs after 500ms
* logArgs.delay(500, 'a') -> logs "a" after 500ms
*
***/
'delay': function(fn, ms, args) {
setDelay(fn, ms, fn, fn, args);
return fn;
},
/***
* @method every([ms] = 1, [arg1], ...)
* @returns Function
* @short Executes the function every <ms> milliseconds.
* @extra Returns a reference to itself. `every` uses `setTimeout`, which
* means that you are guaranteed a period of idle time equal to [ms]
* after execution has finished. Compare this to `setInterval` which
* will try to run a function every [ms], even when execution itself
* takes up a portion of that time. In most cases avoiding `setInterval`
* is better as calls won't "back up" when the CPU is under strain,
* however this also means that calls are less likely to happen at
* exact intervals of [ms], so the use case here should be considered.
* Additionally, `every` can curry arguments passed in after [ms], and
* also be canceled with `cancel`.
*
* @example
*
* logHello.every(1000) -> logs every second
* logArgs.every(1000, 'Hola') -> logs 'hola' every second
*
***/
'every': function(fn, ms, args) {
function execute () {
// Set the delay first here, so that cancel
// can be called within the executing function.
setDelay(fn, ms, execute);
fn.apply(fn, args);
}
setDelay(fn, ms, execute);
return fn;
}
});
/***
* @module RegExp
* @description RegExp escaping and flag manipulation.
*
* Note here that methods on the RegExp class like .exec and .test will fail in
* the current version of SpiderMonkey being used by CouchDB when using
* shorthand regex notation like /foo/. This is the reason for the intermixed
* use of shorthand and compiled regexes here. If you're using JS in CouchDB, it
* is safer to ALWAYS compile your regexes from a string.
*
***/
defineStatic(sugarRegExp, {
/***
* @method escape(<str> = '')
* @returns String
* @static
* @short Escapes all RegExp tokens in a string.
*
* @example
*
* RegExp.escape('really?') -> 'really\?'
* RegExp.escape('yes.') -> 'yes\.'
* RegExp.escape('(not really)') -> '\(not really\)'
*
***/
'escape': function(str) {
return escapeRegExp(str);
}
});
defineInstance(sugarRegExp, {
/***
* @method getFlags()
* @returns String
* @short Returns the flags of the regex as a string.
*
* @example
*
* /texty/gim.getFlags() -> 'gim'
*
***/
'getFlags': function(r) {
return getRegExpFlags(r);
},
/***
* @method setFlags(<flags>)
* @returns RegExp
* @short Creates a copy of the regex with <flags> set.
*
* @example
*
* /texty/.setFlags('gim') -> now has global, ignoreCase, and multiline set
*
***/
'setFlags': function(r, flags) {
return RegExp(r.source, flags);
},
/***
* @method addFlags(<flags>)
* @returns RegExp
* @short Creates a copy of the regex with <flags> added.
*
* @example
*
* /texty/.addFlags('g') -> /texty/g
* /texty/.addFlags('im') -> /texty/im
*
***/
'addFlags': function(r, flags) {
return RegExp(r.source, getRegExpFlags(r, flags));
},
/***
* @method removeFlags(<flags>)
* @returns RegExp
* @short Creates a copy of the regex with <flags> removed.
*
* @example
*
* /texty/gim.removeFlags('g') -> /texty/im
* /texty/gim.removeFlags('im') -> /texty/g
*
***/
'removeFlags': function(r, flags) {
var reg = allCharsReg(flags);
return RegExp(r.source, getRegExpFlags(r).replace(reg, ''));
}
});
/***
* @module Range
* @description Date, Number, and String ranges that can be manipulated and compared,
* or enumerate over specific points within the range.
*
***/
var DURATION_UNITS = 'year|month|week|day|hour|minute|second|millisecond';
var DURATION_REG = RegExp('(\\d+)?\\s*('+ DURATION_UNITS +')s?', 'i');
var MULTIPLIERS = {
'Hours': 60 * 60 * 1000,
'Minutes': 60 * 1000,
'Seconds': 1000,
'Milliseconds': 1
};
var PrimitiveRangeConstructor = function(start, end) {
return new Range(start, end);
};
function Range(start, end) {
this.start = cloneRangeMember(start);
this.end = cloneRangeMember(end);
}
function getRangeMemberNumericValue(m) {
return isString(m) ? m.charCodeAt(0) : m;
}
function getRangeMemberPrimitiveValue(m) {
if (m == null) return m;
return isDate(m) ? m.getTime() : m.valueOf();
}
function getPrecision(n) {
var split = periodSplit(n.toString());
return split[1] ? split[1].length : 0;
}
function getGreaterPrecision(n1, n2) {
return max(getPrecision(n1), getPrecision(n2));
}
function cloneRangeMember(m) {
if (isDate(m)) {
return new Date(m.getTime());
} else {
return getRangeMemberPrimitiveValue(m);
}
}
function isValidRangeMember(m) {
var val = getRangeMemberPrimitiveValue(m);
return (!!val || val === 0) && valueIsNotInfinite(m);
}
function valueIsNotInfinite(m) {
return m !== -Infinity && m !== Infinity;
}
function rangeIsValid(range) {
return isValidRangeMember(range.start) &&
isValidRangeMember(range.end) &&
typeof range.start === typeof range.end;
}
function rangeEvery(range, step, countOnly, fn) {
var increment,
precision,
dio,
unit,
start = range.start,
end = range.end,
inverse = end < start,
current = start,
index = 0,
result = [];
if (!rangeIsValid(range)) {
return [];
}
if (isFunction(step)) {
fn = step;
step = null;
}
step = step || 1;
if (isNumber(start)) {
precision = getGreaterPrecision(start, step);
increment = function() {
return incrementNumber(current, step, precision);
};
} else if (isString(start)) {
increment = function() {
return incrementString(current, step);
};
} else if (isDate(start)) {
dio = getDateIncrementObject(step);
step = dio[0];
unit = dio[1];
increment = function() {
return incrementDate(current, step, unit);
};
}
// Avoiding infinite loops
if (inverse && step > 0) {
step *= -1;
}
while(inverse ? current >= end : current <= end) {
if (!countOnly) {
result.push(current);
}
if (fn) {
fn(current, index, range);
}
current = increment();
index++;
}
return countOnly ? index - 1 : result;
}
function getDateIncrementObject(amt) {
var match, val, unit;
if (isNumber(amt)) {
return [amt, 'Milliseconds'];
}
match = amt.match(DURATION_REG);
val = +match[1] || 1;
unit = simpleCapitalize(match[2].toLowerCase());
if (unit.match(/hour|minute|second/i)) {
unit += 's';
} else if (unit === 'Year') {
unit = 'FullYear';
} else if (unit === 'Week') {
unit = 'Date';
val *= 7;
} else if (unit === 'Day') {
unit = 'Date';
}
return [val, unit];
}
function incrementDate(src, amount, unit) {
var mult = MULTIPLIERS[unit], d;
if (mult) {
d = new Date(src.getTime() + (amount * mult));
} else {
d = new Date(src);
callDateSet(d, unit, callDateGet(src, unit) + amount);
}
return d;
}
function incrementString(current, amount) {
return chr(current.charCodeAt(0) + amount);
}
function incrementNumber(current, amount, precision) {
return withPrecision(current + amount, precision);
}
function rangeClamp(range, obj) {
var clamped,
start = range.start,
end = range.end,
min = end < start ? end : start,
max = start > end ? start : end;
if (obj < min) {
clamped = min;
} else if (obj > max) {
clamped = max;
} else {
clamped = obj;
}
return cloneRangeMember(clamped);
}
defineOnPrototype(Range, {
/***
* @method toString()
* @returns String
* @short Returns a string representation of the range.
*
* @example
*
* Number.range(1, 5).toString() -> 1..5
* janToMay.toString() -> January 1, xxxx..May 1, xxxx
*
***/
'toString': function() {
return rangeIsValid(this) ? this.start + '..' + this.end : 'Invalid Range';
},
/***
* @method isValid()
* @returns Boolean
* @short Returns true if the range is valid, false otherwise.
*
* @example
*
* janToMay.isValid() -> true
* Number.range(NaN, NaN).isValid() -> false
*
***/
'isValid': function() {
return rangeIsValid(this);
},
/***
* @method span()
* @returns Number
* @short Returns the span of the range. If the range is a date range, the
* value is in milliseconds.
* @extra The span includes both the start and the end.
*
* @example
*
* Number.range(5, 10).span() -> 6
* Number.range(40, 25).span() -> 16
* janToMay.span() -> 10368000001 (or more depending on leap year)
*
***/
'span': function() {
var n = getRangeMemberNumericValue(this.end) - getRangeMemberNumericValue(this.start);
return rangeIsValid(this) ? abs(n) + 1 : NaN;
},
/***
* @method contains(<obj>)
* @returns Boolean
* @short Returns true if <obj> is contained inside the range. <obj> may be a
* value or another range.
*
* @example
*
* Number.range(5, 10).contains(7) -> true
* Number.range(5, 10).contains(2) -> false
* janToMay.contains(mar) -> true
* janToMay.contains(marToAug) -> false
* janToMay.contains(febToApr) -> true
*
***/
'contains': function(obj) {
if (obj == null) return false;
if (obj.start && obj.end) {
return obj.start >= this.start && obj.start <= this.end &&
obj.end >= this.start && obj.end <= this.end;
} else {
return obj >= this.start && obj <= this.end;
}
},
/***
* @method every(<amount>, [fn])
* @returns Array
* @short Iterates through the range by <amount>, calling [fn] for each step.
* @extra Returns an array of each increment visited. For date ranges,
* <amount> can also be a string like `"2 days"`. This will step
* through the range by incrementing a date object by that specific
* unit, and so is generally preferable for vague units such as
* `"2 months"`.
*
* @callback fn
*
* el The element of the current iteration.
* i The index of the current iteration.
* r A reference to the range.
*
* @example
*
* Number.range(2, 8).every(2) -> [2,4,6,8]
* janToMay.every('2 months') -> [Jan 1, Mar 1, May 1]
*
* sepToOct.every('week', function() {
* // Will be called every week from September to October
* })
*
***/
'every': function(amount, fn) {
return rangeEvery(this, amount, false, fn);
},
/***
* @method toArray()
* @returns Array
* @short Creates an array from the range.
* @extra If the range is a date range, every millisecond between the start
* and end dates will be returned. To control this use `every` instead.
*
* @example
*
* Number.range(1, 5).toArray() -> [1,2,3,4,5]
* Date.range('1 millisecond ago', 'now').toArray() -> [1ms ago, now]
*
***/
'toArray': function() {
return rangeEvery(this);
},
/***
* @method union(<range>)
* @returns Range
* @short Returns a new range with the earliest starting point as its start,
* and the latest ending point as its end. If the two ranges do not
* intersect this will effectively remove the "gap" between them.
*
* @example
*
* oneToTen.union(fiveToTwenty) -> 1..20
* janToMay.union(marToAug) -> Jan 1, xxxx..Aug 1, xxxx
*
***/
'union': function(range) {
return new Range(
this.start < range.start ? this.start : range.start,
this.end > range.end ? this.end : range.end
);
},
/***
* @method intersect(<range>)
* @returns Range
* @short Returns a new range with the latest starting point as its start,
* and the earliest ending point as its end. If the two ranges do not
* intersect this will effectively produce an invalid range.
*
* @example
*
* oneToTen.intersect(fiveToTwenty) -> 5..10
* janToMay.intersect(marToAug) -> Mar 1, xxxx..May 1, xxxx
*
***/
'intersect': function(range) {
if (range.start > this.end || range.end < this.start) {
return new Range(NaN, NaN);
}
return new Range(
this.start > range.start ? this.start : range.start,
this.end < range.end ? this.end : range.end
);
},
/***
* @method clone()
* @returns Range
* @short Clones the range.
* @extra Members of the range will also be cloned.
*
* @example
*
* Number.range(1, 5).clone() -> Returns a copy of the range.
*
***/
'clone': function() {
return new Range(this.start, this.end);
},
/***
* @method clamp(<obj>)
* @returns Mixed
* @short Clamps <obj> to be within the range if it falls outside.
*
* @example
*
* Number.range(1, 5).clamp(8) -> 5
* janToMay.clamp(aug) -> May 1, xxxx
*
***/
'clamp': function(obj) {
return rangeClamp(this, obj);
}
});
/*** @namespace Number ***/
defineStatic(sugarNumber, {
/***
* @method range([start], [end])
* @returns Range
* @static
* @short Creates a new number range between [start] and [end]. See `ranges`
* for more.
*
* @example
*
* Number.range(5, 10)
* Number.range(20, 15)
*
***/
'range': PrimitiveRangeConstructor
});
defineInstance(sugarNumber, {
/***
* @method upto(<num>, [step] = 1, [fn])
* @returns Array
* @short Returns an array containing numbers from the number up to <num>.
* @extra Optionally calls [fn] for each number in that array. [step] allows
* multiples other than 1. [fn] can be passed in place of [step].
*
* @callback fn
*
* el The element of the current iteration.
* i The index of the current iteration.
* r A reference to the range.
*
* @example
*
* (2).upto(6) -> [2, 3, 4, 5, 6]
* (2).upto(6, function(n) {
* // This function is called 5 times receiving n as the value.
* });
* (2).upto(8, 2) -> [2, 4, 6, 8]
*
***/
'upto': function(n, num, step, fn) {
return rangeEvery(new Range(n, num), step, false, fn);
},
/***
* @method clamp([start] = Infinity, [end] = Infinity)
* @returns Number
* @short Constrains the number so that it falls on or between [start] and
* [end].
* @extra This will build a range object that has an equivalent `clamp` method.
*
* @example
*
* (3).clamp(50, 100) -> 50
* (85).clamp(50, 100) -> 85
*
***/
'clamp': function(n, start, end) {
return rangeClamp(new Range(start, end), n);
},
/***
* @method cap([max] = Infinity)
* @returns Number
* @short Constrains the number so that it is no greater than [max].
* @extra This will build a range object that has an equivalent `cap` method.
*
* @example
*
* (100).cap(80) -> 80
*
***/
'cap': function(n, max) {
return rangeClamp(new Range(undefined, max), n);
}
});
/***
* @method downto(<num>, [step] = 1, [fn])
* @returns Array
* @short Returns an array containing numbers from the number down to <num>.
* @extra Optionally calls [fn] for each number in that array. [step] allows
* multiples other than 1. [fn] can be passed in place of [step].
*
* @callback fn
*
* el The element of the current iteration.
* i The index of the current iteration.
* r A reference to the range.
*
* @example
*
* (8).downto(3) -> [8, 7, 6, 5, 4, 3]
* (8).downto(3, function(n) {
* // This function is called 6 times receiving n as the value.
* });
* (8).downto(2, 2) -> [8, 6, 4, 2]
*
***/
alias(sugarNumber, 'downto', 'upto');
/*** @namespace String ***/
defineStatic(sugarString, {
/***
* @method range([start], [end])
* @returns Range
* @static
* @short Creates a new string range between [start] and [end]. See `ranges`
* for more.
*
* @example
*
* String.range('a', 'z')
* String.range('t', 'm')
*
***/
'range': PrimitiveRangeConstructor
});
/*** @namespace Date ***/
var FULL_CAPTURED_DURATION = '((?:\\d+)?\\s*(?:' + DURATION_UNITS + '))s?';
// Duration text formats
var RANGE_REG_FROM_TO = /(?:from)?\s*(.+)\s+(?:to|until)\s+(.+)$/i,
RANGE_REG_REAR_DURATION = RegExp('(.+)\\s*for\\s*' + FULL_CAPTURED_DURATION, 'i'),
RANGE_REG_FRONT_DURATION = RegExp('(?:for)?\\s*'+ FULL_CAPTURED_DURATION +'\\s*(?:starting)?\\s*at\\s*(.+)', 'i');
var DateRangeConstructor = function(start, end) {
if (arguments.length === 1 && isString(start)) {
return createDateRangeFromString(start);
}
return new Range(getDateForRange(start), getDateForRange(end));
};
function createDateRangeFromString(str) {
var match, datetime, duration, dio, start, end;
if (sugarDate.get && (match = str.match(RANGE_REG_FROM_TO))) {
start = getDateForRange(match[1].replace('from', 'at'));
end = sugarDate.get(start, match[2]);
return new Range(start, end);
}
if (match = str.match(RANGE_REG_FRONT_DURATION)) {
duration = match[1];
datetime = match[2];
}
if (match = str.match(RANGE_REG_REAR_DURATION)) {
datetime = match[1];
duration = match[2];
}
if (datetime && duration) {
start = getDateForRange(datetime);
dio = getDateIncrementObject(duration);
end = incrementDate(start, dio[0], dio[1]);
} else {
start = str;
}
return new Range(getDateForRange(start), getDateForRange(end));
}
function getDateForRange(d) {
if (isDate(d)) {
return d;
} else if (d == null) {
return new Date();
} else if (sugarDate.create) {
return sugarDate.create(d);
}
return new Date(d);
}
/***
* @method [dateUnit]()
* @returns Number
* @namespace Range
* @short Returns the span of a date range in the given unit.
* @extra Higher order units ("days" and greater) walk the date to avoid
* discrepancies with ambiguity. Lower order units simply subtract the
* start from the end.
*
* @set
* milliseconds
* seconds
* minutes
* hours
* days
* weeks
* months
* years
*
* @example
*
* janToMay.months() -> 4
* janToMay.days() -> 121
* janToMay.hours() -> 2904
* janToMay.minutes() -> 220320
*
***/
function buildDateRangeUnits() {
var methods = {};
forEach(DURATION_UNITS.split('|'), function(unit, i) {
var name = unit + 's', mult, fn;
if (i < 4) {
fn = function() {
return rangeEvery(this, unit, true);
};
} else {
mult = MULTIPLIERS[simpleCapitalize(name)];
fn = function() {
return trunc((this.end - this.start) / mult);
};
}
methods[name] = fn;
});
defineOnPrototype(Range, methods);
}
defineStatic(sugarDate, {
/***
* @method range([start], [end])
* @returns Range
* @static
* @short Creates a new date range between [start] and [end].
* @extra Arguments may be either dates or strings which will be forwarded to
* the date constructor (`create` will be used if present in the build).
* If either [start] or [end] are undefined, they will default to the
* current date. This method also accepts an alternate syntax of a
* single string describing the range in natural language. See `ranges`
* for more.
*
* @example
*
* Date.range(jan, may)
* Date.range('today', 'tomorrow')
* Date.range('now', '5 days ago')
* Date.range('last Monday')
* Date.range('Monday to Friday')
* Date.range('tomorrow from 3pm to 5pm')
* Date.range('1 hour starting at 5pm Tuesday')
*
***/
'range': DateRangeConstructor
});
buildDateRangeUnits();
}).call(this);