Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
  • 4 commits
  • 8 files changed
  • 0 comments
  • 1 contributor
4  static/index.html
@@ -7,6 +7,8 @@
7 7
         <script src="js/jquery-1.9.1.js"></script>
8 8
         <script src="js/angular.js"></script>
9 9
         <script src="js/underscore.js"></script>
  10
+        <script src="js/moment.js"></script>
  11
+        <script src="js/moment-range.js"></script>
10 12
         <script src="js/bootstrap.js"></script>
11 13
         <link rel="stylesheet" href="css/bootstrap.css">
12 14
         <!-- App -->
@@ -53,7 +55,7 @@ <h2 class="form-signin-heading">Please sign in</h2>
53 55
                             <a class="brand" ng-href="#!/">Security Dashboard</a>
54 56
                             <div class="nav-collapse collapse">
55 57
                                 <p class="navbar-text pull-right">
56  
-                                    {{username}} - <a href="#" class="navbar-link" ng-click="logout()">Logout</a>
  58
+                                    {{username}} - <a class="navbar-link" ng-click="logout()">Logout</a>
57 59
                                 </p>
58 60
                                 <ul class="nav">
59 61
                                     <li class="{active: dashboard == 'websecbugs'}"><a ng-href="#!/websecbugs">WebSec Bugs</a></li>
