var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/; var ambigTimeOrZoneRegex = /^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?)?$/; // Creating // ------------------------------------------------------------------------------------------------- // Creates a new moment, similar to the vanilla moment(...) constructor, but with // extra features (ambiguous time, enhanced formatting). When gived an existing moment, // it will function as a clone (and retain the zone of the moment). Anything else will // result in a moment in the local zone. fc.moment = function() { return makeMoment(arguments); }; // Sames as fc.moment, but forces the resulting moment to be in the UTC timezone. fc.moment.utc = function() { var mom = makeMoment(arguments, true); // Force it into UTC because makeMoment doesn't guarantee it. if (mom.hasTime()) { // don't give ambiguously-timed moments a UTC zone mom.utc(); } return mom; }; // Same as fc.moment, but when given an ISO8601 string, the timezone offset is preserved. // ISO8601 strings with no timezone offset will become ambiguously zoned. fc.moment.parseZone = function() { return makeMoment(arguments, true, true); }; // Builds an FCMoment from args. When given an existing moment, it clones. When given a native // Date, or called with no arguments (the current time), the resulting moment will be local. // Anything else needs to be "parsed" (a string or an array), and will be affected by: // parseAsUTC - if there is no zone information, should we parse the input in UTC? // parseZone - if there is zone information, should we force the zone of the moment? function makeMoment(args, parseAsUTC, parseZone) { var input = args[0]; var isSingleString = args.length == 1 && typeof input === 'string'; var isAmbigTime; var isAmbigZone; var ambigMatch; var output; // an object with fields for the new FCMoment object if (moment.isMoment(input)) { output = moment.apply(null, args); // clone it // the ambig properties have not been preserved in the clone, so reassign them if (input._ambigTime) { output._ambigTime = true; } if (input._ambigZone) { output._ambigZone = true; } } else if (isNativeDate(input) || input === undefined) { output = moment.apply(null, args); // will be local } else { // "parsing" is required isAmbigTime = false; isAmbigZone = false; if (isSingleString) { if (ambigDateOfMonthRegex.test(input)) { // accept strings like '2014-05', but convert to the first of the month input += '-01'; args = [ input ]; // for when we pass it on to moment's constructor isAmbigTime = true; isAmbigZone = true; } else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) { isAmbigTime = !ambigMatch[5]; // no time part? isAmbigZone = true; } } else if ($.isArray(input)) { // arrays have no timezone information, so assume ambiguous zone isAmbigZone = true; } // otherwise, probably a string with a format if (parseAsUTC) { output = moment.utc.apply(moment, args); } else { output = moment.apply(null, args); } if (isAmbigTime) { output._ambigTime = true; output._ambigZone = true; // ambiguous time always means ambiguous zone } else if (parseZone) { // let's record the inputted zone somehow if (isAmbigZone) { output._ambigZone = true; } else if (isSingleString) { output.zone(input); // if not a valid zone, will assign UTC } } } return new FCMoment(output); } // Our subclass of Moment. // Accepts an object with the internal Moment properties that should be copied over to // `this` object (most likely another Moment object). The values in this data must not // be referenced by anything else (two moments sharing a Date object for example). function FCMoment(internalData) { extend(this, internalData); } // Chain the prototype to Moment's FCMoment.prototype = createObject(moment.fn); // We need this because Moment's implementation won't create an FCMoment, // nor will it copy over the ambig flags. FCMoment.prototype.clone = function() { return makeMoment([ this ]); }; // Time-of-day // ------------------------------------------------------------------------------------------------- // GETTER // Returns a Duration with the hours/minutes/seconds/ms values of the moment. // If the moment has an ambiguous time, a duration of 00:00 will be returned. // // SETTER // You can supply a Duration, a Moment, or a Duration-like argument. // When setting the time, and the moment has an ambiguous time, it then becomes unambiguous. FCMoment.prototype.time = function(time) { if (time == null) { // getter return moment.duration({ hours: this.hours(), minutes: this.minutes(), seconds: this.seconds(), milliseconds: this.milliseconds() }); } else { // setter delete this._ambigTime; // mark that the moment now has a time if (!moment.isDuration(time) && !moment.isMoment(time)) { time = moment.duration(time); } // The day value should cause overflow (so 24 hours becomes 00:00:00 of next day). // Only for Duration times, not Moment times. var dayHours = 0; if (moment.isDuration(time)) { dayHours = Math.floor(time.asDays()) * 24; } // We need to set the individual fields. // Can't use startOf('day') then add duration. In case of DST at start of day. return this.hours(dayHours + time.hours()) .minutes(time.minutes()) .seconds(time.seconds()) .milliseconds(time.milliseconds()); } }; // Converts the moment to UTC, stripping out its time-of-day and timezone offset, // but preserving its YMD. A moment with a stripped time will display no time // nor timezone offset when .format() is called. FCMoment.prototype.stripTime = function() { var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array // set the internal UTC flag moment.fn.utc.call(this); // call the original method, because we don't want to affect _ambigZone this.year(a[0]) // TODO: find a way to do this in one shot .month(a[1]) .date(a[2]) .hours(0) .minutes(0) .seconds(0) .milliseconds(0); // Mark the time as ambiguous. This needs to happen after the .utc() call, which calls .zone(), which // clears all ambig flags. Same concept with the .year/month/date calls in the case of moment-timezone. this._ambigTime = true; this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset return this; // for chaining }; // Returns if the moment has a non-ambiguous time (boolean) FCMoment.prototype.hasTime = function() { return !this._ambigTime; }; // Timezone // ------------------------------------------------------------------------------------------------- // Converts the moment to UTC, stripping out its timezone offset, but preserving its // YMD and time-of-day. A moment with a stripped timezone offset will display no // timezone offset when .format() is called. FCMoment.prototype.stripZone = function() { var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array var wasAmbigTime = this._ambigTime; moment.fn.utc.call(this); // set the internal UTC flag this.year(a[0]) // TODO: find a way to do this in one shot .month(a[1]) .date(a[2]) .hours(a[3]) .minutes(a[4]) .seconds(a[5]) .milliseconds(a[6]); if (wasAmbigTime) { // the above call to .utc()/.zone() unfortunately clears the ambig flags, so reassign this._ambigTime = true; } // Mark the zone as ambiguous. This needs to happen after the .utc() call, which calls .zone(), which // clears all ambig flags. Same concept with the .year/month/date calls in the case of moment-timezone. this._ambigZone = true; return this; // for chaining }; // Returns of the moment has a non-ambiguous timezone offset (boolean) FCMoment.prototype.hasZone = function() { return !this._ambigZone; }; // this method implicitly marks a zone FCMoment.prototype.zone = function(tzo) { if (tzo != null) { // FYI, the delete statements need to be before the .zone() call or else chaos ensues // for reasons I don't understand. delete this._ambigTime; delete this._ambigZone; } return moment.fn.zone.apply(this, arguments); }; // this method implicitly marks a zone FCMoment.prototype.local = function() { var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array var wasAmbigZone = this._ambigZone; // will happen anyway via .local()/.zone(), but don't want to rely on internal implementation delete this._ambigTime; delete this._ambigZone; moment.fn.local.apply(this, arguments); if (wasAmbigZone) { // If the moment was ambiguously zoned, the date fields were stored as UTC. // We want to preserve these, but in local time. this.year(a[0]) // TODO: find a way to do this in one shot .month(a[1]) .date(a[2]) .hours(a[3]) .minutes(a[4]) .seconds(a[5]) .milliseconds(a[6]); } return this; // for chaining }; // this method implicitly marks a zone FCMoment.prototype.utc = function() { // will happen anyway via .local()/.zone(), but don't want to rely on internal implementation delete this._ambigTime; delete this._ambigZone; return moment.fn.utc.apply(this, arguments); }; // Formatting // ------------------------------------------------------------------------------------------------- FCMoment.prototype.format = function() { if (arguments[0]) { return formatDate(this, arguments[0]); // our extended formatting } if (this._ambigTime) { return momentFormat(this, 'YYYY-MM-DD'); } if (this._ambigZone) { return momentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss'); } return momentFormat(this); // default moment original formatting }; FCMoment.prototype.toISOString = function() { if (this._ambigTime) { return momentFormat(this, 'YYYY-MM-DD'); } if (this._ambigZone) { return momentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss'); } return moment.fn.toISOString.apply(this, arguments); }; // Querying // ------------------------------------------------------------------------------------------------- // Is the moment within the specified range? `end` is exclusive. FCMoment.prototype.isWithin = function(start, end) { var a = commonlyAmbiguate([ this, start, end ]); return a[0] >= a[1] && a[0] < a[2]; }; // Make these query methods work with ambiguous moments $.each([ 'isBefore', 'isAfter', 'isSame' ], function(i, methodName) { FCMoment.prototype[methodName] = function(input, units) { var a = commonlyAmbiguate([ this, input ]); return moment.fn[methodName].call(a[0], a[1], units); }; }); // Misc Internals // ------------------------------------------------------------------------------------------------- // given an array of moment-like inputs, return a parallel array w/ moments similarly ambiguated. // for example, of one moment has ambig time, but not others, all moments will have their time stripped. function commonlyAmbiguate(inputs) { var outputs = []; var anyAmbigTime = false; var anyAmbigZone = false; var i; for (i=0; i