|
/** |
|
* Miso.Dataset - v0.4.1 - 3/13/2013 |
|
* http://github.com/misoproject/dataset |
|
* Copyright (c) 2013 Alex Graul, Irene Ros; |
|
* Dual Licensed: MIT, GPL |
|
* https://github.com/misoproject/dataset/blob/master/LICENSE-MIT |
|
* https://github.com/misoproject/dataset/blob/master/LICENSE-GPL |
|
*/ |
|
// moment.js |
|
// version : 1.7.2 |
|
// author : Tim Wood |
|
// license : MIT |
|
// momentjs.com |
|
|
|
(function (undefined) { |
|
|
|
/************************************ |
|
Constants |
|
************************************/ |
|
|
|
var moment, |
|
VERSION = "1.7.2", |
|
round = Math.round, i, |
|
// internal storage for language config files |
|
languages = {}, |
|
currentLanguage = 'en', |
|
|
|
// check for nodeJS |
|
hasModule = (typeof module !== 'undefined' && module.exports), |
|
|
|
// Parameters to check for on the lang config. This list of properties |
|
// will be inherited from English if not provided in a language |
|
// definition. monthsParse is also a lang config property, but it |
|
// cannot be inherited and as such cannot be enumerated here. |
|
langConfigProperties = 'months|monthsShort|weekdays|weekdaysShort|weekdaysMin|longDateFormat|calendar|relativeTime|ordinal|meridiem'.split('|'), |
|
|
|
// ASP.NET json date format regex |
|
aspNetJsonRegex = /^\/?Date\((\-?\d+)/i, |
|
|
|
// format tokens |
|
formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|YYYY|YY|a|A|hh?|HH?|mm?|ss?|SS?S?|zz?|ZZ?|.)/g, |
|
localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?)/g, |
|
|
|
// parsing tokens |
|
parseMultipleFormatChunker = /([0-9a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+)/gi, |
|
|
|
// parsing token regexes |
|
parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99 |
|
parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999 |
|
parseTokenThreeDigits = /\d{3}/, // 000 - 999 |
|
parseTokenFourDigits = /\d{1,4}/, // 0 - 9999 |
|
parseTokenWord = /[0-9a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+/i, // any word characters or numbers |
|
parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/i, // +00:00 -00:00 +0000 -0000 or Z |
|
parseTokenT = /T/i, // T (ISO seperator) |
|
|
|
// preliminary iso regex |
|
// 0000-00-00 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 |
|
isoRegex = /^\s*\d{4}-\d\d-\d\d(T(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/, |
|
isoFormat = 'YYYY-MM-DDTHH:mm:ssZ', |
|
|
|
// iso time formats and regexes |
|
isoTimes = [ |
|
['HH:mm:ss.S', /T\d\d:\d\d:\d\d\.\d{1,3}/], |
|
['HH:mm:ss', /T\d\d:\d\d:\d\d/], |
|
['HH:mm', /T\d\d:\d\d/], |
|
['HH', /T\d\d/] |
|
], |
|
|
|
// timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"] |
|
parseTimezoneChunker = /([\+\-]|\d\d)/gi, |
|
|
|
// getter and setter names |
|
proxyGettersAndSetters = 'Month|Date|Hours|Minutes|Seconds|Milliseconds'.split('|'), |
|
unitMillisecondFactors = { |
|
'Milliseconds' : 1, |
|
'Seconds' : 1e3, |
|
'Minutes' : 6e4, |
|
'Hours' : 36e5, |
|
'Days' : 864e5, |
|
'Months' : 2592e6, |
|
'Years' : 31536e6 |
|
}, |
|
|
|
// format function strings |
|
formatFunctions = {}, |
|
|
|
// tokens to ordinalize and pad |
|
ordinalizeTokens = 'DDD w M D d'.split(' '), |
|
paddedTokens = 'M D H h m s w'.split(' '), |
|
|
|
/* |
|
* moment.fn.format uses new Function() to create an inlined formatting function. |
|
* Results are a 3x speed boost |
|
* http://jsperf.com/momentjs-cached-format-functions |
|
* |
|
* These strings are appended into a function using replaceFormatTokens and makeFormatFunction |
|
*/ |
|
formatTokenFunctions = { |
|
// a = placeholder |
|
// b = placeholder |
|
// t = the current moment being formatted |
|
// v = getValueAtKey function |
|
// o = language.ordinal function |
|
// p = leftZeroFill function |
|
// m = language.meridiem value or function |
|
M : function () { |
|
return this.month() + 1; |
|
}, |
|
MMM : function (format) { |
|
return getValueFromArray("monthsShort", this.month(), this, format); |
|
}, |
|
MMMM : function (format) { |
|
return getValueFromArray("months", this.month(), this, format); |
|
}, |
|
D : function () { |
|
return this.date(); |
|
}, |
|
DDD : function () { |
|
var a = new Date(this.year(), this.month(), this.date()), |
|
b = new Date(this.year(), 0, 1); |
|
return ~~(((a - b) / 864e5) + 1.5); |
|
}, |
|
d : function () { |
|
return this.day(); |
|
}, |
|
dd : function (format) { |
|
return getValueFromArray("weekdaysMin", this.day(), this, format); |
|
}, |
|
ddd : function (format) { |
|
return getValueFromArray("weekdaysShort", this.day(), this, format); |
|
}, |
|
dddd : function (format) { |
|
return getValueFromArray("weekdays", this.day(), this, format); |
|
}, |
|
w : function () { |
|
var a = new Date(this.year(), this.month(), this.date() - this.day() + 5), |
|
b = new Date(a.getFullYear(), 0, 4); |
|
return ~~((a - b) / 864e5 / 7 + 1.5); |
|
}, |
|
YY : function () { |
|
return leftZeroFill(this.year() % 100, 2); |
|
}, |
|
YYYY : function () { |
|
return leftZeroFill(this.year(), 4); |
|
}, |
|
a : function () { |
|
return this.lang().meridiem(this.hours(), this.minutes(), true); |
|
}, |
|
A : function () { |
|
return this.lang().meridiem(this.hours(), this.minutes(), false); |
|
}, |
|
H : function () { |
|
return this.hours(); |
|
}, |
|
h : function () { |
|
return this.hours() % 12 || 12; |
|
}, |
|
m : function () { |
|
return this.minutes(); |
|
}, |
|
s : function () { |
|
return this.seconds(); |
|
}, |
|
S : function () { |
|
return ~~(this.milliseconds() / 100); |
|
}, |
|
SS : function () { |
|
return leftZeroFill(~~(this.milliseconds() / 10), 2); |
|
}, |
|
SSS : function () { |
|
return leftZeroFill(this.milliseconds(), 3); |
|
}, |
|
Z : function () { |
|
var a = -this.zone(), |
|
b = "+"; |
|
if (a < 0) { |
|
a = -a; |
|
b = "-"; |
|
} |
|
return b + leftZeroFill(~~(a / 60), 2) + ":" + leftZeroFill(~~a % 60, 2); |
|
}, |
|
ZZ : function () { |
|
var a = -this.zone(), |
|
b = "+"; |
|
if (a < 0) { |
|
a = -a; |
|
b = "-"; |
|
} |
|
return b + leftZeroFill(~~(10 * a / 6), 4); |
|
} |
|
}; |
|
|
|
function getValueFromArray(key, index, m, format) { |
|
var lang = m.lang(); |
|
return lang[key].call ? lang[key](m, format) : lang[key][index]; |
|
} |
|
|
|
function padToken(func, count) { |
|
return function (a) { |
|
return leftZeroFill(func.call(this, a), count); |
|
}; |
|
} |
|
function ordinalizeToken(func) { |
|
return function (a) { |
|
var b = func.call(this, a); |
|
return b + this.lang().ordinal(b); |
|
}; |
|
} |
|
|
|
while (ordinalizeTokens.length) { |
|
i = ordinalizeTokens.pop(); |
|
formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i]); |
|
} |
|
while (paddedTokens.length) { |
|
i = paddedTokens.pop(); |
|
formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2); |
|
} |
|
formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3); |
|
|
|
|
|
/************************************ |
|
Constructors |
|
************************************/ |
|
|
|
|
|
// Moment prototype object |
|
function Moment(date, isUTC, lang) { |
|
this._d = date; |
|
this._isUTC = !!isUTC; |
|
this._a = date._a || null; |
|
this._lang = lang || false; |
|
} |
|
|
|
// Duration Constructor |
|
function Duration(duration) { |
|
var data = this._data = {}, |
|
years = duration.years || duration.y || 0, |
|
months = duration.months || duration.M || 0, |
|
weeks = duration.weeks || duration.w || 0, |
|
days = duration.days || duration.d || 0, |
|
hours = duration.hours || duration.h || 0, |
|
minutes = duration.minutes || duration.m || 0, |
|
seconds = duration.seconds || duration.s || 0, |
|
milliseconds = duration.milliseconds || duration.ms || 0; |
|
|
|
// representation for dateAddRemove |
|
this._milliseconds = milliseconds + |
|
seconds * 1e3 + // 1000 |
|
minutes * 6e4 + // 1000 * 60 |
|
hours * 36e5; // 1000 * 60 * 60 |
|
// Because of dateAddRemove treats 24 hours as different from a |
|
// day when working around DST, we need to store them separately |
|
this._days = days + |
|
weeks * 7; |
|
// It is impossible translate months into days without knowing |
|
// which months you are are talking about, so we have to store |
|
// it separately. |
|
this._months = months + |
|
years * 12; |
|
|
|
// The following code bubbles up values, see the tests for |
|
// examples of what that means. |
|
data.milliseconds = milliseconds % 1000; |
|
seconds += absRound(milliseconds / 1000); |
|
|
|
data.seconds = seconds % 60; |
|
minutes += absRound(seconds / 60); |
|
|
|
data.minutes = minutes % 60; |
|
hours += absRound(minutes / 60); |
|
|
|
data.hours = hours % 24; |
|
days += absRound(hours / 24); |
|
|
|
days += weeks * 7; |
|
data.days = days % 30; |
|
|
|
months += absRound(days / 30); |
|
|
|
data.months = months % 12; |
|
years += absRound(months / 12); |
|
|
|
data.years = years; |
|
|
|
this._lang = false; |
|
} |
|
|
|
|
|
/************************************ |
|
Helpers |
|
************************************/ |
|
|
|
|
|
function absRound(number) { |
|
if (number < 0) { |
|
return Math.ceil(number); |
|
} else { |
|
return Math.floor(number); |
|
} |
|
} |
|
|
|
// left zero fill a number |
|
// see http://jsperf.com/left-zero-filling for performance comparison |
|
function leftZeroFill(number, targetLength) { |
|
var output = number + ''; |
|
while (output.length < targetLength) { |
|
output = '0' + output; |
|
} |
|
return output; |
|
} |
|
|
|
// helper function for _.addTime and _.subtractTime |
|
function addOrSubtractDurationFromMoment(mom, duration, isAdding) { |
|
var ms = duration._milliseconds, |
|
d = duration._days, |
|
M = duration._months, |
|
currentDate; |
|
|
|
if (ms) { |
|
mom._d.setTime(+mom + ms * isAdding); |
|
} |
|
if (d) { |
|
mom.date(mom.date() + d * isAdding); |
|
} |
|
if (M) { |
|
currentDate = mom.date(); |
|
mom.date(1) |
|
.month(mom.month() + M * isAdding) |
|
.date(Math.min(currentDate, mom.daysInMonth())); |
|
} |
|
} |
|
|
|
// check if is an array |
|
function isArray(input) { |
|
return Object.prototype.toString.call(input) === '[object Array]'; |
|
} |
|
|
|
// compare two arrays, return the number of differences |
|
function compareArrays(array1, array2) { |
|
var len = Math.min(array1.length, array2.length), |
|
lengthDiff = Math.abs(array1.length - array2.length), |
|
diffs = 0, |
|
i; |
|
for (i = 0; i < len; i++) { |
|
if (~~array1[i] !== ~~array2[i]) { |
|
diffs++; |
|
} |
|
} |
|
return diffs + lengthDiff; |
|
} |
|
|
|
// convert an array to a date. |
|
// the array should mirror the parameters below |
|
// note: all values past the year are optional and will default to the lowest possible value. |
|
// [year, month, day , hour, minute, second, millisecond] |
|
function dateFromArray(input, asUTC, hoursOffset, minutesOffset) { |
|
var i, date, forValid = []; |
|
for (i = 0; i < 7; i++) { |
|
forValid[i] = input[i] = (input[i] == null) ? (i === 2 ? 1 : 0) : input[i]; |
|
} |
|
// we store whether we used utc or not in the input array |
|
input[7] = forValid[7] = asUTC; |
|
// if the parser flagged the input as invalid, we pass the value along |
|
if (input[8] != null) { |
|
forValid[8] = input[8]; |
|
} |
|
// add the offsets to the time to be parsed so that we can have a clean array |
|
// for checking isValid |
|
input[3] += hoursOffset || 0; |
|
input[4] += minutesOffset || 0; |
|
date = new Date(0); |
|
if (asUTC) { |
|
date.setUTCFullYear(input[0], input[1], input[2]); |
|
date.setUTCHours(input[3], input[4], input[5], input[6]); |
|
} else { |
|
date.setFullYear(input[0], input[1], input[2]); |
|
date.setHours(input[3], input[4], input[5], input[6]); |
|
} |
|
date._a = forValid; |
|
return date; |
|
} |
|
|
|
// Loads a language definition into the `languages` cache. The function |
|
// takes a key and optionally values. If not in the browser and no values |
|
// are provided, it will load the language file module. As a convenience, |
|
// this function also returns the language values. |
|
function loadLang(key, values) { |
|
var i, m, |
|
parse = []; |
|
|
|
if (!values && hasModule) { |
|
values = require('./lang/' + key); |
|
} |
|
|
|
for (i = 0; i < langConfigProperties.length; i++) { |
|
// If a language definition does not provide a value, inherit |
|
// from English |
|
values[langConfigProperties[i]] = values[langConfigProperties[i]] || |
|
languages.en[langConfigProperties[i]]; |
|
} |
|
|
|
for (i = 0; i < 12; i++) { |
|
m = moment([2000, i]); |
|
parse[i] = new RegExp('^' + (values.months[i] || values.months(m, '')) + |
|
'|^' + (values.monthsShort[i] || values.monthsShort(m, '')).replace('.', ''), 'i'); |
|
} |
|
values.monthsParse = values.monthsParse || parse; |
|
|
|
languages[key] = values; |
|
|
|
return values; |
|
} |
|
|
|
// Determines which language definition to use and returns it. |
|
// |
|
// With no parameters, it will return the global language. If you |
|
// pass in a language key, such as 'en', it will return the |
|
// definition for 'en', so long as 'en' has already been loaded using |
|
// moment.lang. If you pass in a moment or duration instance, it |
|
// will decide the language based on that, or default to the global |
|
// language. |
|
function getLangDefinition(m) { |
|
var langKey = (typeof m === 'string') && m || |
|
m && m._lang || |
|
null; |
|
|
|
return langKey ? (languages[langKey] || loadLang(langKey)) : moment; |
|
} |
|
|
|
|
|
/************************************ |
|
Formatting |
|
************************************/ |
|
|
|
|
|
function removeFormattingTokens(input) { |
|
if (input.match(/\[.*\]/)) { |
|
return input.replace(/^\[|\]$/g, ""); |
|
} |
|
return input.replace(/\\/g, ""); |
|
} |
|
|
|
function makeFormatFunction(format) { |
|
var array = format.match(formattingTokens), i, length; |
|
|
|
for (i = 0, length = array.length; i < length; i++) { |
|
if (formatTokenFunctions[array[i]]) { |
|
array[i] = formatTokenFunctions[array[i]]; |
|
} else { |
|
array[i] = removeFormattingTokens(array[i]); |
|
} |
|
} |
|
|
|
return function (mom) { |
|
var output = ""; |
|
for (i = 0; i < length; i++) { |
|
output += typeof array[i].call === 'function' ? array[i].call(mom, format) : array[i]; |
|
} |
|
return output; |
|
}; |
|
} |
|
|
|
// format date using native date object |
|
function formatMoment(m, format) { |
|
var i = 5; |
|
|
|
function replaceLongDateFormatTokens(input) { |
|
return m.lang().longDateFormat[input] || input; |
|
} |
|
|
|
while (i-- && localFormattingTokens.test(format)) { |
|
format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); |
|
} |
|
|
|
if (!formatFunctions[format]) { |
|
formatFunctions[format] = makeFormatFunction(format); |
|
} |
|
|
|
return formatFunctions[format](m); |
|
} |
|
|
|
|
|
/************************************ |
|
Parsing |
|
************************************/ |
|
|
|
|
|
// get the regex to find the next token |
|
function getParseRegexForToken(token) { |
|
switch (token) { |
|
case 'DDDD': |
|
return parseTokenThreeDigits; |
|
case 'YYYY': |
|
return parseTokenFourDigits; |
|
case 'S': |
|
case 'SS': |
|
case 'SSS': |
|
case 'DDD': |
|
return parseTokenOneToThreeDigits; |
|
case 'MMM': |
|
case 'MMMM': |
|
case 'dd': |
|
case 'ddd': |
|
case 'dddd': |
|
case 'a': |
|
case 'A': |
|
return parseTokenWord; |
|
case 'Z': |
|
case 'ZZ': |
|
return parseTokenTimezone; |
|
case 'T': |
|
return parseTokenT; |
|
case 'MM': |
|
case 'DD': |
|
case 'YY': |
|
case 'HH': |
|
case 'hh': |
|
case 'mm': |
|
case 'ss': |
|
case 'M': |
|
case 'D': |
|
case 'd': |
|
case 'H': |
|
case 'h': |
|
case 'm': |
|
case 's': |
|
return parseTokenOneOrTwoDigits; |
|
default : |
|
return new RegExp(token.replace('\\', '')); |
|
} |
|
} |
|
|
|
// function to convert string input to date |
|
function addTimeToArrayFromToken(token, input, datePartArray, config) { |
|
var a, b; |
|
|
|
switch (token) { |
|
// MONTH |
|
case 'M' : // fall through to MM |
|
case 'MM' : |
|
datePartArray[1] = (input == null) ? 0 : ~~input - 1; |
|
break; |
|
case 'MMM' : // fall through to MMMM |
|
case 'MMMM' : |
|
for (a = 0; a < 12; a++) { |
|
if (getLangDefinition().monthsParse[a].test(input)) { |
|
datePartArray[1] = a; |
|
b = true; |
|
break; |
|
} |
|
} |
|
// if we didn't find a month name, mark the date as invalid. |
|
if (!b) { |
|
datePartArray[8] = false; |
|
} |
|
break; |
|
// DAY OF MONTH |
|
case 'D' : // fall through to DDDD |
|
case 'DD' : // fall through to DDDD |
|
case 'DDD' : // fall through to DDDD |
|
case 'DDDD' : |
|
if (input != null) { |
|
datePartArray[2] = ~~input; |
|
} |
|
break; |
|
// YEAR |
|
case 'YY' : |
|
datePartArray[0] = ~~input + (~~input > 70 ? 1900 : 2000); |
|
break; |
|
case 'YYYY' : |
|
datePartArray[0] = ~~Math.abs(input); |
|
break; |
|
// AM / PM |
|
case 'a' : // fall through to A |
|
case 'A' : |
|
config.isPm = ((input + '').toLowerCase() === 'pm'); |
|
break; |
|
// 24 HOUR |
|
case 'H' : // fall through to hh |
|
case 'HH' : // fall through to hh |
|
case 'h' : // fall through to hh |
|
case 'hh' : |
|
datePartArray[3] = ~~input; |
|
break; |
|
// MINUTE |
|
case 'm' : // fall through to mm |
|
case 'mm' : |
|
datePartArray[4] = ~~input; |
|
break; |
|
// SECOND |
|
case 's' : // fall through to ss |
|
case 'ss' : |
|
datePartArray[5] = ~~input; |
|
break; |
|
// MILLISECOND |
|
case 'S' : |
|
case 'SS' : |
|
case 'SSS' : |
|
datePartArray[6] = ~~ (('0.' + input) * 1000); |
|
break; |
|
// TIMEZONE |
|
case 'Z' : // fall through to ZZ |
|
case 'ZZ' : |
|
config.isUTC = true; |
|
a = (input + '').match(parseTimezoneChunker); |
|
if (a && a[1]) { |
|
config.tzh = ~~a[1]; |
|
} |
|
if (a && a[2]) { |
|
config.tzm = ~~a[2]; |
|
} |
|
// reverse offsets |
|
if (a && a[0] === '+') { |
|
config.tzh = -config.tzh; |
|
config.tzm = -config.tzm; |
|
} |
|
break; |
|
} |
|
|
|
// if the input is null, the date is not valid |
|
if (input == null) { |
|
datePartArray[8] = false; |
|
} |
|
} |
|
|
|
// date from string and format string |
|
function makeDateFromStringAndFormat(string, format) { |
|
// This array is used to make a Date, either with `new Date` or `Date.UTC` |
|
// We store some additional data on the array for validation |
|
// datePartArray[7] is true if the Date was created with `Date.UTC` and false if created with `new Date` |
|
// datePartArray[8] is false if the Date is invalid, and undefined if the validity is unknown. |
|
var datePartArray = [0, 0, 1, 0, 0, 0, 0], |
|
config = { |
|
tzh : 0, // timezone hour offset |
|
tzm : 0 // timezone minute offset |
|
}, |
|
tokens = format.match(formattingTokens), |
|
i, parsedInput; |
|
|
|
for (i = 0; i < tokens.length; i++) { |
|
parsedInput = (getParseRegexForToken(tokens[i]).exec(string) || [])[0]; |
|
if (parsedInput) { |
|
string = string.slice(string.indexOf(parsedInput) + parsedInput.length); |
|
} |
|
// don't parse if its not a known token |
|
if (formatTokenFunctions[tokens[i]]) { |
|
addTimeToArrayFromToken(tokens[i], parsedInput, datePartArray, config); |
|
} |
|
} |
|
// handle am pm |
|
if (config.isPm && datePartArray[3] < 12) { |
|
datePartArray[3] += 12; |
|
} |
|
// if is 12 am, change hours to 0 |
|
if (config.isPm === false && datePartArray[3] === 12) { |
|
datePartArray[3] = 0; |
|
} |
|
// return |
|
return dateFromArray(datePartArray, config.isUTC, config.tzh, config.tzm); |
|
} |
|
|
|
// date from string and array of format strings |
|
function makeDateFromStringAndArray(string, formats) { |
|
var output, |
|
inputParts = string.match(parseMultipleFormatChunker) || [], |
|
formattedInputParts, |
|
scoreToBeat = 99, |
|
i, |
|
currentDate, |
|
currentScore; |
|
for (i = 0; i < formats.length; i++) { |
|
currentDate = makeDateFromStringAndFormat(string, formats[i]); |
|
formattedInputParts = formatMoment(new Moment(currentDate), formats[i]).match(parseMultipleFormatChunker) || []; |
|
currentScore = compareArrays(inputParts, formattedInputParts); |
|
if (currentScore < scoreToBeat) { |
|
scoreToBeat = currentScore; |
|
output = currentDate; |
|
} |
|
} |
|
return output; |
|
} |
|
|
|
// date from iso format |
|
function makeDateFromString(string) { |
|
var format = 'YYYY-MM-DDT', |
|
i; |
|
if (isoRegex.exec(string)) { |
|
for (i = 0; i < 4; i++) { |
|
if (isoTimes[i][1].exec(string)) { |
|
format += isoTimes[i][0]; |
|
break; |
|
} |
|
} |
|
return parseTokenTimezone.exec(string) ? |
|
makeDateFromStringAndFormat(string, format + ' Z') : |
|
makeDateFromStringAndFormat(string, format); |
|
} |
|
return new Date(string); |
|
} |
|
|
|
|
|
/************************************ |
|
Relative Time |
|
************************************/ |
|
|
|
|
|
// helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize |
|
function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) { |
|
var rt = lang.relativeTime[string]; |
|
return (typeof rt === 'function') ? |
|
rt(number || 1, !!withoutSuffix, string, isFuture) : |
|
rt.replace(/%d/i, number || 1); |
|
} |
|
|
|
function relativeTime(milliseconds, withoutSuffix, lang) { |
|
var seconds = round(Math.abs(milliseconds) / 1000), |
|
minutes = round(seconds / 60), |
|
hours = round(minutes / 60), |
|
days = round(hours / 24), |
|
years = round(days / 365), |
|
args = seconds < 45 && ['s', seconds] || |
|
minutes === 1 && ['m'] || |
|
minutes < 45 && ['mm', minutes] || |
|
hours === 1 && ['h'] || |
|
hours < 22 && ['hh', hours] || |
|
days === 1 && ['d'] || |
|
days <= 25 && ['dd', days] || |
|
days <= 45 && ['M'] || |
|
days < 345 && ['MM', round(days / 30)] || |
|
years === 1 && ['y'] || ['yy', years]; |
|
args[2] = withoutSuffix; |
|
args[3] = milliseconds > 0; |
|
args[4] = lang; |
|
return substituteTimeAgo.apply({}, args); |
|
} |
|
|
|
|
|
/************************************ |
|
Top Level Functions |
|
************************************/ |
|
|
|
|
|
moment = function (input, format) { |
|
if (input === null || input === '') { |
|
return null; |
|
} |
|
var date, |
|
matched; |
|
// parse Moment object |
|
if (moment.isMoment(input)) { |
|
return new Moment(new Date(+input._d), input._isUTC, input._lang); |
|
// parse string and format |
|
} else if (format) { |
|
if (isArray(format)) { |
|
date = makeDateFromStringAndArray(input, format); |
|
} else { |
|
date = makeDateFromStringAndFormat(input, format); |
|
} |
|
// evaluate it as a JSON-encoded date |
|
} else { |
|
matched = aspNetJsonRegex.exec(input); |
|
date = input === undefined ? new Date() : |
|
matched ? new Date(+matched[1]) : |
|
input instanceof Date ? input : |
|
isArray(input) ? dateFromArray(input) : |
|
typeof input === 'string' ? makeDateFromString(input) : |
|
new Date(input); |
|
} |
|
|
|
return new Moment(date); |
|
}; |
|
|
|
// creating with utc |
|
moment.utc = function (input, format) { |
|
if (isArray(input)) { |
|
return new Moment(dateFromArray(input, true), true); |
|
} |
|
// if we don't have a timezone, we need to add one to trigger parsing into utc |
|
if (typeof input === 'string' && !parseTokenTimezone.exec(input)) { |
|
input += ' +0000'; |
|
if (format) { |
|
format += ' Z'; |
|
} |
|
} |
|
return moment(input, format).utc(); |
|
}; |
|
|
|
// creating with unix timestamp (in seconds) |
|
moment.unix = function (input) { |
|
return moment(input * 1000); |
|
}; |
|
|
|
// duration |
|
moment.duration = function (input, key) { |
|
var isDuration = moment.isDuration(input), |
|
isNumber = (typeof input === 'number'), |
|
duration = (isDuration ? input._data : (isNumber ? {} : input)), |
|
ret; |
|
|
|
if (isNumber) { |
|
if (key) { |
|
duration[key] = input; |
|
} else { |
|
duration.milliseconds = input; |
|
} |
|
} |
|
|
|
ret = new Duration(duration); |
|
|
|
if (isDuration) { |
|
ret._lang = input._lang; |
|
} |
|
|
|
return ret; |
|
}; |
|
|
|
// humanizeDuration |
|
// This method is deprecated in favor of the new Duration object. Please |
|
// see the moment.duration method. |
|
moment.humanizeDuration = function (num, type, withSuffix) { |
|
return moment.duration(num, type === true ? null : type).humanize(type === true ? true : withSuffix); |
|
}; |
|
|
|
// version number |
|
moment.version = VERSION; |
|
|
|
// default format |
|
moment.defaultFormat = isoFormat; |
|
|
|
// This function will load languages and then set the global language. If |
|
// no arguments are passed in, it will simply return the current global |
|
// language key. |
|
moment.lang = function (key, values) { |
|
var i; |
|
|
|
if (!key) { |
|
return currentLanguage; |
|
} |
|
if (values || !languages[key]) { |
|
loadLang(key, values); |
|
} |
|
if (languages[key]) { |
|
// deprecated, to get the language definition variables, use the |
|
// moment.fn.lang method or the getLangDefinition function. |
|
for (i = 0; i < langConfigProperties.length; i++) { |
|
moment[langConfigProperties[i]] = languages[key][langConfigProperties[i]]; |
|
} |
|
moment.monthsParse = languages[key].monthsParse; |
|
currentLanguage = key; |
|
} |
|
}; |
|
|
|
// returns language data |
|
moment.langData = getLangDefinition; |
|
|
|
// compare moment object |
|
moment.isMoment = function (obj) { |
|
return obj instanceof Moment; |
|
}; |
|
|
|
// for typechecking Duration objects |
|
moment.isDuration = function (obj) { |
|
return obj instanceof Duration; |
|
}; |
|
|
|
// Set default language, other languages will inherit from English. |
|
moment.lang('en', { |
|
months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"), |
|
monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"), |
|
weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"), |
|
weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"), |
|
weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"), |
|
longDateFormat : { |
|
LT : "h:mm A", |
|
L : "MM/DD/YYYY", |
|
LL : "MMMM D YYYY", |
|
LLL : "MMMM D YYYY LT", |
|
LLLL : "dddd, MMMM D YYYY LT" |
|
}, |
|
meridiem : function (hours, minutes, isLower) { |
|
if (hours > 11) { |
|
return isLower ? 'pm' : 'PM'; |
|
} else { |
|
return isLower ? 'am' : 'AM'; |
|
} |
|
}, |
|
calendar : { |
|
sameDay : '[Today at] LT', |
|
nextDay : '[Tomorrow at] LT', |
|
nextWeek : 'dddd [at] LT', |
|
lastDay : '[Yesterday at] LT', |
|
lastWeek : '[last] dddd [at] LT', |
|
sameElse : 'L' |
|
}, |
|
relativeTime : { |
|
future : "in %s", |
|
past : "%s ago", |
|
s : "a few seconds", |
|
m : "a minute", |
|
mm : "%d minutes", |
|
h : "an hour", |
|
hh : "%d hours", |
|
d : "a day", |
|
dd : "%d days", |
|
M : "a month", |
|
MM : "%d months", |
|
y : "a year", |
|
yy : "%d years" |
|
}, |
|
ordinal : function (number) { |
|
var b = number % 10; |
|
return (~~ (number % 100 / 10) === 1) ? 'th' : |
|
(b === 1) ? 'st' : |
|
(b === 2) ? 'nd' : |
|
(b === 3) ? 'rd' : 'th'; |
|
} |
|
}); |
|
|
|
|
|
/************************************ |
|
Moment Prototype |
|
************************************/ |
|
|
|
|
|
moment.fn = Moment.prototype = { |
|
|
|
clone : function () { |
|
return moment(this); |
|
}, |
|
|
|
valueOf : function () { |
|
return +this._d; |
|
}, |
|
|
|
unix : function () { |
|
return Math.floor(+this._d / 1000); |
|
}, |
|
|
|
toString : function () { |
|
return this._d.toString(); |
|
}, |
|
|
|
toDate : function () { |
|
return this._d; |
|
}, |
|
|
|
toArray : function () { |
|
var m = this; |
|
return [ |
|
m.year(), |
|
m.month(), |
|
m.date(), |
|
m.hours(), |
|
m.minutes(), |
|
m.seconds(), |
|
m.milliseconds(), |
|
!!this._isUTC |
|
]; |
|
}, |
|
|
|
isValid : function () { |
|
if (this._a) { |
|
// if the parser finds that the input is invalid, it sets |
|
// the eighth item in the input array to false. |
|
if (this._a[8] != null) { |
|
return !!this._a[8]; |
|
} |
|
return !compareArrays(this._a, (this._a[7] ? moment.utc(this._a) : moment(this._a)).toArray()); |
|
} |
|
return !isNaN(this._d.getTime()); |
|
}, |
|
|
|
utc : function () { |
|
this._isUTC = true; |
|
return this; |
|
}, |
|
|
|
local : function () { |
|
this._isUTC = false; |
|
return this; |
|
}, |
|
|
|
format : function (inputString) { |
|
return formatMoment(this, inputString ? inputString : moment.defaultFormat); |
|
}, |
|
|
|
add : function (input, val) { |
|
var dur = val ? moment.duration(+val, input) : moment.duration(input); |
|
addOrSubtractDurationFromMoment(this, dur, 1); |
|
return this; |
|
}, |
|
|
|
subtract : function (input, val) { |
|
var dur = val ? moment.duration(+val, input) : moment.duration(input); |
|
addOrSubtractDurationFromMoment(this, dur, -1); |
|
return this; |
|
}, |
|
|
|
diff : function (input, val, asFloat) { |
|
var inputMoment = this._isUTC ? moment(input).utc() : moment(input).local(), |
|
zoneDiff = (this.zone() - inputMoment.zone()) * 6e4, |
|
diff = this._d - inputMoment._d - zoneDiff, |
|
year = this.year() - inputMoment.year(), |
|
month = this.month() - inputMoment.month(), |
|
date = this.date() - inputMoment.date(), |
|
output; |
|
if (val === 'months') { |
|
output = year * 12 + month + date / 30; |
|
} else if (val === 'years') { |
|
output = year + (month + date / 30) / 12; |
|
} else { |
|
output = val === 'seconds' ? diff / 1e3 : // 1000 |
|
val === 'minutes' ? diff / 6e4 : // 1000 * 60 |
|
val === 'hours' ? diff / 36e5 : // 1000 * 60 * 60 |
|
val === 'days' ? diff / 864e5 : // 1000 * 60 * 60 * 24 |
|
val === 'weeks' ? diff / 6048e5 : // 1000 * 60 * 60 * 24 * 7 |
|
diff; |
|
} |
|
return asFloat ? output : round(output); |
|
}, |
|
|
|
from : function (time, withoutSuffix) { |
|
return moment.duration(this.diff(time)).lang(this._lang).humanize(!withoutSuffix); |
|
}, |
|
|
|
fromNow : function (withoutSuffix) { |
|
return this.from(moment(), withoutSuffix); |
|
}, |
|
|
|
calendar : function () { |
|
var diff = this.diff(moment().sod(), 'days', true), |
|
calendar = this.lang().calendar, |
|
allElse = calendar.sameElse, |
|
format = diff < -6 ? allElse : |
|
diff < -1 ? calendar.lastWeek : |
|
diff < 0 ? calendar.lastDay : |
|
diff < 1 ? calendar.sameDay : |
|
diff < 2 ? calendar.nextDay : |
|
diff < 7 ? calendar.nextWeek : allElse; |
|
return this.format(typeof format === 'function' ? format.apply(this) : format); |
|
}, |
|
|
|
isLeapYear : function () { |
|
var year = this.year(); |
|
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; |
|
}, |
|
|
|
isDST : function () { |
|
return (this.zone() < moment([this.year()]).zone() || |
|
this.zone() < moment([this.year(), 5]).zone()); |
|
}, |
|
|
|
day : function (input) { |
|
var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); |
|
return input == null ? day : |
|
this.add({ d : input - day }); |
|
}, |
|
|
|
startOf: function (val) { |
|
// the following switch intentionally omits break keywords |
|
// to utilize falling through the cases. |
|
switch (val.replace(/s$/, '')) { |
|
case 'year': |
|
this.month(0); |
|
/* falls through */ |
|
case 'month': |
|
this.date(1); |
|
/* falls through */ |
|
case 'day': |
|
this.hours(0); |
|
/* falls through */ |
|
case 'hour': |
|
this.minutes(0); |
|
/* falls through */ |
|
case 'minute': |
|
this.seconds(0); |
|
/* falls through */ |
|
case 'second': |
|
this.milliseconds(0); |
|
/* falls through */ |
|
} |
|
return this; |
|
}, |
|
|
|
endOf: function (val) { |
|
return this.startOf(val).add(val.replace(/s?$/, 's'), 1).subtract('ms', 1); |
|
}, |
|
|
|
sod: function () { |
|
return this.clone().startOf('day'); |
|
}, |
|
|
|
eod: function () { |
|
// end of day = start of day plus 1 day, minus 1 millisecond |
|
return this.clone().endOf('day'); |
|
}, |
|
|
|
zone : function () { |
|
return this._isUTC ? 0 : this._d.getTimezoneOffset(); |
|
}, |
|
|
|
daysInMonth : function () { |
|
return moment.utc([this.year(), this.month() + 1, 0]).date(); |
|
}, |
|
|
|
// If passed a language key, it will set the language for this |
|
// instance. Otherwise, it will return the language configuration |
|
// variables for this instance. |
|
lang : function (lang) { |
|
if (lang === undefined) { |
|
return getLangDefinition(this); |
|
} else { |
|
this._lang = lang; |
|
return this; |
|
} |
|
} |
|
}; |
|
|
|
// helper for adding shortcuts |
|
function makeGetterAndSetter(name, key) { |
|
moment.fn[name] = function (input) { |
|
var utc = this._isUTC ? 'UTC' : ''; |
|
if (input != null) { |
|
this._d['set' + utc + key](input); |
|
return this; |
|
} else { |
|
return this._d['get' + utc + key](); |
|
} |
|
}; |
|
} |
|
|
|
// loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds) |
|
for (i = 0; i < proxyGettersAndSetters.length; i ++) { |
|
makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase(), proxyGettersAndSetters[i]); |
|
} |
|
|
|
// add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear') |
|
makeGetterAndSetter('year', 'FullYear'); |
|
|
|
|
|
/************************************ |
|
Duration Prototype |
|
************************************/ |
|
|
|
|
|
moment.duration.fn = Duration.prototype = { |
|
weeks : function () { |
|
return absRound(this.days() / 7); |
|
}, |
|
|
|
valueOf : function () { |
|
return this._milliseconds + |
|
this._days * 864e5 + |
|
this._months * 2592e6; |
|
}, |
|
|
|
humanize : function (withSuffix) { |
|
var difference = +this, |
|
rel = this.lang().relativeTime, |
|
output = relativeTime(difference, !withSuffix, this.lang()), |
|
fromNow = difference <= 0 ? rel.past : rel.future; |
|
|
|
if (withSuffix) { |
|
if (typeof fromNow === 'function') { |
|
output = fromNow(output); |
|
} else { |
|
output = fromNow.replace(/%s/i, output); |
|
} |
|
} |
|
|
|
return output; |
|
}, |
|
|
|
lang : moment.fn.lang |
|
}; |
|
|
|
function makeDurationGetter(name) { |
|
moment.duration.fn[name] = function () { |
|
return this._data[name]; |
|
}; |
|
} |
|
|
|
function makeDurationAsGetter(name, factor) { |
|
moment.duration.fn['as' + name] = function () { |
|
return +this / factor; |
|
}; |
|
} |
|
|
|
for (i in unitMillisecondFactors) { |
|
if (unitMillisecondFactors.hasOwnProperty(i)) { |
|
makeDurationAsGetter(i, unitMillisecondFactors[i]); |
|
makeDurationGetter(i.toLowerCase()); |
|
} |
|
} |
|
|
|
makeDurationAsGetter('Weeks', 6048e5); |
|
|
|
|
|
/************************************ |
|
Exposing Moment |
|
************************************/ |
|
|
|
|
|
// CommonJS module is defined |
|
if (hasModule) { |
|
module.exports = moment; |
|
} |
|
/*global ender:false */ |
|
if (typeof ender === 'undefined') { |
|
// here, `this` means `window` in the browser, or `global` on the server |
|
// add `moment` as a global object via a string identifier, |
|
// for Closure Compiler "advanced" mode |
|
this['moment'] = moment; |
|
} |
|
/*global define:false */ |
|
if (typeof define === "function" && define.amd) { |
|
define("moment", [], function () { |
|
return moment; |
|
}); |
|
} |
|
}).call(this); |
|
/*! |
|
* Lo-Dash v0.9.0 <http://lodash.com> |
|
* (c) 2012 John-David Dalton <http://allyoucanleet.com/> |
|
* Based on Underscore.js 1.4.2 <http://underscorejs.org> |
|
* (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. |
|
* Available under MIT license <http://lodash.com/license> |
|
*/ |
|
;(function(window, undefined) { |
|
|
|
/** Detect free variable `exports` */ |
|
var freeExports = typeof exports == 'object' && exports; |
|
|
|
/** Detect free variable `global` and use it as `window` */ |
|
var freeGlobal = typeof global == 'object' && global; |
|
if (freeGlobal.global === freeGlobal) { |
|
window = freeGlobal; |
|
} |
|
|
|
/** Used for array and object method references */ |
|
var arrayRef = [], |
|
objectRef = {}; |
|
|
|
/** Used to generate unique IDs */ |
|
var idCounter = 0; |
|
|
|
/** Used internally to indicate various things */ |
|
var indicatorObject = {}; |
|
|
|
/** Used by `cachedContains` as the default size when optimizations are enabled for large arrays */ |
|
var largeArraySize = 30; |
|
|
|
/** Used to restore the original `_` reference in `noConflict` */ |
|
var oldDash = window._; |
|
|
|
/** Used to detect delimiter values that should be processed by `tokenizeEvaluate` */ |
|
var reComplexDelimiter = /[-?+=!~*%&^<>|{(\/]|\[\D|\b(?:delete|in|instanceof|new|typeof|void)\b/; |
|
|
|
/** Used to match HTML entities */ |
|
var reEscapedHtml = /&(?:amp|lt|gt|quot|#x27);/g; |
|
|
|
/** Used to match empty string literals in compiled template source */ |
|
var reEmptyStringLeading = /\b__p \+= '';/g, |
|
reEmptyStringMiddle = /\b(__p \+=) '' \+/g, |
|
reEmptyStringTrailing = /(__e\(.*?\)|\b__t\)) \+\n'';/g; |
|
|
|
/** Used to match regexp flags from their coerced string values */ |
|
var reFlags = /\w*$/; |
|
|
|
/** Used to insert the data object variable into compiled template source */ |
|
var reInsertVariable = /(?:__e|__t = )\(\s*(?![\d\s"']|this\.)/g; |
|
|
|
/** Used to detect if a method is native */ |
|
var reNative = RegExp('^' + |
|
(objectRef.valueOf + '') |
|
.replace(/[.*+?^=!:${}()|[\]\/\\]/g, '\\$&') |
|
.replace(/valueOf|for [^\]]+/g, '.+?') + '$' |
|
); |
|
|
|
/** Used to ensure capturing order and avoid matches for undefined delimiters */ |
|
var reNoMatch = /($^)/; |
|
|
|
/** Used to match HTML characters */ |
|
var reUnescapedHtml = /[&<>"']/g; |
|
|
|
/** Used to match unescaped characters in compiled string literals */ |
|
var reUnescapedString = /['\n\r\t\u2028\u2029\\]/g; |
|
|
|
/** Used to fix the JScript [[DontEnum]] bug */ |
|
var shadowed = [ |
|
'constructor', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', |
|
'toLocaleString', 'toString', 'valueOf' |
|
]; |
|
|
|
/** Used to make template sourceURLs easier to identify */ |
|
var templateCounter = 0; |
|
|
|
/** Native method shortcuts */ |
|
var ceil = Math.ceil, |
|
concat = arrayRef.concat, |
|
floor = Math.floor, |
|
getPrototypeOf = reNative.test(getPrototypeOf = Object.getPrototypeOf) && getPrototypeOf, |
|
hasOwnProperty = objectRef.hasOwnProperty, |
|
push = arrayRef.push, |
|
propertyIsEnumerable = objectRef.propertyIsEnumerable, |
|
slice = arrayRef.slice, |
|
toString = objectRef.toString; |
|
|
|
/* Native method shortcuts for methods with the same name as other `lodash` methods */ |
|
var nativeBind = reNative.test(nativeBind = slice.bind) && nativeBind, |
|
nativeIsArray = reNative.test(nativeIsArray = Array.isArray) && nativeIsArray, |
|
nativeIsFinite = window.isFinite, |
|
nativeKeys = reNative.test(nativeKeys = Object.keys) && nativeKeys, |
|
nativeMax = Math.max, |
|
nativeMin = Math.min, |
|
nativeRandom = Math.random; |
|
|
|
/** `Object#toString` result shortcuts */ |
|
var argsClass = '[object Arguments]', |
|
arrayClass = '[object Array]', |
|
boolClass = '[object Boolean]', |
|
dateClass = '[object Date]', |
|
funcClass = '[object Function]', |
|
numberClass = '[object Number]', |
|
objectClass = '[object Object]', |
|
regexpClass = '[object RegExp]', |
|
stringClass = '[object String]'; |
|
|
|
/** |
|
* Detect the JScript [[DontEnum]] bug: |
|
* |
|
* In IE < 9 an objects own properties, shadowing non-enumerable ones, are |
|
* made non-enumerable as well. |
|
*/ |
|
var hasDontEnumBug; |
|
|
|
/** Detect if own properties are iterated after inherited properties (IE < 9) */ |
|
var iteratesOwnLast; |
|
|
|
/** |
|
* Detect if `Array#shift` and `Array#splice` augment array-like objects |
|
* incorrectly: |
|
* |
|
* Firefox < 10, IE compatibility mode, and IE < 9 have buggy Array `shift()` |
|
* and `splice()` functions that fail to remove the last element, `value[0]`, |
|
* of array-like objects even though the `length` property is set to `0`. |
|
* The `shift()` method is buggy in IE 8 compatibility mode, while `splice()` |
|
* is buggy regardless of mode in IE < 9 and buggy in compatibility mode in IE 9. |
|
*/ |
|
var hasObjectSpliceBug = (hasObjectSpliceBug = { '0': 1, 'length': 1 }, |
|
arrayRef.splice.call(hasObjectSpliceBug, 0, 1), hasObjectSpliceBug[0]); |
|
|
|
/** Detect if an `arguments` object's indexes are non-enumerable (IE < 9) */ |
|
var noArgsEnum = true; |
|
|
|
(function() { |
|
var props = []; |
|
function ctor() { this.x = 1; } |
|
ctor.prototype = { 'valueOf': 1, 'y': 1 }; |
|
for (var prop in new ctor) { props.push(prop); } |
|
for (prop in arguments) { noArgsEnum = !prop; } |
|
|
|
hasDontEnumBug = !/valueOf/.test(props); |
|
iteratesOwnLast = props[0] != 'x'; |
|
}(1)); |
|
|
|
/** Detect if an `arguments` object's [[Class]] is unresolvable (Firefox < 4, IE < 9) */ |
|
var noArgsClass = !isArguments(arguments); |
|
|
|
/** Detect if `Array#slice` cannot be used to convert strings to arrays (Opera < 10.52) */ |
|
var noArraySliceOnStrings = slice.call('x')[0] != 'x'; |
|
|
|
/** |
|
* Detect lack of support for accessing string characters by index: |
|
* |
|
* IE < 8 can't access characters by index and IE 8 can only access |
|
* characters by index on string literals. |
|
*/ |
|
var noCharByIndex = ('x'[0] + Object('x')[0]) != 'xx'; |
|
|
|
/** |
|
* Detect if a node's [[Class]] is unresolvable (IE < 9) |
|
* and that the JS engine won't error when attempting to coerce an object to |
|
* a string without a `toString` property value of `typeof` "function". |
|
*/ |
|
try { |
|
var noNodeClass = ({ 'toString': 0 } + '', toString.call(window.document || 0) == objectClass); |
|
} catch(e) { } |
|
|
|
/* Detect if `Function#bind` exists and is inferred to be fast (all but V8) */ |
|
var isBindFast = nativeBind && /\n|Opera/.test(nativeBind + toString.call(window.opera)); |
|
|
|
/* Detect if `Object.keys` exists and is inferred to be fast (IE, Opera, V8) */ |
|
var isKeysFast = nativeKeys && /^.+$|true/.test(nativeKeys + !!window.attachEvent); |
|
|
|
/** |
|
* Detect if sourceURL syntax is usable without erroring: |
|
* |
|
* The JS engine in Adobe products, like InDesign, will throw a syntax error |
|
* when it encounters a single line comment beginning with the `@` symbol. |
|
* |
|
* The JS engine in Narwhal will generate the function `function anonymous(){//}` |
|
* and throw a syntax error. |
|
* |
|
* Avoid comments beginning `@` symbols in IE because they are part of its |
|
* non-standard conditional compilation support. |
|
* http://msdn.microsoft.com/en-us/library/121hztk3(v=vs.94).aspx |
|
*/ |
|
try { |
|
var useSourceURL = (Function('//@')(), !window.attachEvent); |
|
} catch(e) { } |
|
|
|
/** Used to identify object classifications that `_.clone` supports */ |
|
var cloneableClasses = {}; |
|
cloneableClasses[argsClass] = cloneableClasses[funcClass] = false; |
|
cloneableClasses[arrayClass] = cloneableClasses[boolClass] = cloneableClasses[dateClass] = |
|
cloneableClasses[numberClass] = cloneableClasses[objectClass] = cloneableClasses[regexpClass] = |
|
cloneableClasses[stringClass] = true; |
|
|
|
/** Used to determine if values are of the language type Object */ |
|
var objectTypes = { |
|
'boolean': false, |
|
'function': true, |
|
'object': true, |
|
'number': false, |
|
'string': false, |
|
'undefined': false |
|
}; |
|
|
|
/** Used to escape characters for inclusion in compiled string literals */ |
|
var stringEscapes = { |
|
'\\': '\\', |
|
"'": "'", |
|
'\n': 'n', |
|
'\r': 'r', |
|
'\t': 't', |
|
'\u2028': 'u2028', |
|
'\u2029': 'u2029' |
|
}; |
|
|
|
/*--------------------------------------------------------------------------*/ |
|
|
|
/** |
|
* The `lodash` function. |
|
* |
|
* @name _ |
|
* @constructor |
|
* @category Chaining |
|
* @param {Mixed} value The value to wrap in a `lodash` instance. |
|
* @returns {Object} Returns a `lodash` instance. |
|
*/ |
|
function lodash(value) { |
|
// exit early if already wrapped |
|
if (value && value.__wrapped__) { |
|
return value; |
|
} |
|
// allow invoking `lodash` without the `new` operator |
|
if (!(this instanceof lodash)) { |
|
return new lodash(value); |
|
} |
|
this.__wrapped__ = value; |
|
} |
|
|
|
/** |
|
* By default, the template delimiters used by Lo-Dash are similar to those in |
|
* embedded Ruby (ERB). Change the following template settings to use alternative |
|
* delimiters. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @type Object |
|
*/ |
|
lodash.templateSettings = { |
|
|
|
/** |
|
* Used to detect `data` property values to be HTML-escaped. |
|
* |
|
* @static |
|
* @memberOf _.templateSettings |
|
* @type RegExp |
|
*/ |
|
'escape': /<%-([\s\S]+?)%>/g, |
|
|
|
/** |
|
* Used to detect code to be evaluated. |
|
* |
|
* @static |
|
* @memberOf _.templateSettings |
|
* @type RegExp |
|
*/ |
|
'evaluate': /<%([\s\S]+?)%>/g, |
|
|
|
/** |
|
* Used to detect `data` property values to inject. |
|
* |
|
* @static |
|
* @memberOf _.templateSettings |
|
* @type RegExp |
|
*/ |
|
'interpolate': /<%=([\s\S]+?)%>/g, |
|
|
|
/** |
|
* Used to reference the data object in the template text. |
|
* |
|
* @static |
|
* @memberOf _.templateSettings |
|
* @type String |
|
*/ |
|
'variable': '' |
|
}; |
|
|
|
/*--------------------------------------------------------------------------*/ |
|
|
|
/** |
|
* The template used to create iterator functions. |
|
* |
|
* @private |
|
* @param {Obect} data The data object used to populate the text. |
|
* @returns {String} Returns the interpolated text. |
|
*/ |
|
var iteratorTemplate = template( |
|
// conditional strict mode |
|
'<% if (obj.useStrict) { %>\'use strict\';\n<% } %>' + |
|
|
|
// the `iteratee` may be reassigned by the `top` snippet |
|
'var index, value, iteratee = <%= firstArg %>, ' + |
|
// assign the `result` variable an initial value |
|
'result = <%= firstArg %>;\n' + |
|
// exit early if the first argument is falsey |
|
'if (!<%= firstArg %>) return result;\n' + |
|
// add code before the iteration branches |
|
'<%= top %>;\n' + |
|
|
|
// array-like iteration: |
|
'<% if (arrayLoop) { %>' + |
|
'var length = iteratee.length; index = -1;\n' + |
|
'if (typeof length == \'number\') {' + |
|
|
|
// add support for accessing string characters by index if needed |
|
' <% if (noCharByIndex) { %>\n' + |
|
' if (toString.call(iteratee) == stringClass) {\n' + |
|
' iteratee = iteratee.split(\'\')\n' + |
|
' }' + |
|
' <% } %>\n' + |
|
|
|
// iterate over the array-like value |
|
' while (++index < length) {\n' + |
|
' value = iteratee[index];\n' + |
|
' <%= arrayLoop %>\n' + |
|
' }\n' + |
|
'}\n' + |
|
'else {' + |
|
|
|
// object iteration: |
|
// add support for iterating over `arguments` objects if needed |
|
' <% } else if (noArgsEnum) { %>\n' + |
|
' var length = iteratee.length; index = -1;\n' + |
|
' if (length && isArguments(iteratee)) {\n' + |
|
' while (++index < length) {\n' + |
|
' value = iteratee[index += \'\'];\n' + |
|
' <%= objectLoop %>\n' + |
|
' }\n' + |
|
' } else {' + |
|
' <% } %>' + |
|
|
|
// Firefox < 3.6, Opera > 9.50 - Opera < 11.60, and Safari < 5.1 |
|
// (if the prototype or a property on the prototype has been set) |
|
// incorrectly sets a function's `prototype` property [[Enumerable]] |
|
// value to `true`. Because of this Lo-Dash standardizes on skipping |
|
// the the `prototype` property of functions regardless of its |
|
// [[Enumerable]] value. |
|
' <% if (!hasDontEnumBug) { %>\n' + |
|
' var skipProto = typeof iteratee == \'function\' && \n' + |
|
' propertyIsEnumerable.call(iteratee, \'prototype\');\n' + |
|
' <% } %>' + |
|
|
|
// iterate own properties using `Object.keys` if it's fast |
|
' <% if (isKeysFast && useHas) { %>\n' + |
|
' var ownIndex = -1,\n' + |
|
' ownProps = objectTypes[typeof iteratee] ? nativeKeys(iteratee) : [],\n' + |
|
' length = ownProps.length;\n\n' + |
|
' while (++ownIndex < length) {\n' + |
|
' index = ownProps[ownIndex];\n' + |
|
' <% if (!hasDontEnumBug) { %>if (!(skipProto && index == \'prototype\')) {\n <% } %>' + |
|
' value = iteratee[index];\n' + |
|
' <%= objectLoop %>\n' + |
|
' <% if (!hasDontEnumBug) { %>}\n<% } %>' + |
|
' }' + |
|
|
|
// else using a for-in loop |
|
' <% } else { %>\n' + |
|
' for (index in iteratee) {<%' + |
|
' if (!hasDontEnumBug || useHas) { %>\n if (<%' + |
|
' if (!hasDontEnumBug) { %>!(skipProto && index == \'prototype\')<% }' + |
|
' if (!hasDontEnumBug && useHas) { %> && <% }' + |
|
' if (useHas) { %>hasOwnProperty.call(iteratee, index)<% }' + |
|
' %>) {' + |
|
' <% } %>\n' + |
|
' value = iteratee[index];\n' + |
|
' <%= objectLoop %>;' + |
|
' <% if (!hasDontEnumBug || useHas) { %>\n }<% } %>\n' + |
|
' }' + |
|
' <% } %>' + |
|
|
|
// Because IE < 9 can't set the `[[Enumerable]]` attribute of an |
|
// existing property and the `constructor` property of a prototype |
|
// defaults to non-enumerable, Lo-Dash skips the `constructor` |
|
// property when it infers it's iterating over a `prototype` object. |
|
' <% if (hasDontEnumBug) { %>\n\n' + |
|
' var ctor = iteratee.constructor;\n' + |
|
' <% for (var k = 0; k < 7; k++) { %>\n' + |
|
' index = \'<%= shadowed[k] %>\';\n' + |
|
' if (<%' + |
|
' if (shadowed[k] == \'constructor\') {' + |
|
' %>!(ctor && ctor.prototype === iteratee) && <%' + |
|
' } %>hasOwnProperty.call(iteratee, index)) {\n' + |
|
' value = iteratee[index];\n' + |
|
' <%= objectLoop %>\n' + |
|
' }' + |
|
' <% } %>' + |
|
' <% } %>' + |
|
' <% if (arrayLoop || noArgsEnum) { %>\n}<% } %>\n' + |
|
|
|
// add code to the bottom of the iteration function |
|
'<%= bottom %>;\n' + |
|
// finally, return the `result` |
|
'return result' |
|
); |
|
|
|
/** |
|
* Reusable iterator options shared by `forEach`, `forIn`, and `forOwn`. |
|
*/ |
|
var forEachIteratorOptions = { |
|
'args': 'collection, callback, thisArg', |
|
'top': 'callback = createCallback(callback, thisArg)', |
|
'arrayLoop': 'if (callback(value, index, collection) === false) return result', |
|
'objectLoop': 'if (callback(value, index, collection) === false) return result' |
|
}; |
|
|
|
/** Reusable iterator options for `defaults`, and `extend` */ |
|
var extendIteratorOptions = { |
|
'useHas': false, |
|
'args': 'object', |
|
'top': |
|
'for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) {\n' + |
|
' if (iteratee = arguments[argsIndex]) {', |
|
'objectLoop': 'result[index] = value', |
|
'bottom': ' }\n}' |
|
}; |
|
|
|
/** Reusable iterator options for `forIn` and `forOwn` */ |
|
var forOwnIteratorOptions = { |
|
'arrayLoop': null |
|
}; |
|
|
|
/*--------------------------------------------------------------------------*/ |
|
|
|
/** |
|
* Creates a function optimized for searching large arrays for a given `value`, |
|
* starting at `fromIndex`, using strict equality for comparisons, i.e. `===`. |
|
* |
|
* @private |
|
* @param {Array} array The array to search. |
|
* @param {Mixed} value The value to search for. |
|
* @param {Number} [fromIndex=0] The index to start searching from. |
|
* @param {Number} [largeSize=30] The length at which an array is considered large. |
|
* @returns {Boolean} Returns `true` if `value` is found, else `false`. |
|
*/ |
|
function cachedContains(array, fromIndex, largeSize) { |
|
fromIndex || (fromIndex = 0); |
|
|
|
var length = array.length, |
|
isLarge = (length - fromIndex) >= (largeSize || largeArraySize), |
|
cache = isLarge ? {} : array; |
|
|
|
if (isLarge) { |
|
// init value cache |
|
var index = fromIndex - 1; |
|
while (++index < length) { |
|
// manually coerce `value` to string because `hasOwnProperty`, in some |
|
// older versions of Firefox, coerces objects incorrectly |
|
var key = array[index] + ''; |
|
(hasOwnProperty.call(cache, key) ? cache[key] : (cache[key] = [])).push(array[index]); |
|
} |
|
} |
|
return function(value) { |
|
if (isLarge) { |
|
var key = value + ''; |
|
return hasOwnProperty.call(cache, key) && indexOf(cache[key], value) > -1; |
|
} |
|
return indexOf(cache, value, fromIndex) > -1; |
|
} |
|
} |
|
|
|
/** |
|
* Used by `sortBy` to compare transformed `collection` values, stable sorting |
|
* them in ascending order. |
|
* |
|
* @private |
|
* @param {Object} a The object to compare to `b`. |
|
* @param {Object} b The object to compare to `a`. |
|
* @returns {Number} Returns the sort order indicator of `1` or `-1`. |
|
*/ |
|
function compareAscending(a, b) { |
|
var ai = a.index, |
|
bi = b.index; |
|
|
|
a = a.criteria; |
|
b = b.criteria; |
|
|
|
// ensure a stable sort in V8 and other engines |
|
// http://code.google.com/p/v8/issues/detail?id=90 |
|
if (a !== b) { |
|
if (a > b || a === undefined) { |
|
return 1; |
|
} |
|
if (a < b || b === undefined) { |
|
return -1; |
|
} |
|
} |
|
return ai < bi ? -1 : 1; |
|
} |
|
|
|
/** |
|
* Creates a function that, when called, invokes `func` with the `this` |
|
* binding of `thisArg` and prepends any `partailArgs` to the arguments passed |
|
* to the bound function. |
|
* |
|
* @private |
|
* @param {Function|String} func The function to bind or the method name. |
|
* @param {Mixed} [thisArg] The `this` binding of `func`. |
|
* @param {Array} partialArgs An array of arguments to be partially applied. |
|
* @returns {Function} Returns the new bound function. |
|
*/ |
|
function createBound(func, thisArg, partialArgs) { |
|
var isFunc = isFunction(func), |
|
isPartial = !partialArgs, |
|
methodName = func; |
|
|
|
// juggle arguments |
|
if (isPartial) { |
|
partialArgs = thisArg; |
|
} |
|
|
|
function bound() { |
|
// `Function#bind` spec |
|
// http://es5.github.com/#x15.3.4.5 |
|
var args = arguments, |
|
thisBinding = isPartial ? this : thisArg; |
|
|
|
if (!isFunc) { |
|
func = thisArg[methodName]; |
|
} |
|
if (partialArgs.length) { |
|
args = args.length |
|
? partialArgs.concat(slice.call(args)) |
|
: partialArgs; |
|
} |
|
if (this instanceof bound) { |
|
// get `func` instance if `bound` is invoked in a `new` expression |
|
noop.prototype = func.prototype; |
|
thisBinding = new noop; |
|
|
|
// mimic the constructor's `return` behavior |
|
// http://es5.github.com/#x13.2.2 |
|
var result = func.apply(thisBinding, args); |
|
return result && objectTypes[typeof result] |
|
? result |
|
: thisBinding |
|
} |
|
return func.apply(thisBinding, args); |
|
} |
|
return bound; |
|
} |
|
|
|
/** |
|
* Produces an iteration callback bound to an optional `thisArg`. If `func` is |
|
* a property name, the callback will return the property value for a given element. |
|
* |
|
* @private |
|
* @param {Function|String} [func=identity|property] The function called per |
|
* iteration or property name to query. |
|
* @param {Mixed} [thisArg] The `this` binding of `callback`. |
|
* @returns {Function} Returns a callback function. |
|
*/ |
|
function createCallback(func, thisArg) { |
|
if (!func) { |
|
return identity; |
|
} |
|
if (typeof func != 'function') { |
|
return function(object) { |
|
return object[func]; |
|
}; |
|
} |
|
if (thisArg !== undefined) { |
|
return function(value, index, object) { |
|
return func.call(thisArg, value, index, object); |
|
}; |
|
} |
|
return func; |
|
} |
|
|
|
/** |
|
* Creates compiled iteration functions. |
|
* |
|
* @private |
|
* @param {Object} [options1, options2, ...] The compile options object(s). |
|
* useHas - A boolean to specify using `hasOwnProperty` checks in the object loop. |
|
* args - A string of comma separated arguments the iteration function will accept. |
|
* top - A string of code to execute before the iteration branches. |
|
* arrayLoop - A string of code to execute in the array loop. |
|
* objectLoop - A string of code to execute in the object loop. |
|
* bottom - A string of code to execute after the iteration branches. |
|
* |
|
* @returns {Function} Returns the compiled function. |
|
*/ |
|
function createIterator() { |
|
var data = { |
|
'arrayLoop': '', |
|
'bottom': '', |
|
'hasDontEnumBug': hasDontEnumBug, |
|
'isKeysFast': isKeysFast, |
|
'objectLoop': '', |
|
'noArgsEnum': noArgsEnum, |
|
'noCharByIndex': noCharByIndex, |
|
'shadowed': shadowed, |
|
'top': '', |
|
'useHas': true |
|
}; |
|
|
|
// merge options into a template data object |
|
for (var object, index = 0; object = arguments[index]; index++) { |
|
for (var key in object) { |
|
data[key] = object[key]; |
|
} |
|
} |
|
var args = data.args; |
|
data.firstArg = /^[^,]+/.exec(args)[0]; |
|
|
|
// create the function factory |
|
var factory = Function( |
|
'createCallback, hasOwnProperty, isArguments, objectTypes, nativeKeys, ' + |
|
'propertyIsEnumerable, stringClass, toString', |
|
'return function(' + args + ') {\n' + iteratorTemplate(data) + '\n}' |
|
); |
|
// return the compiled function |
|
return factory( |
|
createCallback, hasOwnProperty, isArguments, objectTypes, nativeKeys, |
|
propertyIsEnumerable, stringClass, toString |
|
); |
|
} |
|
|
|
/** |
|
* Used by `template` to escape characters for inclusion in compiled |
|
* string literals. |
|
* |
|
* @private |
|
* @param {String} match The matched character to escape. |
|
* @returns {String} Returns the escaped character. |
|
*/ |
|
function escapeStringChar(match) { |
|
return '\\' + stringEscapes[match]; |
|
} |
|
|
|
/** |
|
* Used by `escape` to convert characters to HTML entities. |
|
* |
|
* @private |
|
* @param {String} match The matched character to escape. |
|
* @returns {String} Returns the escaped character. |
|
*/ |
|
function escapeHtmlChar(match) { |
|
return htmlEscapes[match]; |
|
} |
|
|
|
/** |
|
* A no-operation function. |
|
* |
|
* @private |
|
*/ |
|
function noop() { |
|
// no operation performed |
|
} |
|
|
|
/** |
|
* Used by `unescape` to convert HTML entities to characters. |
|
* |
|
* @private |
|
* @param {String} match The matched character to unescape. |
|
* @returns {String} Returns the unescaped character. |
|
*/ |
|
function unescapeHtmlChar(match) { |
|
return htmlUnescapes[match]; |
|
} |
|
|
|
/*--------------------------------------------------------------------------*/ |
|
|
|
/** |
|
* Checks if `value` is an `arguments` object. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Mixed} value The value to check. |
|
* @returns {Boolean} Returns `true` if the `value` is an `arguments` object, else `false`. |
|
* @example |
|
* |
|
* (function() { return _.isArguments(arguments); })(1, 2, 3); |
|
* // => true |
|
* |
|
* _.isArguments([1, 2, 3]); |
|
* // => false |
|
*/ |
|
function isArguments(value) { |
|
return toString.call(value) == argsClass; |
|
} |
|
// fallback for browsers that can't detect `arguments` objects by [[Class]] |
|
if (noArgsClass) { |
|
isArguments = function(value) { |
|
return value ? hasOwnProperty.call(value, 'callee') : false; |
|
}; |
|
} |
|
|
|
/** |
|
* Iterates over `object`'s own and inherited enumerable properties, executing |
|
* the `callback` for each property. The `callback` is bound to `thisArg` and |
|
* invoked with three arguments; (value, key, object). Callbacks may exit iteration |
|
* early by explicitly returning `false`. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Object} object The object to iterate over. |
|
* @param {Function} callback The function called per iteration. |
|
* @param {Mixed} [thisArg] The `this` binding of `callback`. |
|
* @returns {Object} Returns `object`. |
|
* @example |
|
* |
|
* function Dog(name) { |
|
* this.name = name; |
|
* } |
|
* |
|
* Dog.prototype.bark = function() { |
|
* alert('Woof, woof!'); |
|
* }; |
|
* |
|
* _.forIn(new Dog('Dagny'), function(value, key) { |
|
* alert(key); |
|
* }); |
|
* // => alerts 'name' and 'bark' (order is not guaranteed) |
|
*/ |
|
var forIn = createIterator(forEachIteratorOptions, forOwnIteratorOptions, { |
|
'useHas': false |
|
}); |
|
|
|
/** |
|
* Iterates over `object`'s own enumerable properties, executing the `callback` |
|
* for each property. The `callback` is bound to `thisArg` and invoked with three |
|
* arguments; (value, key, object). Callbacks may exit iteration early by explicitly |
|
* returning `false`. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Object} object The object to iterate over. |
|
* @param {Function} callback The function called per iteration. |
|
* @param {Mixed} [thisArg] The `this` binding of `callback`. |
|
* @returns {Object} Returns `object`. |
|
* @example |
|
* |
|
* _.forOwn({ '0': 'zero', '1': 'one', 'length': 2 }, function(num, key) { |
|
* alert(key); |
|
* }); |
|
* // => alerts '0', '1', and 'length' (order is not guaranteed) |
|
*/ |
|
var forOwn = createIterator(forEachIteratorOptions, forOwnIteratorOptions); |
|
|
|
/** |
|
* A fallback implementation of `isPlainObject` that checks if a given `value` |
|
* is an object created by the `Object` constructor, assuming objects created |
|
* by the `Object` constructor have no inherited enumerable properties and that |
|
* there are no `Object.prototype` extensions. |
|
* |
|
* @private |
|
* @param {Mixed} value The value to check. |
|
* @returns {Boolean} Returns `true` if `value` is a plain object, else `false`. |
|
*/ |
|
function shimIsPlainObject(value) { |
|
// avoid non-objects and false positives for `arguments` objects |
|
var result = false; |
|
if (!(value && typeof value == 'object') || isArguments(value)) { |
|
return result; |
|
} |
|
// IE < 9 presents DOM nodes as `Object` objects except they have `toString` |
|
// methods that are `typeof` "string" and still can coerce nodes to strings. |
|
// Also check that the constructor is `Object` (i.e. `Object instanceof Object`) |
|
var ctor = value.constructor; |
|
if ((!noNodeClass || !(typeof value.toString != 'function' && typeof (value + '') == 'string')) && |
|
(!isFunction(ctor) || ctor instanceof ctor)) { |
|
// IE < 9 iterates inherited properties before own properties. If the first |
|
// iterated property is an object's own property then there are no inherited |
|
// enumerable properties. |
|
if (iteratesOwnLast) { |
|
forIn(value, function(value, key, object) { |
|
result = !hasOwnProperty.call(object, key); |
|
return false; |
|
}); |
|
return result === false; |
|
} |
|
// In most environments an object's own properties are iterated before |
|
// its inherited properties. If the last iterated property is an object's |
|
// own property then there are no inherited enumerable properties. |
|
forIn(value, function(value, key) { |
|
result = key; |
|
}); |
|
return result === false || hasOwnProperty.call(value, result); |
|
} |
|
return result; |
|
} |
|
|
|
/** |
|
* A fallback implementation of `Object.keys` that produces an array of the |
|
* given object's own enumerable property names. |
|
* |
|
* @private |
|
* @param {Object} object The object to inspect. |
|
* @returns {Array} Returns a new array of property names. |
|
*/ |
|
function shimKeys(object) { |
|
var result = []; |
|
forOwn(object, function(value, key) { |
|
result.push(key); |
|
}); |
|
return result; |
|
} |
|
|
|
/** |
|
* Used to convert characters to HTML entities: |
|
* |
|
* Though the `>` character is escaped for symmetry, characters like `>` and `/` |
|
* don't require escaping in HTML and have no special meaning unless they're part |
|
* of a tag or an unquoted attribute value. |
|
* http://mathiasbynens.be/notes/ambiguous-ampersands (under "semi-related fun fact") |
|
*/ |
|
var htmlEscapes = { |
|
'&': '&', |
|
'<': '<', |
|
'>': '>', |
|
'"': '"', |
|
"'": ''' |
|
}; |
|
|
|
/** Used to convert HTML entities to characters */ |
|
var htmlUnescapes = invert(htmlEscapes); |
|
|
|
/*--------------------------------------------------------------------------*/ |
|
|
|
/** |
|
* Creates a clone of `value`. If `deep` is `true`, all nested objects will |
|
* also be cloned otherwise they will be assigned by reference. Functions, DOM |
|
* nodes, `arguments` objects, and objects created by constructors other than |
|
* `Object` are **not** cloned. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Mixed} value The value to clone. |
|
* @param {Boolean} deep A flag to indicate a deep clone. |
|
* @param- {Object} [guard] Internally used to allow this method to work with |
|
* others like `_.map` without using their callback `index` argument for `deep`. |
|
* @param- {Array} [stackA=[]] Internally used to track traversed source objects. |
|
* @param- {Array} [stackB=[]] Internally used to associate clones with their |
|
* source counterparts. |
|
* @returns {Mixed} Returns the cloned `value`. |
|
* @example |
|
* |
|
* var stooges = [ |
|
* { 'name': 'moe', 'age': 40 }, |
|
* { 'name': 'larry', 'age': 50 }, |
|
* { 'name': 'curly', 'age': 60 } |
|
* ]; |
|
* |
|
* _.clone({ 'name': 'moe' }); |
|
* // => { 'name': 'moe' } |
|
* |
|
* var shallow = _.clone(stooges); |
|
* shallow[0] === stooges[0]; |
|
* // => true |
|
* |
|
* var deep = _.clone(stooges, true); |
|
* shallow[0] === stooges[0]; |
|
* // => false |
|
*/ |
|
function clone(value, deep, guard, stackA, stackB) { |
|
if (value == null) { |
|
return value; |
|
} |
|
if (guard) { |
|
deep = false; |
|
} |
|
// inspect [[Class]] |
|
var isObj = objectTypes[typeof value]; |
|
if (isObj) { |
|
// don't clone `arguments` objects, functions, or non-object Objects |
|
var className = toString.call(value); |
|
if (!cloneableClasses[className] || (noArgsClass && isArguments(value))) { |
|
return value; |
|
} |
|
var isArr = className == arrayClass; |
|
isObj = isArr || (className == objectClass ? isPlainObject(value) : isObj); |
|
} |
|
// shallow clone |
|
if (!isObj || !deep) { |
|
// don't clone functions |
|
return isObj |
|
? (isArr ? slice.call(value) : extend({}, value)) |
|
: value; |
|
} |
|
|
|
var ctor = value.constructor; |
|
switch (className) { |
|
case boolClass: |
|
case dateClass: |
|
return new ctor(+value); |
|
|
|
case numberClass: |
|
case stringClass: |
|
return new ctor(value); |
|
|
|
case regexpClass: |
|
return ctor(value.source, reFlags.exec(value)); |
|
} |
|
// check for circular references and return corresponding clone |
|
stackA || (stackA = []); |
|
stackB || (stackB = []); |
|
|
|
var length = stackA.length; |
|
while (length--) { |
|
if (stackA[length] == value) { |
|
return stackB[length]; |
|
} |
|
} |
|
// init cloned object |
|
var result = isArr ? ctor(value.length) : {}; |
|
|
|
// add the source value to the stack of traversed objects |
|
// and associate it with its clone |
|
stackA.push(value); |
|
stackB.push(result); |
|
|
|
// recursively populate clone (susceptible to call stack limits) |
|
(isArr ? forEach : forOwn)(value, function(objValue, key) { |
|
result[key] = clone(objValue, deep, null, stackA, stackB); |
|
}); |
|
|
|
return result; |
|
} |
|
|
|
/** |
|
* Assigns enumerable properties of the default object(s) to the `destination` |
|
* object for all `destination` properties that resolve to `null`/`undefined`. |
|
* Once a property is set, additional defaults of the same property will be |
|
* ignored. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Object} object The destination object. |
|
* @param {Object} [default1, default2, ...] The default objects. |
|
* @returns {Object} Returns the destination object. |
|
* @example |
|
* |
|
* var iceCream = { 'flavor': 'chocolate' }; |
|
* _.defaults(iceCream, { 'flavor': 'vanilla', 'sprinkles': 'rainbow' }); |
|
* // => { 'flavor': 'chocolate', 'sprinkles': 'rainbow' } |
|
*/ |
|
var defaults = createIterator(extendIteratorOptions, { |
|
'objectLoop': 'if (result[index] == null) ' + extendIteratorOptions.objectLoop |
|
}); |
|
|
|
/** |
|
* Assigns enumerable properties of the source object(s) to the `destination` |
|
* object. Subsequent sources will overwrite propery assignments of previous |
|
* sources. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Object} object The destination object. |
|
* @param {Object} [source1, source2, ...] The source objects. |
|
* @returns {Object} Returns the destination object. |
|
* @example |
|
* |
|
* _.extend({ 'name': 'moe' }, { 'age': 40 }); |
|
* // => { 'name': 'moe', 'age': 40 } |
|
*/ |
|
var extend = createIterator(extendIteratorOptions); |
|
|
|
/** |
|
* Creates a sorted array of all enumerable properties, own and inherited, |
|
* of `object` that have function values. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @alias methods |
|
* @category Objects |
|
* @param {Object} object The object to inspect. |
|
* @returns {Array} Returns a new array of property names that have function values. |
|
* @example |
|
* |
|
* _.functions(_); |
|
* // => ['all', 'any', 'bind', 'bindAll', 'clone', 'compact', 'compose', ...] |
|
*/ |
|
function functions(object) { |
|
var result = []; |
|
forIn(object, function(value, key) { |
|
if (isFunction(value)) { |
|
result.push(key); |
|
} |
|
}); |
|
return result.sort(); |
|
} |
|
|
|
/** |
|
* Checks if the specified object `property` exists and is a direct property, |
|
* instead of an inherited property. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Object} object The object to check. |
|
* @param {String} property The property to check for. |
|
* @returns {Boolean} Returns `true` if key is a direct property, else `false`. |
|
* @example |
|
* |
|
* _.has({ 'a': 1, 'b': 2, 'c': 3 }, 'b'); |
|
* // => true |
|
*/ |
|
function has(object, property) { |
|
return object ? hasOwnProperty.call(object, property) : false; |
|
} |
|
|
|
/** |
|
* Creates an object composed of the inverted keys and values of the given `object`. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Object} object The object to invert. |
|
* @returns {Object} Returns the created inverted object. |
|
* @example |
|
* |
|
* _.invert({ 'first': 'Moe', 'second': 'Larry', 'third': 'Curly' }); |
|
* // => { 'Moe': 'first', 'Larry': 'second', 'Curly': 'third' } (order is not guaranteed) |
|
*/ |
|
function invert(object) { |
|
var result = {}; |
|
forOwn(object, function(value, key) { |
|
result[value] = key; |
|
}); |
|
return result; |
|
} |
|
|
|
/** |
|
* Checks if `value` is an array. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Mixed} value The value to check. |
|
* @returns {Boolean} Returns `true` if the `value` is an array, else `false`. |
|
* @example |
|
* |
|
* (function() { return _.isArray(arguments); })(); |
|
* // => false |
|
* |
|
* _.isArray([1, 2, 3]); |
|
* // => true |
|
*/ |
|
var isArray = nativeIsArray || function(value) { |
|
return toString.call(value) == arrayClass; |
|
}; |
|
|
|
/** |
|
* Checks if `value` is a boolean (`true` or `false`) value. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Mixed} value The value to check. |
|
* @returns {Boolean} Returns `true` if the `value` is a boolean value, else `false`. |
|
* @example |
|
* |
|
* _.isBoolean(null); |
|
* // => false |
|
*/ |
|
function isBoolean(value) { |
|
return value === true || value === false || toString.call(value) == boolClass; |
|
} |
|
|
|
/** |
|
* Checks if `value` is a date. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Mixed} value The value to check. |
|
* @returns {Boolean} Returns `true` if the `value` is a date, else `false`. |
|
* @example |
|
* |
|
* _.isDate(new Date); |
|
* // => true |
|
*/ |
|
function isDate(value) { |
|
return toString.call(value) == dateClass; |
|
} |
|
|
|
/** |
|
* Checks if `value` is a DOM element. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Mixed} value The value to check. |
|
* @returns {Boolean} Returns `true` if the `value` is a DOM element, else `false`. |
|
* @example |
|
* |
|
* _.isElement(document.body); |
|
* // => true |
|
*/ |
|
function isElement(value) { |
|
return value ? value.nodeType === 1 : false; |
|
} |
|
|
|
/** |
|
* Checks if `value` is empty. Arrays, strings, or `arguments` objects with a |
|
* length of `0` and objects with no own enumerable properties are considered |
|
* "empty". |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Array|Object|String} value The value to inspect. |
|
* @returns {Boolean} Returns `true` if the `value` is empty, else `false`. |
|
* @example |
|
* |
|
* _.isEmpty([1, 2, 3]); |
|
* // => false |
|
* |
|
* _.isEmpty({}); |
|
* // => true |
|
* |
|
* _.isEmpty(''); |
|
* // => true |
|
*/ |
|
function isEmpty(value) { |
|
var result = true; |
|
if (!value) { |
|
return result; |
|
} |
|
var className = toString.call(value), |
|
length = value.length; |
|
|
|
if ((className == arrayClass || className == stringClass || |
|
className == argsClass || (noArgsClass && isArguments(value))) || |
|
(className == objectClass && typeof length == 'number' && isFunction(value.splice))) { |
|
return !length; |
|
} |
|
forOwn(value, function() { |
|
return (result = false); |
|
}); |
|
return result; |
|
} |
|
|
|
/** |
|
* Performs a deep comparison between two values to determine if they are |
|
* equivalent to each other. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Mixed} a The value to compare. |
|
* @param {Mixed} b The other value to compare. |
|
* @param- {Object} [stackA=[]] Internally used track traversed `a` objects. |
|
* @param- {Object} [stackB=[]] Internally used track traversed `b` objects. |
|
* @returns {Boolean} Returns `true` if the values are equvalent, else `false`. |
|
* @example |
|
* |
|
* var moe = { 'name': 'moe', 'luckyNumbers': [13, 27, 34] }; |
|
* var clone = { 'name': 'moe', 'luckyNumbers': [13, 27, 34] }; |
|
* |
|
* moe == clone; |
|
* // => false |
|
* |
|
* _.isEqual(moe, clone); |
|
* // => true |
|
*/ |
|
function isEqual(a, b, stackA, stackB) { |
|
// exit early for identical values |
|
if (a === b) { |
|
// treat `+0` vs. `-0` as not equal |
|
return a !== 0 || (1 / a == 1 / b); |
|
} |
|
// a strict comparison is necessary because `null == undefined` |
|
if (a == null || b == null) { |
|
return a === b; |
|
} |
|
// compare [[Class]] names |
|
var className = toString.call(a); |
|
if (className != toString.call(b)) { |
|
return false; |
|
} |
|
switch (className) { |
|
case boolClass: |
|
case dateClass: |
|
// coerce dates and booleans to numbers, dates to milliseconds and booleans |
|
// to `1` or `0`, treating invalid dates coerced to `NaN` as not equal |
|
return +a == +b; |
|
|
|
case numberClass: |
|
// treat `NaN` vs. `NaN` as equal |
|
return a != +a |
|
? b != +b |
|
// but treat `+0` vs. `-0` as not equal |
|
: (a == 0 ? (1 / a == 1 / b) : a == +b); |
|
|
|
case regexpClass: |
|
case stringClass: |
|
// coerce regexes to strings (http://es5.github.com/#x15.10.6.4) |
|
// treat string primitives and their corresponding object instances as equal |
|
return a == b + ''; |
|
} |
|
// exit early, in older browsers, if `a` is array-like but not `b` |
|
var isArr = className == arrayClass || className == argsClass; |
|
if (noArgsClass && !isArr && (isArr = isArguments(a)) && !isArguments(b)) { |
|
return false; |
|
} |
|
if (!isArr) { |
|
// unwrap any `lodash` wrapped values |
|
if (a.__wrapped__ || b.__wrapped__) { |
|
return isEqual(a.__wrapped__ || a, b.__wrapped__ || b); |
|
} |
|
// exit for functions and DOM nodes |
|
if (className != objectClass || (noNodeClass && ( |
|
(typeof a.toString != 'function' && typeof (a + '') == 'string') || |
|
(typeof b.toString != 'function' && typeof (b + '') == 'string')))) { |
|
return false; |
|
} |
|
var ctorA = a.constructor, |
|
ctorB = b.constructor; |
|
|
|
// non `Object` object instances with different constructors are not equal |
|
if (ctorA != ctorB && !( |
|
isFunction(ctorA) && ctorA instanceof ctorA && |
|
isFunction(ctorB) && ctorB instanceof ctorB |
|
)) { |
|
return false; |
|
} |
|
} |
|
// assume cyclic structures are equal |
|
// the algorithm for detecting cyclic structures is adapted from ES 5.1 |
|
// section 15.12.3, abstract operation `JO` (http://es5.github.com/#x15.12.3) |
|
stackA || (stackA = []); |
|
stackB || (stackB = []); |
|
|
|
var length = stackA.length; |
|
while (length--) { |
|
if (stackA[length] == a) { |
|
return stackB[length] == b; |
|
} |
|
} |
|
|
|
var index = -1, |
|
result = true, |
|
size = 0; |
|
|
|
// add `a` and `b` to the stack of traversed objects |
|
stackA.push(a); |
|
stackB.push(b); |
|
|
|
// recursively compare objects and arrays (susceptible to call stack limits) |
|
if (isArr) { |
|
// compare lengths to determine if a deep comparison is necessary |
|
size = a.length; |
|
result = size == b.length; |
|
|
|
if (result) { |
|
// deep compare the contents, ignoring non-numeric properties |
|
while (size--) { |
|
if (!(result = isEqual(a[size], b[size], stackA, stackB))) { |
|
break; |
|
} |
|
} |
|
} |
|
return result; |
|
} |
|
// deep compare objects |
|
for (var key in a) { |
|
if (hasOwnProperty.call(a, key)) { |
|
// count the number of properties. |
|
size++; |
|
// deep compare each property value. |
|
if (!(hasOwnProperty.call(b, key) && isEqual(a[key], b[key], stackA, stackB))) { |
|
return false; |
|
} |
|
} |
|
} |
|
// ensure both objects have the same number of properties |
|
for (key in b) { |
|
// The JS engine in Adobe products, like InDesign, has a bug that causes |
|
// `!size--` to throw an error so it must be wrapped in parentheses. |
|
// https://github.com/documentcloud/underscore/issues/355 |
|
if (hasOwnProperty.call(b, key) && !(size--)) { |
|
// `size` will be `-1` if `b` has more properties than `a` |
|
return false; |
|
} |
|
} |
|
// handle JScript [[DontEnum]] bug |
|
if (hasDontEnumBug) { |
|
while (++index < 7) { |
|
key = shadowed[index]; |
|
if (hasOwnProperty.call(a, key) && |
|
!(hasOwnProperty.call(b, key) && isEqual(a[key], b[key], stackA, stackB))) { |
|
return false; |
|
} |
|
} |
|
} |
|
return true; |
|
} |
|
|
|
/** |
|
* Checks if `value` is, or can be coerced to, a finite number. |
|
* |
|
* Note: This is not the same as native `isFinite`, which will return true for |
|
* booleans and empty strings. See http://es5.github.com/#x15.1.2.5. |
|
* |
|
* @deprecated |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Mixed} value The value to check. |
|
* @returns {Boolean} Returns `true` if the `value` is a finite number, else `false`. |
|
* @example |
|
* |
|
* _.isFinite(-101); |
|
* // => true |
|
* |
|
* _.isFinite('10'); |
|
* // => true |
|
* |
|
* _.isFinite(true); |
|
* // => false |
|
* |
|
* _.isFinite(''); |
|
* // => false |
|
* |
|
* _.isFinite(Infinity); |
|
* // => false |
|
*/ |
|
function isFinite(value) { |
|
return nativeIsFinite(value ? +value : parseFloat(value)); |
|
} |
|
|
|
/** |
|
* Checks if `value` is a function. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Mixed} value The value to check. |
|
* @returns {Boolean} Returns `true` if the `value` is a function, else `false`. |
|
* @example |
|
* |
|
* _.isFunction(_); |
|
* // => true |
|
*/ |
|
function isFunction(value) { |
|
return typeof value == 'function'; |
|
} |
|
// fallback for older versions of Chrome and Safari |
|
if (isFunction(/x/)) { |
|
isFunction = function(value) { |
|
return toString.call(value) == funcClass; |
|
}; |
|
} |
|
|
|
/** |
|
* Checks if `value` is the language type of Object. |
|
* (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Mixed} value The value to check. |
|
* @returns {Boolean} Returns `true` if the `value` is an object, else `false`. |
|
* @example |
|
* |
|
* _.isObject({}); |
|
* // => true |
|
* |
|
* _.isObject([1, 2, 3]); |
|
* // => true |
|
* |
|
* _.isObject(1); |
|
* // => false |
|
*/ |
|
function isObject(value) { |
|
// check if the value is the ECMAScript language type of Object |
|
// http://es5.github.com/#x8 |
|
// and avoid a V8 bug |
|
// http://code.google.com/p/v8/issues/detail?id=2291 |
|
return value ? objectTypes[typeof value] : false; |
|
} |
|
|
|
/** |
|
* Checks if `value` is `NaN`. |
|
* |
|
* Note: This is not the same as native `isNaN`, which will return true for |
|
* `undefined` and other values. See http://es5.github.com/#x15.1.2.4. |
|
* |
|
* @deprecated |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Mixed} value The value to check. |
|
* @returns {Boolean} Returns `true` if the `value` is `NaN`, else `false`. |
|
* @example |
|
* |
|
* _.isNaN(NaN); |
|
* // => true |
|
* |
|
* _.isNaN(new Number(NaN)); |
|
* // => true |
|
* |
|
* isNaN(undefined); |
|
* // => true |
|
* |
|
* _.isNaN(undefined); |
|
* // => false |
|
*/ |
|
function isNaN(value) { |
|
// `NaN` as a primitive is the only value that is not equal to itself |
|
// (perform the [[Class]] check first to avoid errors with some host objects in IE) |
|
return toString.call(value) == numberClass && value != +value |
|
} |
|
|
|
/** |
|
* Checks if `value` is `null`. |
|
* |
|
* @deprecated |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Mixed} value The value to check. |
|
* @returns {Boolean} Returns `true` if the `value` is `null`, else `false`. |
|
* @example |
|
* |
|
* _.isNull(null); |
|
* // => true |
|
* |
|
* _.isNull(undefined); |
|
* // => false |
|
*/ |
|
function isNull(value) { |
|
return value === null; |
|
} |
|
|
|
/** |
|
* Checks if `value` is a number. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Mixed} value The value to check. |
|
* @returns {Boolean} Returns `true` if the `value` is a number, else `false`. |
|
* @example |
|
* |
|
* _.isNumber(8.4 * 5); |
|
* // => true |
|
*/ |
|
function isNumber(value) { |
|
return toString.call(value) == numberClass; |
|
} |
|
|
|
/** |
|
* Checks if a given `value` is an object created by the `Object` constructor. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Mixed} value The value to check. |
|
* @returns {Boolean} Returns `true` if `value` is a plain object, else `false`. |
|
* @example |
|
* |
|
* function Stooge(name, age) { |
|
* this.name = name; |
|
* this.age = age; |
|
* } |
|
* |
|
* _.isPlainObject(new Stooge('moe', 40)); |
|
* // => false |
|
* |
|
* _.isPlainObject([1, 2, 3]); |
|
* // => false |
|
* |
|
* _.isPlainObject({ 'name': 'moe', 'age': 40 }); |
|
* // => true |
|
*/ |
|
var isPlainObject = !getPrototypeOf ? shimIsPlainObject : function(value) { |
|
if (!(value && typeof value == 'object')) { |
|
return false; |
|
} |
|
var valueOf = value.valueOf, |
|
objProto = typeof valueOf == 'function' && (objProto = getPrototypeOf(valueOf)) && getPrototypeOf(objProto); |
|
|
|
return objProto |
|
? value == objProto || (getPrototypeOf(value) == objProto && !isArguments(value)) |
|
: shimIsPlainObject(value); |
|
}; |
|
|
|
/** |
|
* Checks if `value` is a regular expression. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Mixed} value The value to check. |
|
* @returns {Boolean} Returns `true` if the `value` is a regular expression, else `false`. |
|
* @example |
|
* |
|
* _.isRegExp(/moe/); |
|
* // => true |
|
*/ |
|
function isRegExp(value) { |
|
return toString.call(value) == regexpClass; |
|
} |
|
|
|
/** |
|
* Checks if `value` is a string. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Mixed} value The value to check. |
|
* @returns {Boolean} Returns `true` if the `value` is a string, else `false`. |
|
* @example |
|
* |
|
* _.isString('moe'); |
|
* // => true |
|
*/ |
|
function isString(value) { |
|
return toString.call(value) == stringClass; |
|
} |
|
|
|
/** |
|
* Checks if `value` is `undefined`. |
|
* |
|
* @deprecated |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Mixed} value The value to check. |
|
* @returns {Boolean} Returns `true` if the `value` is `undefined`, else `false`. |
|
* @example |
|
* |
|
* _.isUndefined(void 0); |
|
* // => true |
|
*/ |
|
function isUndefined(value) { |
|
return value === undefined; |
|
} |
|
|
|
/** |
|
* Creates an array composed of the own enumerable property names of `object`. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Object} object The object to inspect. |
|
* @returns {Array} Returns a new array of property names. |
|
* @example |
|
* |
|
* _.keys({ 'one': 1, 'two': 2, 'three': 3 }); |
|
* // => ['one', 'two', 'three'] (order is not guaranteed) |
|
*/ |
|
var keys = !nativeKeys ? shimKeys : function(object) { |
|
var type = typeof object; |
|
|
|
// avoid iterating over the `prototype` property |
|
if (type == 'function' && propertyIsEnumerable.call(object, 'prototype')) { |
|
return shimKeys(object); |
|
} |
|
return object && objectTypes[type] |
|
? nativeKeys(object) |
|
: []; |
|
}; |
|
|
|
/** |
|
* Merges enumerable properties of the source object(s) into the `destination` |
|
* object. Subsequent sources will overwrite propery assignments of previous |
|
* sources. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Object} object The destination object. |
|
* @param {Object} [source1, source2, ...] The source objects. |
|
* @param- {Object} [indicator] Internally used to indicate that the `stack` |
|
* argument is an array of traversed objects instead of another source object. |
|
* @param- {Array} [stackA=[]] Internally used to track traversed source objects. |
|
* @param- {Array} [stackB=[]] Internally used to associate values with their |
|
* source counterparts. |
|
* @returns {Object} Returns the destination object. |
|
* @example |
|
* |
|
* var stooges = [ |
|
* { 'name': 'moe' }, |
|
* { 'name': 'larry' } |
|
* ]; |
|
* |
|
* var ages = [ |
|
* { 'age': 40 }, |
|
* { 'age': 50 } |
|
* ]; |
|
* |
|
* _.merge(stooges, ages); |
|
* // => [{ 'name': 'moe', 'age': 40 }, { 'name': 'larry', 'age': 50 }] |
|
*/ |
|
function merge(object, source, indicator) { |
|
var args = arguments, |
|
index = 0, |
|
length = 2, |
|
stackA = args[3], |
|
stackB = args[4]; |
|
|
|
if (indicator !== indicatorObject) { |
|
stackA = []; |
|
stackB = []; |
|
length = args.length; |
|
} |
|
while (++index < length) { |
|
forOwn(args[index], function(source, key) { |
|
var found, isArr, value; |
|
if (source && ((isArr = isArray(source)) || isPlainObject(source))) { |
|
// avoid merging previously merged cyclic sources |
|
var stackLength = stackA.length; |
|
while (stackLength--) { |
|
found = stackA[stackLength] == source; |
|
if (found) { |
|
break; |
|
} |
|
} |
|
if (found) { |
|
object[key] = stackB[stackLength]; |
|
} |
|
else { |
|
// add `source` and associated `value` to the stack of traversed objects |
|
stackA.push(source); |
|
stackB.push(value = (value = object[key], isArr) |
|
? (isArray(value) ? value : []) |
|
: (isPlainObject(value) ? value : {}) |
|
); |
|
// recursively merge objects and arrays (susceptible to call stack limits) |
|
object[key] = merge(value, source, indicatorObject, stackA, stackB); |
|
} |
|
} else if (source != null) { |
|
object[key] = source; |
|
} |
|
}); |
|
} |
|
return object; |
|
} |
|
|
|
/** |
|
* Creates a shallow clone of `object` excluding the specified properties. |
|
* Property names may be specified as individual arguments or as arrays of |
|
* property names. If `callback` is passed, it will be executed for each property |
|
* in the `object`, omitting the properties `callback` returns truthy for. The |
|
* `callback` is bound to `thisArg` and invoked with three arguments; (value, key, object). |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Object} object The source object. |
|
* @param {Function|String} callback|[prop1, prop2, ...] The properties to omit |
|
* or the function called per iteration. |
|
* @param {Mixed} [thisArg] The `this` binding of `callback`. |
|
* @returns {Object} Returns an object without the omitted properties. |
|
* @example |
|
* |
|
* _.omit({ 'name': 'moe', 'age': 40, 'userid': 'moe1' }, 'userid'); |
|
* // => { 'name': 'moe', 'age': 40 } |
|
* |
|
* _.omit({ 'name': 'moe', '_hint': 'knucklehead', '_seed': '96c4eb' }, function(value, key) { |
|
* return key.charAt(0) == '_'; |
|
* }); |
|
* // => { 'name': 'moe' } |
|
*/ |
|
function omit(object, callback, thisArg) { |
|
var isFunc = typeof callback == 'function', |
|
result = {}; |
|
|
|
if (isFunc) { |
|
callback = createCallback(callback, thisArg); |
|
} else { |
|
var props = concat.apply(arrayRef, arguments); |
|
} |
|
forIn(object, function(value, key, object) { |
|
if (isFunc |
|
? !callback(value, key, object) |
|
: indexOf(props, key, 1) < 0 |
|
) { |
|
result[key] = value; |
|
} |
|
}); |
|
return result; |
|
} |
|
|
|
/** |
|
* Creates a two dimensional array of the given object's key-value pairs, |
|
* i.e. `[[key1, value1], [key2, value2]]`. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Object} object The object to inspect. |
|
* @returns {Array} Returns new array of key-value pairs. |
|
* @example |
|
* |
|
* _.pairs({ 'moe': 30, 'larry': 40, 'curly': 50 }); |
|
* // => [['moe', 30], ['larry', 40], ['curly', 50]] (order is not guaranteed) |
|
*/ |
|
function pairs(object) { |
|
var result = []; |
|
forOwn(object, function(value, key) { |
|
result.push([key, value]); |
|
}); |
|
return result; |
|
} |
|
|
|
/** |
|
* Creates a shallow clone of `object` composed of the specified properties. |
|
* Property names may be specified as individual arguments or as arrays of |
|
* property names. If `callback` is passed, it will be executed for each property |
|
* in the `object`, picking the properties `callback` returns truthy for. The |
|
* `callback` is bound to `thisArg` and invoked with three arguments; (value, key, object). |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Object} object The source object. |
|
* @param {Function|String} callback|[prop1, prop2, ...] The properties to pick |
|
* or the function called per iteration. |
|
* @param {Mixed} [thisArg] The `this` binding of `callback`. |
|
* @returns {Object} Returns an object composed of the picked properties. |
|
* @example |
|
* |
|
* _.pick({ 'name': 'moe', 'age': 40, 'userid': 'moe1' }, 'name', 'age'); |
|
* // => { 'name': 'moe', 'age': 40 } |
|
* |
|
* _.pick({ 'name': 'moe', '_hint': 'knucklehead', '_seed': '96c4eb' }, function(value, key) { |
|
* return key.charAt(0) != '_'; |
|
* }); |
|
* // => { 'name': 'moe' } |
|
*/ |
|
function pick(object, callback, thisArg) { |
|
var result = {}; |
|
if (typeof callback != 'function') { |
|
var index = 0, |
|
props = concat.apply(arrayRef, arguments), |
|
length = props.length; |
|
|
|
while (++index < length) { |
|
var key = props[index]; |
|
if (key in object) { |
|
result[key] = object[key]; |
|
} |
|
} |
|
} else { |
|
callback = createCallback(callback, thisArg); |
|
forIn(object, function(value, key, object) { |
|
if (callback(value, key, object)) { |
|
result[key] = value; |
|
} |
|
}); |
|
} |
|
return result; |
|
} |
|
|
|
/** |
|
* Creates an array composed of the own enumerable property values of `object`. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Objects |
|
* @param {Object} object The object to inspect. |
|
* @returns {Array} Returns a new array of property values. |
|
* @example |
|
* |
|
* _.values({ 'one': 1, 'two': 2, 'three': 3 }); |
|
* // => [1, 2, 3] |
|
*/ |
|
function values(object) { |
|
var result = []; |
|
forOwn(object, function(value) { |
|
result.push(value); |
|
}); |
|
return result; |
|
} |
|
|
|
/*--------------------------------------------------------------------------*/ |
|
|
|
/** |
|
* Checks if a given `target` element is present in a `collection` using strict |
|
* equality for comparisons, i.e. `===`. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @alias include |
|
* @category Collections |
|
* @param {Array|Object|String} collection The collection to iterate over. |
|
* @param {Mixed} target The value to check for. |
|
* @returns {Boolean} Returns `true` if the `target` element is found, else `false`. |
|
* @example |
|
* |
|
* _.contains([1, 2, 3], 3); |
|
* // => true |
|
* |
|
* _.contains({ 'name': 'moe', 'age': 40 }, 'moe'); |
|
* // => true |
|
* |
|
* _.contains('curly', 'ur'); |
|
* // => true |
|
*/ |
|
function contains(collection, target) { |
|
var length = collection ? collection.length : 0; |
|
if (typeof length == 'number') { |
|
return (toString.call(collection) == stringClass |
|
? collection.indexOf(target) |
|
: indexOf(collection, target) |
|
) > -1; |
|
} |
|
return some(collection, function(value) { |
|
return value === target; |
|
}); |
|
} |
|
|
|
/** |
|
* Creates an object composed of keys returned from running each element of |
|
* `collection` through a `callback`. The corresponding value of each key is |
|
* the number of times the key was returned by `callback`. The `callback` is |
|
* bound to `thisArg` and invoked with three arguments; (value, index|key, collection). |
|
* The `callback` argument may also be the name of a property to count by (e.g. 'length'). |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Collections |
|
* @param {Array|Object|String} collection The collection to iterate over. |
|
* @param {Function|String} callback|property The function called per iteration |
|
* or property name to count by. |
|
* @param {Mixed} [thisArg] The `this` binding of `callback`. |
|
* @returns {Object} Returns the composed aggregate object. |
|
* @example |
|
* |
|
* _.countBy([4.3, 6.1, 6.4], function(num) { return Math.floor(num); }); |
|
* // => { '4': 1, '6': 2 } |
|
* |
|
* _.countBy([4.3, 6.1, 6.4], function(num) { return this.floor(num); }, Math); |
|
* // => { '4': 1, '6': 2 } |
|
* |
|
* _.countBy(['one', 'two', 'three'], 'length'); |
|
* // => { '3': 2, '5': 1 } |
|
*/ |
|
function countBy(collection, callback, thisArg) { |
|
var result = {}; |
|
callback = createCallback(callback, thisArg); |
|
forEach(collection, function(value, key, collection) { |
|
key = callback(value, key, collection); |
|
(hasOwnProperty.call(result, key) ? result[key]++ : result[key] = 1); |
|
}); |
|
return result; |
|
} |
|
|
|
/** |
|
* Checks if the `callback` returns a truthy value for **all** elements of a |
|
* `collection`. The `callback` is bound to `thisArg` and invoked with three |
|
* arguments; (value, index|key, collection). |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @alias all |
|
* @category Collections |
|
* @param {Array|Object|String} collection The collection to iterate over. |
|
* @param {Function} [callback=identity] The function called per iteration. |
|
* @param {Mixed} [thisArg] The `this` binding of `callback`. |
|
* @returns {Boolean} Returns `true` if all elements pass the callback check, |
|
* else `false`. |
|
* @example |
|
* |
|
* _.every([true, 1, null, 'yes'], Boolean); |
|
* // => false |
|
*/ |
|
function every(collection, callback, thisArg) { |
|
var result = true; |
|
callback = createCallback(callback, thisArg); |
|
forEach(collection, function(value, index, collection) { |
|
return (result = callback(value, index, collection)); |
|
}); |
|
return !!result; |
|
} |
|
|
|
/** |
|
* Examines each element in a `collection`, returning an array of all elements |
|
* the `callback` returns truthy for. The `callback` is bound to `thisArg` and |
|
* invoked with three arguments; (value, index|key, collection). |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @alias select |
|
* @category Collections |
|
* @param {Array|Object|String} collection The collection to iterate over. |
|
* @param {Function} [callback=identity] The function called per iteration. |
|
* @param {Mixed} [thisArg] The `this` binding of `callback`. |
|
* @returns {Array} Returns a new array of elements that passed the callback check. |
|
* @example |
|
* |
|
* var evens = _.filter([1, 2, 3, 4, 5, 6], function(num) { return num % 2 == 0; }); |
|
* // => [2, 4, 6] |
|
*/ |
|
function filter(collection, callback, thisArg) { |
|
var result = []; |
|
callback = createCallback(callback, thisArg); |
|
forEach(collection, function(value, index, collection) { |
|
if (callback(value, index, collection)) { |
|
result.push(value); |
|
} |
|
}); |
|
return result; |
|
} |
|
|
|
/** |
|
* Examines each element in a `collection`, returning the first one the `callback` |
|
* returns truthy for. The function returns as soon as it finds an acceptable |
|
* element, and does not iterate over the entire `collection`. The `callback` is |
|
* bound to `thisArg` and invoked with three arguments; (value, index|key, collection). |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @alias detect |
|
* @category Collections |
|
* @param {Array|Object|String} collection The collection to iterate over. |
|
* @param {Function} callback The function called per iteration. |
|
* @param {Mixed} [thisArg] The `this` binding of `callback`. |
|
* @returns {Mixed} Returns the element that passed the callback check, |
|
* else `undefined`. |
|
* @example |
|
* |
|
* var even = _.find([1, 2, 3, 4, 5, 6], function(num) { return num % 2 == 0; }); |
|
* // => 2 |
|
*/ |
|
function find(collection, callback, thisArg) { |
|
var result; |
|
callback = createCallback(callback, thisArg); |
|
some(collection, function(value, index, collection) { |
|
return callback(value, index, collection) && (result = value, true); |
|
}); |
|
return result; |
|
} |
|
|
|
/** |
|
* Iterates over a `collection`, executing the `callback` for each element in |
|
* the `collection`. The `callback` is bound to `thisArg` and invoked with three |
|
* arguments; (value, index|key, collection). Callbacks may exit iteration early |
|
* by explicitly returning `false`. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @alias each |
|
* @category Collections |
|
* @param {Array|Object|String} collection The collection to iterate over. |
|
* @param {Function} callback The function called per iteration. |
|
* @param {Mixed} [thisArg] The `this` binding of `callback`. |
|
* @returns {Array|Object|String} Returns `collection`. |
|
* @example |
|
* |
|
* _([1, 2, 3]).forEach(alert).join(','); |
|
* // => alerts each number and returns '1,2,3' |
|
* |
|
* _.forEach({ 'one': 1, 'two': 2, 'three': 3 }, alert); |
|
* // => alerts each number (order is not guaranteed) |
|
*/ |
|
var forEach = createIterator(forEachIteratorOptions); |
|
|
|
/** |
|
* Creates an object composed of keys returned from running each element of |
|
* `collection` through a `callback`. The corresponding value of each key is an |
|
* array of elements passed to `callback` that returned the key. The `callback` |
|
* is bound to `thisArg` and invoked with three arguments; (value, index|key, collection). |
|
* The `callback` argument may also be the name of a property to group by (e.g. 'length'). |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Collections |
|
* @param {Array|Object|String} collection The collection to iterate over. |
|
* @param {Function|String} callback|property The function called per iteration |
|
* or property name to group by. |
|
* @param {Mixed} [thisArg] The `this` binding of `callback`. |
|
* @returns {Object} Returns the composed aggregate object. |
|
* @example |
|
* |
|
* _.groupBy([4.2, 6.1, 6.4], function(num) { return Math.floor(num); }); |
|
* // => { '4': [4.2], '6': [6.1, 6.4] } |
|
* |
|
* _.groupBy([4.2, 6.1, 6.4], function(num) { return this.floor(num); }, Math); |
|
* // => { '4': [4.2], '6': [6.1, 6.4] } |
|
* |
|
* _.groupBy(['one', 'two', 'three'], 'length'); |
|
* // => { '3': ['one', 'two'], '5': ['three'] } |
|
*/ |
|
function groupBy(collection, callback, thisArg) { |
|
var result = {}; |
|
callback = createCallback(callback, thisArg); |
|
forEach(collection, function(value, key, collection) { |
|
key = callback(value, key, collection); |
|
(hasOwnProperty.call(result, key) ? result[key] : result[key] = []).push(value); |
|
}); |
|
return result; |
|
} |
|
|
|
/** |
|
* Invokes the method named by `methodName` on each element in the `collection`, |
|
* returning an array of the results of each invoked method. Additional arguments |
|
* will be passed to each invoked method. If `methodName` is a function it will |
|
* be invoked for, and `this` bound to, each element in the `collection`. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Collections |
|
* @param {Array|Object|String} collection The collection to iterate over. |
|
* @param {Function|String} methodName The name of the method to invoke or |
|
* the function invoked per iteration. |
|
* @param {Mixed} [arg1, arg2, ...] Arguments to invoke the method with. |
|
* @returns {Array} Returns a new array of the results of each invoked method. |
|
* @example |
|
* |
|
* _.invoke([[5, 1, 7], [3, 2, 1]], 'sort'); |
|
* // => [[1, 5, 7], [1, 2, 3]] |
|
* |
|
* _.invoke([123, 456], String.prototype.split, ''); |
|
* // => [['1', '2', '3'], ['4', '5', '6']] |
|
*/ |
|
function invoke(collection, methodName) { |
|
var args = slice.call(arguments, 2), |
|
isFunc = typeof methodName == 'function', |
|
result = []; |
|
|
|
forEach(collection, function(value) { |
|
result.push((isFunc ? methodName : value[methodName]).apply(value, args)); |
|
}); |
|
return result; |
|
} |
|
|
|
/** |
|
* Creates an array of values by running each element in the `collection` |
|
* through a `callback`. The `callback` is bound to `thisArg` and invoked with |
|
* three arguments; (value, index|key, collection). |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @alias collect |
|
* @category Collections |
|
* @param {Array|Object|String} collection The collection to iterate over. |
|
* @param {Function} [callback=identity] The function called per iteration. |
|
* @param {Mixed} [thisArg] The `this` binding of `callback`. |
|
* @returns {Array} Returns a new array of the results of each `callback` execution. |
|
* @example |
|
* |
|
* _.map([1, 2, 3], function(num) { return num * 3; }); |
|
* // => [3, 6, 9] |
|
* |
|
* _.map({ 'one': 1, 'two': 2, 'three': 3 }, function(num) { return num * 3; }); |
|
* // => [3, 6, 9] (order is not guaranteed) |
|
*/ |
|
function map(collection, callback, thisArg) { |
|
var index = -1, |
|
length = collection ? collection.length : 0, |
|
result = Array(typeof length == 'number' ? length : 0); |
|
|
|
callback = createCallback(callback, thisArg); |
|
if (isArray(collection)) { |
|
while (++index < length) { |
|
result[index] = callback(collection[index], index, collection); |
|
} |
|
} else { |
|
forEach(collection, function(value, key, collection) { |
|
result[++index] = callback(value, key, collection); |
|
}); |
|
} |
|
return result; |
|
} |
|
|
|
/** |
|
* Retrieves the maximum value of an `array`. If `callback` is passed, |
|
* it will be executed for each value in the `array` to generate the |
|
* criterion by which the value is ranked. The `callback` is bound to |
|
* `thisArg` and invoked with three arguments; (value, index, collection). |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Collections |
|
* @param {Array} collection The collection to iterate over. |
|
* @param {Function} [callback] The function called per iteration. |
|
* @param {Mixed} [thisArg] The `this` binding of `callback`. |
|
* @returns {Mixed} Returns the maximum value. |
|
* @example |
|
* |
|
* var stooges = [ |
|
* { 'name': 'moe', 'age': 40 }, |
|
* { 'name': 'larry', 'age': 50 }, |
|
* { 'name': 'curly', 'age': 60 } |
|
* ]; |
|
* |
|
* _.max(stooges, function(stooge) { return stooge.age; }); |
|
* // => { 'name': 'curly', 'age': 60 }; |
|
*/ |
|
function max(collection, callback, thisArg) { |
|
var computed = -Infinity, |
|
index = -1, |
|
length = collection ? collection.length : 0, |
|
result = computed; |
|
|
|
if (callback || typeof length != 'number') { |
|
callback = createCallback(callback, thisArg); |
|
forEach(collection, function(value, index, collection) { |
|
var current = callback(value, index, collection); |
|
if (current > computed) { |
|
computed = current; |
|
result = value; |
|
} |
|
}); |
|
} else { |
|
while (++index < length) { |
|
if (collection[index] > result) { |
|
result = collection[index]; |
|
} |
|
} |
|
} |
|
return result; |
|
} |
|
|
|
/** |
|
* Retrieves the minimum value of an `array`. If `callback` is passed, |
|
* it will be executed for each value in the `array` to generate the |
|
* criterion by which the value is ranked. The `callback` is bound to `thisArg` |
|
* and invoked with three arguments; (value, index, collection). |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Collections |
|
* @param {Array} collection The collection to iterate over. |
|
* @param {Function} [callback] The function called per iteration. |
|
* @param {Mixed} [thisArg] The `this` binding of `callback`. |
|
* @returns {Mixed} Returns the minimum value. |
|
* @example |
|
* |
|
* _.min([10, 5, 100, 2, 1000]); |
|
* // => 2 |
|
*/ |
|
function min(collection, callback, thisArg) { |
|
var computed = Infinity, |
|
index = -1, |
|
length = collection ? collection.length : 0, |
|
result = computed; |
|
|
|
if (callback || typeof length != 'number') { |
|
callback = createCallback(callback, thisArg); |
|
forEach(collection, function(value, index, collection) { |
|
var current = callback(value, index, collection); |
|
if (current < computed) { |
|
computed = current; |
|
result = value; |
|
} |
|
}); |
|
} else { |
|
while (++index < length) { |
|
if (collection[index] < result) { |
|
result = collection[index]; |
|
} |
|
} |
|
} |
|
return result; |
|
} |
|
|
|
/** |
|
* Retrieves the value of a specified property from all elements in |
|
* the `collection`. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Collections |
|
* @param {Array|Object|String} collection The collection to iterate over. |
|
* @param {String} property The property to pluck. |
|
* @returns {Array} Returns a new array of property values. |
|
* @example |
|
* |
|
* var stooges = [ |
|
* { 'name': 'moe', 'age': 40 }, |
|
* { 'name': 'larry', 'age': 50 }, |
|
* { 'name': 'curly', 'age': 60 } |
|
* ]; |
|
* |
|
* _.pluck(stooges, 'name'); |
|
* // => ['moe', 'larry', 'curly'] |
|
*/ |
|
function pluck(collection, property) { |
|
var result = []; |
|
forEach(collection, function(value) { |
|
result.push(value[property]); |
|
}); |
|
return result; |
|
} |
|
|
|
/** |
|
* Boils down a `collection` to a single value. The initial state of the |
|
* reduction is `accumulator` and each successive step of it should be returned |
|
* by the `callback`. The `callback` is bound to `thisArg` and invoked with 4 |
|
* arguments; for arrays they are (accumulator, value, index|key, collection). |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @alias foldl, inject |
|
* @category Collections |
|
* @param {Array|Object|String} collection The collection to iterate over. |
|
* @param {Function} callback The function called per iteration. |
|
* @param {Mixed} [accumulator] Initial value of the accumulator. |
|
* @param {Mixed} [thisArg] The `this` binding of `callback`. |
|
* @returns {Mixed} Returns the accumulated value. |
|
* @example |
|
* |
|
* var sum = _.reduce([1, 2, 3], function(memo, num) { return memo + num; }); |
|
* // => 6 |
|
*/ |
|
function reduce(collection, callback, accumulator, thisArg) { |
|
var noaccum = arguments.length < 3; |
|
callback = createCallback(callback, thisArg); |
|
forEach(collection, function(value, index, collection) { |
|
accumulator = noaccum |
|
? (noaccum = false, value) |
|
: callback(accumulator, value, index, collection) |
|
}); |
|
return accumulator; |
|
} |
|
|
|
/** |
|
* The right-associative version of `_.reduce`. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @alias foldr |
|
* @category Collections |
|
* @param {Array|Object|String} collection The collection to iterate over. |
|
* @param {Function} callback The function called per iteration. |
|
* @param {Mixed} [accumulator] Initial value of the accumulator. |
|
* @param {Mixed} [thisArg] The `this` binding of `callback`. |
|
* @returns {Mixed} Returns the accumulated value. |
|
* @example |
|
* |
|
* var list = [[0, 1], [2, 3], [4, 5]]; |
|
* var flat = _.reduceRight(list, function(a, b) { return a.concat(b); }, []); |
|
* // => [4, 5, 2, 3, 0, 1] |
|
*/ |
|
function reduceRight(collection, callback, accumulator, thisArg) { |
|
var iteratee = collection, |
|
length = collection ? collection.length : 0, |
|
noaccum = arguments.length < 3; |
|
|
|
if (typeof length != 'number') { |
|
var props = keys(collection); |
|
length = props.length; |
|
} else if (noCharByIndex && toString.call(collection) == stringClass) { |
|
iteratee = collection.split(''); |
|
} |
|
forEach(collection, function(value, index, collection) { |
|
index = props ? props[--length] : --length; |
|
accumulator = noaccum |
|
? (noaccum = false, iteratee[index]) |
|
: callback.call(thisArg, accumulator, iteratee[index], index, collection); |
|
}); |
|
return accumulator; |
|
} |
|
|
|
/** |
|
* The opposite of `_.filter`, this method returns the values of a |
|
* `collection` that `callback` does **not** return truthy for. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Collections |
|
* @param {Array|Object|String} collection The collection to iterate over. |
|
* @param {Function} [callback=identity] The function called per iteration. |
|
* @param {Mixed} [thisArg] The `this` binding of `callback`. |
|
* @returns {Array} Returns a new array of elements that did **not** pass the |
|
* callback check. |
|
* @example |
|
* |
|
* var odds = _.reject([1, 2, 3, 4, 5, 6], function(num) { return num % 2 == 0; }); |
|
* // => [1, 3, 5] |
|
*/ |
|
function reject(collection, callback, thisArg) { |
|
callback = createCallback(callback, thisArg); |
|
return filter(collection, function(value, index, collection) { |
|
return !callback(value, index, collection); |
|
}); |
|
} |
|
|
|
/** |
|
* Creates an array of shuffled `array` values, using a version of the |
|
* Fisher-Yates shuffle. See http://en.wikipedia.org/wiki/Fisher-Yates_shuffle. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Collections |
|
* @param {Array} collection The collection to shuffle. |
|
* @returns {Array} Returns a new shuffled collection. |
|
* @example |
|
* |
|
* _.shuffle([1, 2, 3, 4, 5, 6]); |
|
* // => [4, 1, 6, 3, 5, 2] |
|
*/ |
|
function shuffle(collection) { |
|
var index = -1, |
|
result = Array(collection ? collection.length : 0); |
|
|
|
forEach(collection, function(value) { |
|
var rand = floor(nativeRandom() * (++index + 1)); |
|
result[index] = result[rand]; |
|
result[rand] = value; |
|
}); |
|
return result; |
|
} |
|
|
|
/** |
|
* Gets the size of the `collection` by returning `collection.length` for arrays |
|
* and array-like objects or the number of own enumerable properties for objects. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Collections |
|
* @param {Array|Object|String} collection The collection to inspect. |
|
* @returns {Number} Returns `collection.length` or number of own enumerable properties. |
|
* @example |
|
* |
|
* _.size([1, 2]); |
|
* // => 2 |
|
* |
|
* _.size({ 'one': 1, 'two': 2, 'three': 3 }); |
|
* // => 3 |
|
* |
|
* _.size('curly'); |
|
* // => 5 |
|
*/ |
|
function size(collection) { |
|
var length = collection ? collection.length : 0; |
|
return typeof length == 'number' ? length : keys(collection).length; |
|
} |
|
|
|
/** |
|
* Checks if the `callback` returns a truthy value for **any** element of a |
|
* `collection`. The function returns as soon as it finds passing value, and |
|
* does not iterate over the entire `collection`. The `callback` is bound to |
|
* `thisArg` and invoked with three arguments; (value, index|key, collection). |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @alias any |
|
* @category Collections |
|
* @param {Array|Object|String} collection The collection to iterate over. |
|
* @param {Function} [callback=identity] The function called per iteration. |
|
* @param {Mixed} [thisArg] The `this` binding of `callback`. |
|
* @returns {Boolean} Returns `true` if any element passes the callback check, |
|
* else `false`. |
|
* @example |
|
* |
|
* _.some([null, 0, 'yes', false]); |
|
* // => true |
|
*/ |
|
function some(collection, callback, thisArg) { |
|
var result; |
|
callback = createCallback(callback, thisArg); |
|
forEach(collection, function(value, index, collection) { |
|
return !(result = callback(value, index, collection)); |
|
}); |
|
return !!result; |
|
} |
|
|
|
/** |
|
* Creates an array, stable sorted in ascending order by the results of |
|
* running each element of `collection` through a `callback`. The `callback` |
|
* is bound to `thisArg` and invoked with three arguments; (value, index|key, collection). |
|
* The `callback` argument may also be the name of a property to sort by (e.g. 'length'). |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Collections |
|
* @param {Array|Object|String} collection The collection to iterate over. |
|
* @param {Function|String} callback|property The function called per iteration |
|
* or property name to sort by. |
|
* @param {Mixed} [thisArg] The `this` binding of `callback`. |
|
* @returns {Array} Returns a new array of sorted elements. |
|
* @example |
|
* |
|
* _.sortBy([1, 2, 3], function(num) { return Math.sin(num); }); |
|
* // => [3, 1, 2] |
|
* |
|
* _.sortBy([1, 2, 3], function(num) { return this.sin(num); }, Math); |
|
* // => [3, 1, 2] |
|
* |
|
* _.sortBy(['larry', 'brendan', 'moe'], 'length'); |
|
* // => ['moe', 'larry', 'brendan'] |
|
*/ |
|
function sortBy(collection, callback, thisArg) { |
|
var result = []; |
|
callback = createCallback(callback, thisArg); |
|
forEach(collection, function(value, index, collection) { |
|
result.push({ |
|
'criteria': callback(value, index, collection), |
|
'index': index, |
|
'value': value |
|
}); |
|
}); |
|
|
|
var length = result.length; |
|
result.sort(compareAscending); |
|
while (length--) { |
|
result[length] = result[length].value; |
|
} |
|
return result; |
|
} |
|
|
|
/** |
|
* Converts the `collection`, to an array. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Collections |
|
* @param {Array|Object|String} collection The collection to convert. |
|
* @returns {Array} Returns the new converted array. |
|
* @example |
|
* |
|
* (function() { return _.toArray(arguments).slice(1); })(1, 2, 3, 4); |
|
* // => [2, 3, 4] |
|
*/ |
|
function toArray(collection) { |
|
if (collection && typeof collection.length == 'number') { |
|
return (noArraySliceOnStrings ? toString.call(collection) == stringClass : typeof collection == 'string') |
|
? collection.split('') |
|
: slice.call(collection); |
|
} |
|
return values(collection); |
|
} |
|
|
|
/** |
|
* Examines each element in a `collection`, returning an array of all elements |
|
* that contain the given `properties`. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Collections |
|
* @param {Array|Object|String} collection The collection to iterate over. |
|
* @param {Object} properties The object of property values to filter by. |
|
* @returns {Array} Returns a new array of elements that contain the given `properties`. |
|
* @example |
|
* |
|
* var stooges = [ |
|
* { 'name': 'moe', 'age': 40 }, |
|
* { 'name': 'larry', 'age': 50 }, |
|
* { 'name': 'curly', 'age': 60 } |
|
* ]; |
|
* |
|
* _.where(stooges, { 'age': 40 }); |
|
* // => [{ 'name': 'moe', 'age': 40 }] |
|
*/ |
|
function where(collection, properties) { |
|
var props = []; |
|
forIn(properties, function(value, prop) { |
|
props.push(prop); |
|
}); |
|
return filter(collection, function(object) { |
|
var length = props.length; |
|
while (length--) { |
|
var result = object[props[length]] === properties[props[length]]; |
|
if (!result) { |
|
break; |
|
} |
|
} |
|
return !!result; |
|
}); |
|
} |
|
|
|
/*--------------------------------------------------------------------------*/ |
|
|
|
/** |
|
* Creates an array with all falsey values of `array` removed. The values |
|
* `false`, `null`, `0`, `""`, `undefined` and `NaN` are all falsey. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Arrays |
|
* @param {Array} array The array to compact. |
|
* @returns {Array} Returns a new filtered array. |
|
* @example |
|
* |
|
* _.compact([0, 1, false, 2, '', 3]); |
|
* // => [1, 2, 3] |
|
*/ |
|
function compact(array) { |
|
var index = -1, |
|
length = array ? array.length : 0, |
|
result = []; |
|
|
|
while (++index < length) { |
|
var value = array[index]; |
|
if (value) { |
|
result.push(value); |
|
} |
|
} |
|
return result; |
|
} |
|
|
|
/** |
|
* Creates an array of `array` elements not present in the other arrays |
|
* using strict equality for comparisons, i.e. `===`. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Arrays |
|
* @param {Array} array The array to process. |
|
* @param {Array} [array1, array2, ...] Arrays to check. |
|
* @returns {Array} Returns a new array of `array` elements not present in the |
|
* other arrays. |
|
* @example |
|
* |
|
* _.difference([1, 2, 3, 4, 5], [5, 2, 10]); |
|
* // => [1, 3, 4] |
|
*/ |
|
function difference(array) { |
|
var index = -1, |
|
length = array ? array.length : 0, |
|
flattened = concat.apply(arrayRef, arguments), |
|
contains = cachedContains(flattened, length), |
|
result = []; |
|
|
|
while (++index < length) { |
|
var value = array[index]; |
|
if (!contains(value)) { |
|
result.push(value); |
|
} |
|
} |
|
return result; |
|
} |
|
|
|
/** |
|
* Gets the first element of the `array`. Pass `n` to return the first `n` |
|
* elements of the `array`. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @alias head, take |
|
* @category Arrays |
|
* @param {Array} array The array to query. |
|
* @param {Number} [n] The number of elements to return. |
|
* @param- {Object} [guard] Internally used to allow this method to work with |
|
* others like `_.map` without using their callback `index` argument for `n`. |
|
* @returns {Mixed} Returns the first element or an array of the first `n` |
|
* elements of `array`. |
|
* @example |
|
* |
|
* _.first([5, 4, 3, 2, 1]); |
|
* // => 5 |
|
*/ |
|
function first(array, n, guard) { |
|
if (array) { |
|
return (n == null || guard) ? array[0] : slice.call(array, 0, n); |
|
} |
|
} |
|
|
|
/** |
|
* Flattens a nested array (the nesting can be to any depth). If `shallow` is |
|
* truthy, `array` will only be flattened a single level. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Arrays |
|
* @param {Array} array The array to compact. |
|
* @param {Boolean} shallow A flag to indicate only flattening a single level. |
|
* @returns {Array} Returns a new flattened array. |
|
* @example |
|
* |
|
* _.flatten([1, [2], [3, [[4]]]]); |
|
* // => [1, 2, 3, 4]; |
|
* |
|
* _.flatten([1, [2], [3, [[4]]]], true); |
|
* // => [1, 2, 3, [[4]]]; |
|
*/ |
|
function flatten(array, shallow) { |
|
var index = -1, |
|
length = array ? array.length : 0, |
|
result = []; |
|
|
|
while (++index < length) { |
|
var value = array[index]; |
|
|
|
// recursively flatten arrays (susceptible to call stack limits) |
|
if (isArray(value)) { |
|
push.apply(result, shallow ? value : flatten(value)); |
|
} else { |
|
result.push(value); |
|
} |
|
} |
|
return result; |
|
} |
|
|
|
/** |
|
* Gets the index at which the first occurrence of `value` is found using |
|
* strict equality for comparisons, i.e. `===`. If the `array` is already |
|
* sorted, passing `true` for `fromIndex` will run a faster binary search. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Arrays |
|
* @param {Array} array The array to search. |
|
* @param {Mixed} value The value to search for. |
|
* @param {Boolean|Number} [fromIndex=0] The index to start searching from or |
|
* `true` to perform a binary search on a sorted `array`. |
|
* @returns {Number} Returns the index of the matched value or `-1`. |
|
* @example |
|
* |
|
* _.indexOf([1, 2, 3, 1, 2, 3], 2); |
|
* // => 1 |
|
* |
|
* _.indexOf([1, 2, 3, 1, 2, 3], 2, 3); |
|
* // => 4 |
|
* |
|
* _.indexOf([1, 1, 2, 2, 3, 3], 2, true); |
|
* // => 2 |
|
*/ |
|
function indexOf(array, value, fromIndex) { |
|
var index = -1, |
|
length = array ? array.length : 0; |
|
|
|
if (typeof fromIndex == 'number') { |
|
index = (fromIndex < 0 ? nativeMax(0, length + fromIndex) : fromIndex || 0) - 1; |
|
} else if (fromIndex) { |
|
index = sortedIndex(array, value); |
|
return array[index] === value ? index : -1; |
|
} |
|
while (++index < length) { |
|
if (array[index] === value) { |
|
return index; |
|
} |
|
} |
|
return -1; |
|
} |
|
|
|
/** |
|
* Gets all but the last element of `array`. Pass `n` to exclude the last `n` |
|
* elements from the result. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Arrays |
|
* @param {Array} array The array to query. |
|
* @param {Number} [n] The number of elements to return. |
|
* @param- {Object} [guard] Internally used to allow this method to work with |
|
* others like `_.map` without using their callback `index` argument for `n`. |
|
* @returns {Array} Returns all but the last element or `n` elements of `array`. |
|
* @example |
|
* |
|
* _.initial([3, 2, 1]); |
|
* // => [3, 2] |
|
*/ |
|
function initial(array, n, guard) { |
|
return array |
|
? slice.call(array, 0, -((n == null || guard) ? 1 : n)) |
|
: []; |
|
} |
|
|
|
/** |
|
* Computes the intersection of all the passed-in arrays using strict equality |
|
* for comparisons, i.e. `===`. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Arrays |
|
* @param {Array} [array1, array2, ...] Arrays to process. |
|
* @returns {Array} Returns a new array of unique elements, in order, that are |
|
* present in **all** of the arrays. |
|
* @example |
|
* |
|
* _.intersection([1, 2, 3], [101, 2, 1, 10], [2, 1]); |
|
* // => [1, 2] |
|
*/ |
|
function intersection(array) { |
|
var args = arguments, |
|
argsLength = args.length, |
|
cache = {}, |
|
result = []; |
|
|
|
forEach(array, function(value) { |
|
if (indexOf(result, value) < 0) { |
|
var length = argsLength; |
|
while (--length) { |
|
if (!(cache[length] || (cache[length] = cachedContains(args[length])))(value)) { |
|
return; |
|
} |
|
} |
|
result.push(value); |
|
} |
|
}); |
|
return result; |
|
} |
|
|
|
/** |
|
* Gets the last element of the `array`. Pass `n` to return the last `n` |
|
* elements of the `array`. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Arrays |
|
* @param {Array} array The array to query. |
|
* @param {Number} [n] The number of elements to return. |
|
* @param- {Object} [guard] Internally used to allow this method to work with |
|
* others like `_.map` without using their callback `index` argument for `n`. |
|
* @returns {Mixed} Returns the last element or an array of the last `n` |
|
* elements of `array`. |
|
* @example |
|
* |
|
* _.last([3, 2, 1]); |
|
* // => 1 |
|
*/ |
|
function last(array, n, guard) { |
|
if (array) { |
|
var length = array.length; |
|
return (n == null || guard) ? array[length - 1] : slice.call(array, -n || length); |
|
} |
|
} |
|
|
|
/** |
|
* Gets the index at which the last occurrence of `value` is found using |
|
* strict equality for comparisons, i.e. `===`. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Arrays |
|
* @param {Array} array The array to search. |
|
* @param {Mixed} value The value to search for. |
|
* @param {Number} [fromIndex=array.length-1] The index to start searching from. |
|
* @returns {Number} Returns the index of the matched value or `-1`. |
|
* @example |
|
* |
|
* _.lastIndexOf([1, 2, 3, 1, 2, 3], 2); |
|
* // => 4 |
|
* |
|
* _.lastIndexOf([1, 2, 3, 1, 2, 3], 2, 3); |
|
* // => 1 |
|
*/ |
|
function lastIndexOf(array, value, fromIndex) { |
|
var index = array ? array.length : 0; |
|
if (typeof fromIndex == 'number') { |
|
index = (fromIndex < 0 ? nativeMax(0, index + fromIndex) : nativeMin(fromIndex, index - 1)) + 1; |
|
} |
|
while (index--) { |
|
if (array[index] === value) { |
|
return index; |
|
} |
|
} |
|
return -1; |
|
} |
|
|
|
/** |
|
* Creates an object composed from arrays of `keys` and `values`. Pass either |
|
* a single two dimensional array, i.e. `[[key1, value1], [key2, value2]]`, or |
|
* two arrays, one of `keys` and one of corresponding `values`. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Arrays |
|
* @param {Array} keys The array of keys. |
|
* @param {Array} [values=[]] The array of values. |
|
* @returns {Object} Returns an object composed of the given keys and |
|
* corresponding values. |
|
* @example |
|
* |
|
* _.object(['moe', 'larry', 'curly'], [30, 40, 50]); |
|
* // => { 'moe': 30, 'larry': 40, 'curly': 50 } |
|
*/ |
|
function object(keys, values) { |
|
var index = -1, |
|
length = keys ? keys.length : 0, |
|
result = {}; |
|
|
|
while (++index < length) { |
|
var key = keys[index]; |
|
if (values) { |
|
result[key] = values[index]; |
|
} else { |
|
result[key[0]] = key[1]; |
|
} |
|
} |
|
return result; |
|
} |
|
|
|
/** |
|
* Creates an array of numbers (positive and/or negative) progressing from |
|
* `start` up to but not including `stop`. This method is a port of Python's |
|
* `range()` function. See http://docs.python.org/library/functions.html#range. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Arrays |
|
* @param {Number} [start=0] The start of the range. |
|
* @param {Number} end The end of the range. |
|
* @param {Number} [step=1] The value to increment or descrement by. |
|
* @returns {Array} Returns a new range array. |
|
* @example |
|
* |
|
* _.range(10); |
|
* // => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] |
|
* |
|
* _.range(1, 11); |
|
* // => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] |
|
* |
|
* _.range(0, 30, 5); |
|
* // => [0, 5, 10, 15, 20, 25] |
|
* |
|
* _.range(0, -10, -1); |
|
* // => [0, -1, -2, -3, -4, -5, -6, -7, -8, -9] |
|
* |
|
* _.range(0); |
|
* // => [] |
|
*/ |
|
function range(start, end, step) { |
|
start = +start || 0; |
|
step = +step || 1; |
|
|
|
if (end == null) { |
|
end = start; |
|
start = 0; |
|
} |
|
// use `Array(length)` so V8 will avoid the slower "dictionary" mode |
|
// http://www.youtube.com/watch?v=XAqIpGU8ZZk#t=16m27s |
|
var index = -1, |
|
length = nativeMax(0, ceil((end - start) / step)), |
|
result = Array(length); |
|
|
|
while (++index < length) { |
|
result[index] = start; |
|
start += step; |
|
} |
|
return result; |
|
} |
|
|
|
/** |
|
* The opposite of `_.initial`, this method gets all but the first value of |
|
* `array`. Pass `n` to exclude the first `n` values from the result. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @alias drop, tail |
|
* @category Arrays |
|
* @param {Array} array The array to query. |
|
* @param {Number} [n] The number of elements to return. |
|
* @param- {Object} [guard] Internally used to allow this method to work with |
|
* others like `_.map` without using their callback `index` argument for `n`. |
|
* @returns {Array} Returns all but the first value or `n` values of `array`. |
|
* @example |
|
* |
|
* _.rest([3, 2, 1]); |
|
* // => [2, 1] |
|
*/ |
|
function rest(array, n, guard) { |
|
return array |
|
? slice.call(array, (n == null || guard) ? 1 : n) |
|
: []; |
|
} |
|
|
|
/** |
|
* Uses a binary search to determine the smallest index at which the `value` |
|
* should be inserted into `array` in order to maintain the sort order of the |
|
* sorted `array`. If `callback` is passed, it will be executed for `value` and |
|
* each element in `array` to compute their sort ranking. The `callback` is |
|
* bound to `thisArg` and invoked with one argument; (value). The `callback` |
|
* argument may also be the name of a property to order by. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Arrays |
|
* @param {Array} array The array to iterate over. |
|
* @param {Mixed} value The value to evaluate. |
|
* @param {Function|String} [callback=identity|property] The function called |
|
* per iteration or property name to order by. |
|
* @param {Mixed} [thisArg] The `this` binding of `callback`. |
|
* @returns {Number} Returns the index at which the value should be inserted |
|
* into `array`. |
|
* @example |
|
* |
|
* _.sortedIndex([20, 30, 50], 40); |
|
* // => 2 |
|
* |
|
* _.sortedIndex([{ 'x': 20 }, { 'x': 30 }, { 'x': 50 }], { 'x': 40 }, 'x'); |
|
* // => 2 |
|
* |
|
* var dict = { |
|
* 'wordToNumber': { 'twenty': 20, 'thirty': 30, 'fourty': 40, 'fifty': 50 } |
|
* }; |
|
* |
|
* _.sortedIndex(['twenty', 'thirty', 'fifty'], 'fourty', function(word) { |
|
* return dict.wordToNumber[word]; |
|
* }); |
|
* // => 2 |
|
* |
|
* _.sortedIndex(['twenty', 'thirty', 'fifty'], 'fourty', function(word) { |
|
* return this.wordToNumber[word]; |
|
* }, dict); |
|
* // => 2 |
|
*/ |
|
function sortedIndex(array, value, callback, thisArg) { |
|
var low = 0, |
|
high = array ? array.length : low; |
|
|
|
// explicitly reference `identity` for better engine inlining |
|
callback = callback ? createCallback(callback, thisArg) : identity; |
|
value = callback(value); |
|
while (low < high) { |
|
var mid = (low + high) >>> 1; |
|
callback(array[mid]) < value |
|
? low = mid + 1 |
|
: high = mid; |
|
} |
|
return low; |
|
} |
|
|
|
/** |
|
* Computes the union of the passed-in arrays using strict equality for |
|
* comparisons, i.e. `===`. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Arrays |
|
* @param {Array} [array1, array2, ...] Arrays to process. |
|
* @returns {Array} Returns a new array of unique values, in order, that are |
|
* present in one or more of the arrays. |
|
* @example |
|
* |
|
* _.union([1, 2, 3], [101, 2, 1, 10], [2, 1]); |
|
* // => [1, 2, 3, 101, 10] |
|
*/ |
|
function union() { |
|
var index = -1, |
|
flattened = concat.apply(arrayRef, arguments), |
|
length = flattened.length, |
|
result = []; |
|
|
|
while (++index < length) { |
|
var value = flattened[index]; |
|
if (indexOf(result, value) < 0) { |
|
result.push(value); |
|
} |
|
} |
|
return result; |
|
} |
|
|
|
/** |
|
* Creates a duplicate-value-free version of the `array` using strict equality |
|
* for comparisons, i.e. `===`. If the `array` is already sorted, passing `true` |
|
* for `isSorted` will run a faster algorithm. If `callback` is passed, each |
|
* element of `array` is passed through a callback` before uniqueness is computed. |
|
* The `callback` is bound to `thisArg` and invoked with three arguments; (value, index, array). |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @alias unique |
|
* @category Arrays |
|
* @param {Array} array The array to process. |
|
* @param {Boolean} [isSorted=false] A flag to indicate that the `array` is already sorted. |
|
* @param {Function} [callback=identity] The function called per iteration. |
|
* @param {Mixed} [thisArg] The `this` binding of `callback`. |
|
* @returns {Array} Returns a duplicate-value-free array. |
|
* @example |
|
* |
|
* _.uniq([1, 2, 1, 3, 1]); |
|
* // => [1, 2, 3] |
|
* |
|
* _.uniq([1, 1, 2, 2, 3], true); |
|
* // => [1, 2, 3] |
|
* |
|
* _.uniq([1, 2, 1.5, 3, 2.5], function(num) { return Math.floor(num); }); |
|
* // => [1, 2, 3] |
|
* |
|
* _.uniq([1, 2, 1.5, 3, 2.5], function(num) { return this.floor(num); }, Math); |
|
* // => [1, 2, 3] |
|
*/ |
|
function uniq(array, isSorted, callback, thisArg) { |
|
var index = -1, |
|
length = array ? array.length : 0, |
|
result = [], |
|
seen = []; |
|
|
|
// juggle arguments |
|
if (typeof isSorted == 'function') { |
|
thisArg = callback; |
|
callback = isSorted; |
|
isSorted = false; |
|
} |
|
callback = createCallback(callback, thisArg); |
|
while (++index < length) { |
|
var computed = callback(array[index], index, array); |
|
if (isSorted |
|
? !index || seen[seen.length - 1] !== computed |
|
: indexOf(seen, computed) < 0 |
|
) { |
|
seen.push(computed); |
|
result.push(array[index]); |
|
} |
|
} |
|
return result; |
|
} |
|
|
|
/** |
|
* Creates an array with all occurrences of the passed values removed using |
|
* strict equality for comparisons, i.e. `===`. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Arrays |
|
* @param {Array} array The array to filter. |
|
* @param {Mixed} [value1, value2, ...] Values to remove. |
|
* @returns {Array} Returns a new filtered array. |
|
* @example |
|
* |
|
* _.without([1, 2, 1, 0, 3, 1, 4], 0, 1); |
|
* // => [2, 3, 4] |
|
*/ |
|
function without(array) { |
|
var index = -1, |
|
length = array ? array.length : 0, |
|
contains = cachedContains(arguments, 1, 20), |
|
result = []; |
|
|
|
while (++index < length) { |
|
var value = array[index]; |
|
if (!contains(value)) { |
|
result.push(value); |
|
} |
|
} |
|
return result; |
|
} |
|
|
|
/** |
|
* Groups the elements of each array at their corresponding indexes. Useful for |
|
* separate data sources that are coordinated through matching array indexes. |
|
* For a matrix of nested arrays, `_.zip.apply(...)` can transpose the matrix |
|
* in a similar fashion. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Arrays |
|
* @param {Array} [array1, array2, ...] Arrays to process. |
|
* @returns {Array} Returns a new array of grouped elements. |
|
* @example |
|
* |
|
* _.zip(['moe', 'larry', 'curly'], [30, 40, 50], [true, false, false]); |
|
* // => [['moe', 30, true], ['larry', 40, false], ['curly', 50, false]] |
|
*/ |
|
function zip(array) { |
|
var index = -1, |
|
length = array ? max(pluck(arguments, 'length')) : 0, |
|
result = Array(length); |
|
|
|
while (++index < length) { |
|
result[index] = pluck(arguments, index); |
|
} |
|
return result; |
|
} |
|
|
|
/*--------------------------------------------------------------------------*/ |
|
|
|
/** |
|
* Creates a function that is restricted to executing only after it is |
|
* called `n` times. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Functions |
|
* @param {Number} n The number of times the function must be called before |
|
* it is executed. |
|
* @param {Function} func The function to restrict. |
|
* @returns {Function} Returns the new restricted function. |
|
* @example |
|
* |
|
* var renderNotes = _.after(notes.length, render); |
|
* _.forEach(notes, function(note) { |
|
* note.asyncSave({ 'success': renderNotes }); |
|
* }); |
|
* // `renderNotes` is run once, after all notes have saved |
|
*/ |
|
function after(n, func) { |
|
if (n < 1) { |
|
return func(); |
|
} |
|
return function() { |
|
if (--n < 1) { |
|
return func.apply(this, arguments); |
|
} |
|
}; |
|
} |
|
|
|
/** |
|
* Creates a function that, when called, invokes `func` with the `this` |
|
* binding of `thisArg` and prepends any additional `bind` arguments to those |
|
* passed to the bound function. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Functions |
|
* @param {Function} func The function to bind. |
|
* @param {Mixed} [thisArg] The `this` binding of `func`. |
|
* @param {Mixed} [arg1, arg2, ...] Arguments to be partially applied. |
|
* @returns {Function} Returns the new bound function. |
|
* @example |
|
* |
|
* var func = function(greeting) { |
|
* return greeting + ' ' + this.name; |
|
* }; |
|
* |
|
* func = _.bind(func, { 'name': 'moe' }, 'hi'); |
|
* func(); |
|
* // => 'hi moe' |
|
*/ |
|
function bind(func, thisArg) { |
|
// use `Function#bind` if it exists and is fast |
|
// (in V8 `Function#bind` is slower except when partially applied) |
|
return isBindFast || (nativeBind && arguments.length > 2) |
|
? nativeBind.call.apply(nativeBind, arguments) |
|
: createBound(func, thisArg, slice.call(arguments, 2)); |
|
} |
|
|
|
/** |
|
* Binds methods on `object` to `object`, overwriting the existing method. |
|
* If no method names are provided, all the function properties of `object` |
|
* will be bound. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Functions |
|
* @param {Object} object The object to bind and assign the bound methods to. |
|
* @param {String} [methodName1, methodName2, ...] Method names on the object to bind. |
|
* @returns {Object} Returns `object`. |
|
* @example |
|
* |
|
* var buttonView = { |
|
* 'label': 'lodash', |
|
* 'onClick': function() { alert('clicked: ' + this.label); } |
|
* }; |
|
* |
|
* _.bindAll(buttonView); |
|
* jQuery('#lodash_button').on('click', buttonView.onClick); |
|
* // => When the button is clicked, `this.label` will have the correct value |
|
*/ |
|
function bindAll(object) { |
|
var funcs = arguments, |
|
index = funcs.length > 1 ? 0 : (funcs = functions(object), -1), |
|
length = funcs.length; |
|
|
|
while (++index < length) { |
|
var key = funcs[index]; |
|
object[key] = bind(object[key], object); |
|
} |
|
return object; |
|
} |
|
|
|
/** |
|
* Creates a function that is the composition of the passed functions, |
|
* where each function consumes the return value of the function that follows. |
|
* In math terms, composing the functions `f()`, `g()`, and `h()` produces `f(g(h()))`. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Functions |
|
* @param {Function} [func1, func2, ...] Functions to compose. |
|
* @returns {Function} Returns the new composed function. |
|
* @example |
|
* |
|
* var greet = function(name) { return 'hi: ' + name; }; |
|
* var exclaim = function(statement) { return statement + '!'; }; |
|
* var welcome = _.compose(exclaim, greet); |
|
* welcome('moe'); |
|
* // => 'hi: moe!' |
|
*/ |
|
function compose() { |
|
var funcs = arguments; |
|
return function() { |
|
var args = arguments, |
|
length = funcs.length; |
|
|
|
while (length--) { |
|
args = [funcs[length].apply(this, args)]; |
|
} |
|
return args[0]; |
|
}; |
|
} |
|
|
|
/** |
|
* Creates a function that will delay the execution of `func` until after |
|
* `wait` milliseconds have elapsed since the last time it was invoked. Pass |
|
* `true` for `immediate` to cause debounce to invoke `func` on the leading, |
|
* instead of the trailing, edge of the `wait` timeout. Subsequent calls to |
|
* the debounced function will return the result of the last `func` call. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Functions |
|
* @param {Function} func The function to debounce. |
|
* @param {Number} wait The number of milliseconds to delay. |
|
* @param {Boolean} immediate A flag to indicate execution is on the leading |
|
* edge of the timeout. |
|
* @returns {Function} Returns the new debounced function. |
|
* @example |
|
* |
|
* var lazyLayout = _.debounce(calculateLayout, 300); |
|
* jQuery(window).on('resize', lazyLayout); |
|
*/ |
|
function debounce(func, wait, immediate) { |
|
var args, |
|
result, |
|
thisArg, |
|
timeoutId; |
|
|
|
function delayed() { |
|
timeoutId = null; |
|
if (!immediate) { |
|
result = func.apply(thisArg, args); |
|
} |
|
} |
|
return function() { |
|
var isImmediate = immediate && !timeoutId; |
|
args = arguments; |
|
thisArg = this; |
|
|
|
clearTimeout(timeoutId); |
|
timeoutId = setTimeout(delayed, wait); |
|
|
|
if (isImmediate) { |
|
result = func.apply(thisArg, args); |
|
} |
|
return result; |
|
}; |
|
} |
|
|
|
/** |
|
* Executes the `func` function after `wait` milliseconds. Additional arguments |
|
* will be passed to `func` when it is invoked. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Functions |
|
* @param {Function} func The function to delay. |
|
* @param {Number} wait The number of milliseconds to delay execution. |
|
* @param {Mixed} [arg1, arg2, ...] Arguments to invoke the function with. |
|
* @returns {Number} Returns the `setTimeout` timeout id. |
|
* @example |
|
* |
|
* var log = _.bind(console.log, console); |
|
* _.delay(log, 1000, 'logged later'); |
|
* // => 'logged later' (Appears after one second.) |
|
*/ |
|
function delay(func, wait) { |
|
var args = slice.call(arguments, 2); |
|
return setTimeout(function() { func.apply(undefined, args); }, wait); |
|
} |
|
|
|
/** |
|
* Defers executing the `func` function until the current call stack has cleared. |
|
* Additional arguments will be passed to `func` when it is invoked. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Functions |
|
* @param {Function} func The function to defer. |
|
* @param {Mixed} [arg1, arg2, ...] Arguments to invoke the function with. |
|
* @returns {Number} Returns the `setTimeout` timeout id. |
|
* @example |
|
* |
|
* _.defer(function() { alert('deferred'); }); |
|
* // returns from the function before `alert` is called |
|
*/ |
|
function defer(func) { |
|
var args = slice.call(arguments, 1); |
|
return setTimeout(function() { func.apply(undefined, args); }, 1); |
|
} |
|
|
|
/** |
|
* Creates a function that, when called, invokes `object[methodName]` and |
|
* prepends any additional `lateBind` arguments to those passed to the bound |
|
* function. This method differs from `_.bind` by allowing bound functions to |
|
* reference methods that will be redefined or don't yet exist. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Functions |
|
* @param {Object} object The object the method belongs to. |
|
* @param {String} methodName The method name. |
|
* @param {Mixed} [arg1, arg2, ...] Arguments to be partially applied. |
|
* @returns {Function} Returns the new bound function. |
|
* @example |
|
* |
|
* var object = { |
|
* 'name': 'moe', |
|
* 'greet': function(greeting) { |
|
* return greeting + ' ' + this.name; |
|
* } |
|
* }; |
|
* |
|
* var func = _.lateBind(object, 'greet', 'hi'); |
|
* func(); |
|
* // => 'hi moe' |
|
* |
|
* object.greet = function(greeting) { |
|
* return greeting + ', ' + this.name + '!'; |
|
* }; |
|
* |
|
* func(); |
|
* // => 'hi, moe!' |
|
*/ |
|
function lateBind(object, methodName) { |
|
return createBound(methodName, object, slice.call(arguments, 2)); |
|
} |
|
|
|
/** |
|
* Creates a function that memoizes the result of `func`. If `resolver` is |
|
* passed, it will be used to determine the cache key for storing the result |
|
* based on the arguments passed to the memoized function. By default, the first |
|
* argument passed to the memoized function is used as the cache key. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Functions |
|
* @param {Function} func The function to have its output memoized. |
|
* @param {Function} [resolver] A function used to resolve the cache key. |
|
* @returns {Function} Returns the new memoizing function. |
|
* @example |
|
* |
|
* var fibonacci = _.memoize(function(n) { |
|
* return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2); |
|
* }); |
|
*/ |
|
function memoize(func, resolver) { |
|
var cache = {}; |
|
return function() { |
|
var key = resolver ? resolver.apply(this, arguments) : arguments[0]; |
|
return hasOwnProperty.call(cache, key) |
|
? cache[key] |
|
: (cache[key] = func.apply(this, arguments)); |
|
}; |
|
} |
|
|
|
/** |
|
* Creates a function that is restricted to one execution. Repeat calls to |
|
* the function will return the value of the first call. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Functions |
|
* @param {Function} func The function to restrict. |
|
* @returns {Function} Returns the new restricted function. |
|
* @example |
|
* |
|
* var initialize = _.once(createApplication); |
|
* initialize(); |
|
* initialize(); |
|
* // Application is only created once. |
|
*/ |
|
function once(func) { |
|
var result, |
|
ran = false; |
|
|
|
return function() { |
|
if (ran) { |
|
return result; |
|
} |
|
ran = true; |
|
result = func.apply(this, arguments); |
|
|
|
// clear the `func` variable so the function may be garbage collected |
|
func = null; |
|
return result; |
|
}; |
|
} |
|
|
|
/** |
|
* Creates a function that, when called, invokes `func` with any additional |
|
* `partial` arguments prepended to those passed to the new function. This method |
|
* is similar to `bind`, except it does **not** alter the `this` binding. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Functions |
|
* @param {Function} func The function to partially apply arguments to. |
|
* @param {Mixed} [arg1, arg2, ...] Arguments to be partially applied. |
|
* @returns {Function} Returns the new partially applied function. |
|
* @example |
|
* |
|
* var greet = function(greeting, name) { return greeting + ': ' + name; }; |
|
* var hi = _.partial(greet, 'hi'); |
|
* hi('moe'); |
|
* // => 'hi: moe' |
|
*/ |
|
function partial(func) { |
|
return createBound(func, slice.call(arguments, 1)); |
|
} |
|
|
|
/** |
|
* Creates a function that, when executed, will only call the `func` |
|
* function at most once per every `wait` milliseconds. If the throttled |
|
* function is invoked more than once during the `wait` timeout, `func` will |
|
* also be called on the trailing edge of the timeout. Subsequent calls to the |
|
* throttled function will return the result of the last `func` call. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Functions |
|
* @param {Function} func The function to throttle. |
|
* @param {Number} wait The number of milliseconds to throttle executions to. |
|
* @returns {Function} Returns the new throttled function. |
|
* @example |
|
* |
|
* var throttled = _.throttle(updatePosition, 100); |
|
* jQuery(window).on('scroll', throttled); |
|
*/ |
|
function throttle(func, wait) { |
|
var args, |
|
result, |
|
thisArg, |
|
timeoutId, |
|
lastCalled = 0; |
|
|
|
function trailingCall() { |
|
lastCalled = new Date; |
|
timeoutId = null; |
|
result = func.apply(thisArg, args); |
|
} |
|
return function() { |
|
var now = new Date, |
|
remaining = wait - (now - lastCalled); |
|
|
|
args = arguments; |
|
thisArg = this; |
|
|
|
if (remaining <= 0) { |
|
clearTimeout(timeoutId); |
|
lastCalled = now; |
|
result = func.apply(thisArg, args); |
|
} |
|
else if (!timeoutId) { |
|
timeoutId = setTimeout(trailingCall, remaining); |
|
} |
|
return result; |
|
}; |
|
} |
|
|
|
/** |
|
* Creates a function that passes `value` to the `wrapper` function as its |
|
* first argument. Additional arguments passed to the new function are appended |
|
* to those passed to the `wrapper` function. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Functions |
|
* @param {Mixed} value The value to wrap. |
|
* @param {Function} wrapper The wrapper function. |
|
* @returns {Function} Returns the new function. |
|
* @example |
|
* |
|
* var hello = function(name) { return 'hello ' + name; }; |
|
* hello = _.wrap(hello, function(func) { |
|
* return 'before, ' + func('moe') + ', after'; |
|
* }); |
|
* hello(); |
|
* // => 'before, hello moe, after' |
|
*/ |
|
function wrap(value, wrapper) { |
|
return function() { |
|
var args = [value]; |
|
push.apply(args, arguments); |
|
return wrapper.apply(this, args); |
|
}; |
|
} |
|
|
|
/*--------------------------------------------------------------------------*/ |
|
|
|
/** |
|
* Converts the characters `&`, `<`, `>`, `"`, and `'` in `string` to their |
|
* corresponding HTML entities. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Utilities |
|
* @param {String} string The string to escape. |
|
* @returns {String} Returns the escaped string. |
|
* @example |
|
* |
|
* _.escape('Moe, Larry & Curly'); |
|
* // => "Moe, Larry & Curly" |
|
*/ |
|
function escape(string) { |
|
return string == null ? '' : (string + '').replace(reUnescapedHtml, escapeHtmlChar); |
|
} |
|
|
|
/** |
|
* This function returns the first argument passed to it. |
|
* |
|
* Note: It is used throughout Lo-Dash as a default callback. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Utilities |
|
* @param {Mixed} value Any value. |
|
* @returns {Mixed} Returns `value`. |
|
* @example |
|
* |
|
* var moe = { 'name': 'moe' }; |
|
* moe === _.identity(moe); |
|
* // => true |
|
*/ |
|
function identity(value) { |
|
return value; |
|
} |
|
|
|
/** |
|
* Adds functions properties of `object` to the `lodash` function and chainable |
|
* wrapper. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Utilities |
|
* @param {Object} object The object of function properties to add to `lodash`. |
|
* @example |
|
* |
|
* _.mixin({ |
|
* 'capitalize': function(string) { |
|
* return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase(); |
|
* } |
|
* }); |
|
* |
|
* _.capitalize('larry'); |
|
* // => 'Larry' |
|
* |
|
* _('curly').capitalize(); |
|
* // => 'Curly' |
|
*/ |
|
function mixin(object) { |
|
forEach(functions(object), function(methodName) { |
|
var func = lodash[methodName] = object[methodName]; |
|
|
|
lodash.prototype[methodName] = function() { |
|
var args = [this.__wrapped__]; |
|
push.apply(args, arguments); |
|
|
|
var result = func.apply(lodash, args); |
|
if (this.__chain__) { |
|
result = new lodash(result); |
|
result.__chain__ = true; |
|
} |
|
return result; |
|
}; |
|
}); |
|
} |
|
|
|
/** |
|
* Reverts the '_' variable to its previous value and returns a reference to |
|
* the `lodash` function. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Utilities |
|
* @returns {Function} Returns the `lodash` function. |
|
* @example |
|
* |
|
* var lodash = _.noConflict(); |
|
*/ |
|
function noConflict() { |
|
window._ = oldDash; |
|
return this; |
|
} |
|
|
|
/** |
|
* Produces a random number between `min` and `max` (inclusive). If only one |
|
* argument is passed, a number between `0` and the given number will be returned. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Utilities |
|
* @param {Number} [min=0] The minimum possible value. |
|
* @param {Number} [max=1] The maximum possible value. |
|
* @returns {Number} Returns a random number. |
|
* @example |
|
* |
|
* _.random(0, 5); |
|
* // => a number between 1 and 5 |
|
* |
|
* _.random(5); |
|
* // => also a number between 1 and 5 |
|
*/ |
|
function random(min, max) { |
|
if (min == null && max == null) { |
|
max = 1; |
|
} |
|
min = +min || 0; |
|
if (max == null) { |
|
max = min; |
|
min = 0; |
|
} |
|
return min + floor(nativeRandom() * ((+max || 0) - min + 1)); |
|
} |
|
|
|
/** |
|
* Resolves the value of `property` on `object`. If `property` is a function |
|
* it will be invoked and its result returned, else the property value is |
|
* returned. If `object` is falsey, then `null` is returned. |
|
* |
|
* @deprecated |
|
* @static |
|
* @memberOf _ |
|
* @category Utilities |
|
* @param {Object} object The object to inspect. |
|
* @param {String} property The property to get the value of. |
|
* @returns {Mixed} Returns the resolved value. |
|
* @example |
|
* |
|
* var object = { |
|
* 'cheese': 'crumpets', |
|
* 'stuff': function() { |
|
* return 'nonsense'; |
|
* } |
|
* }; |
|
* |
|
* _.result(object, 'cheese'); |
|
* // => 'crumpets' |
|
* |
|
* _.result(object, 'stuff'); |
|
* // => 'nonsense' |
|
*/ |
|
function result(object, property) { |
|
// based on Backbone's private `getValue` function |
|
// https://github.com/documentcloud/backbone/blob/0.9.2/backbone.js#L1419-1424 |
|
var value = object ? object[property] : null; |
|
return isFunction(value) ? object[property]() : value; |
|
} |
|
|
|
/** |
|
* A micro-templating method that handles arbitrary delimiters, preserves |
|
* whitespace, and correctly escapes quotes within interpolated code. |
|
* |
|
* Note: In the development build `_.template` utilizes sourceURLs for easier |
|
* debugging. See http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-sourceurl |
|
* |
|
* Note: Lo-Dash may be used in Chrome extensions by either creating a `lodash csp` |
|
* build and avoiding `_.template` use, or loading Lo-Dash in a sandboxed page. |
|
* See http://developer.chrome.com/trunk/extensions/sandboxingEval.html |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Utilities |
|
* @param {String} text The template text. |
|
* @param {Obect} data The data object used to populate the text. |
|
* @param {Object} options The options object. |
|
* escape - The "escape" delimiter regexp. |
|
* evaluate - The "evaluate" delimiter regexp. |
|
* interpolate - The "interpolate" delimiter regexp. |
|
* sourceURL - The sourceURL of the template's compiled source. |
|
* variable - The data object variable name. |
|
* |
|
* @returns {Function|String} Returns a compiled function when no `data` object |
|
* is given, else it returns the interpolated text. |
|
* @example |
|
* |
|
* // using a compiled template |
|
* var compiled = _.template('hello <%= name %>'); |
|
* compiled({ 'name': 'moe' }); |
|
* // => 'hello moe' |
|
* |
|
* var list = '<% _.forEach(people, function(name) { %><li><%= name %></li><% }); %>'; |
|
* _.template(list, { 'people': ['moe', 'larry', 'curly'] }); |
|
* // => '<li>moe</li><li>larry</li><li>curly</li>' |
|
* |
|
* // using the "escape" delimiter to escape HTML in data property values |
|
* _.template('<b><%- value %></b>', { 'value': '<script>' }); |
|
* // => '<b><script></b>' |
|
* |
|
* // using the internal `print` function in "evaluate" delimiters |
|
* _.template('<% print("hello " + epithet); %>!', { 'epithet': 'stooge' }); |
|
* // => 'hello stooge!' |
|
* |
|
* // using custom template delimiters |
|
* _.templateSettings = { |
|
* 'interpolate': /\{\{([\s\S]+?)\}\}/g |
|
* }; |
|
* |
|
* _.template('hello {{ name }}!', { 'name': 'mustache' }); |
|
* // => 'hello mustache!' |
|
* |
|
* // using the `sourceURL` option to specify a custom sourceURL for the template |
|
* var compiled = _.template('hello <%= name %>', null, { 'sourceURL': '/basic/greeting.jst' }); |
|
* compiled(data); |
|
* // => find the source of "greeting.jst" under the Sources tab or Resources panel of the web inspector |
|
* |
|
* // using the `variable` option to ensure a with-statement isn't used in the compiled template |
|
* var compiled = _.template('hello <%= data.name %>!', null, { 'variable': 'data' }); |
|
* compiled.source; |
|
* // => function(data) { |
|
* var __t, __p = '', __e = _.escape; |
|
* __p += 'hello ' + ((__t = ( data.name )) == null ? '' : __t) + '!'; |
|
* return __p; |
|
* } |
|
* |
|
* // using the `source` property to inline compiled templates for meaningful |
|
* // line numbers in error messages and a stack trace |
|
* fs.writeFileSync(path.join(cwd, 'jst.js'), '\ |
|
* var JST = {\ |
|
* "main": ' + _.template(mainText).source + '\ |
|
* };\ |
|
* '); |
|
*/ |
|
function template(text, data, options) { |
|
// based on John Resig's `tmpl` implementation |
|
// http://ejohn.org/blog/javascript-micro-templating/ |
|
// and Laura Doktorova's doT.js |
|
// https://github.com/olado/doT |
|
text || (text = ''); |
|
options || (options = {}); |
|
|
|
var isEvaluating, |
|
result, |
|
index = 0, |
|
settings = lodash.templateSettings, |
|
source = "__p += '", |
|
variable = options.variable || settings.variable, |
|
hasVariable = variable; |
|
|
|
// compile regexp to match each delimiter |
|
var reDelimiters = RegExp( |
|
(options.escape || settings.escape || reNoMatch).source + '|' + |
|
(options.interpolate || settings.interpolate || reNoMatch).source + '|' + |
|
(options.evaluate || settings.evaluate || reNoMatch).source + '|$' |
|
, 'g'); |
|
|
|
text.replace(reDelimiters, function(match, escapeValue, interpolateValue, evaluateValue, offset) { |
|
// escape characters that cannot be included in string literals |
|
source += text.slice(index, offset).replace(reUnescapedString, escapeStringChar); |
|
|
|
// replace delimiters with snippets |
|
source += |
|
escapeValue ? "' +\n__e(" + escapeValue + ") +\n'" : |
|
evaluateValue ? "';\n" + evaluateValue + ";\n__p += '" : |
|
interpolateValue ? "' +\n((__t = (" + interpolateValue + ")) == null ? '' : __t) +\n'" : ''; |
|
|
|
isEvaluating || (isEvaluating = evaluateValue || reComplexDelimiter.test(escapeValue || interpolateValue)); |
|
index = offset + match.length; |
|
}); |
|
|
|
source += "';\n"; |
|
|
|
// if `variable` is not specified and the template contains "evaluate" |
|
// delimiters, wrap a with-statement around the generated code to add the |
|
// data object to the top of the scope chain |
|
if (!hasVariable) { |
|
variable = 'obj'; |
|
if (isEvaluating) { |
|
source = 'with (' + variable + ') {\n' + source + '\n}\n'; |
|
} |
|
else { |
|
// avoid a with-statement by prepending data object references to property names |
|
var reDoubleVariable = RegExp('(\\(\\s*)' + variable + '\\.' + variable + '\\b', 'g'); |
|
source = source |
|
.replace(reInsertVariable, '$&' + variable + '.') |
|
.replace(reDoubleVariable, '$1__d'); |
|
} |
|
} |
|
|
|
// cleanup code by stripping empty strings |
|
source = (isEvaluating ? source.replace(reEmptyStringLeading, '') : source) |
|
.replace(reEmptyStringMiddle, '$1') |
|
.replace(reEmptyStringTrailing, '$1;'); |
|
|
|
// frame code as the function body |
|
source = 'function(' + variable + ') {\n' + |
|
(hasVariable ? '' : variable + ' || (' + variable + ' = {});\n') + |
|
'var __t, __p = \'\', __e = _.escape' + |
|
(isEvaluating |
|
? ', __j = Array.prototype.join;\n' + |
|
'function print() { __p += __j.call(arguments, \'\') }\n' |
|
: (hasVariable ? '' : ', __d = ' + variable + '.' + variable + ' || ' + variable) + ';\n' |
|
) + |
|
source + |
|
'return __p\n}'; |
|
|
|
// use a sourceURL for easier debugging |
|
// http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-sourceurl |
|
var sourceURL = useSourceURL |
|
? '\n//@ sourceURL=' + (options.sourceURL || '/lodash/template/source[' + (templateCounter++) + ']') |
|
: ''; |
|
|
|
try { |
|
result = Function('_', 'return ' + source + sourceURL)(lodash); |
|
} catch(e) { |
|
e.source = source; |
|
throw e; |
|
} |
|
|
|
if (data) { |
|
return result(data); |
|
} |
|
// provide the compiled function's source via its `toString` method, in |
|
// supported environments, or the `source` property as a convenience for |
|
// inlining compiled templates during the build process |
|
result.source = source; |
|
return result; |
|
} |
|
|
|
/** |
|
* Executes the `callback` function `n` times, returning an array of the results |
|
* of each `callback` execution. The `callback` is bound to `thisArg` and invoked |
|
* with one argument; (index). |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Utilities |
|
* @param {Number} n The number of times to execute the callback. |
|
* @param {Function} callback The function called per iteration. |
|
* @param {Mixed} [thisArg] The `this` binding of `callback`. |
|
* @returns {Array} Returns a new array of the results of each `callback` execution. |
|
* @example |
|
* |
|
* var diceRolls = _.times(3, _.partial(_.random, 1, 6)); |
|
* // => [3, 6, 4] |
|
* |
|
* _.times(3, function(n) { mage.castSpell(n); }); |
|
* // => calls `mage.castSpell(n)` three times, passing `n` of `0`, `1`, and `2` respectively |
|
* |
|
* _.times(3, function(n) { this.cast(n); }, mage); |
|
* // => also calls `mage.castSpell(n)` three times |
|
*/ |
|
function times(n, callback, thisArg) { |
|
n = +n || 0; |
|
var index = -1, |
|
result = Array(n); |
|
|
|
while (++index < n) { |
|
result[index] = callback.call(thisArg, index); |
|
} |
|
return result; |
|
} |
|
|
|
/** |
|
* The opposite of `_.escape`, this method converts the HTML entities |
|
* `&`, `<`, `>`, `"`, and `'` in `string` to their |
|
* corresponding characters. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Utilities |
|
* @param {String} string The string to unescape. |
|
* @returns {String} Returns the unescaped string. |
|
* @example |
|
* |
|
* _.unescape('Moe, Larry & Curly'); |
|
* // => "Moe, Larry & Curly" |
|
*/ |
|
function unescape(string) { |
|
return string == null ? '' : (string + '').replace(reEscapedHtml, unescapeHtmlChar); |
|
} |
|
|
|
/** |
|
* Generates a unique id. If `prefix` is passed, the id will be appended to it. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Utilities |
|
* @param {String} [prefix] The value to prefix the id with. |
|
* @returns {Number|String} Returns a numeric id if no prefix is passed, else |
|
* a string id may be returned. |
|
* @example |
|
* |
|
* _.uniqueId('contact_'); |
|
* // => 'contact_104' |
|
*/ |
|
function uniqueId(prefix) { |
|
var id = idCounter++; |
|
return prefix ? prefix + id : id; |
|
} |
|
|
|
/*--------------------------------------------------------------------------*/ |
|
|
|
/** |
|
* Wraps the value in a `lodash` wrapper object. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Chaining |
|
* @param {Mixed} value The value to wrap. |
|
* @returns {Object} Returns the wrapper object. |
|
* @example |
|
* |
|
* var stooges = [ |
|
* { 'name': 'moe', 'age': 40 }, |
|
* { 'name': 'larry', 'age': 50 }, |
|
* { 'name': 'curly', 'age': 60 } |
|
* ]; |
|
* |
|
* var youngest = _.chain(stooges) |
|
* .sortBy(function(stooge) { return stooge.age; }) |
|
* .map(function(stooge) { return stooge.name + ' is ' + stooge.age; }) |
|
* .first() |
|
* .value(); |
|
* // => 'moe is 40' |
|
*/ |
|
function chain(value) { |
|
value = new lodash(value); |
|
value.__chain__ = true; |
|
return value; |
|
} |
|
|
|
/** |
|
* Invokes `interceptor` with the `value` as the first argument, and then |
|
* returns `value`. The purpose of this method is to "tap into" a method chain, |
|
* in order to perform operations on intermediate results within the chain. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @category Chaining |
|
* @param {Mixed} value The value to pass to `interceptor`. |
|
* @param {Function} interceptor The function to invoke. |
|
* @returns {Mixed} Returns `value`. |
|
* @example |
|
* |
|
* _.chain([1, 2, 3, 200]) |
|
* .filter(function(num) { return num % 2 == 0; }) |
|
* .tap(alert) |
|
* .map(function(num) { return num * num }) |
|
* .value(); |
|
* // => // [2, 200] (alerted) |
|
* // => [4, 40000] |
|
*/ |
|
function tap(value, interceptor) { |
|
interceptor(value); |
|
return value; |
|
} |
|
|
|
/** |
|
* Enables method chaining on the wrapper object. |
|
* |
|
* @name chain |
|
* @deprecated |
|
* @memberOf _ |
|
* @category Chaining |
|
* @returns {Mixed} Returns the wrapper object. |
|
* @example |
|
* |
|
* _([1, 2, 3]).value(); |
|
* // => [1, 2, 3] |
|
*/ |
|
function wrapperChain() { |
|
this.__chain__ = true; |
|
return this; |
|
} |
|
|
|
/** |
|
* Extracts the wrapped value. |
|
* |
|
* @name value |
|
* @memberOf _ |
|
* @category Chaining |
|
* @returns {Mixed} Returns the wrapped value. |
|
* @example |
|
* |
|
* _([1, 2, 3]).value(); |
|
* // => [1, 2, 3] |
|
*/ |
|
function wrapperValue() { |
|
return this.__wrapped__; |
|
} |
|
|
|
/*--------------------------------------------------------------------------*/ |
|
|
|
/** |
|
* The semantic version number. |
|
* |
|
* @static |
|
* @memberOf _ |
|
* @type String |
|
*/ |
|
lodash.VERSION = '0.9.0'; |
|
|
|
// assign static methods |
|
lodash.after = after; |
|
lodash.bind = bind; |
|
lodash.bindAll = bindAll; |
|
lodash.chain = chain; |
|
lodash.clone = clone; |
|
lodash.compact = compact; |
|
lodash.compose = compose; |
|
lodash.contains = contains; |
|
lodash.countBy = countBy; |
|
lodash.debounce = debounce; |
|
lodash.defaults = defaults; |
|
lodash.defer = defer; |
|
lodash.delay = delay; |
|
lodash.difference = difference; |
|
lodash.escape = escape; |
|
lodash.every = every; |
|
lodash.extend = extend; |
|
lodash.filter = filter; |
|
lodash.find = find; |
|
lodash.first = first; |
|
lodash.flatten = flatten; |
|
lodash.forEach = forEach; |
|
lodash.forIn = forIn; |
|
lodash.forOwn = forOwn; |
|
lodash.functions = functions; |
|
lodash.groupBy = groupBy; |
|
lodash.has = has; |
|
lodash.identity = identity; |
|
lodash.indexOf = indexOf; |
|
lodash.initial = initial; |
|
lodash.intersection = intersection; |
|
lodash.invert = invert; |
|
lodash.invoke = invoke; |
|
lodash.isArguments = isArguments; |
|
lodash.isArray = isArray; |
|
lodash.isBoolean = isBoolean; |
|
lodash.isDate = isDate; |
|
lodash.isElement = isElement; |
|
lodash.isEmpty = isEmpty; |
|
lodash.isEqual = isEqual; |
|
lodash.isFinite = isFinite; |
|
lodash.isFunction = isFunction; |
|
lodash.isNaN = isNaN; |
|
lodash.isNull = isNull; |
|
lodash.isNumber = isNumber; |
|
lodash.isObject = isObject; |
|
lodash.isPlainObject = isPlainObject; |
|
lodash.isRegExp = isRegExp; |
|
lodash.isString = isString; |
|
lodash.isUndefined = isUndefined; |
|
lodash.keys = keys; |
|
lodash.last = last; |
|
lodash.lastIndexOf = lastIndexOf; |
|
lodash.lateBind = lateBind; |
|
lodash.map = map; |
|
lodash.max = max; |
|
lodash.memoize = memoize; |
|
lodash.merge = merge; |
|
lodash.min = min; |
|
lodash.mixin = mixin; |
|
lodash.noConflict = noConflict; |
|
lodash.object = object; |
|
lodash.omit = omit; |
|
lodash.once = once; |
|
lodash.pairs = pairs; |
|
lodash.partial = partial; |
|
lodash.pick = pick; |
|
lodash.pluck = pluck; |
|
lodash.random = random; |
|
lodash.range = range; |
|
lodash.reduce = reduce; |
|
lodash.reduceRight = reduceRight; |
|
lodash.reject = reject; |
|
lodash.rest = rest; |
|
lodash.result = result; |
|
lodash.shuffle = shuffle; |
|
lodash.size = size; |
|
lodash.some = some; |
|
lodash.sortBy = sortBy; |
|
lodash.sortedIndex = sortedIndex; |
|
lodash.tap = tap; |
|
lodash.template = template; |
|
lodash.throttle = throttle; |
|
lodash.times = times; |
|
lodash.toArray = toArray; |
|
lodash.unescape = unescape; |
|
lodash.union = union; |
|
lodash.uniq = uniq; |
|
lodash.uniqueId = uniqueId; |
|
lodash.values = values; |
|
lodash.where = where; |
|
lodash.without = without; |
|
lodash.wrap = wrap; |
|
lodash.zip = zip; |
|
|
|
// assign aliases |
|
lodash.all = every; |
|
lodash.any = some; |
|
lodash.collect = map; |
|
lodash.detect = find; |
|
lodash.drop = rest; |
|
lodash.each = forEach; |
|
lodash.foldl = reduce; |
|
lodash.foldr = reduceRight; |
|
lodash.head = first; |
|
lodash.include = contains; |
|
lodash.inject = reduce; |
|
lodash.methods = functions; |
|
lodash.select = filter; |
|
lodash.tail = rest; |
|
lodash.take = first; |
|
lodash.unique = uniq; |
|
|
|
// add pseudo private property to be used and removed during the build process |
|
lodash._iteratorTemplate = iteratorTemplate; |
|
|
|
/*--------------------------------------------------------------------------*/ |
|
|
|
// add all static functions to `lodash.prototype` |
|
mixin(lodash); |
|
|
|
// add `lodash.prototype.chain` after calling `mixin()` to avoid overwriting |
|
// it with the wrapped `lodash.chain` |
|
lodash.prototype.chain = wrapperChain; |
|
lodash.prototype.value = wrapperValue; |
|
|
|
// add all mutator Array functions to the wrapper. |
|
forEach(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(methodName) { |
|
var func = arrayRef[methodName]; |
|
|
|
lodash.prototype[methodName] = function() { |
|
var value = this.__wrapped__; |
|
func.apply(value, arguments); |
|
|
|
// avoid array-like object bugs with `Array#shift` and `Array#splice` in |
|
// Firefox < 10 and IE < 9 |
|
if (hasObjectSpliceBug && value.length === 0) { |
|
delete value[0]; |
|
} |
|
if (this.__chain__) { |
|
value = new lodash(value); |
|
value.__chain__ = true; |
|
} |
|
return value; |
|
}; |
|
}); |
|
|
|
// add all accessor Array functions to the wrapper. |
|
forEach(['concat', 'join', 'slice'], function(methodName) { |
|
var func = arrayRef[methodName]; |
|
|
|
lodash.prototype[methodName] = function() { |
|
var value = this.__wrapped__, |
|
result = func.apply(value, arguments); |
|
|
|
if (this.__chain__) { |
|
result = new lodash(result); |
|
result.__chain__ = true; |
|
} |
|
return result; |
|
}; |
|
}); |
|
|
|
/*--------------------------------------------------------------------------*/ |
|
|
|
// expose Lo-Dash |
|
// some AMD build optimizers, like r.js, check for specific condition patterns like the following: |
|
if (typeof define == 'function' && typeof define.amd == 'object' && define.amd) { |
|
// Expose Lo-Dash to the global object even when an AMD loader is present in |
|
// case Lo-Dash was injected by a third-party script and not intended to be |
|
// loaded as a module. The global assignment can be reverted in the Lo-Dash |
|
// module via its `noConflict()` method. |
|
window._ = lodash; |
|
|
|
// define as an anonymous module so, through path mapping, it can be |
|
// referenced as the "underscore" module |
|
define(function() { |
|
return lodash; |
|
}); |
|
} |
|
// check for `exports` after `define` in case a build optimizer adds an `exports` object |
|
else if (freeExports) { |
|
// in Node.js or RingoJS v0.8.0+ |
|
if (typeof module == 'object' && module && module.exports == freeExports) { |
|
(module.exports = lodash)._ = lodash; |
|
} |
|
// in Narwhal or RingoJS v0.7.0- |
|
else { |
|
freeExports._ = lodash; |
|
} |
|
} |
|
else { |
|
// in a browser or Rhino |
|
window._ = lodash; |
|
} |
|
}(this)); |
|
// Math.js |
|
|
|
(function() { |
|
|
|
var math = this.math = {}; |
|
|
|
// Arithmetic mean |
|
// math.mean([1,2,3]) |
|
// => 2 |
|
math.mean = math.ave = math.average = function(obj, key) { |
|
return math.sum(obj, key) / _(obj).size(); |
|
}; |
|
|
|
// math.median([1,2,3,4]) |
|
// => 2.5 |
|
// TODO {}, [{}] |
|
math.median = function(arr) { |
|
var middle = (arr.length + 1) /2; |
|
var sorted = math.sort(arr); |
|
return (sorted.length % 2) ? sorted[middle - 1] : (sorted[middle - 1.5] + sorted[middle - 0.5]) / 2; |
|
}; |
|
|
|
// Power, exponent |
|
// math.pow(2,3) |
|
// => 8 |
|
math.pow = function(x, n) { |
|
if (_.isNumber(x)) |
|
return Math.pow(x, n); |
|
if (_.isArray(x)) |
|
return _.map(x, function(i) { return _.pow(i,n); }); |
|
}; |
|
|
|
// Scale to max value |
|
// math.scale(1,[2,5,10]) |
|
// => [ 0.2, 0.5, 1] |
|
math.scale = function(arr, max) { |
|
var max = max || 1; |
|
var max0 = _.max(arr); |
|
return _.map(arr, function(i) { return i * (max/max0); }); |
|
}; |
|
|
|
// Slope between two points |
|
// math.slope([0,0],[1,2]) |
|
// => 2 |
|
math.slope = function(x, y) { |
|
return (y[1] - x[1]) / (y[0]-x[0]); |
|
}; |
|
|
|
// Numeric sort |
|
// math.sort([3,1,2]) |
|
// => [1,2,3] |
|
math.sort = function(arr) { |
|
return arr.sort(function(a, b){ |
|
return a - b; |
|
}); |
|
}; |
|
|
|
// math.stdDeviation([1,2,3]) |
|
// => 0.816496580927726 |
|
math.stdDeviation = math.sigma = function(arr) { |
|
return Math.sqrt(_(arr).variance()); |
|
}; |
|
|
|
// Sum of array |
|
// math.sum([1,2,3]) |
|
// => 6 |
|
// math.sum([{b: 4},{b: 5},{b: 6}], 'b') |
|
// => 15 |
|
math.sum = function(obj, key) { |
|
if (_.isArray(obj) && typeof obj[0] === 'number') { |
|
var arr = obj; |
|
} else { |
|
var key = key || 'value'; |
|
var arr = _(obj).pluck(key); |
|
} |
|
var val = 0; |
|
for (var i=0, len = arr.length; i<len; i++) |
|
val += arr[i]; |
|
return val; |
|
}; |
|
|
|
// math.transpose(([1,2,3], [4,5,6], [7,8,9]]) |
|
// => [[1,4,7], [2,5,8], [3,6,9]] |
|
math.transpose = function(arr) { |
|
var trans = []; |
|
_(arr).each(function(row, y){ |
|
_(row).each(function(col, x){ |
|
if (!trans[x]) trans[x] = []; |
|
trans[x][y] = col; |
|
}); |
|
}); |
|
return trans; |
|
}; |
|
|
|
// math.variance([1,2,3]) |
|
// => 2/3 |
|
math.variance = function(arr) { |
|
var mean = _(arr).mean(); |
|
return _(arr).chain().map(function(x) { return _(x-mean).pow(2); }).mean().value(); |
|
}; |
|
|
|
_.mixin(math); |
|
|
|
})(); |
|
(function(root){ |
|
|
|
// Let's borrow a couple of things from Underscore that we'll need |
|
|
|
// _.each |
|
var breaker = {}, |
|
AP = Array.prototype, |
|
OP = Object.prototype, |
|
|
|
hasOwn = OP.hasOwnProperty, |
|
toString = OP.toString, |
|
forEach = AP.forEach, |
|
indexOf = AP.indexOf, |
|
slice = AP.slice; |
|
|
|
var _each = function( obj, iterator, context ) { |
|
var key, i, l; |
|
|
|
if ( !obj ) { |
|
return; |
|
} |
|
if ( forEach && obj.forEach === forEach ) { |
|
obj.forEach( iterator, context ); |
|
} else if ( obj.length === +obj.length ) { |
|
for ( i = 0, l = obj.length; i < l; i++ ) { |
|
if ( i in obj && iterator.call( context, obj[i], i, obj ) === breaker ) { |
|
return; |
|
} |
|
} |
|
} else { |
|
for ( key in obj ) { |
|
if ( hasOwn.call( obj, key ) ) { |
|
if ( iterator.call( context, obj[key], key, obj) === breaker ) { |
|
return; |
|
} |
|
} |
|
} |
|
} |
|
}; |
|
|
|
// _.isFunction |
|
var _isFunction = function( obj ) { |
|
return !!(obj && obj.constructor && obj.call && obj.apply); |
|
}; |
|
|
|
// _.extend |
|
var _extend = function( obj ) { |
|
|
|
_each( slice.call( arguments, 1), function( source ) { |
|
var prop; |
|
|
|
for ( prop in source ) { |
|
if ( source[prop] !== void 0 ) { |
|
obj[ prop ] = source[ prop ]; |
|
} |
|
} |
|
}); |
|
return obj; |
|
}; |
|
|
|
// $.inArray |
|
var _inArray = function( elem, arr, i ) { |
|
var len; |
|
|
|
if ( arr ) { |
|
if ( indexOf ) { |
|
return indexOf.call( arr, elem, i ); |
|
} |
|
|
|
len = arr.length; |
|
i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; |
|
|
|
for ( ; i < len; i++ ) { |
|
// Skip accessing in sparse arrays |
|
if ( i in arr && arr[ i ] === elem ) { |
|
return i; |
|
} |
|
} |
|
} |
|
|
|
return -1; |
|
}; |
|
|
|
// And some jQuery specific helpers |
|
|
|
var class2type = {}; |
|
|
|
// Populate the class2type map |
|
_each("Boolean Number String Function Array Date RegExp Object".split(" "), function(name, i) { |
|
class2type[ "[object " + name + "]" ] = name.toLowerCase(); |
|
}); |
|
|
|
var _type = function( obj ) { |
|
return obj == null ? |
|
String( obj ) : |
|
class2type[ toString.call(obj) ] || "object"; |
|
}; |
|
|
|
// Now start the jQuery-cum-Underscore implementation. Some very |
|
// minor changes to the jQuery source to get this working. |
|
|
|
// Internal Deferred namespace |
|
var _d = {}; |
|
// String to Object options format cache |
|
var optionsCache = {}; |
|
|
|
// Convert String-formatted options into Object-formatted ones and store in cache |
|
function createOptions( options ) { |
|
var object = optionsCache[ options ] = {}; |
|
_each( options.split( /\s+/ ), function( flag ) { |
|
object[ flag ] = true; |
|
}); |
|
return object; |
|
} |
|
|
|
_d.Callbacks = function( options ) { |
|
|
|
// Convert options from String-formatted to Object-formatted if needed |
|
// (we check in cache first) |
|
options = typeof options === "string" ? |
|
( optionsCache[ options ] || createOptions( options ) ) : |
|
_extend( {}, options ); |
|
|
|
var // Last fire value (for non-forgettable lists) |
|
memory, |
|
// Flag to know if list was already fired |
|
fired, |
|
// Flag to know if list is currently firing |
|
firing, |
|
// First callback to fire (used internally by add and fireWith) |
|
firingStart, |
|
// End of the loop when firing |
|
firingLength, |
|
// Index of currently firing callback (modified by remove if needed) |
|
firingIndex, |
|
// Actual callback list |
|
list = [], |
|
// Stack of fire calls for repeatable lists |
|
stack = !options.once && [], |
|
// Fire callbacks |
|
fire = function( data ) { |
|
memory = options.memory && data; |
|
fired = true; |
|
firingIndex = firingStart || 0; |
|
firingStart = 0; |
|
firingLength = list.length; |
|
firing = true; |
|
for ( ; list && firingIndex < firingLength; firingIndex++ ) { |
|
if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { |
|
memory = false; // To prevent further calls using add |
|
break; |
|
} |
|
} |
|
firing = false; |
|
if ( list ) { |
|
if ( stack ) { |
|
if ( stack.length ) { |
|
fire( stack.shift() ); |
|
} |
|
} else if ( memory ) { |
|
list = []; |
|
} else { |
|
self.disable(); |
|
} |
|
} |
|
}, |
|
// Actual Callbacks object |
|
self = { |
|
// Add a callback or a collection of callbacks to the list |
|
add: function() { |
|
if ( list ) { |
|
// First, we save the current length |
|
var start = list.length; |
|
(function add( args ) { |
|
_each( args, function( arg ) { |
|
var type = _type( arg ); |
|
if ( type === "function" && ( !options.unique || !self.has( arg ) ) ) { |
|
list.push( arg ); |
|
} else if ( arg && arg.length && type !== "string" ) { |
|
// Inspect recursively |
|
add( arg ); |
|
} |
|
}); |
|
})( arguments ); |
|
// Do we need to add the callbacks to the |
|
// current firing batch? |
|
if ( firing ) { |
|
firingLength = list.length; |
|
// With memory, if we're not firing then |
|
// we should call right away |
|
} else if ( memory ) { |
|
firingStart = start; |
|
fire( memory ); |
|
} |
|
} |
|
return this; |
|
}, |
|
// Remove a callback from the list |
|
remove: function() { |
|
if ( list ) { |
|
_each( arguments, function( arg ) { |
|
var index; |
|
while( ( index = _inArray( arg, list, index ) ) > -1 ) { |
|
list.splice( index, 1 ); |
|
// Handle firing indexes |
|
if ( firing ) { |
|
if ( index <= firingLength ) { |
|
firingLength--; |
|
} |
|
if ( index <= firingIndex ) { |
|
firingIndex--; |
|
} |
|
} |
|
} |
|
}); |
|
} |
|
return this; |
|
}, |
|
// Control if a given callback is in the list |
|
has: function( fn ) { |
|
return _inArray( fn, list ) > -1; |
|
}, |
|
// Remove all callbacks from the list |
|
empty: function() { |
|
list = []; |
|
return this; |
|
}, |
|
// Have the list do nothing anymore |
|
disable: function() { |
|
list = stack = memory = undefined; |
|
return this; |
|
}, |
|
// Is it disabled? |
|
disabled: function() { |
|
return !list; |
|
}, |
|
// Lock the list in its current state |
|
lock: function() { |
|
stack = undefined; |
|
if ( !memory ) { |
|
self.disable(); |
|
} |
|
return this; |
|
}, |
|
// Is it locked? |
|
locked: function() { |
|
return !stack; |
|
}, |
|
// Call all callbacks with the given context and arguments |
|
fireWith: function( context, args ) { |
|
args = args || []; |
|
args = [ context, args.slice ? args.slice() : args ]; |
|
if ( list && ( !fired || stack ) ) { |
|
if ( firing ) { |
|
stack.push( args ); |
|
} else { |
|
fire( args ); |
|
} |
|
} |
|
return this; |
|
}, |
|
// Call all the callbacks with the given arguments |
|
fire: function() { |
|
self.fireWith( this, arguments ); |
|
return this; |
|
}, |
|
// To know if the callbacks have already been called at least once |
|
fired: function() { |
|
return !!fired; |
|
} |
|
}; |
|
|
|
return self; |
|
}; |
|
|
|
_d.Deferred = function( func ) { |
|
|
|
var tuples = [ |
|
// action, add listener, listener list, final state |
|
[ "resolve", "done", _d.Callbacks("once memory"), "resolved" ], |
|
[ "reject", "fail", _d.Callbacks("once memory"), "rejected" ], |
|
[ "notify", "progress", _d.Callbacks("memory") ] |
|
], |
|
state = "pending", |
|
promise = { |
|
state: function() { |
|
return state; |
|
}, |
|
always: function() { |
|
deferred.done( arguments ).fail( arguments ); |
|
return this; |
|
}, |
|
then: function( /* fnDone, fnFail, fnProgress */ ) { |
|
var fns = arguments; |
|
return _d.Deferred(function( newDefer ) { |
|
_each( tuples, function( tuple, i ) { |
|
var action = tuple[ 0 ], |
|
fn = fns[ i ]; |
|
// deferred[ done | fail | progress ] for forwarding actions to newDefer |
|
deferred[ tuple[1] ]( _isFunction( fn ) ? |
|
function() { |
|
var returned = fn.apply( this, arguments ); |
|
if ( returned && _isFunction( returned.promise ) ) { |
|
returned.promise() |
|
.done( newDefer.resolve ) |
|
.fail( newDefer.reject ) |
|
.progress( newDefer.notify ); |
|
} else { |
|
newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] ); |
|
} |
|
} : |
|
newDefer[ action ] |
|
); |
|
}); |
|
fns = null; |
|
}).promise(); |
|
}, |
|
// Get a promise for this deferred |
|
// If obj is provided, the promise aspect is added to the object |
|
promise: function( obj ) { |
|
return typeof obj === "object" ? _extend( obj, promise ) : promise; |
|
} |
|
}, |
|
deferred = {}; |
|
|
|
// Keep pipe for back-compat |
|
promise.pipe = promise.then; |
|
|
|
// Add list-specific methods |
|
_each( tuples, function( tuple, i ) { |
|
var list = tuple[ 2 ], |
|
stateString = tuple[ 3 ]; |
|
|
|
// promise[ done | fail | progress ] = list.add |
|
promise[ tuple[1] ] = list.add; |
|
|
|
// Handle state |
|
if ( stateString ) { |
|
list.add(function() { |
|
// state = [ resolved | rejected ] |
|
state = stateString; |
|
|
|
// [ reject_list | resolve_list ].disable; progress_list.lock |
|
}, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); |
|
} |
|
|
|
// deferred[ resolve | reject | notify ] = list.fire |
|
deferred[ tuple[0] ] = list.fire; |
|
deferred[ tuple[0] + "With" ] = list.fireWith; |
|
}); |
|
|
|
// Make the deferred a promise |
|
promise.promise( deferred ); |
|
|
|
// Call given func if any |
|
if ( func ) { |
|
func.call( deferred, deferred ); |
|
} |
|
|
|
// All done! |
|
return deferred; |
|
}; |
|
|
|
// Deferred helper |
|
_d.when = function( subordinate /* , ..., subordinateN */ ) { |
|
var i = 0, |
|
resolveValues = slice.call( arguments ), |
|
length = resolveValues.length, |
|
|
|
// the count of uncompleted subordinates |
|
remaining = length !== 1 || ( subordinate && _isFunction( subordinate.promise ) ) ? length : 0, |
|
|
|
// the master Deferred. If resolveValues consist of only a single Deferred, just use that. |
|
deferred = remaining === 1 ? subordinate : _d.Deferred(), |
|
|
|
// Update function for both resolve and progress values |
|
updateFunc = function( i, contexts, values ) { |
|
return function( value ) { |
|
contexts[ i ] = this; |
|
values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; |
|
if( values === progressValues ) { |
|
deferred.notifyWith( contexts, values ); |
|
} else if ( !( --remaining ) ) { |
|
deferred.resolveWith( contexts, values ); |
|
} |
|
}; |
|
}, |
|
|
|
progressValues, progressContexts, resolveContexts; |
|
|
|
// add listeners to Deferred subordinates; treat others as resolved |
|
if ( length > 1 ) { |
|
progressValues = new Array( length ); |
|
progressContexts = new Array( length ); |
|
resolveContexts = new Array( length ); |
|
for ( ; i < length; i++ ) { |
|
if ( resolveValues[ i ] && _isFunction( resolveValues[ i ].promise ) ) { |
|
resolveValues[ i ].promise() |
|
.done( updateFunc( i, resolveContexts, resolveValues ) ) |
|
.fail( deferred.reject ) |
|
.progress( updateFunc( i, progressContexts, progressValues ) ); |
|
} else { |
|
--remaining; |
|
} |
|
} |
|
} |
|
|
|
// if we're not waiting on anything, resolve the master |
|
if ( !remaining ) { |
|
deferred.resolveWith( resolveContexts, resolveValues ); |
|
} |
|
|
|
return deferred.promise(); |
|
}; |
|
|
|
// Try exporting as a Common.js Module |
|
if ( typeof module !== "undefined" && module.exports ) { |
|
module.exports = _d; |
|
|
|
// Or mixin to Underscore.js |
|
} else if ( typeof root._ !== "undefined" ) { |
|
root._.mixin(_d); |
|
|
|
// Or assign it to window._ |
|
} else { |
|
root._ = _d; |
|
} |
|
|
|
})(this); |
|
(function(global, _) { |
|
|
|
var Miso = global.Miso = (global.Miso || {}); |
|
|
|
/** |
|
* Miso Events is a small set of methods that can be mixed into any object |
|
* to make it evented. It allows one to then subscribe to specific object events, |
|
* to publish events, unsubscribe and subscribeOnce. |
|
*/ |
|
Miso.Events = { |
|
|
|
/** |
|
* Triggers a specific event and passes any additional arguments |
|
* to the callbacks subscribed to that event. |
|
* Params: |
|
* name - the name of the event to trigger |
|
* .* - any additional arguments to pass to callbacks. |
|
*/ |
|
publish : function(name) { |
|
var args = _.toArray(arguments); |
|
args.shift(); |
|
|
|
if (this._events && this._events[name]) { |
|
_.each(this._events[name], function(subscription) { |
|
subscription.callback.apply(subscription.context || this, args); |
|
}, this); |
|
} |
|
return this; |
|
}, |
|
|
|
/** |
|
* Allows subscribing on an evented object to a specific name. |
|
* Provide a callback to trigger. |
|
* Params: |
|
* name - event to subscribe to |
|
* callback - callback to trigger |
|
* options - optional arguments |
|
* priority - allows rearranging of existing callbacks based on priority |
|
* context - allows attaching diff context to callback |
|
* token - allows callback identification by token. |
|
*/ |
|
subscribe : function(name, callback, options) { |
|
options = options || {}; |
|
this._events = this._events || {}; |
|
this._events[name] = this._events[name] || []; |
|
|
|
var subscription = { |
|
callback : callback, |
|
priority : options.priority || 0, |
|
token : options.token || _.uniqueId('t'), |
|
context : options.context || this |
|
}; |
|
var position; |
|
_.each(this._events[name], function(event, index) { |
|
if (!_.isUndefined(position)) { return; } |
|
if (event.priority <= subscription.priority) { |
|
position = index; |
|
} |
|
}); |
|
|
|
this._events[name].splice(position, 0, subscription); |
|
return subscription.token; |
|
}, |
|
|
|
/** |
|
* Allows subscribing to an event once. When the event is triggered |
|
* this subscription will be removed. |
|
* Params: |
|
* name - name of event |
|
* callback - The callback to trigger |
|
* options - optional arguments |
|
* priority - allows rearranging of existing callbacks based on priority |
|
* context - allows attaching diff context to callback |
|
* token - allows callback identification by token. |
|
*/ |
|
subscribeOnce : function(name, callback, options) { |
|
this._events = this._events || {}; |
|
options = options || {}; |
|
var self = this; |
|
|
|
if (typeof options.token === "undefined") { |
|
options.token = _.uniqueId('t'); |
|
} |
|
|
|
return this.subscribe(name, function() { |
|
self.unsubscribe(name, { token : options.token }); |
|
callback.apply(this, arguments); |
|
}, options); |
|
}, |
|
|
|
/** |
|
* Allows unsubscribing from a specific event |
|
* Params: |
|
* name - event to unsubscribe from |
|
* identifier - callback to remove OR token. |
|
*/ |
|
unsubscribe : function(name, identifier) { |
|
|
|
if (_.isUndefined(this._events[name])) { return this; } |
|
|
|
if (_.isFunction(identifier)) { |
|
this._events[name] = _.reject(this._events[name], function(b) { |
|
return b.callback === identifier; |
|
}); |
|
|
|
} else if ( _.isString(identifier)) { |
|
this._events[name] = _.reject(this._events[name], function(b) { |
|
return b.token === identifier; |
|
}); |
|
|
|
} else { |
|
this._events[name] = []; |
|
} |
|
return this; |
|
} |
|
|
|
}; |
|
|
|
}(this, _)); |
|
|
|
/** |
|
* Miso.Dataset - v0.4.1 - 3/13/2013 |
|
* http://github.com/misoproject/dataset |
|
* Copyright (c) 2013 Alex Graul, Irene Ros; |
|
* Dual Licensed: MIT, GPL |
|
* https://github.com/misoproject/dataset/blob/master/LICENSE-MIT |
|
* https://github.com/misoproject/dataset/blob/master/LICENSE-GPL |
|
*/ |
|
(function(global) { |
|
|
|
/** |
|
* Instantiates a new dataset. |
|
* Parameters: |
|
* options - optional parameters. |
|
* data : "Object - an actual javascript object that already contains the data", |
|
* url : "String - url to fetch data from", |
|
* sync : Set to true to be able to bind to dataset changes. False by default. |
|
* jsonp : "boolean - true if this is a jsonp request", |
|
* delimiter : "String - a delimiter string that is used in a tabular datafile", |
|
* strict : "Whether to expect the json in our format or whether to interpret as raw array of objects, default false", |
|
* extract : "function to apply to JSON before internal interpretation, optional" |
|
* ready : the callback function to act on once the data is fetched. Isn't reuired for local imports |
|
* but is required for remote url fetching. |
|
* columns: A way to manually override column type detection. Expects an array of |
|
* objects of the following structure: |
|
* { name : 'columnname', type: 'columntype', |
|
* ... (additional params required for type here.) } |
|
* comparator : function (optional) - takes two rows and returns 1, 0, or -1 if row1 is |
|
* before, equal or after row2. |
|
* deferred : by default we use underscore.deferred, but if you want to pass your own (like jquery's) just |
|
* pass it here. |
|
* importer : The classname of any importer (passes through auto detection based on parameters. |
|
* For example: <code>Miso.Importers.Polling</code>. |
|
* parser : The classname of any parser (passes through auto detection based on parameters. |
|
* For example: <code>Miso.Parsers.Delimited</code>. |
|
* resetOnFetch : set to true if any subsequent fetches after first one should overwrite the |
|
* current data. |
|
* uniqueAgainst : Set to a column name to check for duplication on subsequent fetches. |
|
* interval : Polling interval. Set to any value in milliseconds to enable polling on a url. |
|
} |
|
*/ |
|
global.Miso = global.Miso || {}; |
|
global.Miso.Dataset = function(options) { |
|
|
|
options = options || {}; |
|
|
|
this.length = 0; |
|
|
|
this._columns = []; |
|
this._columnPositionByName = {}; |
|
this._computedColumns = []; |
|
|
|
this._initialize(options); |
|
}; |
|
}(this)); |
|
|
|
(function(global, _) { |
|
|
|
var Miso = global.Miso || (global.Miso = {}); |
|
var Dataset = global.Miso.Dataset; |
|
|
|
/** |
|
* A single column in a dataset |
|
* Parameters: |
|
* options |
|
* name |
|
* type (from Miso.types) |
|
* data (optional) |
|
* before (a pre coercion formatter) |
|
* format (for time type.) |
|
* any additional arguments here.. |
|
* Returns: |
|
* new Miso.Column |
|
*/ |
|
Dataset.Column = function(options) { |
|
_.extend(this, options); |
|
this._id = options.id || _.uniqueId(); |
|
this.data = options.data || []; |
|
return this; |
|
}; |
|
|
|
_.extend(Dataset.Column.prototype, { |
|
|
|
/** |
|
* Converts any value to this column's type for a given position |
|
* in some source array. |
|
* Parameters: |
|
* value |
|
* Returns: |
|
* number |
|
*/ |
|
toNumeric : function(value) { |
|
return Dataset.types[this.type].numeric(value); |
|
}, |
|
|
|
/** |
|
* Returns the numeric representation of a datum at any index in this |
|
* column. |
|
* Parameters: |
|
* index - position in data array |
|
* Returns |
|
* number |
|
*/ |
|
numericAt : function(index) { |
|
return this.toNumeric(this.data[index]); |
|
}, |
|
|
|
/** |
|
* Coerces the entire column's data to the column type. |
|
*/ |
|
coerce : function() { |
|
this.data = _.map(this.data, function(datum) { |
|
return Dataset.types[this.type].coerce(datum, this); |
|
}, this); |
|
}, |
|
|
|
/** |
|
* If this is a computed column, it calculates the value |
|
* for this column and adds it to the data. |
|
* Parameters: |
|
* row - the row from which column is computed. |
|
* i - Optional. the index at which this value will get added. |
|
* Returns |
|
* val - the computed value |
|
*/ |
|
compute : function(row, i) { |
|
if (this.func) { |
|
var val = this.func(row); |
|
if (typeof i !== "undefined") { |
|
this.data[i] = val; |
|
} else { |
|
this.data.push(val); |
|
} |
|
|
|
return val; |
|
} |
|
}, |
|
|
|
/** |
|
* returns true if this is a computed column. False otherwise. |
|
*/ |
|
isComputed : function() { |
|
return !_.isUndefined(this.func); |
|
}, |
|
|
|
_sum : function() { |
|
return _.sum(this.data); |
|
}, |
|
|
|
_mean : function() { |
|
var m = 0; |
|
for (var j = 0; j < this.data.length; j++) { |
|
m += this.numericAt(j); |
|
} |
|
m /= this.data.length; |
|
return Dataset.types[this.type].coerce(m, this); |
|
}, |
|
|
|
_median : function() { |
|
return Dataset.types[this.type].coerce(_.median(this.data), this); |
|
}, |
|
|
|
_max : function() { |
|
var max = -Infinity; |
|
for (var j = 0; j < this.data.length; j++) { |
|
if (this.data[j] !== null) { |
|
if (Dataset.types[this.type].compare(this.data[j], max) > 0) { |
|
max = this.numericAt(j); |
|
} |
|
} |
|
} |
|
|
|
return Dataset.types[this.type].coerce(max, this); |
|
}, |
|
|
|
_min : function() { |
|
var min = Infinity; |
|
for (var j = 0; j < this.data.length; j++) { |
|
if (this.data[j] !== null) { |
|
if (Dataset.types[this.type].compare(this.data[j], min) < 0) { |
|
min = this.numericAt(j); |
|
} |
|
} |
|
} |
|
return Dataset.types[this.type].coerce(min, this); |
|
} |
|
}); |
|
|
|
/** |
|
* Creates a new view. |
|
* Parameters |
|
* options - initialization parameters: |
|
* parent : parent dataset |
|
* filter : filter specification TODO: document better |
|
* columns : column name or multiple names |
|
* rows : rowId or function |
|
* Returns |
|
* new Miso.Dataview. |
|
*/ |
|
Dataset.DataView = function(options) { |
|
if (typeof options !== "undefined") { |
|
options = options || (options = {}); |
|
|
|
if (_.isUndefined(options.parent)) { |
|
throw new Error("A view must have a parent specified."); |
|
} |
|
this.parent = options.parent; |
|
this._initialize(options); |
|
} |
|
}; |
|
|
|
_.extend(Dataset.DataView.prototype, { |
|
|
|
_initialize: function(options) { |
|
|
|
// is this a syncable dataset? if so, pull |
|
// required methoMiso and mark this as a syncable dataset. |
|
if (this.parent.syncable === true) { |
|
_.extend(this, Miso.Events); |
|
this.syncable = true; |
|
} |
|
|
|
this.idAttribute = this.parent.idAttribute; |
|
|
|
// save filter |
|
this.filter = { }; |
|
this.filter.columns = _.bind(this._columnFilter(options.filter.columns || undefined), this); |
|
this.filter.rows = _.bind(this._rowFilter(options.filter.rows || undefined), this); |
|
|
|
// initialize columns. |
|
this._columns = this._selectData(); |
|
|
|
Dataset.Builder.cacheColumns(this); |
|
Dataset.Builder.cacheRows(this); |
|
|
|
// bind to parent if syncable |
|
if (this.syncable) { |
|
this.parent.subscribe("change", this._sync, { context : this }); |
|
} |
|
}, |
|
|
|
// Syncs up the current view based on a passed delta. |
|
_sync : function(event) { |
|
var deltas = event.deltas, eventType = null; |
|
|
|
// iterate over deltas and update rows that are affected. |
|
_.each(deltas, function(d, deltaIndex) { |
|
|
|
// find row position based on delta _id |
|
var rowPos = this._rowPositionById[d[this.idAttribute]]; |
|
|
|
// ===== ADD NEW ROW |
|
|
|
if (typeof rowPos === "undefined" && Dataset.Event.isAdd(d)) { |
|
// this is an add event, since we couldn't find an |
|
// existing row to update and now need to just add a new |
|
// one. Use the delta's changed properties as the new row |
|
// if it passes the filter. |
|
if (this.filter.rows && this.filter.rows(d.changed)) { |
|
this._add(d.changed); |
|
eventType = "add"; |
|
} |
|
} else { |
|
|
|
//===== UPDATE EXISTING ROW |
|
if (rowPos === "undefined") { return; } |
|
|
|
// iterate over each changed property and update the value |
|
_.each(d.changed, function(newValue, columnName) { |
|
|
|
// find col position based on column name |
|
var colPos = this._columnPositionByName[columnName]; |
|
if (_.isUndefined(colPos)) { return; } |
|
this._columns[colPos].data[rowPos] = newValue; |
|
|
|
eventType = "update"; |
|
}, this); |
|
} |
|
|
|
|
|
// ====== DELETE ROW (either by event or by filter.) |
|
// TODO check if the row still passes filter, if not |
|
// delete it. |
|
var row = this.rowByPosition(rowPos); |
|
|
|
// if this is a delete event OR the row no longer |
|
// passes the filter, remove it. |
|
if (Dataset.Event.isRemove(d) || |
|
(this.filter.row && !this.filter.row(row))) { |
|
|
|
// Since this is now a delete event, we need to convert it |
|
// to such so that any child views, know how to interpet it. |
|
|
|
var newDelta = { |
|
old : this.rowByPosition(rowPos), |
|
changed : {} |
|
}; |
|
newDelta[this.idAttribute] = d[this.idAttribute]; |
|
|
|
// replace the old delta with this delta |
|
event.deltas.splice(deltaIndex, 1, newDelta); |
|
|
|
// remove row since it doesn't match the filter. |
|
this._remove(rowPos); |
|
eventType = "delete"; |
|
} |
|
|
|
}, this); |
|
|
|
// trigger any subscribers |
|
if (this.syncable) { |
|
this.publish(eventType, event); |
|
this.publish("change", event); |
|
} |
|
}, |
|
|
|
/** |
|
* Returns a dataset view based on the filtration parameters |
|
* Parameters: |
|
* filter - object with optional columns array and filter object/function |
|
* options - Options. |
|
* Returns: |
|
* new Miso.Dataset.DataView |
|
*/ |
|
where : function(filter, options) { |
|
options = options || {}; |
|
options.filter = options.filter || {}; |
|
if ( _.isFunction(filter) ) { |
|
options.filter.rows = filter; |
|
} else { |
|
options.filter = filter; |
|
} |
|
|
|
options.parent = this; |
|
|
|
return new Dataset.DataView(options); |
|
}, |
|
|
|
_selectData : function() { |
|
var selectedColumns = []; |
|
|
|
_.each(this.parent._columns, function(parentColumn) { |
|
|
|
// check if this column passes the column filter |
|
if (this.filter.columns(parentColumn)) { |
|
selectedColumns.push(new Dataset.Column({ |
|
name : parentColumn.name, |
|
data : [], |
|
type : parentColumn.type, |
|
_id : parentColumn._id |
|
})); |
|
} |
|
|
|
}, this); |
|
|
|
// get the data that passes the row filter. |
|
this.parent.each(function(row) { |
|
|
|
if (!this.filter.rows(row)) { |
|
return; |
|
} |
|
|
|
for(var i = 0; i < selectedColumns.length; i++) { |
|
selectedColumns[i].data.push(row[selectedColumns[i].name]); |
|
} |
|
}, this); |
|
|
|
return selectedColumns; |
|
}, |
|
|
|
/** |
|
* Returns a normalized version of the column filter function |
|
* that can be executed. |
|
* Parameters: |
|
* columnFilter - function or column name |
|
*/ |
|
_columnFilter: function(columnFilter) { |
|
var columnSelector; |
|
|
|
// if no column filter is specified, then just |
|
// return a passthrough function that will allow |
|
// any column through. |
|
if (_.isUndefined(columnFilter)) { |
|
columnSelector = function() { |
|
return true; |
|
}; |
|
} else { //array |
|
if (_.isString(columnFilter) ) { |
|
columnFilter = [ columnFilter ]; |
|
} |
|
columnFilter.push(this.idAttribute); |
|
columnSelector = function(column) { |
|
return _.indexOf(columnFilter, column.name) === -1 ? false : true; |
|
}; |
|
} |
|
|
|
return columnSelector; |
|
}, |
|
|
|
/** |
|
* Returns a normalized row filter function |
|
* that can be executed |
|
*/ |
|
_rowFilter: function(rowFilter) { |
|
|
|
var rowSelector; |
|
|
|
//support for a single ID; |
|
if (_.isNumber(rowFilter)) { |
|
rowFilter = [rowFilter]; |
|
} |
|
|
|
if (_.isUndefined(rowFilter)) { |
|
rowSelector = function() { |
|
return true; |
|
}; |
|
|
|
} else if (_.isFunction(rowFilter)) { |
|
rowSelector = rowFilter; |
|
|
|
} else { //array |
|
rowSelector = _.bind(function(row) { |
|
return _.indexOf(rowFilter, row[this.idAttribute]) === -1 ? |
|
false : |
|
true; |
|
}, this); |
|
} |
|
|
|
return rowSelector; |
|
}, |
|
|
|
/** |
|
* Returns a dataset view of the given column name |
|
* Parameters: |
|
* name - name of the column to be selected |
|
* Returns: |
|
* Miso.Column. |
|
*/ |
|
column : function(name) { |
|
return this._column(name); |
|
}, |
|
|
|
_column : function(name) { |
|
if (_.isUndefined(this._columnPositionByName)) { return undefined; } |
|
var pos = this._columnPositionByName[name]; |
|
return this._columns[pos]; |
|
}, |
|
|
|
/** |
|
* Returns a dataset view of the given columns |
|
* Parameters: |
|
* columnsArray - an array of column names |
|
* Returns: |
|
* Miso.DataView. |
|
*/ |
|
columns : function(columnsArray) { |
|
return new Dataset.DataView({ |
|
filter : { columns : columnsArray }, |
|
parent : this |
|
}); |
|
}, |
|
|
|
/** |
|
* Returns the names of all columns, not including id column. |
|
* Returns: |
|
* columnNames array |
|
*/ |
|
columnNames : function() { |
|
var cols = _.pluck(this._columns, 'name'); |
|
return _.reject(cols, function( colName ) { |
|
return colName === this.idAttribute || colName === '_oids'; |
|
}, this); |
|
}, |
|
|
|
/** |
|
* Returns true if a column exists, false otherwise. |
|
* Parameters: |
|
* name (string) |
|
* Returns |
|
* true | false |
|
*/ |
|
hasColumn : function(name) { |
|
return (!_.isUndefined(this._columnPositionByName[name])); |
|
}, |
|
|
|
/** |
|
* Iterates over all rows in the dataset |
|
* Paramters: |
|
* iterator - function that is passed each row |
|
* iterator(rowObject, index, dataset) |
|
* context - options object. Optional. |
|
*/ |
|
each : function(iterator, context) { |
|
for(var i = 0; i < this.length; i++) { |
|
iterator.apply(context || this, [this.rowByPosition(i), i]); |
|
} |
|
}, |
|
|
|
/** |
|
* Iterates over all rows in the dataset in reverse order |
|
* Parameters: |
|
* iterator - function that is passed each row |
|
* iterator(rowObject, index, dataset) |
|
* context - options object. Optional. |
|
*/ |
|
reverseEach : function(iterator, context) { |
|
for(var i = this.length-1; i >= 0; i--) { |
|
iterator.apply(context || this, [this.rowByPosition(i), i]); |
|
} |
|
}, |
|
|
|
/** |
|
* Iterates over each column. |
|
* Parameters: |
|
* iterator - function that is passed: |
|
* iterator(colName, column, index) |
|
* context - options object. Optional. |
|
*/ |
|
eachColumn : function(iterator, context) { |
|
// skip id col |
|
var cols = this.columnNames(); |
|
for(var i = 0; i < cols.length; i++) { |
|
iterator.apply(context || this, [cols[i], this.column(cols[i]), i]); |
|
} |
|
}, |
|
|
|
/** |
|
* Returns a single row based on its position (NOT ID.) |
|
* Paramters: |
|
* i - position index |
|
* Returns: |
|
* row object representation |
|
*/ |
|
rowByPosition : function(i) { |
|
return this._row(i); |
|
}, |
|
|
|
/** |
|
* Returns a single row based on its id (NOT Position.) |
|
* Parameters: |
|
* id - unique id |
|
* Returns: |
|
* row object representation |
|
*/ |
|
rowById : function(id) { |
|
return this._row(this._rowPositionById[id]); |
|
}, |
|
|
|
_row : function(pos) { |
|
var row = {}; |
|
_.each(this._columns, function(column) { |
|
row[column.name] = column.data[pos]; |
|
}); |
|
return row; |
|
}, |
|
_remove : function(rowId) { |
|
var rowPos = this._rowPositionById[rowId]; |
|
|
|
// remove all values |
|
_.each(this._columns, function(column) { |
|
column.data.splice(rowPos, 1); |
|
}); |
|
|
|
// update caches |
|
delete this._rowPositionById[rowId]; |
|
this._rowIdByPosition.splice(rowPos, 1); |
|
this.length--; |
|
|
|
return this; |
|
}, |
|
|
|
_add : function(row) { |
|
|
|
// first coerce all the values appropriatly |
|
_.each(row, function(value, key) { |
|
var column = this.column(key); |
|
|
|
// is this a computed column? if so throw an error |
|
if (column.isComputed()) { |
|
throw "You're trying to update a computed column. Those get computed!"; |
|
} |
|
|
|
// if we suddenly see values for data that didn't exist before as a column |
|
// just drop it. First fetch defines the column structure. |
|
if (typeof column !== "undefined") { |
|
var Type = Dataset.types[column.type]; |
|
|
|
// test if value matches column type |
|
if (column.force || Type.test(row[column.name], column)) { |
|
|
|
// do we have a before filter? If so, pass it through that first |
|
if (!_.isUndefined(column.before)) { |
|
row[column.name] = column.before(row[column.name]); |
|
} |
|
|
|
// coerce it. |
|
row[column.name] = Type.coerce(row[column.name], column); |
|
|
|
} else { |
|
throw("incorrect value '" + row[column.name] + |
|
"' of type " + Dataset.typeOf(row[column.name], column) + |
|
" passed to column '" + column.name + "' with type " + column.type); |
|
|
|
} |
|
} |
|
}, this); |
|
|
|
// do we have any computed columns? If so we need to calculate their values. |
|
if (this._computedColumns) { |
|
_.each(this._computedColumns, function(column) { |
|
var newVal = column.compute(row); |
|
row[column.name] = newVal; |
|
}); |
|
} |
|
|
|
// if we don't have a comparator, just append them at the end. |
|
if (_.isUndefined(this.comparator)) { |
|
|
|
// add all data |
|
_.each(this._columns, function(column) { |
|
if (!column.isComputed()) { |
|
column.data.push(!_.isUndefined(row[column.name]) && !_.isNull(row[column.name]) ? row[column.name] : null); |
|
} |
|
}); |
|
|
|
this.length++; |
|
|
|
// add row indeces to the cache |
|
this._rowIdByPosition = this._rowIdByPosition || (this._rowIdByPosition = []); |
|
this._rowPositionById = this._rowPositionById || (this._rowPositionById = {}); |
|
|
|
// if this row already exists, throw an error |
|
if (typeof this._rowPositionById[row[this.idAttribute]] !== "undefined") { |
|
throw "The id " + row[this.idAttribute] + " is not unique. The " + this.idAttribute + " column must be unique"; |
|
} |
|
|
|
this._rowPositionById[row[this.idAttribute]] = this._rowIdByPosition.length; |
|
this._rowIdByPosition.push(row[this.idAttribute]); |
|
|
|
// otherwise insert them in the right place. This is a somewhat |
|
// expensive operation. |
|
} else { |
|
|
|
var insertAt = function(at, value, into) { |
|
Array.prototype.splice.apply(into, [at, 0].concat(value)); |
|
}; |
|
|
|
var i; |
|
this.length++; |
|
for(i = 0; i < this.length; i++) { |
|
var row2 = this.rowByPosition(i); |
|
if (_.isUndefined(row2[this.idAttribute]) || this.comparator(row, row2) < 0) { |
|
|
|
_.each(this._columns, function(column) { |
|
insertAt(i, (row[column.name] ? row[column.name] : null), column.data); |
|
}); |
|
|
|
break; |
|
} |
|
} |
|
|
|
// rebuild position cache... |
|
// we could splice it in but its safer this way. |
|
this._rowIdByPosition = []; |
|
this._rowPositionById = {}; |
|
this.each(function(row, i) { |
|
this._rowIdByPosition.push(row[this.idAttribute]); |
|
this._rowPositionById[row[this.idAttribute]] = i; |
|
}, this); |
|
} |
|
|
|
return this; |
|
}, |
|
|
|
/** |
|
* Returns a dataset view of filtered rows |
|
* @param {function|array} filter - a filter function or object, |
|
* the same as where |
|
*/ |
|
rows : function(filter) { |
|
return new Dataset.DataView({ |
|
filter : { rows : filter }, |
|
parent : this |
|
}); |
|
}, |
|
|
|
/** |
|
* Sort rows based on comparator |
|
* |
|
* roughly taken from here: |
|
* http://jxlib.googlecode.com/svn-history/r977/trunk/src/Source/Data/heapsort.js |
|
* License: |
|
* Copyright (c) 2009, Jon Bomgardner. |
|
* This file is licensed under an MIT style license |
|
* Parameters: |
|
* options - Optional |
|
*/ |
|
sort : function(args) { |
|
var options = {}, cachedRows = []; |
|
|
|
//If the first param is the comparator, set it as such. |
|
if ( _.isFunction(args) ) { |
|
options.comparator = args; |
|
} else { |
|
options = args || {}; |
|
} |
|
|
|
if (options.comparator) { |
|
this.comparator = options.comparator; |
|
} else if (_.isUndefined(this.comparator)) { |
|
throw new Error("Cannot sort without this.comparator."); |
|
} |
|
|
|
// cache rows |
|
var i, j, row; |
|
for(i = 0; i < this.length; i++) { |
|
cachedRows[i] = this._row(i); |
|
} |
|
|
|
cachedRows.sort( this.comparator ); |
|
|
|
// iterate through cached rows, overwriting data in columns |
|
i = cachedRows.length; |
|
while ( i-- ) { |
|
row = cachedRows[i]; |
|
|
|
this._rowIdByPosition[i] = row[ this.idAttribute ]; |
|
this._rowPositionById[ row[ this.idAttribute ] ] = i; |
|
|
|
j = this._columns.length; |
|
while ( j-- ) { |
|
var col = this._columns[j]; |
|
col.data[i] = row[ col.name ]; |
|
} |
|
} |
|
|
|
if (this.syncable && !options.silent) { |
|
this.publish("sort"); |
|
} |
|
|
|
return this; |
|
}, |
|
|
|
/** |
|
* Exports a version of the dataset in json format. |
|
* Returns: |
|
* Array of rows. |
|
*/ |
|
toJSON : function() { |
|
var rows = []; |
|
for(var i = 0; i < this.length; i++) { |
|
rows.push(this.rowByPosition(i)); |
|
} |
|
return rows; |
|
} |
|
}); |
|
|
|
}(this, _)); |
|
|
|
/** |
|
Library Deets go here |
|
USE OUR CODES |
|
|
|
Version 0.0.1.2 |
|
*/ |
|
|
|
(function(global, _, moment) { |
|
|
|
var Miso = global.Miso || (global.Miso = {}); |
|
var Dataset = global.Miso.Dataset; |
|
|
|
// take on miso dataview's prototype |
|
Dataset.prototype = new Dataset.DataView(); |
|
|
|
// add dataset methods to dataview. |
|
_.extend(Dataset.prototype, { |
|
|
|
/** |
|
* @private |
|
* Internal initialization method. Reponsible for data parsing. |
|
* @param {object} options - Optional options |
|
*/ |
|
_initialize: function(options) { |
|
|
|
// is this a syncable dataset? if so, pull |
|
// required methods and mark this as a syncable dataset. |
|
if (options.sync === true) { |
|
_.extend(this, Miso.Events); |
|
this.syncable = true; |
|
} |
|
|
|
this.idAttribute = options.idAttribute || '_id'; |
|
|
|
// initialize importer from options or just create a blank |
|
// one for now, we'll detect it later. |
|
this.importer = options.importer || null; |
|
|
|
// default parser is object parser, unless otherwise specified. |
|
this.parser = options.parser || Dataset.Parsers.Obj; |
|
|
|
// figure out out if we need another parser. |
|
if (_.isUndefined(options.parser)) { |
|
if (options.strict) { |
|
this.parser = Dataset.Parsers.Strict; |
|
} else if (options.delimiter) { |
|
this.parser = Dataset.Parsers.Delimited; |
|
} |
|
} |
|
|
|
// initialize the proper importer |
|
if (this.importer === null) { |
|
if (options.url) { |
|
|
|
if (!options.interval) { |
|
this.importer = Dataset.Importers.Remote; |
|
} else { |
|
this.importer = Dataset.Importers.Polling; |
|
this.interval = options.interval; |
|
} |
|
|
|
} else { |
|
this.importer = Dataset.Importers.Local; |
|
} |
|
} |
|
|
|
// initialize importer and parser |
|
this.parser = new this.parser(options); |
|
|
|
if (this.parser instanceof Dataset.Parsers.Delimited) { |
|
options.dataType = "text"; |
|
} |
|
|
|
this.importer = new this.importer(options); |
|
|
|
// save comparator if we have one |
|
if (options.comparator) { |
|
this.comparator = options.comparator; |
|
} |
|
|
|
// if we have a ready callback, save it too |
|
if (options.ready) { |
|
this.ready = options.ready; |
|
} |
|
|
|
// If new data is being fetched and we want to just |
|
// replace existing rows, save this flag. |
|
if (options.resetOnFetch) { |
|
this.resetOnFetch = options.resetOnFetch; |
|
} |
|
|
|
// if new data is being fetched and we want to make sure |
|
// only new rows are appended, a column must be provided |
|
// against which uniqueness will be checked. |
|
// otherwise we are just going to blindly add rows. |
|
if (options.uniqueAgainst) { |
|
this.uniqueAgainst = options.uniqueAgainst; |
|
} |
|
|
|
// if there is no data and no url set, we must be building |
|
// the dataset from scratch, so create an id column. |
|
if (_.isUndefined(options.data) && _.isUndefined(options.url)) { |
|
this._addIdColumn(); |
|
} |
|
|
|
// if for any reason, you want to use a different deferred |
|
// implementation, pass it as an option |
|
if (options.deferred) { |
|
this.deferred = options.deferred; |
|
} else { |
|
this.deferred = new _.Deferred(); |
|
} |
|
|
|
//build any columns present in the constructor |
|
if ( options.columns ) { |
|
this.addColumns(options.columns); |
|
} |
|
}, |
|
|
|
/** |
|
* Responsible for actually fetching the data based on the initialized dataset. |
|
* Note that this needs to be called for either local or remote data. |
|
* There are three different ways to use this method: |
|
* ds.fetch() - will just fetch the data based on the importer. Note that for async |
|
* fetching this isn't blocking so don't put your next set of instructions |
|
* expecting the data to be there. |
|
* ds.fetch({ |
|
* success: function() { |
|
* // do stuff |
|
* // this is the dataset. |
|
* }, |
|
* error : function(e) { |
|
* // do stuff |
|
* } |
|
* }) - Allows you to pass success and error callbacks that will be called once data |
|
* is property fetched. |
|
* |
|
* _.when(ds.fetch(), function() { |
|
* // do stuff |
|
* // note 'this' is NOT the dataset. |
|
* }) - Allows you to use deferred behavior to potentially chain multiple datasets. |
|
* |
|
* @param {object} options Optional success/error callbacks. |
|
**/ |
|
fetch : function(options) { |
|
options = options || {}; |
|
|
|
var dfd = this.deferred; |
|
|
|
if ( _.isNull(this.importer) ) { |
|
throw "No importer defined"; |
|
} |
|
|
|
this.importer.fetch({ |
|
success: _.bind(function( data ) { |
|
|
|
try { |
|
this._apply( data ); |
|
} catch (e) { |
|
if (options.error) { |
|
options.error.call(this, e); |
|
} else { |
|
throw e; |
|
} |
|
} |
|
|
|
// if a comparator was defined, sort the data |
|
if (this.comparator) { |
|
this.sort(); |
|
} |
|
|
|
if (this.ready) { |
|
this.ready.call(this); |
|
} |
|
|
|
if (options.success) { |
|
options.success.call(this); |
|
} |
|
|
|
// Ensure the context of the promise is set to the Dataset |
|
dfd.resolveWith(this, [this]); |
|
|
|
}, this), |
|
|
|
error : _.bind(function(e) { |
|
if (options.error) { |
|
options.error.call(this, e); |
|
} |
|
|
|
dfd.reject(e); |
|
}, this) |
|
}); |
|
|
|
return dfd.promise(); |
|
}, |
|
|
|
//These are the methods that will be used to determine |
|
//how to update a dataset's data when fetch() is called |
|
_applications : { |
|
|
|
//Update existing values, used the pass column to match |
|
//incoming data to existing rows. |
|
againstColumn : function(data) { |
|
var rows = [], |
|
colNames = _.keys(data), |
|
row, |
|
uniqName = this.uniqueAgainst, |
|
uniqCol = this.column(uniqName), |
|
toAdd = [], |
|
toUpdate = [], |
|
toRemove = []; |
|
|
|
_.each(data[uniqName], function(key, dataIndex) { |
|
var rowIndex = uniqCol.data.indexOf( Dataset.types[uniqCol.type].coerce(key) ); |
|
|
|
var row = {}; |
|
_.each(data, function(col, name) { |
|
row[name] = col[dataIndex]; |
|
}); |
|
|
|
if (rowIndex === -1) { |
|
toAdd.push( row ); |
|
} else { |
|
toUpdate.push( row ); |
|
row[this.idAttribute] = this.rowById(this.column(this.idAttribute).data[rowIndex])[this.idAttribute]; |
|
this.update(row); |
|
} |
|
}, this); |
|
if (toAdd.length > 0) { |
|
this.add(toAdd); |
|
} |
|
}, |
|
|
|
//Always blindly add new rows |
|
blind : function( data ) { |
|
var columnName, columnData, rows = [], row; |
|
|
|
// figure out the length of rows we have. |
|
var colNames = _.keys(data), |
|
dataLength = _.max(_.map(colNames, function(name) { |
|
return data[name].length; |
|
}, this)); |
|
|
|
// build row objects |
|
for( var i = 0; i < dataLength; i++) { |
|
row = {}; |
|
for(var j = 0; j < colNames.length; j++) { |
|
row[colNames[j]] = data[colNames[j]][i]; |
|
} |
|
rows.push(row); |
|
} |
|
|
|
this.add(rows); |
|
} |
|
}, |
|
|
|
//Takes a dataset and some data and applies one to the other |
|
_apply : function( data ) { |
|
|
|
var parsed = this.parser.parse( data ); |
|
|
|
// first time fetch |
|
if ( !this.fetched ) { |
|
|
|
// create columns (inc _id col.) |
|
this._addIdColumn(); |
|
this.addColumns( _.map(parsed.columns, function( name ) { |
|
return { name : name }; |
|
}) |
|
); |
|
|
|
// detect column types, add all rows blindly and cache them. |
|
Dataset.Builder.detectColumnTypes(this, parsed.data); |
|
this._applications.blind.call( this, parsed.data ); |
|
|
|
this.fetched = true; |
|
|
|
// reset on fetch |
|
} else if (this.resetOnFetch) { |
|
|
|
// clear the data |
|
this.reset(); |
|
|
|
// blindly add the data. |
|
this._applications.blind.call( this, parsed.data ); |
|
|
|
// append |
|
} else if (this.uniqueAgainst) { |
|
|
|
// make sure we actually have this column |
|
if (!this.hasColumn(this.uniqueAgainst)) { |
|
throw new Error("You requested a unique add against a column that doesn't exist."); |
|
} |
|
|
|
this._applications.againstColumn.call(this, parsed.data); |
|
|
|
// polling fetch, just blindly add rows |
|
} else { |
|
this._applications.blind.call( this, parsed.data ); |
|
} |
|
|
|
Dataset.Builder.cacheRows(this); |
|
}, |
|
|
|
/** |
|
* Adds columns to the dataset. |
|
*/ |
|
addColumns : function( columns ) { |
|
_.each(columns, function( column ) { |
|
this.addColumn( column ); |
|
}, this); |
|
}, |
|
|
|
/** |
|
* Allows adding of a computed column. A computed column is |
|
* a column that is somehow based on the other existing columns. |
|
* Parameters: |
|
* name : name of new column |
|
* type : The type of the column based on existing types. |
|
* func : The way that the column is derived. It takes a row as a parameter. |
|
*/ |
|
addComputedColumn : function(name, type, func) { |
|
// check if we already ahve a column by this name. |
|
if ( !_.isUndefined(this.column(name)) ) { |
|
throw "There is already a column by this name."; |
|
} else { |
|
|
|
// check that this is a known type. |
|
if (typeof Dataset.types[type] === "undefined") { |
|
throw "The type " + type + " doesn't exist"; |
|
} |
|
|
|
var column = new Dataset.Column({ |
|
name : name, |
|
type : type, |
|
func : _.bind(func, this) |
|
}); |
|
|
|
this._columns.push(column); |
|
this._computedColumns.push(column); |
|
this._columnPositionByName[column.name] = this._columns.length - 1; |
|
|
|
// do we already have data? if so compute the values for this column. |
|
if (this.length > 0) { |
|
this.each(function(row, i) { |
|
column.compute(row, i); |
|
}, this); |
|
} |
|
|
|
return column; |
|
} |
|
}, |
|
|
|
/** |
|
* Adds a single column to the dataset |
|
* Parameters: |
|
* column : a set of properties describing a column (name, type, data etc.) |
|
* Returns |
|
* Miso.Column object. |
|
*/ |
|
addColumn : function(column) { |
|
//don't create a column that already exists |
|
if ( !_.isUndefined(this.column(column.name)) ) { |
|
return false; |
|
} |
|
|
|
column = new Dataset.Column( column ); |
|
|
|
this._columns.push( column ); |
|
this._columnPositionByName[column.name] = this._columns.length - 1; |
|
|
|
return column; |
|
}, |
|
|
|
/** |
|
* Adds an id column to the column definition. If a count |
|
* is provided, also generates unique ids. |
|
* Parameters: |
|
* count - the number of ids to generate. |
|
*/ |
|
_addIdColumn : function( count ) { |
|
// if we have any data, generate actual ids. |
|
|
|
if (!_.isUndefined(this.column(this.idAttribute))) { |
|
return; |
|
} |
|
|
|
var ids = []; |
|
if (count && count > 0) { |
|
_.times(count, function() { |
|
ids.push(_.uniqueId()); |
|
}); |
|
} |
|
|
|
// add the id column |
|
var idCol = this.addColumn({ name: this.idAttribute, data : ids }); |
|
// is this the default _id? if so set numeric type. Otherwise, |
|
// detect data |
|
if (this.idAttribute === "_id") { |
|
idCol.type = "number"; |
|
} |
|
|
|
// did we accidentally add it to the wrong place? (it should always be first.) |
|
if (this._columnPositionByName[this.idAttribute] !== 0) { |
|
|
|
// we need to move it to the beginning and unshift all the other |
|
// columns |
|
var oldIdColPos = this._columnPositionByName[this.idAttribute]; |
|
|
|
// move col back |
|
this._columns.splice(oldIdColPos, 1); |
|
this._columns.unshift(idCol); |
|
|
|
this._columnPositionByName[this.idAttribute] = 0; |
|
_.each(this._columnPositionByName, function(pos, colName) { |
|
if (colName !== this.idAttribute && this._columnPositionByName[colName] < oldIdColPos) { |
|
this._columnPositionByName[colName]++; |
|
} |
|
}, this); |
|
} |
|
|
|
}, |
|
|
|
/** |
|
* Add a row to the dataset. Triggers add and change. |
|
* Parameters: |
|
* row - an object representing a row in the form of: |
|
* {columnName: value} |
|
* options - options |
|
* silent: boolean, do not trigger an add (and thus view updates) event |
|
*/ |
|
add : function(rows, options) { |
|
|
|
options = options || {}; |
|
|
|
if (!_.isArray(rows)) { |
|
rows = [rows]; |
|
} |
|
|
|
var deltas = []; |
|
|
|
_.each(rows, function(row) { |
|
if (!row[this.idAttribute]) { |
|
row[this.idAttribute] = _.uniqueId(); |
|
} |
|
|
|
this._add(row, options); |
|
|
|
// store all deltas for a single fire event. |
|
if (this.syncable && !options.silent) { |
|
deltas.push({ changed : row }); |
|
} |
|
|
|
}, this); |
|
|
|
if (this.syncable && !options.silent) { |
|
var e = Dataset.Events._buildEvent(deltas, this); |
|
this.publish('add', e ); |
|
this.publish('change', e ); |
|
} |
|
|
|
return this; |
|
}, |
|
|
|
/** |
|
* Remove all rows that match the filter. Fires remove and change. |
|
* Parameters: |
|
* filter - row id OR function applied to each row to see if it should be removed. |
|
* options - options. Optional. |
|
* silent: boolean, do not trigger an add (and thus view updates) event |
|
*/ |
|
remove : function(filter, options) { |
|
filter = this._rowFilter(filter); |
|
var deltas = [], rowsToRemove = []; |
|
|
|
this.each(function(row, rowIndex) { |
|
if (filter(row)) { |
|
rowsToRemove.push(row[this.idAttribute]); |
|
deltas.push( { old: row } ); |
|
} |
|
}); |
|
|
|
// don't attempt tp remove the rows while iterating over them |
|
// since that modifies the length of the dataset and thus |
|
// terminates the each loop early. |
|
_.each(rowsToRemove, function(rowId) { |
|
this._remove(rowId); |
|
}, this); |
|
|
|
if (this.syncable && (!options || !options.silent)) { |
|
var ev = Dataset.Events._buildEvent( deltas, this ); |
|
this.publish('remove', ev ); |
|
this.publish('change', ev ); |
|
} |
|
}, |
|
|
|
_arrayUpdate : function(rows) { |
|
var deltas = []; |
|
_.each(rows, function(newRow) { |
|
var delta = { old : {}, changed : {} }; |
|
delta[this.idAttribute] = newRow[this.idAttribute]; |
|
|
|
var pos = this._rowPositionById[newRow[this.idAttribute]]; |
|
_.each(newRow, function(value, prop) { |
|
var column = this._columns[this._columnPositionByName[prop]]; |
|
var type = Dataset.types[column.type]; |
|
|
|
if ((column.name === this.idAttribute) && (column.data[pos] !== value)) { |
|
throw "You can't update the id column"; |
|
} |
|
|
|
if (typeof column === "undefined") { |
|
throw "column " + prop + " not found!"; |
|
} |
|
|
|
//Ensure value passes the type test |
|
if (!type.test(value, column)) { |
|
throw "Value is incorrect type"; |
|
} |
|
|
|
//skip if computed column |
|
if (this._computedColumns[column.name]) { |
|
return; |
|
} |
|
|
|
value = type.coerce(value, column); |
|
|
|
//Run any before filters on the column |
|
if (!_.isUndefined(column.before)) { |
|
value = column.before(value); |
|
} |
|
|
|
if (column.data[pos] !== value) { |
|
delta.old[prop] = column.data[pos]; |
|
column.data[pos] = value; |
|
delta.changed[prop] = value; |
|
} |
|
|
|
|
|
}, this); |
|
|
|
// Update any computed columns |
|
if (typeof this._computedColumns !== "undefined") { |
|
_.each(this._computedColumns, function(column) { |
|
var temprow = _.extend({}, this._row(pos)), |
|
oldValue = temprow[column.name], |
|
newValue = column.compute(temprow, pos); |
|
if (oldValue !== newValue) { |
|
delta.old[column.name] = oldValue; |
|
column.data[pos] = newValue; |
|
delta.changed[column.name] = newValue; |
|
} |
|
}, this); |
|
} |
|
if ( _.keys(delta.changed).length > 0 ) { |
|
deltas.push(delta); |
|
} |
|
}, this); |
|
return deltas; |
|
}, |
|
|
|
_functionUpdate : function(func) { |
|
var rows = []; |
|
for(var i = 0; i < this.length; i++) { |
|
var newRow = func(this.rowByPosition(i)); |
|
if (newRow !== false) { |
|
rows.push( newRow ); |
|
} |
|
} |
|
return this._arrayUpdate(rows); |
|
}, |
|
|
|
/** |
|
* Update can be used on one of three ways. |
|
* 1: To update specific rows by passing in an object with the _id |
|
* 2: To update a number of rows by passing in an array of objects with _ids |
|
* 3: To update a number of row by passing in a function which will be applied to |
|
* all rows. |
|
* */ |
|
update : function( rowsOrFunction, options ) { |
|
var deltas; |
|
|
|
if ( _.isFunction(rowsOrFunction) ) { |
|
deltas = this._functionUpdate(rowsOrFunction); |
|
} else { |
|
var rows = _.isArray(rowsOrFunction) ? rowsOrFunction : [rowsOrFunction]; |
|
deltas = this._arrayUpdate(rows); |
|
} |
|
|
|
//computer column updates |
|
//update triggers |
|
if (this.syncable && (!options || !options.silent)) { |
|
var ev = Dataset.Events._buildEvent( deltas, this ); |
|
this.publish('update', ev ); |
|
this.publish('change', ev ); |
|
} |
|
return this; |
|
}, |
|
|
|
/** |
|
* Clears all the rows |
|
* Fires a "reset" event. |
|
* Parameters: |
|
* options (object) |
|
* silent : true | false. |
|
*/ |
|
reset : function(options) { |
|
_.each(this._columns, function(col) { |
|
col.data = []; |
|
}); |
|
this.length = 0; |
|
if (this.syncable && (!options || !options.silent)) { |
|
this.publish("reset"); |
|
} |
|
} |
|
|
|
}); |
|
|
|
}(this, _, moment)); |
|
|
|
|
|
(function(global, _) { |
|
|
|
var Dataset = global.Miso.Dataset; |
|
|
|
Dataset.typeOf = function(value, options) { |
|
var types = _.keys(Dataset.types), |
|
chosenType; |
|
|
|
//move string and mixed to the end |
|
types.push(types.splice(_.indexOf(types, 'string'), 1)[0]); |
|
types.push(types.splice(_.indexOf(types, 'mixed'), 1)[0]); |
|
|
|
chosenType = _.find(types, function(type) { |
|
return Dataset.types[type].test(value, options); |
|
}); |
|
|
|
chosenType = _.isUndefined(chosenType) ? 'string' : chosenType; |
|
|
|
return chosenType; |
|
}; |
|
|
|
Dataset.types = { |
|
|
|
mixed : { |
|
name : 'mixed', |
|
coerce : function(v) { |
|
if (_.isNull(v) || typeof v === "undefined" || _.isNaN(v)) { |
|
return null; |
|
} |
|
return v; |
|
}, |
|
test : function() { |
|
return true; |
|
}, |
|
compare : function(s1, s2) { |
|
if ( _.isEqual(s1, s2) ) { return 0; } |
|
if (s1 < s2) { return -1;} |
|
if (s1 > s2) { return 1; } |
|
}, |
|
numeric : function(v) { |
|
return v === null || _.isNaN(+v) ? null : +v; |
|
} |
|
}, |
|
|
|
string : { |
|
name : "string", |
|
coerce : function(v) { |
|
if (_.isNaN(v) || v === null || typeof v === "undefined") { |
|
return null; |
|
} |
|
return v.toString(); |
|
}, |
|
|
|
test : function(v) { |
|
return (v === null || typeof v === "undefined" || typeof v === 'string'); |
|
}, |
|
|
|
compare : function(s1, s2) { |
|
if (s1 == null && s2 != null) { return -1; } |
|
if (s1 != null && s2 == null) { return 1; } |
|
if (s1 < s2) { return -1; } |
|
if (s1 > s2) { return 1; } |
|
return 0; |
|
}, |
|
|
|
numeric : function(value) { |
|
if (_.isNaN(+value) || value === null) { |
|
return null; |
|
} else if (_.isNumber(+value)) { |
|
return +value; |
|
} else { |
|
return null; |
|
} |
|
} |
|
}, |
|
|
|
"boolean" : { |
|
name : "boolean", |
|
regexp : /^(true|false)$/, |
|
coerce : function(v) { |
|
if (_.isNaN(v) || v === null || typeof v === "undefined") { |
|
return null; |
|
} |
|
if (v === 'false') { return false; } |
|
return Boolean(v); |
|
}, |
|
test : function(v) { |
|
if (v === null || typeof v === "undefined" || typeof v === 'boolean' || this.regexp.test( v ) ) { |
|
return true; |
|
} else { |
|
return false; |
|
} |
|
}, |
|
compare : function(n1, n2) { |
|
if (n1 == null && n2 != null) { return -1; } |
|
if (n1 != null && n2 == null) { return 1; } |
|
if (n1 == null && n2 == null) { return 0; } |
|
if (n1 === n2) { return 0; } |
|
return (n1 < n2 ? -1 : 1); |
|
}, |
|
numeric : function(value) { |
|
if (value === null || _.isNaN(value)) { |
|
return null; |
|
} else { |
|
return (value) ? 1 : 0; |
|
} |
|
} |
|
}, |
|
|
|
number : { |
|
name : "number", |
|
regexp : /^\s*[\-\.]?[0-9]+([\.][0-9]+)?\s*$/, |
|
coerce : function(v) { |
|
var cv = +v; |
|
if (_.isNull(v) || typeof v === "undefined" || _.isNaN(cv)) { |
|
return null; |
|
} |
|
return cv; |
|
}, |
|
test : function(v) { |
|
if (v === null || typeof v === "undefined" || typeof v === 'number' || this.regexp.test( v ) ) { |
|
return true; |
|
} else { |
|
return false; |
|
} |
|
}, |
|
compare : function(n1, n2) { |
|
if (n1 == null && n2 != null) { return -1; } |
|
if (n1 != null && n2 == null) { return 1; } |
|
if (n1 == null && n2 == null) { return 0; } |
|
if (n1 === n2) { return 0; } |
|
return (n1 < n2 ? -1 : 1); |
|
}, |
|
numeric : function(value) { |
|
if (_.isNaN(value) || value === null) { |
|
return null; |
|
} |
|
return value; |
|
} |
|
}, |
|
|
|
time : { |
|
name : "time", |
|
format : "DD/MM/YYYY", |
|
_formatLookup : [ |
|
['DD', "\\d{2}"], |
|
['D' , "\\d{1}|\\d{2}"], |
|
['MM', "\\d{2}"], |
|
['M' , "\\d{1}|\\d{2}"], |
|
['YYYY', "\\d{4}"], |
|
['YY', "\\d{2}"], |
|
['A', "[AM|PM]"], |
|
['hh', "\\d{2}"], |
|
['h', "\\d{1}|\\d{2}"], |
|
['mm', "\\d{2}"], |
|
['m', "\\d{1}|\\d{2}"], |
|
['ss', "\\d{2}"], |
|
['s', "\\d{1}|\\d{2}"], |
|
['ZZ',"[-|+]\\d{4}"], |
|
['Z', "[-|+]\\d{2}:\\d{2}"] |
|
], |
|
_regexpTable : {}, |
|
|
|
_regexp: function(format) { |
|
//memoise |
|
if (this._regexpTable[format]) { |
|
return new RegExp(this._regexpTable[format], 'g'); |
|
} |
|
|
|
//build the regexp for substitutions |
|
var regexp = format; |
|
_.each(this._formatLookup, function(pair) { |
|
regexp = regexp.replace(pair[0], pair[1]); |
|
}, this); |
|
|
|
// escape all forward slashes |
|
regexp = regexp.split("/").join("\\/"); |
|
|
|
// save the string of the regexp, NOT the regexp itself. |
|
// For some reason, this resulted in inconsistant behavior |
|
this._regexpTable[format] = regexp; |
|
return new RegExp(this._regexpTable[format], 'g'); |
|
}, |
|
|
|
coerce : function(v, options) { |
|
options = options || {}; |
|
|
|
if (_.isNull(v) || typeof v === "undefined" || _.isNaN(v)) { |
|
return null; |
|
} |
|
|
|
// if string, then parse as a time |
|
if (_.isString(v)) { |
|
var format = options.format || this.format; |
|
return moment(v, format); |
|
} else if (_.isNumber(v)) { |
|
return moment(v); |
|
} else { |
|
return v; |
|
} |
|
|
|
}, |
|
|
|
test : function(v, options) { |
|
options = options || {}; |
|
if (v === null || typeof v === "undefined") { |
|
return true; |
|
} |
|
if (_.isString(v) ) { |
|
var format = options.format || this.format, |
|
regex = this._regexp(format); |
|
return regex.test(v); |
|
} else { |
|
//any number or moment obj basically |
|
return true; |
|
} |
|
}, |
|
compare : function(d1, d2) { |
|
if (d1 < d2) {return -1;} |
|
if (d1 > d2) {return 1;} |
|
return 0; |
|
}, |
|
numeric : function( value ) { |
|
if (_.isNaN(value) || value === null) { |
|
return null; |
|
} |
|
return value.valueOf(); |
|
} |
|
} |
|
}; |
|
|
|
}(this, _)); |
|
|
|
(function(global, _) { |
|
|
|
var Dataset = global.Miso.Dataset; |
|
|
|
/** |
|
* A representation of an event as it is passed through the |
|
* system. Used for view synchronization and other default |
|
* CRUD ops. |
|
* Parameters: |
|
* deltas - array of deltas. |
|
* each delta: { changed : {}, old : {} } |
|
*/ |
|
Dataset.Event = function(deltas, dataset) { |
|
if (!_.isArray(deltas)) { |
|
deltas = [deltas]; |
|
} |
|
this.deltas = deltas; |
|
this.dataset = dataset || null; |
|
}; |
|
|
|
_.extend(Dataset.Event.prototype, { |
|
affectedColumns : function() { |
|
var cols = []; |
|
_.each(this.deltas, function(delta) { |
|
delta.old = (delta.old || []); |
|
delta.changed = (delta.changed || []); |
|
cols = _.chain(cols) |
|
.union(_.keys(delta.old), _.keys(delta.changed) ) |
|
.reject(function(col) { |
|
return col === this.dataset.idAttribute; |
|
}, this).value(); |
|
}, this); |
|
|
|
return cols; |
|
} |
|
}); |
|
|
|
_.extend(Dataset.Event, { |
|
/** |
|
* Returns true if the event is a deletion |
|
*/ |
|
isRemove : function(delta) { |
|
if (_.isUndefined(delta.changed) || _.keys(delta.changed).length === 0) { |
|
return true; |
|
} else { |
|
return false; |
|
} |
|
}, |
|
|
|
/** |
|
* Returns true if the event is an add event. |
|
*/ |
|
isAdd : function(delta) { |
|
if (_.isUndefined(delta.old) || _.keys(delta.old).length === 0) { |
|
return true; |
|
} else { |
|
return false; |
|
} |
|
}, |
|
|
|
/** |
|
* Returns true if the event is an update. |
|
*/ |
|
isUpdate : function(delta) { |
|
if (!this.isRemove(delta) && !this.isAdd(delta)) { |
|
return true; |
|
} else { |
|
return false; |
|
} |
|
} |
|
}); |
|
|
|
|
|
//Event Related Methods |
|
Dataset.Events = {}; |
|
|
|
// Used to build event objects accross the application. |
|
Dataset.Events._buildEvent = function(delta, dataset) { |
|
return new Dataset.Event(delta, dataset); |
|
}; |
|
|
|
}(this, _)); |
|
|
|
(function(global, _) { |
|
|
|
var Dataset = global.Miso.Dataset; |
|
|
|
/** |
|
* This is a generic collection of dataset-building utilities |
|
* that are used by Miso.Dataset and Miso.DataView. |
|
*/ |
|
Dataset.Builder = { |
|
|
|
/** |
|
* Detects the type of a column based on some input data. |
|
* Parameters: |
|
* column - the Miso.Column object |
|
* data - an array of data to be scanned for type detection |
|
* Returns: |
|
* input column but typed. |
|
*/ |
|
detectColumnType : function(column, data) { |
|
|
|
// compute the type by assembling a sample of computed types |
|
// and then squashing it to create a unique subset. |
|
var type = _.inject(data.slice(0, 5), function(memo, value) { |
|
|
|
var t = Dataset.typeOf(value); |
|
|
|
if (value !== "" && memo.indexOf(t) === -1 && !_.isNull(value)) { |
|
memo.push(t); |
|
} |
|
return memo; |
|
}, []); |
|
|
|
// if we only have one type in our sample, save it as the type |
|
if (type.length === 1) { |
|
column.type = type[0]; |
|
} else { |
|
//empty column or mixed type |
|
column.type = 'mixed'; |
|
} |
|
|
|
return column; |
|
}, |
|
|
|
/** |
|
* Detects the types of all columns in a dataset. |
|
* Parameters: |
|
* dataset - the dataset to test the columns of |
|
* parsedData - the data to check the type of |
|
*/ |
|
detectColumnTypes : function(dataset, parsedData) { |
|
|
|
_.each(parsedData, function(data, columnName) { |
|
|
|
var column = dataset.column( columnName ); |
|
|
|
// check if the column already has a type defined |
|
if ( column.type ) { |
|
column.force = true; |
|
return; |
|
} else { |
|
Dataset.Builder.detectColumnType(column, data); |
|
} |
|
|
|
}, this); |
|
}, |
|
|
|
/** |
|
* Used by internal importers to cache the rows |
|
* in quick lookup tables for any id based operations. |
|
* also used by views to cache the new rows they get |
|
* as a result of whatever filter created them. |
|
*/ |
|
cacheRows : function(dataset) { |
|
|
|
Dataset.Builder.clearRowCache(dataset); |
|
|
|
// cache the row id positions in both directions. |
|
// iterate over the _id column and grab the row ids |
|
_.each(dataset._columns[dataset._columnPositionByName[dataset.idAttribute]].data, function(id, index) { |
|
dataset._rowPositionById[id] = index; |
|
dataset._rowIdByPosition.push(id); |
|
}, dataset); |
|
|
|
// cache the total number of rows. There should be same |
|
// number in each column's data |
|
var rowLengths = _.uniq( _.map(dataset._columns, function(column) { |
|
return column.data.length; |
|
})); |
|
|
|
if (rowLengths.length > 1) { |
|
throw new Error("Row lengths need to be the same. Empty values should be set to null." + |
|
_.map(dataset._columns, function(c) { return c.data + "|||" ; })); |
|
} else { |
|
dataset.length = rowLengths[0]; |
|
} |
|
}, |
|
|
|
/** |
|
* Clears the row cache objects of a dataset |
|
* Parameters: |
|
* dataset - Miso.Dataset instance. |
|
*/ |
|
clearRowCache : function(dataset) { |
|
dataset._rowPositionById = {}; |
|
dataset._rowIdByPosition = []; |
|
}, |
|
|
|
/** |
|
* Caches the column positions by name |
|
* Parameters: |
|
* dataset - Miso.Dataset instance. |
|
*/ |
|
cacheColumns : function(dataset) { |
|
dataset._columnPositionByName = {}; |
|
_.each(dataset._columns, function(column, i) { |
|
dataset._columnPositionByName[column.name] = i; |
|
}); |
|
} |
|
}; |
|
|
|
// fix lack of IE indexOf. Again. |
|
if (!Array.prototype.indexOf) { |
|
Array.prototype.indexOf = function(obj, start) { |
|
for (var i = (start || 0), j = this.length; i < j; i++) { |
|
if (this[i] === obj) { return i; } |
|
} |
|
return -1; |
|
}; |
|
} |
|
|
|
}(this, _)); |
|
|
|
(function(global, _) { |
|
|
|
var Miso = global.Miso || (global.Miso = {}); |
|
var Dataset = global.Miso.Dataset; |
|
|
|
/** |
|
* A Miso.Product is a single computed value that can be obtained |
|
* from a Miso.Dataset. When a dataset is syncable, it will be an object |
|
* that one can subscribe to the changes of. Otherwise, it returns |
|
* the actual computed value. |
|
* Parameters: |
|
* func - the function that derives the computation. |
|
* columns - the columns from which the function derives the computation |
|
*/ |
|
Dataset.Product = function(options) { |
|
options = options || {}; |
|
|
|
// save column name. This will be necessary later |
|
// when we decide whether we need to update the column |
|
// when sync is called. |
|
this.func = options.func; |
|
|
|
// determine the product type (numeric, string, time etc.) |
|
if (options.columns) { |
|
var column = options.columns; |
|
if (_.isArray(options.columns)) { |
|
column = options.columns[0]; |
|
} |
|
|
|
this.valuetype = column.type; |
|
this.numeric = function() { |
|
return column.toNumeric(this.value); |
|
}; |
|
} |
|
|
|
this.func({ silent : true }); |
|
return this; |
|
}; |
|
|
|
_.extend(Dataset.Product.prototype, Miso.Events, { |
|
|
|
/** |
|
* return the raw value of the product |
|
* Returns: |
|
* The value of the product. Most likely a number. |
|
*/ |
|
val : function() { |
|
return this.value; |
|
}, |
|
|
|
/** |
|
* return the type of product this is (numeric, time etc.) |
|
* Returns |
|
* type. Matches the name of one of the Miso.types. |
|
*/ |
|
type : function() { |
|
return this.valuetype; |
|
}, |
|
|
|
//This is a callback method that is responsible for recomputing |
|
//the value based on the column its closed on. |
|
_sync : function(event) { |
|
this.func(); |
|
}, |
|
|
|
// builds a delta object. |
|
_buildDelta : function(old, changed) { |
|
return { |
|
old : old, |
|
changed : changed |
|
}; |
|
} |
|
}); |
|
|
|
Dataset.Product.define = function(func) { |
|
return function(columns, options) { |
|
options = options || {}; |
|
var columnObjects = this._findColumns(columns); |
|
var _self = this; |
|
options.type = options.type || columnObjects[0].type; |
|
options.typeOptions = options.typeOptions || columnObjects[0].typeOptions; |
|
|
|
//define wrapper function to handle coercion |
|
var producer = function() { |
|
var val = func.call(_self, columnObjects, options); |
|
return Dataset.types[options.type].coerce(val, options.typeOptions); |
|
}; |
|
|
|
if (this.syncable) { |
|
//create product object to pass back for syncable datasets/views |
|
var prod = new Dataset.Product({ |
|
columns : columnObjects, |
|
func : function(options) { |
|
options = options || {}; |
|
var delta = this._buildDelta(this.value, producer.call(_self)); |
|
this.value = delta.changed; |
|
if (_self.syncable) { |
|
var event = Dataset.Events._buildEvent(delta, this); |
|
if (!_.isUndefined(delta.old) && !options.silent && delta.old !== delta.changed) { |
|
this.publish("change", event); |
|
} |
|
} |
|
} |
|
}); |
|
this.subscribe("change", prod._sync, { context : prod }); |
|
return prod; |
|
|
|
} else { |
|
return producer.call(_self); |
|
} |
|
|
|
}; |
|
}; |
|
|
|
|
|
_.extend(Dataset.DataView.prototype, { |
|
|
|
// finds the column objects that match the single/multiple |
|
// input columns. Helper method. |
|
_findColumns : function(columns) { |
|
var columnObjects = []; |
|
|
|
// if no column names were specified, get all column names. |
|
if (_.isUndefined(columns)) { |
|
columns = this.columnNames(); |
|
} |
|
|
|
// convert columns to an array in case we only got one column name. |
|
columns = _.isArray(columns) ? columns : [columns]; |
|
|
|
// assemble actual column objecets together. |
|
_.each(columns, function(column) { |
|
column = this._columns[this._columnPositionByName[column]]; |
|
columnObjects.push(column); |
|
}, this); |
|
|
|
return columnObjects; |
|
}, |
|
|
|
/** |
|
* Computes the sum of one or more columns. |
|
* Parameters: |
|
* columns - string or array of column names on which the value is calculated |
|
* options |
|
* silent - set to tue to prevent event propagation |
|
*/ |
|
sum : Dataset.Product.define( function(columns, options) { |
|
_.each(columns, function(col) { |
|
if (col.type === Dataset.types.time.name) { |
|
throw new Error("Can't sum up time"); |
|
} |
|
}); |
|
return _.sum(_.map(columns, function(c) { return c._sum(); })); |
|
}), |
|
|
|
/** |
|
* return a Product with the value of the maximum |
|
* value of the column |
|
* Parameters: |
|
* column - string or array of column names on which the value is calculated |
|
*/ |
|
max : Dataset.Product.define( function(columns, options) { |
|
return _.max(_.map(columns, function(c) { |
|
return c._max(); |
|
})); |
|
}), |
|
|
|
|
|
/** |
|
* return a Product with the value of the minimum |
|
* value of the column |
|
* Paramaters: |
|
* columns - string or array of column names on which the value is calculated |
|
*/ |
|
min : Dataset.Product.define( function(columns, options) { |
|
return _.min(_.map(columns, function(c) { |
|
return c._min(); |
|
})); |
|
}), |
|
|
|
/** |
|
* return a Product with the value of the average |
|
* value of the column |
|
* Parameters: |
|
* column - string or array of column names on which the value is calculated |
|
*/ |
|
mean : Dataset.Product.define( function(columns, options) { |
|
var vals = []; |
|
_.each(columns, function(col) { |
|
vals.push(col.data); |
|
}); |
|
|
|
vals = _.flatten(vals); |
|
|
|
// save types and type options to later coerce |
|
var type = columns[0].type; |
|
|
|
// convert the values to their appropriate numeric value |
|
vals = _.map(vals, function(v) { return Dataset.types[type].numeric(v); }); |
|
return _.mean(vals); |
|
}) |
|
|
|
}); |
|
|
|
}(this, _)); |
|
|
|
|
|
(function(global, _) { |
|
|
|
var Dataset = global.Miso.Dataset; |
|
|
|
Dataset.Importers = function(data, options) {}; |
|
|
|
/** |
|
* Simple base extract method, passing data through |
|
* If your importer needs to extract the data from the |
|
* returned payload before converting it to |
|
* a dataset, overwrite this method to return the |
|
* actual data object. |
|
*/ |
|
Dataset.Importers.prototype.extract = function(data) { |
|
data = _.clone(data); |
|
return data; |
|
}; |
|
|
|
}(this, _)); |
|
|
|
(function(global, _) { |
|
|
|
var Dataset = global.Miso.Dataset; |
|
|
|
/** |
|
* Local data importer is responsible for just using |
|
* a data object and passing it appropriately. |
|
*/ |
|
Dataset.Importers.Local = function(options) { |
|
options = options || {}; |
|
|
|
this.data = options.data || null; |
|
this.extract = options.extract || this.extract; |
|
}; |
|
|
|
_.extend(Dataset.Importers.Local.prototype, Dataset.Importers.prototype, { |
|
fetch : function(options) { |
|
var data = options.data ? options.data : this.data; |
|
options.success( this.extract(data) ); |
|
} |
|
}); |
|
|
|
}(this, _)); |
|
|
|
(function(global, _) { |
|
|
|
var Dataset = global.Miso.Dataset; |
|
|
|
|
|
/** |
|
* A remote importer is responsible for fetching data from a url. |
|
* Parameters: |
|
* options |
|
* url - url to query |
|
* extract - a method to pass raw data through before handing back to parser. |
|
* dataType - ajax datatype |
|
* jsonp - true if it's a jsonp request, false otherwise. |
|
*/ |
|
Dataset.Importers.Remote = function(options) { |
|
options = options || {}; |
|
|
|
this._url = options.url; |
|
this.extract = options.extract || this.extract; |
|
|
|
// Default ajax request parameters |
|
this.params = { |
|
type : "GET", |
|
url : _.isFunction(this._url) ? _.bind(this._url, this) : this._url, |
|
dataType : options.dataType ? options.dataType : (options.jsonp ? "jsonp" : "json"), |
|
callback : options.callback |
|
}; |
|
}; |
|
|
|
_.extend(Dataset.Importers.Remote.prototype, Dataset.Importers.prototype, { |
|
fetch : function(options) { |
|
|
|
// call the original fetch method of object parsing. |
|
// we are assuming the parsed version of the data will |
|
// be an array of objects. |
|
var callback = _.bind(function(data) { |
|
options.success( this.extract(data) ); |
|
}, this); |
|
|
|
// do we have a named callback? We need to wrap our |
|
// success callback in this name |
|
if (this.callback) { |
|
window[this.callback] = callback; |
|
} |
|
|
|
// make ajax call to fetch remote url. |
|
Dataset.Xhr(_.extend(this.params, { |
|
success : this.callback ? this.callback : callback, |
|
error : options.error |
|
})); |
|
} |
|
}); |
|
|
|
// this XHR code is from @rwldron. |
|
var _xhrSetup = { |
|
url : "", |
|
data : "", |
|
dataType : "", |
|
success : function() {}, |
|
type : "GET", |
|
async : true, |
|
xhr : function() { |
|
return global.ActiveXObject ? new global.ActiveXObject("Microsoft.XMLHTTP") : new global.XMLHttpRequest(); |
|
} |
|
}, rparams = /\?/; |
|
|
|
Dataset.Xhr = function(options) { |
|
|
|
// json|jsonp etc. |
|
options.dataType = options.dataType && options.dataType.toLowerCase() || null; |
|
|
|
var url = _.isFunction(options.url) ? options.url() : options.url; |
|
|
|
if (options.dataType && |
|
(options.dataType === "jsonp" || options.dataType === "script" )) { |
|
|
|
Dataset.Xhr.getJSONP( |
|
url, |
|
options.success, |
|
options.dataType === "script", |
|
options.error, |
|
options.callback |
|
); |
|
|
|
return; |
|
} |
|
|
|
var settings = _.extend({}, _xhrSetup, options, { url : url }); |
|
|
|
// create new xhr object |
|
settings.ajax = settings.xhr(); |
|
|
|
if (settings.ajax) { |
|
if (settings.type === "GET" && settings.data) { |
|
|
|
// append query string |
|
settings.url += (rparams.test(settings.url) ? "&" : "?") + settings.data; |
|
|
|
// Garbage collect and reset settings.data |
|
settings.data = null; |
|
} |
|
|
|
settings.ajax.open(settings.type, settings.url, settings.async); |
|
settings.ajax.send(settings.data || null); |
|
|
|
return Dataset.Xhr.httpData(settings); |
|
} |
|
}; |
|
|
|
Dataset.Xhr.getJSONP = function(url, success, isScript, error, callback) { |
|
// If this is a script request, ensure that we do not |
|
// call something that has already been loaded |
|
if (isScript) { |
|
|
|
var scripts = document.querySelectorAll("script[src=\"" + url + "\"]"); |
|
|
|
// If there are scripts with this url loaded, early return |
|
if (scripts.length) { |
|
|
|
// Execute success callback and pass "exists" flag |
|
if (success) { |
|
success(true); |
|
} |
|
|
|
return; |
|
} |
|
} |
|
|
|
var head = document.head || |
|
document.getElementsByTagName("head")[0] || |
|
document.documentElement, |
|
|
|
script = document.createElement("script"), |
|
paramStr = url.split("?")[ 1 ], |
|
isFired = false, |
|
params = [], |
|
parts; |
|
|
|
// Extract params |
|
if (paramStr && !isScript) { |
|
params = paramStr.split("&"); |
|
} |
|
if (params.length) { |
|
parts = params[params.length - 1].split("="); |
|
} |
|
if (!callback) { |
|
var fallback = _.uniqueId('callback'); |
|
callback = params.length ? (parts[ 1 ] ? parts[ 1 ] : fallback) : fallback; |
|
} |
|
|
|
if (!paramStr && !isScript) { |
|
url += "?"; |
|
} |
|
|
|
if ( !paramStr || !/callback/.test(paramStr) ) { |
|
if (paramStr) { url += '&'; } |
|
url += "callback=" + callback; |
|
} |
|
|
|
if (callback && !isScript) { |
|
|
|
// If a callback name already exists |
|
if (!!window[callback]) { |
|
callback = callback + (+new Date()) + _.uniqueId(); |
|
} |
|
|
|
// Define the JSONP success callback globally |
|
window[callback] = function(data) { |
|
if (success) { |
|
success(data); |
|
} |
|
isFired = true; |
|
}; |
|
|
|
// Replace callback param and callback name |
|
if (parts) { |
|
url = url.replace(parts.join("="), parts[0] + "=" + callback); |
|
} |
|
} |
|
|
|
script.onload = script.onreadystatechange = function() { |
|
if (!script.readyState || /loaded|complete/.test(script.readyState)) { |
|
|
|
// Handling remote script loading callbacks |
|
if (isScript) { |
|
|
|
// getScript |
|
if (success) { |
|
success(); |
|
} |
|
} |
|
|
|
// Executing for JSONP requests |
|
if (isFired) { |
|
|
|
// Garbage collect the callback |
|
try { |
|
delete window[callback]; |
|
} catch(e) { |
|
window[callback] = void 0; |
|
} |
|
|
|
// Garbage collect the script resource |
|
head.removeChild(script); |
|
} |
|
} |
|
}; |
|
|
|
script.onerror = function(e) { |
|
if (error) { |
|
error.call(null, e); |
|
} |
|
}; |
|
|
|
script.src = url; |
|
head.insertBefore(script, head.firstChild); |
|
return; |
|
}; |
|
|
|
Dataset.Xhr.httpData = function(settings) { |
|
var data, json = null, handleResponse; |
|
|
|
handleResponse = function () { |
|
if (settings.ajax.readyState === 4) { |
|
try { |
|
json = JSON.parse(settings.ajax.responseText); |
|
} catch (e) { |
|
// suppress |
|
} |
|
|
|
data = { |
|
xml : settings.ajax.responseXML, |
|
text : settings.ajax.responseText, |
|
json : json |
|
}; |
|
|
|
if (settings.dataType) { |
|
data = data[settings.dataType]; |
|
} |
|
|
|
// if we got an ok response, call success, otherwise fail. |
|
if (/(2..)/.test(settings.ajax.status)) { |
|
settings.success.call(settings.ajax, data); |
|
} else { |
|
if (settings.error) { |
|
settings.error.call(null, settings.ajax.statusText); |
|
} |
|
} |
|
} |
|
}; |
|
|
|
if ( settings.ajax.readyState === 4 ) { |
|
handleResponse(); // Internet Exploder doesn't bother with readystatechange handlers for cached resources... trigger the handler manually |
|
} else { |
|
settings.ajax.onreadystatechange = handleResponse; |
|
} |
|
|
|
return data; |
|
}; |
|
|
|
}(this, _)); |
|
|
|
(function(global,_){ |
|
|
|
var Dataset = global.Miso.Dataset; |
|
|
|
/** |
|
* A remote polling importer that queries a url once every 1000 |
|
* seconds. |
|
* Parameters: |
|
* interval - poll every N milliseconds. Default is 1000. |
|
* extract - a method to pass raw data through before handing back to parser. |
|
*/ |
|
Dataset.Importers.Polling = function(options) { |
|
options = options || {}; |
|
this.interval = options.interval || 1000; |
|
this._def = null; |
|
|
|
Dataset.Importers.Remote.apply(this, [options]); |
|
}; |
|
|
|
_.extend(Dataset.Importers.Polling.prototype, Dataset.Importers.Remote.prototype, { |
|
fetch : function(options) { |
|
|
|
if (this._def === null) { |
|
|
|
this._def = _.Deferred(); |
|
|
|
// wrap success with deferred resolution |
|
this.success_callback = _.bind(function(data) { |
|
options.success(this.extract(data)); |
|
this._def.resolve(this); |
|
}, this); |
|
|
|
// wrap error with defered rejection |
|
this.error_callback = _.bind(function(error) { |
|
options.error(error); |
|
this._def.reject(error); |
|
}, this); |
|
} |
|
|
|
// on success, setTimeout another call |
|
_.when(this._def.promise()).then(function(importer) { |
|
|
|
var callback = _.bind(function() { |
|
this.fetch({ |
|
success : this.success_callback, |
|
error : this.error_callback |
|
}); |
|
}, importer); |
|
|
|
importer._timeout = setTimeout(callback, importer.interval); |
|
// reset deferred |
|
importer._def = _.Deferred(); |
|
}); |
|
|
|
Dataset.Xhr(_.extend(this.params, { |
|
success : this.success_callback, |
|
error : this.error_callback |
|
})); |
|
|
|
global.imp = this; |
|
}, |
|
|
|
stop : function() { |
|
if (this._def !== null) { |
|
this._def.reject(); |
|
} |
|
if (typeof this._timeout !== "undefined") { |
|
clearTimeout(this._timeout); |
|
} |
|
}, |
|
|
|
start : function() { |
|
if (this._def !== null) { |
|
this._def = _.Deferred(); |
|
this.fetch(); |
|
} |
|
} |
|
}); |
|
|
|
}(this, _)); |
|
|
|
(function(global, _) { |
|
|
|
var Dataset = global.Miso.Dataset; |
|
|
|
/** |
|
* Instantiates a new google spreadsheet importer. |
|
* Parameters |
|
* options - Options object. Requires at the very least: |
|
* key - the google spreadsheet key |
|
* gid - the index of the spreadsheet to be retrieved (1 default) |
|
* OR |
|
* sheetName - the name of the sheet to fetch ("Sheet1" default) |
|
* OR |
|
* url - a more complex url (that may include filtering.) In this case |
|
* make sure it's returning the feed json data. |
|
*/ |
|
Dataset.Importers.GoogleSpreadsheet = function(options) { |
|
options = options || {}; |
|
if (options.url) { |
|
|
|
options.url = options.url; |
|
|
|
} else { |
|
|
|
if (_.isUndefined(options.key)) { |
|
|
|
throw new Error("Set options 'key' properties to point to your google document."); |
|
} else { |
|
|
|
// turning on the "fast" option will use the farser parser |
|
// that downloads less data but it's flakier (due to google not |
|
// correctly escaping various strings when returning json.) |
|
if (options.fast) { |
|
|
|
options.url = "https://spreadsheets.google.com/tq?key=" + options.key; |
|
|
|
if (typeof options.sheetName === "undefined") { |
|
options.sheetName = "Sheet1"; |
|
} |
|
|
|
options.url += "&sheet=" + options.sheetName; |
|
this.callback = "misodsgs" + new Date().getTime(); |
|
options.url += "&tqx=version:0.6;responseHandler:" + this.callback; |
|
options.url += ";reqId:0;out:json&tq&_=1335871249558#"; |
|
|
|
delete options.sheetName; |
|
} else { |
|
options.url = "https://spreadsheets.google.com/feeds/cells/" + |
|
options.key + "/" + |
|
options.worksheet + |
|
"/public/basic?alt=json-in-script&callback="; |
|
} |
|
|
|
delete options.key; |
|
} |
|
} |
|
|
|
|
|
this.params = { |
|
type : "GET", |
|
url : options.url, |
|
dataType : "jsonp" |
|
}; |
|
|
|
return this; |
|
}; |
|
|
|
_.extend(Dataset.Importers.GoogleSpreadsheet.prototype, Dataset.Importers.Remote.prototype); |
|
|
|
}(this, _)); |
|
|
|
(function(global, _) { |
|
|
|
var Dataset = global.Miso.Dataset; |
|
|
|
/** |
|
* Base Miso.Parser class. |
|
*/ |
|
Dataset.Parsers = function( options ) { |
|
this.options = options || {}; |
|
}; |
|
|
|
_.extend(Dataset.Parsers.prototype, { |
|
|
|
//this is the main function for the parser, |
|
//it must return an object with the columns names |
|
//and the data in columns |
|
parse : function() {} |
|
|
|
}); |
|
}(this, _)); |
|
|
|
(function(global, _) { |
|
|
|
var Dataset = global.Miso.Dataset; |
|
|
|
/** |
|
* Strict format parser. |
|
* Handles basic strict data format. |
|
* Looks like: { |
|
* data : { |
|
* columns : [ |
|
* { name : colName, type : colType, data : [...] } |
|
* ] |
|
* } |
|
* } |
|
*/ |
|
Dataset.Parsers.Strict = function( options ) { |
|
this.options = options || {}; |
|
}; |
|
|
|
_.extend( Dataset.Parsers.Strict.prototype, Dataset.Parsers.prototype, { |
|
|
|
parse : function( data ) { |
|
var columnData = {}, columnNames = []; |
|
|
|
_.each(data.columns, function(column) { |
|
if (columnNames.indexOf(column.name) !== -1) { |
|
throw new Error("You have more than one column named \"" + column.name + "\""); |
|
} else { |
|
columnNames.push( column.name ); |
|
columnData[ column.name ] = column.data; |
|
} |
|
}); |
|
|
|
return { |
|
columns : columnNames, |
|
data : columnData |
|
}; |
|
} |
|
|
|
}); |
|
|
|
}(this, _)); |
|
|
|
(function(global, _) { |
|
|
|
var Dataset = global.Miso.Dataset; |
|
|
|
/** |
|
* Object parser |
|
* Converts an array of objects to strict format. |
|
* Each object is a flat json object of properties. |
|
*/ |
|
Dataset.Parsers.Obj = Dataset.Parsers; |
|
|
|
_.extend(Dataset.Parsers.Obj.prototype, Dataset.Parsers.prototype, { |
|
|
|
parse : function( data ) { |
|
var columns = _.keys(data[0]), |
|
columnData = {}; |
|
|
|
//create the empty arrays |
|
_.each(columns, function( key ) { |
|
columnData[ key ] = []; |
|
}); |
|
|
|
// iterate over properties in each row and add them |
|
// to the appropriate column data. |
|
_.each(columns, function( col ) { |
|
_.times(data.length, function( i ) { |
|
columnData[ col ].push( data[i][col] ); |
|
}); |
|
}); |
|
|
|
return { |
|
columns : columns, |
|
data : columnData |
|
}; |
|
} |
|
|
|
}); |
|
|
|
}(this, _)); |
|
|
|
// --------- Google Spreadsheet Parser ------- |
|
// |
|
|
|
(function(global, _) { |
|
|
|
var Dataset = global.Miso.Dataset; |
|
/** |
|
* Google Spreadsheet Parser. |
|
* This is utilizing the format that can be obtained using this: |
|
* http://code.google.com/apis/gdata/samples/spreadsheet_sample.html |
|
* Used in conjunction with the Google Spreadsheet Importer. |
|
*/ |
|
Dataset.Parsers.GoogleSpreadsheet = function(options) { |
|
this.fast = options.fast || false; |
|
}; |
|
|
|
_.extend(Dataset.Parsers.GoogleSpreadsheet.prototype, Dataset.Parsers.prototype, { |
|
|
|
parse : function(data) { |
|
var columns = [], |
|
columnData = [], |
|
keyedData = {}, |
|
i; |
|
|
|
// the fast importer API is not available |
|
if (typeof data.status !== "undefined" && data.status === "error") { |
|
throw new Error("You can't use the fast importer for this url. Disable the fast flag"); |
|
} |
|
|
|
if (this.fast) { |
|
|
|
// init column names |
|
columns = _.pluck(data.table.cols, "label"); |
|
|
|
// check that the column names don't have duplicates |
|
if (_.unique(columns).length < columns.length) { |
|
var dup = ""; |
|
|
|
_.inject(columns, function(memo, val) { |
|
|
|
memo[val] = (memo[val] + 1) || 1; |
|
if (memo[val] > 1) { |
|
dup = val; |
|
} |
|
return memo; |
|
}, {}); |
|
|
|
throw new Error("You have more than one column named \"" + dup + "\""); |
|
} |
|
|
|
// save data |
|
_.each(data.table.rows, function(row) { |
|
row = row.c; |
|
for(i = 0; i < row.length; i++) { |
|
columnData[i] = columnData[i] || []; |
|
if (row[i].v === "") { |
|
columnData[i].push(null); |
|
} else { |
|
columnData[i].push(row[i].v); |
|
} |
|
} |
|
}); |
|
|
|
// convert to keyed data. |
|
_.each(columns, function(colName, index) { |
|
keyedData[colName] = columnData[index]; |
|
}); |
|
|
|
} else { |
|
var positionRegex = /([A-Z]+)(\d+)/, |
|
columnPositions = {}; |
|
|
|
_.each(data.feed.entry, function(cell) { |
|
|
|
var parts = positionRegex.exec(cell.title.$t), |
|
column = parts[1], |
|
position = parseInt(parts[2], 10); |
|
|
|
// this is the first row, thus column names. |
|
if (position === 1) { |
|
|
|
// if we've already seen this column name, throw an exception |
|
if (columns.indexOf(cell.content.$t) !== -1) { |
|
throw new Error("You have more than one column named \"" + cell.content.$t + "\""); |
|
} else { |
|
// cache the column position |
|
columnPositions[column] = columnData.length; |
|
|
|
// we found a new column, so build a new column type. |
|
columns[columnPositions[column]] = cell.content.$t; |
|
columnData[columnPositions[column]] = []; |
|
} |
|
|
|
} else { |
|
|
|
// find position: |
|
var colpos = columnPositions[column]; |
|
|
|
// this is a value for an existing column, so push it. |
|
columnData[colpos][position-1] = cell.content.$t; |
|
|
|
} |
|
}, this); |
|
|
|
_.each(columnData, function(coldata, column) { |
|
// fill whatever empty spaces we might have in the data due to empty cells |
|
coldata.length = _.max(_.pluck(columnData, "length")); |
|
|
|
// slice off first space. It was alocated for the column name |
|
// and we've moved that off. |
|
coldata.splice(0,1); |
|
|
|
for (var i = 0; i < coldata.length; i++) { |
|
if (_.isUndefined(coldata[i]) || coldata[i] === "") { |
|
coldata[i] = null; |
|
} |
|
} |
|
|
|
keyedData[columns[column]] = coldata; |
|
}); |
|
|
|
} |
|
|
|
return { |
|
columns : columns, |
|
data : keyedData |
|
}; |
|
} |
|
|
|
}); |
|
}(this, _)); |
|
|
|
|
|
(function(global, _) { |
|
|
|
var Dataset = global.Miso.Dataset; |
|
|
|
/** |
|
* Delimited data parser. |
|
* Handles CSV and other delimited data. |
|
* Parameters: |
|
* options |
|
* delimiter : "," |
|
*/ |
|
Dataset.Parsers.Delimited = function(options) { |
|
options = options || {}; |
|
|
|
this.delimiter = options.delimiter || ","; |
|
|
|
this.skipRows = options.skipRows || 0; |
|
|
|
this.emptyValue = options.emptyValue || null; |
|
|
|
this.__delimiterPatterns = new RegExp( |
|
( |
|
// Delimiters. |
|
"(\\" + this.delimiter + "|\\r?\\n|\\r|^)" + |
|
|
|
// Quoted fields. |
|
"(?:\"([^\"]*(?:\"\"[^\"]*)*)\"|" + |
|
|
|
// Standard fields. |
|
"([^\"\\" + this.delimiter + "\\r\\n]*))" |
|
), |
|
"gi" |
|
); |
|
}; |
|
|
|
// Ie is not aware of trim method... |
|
if(typeof String.prototype.trim !== 'function') { |
|
String.prototype.trim = function() { |
|
return this.replace(/^\s+|\s+$/g, ''); |
|
}; |
|
} |
|
|
|
_.extend(Dataset.Parsers.Delimited.prototype, Dataset.Parsers.prototype, { |
|
|
|
parse : function(data) { |
|
var columns = [], |
|
columnData = {}, |
|
uniqueSequence = {}; |
|
|
|
var uniqueId = function(str) { |
|
if ( !uniqueSequence[str] ) { |
|
uniqueSequence[str] = 0; |
|
} |
|
var id = str + uniqueSequence[str]; |
|
uniqueSequence[str] += 1; |
|
return id; |
|
}; |
|
|
|
|
|
var parseCSV = function(delimiterPattern, strData, strDelimiter, skipRows, emptyValue) { |
|
|
|
// Check to see if the delimiter is defined. If not, |
|
// then default to comma. |
|
strDelimiter = (strDelimiter || ","); |
|
|
|
// Create an array to hold our individual pattern |
|
// matching groups. |
|
var arrMatches = null; |
|
|
|
// track how many columns we have. Once we reach a new line |
|
// mark a flag that we're done calculating that. |
|
var columnCount = 0; |
|
var columnCountComputed = false; |
|
|
|
// track which column we're on. Start with -1 because we increment it before |
|
// we actually save the value. |
|
var columnIndex = -1; |
|
|
|
// track which row we're on |
|
var rowIndex = 0; |
|
|
|
try { |
|
|
|
// trim any empty lines at the end |
|
strData = strData//.trim(); |
|
.replace(/\s+$/,"") |
|
.replace(/^[\r|\n|\s]+[\r|\n]/,"\n"); |
|
|
|
// do we have any rows to skip? if so, remove them from the string |
|
if (skipRows > 0) { |
|
var rowsSeen = 0, |
|
charIndex = 0, |
|
strLen = strData.length; |
|
|
|
while (rowsSeen < skipRows && charIndex < strLen) { |
|
if (/\n|\r|\r\n/.test(strData.charAt(charIndex))) { |
|
rowsSeen++; |
|
} |
|
charIndex++; |
|
} |
|
|
|
strData = strData.slice(charIndex, strLen); |
|
} |
|
|
|
// Keep looping over the regular expression matches |
|
// until we can no longer find a match. |
|
function matchHandler(arrMatches) { |
|
// Get the delimiter that was found. |
|
var strMatchedDelimiter = arrMatches[ 1 ]; |
|
|
|
// Check to see if the given delimiter has a length |
|
// (is not the start of string) and if it matches |
|
// field delimiter. If id does not, then we know |
|
// that this delimiter is a row delimiter. |
|
if ( strMatchedDelimiter.length && |
|
( strMatchedDelimiter !== strDelimiter )){ |
|
|
|
// we have reached a new row. |
|
rowIndex++; |
|
|
|
// if we caught less items than we expected, throw an error |
|
if (columnIndex < columnCount-1) { |
|
rowIndex--; |
|
throw new Error("Not enough items in row"); |
|
} |
|
|
|
// We are clearly done computing columns. |
|
columnCountComputed = true; |
|
|
|
// when we're done with a row, reset the row index to 0 |
|
columnIndex = 0; |
|
|
|
} else { |
|
|
|
// Find the number of columns we're fetching and |
|
// create placeholders for them. |
|
if (!columnCountComputed) { |
|
columnCount++; |
|
} |
|
|
|
columnIndex++; |
|
} |
|
|
|
|
|
// Now that we have our delimiter out of the way, |
|
// let's check to see which kind of value we |
|
// captured (quoted or unquoted). |
|
var strMatchedValue = null; |
|
if (arrMatches[ 2 ]){ |
|
|
|
// We found a quoted value. When we capture |
|
// this value, unescape any double quotes. |
|
strMatchedValue = arrMatches[ 2 ].replace( |
|
new RegExp( "\"\"", "g" ), |
|
"\"" |
|
); |
|
|
|
} else { |
|
|
|
// We found a non-quoted value. |
|
strMatchedValue = arrMatches[ 3 ]; |
|
} |
|
|
|
|
|
// Now that we have our value string, let's add |
|
// it to the data array. |
|
|
|
if (columnCountComputed) { |
|
|
|
if (strMatchedValue === '') { |
|
strMatchedValue = emptyValue; |
|
} |
|
|
|
if (typeof columnData[columns[columnIndex]] === "undefined") { |
|
throw new Error("Too many items in row"); |
|
} |
|
|
|
columnData[columns[columnIndex]].push(strMatchedValue); |
|
|
|
} else { |
|
|
|
var createColumnName = function(start) { |
|
var newName = uniqueId(start); |
|
while ( columns.indexOf(newName) !== -1 ) { |
|
newName = uniqueId(start); |
|
} |
|
return newName; |
|
}; |
|
|
|
//No column name? Create one starting with X |
|
if ( _.isUndefined(strMatchedValue) || strMatchedValue === '' ) { |
|
strMatchedValue = 'X'; |
|
} |
|
|
|
//Duplicate column name? Create a new one starting with the name |
|
if (columns.indexOf(strMatchedValue) !== -1) { |
|
strMatchedValue = createColumnName(strMatchedValue); |
|
} |
|
|
|
// we are building the column names here |
|
columns.push(strMatchedValue); |
|
columnData[strMatchedValue] = []; |
|
} |
|
} |
|
|
|
//missing column header at start |
|
if ( new RegExp('^' + strDelimiter).test(strData) ) { |
|
matchHandler(['','',undefined,'']); |
|
} |
|
while (arrMatches = delimiterPattern.exec(strData)) { |
|
matchHandler(arrMatches); |
|
} // end while |
|
} catch (e) { |
|
throw new Error("Error while parsing delimited data on row " + rowIndex + ". Message: " + e.message); |
|
} |
|
|
|
|
|
|
|
// Return the parsed data. |
|
return { |
|
columns : columns, |
|
data : columnData |
|
}; |
|
}; |
|
|
|
return parseCSV( |
|
this.__delimiterPatterns, |
|
data, |
|
this.delimiter, |
|
this.skipRows, |
|
this.emptyValue); |
|
} |
|
|
|
}); |
|
|
|
|
|
}(this, _)); |
|
|
|
(function(global, _) { |
|
|
|
var Miso = global.Miso || (global.Miso = {}); |
|
var Dataset = Miso.Dataset; |
|
|
|
/** |
|
* A Miso.Derived dataset is a regular dataset that has been derived |
|
* through some computation from a parent dataset. It behaves just like |
|
* a regular dataset except it also maintains a reference to its parent |
|
* and the method that computed it. |
|
* Parameters: |
|
* options |
|
* parent - the parent dataset |
|
* method - the method by which this derived dataset was computed |
|
* Returns |
|
* a derived dataset instance |
|
*/ |
|
|
|
Dataset.Derived = function(options) { |
|
options = options || {}; |
|
|
|
Dataset.call(this); |
|
|
|
// save parent dataset reference |
|
this.parent = options.parent; |
|
|
|
// the id column in a derived dataset is always _id |
|
// since there might not be a 1-1 mapping to each row |
|
// but could be a 1-* mapping at which point a new id |
|
// is needed. |
|
this.idAttribute = "_id"; |
|
|
|
// save the method we apply to bins. |
|
this.method = options.method; |
|
|
|
this._addIdColumn(); |
|
|
|
this.addColumn({ |
|
name : "_oids", |
|
type : "mixed" |
|
}); |
|
|
|
if (this.parent.syncable) { |
|
_.extend(this, Miso.Events); |
|
this.syncable = true; |
|
this.parent.subscribe("change", this._sync, { context : this }); |
|
} |
|
}; |
|
|
|
// take in dataset's prototype. |
|
Dataset.Derived.prototype = new Dataset(); |
|
|
|
// inherit all of dataset's methods. |
|
_.extend(Dataset.Derived.prototype, { |
|
_sync : function() { |
|
// recompute the function on an event. |
|
// TODO: would be nice to be more clever about this at some point. |
|
this.func.call(this.args); |
|
this.publish("change"); |
|
} |
|
}); |
|
|
|
|
|
// add derived methods to dataview (and thus dataset & derived) |
|
_.extend(Dataset.DataView.prototype, { |
|
|
|
/** |
|
* moving average |
|
* Parameters: |
|
* column - The column on which to calculate the average |
|
* size - The window size to utilize for the moving average |
|
* options |
|
* method - the method to apply to all values in a window. Mean by default. |
|
* Returns: |
|
* a miso.derived dataset instance |
|
*/ |
|
movingAverage : function(columns, size, options) { |
|
|
|
options = options || {}; |
|
|
|
var d = new Dataset.Derived({ |
|
parent : this, |
|
method : options.method || _.mean, |
|
size : size, |
|
args : arguments |
|
}); |
|
|
|
// copy over all columns |
|
this.eachColumn(function(columnName) { |
|
|
|
// don't try to compute a moving average on the id column. |
|
if (columnName === this.idAttribute) { |
|
throw "You can't compute a moving average on the id column"; |
|
} |
|
|
|
d.addColumn({ |
|
name : columnName, type : this.column(columnName).type, data : [] |
|
}); |
|
}, this); |
|
|
|
// save column positions on new dataset. |
|
Dataset.Builder.cacheColumns(d); |
|
|
|
// apply with the arguments columns, size, method |
|
var computeMovingAverage = function() { |
|
|
|
// normalize columns arg - if single string, to array it. |
|
if (typeof columns === "string") { |
|
columns = [columns]; |
|
} |
|
|
|
// copy the ids |
|
this.column(this.idAttribute).data = this.parent |
|
.column(this.parent.idAttribute) |
|
.data.slice(size-1, this.parent.length); |
|
|
|
// copy the columns we are NOT combining minus the sliced size. |
|
this.eachColumn(function(columnName, column) { |
|
if (columns.indexOf(columnName) === -1 && columnName !== "_oids") { |
|
// copy data |
|
column.data = this.parent.column(columnName).data.slice(size-1, this.parent.length); |
|
} else { |
|
// compute moving average for each column and set that as the data |
|
column.data = _.movingAvg(this.parent.column(columnName).data, size, this.method); |
|
} |
|
}, this); |
|
|
|
this.length = this.parent.length - size + 1; |
|
|
|
// generate oids for the oid col |
|
var oidcol = this.column("_oids"); |
|
oidcol.data = []; |
|
for(var i = 0; i < this.length; i++) { |
|
oidcol.data.push(this.parent.column(this.parent.idAttribute).data.slice(i, i+size)); |
|
} |
|
|
|
Dataset.Builder.cacheRows(this); |
|
|
|
return this; |
|
}; |
|
|
|
d.func = _.bind(computeMovingAverage, d); |
|
return d.func.call(d.args); |
|
}, |
|
|
|
/** |
|
* Group rows by the column passed and return a column with the |
|
* counts of the instance of each value in the column passed. |
|
*/ |
|
countBy : function(byColumn, options) { |
|
|
|
options = options || {}; |
|
var d = new Dataset.Derived({ |
|
parent : this, |
|
method : _.sum, |
|
args : arguments |
|
}); |
|
|
|
var parentByColumn = this.column(byColumn); |
|
//add columns |
|
d.addColumn({ |
|
name : byColumn, |
|
type : parentByColumn.type |
|
}); |
|
|
|
d.addColumn({ name : 'count', type : 'number' }); |
|
d.addColumn({ name : '_oids', type : 'mixed' }); |
|
Dataset.Builder.cacheColumns(d); |
|
|
|
var names = d.column(byColumn).data, |
|
values = d.column('count').data, |
|
_oids = d.column('_oids').data, |
|
_ids = d.column(d.idAttribute).data; |
|
|
|
function findIndex(names, datum, type) { |
|
var i; |
|
for(i = 0; i < names.length; i++) { |
|
if (Dataset.types[type].compare(names[i], datum) === 0) { |
|
return i; |
|
} |
|
} |
|
return -1; |
|
} |
|
|
|
this.each(function(row) { |
|
var index = findIndex(names, row[byColumn], parentByColumn.type); |
|
if ( index === -1 ) { |
|
names.push( row[byColumn] ); |
|
_ids.push( _.uniqueId() ); |
|
values.push( 1 ); |
|
_oids.push( [row[this.parent.idAttribute]] ); |
|
} else { |
|
values[index] += 1; |
|
_oids[index].push( row[this.parent.idAttribute]); |
|
} |
|
}, d); |
|
|
|
Dataset.Builder.cacheRows(d); |
|
return d; |
|
}, |
|
|
|
/** |
|
* group rows by values in a given column |
|
* Parameters: |
|
* byColumn - The column by which rows will be grouped (string) |
|
* columns - The columns to be included (string array of column names) |
|
* options |
|
* method - function to be applied, default is sum |
|
* preprocess - specify a normalization function for the |
|
* byColumn values if you need to group by some kind of |
|
* derivation of those values that are not just equality based. |
|
* Returns: |
|
* a miso.derived dataset instance |
|
*/ |
|
groupBy : function(byColumn, columns, options) { |
|
|
|
options = options || {}; |
|
|
|
var d = new Dataset.Derived({ |
|
|
|
// save a reference to parent dataset |
|
parent : this, |
|
|
|
// default method is addition |
|
method : options.method || _.sum, |
|
|
|
// save current arguments |
|
args : arguments |
|
}); |
|
|
|
if (options && options.preprocess) { |
|
d.preprocess = options.preprocess; |
|
} |
|
|
|
// copy columns we want - just types and names. No data. |
|
var newCols = _.union([byColumn], columns); |
|
|
|
_.each(newCols, function(columnName) { |
|
|
|
this.addColumn({ |
|
name : columnName, |
|
type : this.parent.column(columnName).type |
|
}); |
|
}, d); |
|
|
|
// save column positions on new dataset. |
|
Dataset.Builder.cacheColumns(d); |
|
|
|
// will get called with all the arguments passed to this |
|
// host function |
|
var computeGroupBy = function() { |
|
|
|
var self = this; |
|
|
|
// clear row cache if it exists |
|
Dataset.Builder.clearRowCache(this); |
|
|
|
// a cache of values |
|
var categoryPositions = {}, |
|
categoryCount = 0, |
|
originalByColumn = this.parent.column(byColumn); |
|
|
|
// bin all values by their |
|
for(var i = 0; i < this.parent.length; i++) { |
|
var category = null; |
|
|
|
// compute category. If a pre-processing function was specified |
|
// (for binning time for example,) run that first. |
|
if (this.preprocess) { |
|
category = this.preprocess(originalByColumn.data[i]); |
|
} else { |
|
category = originalByColumn.data[i]; |
|
} |
|
|
|
if (_.isUndefined(categoryPositions[category])) { |
|
|
|
// this is a new value, we haven't seen yet so cache |
|
// its position for lookup of row vals |
|
categoryPositions[category] = categoryCount; |
|
|
|
// add an empty array to all columns at that position to |
|
// bin the values |
|
_.each(columns, function(columnToGroup) { |
|
var column = this.column(columnToGroup); |
|
var idCol = this.column(this.idAttribute); |
|
column.data[categoryCount] = []; |
|
idCol.data[categoryCount] = _.uniqueId(); |
|
}, this); |
|
|
|
// add the actual bin number to the right col |
|
this.column(byColumn).data[categoryCount] = category; |
|
|
|
categoryCount++; |
|
} |
|
|
|
_.each(columns, function(columnToGroup) { |
|
|
|
var column = this.column(columnToGroup), |
|
binPosition = categoryPositions[category]; |
|
|
|
column.data[binPosition].push(this.parent.rowByPosition(i)); |
|
}, this); |
|
} |
|
|
|
// now iterate over all the bins and combine their |
|
// values using the supplied method. |
|
var oidcol = this._columns[this._columnPositionByName._oids]; |
|
oidcol.data = []; |
|
|
|
_.each(columns, function(colName) { |
|
var column = this.column(colName); |
|
|
|
_.each(column.data, function(bin, binPos) { |
|
if (_.isArray(bin)) { |
|
|
|
// save the original ids that created this group by? |
|
oidcol.data[binPos] = oidcol.data[binPos] || []; |
|
oidcol.data[binPos].push(_.map(bin, function(row) { return row[self.parent.idAttribute]; })); |
|
oidcol.data[binPos] = _.flatten(oidcol.data[binPos]); |
|
|
|
// compute the final value. |
|
column.data[binPos] = this.method(_.map(bin, function(row) { return row[colName]; })); |
|
this.length++; |
|
} |
|
}, this); |
|
|
|
}, this); |
|
|
|
Dataset.Builder.cacheRows(this); |
|
return this; |
|
}; |
|
|
|
// bind the recomputation function to the dataset as the context. |
|
d.func = _.bind(computeGroupBy, d); |
|
|
|
return d.func.call(d.args); |
|
} |
|
}); |
|
|
|
}(this, _)); |