23  static/js/dashboard.js
@@ -140,15 +140,25 @@ app.factory('bugzillaService', function ($rootScope, $http, sessionService)
140 140
 
141 141
     sharedBugzillaService.cleanupBug = function(bug) {
142 142
         if (bug.creation_time) {
143  
-            bug.creation_time = Date.parse(bug.creation_time);
  143
+            bug.creation_time = moment(bug.creation_time);
144 144
         }
145 145
 
146 146
         if (bug.last_change_time) {
147  
-            bug.last_change_time = Date.parse(bug.last_change_time);
  147
+            bug.last_change_time = moment(bug.last_change_time);
  148
+        }
  149
+
  150
+        if (bug.history) {
  151
+            _.each(bug.history, function (event) {
  152
+                event.change_time = moment(event.change_time);
  153
+            });
  154
+        }
  155
+
  156
+        if (!bug.depends_on) {
  157
+            bug.depends_on = [];
148 158
         }
149 159
 
150 160
         // TODO This should move to some utility functions instead
151  
-        bug.age = Math.floor((Date.now() - bug.creation_time) / (24 * 60 * 60 * 1000));
  161
+        bug.age = moment().diff(bug.creation_time, 'days'); // Math.floor((Date.now() - bug.creation_time) / (24 * 60 * 60 * 1000));
152 162
 
153 163
         bug.ageLabel = "default";
154 164
         if (bug.age < 7) {
@@ -257,6 +267,13 @@ app.factory('bugzillaService', function ($rootScope, $http, sessionService)
257 267
     return sharedBugzillaService;
258 268
 });
259 269
 
  270
+app.filter('moment', function () {
  271
+    console.log("FILTER MOMENT");
  272
+    return function(input, format) {
  273
+        console.log("FILTERING MOMENT WITH FORMAT", input, format);
  274
+        return moment(input).format(format);
  275
+    };
  276
+});
260 277
 
261 278
 
262 279
 
228  static/js/dashboard/kickoff.js
@@ -2,8 +2,14 @@
2 2
 
3 3
 app.controller('KickoffController', function ($scope, $http, bugzillaService) {
4 4
 
  5
+    $scope.tab = "open";
5 6
     $scope.loading = true;
6 7
 
  8
+    $scope.showTab = function(tabName)
  9
+    {
  10
+        $scope.tab = tabName;
  11
+    };
  12
+
7 13
     // TODO Do not filter on blocker bugs that are resolved
8 14
 
9 15
     var filterByStatus = function(bug) {
@@ -16,7 +22,7 @@ app.controller('KickoffController', function ($scope, $http, bugzillaService) {
16 22
 
17 23
     var filterByProduct = function(product) {
18 24
         return function(bug) {
19  
-            bug.product === product;
  25
+            return bug.product === product;
20 26
         };
21 27
     };
22 28
 
@@ -24,12 +30,13 @@ app.controller('KickoffController', function ($scope, $http, bugzillaService) {
24 30
         return function(bug) {
25 31
             for (var i = 0; i < bug.depends_on.length; i++) {
26 32
                 var blockingBug = bug.depends_on[i];
27  
-                if (blockingBug.status == "NEW" || blockingBug.status == "REOPENED") {
  33
+                if (blockingBug.status == "NEW" || blockingBug.status == "REOPENED" || blockingBug.status == "ASSIGNED") {
28 34
                     if (blockingBug.product === product && blockingBug.component === component) {
29 35
                         return true;
30 36
                     }
31 37
                 }
32 38
             }
  39
+            return undefined;
33 40
         };
34 41
     };
35 42
 
@@ -71,6 +78,191 @@ app.controller('KickoffController', function ($scope, $http, bugzillaService) {
71 78
         $scope.filter = what;
72 79
     };
73 80
 
  81
+
  82
+
  83
+
  84
+    var shortStatus = function(bug) {
  85
+        switch (bug.status) {
  86
+            case "NEW":
  87
+                return {status: "NEW", color: "info"};
  88
+                break;
  89
+            case "RESOLVED":
  90
+                return {status: bug.resolution.substr(0,3), color: "default"};
  91
+                break;
  92
+            case "VERIFIED":
  93
+                return {status: bug.resolution.substr(0,3), color: "default"};
  94
+                break;
  95
+            case "REOPENED":
  96
+                return {status: "NEW", color: "info"};
  97
+                break;
  98
+            case "ASSIGNED":
  99
+                return {status: "ASS", color: "info"};
  100
+                break;
  101
+        }
  102
+        return {status: "UNK", color: "default"};
  103
+    };
  104
+
  105
+
  106
+
  107
+
  108
+    // Calculate statistics
  109
+
  110
+    var teamForBug = function (bug) {
  111
+        if (bug.product == 'mozilla.org' && bug.component == 'Security Assurance: Review Request') {
  112
+            return 'security';
  113
+        }
  114
+        if (bug.product === 'Legal') {
  115
+            return 'legal';
  116
+        }
  117
+        if (bug.product === 'Privacy') {
  118
+            return 'privacy';
  119
+        }
  120
+        if (bug.product === 'Data Safety') {
  121
+            return 'data';
  122
+        }
  123
+        if (bug.product === 'Finance') {
  124
+            return 'finance';
  125
+        }
  126
+        return 'other';
  127
+    };
  128
+
  129
+    var createKickoffStats = function (bugs, quarter) {
  130
+        return {};
  131
+    };
  132
+
  133
+    var createProjectStats = function (bugs) {
  134
+        var projectStats = { total: 0, open: 0, resolved: 0 };
  135
+        _.each(bugs, function (bug) {
  136
+            projectStats.total++;
  137
+            if (_.contains(['NEW', 'REOPENED', 'ASSIGNED'], bug.status)) {
  138
+                projectStats.open++;
  139
+            } else if (_.contains(['RESOLVED', 'VERIFIED'], bug.status)) {
  140
+                projectStats.resolved++;
  141
+            }
  142
+        });
  143
+        return projectStats;
  144
+    };
  145
+
  146
+    var isProjectReviewBug = function (bug) {
  147
+        return bug.product === 'mozilla.org' && bug.component == 'Project Review';
  148
+    };
  149
+
  150
+    var warpBugToDate = function (bug, date)
  151
+    {
  152
+        // TODO Find out how to clone something in JavaScript
  153
+        var warpedBug = {
  154
+            id: bug.id,
  155
+            summary: bug.summary,
  156
+            status: bug.status,
  157
+            resolution: bug.resolution,
  158
+            depends_on: bug.depends_on,
  159
+            creation_time: bug.creation_time,
  160
+            product: bug.product,
  161
+            component: bug.component
  162
+        };
  163
+
  164
+        var warpedHistory = [];
  165
+        _.chain(bug.history).reverse().each(function (event) {
  166
+            if (event.change_time >= date) {
  167
+                _.each(event.changes, function (change) {
  168
+                    switch (change.field_name) {
  169
+                        case 'status':
  170
+                            warpedBug.status = change.removed;
  171
+                            break;
  172
+                        case 'resolution':
  173
+                            warpedBug.resolution = change.removed;
  174
+                            break;
  175
+                        case 'summary':
  176
+                            warpedBug.summary = change.remove;
  177
+                            break;
  178
+                    }
  179
+                });
  180
+            } else {
  181
+                warpedHistory.splice(0, 0, event);
  182
+            }
  183
+        });
  184
+        warpedBug.history = warpedHistory;
  185
+
  186
+        return warpedBug;
  187
+    };
  188
+
  189
+    var warpBugsToDate = function (bugs, date) {
  190
+        return _.chain(bugs)
  191
+            //.filter(function (bug) { return bug.creation_time <= date; })
  192
+            .map(function (bug) { return warpBugToDate(bug, date); })
  193
+            .value();
  194
+    };
  195
+
  196
+    var filterBugsByCreationDateRange = function (bugs, range) {
  197
+        return _.filter(bugs, function (bug) {
  198
+            return bug.creation_time >= range.start && bug.creation_time <= range.end;
  199
+        });
  200
+    };
  201
+
  202
+    var createQuarterlyStats = function (projectBugs, teamBugs) {
  203
+        var dateRanges = [
  204
+            // TODO Generate these from first known quarter till now.
  205
+            moment().range(moment("2012-10-01 00:00:00"), moment("2012-12-31 23:59:59")),
  206
+            moment().range(moment("2013-01-01 00:00:00"), moment("2013-03-31 23:59:59")),
  207
+            moment().range(moment("2013-04-01 00:00:00"), moment("2013-06-31 23:59:59"))
  208
+        ];
  209
+        return _.map(dateRanges, function (range) {
  210
+            var projectStats = createProjectStats(filterBugsByCreationDateRange(warpBugsToDate(projectBugs, range.end), range));
  211
+            var teamStats = createTeamStats(filterBugsByCreationDateRange(warpBugsToDate(projectBugs, range.end), range),
  212
+                                            filterBugsByCreationDateRange(warpBugsToDate(teamBugs, range.end), range));
  213
+            return { range: range, projectStats: projectStats, teamStats: teamStats };
  214
+        });
  215
+    };
  216
+
  217
+    var createTeamStats = function (projectBugs, teamBugs)
  218
+    {
  219
+        var teamStats = [];
  220
+        var teamStatsByTeam = {};
  221
+        _.each(['security', 'legal', 'privacy', 'data', 'finance', 'other', 'hidden'], function (team) {
  222
+            var stats = { team: team, total: 0, open: 0, resolved_fixed: 0, resolved_invalid: 0, resolved_other: 0 };
  223
+            teamStats.push(stats);
  224
+            teamStatsByTeam[team] = stats;
  225
+        });
  226
+
  227
+        var teamBugsById = {};
  228
+        _.each(teamBugs, function (teamBug) {
  229
+            teamBugsById[teamBug.id] = teamBug;
  230
+        });
  231
+
  232
+        _.each(projectBugs, function (projectBug) {
  233
+            _.each(projectBug.depends_on, function (teamBug) {
  234
+                if (teamBug.status) {
  235
+                    var team = teamForBug(teamBug);
  236
+                    teamStatsByTeam[team].total++;
  237
+                    if (_.contains(['NEW', 'REOPENED', 'ASSIGNED'], teamBug.status)) {
  238
+                        teamStatsByTeam[team].open++;
  239
+                    } else if (_.contains(['RESOLVED', 'VERIFIED'], teamBug.status)) {
  240
+                        switch (teamBug.resolution) {
  241
+                            case 'FIXED':
  242
+                                teamStatsByTeam[team].resolved_fixed++;
  243
+                                break;
  244
+                            case 'INVALID':
  245
+                                teamStatsByTeam[team].resolved_invalid++;
  246
+                                break;
  247
+                            default:
  248
+                                teamStatsByTeam[team].resolved_other++;
  249
+                        }
  250
+                    }
  251
+                } else {
  252
+                    teamStatsByTeam['hidden'].total++;
  253
+                }
  254
+            });
  255
+        });
  256
+
  257
+        return teamStats;
  258
+    };
  259
+
  260
+
  261
+
  262
+
  263
+
  264
+
  265
+
74 266
     $scope.reload = function()
75 267
     {
76 268
         $scope.bugs = [];
@@ -83,7 +275,7 @@ app.controller('KickoffController', function ($scope, $http, bugzillaService) {
83 275
             component:"Project Review",
84 276
             product:"mozilla.org",
85 277
             //status: ["NEW", "REOPENED"],
86  
-            include_fields:"id,status,summary,depends_on,creation_time"
  278
+            include_fields:"id,status,summary,depends_on,creation_time,history,resolution"
87 279
         };
88 280
 
89 281
         var startTime = Date.now();
@@ -105,39 +297,20 @@ app.controller('KickoffController', function ($scope, $http, bugzillaService) {
105 297
 
106 298
                 var options = {
107 299
                     id: blockingBugIds.join(","),
108  
-                    include_fields:"id,status,summary,product,component,resolution"
109  
-                }
  300
+                    include_fields:"id,creation_time,status,summary,product,component,resolution,depends_on,history"
  301
+                };
110 302
 
111 303
                 bugzillaService.getBugs(options)
112 304
                     .success(function(data) {
113 305
                         console.log("Loading bugs took ", (Date.now() - startTime) / 1000.0);
114 306
 
115 307
                         // Store all the blockers in a map
  308
+                        $scope.blockingBugs = data.bugs;
116 309
                         _.each(data.bugs, function (bug) {
  310
+                            bugzillaService.cleanupBug(bug);
117 311
                             $scope.blockingBugsById[bug.id] = bug;
118 312
                         });
119 313
 
120  
-                        var shortStatus = function(bug) {
121  
-                            switch (bug.status) {
122  
-                                case "NEW":
123  
-                                  return {status: "NEW", color: "info"};
124  
-                                  break;
125  
-                                case "RESOLVED":
126  
-                                  return {status: bug.resolution.substr(0,3), color: "default"};
127  
-                                  break;
128  
-                                case "VERIFIED":
129  
-                                  return {status: bug.resolution.substr(0,3), color: "default"};
130  
-                                  break;
131  
-                                case "REOPENED":
132  
-                                  return {status: "NEW", color: "info"};
133  
-                                  break;
134  
-                                case "ASSIGNED":
135  
-                                  return {status: "ASS", color: "info"};
136  
-                                  break;
137  
-                            }
138  
-                            return {status: "UNK", color: "default"};
139  
-                        };
140  
-
141 314
                         // Loop over all review bugs and replace the dependend bug numbers with real bug records
142 315
                         _.each($scope.projectReviewBugs, function (bug) {
143 316
                             _.each(bug.depends_on, function (blockingBugId, idx) {
@@ -154,8 +327,9 @@ app.controller('KickoffController', function ($scope, $http, bugzillaService) {
154 327
                         // Display all bugs by default
155 328
                         $scope.loading = false;
156 329
                         $scope.filterBy('all');
157  
-                    });
158 330
 
  331
+                        $scope.quarterlyStats = createQuarterlyStats($scope.projectReviewBugs, $scope.blockingBugs);
  332
+                    });
159 333
             })
160 334
             .error(function(data, status, headers, config) {
161 335
                 console.log("Error getting bugs", data, status);
141  static/js/moment-range.js
... ...
@@ -0,0 +1,141 @@
  1
+// Generated by CoffeeScript 1.5.0
  2
+(function() {
  3
+  var DateRange, moment;
  4
+
  5
+  moment = typeof require !== "undefined" && require !== null ? require("moment") : this.moment;
  6
+
  7
+  /**
  8
+    * DateRange class to store ranges and query dates.
  9
+    * @typedef {!Object}
  10
+  *
  11
+  */
  12
+
  13
+
  14
+  DateRange = (function() {
  15
+    /**
  16
+      * DateRange instance.
  17
+      * @param {(Moment|Date)} start Start of interval.
  18
+      * @param {(Moment|Date)} end   End of interval.
  19
+      * @constructor
  20
+    *
  21
+    */
  22
+
  23
+    function DateRange(start, end) {
  24
+      this.start = start;
  25
+      this.end = end;
  26
+    }
  27
+
  28
+    /**
  29
+      * Determine if the current interval contains a given moment/date.
  30
+      * @param {(Moment|Date)} moment Date to check.
  31
+      * @return {!boolean}
  32
+    *
  33
+    */
  34
+
  35
+
  36
+    DateRange.prototype.contains = function(moment) {
  37
+      return (this.start <= moment && moment <= this.end);
  38
+    };
  39
+
  40
+    DateRange.prototype._by_string = function(interval, hollaback) {
  41
+      var current, _results;
  42
+      current = moment(this.start);
  43
+      _results = [];
  44
+      while (this.contains(current)) {
  45
+        hollaback.call(this, current.clone());
  46
+        _results.push(current.add(interval, 1));
  47
+      }
  48
+      return _results;
  49
+    };
  50
+
  51
+    DateRange.prototype._by_range = function(range_interval, hollaback) {
  52
+      var i, l, _i, _results;
  53
+      l = Math.round(this / range_interval);
  54
+      if (l === Infinity) {
  55
+        return this;
  56
+      }
  57
+      _results = [];
  58
+      for (i = _i = 0; 0 <= l ? _i <= l : _i >= l; i = 0 <= l ? ++_i : --_i) {
  59
+        _results.push(hollaback.call(this, moment(this.start.valueOf() + range_interval.valueOf() * i)));
  60
+      }
  61
+      return _results;
  62
+    };
  63
+
  64
+    /**
  65
+      * Determine if the current date range overlaps a given date range.
  66
+      * @param {DateRange} range Date range to check.
  67
+      * @return {!boolean}
  68
+    *
  69
+    */
  70
+
  71
+
  72
+    DateRange.prototype.overlaps = function(range) {
  73
+      return this.start < range.end && this.end > range.start;
  74
+    };
  75
+
  76
+    /**
  77
+      * Iterate over the date range by a given date range, executing a function
  78
+      * for each sub-range.
  79
+      * @param {!DateRange|String}        range     Date range to be used for iteration or shorthand string (shorthands: http://momentjs.com/docs/#/manipulating/add/)
  80
+      * @param {!function(Moment)} hollaback Function to execute for each sub-range.
  81
+      * @return {!boolean}
  82
+    *
  83
+    */
  84
+
  85
+
  86
+    DateRange.prototype.by = function(range, hollaback) {
  87
+      if (typeof range === 'string') {
  88
+        this._by_string(range, hollaback);
  89
+      } else {
  90
+        this._by_range(range, hollaback);
  91
+      }
  92
+      return this;
  93
+    };
  94
+
  95
+    /**
  96
+      * Date range in milliseconds. Allows basic coercion math of date ranges.
  97
+      * @return {!number}
  98
+    *
  99
+    */
  100
+
  101
+
  102
+    DateRange.prototype.valueOf = function() {
  103
+      return this.end - this.start;
  104
+    };
  105
+
  106
+    return DateRange;
  107
+
  108
+  })();
  109
+
  110
+  /**
  111
+    * Build a date range.
  112
+    * @param {(Moment|Date)} start Start of range.
  113
+    * @param {(Moment|Date)} end   End of range.
  114
+    * @this {Moment}
  115
+    * @return {!DateRange}
  116
+  *
  117
+  */
  118
+
  119
+
  120
+  moment.fn.range = function(start, end) {
  121
+    return new DateRange(start, end);
  122
+  };
  123
+
  124
+  /**
  125
+    * Check if the current moment is within a given date range.
  126
+    * @param {!DateRange} range Date range to check.
  127
+    * @this {Moment}
  128
+    * @return {!boolean}
  129
+  *
  130
+  */
  131
+
  132
+
  133
+  moment.fn.within = function(range) {
  134
+    return range.contains(this._d);
  135
+  };
  136
+
  137
+  if ((typeof module !== "undefined" && module !== null ? module.exports : void 0) != null) {
  138
+    module.exports = moment;
  139
+  }
  140
+
  141
+}).call(this);
1,400  static/js/moment.js
... ...
@@ -0,0 +1,1400 @@
  1
+// moment.js
  2
+// version : 2.0.0
  3
+// author : Tim Wood
  4
+// license : MIT
  5
+// momentjs.com
  6
+
  7
+(function (undefined) {
  8
+
  9
+    /************************************
  10
+        Constants
  11
+    ************************************/
  12
+
  13
+    var moment,
  14
+        VERSION = "2.0.0",
  15
+        round = Math.round, i,
  16
+        // internal storage for language config files
  17
+        languages = {},
  18
+
  19
+        // check for nodeJS
  20
+        hasModule = (typeof module !== 'undefined' && module.exports),
  21
+
  22
+        // ASP.NET json date format regex
  23
+        aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
  24
+
  25
+        // format tokens
  26
+        formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|a|A|hh?|HH?|mm?|ss?|SS?S?|X|zz?|ZZ?|.)/g,
  27
+        localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
  28
+
  29
+        // parsing tokens
  30
+        parseMultipleFormatChunker = /([0-9a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+)/gi,
  31
+
  32
+        // parsing token regexes
  33
+        parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
  34
+        parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
  35
+        parseTokenThreeDigits = /\d{3}/, // 000 - 999
  36
+        parseTokenFourDigits = /\d{1,4}/, // 0 - 9999
  37
+        parseTokenSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
  38
+        parseTokenWord = /[0-9]*[a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF]+\s*?[\u0600-\u06FF]+/i, // any word (or two) characters or numbers including two word month in arabic.
  39
+        parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/i, // +00:00 -00:00 +0000 -0000 or Z
  40
+        parseTokenT = /T/i, // T (ISO seperator)
  41
+        parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
  42
+
  43
+        // preliminary iso regex
  44
+        // 0000-00-00 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000
  45
+        isoRegex = /^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/,
  46
+        isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
  47
+
  48
+        // iso time formats and regexes
  49
+        isoTimes = [
  50
+            ['HH:mm:ss.S', /(T| )\d\d:\d\d:\d\d\.\d{1,3}/],
  51
+            ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
  52
+            ['HH:mm', /(T| )\d\d:\d\d/],
  53
+            ['HH', /(T| )\d\d/]
  54
+        ],
  55
+
  56
+        // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
  57
+        parseTimezoneChunker = /([\+\-]|\d\d)/gi,
  58
+
  59
+        // getter and setter names
  60
+        proxyGettersAndSetters = 'Month|Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
  61
+        unitMillisecondFactors = {
  62
+            'Milliseconds' : 1,
  63
+            'Seconds' : 1e3,
  64
+            'Minutes' : 6e4,
  65
+            'Hours' : 36e5,
  66
+            'Days' : 864e5,
  67
+            'Months' : 2592e6,
  68
+            'Years' : 31536e6
  69
+        },
  70
+
  71
+        // format function strings
  72
+        formatFunctions = {},
  73
+
  74
+        // tokens to ordinalize and pad
  75
+        ordinalizeTokens = 'DDD w W M D d'.split(' '),
  76
+        paddedTokens = 'M D H h m s w W'.split(' '),
  77
+
  78
+        formatTokenFunctions = {
  79
+            M    : function () {
  80
+                return this.month() + 1;
  81
+            },
  82
+            MMM  : function (format) {
  83
+                return this.lang().monthsShort(this, format);
  84
+            },
  85
+            MMMM : function (format) {
  86
+                return this.lang().months(this, format);
  87
+            },
  88
+            D    : function () {
  89
+                return this.date();
  90
+            },
  91
+            DDD  : function () {
  92
+                return this.dayOfYear();
  93
+            },
  94
+            d    : function () {
  95
+                return this.day();
  96
+            },
  97
+            dd   : function (format) {
  98
+                return this.lang().weekdaysMin(this, format);
  99
+            },
  100
+            ddd  : function (format) {
  101
+                return this.lang().weekdaysShort(this, format);
  102
+            },
  103
+            dddd : function (format) {
  104
+                return this.lang().weekdays(this, format);
  105
+            },
  106
+            w    : function () {
  107
+                return this.week();
  108
+            },
  109
+            W    : function () {
  110
+                return this.isoWeek();
  111
+            },
  112
+            YY   : function () {
  113
+                return leftZeroFill(this.year() % 100, 2);
  114
+            },
  115
+            YYYY : function () {
  116
+                return leftZeroFill(this.year(), 4);
  117
+            },
  118
+            YYYYY : function () {
  119
+                return leftZeroFill(this.year(), 5);
  120
+            },
  121
+            a    : function () {
  122
+                return this.lang().meridiem(this.hours(), this.minutes(), true);
  123
+            },
  124
+            A    : function () {
  125
+                return this.lang().meridiem(this.hours(), this.minutes(), false);
  126
+            },
  127
+            H    : function () {
  128
+                return this.hours();
  129
+            },
  130
+            h    : function () {
  131
+                return this.hours() % 12 || 12;
  132
+            },
  133
+            m    : function () {
  134
+                return this.minutes();
  135
+            },
  136
+            s    : function () {
  137
+                return this.seconds();
  138
+            },
  139
+            S    : function () {
  140
+                return ~~(this.milliseconds() / 100);
  141
+            },
  142
+            SS   : function () {
  143
+                return leftZeroFill(~~(this.milliseconds() / 10), 2);
  144
+            },
  145
+            SSS  : function () {
  146
+                return leftZeroFill(this.milliseconds(), 3);
  147
+            },
  148
+            Z    : function () {
  149
+                var a = -this.zone(),
  150
+                    b = "+";
  151
+                if (a < 0) {
  152
+                    a = -a;
  153
+                    b = "-";
  154
+                }
  155
+                return b + leftZeroFill(~~(a / 60), 2) + ":" + leftZeroFill(~~a % 60, 2);
  156
+            },
  157
+            ZZ   : function () {
  158
+                var a = -this.zone(),
  159
+                    b = "+";
  160
+                if (a < 0) {
  161
+                    a = -a;
  162
+                    b = "-";
  163
+                }
  164
+                return b + leftZeroFill(~~(10 * a / 6), 4);
  165
+            },
  166
+            X    : function () {
  167
+                return this.unix();
  168
+            }
  169
+        };
  170
+
  171
+    function padToken(func, count) {
  172
+        return function (a) {
  173
+            return leftZeroFill(func.call(this, a), count);
  174
+        };
  175
+    }
  176
+    function ordinalizeToken(func) {
  177
+        return function (a) {
  178
+            return this.lang().ordinal(func.call(this, a));
  179
+        };
  180
+    }
  181
+
  182
+    while (ordinalizeTokens.length) {
  183
+        i = ordinalizeTokens.pop();
  184
+        formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i]);
  185
+    }
  186
+    while (paddedTokens.length) {
  187
+        i = paddedTokens.pop();
  188
+        formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
  189
+    }
  190
+    formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
  191
+
  192
+
  193
+    /************************************
  194
+        Constructors
  195
+    ************************************/
  196
+
  197
+    function Language() {
  198
+
  199
+    }
  200
+
  201
+    // Moment prototype object
  202
+    function Moment(config) {
  203
+        extend(this, config);
  204
+    }
  205
+
  206
+    // Duration Constructor
  207
+    function Duration(duration) {
  208
+        var data = this._data = {},
  209
+            years = duration.years || duration.year || duration.y || 0,
  210
+            months = duration.months || duration.month || duration.M || 0,
  211
+            weeks = duration.weeks || duration.week || duration.w || 0,
  212
+            days = duration.days || duration.day || duration.d || 0,
  213
+            hours = duration.hours || duration.hour || duration.h || 0,
  214
+            minutes = duration.minutes || duration.minute || duration.m || 0,
  215
+            seconds = duration.seconds || duration.second || duration.s || 0,
  216
+            milliseconds = duration.milliseconds || duration.millisecond || duration.ms || 0;
  217
+
  218
+        // representation for dateAddRemove
  219
+        this._milliseconds = milliseconds +
  220
+            seconds * 1e3 + // 1000
  221
+            minutes * 6e4 + // 1000 * 60
  222
+            hours * 36e5; // 1000 * 60 * 60
  223
+        // Because of dateAddRemove treats 24 hours as different from a
  224
+        // day when working around DST, we need to store them separately
  225
+        this._days = days +
  226
+            weeks * 7;
  227
+        // It is impossible translate months into days without knowing
  228
+        // which months you are are talking about, so we have to store
  229
+        // it separately.
  230
+        this._months = months +
  231
+            years * 12;
  232
+
  233
+        // The following code bubbles up values, see the tests for
  234
+        // examples of what that means.
  235
+        data.milliseconds = milliseconds % 1000;
  236
+        seconds += absRound(milliseconds / 1000);
  237
+
  238
+        data.seconds = seconds % 60;
  239
+        minutes += absRound(seconds / 60);
  240
+
  241
+        data.minutes = minutes % 60;
  242
+        hours += absRound(minutes / 60);
  243
+
  244
+        data.hours = hours % 24;
  245
+        days += absRound(hours / 24);
  246
+
  247
+        days += weeks * 7;
  248
+        data.days = days % 30;
  249
+
  250
+        months += absRound(days / 30);
  251
+
  252
+        data.months = months % 12;
  253
+        years += absRound(months / 12);
  254
+
  255
+        data.years = years;
  256
+    }
  257
+
  258
+
  259
+    /************************************
  260
+        Helpers
  261
+    ************************************/
  262
+
  263
+
  264
+    function extend(a, b) {
  265
+        for (var i in b) {
  266
+            if (b.hasOwnProperty(i)) {
  267
+                a[i] = b[i];
  268
+            }
  269
+        }
  270
+        return a;
  271
+    }
  272
+
  273
+    function absRound(number) {
  274
+        if (number < 0) {
  275
+            return Math.ceil(number);
  276
+        } else {
  277
+            return Math.floor(number);
  278
+        }
  279
+    }
  280
+
  281
+    // left zero fill a number
  282
+    // see http://jsperf.com/left-zero-filling for performance comparison
  283
+    function leftZeroFill(number, targetLength) {
  284
+        var output = number + '';
  285
+        while (output.length < targetLength) {
  286
+            output = '0' + output;
  287
+        }
  288
+        return output;
  289
+    }
  290
+
  291
+    // helper function for _.addTime and _.subtractTime
  292
+    function addOrSubtractDurationFromMoment(mom, duration, isAdding) {
  293
+        var ms = duration._milliseconds,
  294
+            d = duration._days,
  295
+            M = duration._months,
  296
+            currentDate;
  297
+
  298
+        if (ms) {
  299
+            mom._d.setTime(+mom + ms * isAdding);
  300
+        }
  301
+        if (d) {
  302
+            mom.date(mom.date() + d * isAdding);
  303
+        }
  304
+        if (M) {
  305
+            currentDate = mom.date();
  306
+            mom.date(1)
  307
+                .month(mom.month() + M * isAdding)
  308
+                .date(Math.min(currentDate, mom.daysInMonth()));
  309
+        }
  310
+    }
  311
+
  312
+    // check if is an array
  313
+    function isArray(input) {
  314
+        return Object.prototype.toString.call(input) === '[object Array]';
  315
+    }
  316
+
  317
+    // compare two arrays, return the number of differences
  318
+    function compareArrays(array1, array2) {
  319
+        var len = Math.min(array1.length, array2.length),
  320
+            lengthDiff = Math.abs(array1.length - array2.length),
  321
+            diffs = 0,
  322
+            i;
  323
+        for (i = 0; i < len; i++) {
  324
+            if (~~array1[i] !== ~~array2[i]) {
  325
+                diffs++;
  326
+            }
  327
+        }
  328
+        return diffs + lengthDiff;
  329
+    }
  330
+
  331
+
  332
+    /************************************
  333
+        Languages
  334
+    ************************************/
  335
+
  336
+
  337
+    Language.prototype = {
  338
+        set : function (config) {
  339
+            var prop, i;
  340
+            for (i in config) {
  341
+                prop = config[i];
  342
+                if (typeof prop === 'function') {
  343
+                    this[i] = prop;
  344
+                } else {
  345
+                    this['_' + i] = prop;
  346
+                }
  347
+            }
  348
+        },
  349
+
  350
+        _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
  351
+        months : function (m) {
  352
+            return this._months[m.month()];
  353
+        },
  354
+
  355
+        _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
  356
+        monthsShort : function (m) {
  357
+            return this._monthsShort[m.month()];
  358
+        },
  359
+
  360
+        monthsParse : function (monthName) {
  361
+            var i, mom, regex, output;
  362
+
  363
+            if (!this._monthsParse) {
  364
+                this._monthsParse = [];
  365
+            }
  366
+
  367
+            for (i = 0; i < 12; i++) {
  368
+                // make the regex if we don't have it already
  369
+                if (!this._monthsParse[i]) {
  370
+                    mom = moment([2000, i]);
  371
+                    regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
  372
+                    this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
  373
+                }
  374
+                // test the regex
  375
+                if (this._monthsParse[i].test(monthName)) {
  376
+                    return i;
  377
+                }
  378
+            }
  379
+        },
  380
+
  381
+        _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
  382
+        weekdays : function (m) {
  383
+            return this._weekdays[m.day()];
  384
+        },
  385
+
  386
+        _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
  387
+        weekdaysShort : function (m) {
  388
+            return this._weekdaysShort[m.day()];
  389
+        },
  390
+
  391
+        _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
  392
+        weekdaysMin : function (m) {
  393
+            return this._weekdaysMin[m.day()];
  394
+        },
  395
+
  396
+        _longDateFormat : {
  397
+            LT : "h:mm A",
  398
+            L : "MM/DD/YYYY",
  399
+            LL : "MMMM D YYYY",
  400
+            LLL : "MMMM D YYYY LT",
  401
+            LLLL : "dddd, MMMM D YYYY LT"
  402
+        },
  403
+        longDateFormat : function (key) {
  404
+            var output = this._longDateFormat[key];
  405
+            if (!output && this._longDateFormat[key.toUpperCase()]) {
  406
+                output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
  407
+                    return val.slice(1);
  408
+                });
  409
+                this._longDateFormat[key] = output;
  410
+            }
  411
+            return output;
  412
+        },
  413
+
  414
+        meridiem : function (hours, minutes, isLower) {
  415
+            if (hours > 11) {
  416
+                return isLower ? 'pm' : 'PM';
  417
+            } else {
  418
+                return isLower ? 'am' : 'AM';
  419
+            }
  420
+        },
  421
+
  422
+        _calendar : {
  423
+            sameDay : '[Today at] LT',
  424
+            nextDay : '[Tomorrow at] LT',
  425
+            nextWeek : 'dddd [at] LT',
  426
+            lastDay : '[Yesterday at] LT',
  427
+            lastWeek : '[last] dddd [at] LT',
  428
+            sameElse : 'L'
  429
+        },
  430
+        calendar : function (key, mom) {
  431
+            var output = this._calendar[key];
  432
+            return typeof output === 'function' ? output.apply(mom) : output;
  433
+        },
  434
+
  435
+        _relativeTime : {
  436
+            future : "in %s",
  437
+            past : "%s ago",
  438
+            s : "a few seconds",
  439
+            m : "a minute",
  440
+            mm : "%d minutes",
  441
+            h : "an hour",
  442
+            hh : "%d hours",
  443
+            d : "a day",
  444
+            dd : "%d days",
  445
+            M : "a month",
  446
+            MM : "%d months",
  447
+            y : "a year",
  448
+            yy : "%d years"
  449
+        },
  450
+        relativeTime : function (number, withoutSuffix, string, isFuture) {
  451
+            var output = this._relativeTime[string];
  452
+            return (typeof output === 'function') ?
  453
+                output(number, withoutSuffix, string, isFuture) :
  454
+                output.replace(/%d/i, number);
  455
+        },
  456
+        pastFuture : function (diff, output) {
  457
+            var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
  458
+            return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
  459
+        },
  460
+
  461
+        ordinal : function (number) {
  462
+            return this._ordinal.replace("%d", number);
  463
+        },
  464
+        _ordinal : "%d",
  465
+
  466
+        preparse : function (string) {
  467
+            return string;
  468
+        },
  469
+
  470
+        postformat : function (string) {
  471
+            return string;
  472
+        },
  473
+
  474
+        week : function (mom) {
  475
+            return weekOfYear(mom, this._week.dow, this._week.doy);
  476
+        },
  477
+        _week : {
  478
+            dow : 0, // Sunday is the first day of the week.
  479
+            doy : 6  // The week that contains Jan 1st is the first week of the year.
  480
+        }
  481
+    };
  482
+
  483
+    // Loads a language definition into the `languages` cache.  The function
  484
+    // takes a key and optionally values.  If not in the browser and no values
  485
+    // are provided, it will load the language file module.  As a convenience,
  486
+    // this function also returns the language values.
  487
+    function loadLang(key, values) {
  488
+        values.abbr = key;
  489
+        if (!languages[key]) {
  490
+            languages[key] = new Language();
  491
+        }
  492
+        languages[key].set(values);
  493
+        return languages[key];
  494
+    }
  495
+
  496
+    // Determines which language definition to use and returns it.
  497
+    //
  498
+    // With no parameters, it will return the global language.  If you
  499
+    // pass in a language key, such as 'en', it will return the
  500
+    // definition for 'en', so long as 'en' has already been loaded using
  501
+    // moment.lang.
  502
+    function getLangDefinition(key) {
  503
+        if (!key) {
  504
+            return moment.fn._lang;
  505
+        }
  506
+        if (!languages[key] && hasModule) {
  507
+            require('./lang/' + key);
  508
+        }
  509
+        return languages[key];
  510
+    }
  511
+
  512
+
  513
+    /************************************
  514
+        Formatting
  515
+    ************************************/
  516
+
  517
+
  518
+    function removeFormattingTokens(input) {
  519
+        if (input.match(/\[.*\]/)) {
  520
+            return input.replace(/^\[|\]$/g, "");
  521
+        }
  522
+        return input.replace(/\\/g, "");
  523
+    }
  524
+
  525
+    function makeFormatFunction(format) {
  526
+        var array = format.match(formattingTokens), i, length;
  527
+
  528
+        for (i = 0, length = array.length; i < length; i++) {
  529
+            if (formatTokenFunctions[array[i]]) {
  530
+                array[i] = formatTokenFunctions[array[i]];
  531
+            } else {
  532
+                array[i] = removeFormattingTokens(array[i]);
  533
+            }
  534
+        }
  535
+
  536
+        return function (mom) {
  537
+            var output = "";
  538
+            for (i = 0; i < length; i++) {
  539
+                output += typeof array[i].call === 'function' ? array[i].call(mom, format) : array[i];
  540
+            }
  541
+            return output;
  542
+        };
  543
+    }
  544
+
  545
+    // format date using native date object
  546
+    function formatMoment(m, format) {
  547
+        var i = 5;
  548
+
  549
+        function replaceLongDateFormatTokens(input) {
  550
+            return m.lang().longDateFormat(input) || input;
  551
+        }
  552
+
  553
+        while (i-- && localFormattingTokens.test(format)) {
  554
+            format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
  555
+        }
  556
+
  557
+        if (!formatFunctions[format]) {
  558
+            formatFunctions[format] = makeFormatFunction(format);
  559
+        }
  560
+
  561
+        return formatFunctions[format](m);
  562
+    }
  563
+
  564
+
  565
+    /************************************
  566
+        Parsing
  567
+    ************************************/
  568
+
  569
+
  570
+    // get the regex to find the next token
  571
+    function getParseRegexForToken(token) {
  572
+        switch (token) {
  573
+        case 'DDDD':
  574
+            return parseTokenThreeDigits;
  575
+        case 'YYYY':
  576
+            return parseTokenFourDigits;
  577
+        case 'YYYYY':
  578
+            return parseTokenSixDigits;
  579
+        case 'S':
  580
+        case 'SS':
  581
+        case 'SSS':
  582
+        case 'DDD':
  583
+            return parseTokenOneToThreeDigits;
  584
+        case 'MMM':
  585
+        case 'MMMM':
  586
+        case 'dd':
  587
+        case 'ddd':
  588
+        case 'dddd':
  589
+        case 'a':
  590
+        case 'A':
  591
+            return parseTokenWord;
  592
+        case 'X':
  593
+            return parseTokenTimestampMs;
  594
+        case 'Z':
  595
+        case 'ZZ':
  596
+            return parseTokenTimezone;
  597
+        case 'T':
  598
+            return parseTokenT;
  599
+        case 'MM':
  600
+        case 'DD':
  601
+        case 'YY':
  602
+        case 'HH':
  603
+        case 'hh':
  604
+        case 'mm':
  605
+        case 'ss':
  606
+        case 'M':
  607
+        case 'D':
  608
+        case 'd':
  609
+        case 'H':
  610
+        case 'h':
  611
+        case 'm':
  612
+        case 's':
  613
+            return parseTokenOneOrTwoDigits;
  614
+        default :
  615
+            return new RegExp(token.replace('\\', ''));
  616
+        }
  617
+    }
  618
+
  619
+    // function to convert string input to date
  620
+    function addTimeToArrayFromToken(token, input, config) {
  621
+        var a, b,
  622
+            datePartArray = config._a;
  623
+
  624
+        switch (token) {
  625
+        // MONTH
  626
+        case 'M' : // fall through to MM
  627
+        case 'MM' :
  628
+            datePartArray[1] = (input == null) ? 0 : ~~input - 1;
  629
+            break;
  630
+        case 'MMM' : // fall through to MMMM
  631
+        case 'MMMM' :
  632
+            a = getLangDefinition(config._l).monthsParse(input);
  633
+            // if we didn't find a month name, mark the date as invalid.
  634
+            if (a != null) {
  635
+                datePartArray[1] = a;
  636
+            } else {
  637
+                config._isValid = false;
  638
+            }
  639
+            break;
  640
+        // DAY OF MONTH
  641
+        case 'D' : // fall through to DDDD
  642
+        case 'DD' : // fall through to DDDD
  643
+        case 'DDD' : // fall through to DDDD
  644
+        case 'DDDD' :
  645
+            if (input != null) {
  646
+                datePartArray[2] = ~~input;
  647
+            }
  648
+            break;
  649
+        // YEAR
  650
+        case 'YY' :
  651
+            datePartArray[0] = ~~input + (~~input > 68 ? 1900 : 2000);
  652
+            break;
  653
+        case 'YYYY' :
  654
+        case 'YYYYY' :
  655
+            datePartArray[0] = ~~input;
  656
+            break;
  657
+        // AM / PM
  658
+        case 'a' : // fall through to A
  659
+        case 'A' :
  660
+            config._isPm = ((input + '').toLowerCase() === 'pm');
  661
+            break;
  662
+        // 24 HOUR
  663
+        case 'H' : // fall through to hh
  664
+        case 'HH' : // fall through to hh
  665
+        case 'h' : // fall through to hh
  666
+        case 'hh' :
  667
+            datePartArray[3] = ~~input;
  668
+            break;
  669
+        // MINUTE
  670
+        case 'm' : // fall through to mm
  671
+        case 'mm' :
  672
+            datePartArray[4] = ~~input;
  673
+            break;
  674
+        // SECOND
  675
+        case 's' : // fall through to ss
  676
+        case 'ss' :
  677
+            datePartArray[5] = ~~input;
  678
+            break;
  679
+        // MILLISECOND
  680
+        case 'S' :
  681
+        case 'SS' :
  682
+        case 'SSS' :
  683
+            datePartArray[6] = ~~ (('0.' + input) * 1000);
  684
+            break;
  685
+        // UNIX TIMESTAMP WITH MS
  686
+        case 'X':
  687
+            config._d = new Date(parseFloat(input) * 1000);
  688
+            break;
  689
+        // TIMEZONE
  690
+        case 'Z' : // fall through to ZZ
  691
+        case 'ZZ' :
  692
+            config._useUTC = true;
  693
+            a = (input + '').match(parseTimezoneChunker);
  694
+            if (a && a[1]) {
  695
+                config._tzh = ~~a[1];
  696
+            }
  697
+            if (a && a[2]) {
  698
+                config._tzm = ~~a[2];
  699
+            }
  700
+            // reverse offsets
  701
+            if (a && a[0] === '+') {
  702
+                config._tzh = -config._tzh;
  703
+                config._tzm = -config._tzm;
  704
+            }
  705
+            break;
  706
+        }
  707
+
  708
+        // if the input is null, the date is not valid
  709
+        if (input == null) {
  710
+            config._isValid = false;
  711
+        }
  712
+    }
  713
+
  714
+    // convert an array to a date.
  715
+    // the array should mirror the parameters below
  716
+    // note: all values past the year are optional and will default to the lowest possible value.
  717
+    // [year, month, day , hour, minute, second, millisecond]
  718
+    function dateFromArray(config) {
  719
+        var i, date, input = [];
  720
+
  721
+        if (config._d) {
  722
+            return;
  723
+        }
  724
+
  725
+        for (i = 0; i < 7; i++) {
  726
+            config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
  727
+        }
  728
+
  729
+        // add the offsets to the time to be parsed so that we can have a clean array for checking isValid
  730
+        input[3] += config._tzh || 0;
  731
+        input[4] += config._tzm || 0;
  732
+
  733
+        date = new Date(0);
  734
+
  735
+        if (config._useUTC) {
  736
+            date.setUTCFullYear(input[0], input[1], input[2]);
  737
+            date.setUTCHours(input[3], input[4], input[5], input[6]);
  738
+        } else {
  739
+            date.setFullYear(input[0], input[1], input[2]);
  740
+            date.setHours(input[3], input[4], input[5], input[6]);
  741
+        }
  742
+
  743
+        config._d = date;
  744
+    }
  745
+
  746
+    // date from string and format string
  747
+    function makeDateFromStringAndFormat(config) {
  748
+        // This array is used to make a Date, either with `new Date` or `Date.UTC`
  749
+        var tokens = config._f.match(formattingTokens),
  750
+            string = config._i,
  751
+            i, parsedInput;
  752
+
  753
+        config._a = [];
  754
+
  755
+        for (i = 0; i < tokens.length; i++) {
  756
+            parsedInput = (getParseRegexForToken(tokens[i]).exec(string) || [])[0];
  757
+            if (parsedInput) {
  758
+                string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
  759
+            }
  760
+            // don't parse if its not a known token
  761
+            if (formatTokenFunctions[tokens[i]]) {
  762
+                addTimeToArrayFromToken(tokens[i], parsedInput, config);
  763
+            }
  764
+        }
  765
+        // handle am pm
  766
+        if (config._isPm && config._a[3] < 12) {
  767
+            config._a[3] += 12;
  768
+        }
  769
+        // if is 12 am, change hours to 0
  770
+        if (config._isPm === false && config._a[3] === 12) {
  771
+            config._a[3] = 0;
  772
+        }
  773
+        // return
  774
+        dateFromArray(config);
  775
+    }
  776
+
  777
+    // date from string and array of format strings
  778
+    function makeDateFromStringAndArray(config) {
  779
+        var tempConfig,
  780
+            tempMoment,
  781
+            bestMoment,
  782
+
  783
+            scoreToBeat = 99,
  784
+            i,
  785
+            currentDate,
  786
+            currentScore;
  787
+
  788
+        while (config._f.length) {
  789
+            tempConfig = extend({}, config);
  790
+            tempConfig._f = config._f.pop();
  791
+            makeDateFromStringAndFormat(tempConfig);
  792
+            tempMoment = new Moment(tempConfig);
  793
+
  794
+            if (tempMoment.isValid()) {
  795
+                bestMoment = tempMoment;
  796
+                break;
  797
+            }
  798
+
  799
+            currentScore = compareArrays(tempConfig._a, tempMoment.toArray());
  800
+
  801
+            if (currentScore < scoreToBeat) {
  802
+                scoreToBeat = currentScore;
  803
+                bestMoment = tempMoment;
  804
+            }
  805
+        }
  806
+
  807
+        extend(config, bestMoment);
  808
+    }
  809
+
  810
+    // date from iso format
  811
+    function makeDateFromString(config) {
  812
+        var i,
  813
+            string = config._i;
  814
+        if (isoRegex.exec(string)) {
  815
+            config._f = 'YYYY-MM-DDT';
  816
+            for (i = 0; i < 4; i++) {
  817
+                if (isoTimes[i][1].exec(string)) {
  818
+                    config._f += isoTimes[i][0];
  819
+                    break;
  820
+                }
  821
+            }
  822
+            if (parseTokenTimezone.exec(string)) {
  823
+                config._f += " Z";
  824
+            }
  825
+            makeDateFromStringAndFormat(config);
  826
+        } else {
  827
+            config._d = new Date(string);
  828
+        }
  829
+    }
  830
+
  831
+    function makeDateFromInput(config) {
  832
+        var input = config._i,
  833
+            matched = aspNetJsonRegex.exec(input);
  834
+
  835
+        if (input === undefined) {
  836
+            config._d = new Date();
  837
+        } else if (matched) {
  838
+            config._d = new Date(+matched[1]);
  839
+        } else if (typeof input === 'string') {
  840
+            makeDateFromString(config);
  841
+        } else if (isArray(input)) {
  842
+            config._a = input.slice(0);
  843
+            dateFromArray(config);
  844
+        } else {
  845
+            config._d = input instanceof Date ? new Date(+input) : new Date(input);
  846
+        }
  847
+    }
  848
+
  849
+
  850
+    /************************************
  851
+        Relative Time
  852
+    ************************************/
  853
+
  854
+
  855
+    // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
  856
+    function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
  857
+        return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
  858
+    }
  859
+
  860
+    function relativeTime(milliseconds, withoutSuffix, lang) {
  861
+        var seconds = round(Math.abs(milliseconds) / 1000),
  862
+            minutes = round(seconds / 60),
  863
+            hours = round(minutes / 60),
  864
+            days = round(hours / 24),
  865
+            years = round(days / 365),
  866
+            args = seconds < 45 && ['s', seconds] ||
  867
+                minutes === 1 && ['m'] ||
  868
+                minutes < 45 && ['mm', minutes] ||
  869
+                hours === 1 && ['h'] ||
  870
+                hours < 22 && ['hh', hours] ||
  871
+                days === 1 && ['d'] ||
  872
+                days <= 25 && ['dd', days] ||
  873
+                days <= 45 && ['M'] ||
  874
+                days < 345 && ['MM', round(days / 30)] ||
  875
+                years === 1 && ['y'] || ['yy', years];
  876
+        args[2] = withoutSuffix;
  877
+        args[3] = milliseconds > 0;
  878
+        args[4] = lang;
  879
+        return substituteTimeAgo.apply({}, args);
  880
+    }
  881
+
  882
+
  883
+    /************************************
  884
+        Week of Year
  885
+    ************************************/
  886
+
  887
+
  888
+    // firstDayOfWeek       0 = sun, 6 = sat
  889
+    //                      the day of the week that starts the week
  890
+    //                      (usually sunday or monday)
  891
+    // firstDayOfWeekOfYear 0 = sun, 6 = sat
  892
+    //                      the first week is the week that contains the first
  893
+    //                      of this day of the week
  894
+    //                      (eg. ISO weeks use thursday (4))
  895
+    function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
  896
+        var end = firstDayOfWeekOfYear - firstDayOfWeek,
  897
+            daysToDayOfWeek = firstDayOfWeekOfYear - mom.day();
  898
+
  899
+
  900
+        if (daysToDayOfWeek > end) {
  901
+            daysToDayOfWeek -= 7;
  902
+        }
  903
+
  904
+        if (daysToDayOfWeek < end - 7) {
  905
+            daysToDayOfWeek += 7;
  906
+        }
  907
+
  908
+        return Math.ceil(moment(mom).add('d', daysToDayOfWeek).dayOfYear() / 7);
  909
+    }
  910
+
  911
+
  912
+    /************************************
  913
+        Top Level Functions
  914
+    ************************************/
  915
+
  916
+    function makeMoment(config) {
  917
+        var input = config._i,
  918
+            format = config._f;
  919
+
  920
+        if (input === null || input === '') {
  921
+            return null;
  922
+        }
  923
+
  924
+        if (typeof input === 'string') {
  925
+            config._i = input = getLangDefinition().preparse(input);
  926
+        }
  927
+
  928
+        if (moment.isMoment(input)) {
  929
+            config = extend({}, input);
  930
+            config._d = new Date(+input._d);
  931
+        } else if (format) {
  932
+            if (isArray(format)) {
  933
+                makeDateFromStringAndArray(config);
  934
+            } else {
  935
+                makeDateFromStringAndFormat(config);
  936
+            }
  937
+        } else {
  938
+            makeDateFromInput(config);
  939
+        }
  940
+
  941
+        return new Moment(config);
  942
+    }
  943
+
  944
+    moment = function (input, format, lang) {
  945
+        return makeMoment({
  946
+            _i : input,
  947
+            _f : format,
  948
+            _l : lang,
  949
+            _isUTC : false
  950
+        });
  951
+    };
  952
+
  953
+    // creating with utc
  954
+    moment.utc = function (input, format, lang) {
  955
+        return makeMoment({
  956
+            _useUTC : true,
  957
+            _isUTC : true,
  958
+            _l : lang,
  959
+            _i : input,
  960
+            _f : format
  961
+        });
  962
+    };
  963
+
  964
+    // creating with unix timestamp (in seconds)
  965
+    moment.unix = function (input) {
  966
+        return moment(input * 1000);
  967
+    };
  968
+
  969
+    // duration
  970
+    moment.duration = function (input, key) {
  971
+        var isDuration = moment.isDuration(input),
  972
+            isNumber = (typeof input === 'number'),
  973
+            duration = (isDuration ? input._data : (isNumber ? {} : input)),
  974
+            ret;
  975
+
  976
+        if (isNumber) {
  977
+            if (key) {
  978
+                duration[key] = input;
  979
+            } else {
  980
+                duration.milliseconds = input;
  981
+            }
  982
+        }
  983
+
  984
+        ret = new Duration(duration);
  985
+
  986
+        if (isDuration && input.hasOwnProperty('_lang')) {
  987
+            ret._lang = input._lang;
  988
+        }
  989
+
  990
+        return ret;
  991
+    };
  992
+
  993
+    // version number
  994
+    moment.version = VERSION;
  995
+
  996
+    // default format
  997
+    moment.defaultFormat = isoFormat;
  998
+
  999
+    // This function will load languages and then set the global language.  If
  1000
+    // no arguments are passed in, it will simply return the current global
  1001
+    // language key.
  1002
+    moment.lang = function (key, values) {
  1003
+        var i;
  1004
+
  1005
+        if (!key) {
  1006
+            return moment.fn._lang._abbr;
  1007
+        }
  1008
+        if (values) {
  1009
+            loadLang(key, values);
  1010
+        } else if (!languages[key]) {
  1011
+            getLangDefinition(key);
  1012
+        }
  1013
+        moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key);
  1014
+    };
  1015
+
  1016
+    // returns language data
  1017
+    moment.langData = function (key) {
  1018
+        if (key && key._lang && key._lang._abbr) {
  1019
+            key = key._lang._abbr;
  1020
+        }
  1021
+        return getLangDefinition(key);
  1022
+    };
  1023
+
  1024
+    // compare moment object
  1025
+    moment.isMoment = function (obj) {
  1026
+        return obj instanceof Moment;
  1027
+    };
  1028
+
  1029
+    // for typechecking Duration objects
  1030
+    moment.isDuration = function (obj) {
  1031
+        return obj instanceof Duration;
  1032
+    };
  1033
+
  1034
+
  1035
+    /************************************
  1036
+        Moment Prototype
  1037
+    ************************************/
  1038
+
  1039
+
  1040
+    moment.fn = Moment.prototype = {
  1041
+
  1042
+        clone : function () {
  1043
+            return moment(this);
  1044
+        },
  1045
+
  1046
+        valueOf : function () {
  1047
+            return +this._d;
  1048
+        },
  1049
+
  1050
+        unix : function () {
  1051
+            return Math.floor(+this._d / 1000);
  1052
+        },
  1053
+
  1054
+        toString : function () {
  1055
+            return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
  1056
+        },
  1057
+
  1058
+        toDate : function () {
  1059
+            return this._d;
  1060
+        },
  1061
+
  1062
+        toJSON : function () {
  1063
+            return moment.utc(this).format('YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  1064
+        },
  1065
+
  1066
+        toArray : function () {
  1067
+            var m = this;
  1068
+            return [
  1069
+                m.year(),
  1070
+                m.month(),
  1071
+                m.date(),
  1072
+                m.hours(),
  1073
+                m.minutes(),
  1074
+                m.seconds(),
  1075
+                m.milliseconds()
  1076
+            ];
  1077
+        },
  1078
+
  1079
+        isValid : function () {
  1080
+            if (this._isValid == null) {
  1081
+                if (this._a) {
  1082
+                    this._isValid = !compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray());
  1083
+                } else {
  1084
+                    this._isValid = !isNaN(this._d.getTime());
  1085
+                }
  1086
+            }
  1087
+            return !!this._isValid;
  1088
+        },
  1089
+
  1090
+        utc : function () {
  1091
+            this._isUTC = true;
  1092
+            return this;
  1093
+        },
  1094
+
  1095
+        local : function () {
  1096
+            this._isUTC = false;
  1097
+            return this;
  1098
+        },
  1099
+
  1100
+        format : function (inputString) {
  1101
+            var output = formatMoment(this, inputString || moment.defaultFormat);
  1102
+            return this.lang().postformat(output);
  1103
+        },
  1104
+
  1105
+        add : function (input, val) {
  1106
+            var dur;
  1107
+            // switch args to support add('s', 1) and add(1, 's')
  1108
+            if (typeof input === 'string') {
  1109
+                dur = moment.duration(+val, input);
  1110
+            } else {
  1111
+                dur = moment.duration(input, val);
  1112
+            }
  1113
+            addOrSubtractDurationFromMoment(this, dur, 1);
  1114
+            return this;
  1115
+        },
  1116
+
  1117
+        subtract : function (input, val) {
  1118
+            var dur;
  1119
+            // switch args to support subtract('s', 1) and subtract(1, 's')
  1120
+            if (typeof input === 'string') {
  1121
+                dur = moment.duration(+val, input);
  1122
+            } else {
  1123
+                dur = moment.duration(input, val);
  1124
+            }
  1125
+            addOrSubtractDurationFromMoment(this, dur, -1);
  1126
+            return this;
  1127
+        },
  1128
+
  1129
+        diff : function (input, units, asFloat) {
  1130
+            var that = this._isUTC ? moment(input).utc() : moment(input).local(),
  1131
+                zoneDiff = (this.zone() - that.zone()) * 6e4,
  1132
+                diff, output;
  1133
+
  1134
+            if (units) {
  1135
+                // standardize on singular form
  1136
+                units = units.replace(/s$/, '');