canvas-lms/public/javascripts/timezone_core.js

325 lines
13 KiB
JavaScript

define([
"jquery",
"underscore",
"require",
"vendor/timezone",
"i18nObj",
"moment",
"moment_formats"
], function($, _, require, tz, I18n, moment, MomentFormats) {
// start with the bare vendor-provided tz() function
var currentLocale = "en_US" // default to US locale
var momentLocale = "en"
var _tz = tz;
var _preloadedData = {};
// wrap it up in a set of methods that will always call the most up-to-date
// version. each method is intended to act as a subset of bigeasy's generic
// tz() functionality.
tz = {
// wrap's moment() for parsing datetime strings. assumes the string to be
// parsed is in the profile timezone unless if contains an offset string
// *and* a format token to parse it, and unfudges the result.
moment: function(input, format) {
// ensure first argument is a string and second is a format or an array
// of formats
if (!_.isString(input) || !(_.isString(format) || _.isArray(format)))
throw new Error("tz.moment only works on string+format(s). just use " +
"moment() directly for any other signature");
// call out to moment, leaving the result alone if invalid
var m = moment.apply(null, [input, format, momentLocale]);
if (m._pf.unusedTokens.length > 0) {
// we didn't use strict at first, because we want to accept when
// there's unused input as long as we're using all tokens. but if the
// best non-strict match has unused tokens, reparse with strict
m = moment.apply(null, [input, format, momentLocale, true]);
}
if (!m.isValid()) return m;
// unfudge the result unless an offset was both specified and used in the
// parsed string.
//
// using moment internals here because I can't come up with any better
// reliable way to test for this :( fortunately, both _f and
// _pf.unusedTokens are always set as long as format is explicitly
// specified as either a string or array (which we've already checked
// for).
//
// _f lacking a 'Z' indicates that no offset token was specified in the
// format string used in parsing. we check this instead of just format in
// case format is an array, of which one contains a Z and the other
// doesn't, and we don't know until after parsing which format would best
// match the input.
//
// _pf.unusedTokens having a 'Z' token indicates that even though the
// format used contained a 'Z' token (since the first condition wasn't
// false), that token was not used during parsing; i.e. the input string
// didn't provide a value for it.
//
if (!m._f.match(/Z/) || m._pf.unusedTokens.indexOf('Z') >= 0) {
var l = m.locale();
m = moment(tz.raw_parse(m.locale('en').format('YYYY-MM-DD HH:mm:ss')));
m.locale(l);
}
return m;
},
// interprets a date value (string, integer, Date, date array, etc. -- see
// bigeasy's tz() docs) according to _tz. returns null on parse failure.
// otherwise returns a Date (rather than _tz()'s timestamp integer)
// because, when treated correctly, they are interchangeable but the Date
// is more convenient.
raw_parse: function(value) {
var timestamp = _tz(value);
if (typeof timestamp === 'number') {
return new Date(timestamp);
}
return null;
},
// parses a date value but more robustly. returns null on parse failure. if
// the value is a string but does not look like an ISO8601 string
// (loosely), or otherwise fails to be interpreted by raw_parse(), then
// parsing will be attempted with tz.moment() according to the formats
// defined in MomentFormats.getFormats(). also note that raw_parse('') will
// return the epoch, but parse('') will return null.
parse: function(value) {
// hard code '' and null as unparseable
if (value === '' || value == null) return null;
if (!_.isString(value)) {
// try and understand the value through _tz. if it doesn't work, we
// don't know what else to do with it as a non-string
return tz.raw_parse(value);
}
// only try _tz with strings looking loosely like an ISO8601 value. in
// particular, we want to avoid parsing e.g. '2016' as 2,016 milliseconds
// since the epoch
if (value.match(/[-:]/)) {
var result = tz.raw_parse(value);
if (result) return result;
}
// _tz parsing failed or skipped. try moment parsing
var formats = MomentFormats.getFormats()
var cleanValue = this.removeUnwantedChars(value)
var m = tz.moment(cleanValue, formats)
return m.isValid() ? m.toDate() : null
},
removeUnwantedChars: function(value){
return _.isString(value) ?
value.replace(".","") :
value
},
// format a date value (parsing it if necessary). returns null for parse
// failure on the value or an unrecognized format string.
format: function(value, format, otherZone) {
var localTz = _tz;
var usingOtherZone = (arguments.length == 3 && otherZone)
if(usingOtherZone){
if(!(otherZone in _preloadedData)) return null;
localTz = _tz(_preloadedData[otherZone]);
}
// make sure we have a good value first
var datetime = tz.parse(value);
if (datetime == null) return null;
// translate recognized 'date.formats.*' and 'time.formats.*' to
// appropriate format strings according to locale.
if (format.match(/^(date|time)\.formats\./)) {
var locale_format = I18n.lookup(format);
if (locale_format) {
// in the process, turn %l, %k, and %e into %-l, %-k, and %-e
// (respectively) to avoid extra unnecessary space characters
//
// javascript doesn't have lookbehind, so do the fixing on the reversed
// string so we can use lookahead instead. the funky '(%%)*(?!%)' pattern
// in all the regexes is to make sure we match (once unreversed), e.g.,
// both %l and %%%l (literal-% + %l) but not %%l (literal-% + l).
format = locale_format.
split("").reverse().join("").
replace(/([lke])(?=%(%%)*(?!%))/, '$1-').
split("").reverse().join("");
}
}
// some locales may not (according to bigeasy's localization files) use
// an am/pm distinction, but could then be incorrectly used with 12-hour
// format strings (e.g. %l:%M%P), whether by erroneous format strings in
// canvas' localization files or by unlocalized format strings. as a
// result, you might get 3am and 3pm both formatting to the same value.
// to prevent this, 12-hour indicators with an am/pm indicator should be
// promoted to the equivalent 24-hour indicator when the locale defines
// %P as an empty string. ("reverse, look-ahead, reverse" pattern for
// same reason as above)
format = format.split("").reverse().join("");
if (!tz.hasMeridian() &&
((format.match(/[lI][-_]?%(%%)*(?!%)/) &&
format.match(/p%(%%)*(?!%)/i)) ||
format.match(/r[-_]?%(%%)*(?!%)/))) {
format = format.replace(/l(?=[-_]?%(%%)*(?!%))/, 'k');
format = format.replace(/I(?=[-_]?%(%%)*(?!%))/, 'H');
format = format.replace(/r(?=[-_]?%(%%)*(?!%))/, 'T');
}
format = format.split("").reverse().join("");
// try and apply the format string to the datetime. if it succeeds, we'll
// get a string; otherwise we'll get the (non-string) date back.
var formatted = null;
if (usingOtherZone){
formatted = localTz(datetime, format, otherZone);
} else {
formatted = localTz(datetime, format);
}
if (typeof formatted !== 'string') return null;
return formatted;
},
hasMeridian: function() {
return _tz(new Date(), '%P') !== '';
},
useMeridian: function() {
if (!this.hasMeridian()) return false;
var tiny = I18n.lookup('time.formats.tiny');
return tiny && tiny.match(/%-?l/);
},
// apply any number of non-format directives to the value (parsing it if
// necessary). return null for parse failure on the value or if one of the
// directives was mistakenly a format string. returns the modified Date
// otherwise. typical directives will be for date math, e.g. '-3 days'.
// non-format unrecognized directives are ignored.
shift: function(value) {
// make sure we have a good value first
var datetime = tz.parse(value);
if (datetime == null) return null;
// no application strings given? just regurgitate the input (though
// parsed now).
if (arguments.length == 1) return datetime;
// try and apply the directives to the datetime. if one was a format
// string (unacceptable) we'll get a (non-integer) string back.
// otherwise, we'll get a new timestamp integer back (even if some
// unrecognized non-format applications were ignored).
var args = [datetime].concat([].slice.apply(arguments, [1]))
var timestamp = _tz.apply(null, args);
if (typeof timestamp !== 'number') return null;
return new Date(timestamp);
},
// allow snapshotting and restoration, and extending through the
// vendor-provided tz()'s functional composition
snapshot: function() {
return [_tz, currentLocale, momentLocale];
},
restore: function(snapshot) {
// we can't actually check that the snapshot has appropriate values, but
// we can at least verify the shape of [function, string, string]
if (!_.isArray(snapshot)) throw new Error('invalid tz() snapshot');
if (typeof snapshot[0] !== 'function') throw new Error('invalid tz() snapshot');
if (!_.isString(snapshot[1])) throw new Error('invalid tz() snapshot');
if (!_.isString(snapshot[2])) throw new Error('invalid tz() snapshot');
_tz = snapshot[0];
currentLocale = snapshot[1];
momentLocale = snapshot[2];
},
extendConfiguration: function() {
var extended = _tz.apply(null, arguments);
if (typeof extended !== 'function') throw new Error('invalid tz() extension');
_tz = extended;
},
// apply a "feature" to tz (NOTE: persistent and shared). the provided
// feature can be a chunk of previously loaded data, which is applied
// immediately, or the name of a data file to load and then apply
// asynchronously.
applyFeature: function(data, name) {
var promise = $.Deferred();
if (arguments.length > 1) {
this.preload(name, data);
tz.extendConfiguration(data, name);
promise.resolve();
return promise;
}
name = data;
this.preload(name).then(function(preloadedData){
tz.extendConfiguration(preloadedData, name);
promise.resolve();
});
return promise;
},
// preload a specific data file without having to actually
// change the timezone to do it. Future "applyFeature" calls
// will apply synchronously if their data is already preloaded.
preload: function(name, data) {
var promise = $.Deferred();
if (arguments.length > 1){
_preloadedData[name] = data;
promise.resolve(data);
} else if(_preloadedData[name]){
promise.resolve(_preloadedData[name]);
} else {
require(["vendor/timezone/" + name], function(data){
_preloadedData[name] = data;
promise.resolve(data);
});
}
return promise;
},
changeLocale: function(){
if (arguments.length > 2) {
currentLocale = arguments[1];
momentLocale = arguments[2];
} else {
currentLocale = arguments[0];
momentLocale = arguments[1];
};
// take off the momentLocale before passing up the chain
var args = [].slice.apply(arguments).slice(0, arguments.length - 1);
return this.applyFeature.apply(this, args);
},
isMidnight: function(date, options){
if (date == null) { return false };
var timezone = options && options.timezone;
if (typeof timezone == 'string' || timezone instanceof String) {
return tz.format(date, '%R', timezone) == '00:00';
} else {
return tz.format(date, '%R') == '00:00';
};
},
changeToTheSecondBeforeMidnight: function(date){
return tz.parse(tz.format(date, "%F 23:59:59"));
},
// finds the given time of day on the given date ignoring dst conversion and such.
// e.g. if time is 2016-05-20 14:00:00 and date is 2016-03-17 23:59:59, the result will
// be 2016-03-17 14:00:00
mergeTimeAndDate: function(time, date) {
return tz.parse(tz.format(date, '%F ') + tz.format(time, '%T'));
}
};
// changing zone and locale are just aliases for applying a feature
tz.changeZone = tz.applyFeature;
return tz;
});