@qooxdoo/framework
Version:
The JS Framework for Coders
1,703 lines (1,473 loc) • 53.2 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2006 STZ-IDA, Germany, http://www.stz-ida.de
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* Til Schneider (til132)
* Fabian Jakobs (fjakobs)
************************************************************************ */
/**
* A formatter and parser for dates, see
* http://www.unicode.org/reports/tr35/#Date_Format_Patterns
*
* Here is a quick overview of the format pattern keys:
* <table>
* <tr><th>Key <th>Description
* <tr><td><code> G </code><td> era, e.g. "AD"
* <tr><td><code> y </code><td> year
* <tr><td><code> Y </code><td> week year
* <tr><td><code> u </code><td> extended year [Not supported yet]
* <tr><td><code> Q </code><td> quarter
* <tr><td><code> q </code><td> stand-alone quarter
* <tr><td><code> M </code><td> month
* <tr><td><code> L </code><td> stand-alone month
* <tr><td><code> I </code><td> chinese leap month [Not supported yet]
* <tr><td><code> w </code><td> week of year
* <tr><td><code> W </code><td> week of month
* <tr><td><code> d </code><td> day of month
* <tr><td><code> D </code><td> day of year
* <tr><td><code> F </code><td> day of week in month [Not supported yet]
* <tr><td><code> g </code><td> modified Julian day [Not supported yet]
* <tr><td><code> E </code><td> day of week
* <tr><td><code> e </code><td> local day of week
* <tr><td><code> c </code><td> stand-alone local day of week
* <tr><td><code> a </code><td> period of day (am or pm)
* <tr><td><code> h </code><td> 12-hour hour
* <tr><td><code> H </code><td> 24-hour hour
* <tr><td><code> K </code><td> hour [0-11]
* <tr><td><code> k </code><td> hour [1-24]
* <tr><td><code> j </code><td> special symbol [Not supported yet]
* <tr><td><code> m </code><td> minute
* <tr><td><code> s </code><td> second
* <tr><td><code> S </code><td> fractional second
* <tr><td><code> A </code><td> millisecond in day [Not supported yet]
* <tr><td><code> z </code><td> time zone, specific non-location format
* <tr><td><code> Z </code><td> time zone, rfc822/gmt format
* <tr><td><code> v </code><td> time zone, generic non-location format [Not supported yet]
* <tr><td><code> V </code><td> time zone, like z except metazone abbreviations [Not supported yet]
* </table>
*
* (This list is preliminary, not all format keys might be implemented). Most
* keys support repetitions that influence the meaning of the format. Parts of the
* format string that should not be interpreted as format keys have to be
* single-quoted.
*
* The same format patterns will be used for both parsing and output formatting.
*
* NOTE: Instances of this class must be disposed of after use
*
*/
qx.Class.define("qx.util.format.DateFormat", {
extend: qx.core.Object,
implement: [qx.util.format.IFormat],
/*
*****************************************************************************
CONSTRUCTOR
*****************************************************************************
*/
/**
* @param format {String|null} The format to use. If null, the locale's default
* format is used.
* @param locale {String?} optional locale to be used. In case this is not present, the {@link #locale} property of DateFormat
* will be following the {@link qx.locale.Manager#locale} property of qx.locale.Manager
*/
construct(format, locale) {
super();
this.__initialLocale = this.__locale = locale;
if (format != null) {
this.__format = format.toString();
if (this.__format in qx.util.format.DateFormat.ISO_MASKS) {
if (this.__format === "isoUtcDateTime") {
this.__UTC = true;
}
this.__format = qx.util.format.DateFormat.ISO_MASKS[this.__format];
}
} else {
this.__format =
qx.locale.Date.getDateFormat("long", this.getLocale()) +
" " +
qx.locale.Date.getDateTimeFormat(
"HHmmss",
"HH:mm:ss",
this.getLocale()
);
}
},
/*
*****************************************************************************
STATICS
*****************************************************************************
*/
statics: {
/**
* Convenience factory that returns a <code>DateFomat</code> instance that
* uses a short date-only format. Beware that the overall layout of the
* date/time format string is that of the locale in effect when the factory
* function is called.
*
* Implemented as a quasi-singleton, so beware of side effects.
*
* @return {DateFormat} a DateFormat instance.
*/
getDateInstance() {
var DateFormat = qx.util.format.DateFormat;
var format = qx.locale.Date.getDateFormat("short") + "";
// Memoizing the instance, so caller doesn't have to dispose it.
if (
DateFormat._dateInstance == null ||
DateFormat._dateInstance.__format != format
) {
DateFormat._dateInstance = new DateFormat(format);
}
return DateFormat._dateInstance;
},
/**
* Convenience factory that returns a <code>DateFomat</code> instance that
* uses a long date/time format. Beware that the overall layout of the
* date/time format string is that of the locale in effect when the factory
* function is called.
*
* Implemented as a quasi-singleton, so beware of side effects.
*
* @return {DateFormat} a DateFormat instance.
*/
getDateTimeInstance() {
var DateFormat = qx.util.format.DateFormat;
var format =
qx.locale.Date.getDateFormat("long") +
" " +
qx.locale.Date.getDateTimeFormat("HHmmss", "HH:mm:ss");
// Memoizing the instance, so caller doesn't have to dispose it.
if (
DateFormat._dateTimeInstance == null ||
DateFormat._dateTimeInstance.__format != format
) {
DateFormat._dateTimeInstance = new DateFormat(format);
}
return DateFormat._dateTimeInstance;
},
/**
* @type {Integer} The threshold until when a year should be assumed to belong to the
* 21st century (e.g. 12 -> 2012). Years over this threshold but below 100 will be
* assumed to belong to the 20th century (e.g. 88 -> 1988). Years over 100 will be
* used unchanged (e.g. 1792 -> 1792).
*/
ASSUME_YEAR_2000_THRESHOLD: 30,
/** @type {Map} Special masks of patterns that are used frequently*/
ISO_MASKS: {
isoDate: "yyyy-MM-dd",
isoTime: "HH:mm:ss",
isoDateTime: "yyyy-MM-dd'T'HH:mm:ss",
isoDateTimeTz: "yyyy-MM-dd'T'HH:mm:ssZ",
isoUtcDateTime: "yyyy-MM-dd'T'HH:mm:ss'Z'"
},
/** @type {String} The am marker. */
AM_MARKER: "am",
/** @type {String} The pm marker. */
PM_MARKER: "pm"
},
/*
*****************************************************************************
MEMBERS
*****************************************************************************
*/
members: {
__locale: null,
__initialLocale: null,
__format: null,
__parseFeed: null,
__parseRules: null,
__formatTree: null,
__UTC: null,
/**
* Fills a number with leading zeros ("25" -> "0025").
*
* @param number {Integer} the number to fill.
* @param minSize {Integer} the minimum size the returned string should have.
* @return {String} the filled number as string.
*/
__fillNumber(number, minSize) {
var str = "" + (number < 0 ? -1 * number : number);
while (str.length < minSize) {
str = "0" + str;
}
return number < 0 ? "-" + str : str;
},
/**
* Returns the day in year of a date.
*
* @param date {Date} the date.
* @return {Integer} the day in year.
*/
__getDayInYear(date) {
var helpDate = new Date(date.getTime());
var day = helpDate.getDate();
while (helpDate.getMonth() != 0) {
// Set the date to the last day of the previous month
helpDate.setDate(-1);
day += helpDate.getDate() + 1;
}
return day;
},
/**
* Returns the thursday in the same week as the date.
*
* @param date {Date} the date to get the thursday of.
* @return {Date} the thursday in the same week as the date.
*/
__thursdayOfSameWeek(date) {
return new Date(
date.getTime() + (3 - ((date.getDay() + 6) % 7)) * 86400000
);
},
/**
* Returns the week in year of a date.
*
* @param date {Date} the date to get the week in year of.
* @return {Integer} the week in year.
*/
__getWeekInYear(date) {
// The following algorithm comes from http://www.salesianer.de/util/kalwoch.html
// Get the thursday of the week the date belongs to
var thursdayDate = this.__thursdayOfSameWeek(date);
// Get the year the thursday (and therefore the week) belongs to
var weekYear = thursdayDate.getFullYear();
// Get the thursday of the week january 4th belongs to
// (which defines week 1 of a year)
var thursdayWeek1 = this.__thursdayOfSameWeek(new Date(weekYear, 0, 4));
// Calculate the calendar week
return Math.floor(
1.5 + (thursdayDate.getTime() - thursdayWeek1.getTime()) / 86400000 / 7
);
},
/**
* Returns the week in month of a date.
*
* @param date {Date} the date to get the week in year of.
* @return {Integer} the week in month.
*/
__getWeekInMonth(date) {
var thursdayDate = this.__thursdayOfSameWeek(date);
var thursdayWeek1 = this.__thursdayOfSameWeek(
new Date(date.getFullYear(), date.getMonth(), 4)
);
return Math.floor(
1.5 + (thursdayDate.getTime() - thursdayWeek1.getTime()) / 86400000 / 7
);
},
/**
* Returns the week year of a date. (that is the year of the week where this date happens to be)
* For a week in the middle of the summer, the year is easily obtained, but for a week
* when New Year's Eve takes place, the year of that week is ambiguous.
* The thursday day of that week is used to determine the year.
*
* @param date {Date} the date to get the week in year of.
* @return {Integer} the week year.
*/
__getWeekYear(date) {
var thursdayDate = this.__thursdayOfSameWeek(date);
return thursdayDate.getFullYear();
},
/**
* Returns true if the year is a leap one.
*
* @param year {Integer} the year to check.
* @return {Boolean} true if it is a leap year.
*/
__isLeapYear(year) {
var februaryDate = new Date(year, 2, 1);
februaryDate.setDate(-1);
return februaryDate.getDate() + 1 === 29;
},
/**
* Returns a json object with month and day as keys.
*
* @param dayOfYear {Integer} the day of year.
* @param year {Integer} the year to check.
* @return {Object} a json object {month: M, day: D}.
*/
__getMonthAndDayFromDayOfYear(dayOfYear, year) {
var month = 0;
var day = 0;
// if we don't know the year, we take a non-leap year'
if (!year) {
year = 1971;
}
var dayCounter = 0;
for (var i = 1; i <= 12; i++) {
var tempDate = new Date(year, i, 1);
tempDate.setDate(-1);
var days = tempDate.getDate() + 1;
dayCounter += days;
if (dayCounter < dayOfYear) {
month++;
day += days;
} else {
day = dayOfYear - (dayCounter - days);
break;
}
}
return { month: month, day: day };
},
/**
* Returns the year of a date when we know the week year
*
* @param weekYear {Integer} the week year.
* @param month {Integer} the month
* @param dayOfMonth {Integer} the day in month
* @return {Integer} the year.
*/
__getYearFromWeekYearAndMonth(weekYear, month, dayOfMonth) {
var year;
switch (month) {
case 11:
year = weekYear - 1;
if (
weekYear != this.__getWeekYear(new Date(year, month, dayOfMonth))
) {
year = weekYear;
}
break;
case 0:
year = weekYear + 1;
if (
weekYear != this.__getWeekYear(new Date(year, month, dayOfMonth))
) {
year = weekYear;
}
break;
default:
year = weekYear;
}
return year;
},
/**
* Sets the new value for locale property
* @param value {String} The new value.
*
*/
setLocale(value) {
if (value !== null && typeof value != "string") {
throw new Error(
"Cannot set locale to " + value + " - please provide a string"
);
}
this.__locale = value === null ? this.__initialLocale : value;
},
/**
* Resets the Locale
*/
resetLocale() {
this.setLocale(null);
},
/**
* Returns the locale
*/
getLocale() {
var locale = this.__locale;
if (locale === undefined) {
locale = qx.locale.Manager.getInstance().getLocale();
}
return locale;
},
/**
* Returns the original format string
*
* @return {String}
*/
getFormatString() {
return this.__format;
},
/**
* Formats a date.
*
* @param date {Date} The date to format.
* @return {String} the formatted date.
*/
format(date) {
// check for null dates
if (date == null) {
return null;
}
if (isNaN(date.getTime())) {
if (qx.core.Environment.get("qx.debug")) {
qx.log.Logger.error("Provided date is invalid");
}
return null;
}
if (this.__UTC) {
date = new Date(
date.getUTCFullYear(),
date.getUTCMonth(),
date.getUTCDate(),
date.getUTCHours(),
date.getUTCMinutes(),
date.getUTCSeconds(),
date.getUTCMilliseconds()
);
}
var locale = this.getLocale();
var fullYear = date.getFullYear();
var month = date.getMonth();
var dayOfMonth = date.getDate();
var dayOfWeek = date.getDay();
var hours = date.getHours();
var minutes = date.getMinutes();
var seconds = date.getSeconds();
var ms = date.getMilliseconds();
var timezoneOffset = date.getTimezoneOffset();
var timezoneSign = timezoneOffset > 0 ? 1 : -1;
var timezoneHours = Math.floor(Math.abs(timezoneOffset) / 60);
var timezoneMinutes = Math.abs(timezoneOffset) % 60;
// Create the output
this.__initFormatTree();
var output = "";
for (var i = 0; i < this.__formatTree.length; i++) {
var currAtom = this.__formatTree[i];
if (currAtom.type == "literal") {
output += currAtom.text;
} else {
// This is a wildcard
var wildcardChar = currAtom.character;
var wildcardSize = currAtom.size;
// Get its replacement
var replacement = "?";
switch (wildcardChar) {
case "y": // Year
if (wildcardSize == 2) {
replacement = this.__fillNumber(fullYear % 100, 2);
} else {
var year = Math.abs(fullYear);
replacement = year + "";
if (wildcardSize > replacement.length) {
for (var j = replacement.length; j < wildcardSize; j++) {
replacement = "0" + replacement;
}
}
if (fullYear < 0) {
replacement = "-" + replacement;
}
}
break;
case "Y": // Year
replacement = this.__getWeekYear(date) + "";
var year = replacement.replace("-", "");
if (wildcardSize > replacement.length) {
for (var j = year.length; j < wildcardSize; j++) {
year = "0" + year;
}
}
replacement = replacement.indexOf("-") != -1 ? "-" + year : year;
break;
case "G": // Era - there is no CLDR data for ERA yet
if (wildcardSize >= 1 && wildcardSize <= 3) {
replacement = fullYear > 0 ? "AD" : "BC";
} else if (wildcardSize == 4) {
replacement = fullYear > 0 ? "Anno Domini" : "Before Christ";
} else if (wildcardSize == 5) {
replacement = fullYear > 0 ? "A" : "B";
}
break;
case "Q": // quarter
if (wildcardSize == 1 || wildcardSize == 2) {
replacement = this.__fillNumber(
parseInt(month / 4) + 1,
wildcardSize
);
}
if (wildcardSize == 3) {
replacement = "Q" + (parseInt(month / 4) + 1);
}
break;
case "q": // quarter stand alone
if (wildcardSize == 1 || wildcardSize == 2) {
replacement = this.__fillNumber(
parseInt(month / 4) + 1,
wildcardSize
);
}
if (wildcardSize == 3) {
replacement = "Q" + (parseInt(month / 4) + 1);
}
break;
case "D": // Day in year (e.g. 189)
replacement = this.__fillNumber(
this.__getDayInYear(date),
wildcardSize
);
break;
case "d": // Day in month
replacement = this.__fillNumber(dayOfMonth, wildcardSize);
break;
case "w": // Week in year (e.g. 27)
replacement = this.__fillNumber(
this.__getWeekInYear(date),
wildcardSize
);
break;
case "W": // Week in year (e.g. 27)
replacement = this.__getWeekInMonth(date);
break;
case "E": // Day in week
if (wildcardSize >= 1 && wildcardSize <= 3) {
replacement = qx.locale.Date.getDayName(
"abbreviated",
dayOfWeek,
locale,
"format",
true
);
} else if (wildcardSize == 4) {
replacement = qx.locale.Date.getDayName(
"wide",
dayOfWeek,
locale,
"format",
true
);
} else if (wildcardSize == 5) {
replacement = qx.locale.Date.getDayName(
"narrow",
dayOfWeek,
locale,
"format",
true
);
}
break;
case "e": // Day in week
var startOfWeek = qx.locale.Date.getWeekStart(locale);
// the index is 1 based
var localeDayOfWeek =
1 +
(dayOfWeek - startOfWeek >= 0
? dayOfWeek - startOfWeek
: 7 + (dayOfWeek - startOfWeek));
if (wildcardSize >= 1 && wildcardSize <= 2) {
replacement = this.__fillNumber(localeDayOfWeek, wildcardSize);
} else if (wildcardSize == 3) {
replacement = qx.locale.Date.getDayName(
"abbreviated",
dayOfWeek,
locale,
"format",
true
);
} else if (wildcardSize == 4) {
replacement = qx.locale.Date.getDayName(
"wide",
dayOfWeek,
locale,
"format",
true
);
} else if (wildcardSize == 5) {
replacement = qx.locale.Date.getDayName(
"narrow",
dayOfWeek,
locale,
"format",
true
);
}
break;
case "c": // Stand-alone local day in week
var startOfWeek = qx.locale.Date.getWeekStart(locale);
// the index is 1 based
var localeDayOfWeek =
1 +
(dayOfWeek - startOfWeek >= 0
? dayOfWeek - startOfWeek
: 7 + (dayOfWeek - startOfWeek));
if (wildcardSize == 1) {
replacement = "" + localeDayOfWeek;
} else if (wildcardSize == 3) {
replacement = qx.locale.Date.getDayName(
"abbreviated",
dayOfWeek,
locale,
"stand-alone",
true
);
} else if (wildcardSize == 4) {
replacement = qx.locale.Date.getDayName(
"wide",
dayOfWeek,
locale,
"stand-alone",
true
);
} else if (wildcardSize == 5) {
replacement = qx.locale.Date.getDayName(
"narrow",
dayOfWeek,
locale,
"stand-alone",
true
);
}
break;
case "M": // Month
if (wildcardSize == 1 || wildcardSize == 2) {
replacement = this.__fillNumber(month + 1, wildcardSize);
} else if (wildcardSize == 3) {
replacement = qx.locale.Date.getMonthName(
"abbreviated",
month,
locale,
"format",
true
);
} else if (wildcardSize == 4) {
replacement = qx.locale.Date.getMonthName(
"wide",
month,
locale,
"format",
true
);
} else if (wildcardSize == 5) {
replacement = qx.locale.Date.getMonthName(
"narrow",
month,
locale,
"format",
true
);
}
break;
case "L": // Stand-alone month
if (wildcardSize == 1 || wildcardSize == 2) {
replacement = this.__fillNumber(month + 1, wildcardSize);
} else if (wildcardSize == 3) {
replacement = qx.locale.Date.getMonthName(
"abbreviated",
month,
locale,
"stand-alone",
true
);
} else if (wildcardSize == 4) {
replacement = qx.locale.Date.getMonthName(
"wide",
month,
locale,
"stand-alone",
true
);
} else if (wildcardSize == 5) {
replacement = qx.locale.Date.getMonthName(
"narrow",
month,
locale,
"stand-alone",
true
);
}
break;
case "a": // am/pm marker
// NOTE: 0:00 is am, 12:00 is pm
replacement =
hours < 12
? qx.locale.Date.getAmMarker(locale)
: qx.locale.Date.getPmMarker(locale);
break;
case "H": // Hour in day (0-23)
replacement = this.__fillNumber(hours, wildcardSize);
break;
case "k": // Hour in day (1-24)
replacement = this.__fillNumber(
hours == 0 ? 24 : hours,
wildcardSize
);
break;
case "K": // Hour in am/pm (0-11)
replacement = this.__fillNumber(hours % 12, wildcardSize);
break;
case "h": // Hour in am/pm (1-12)
replacement = this.__fillNumber(
hours % 12 == 0 ? 12 : hours % 12,
wildcardSize
);
break;
case "m": // Minute in hour
replacement = this.__fillNumber(minutes, wildcardSize);
break;
case "s": // Second in minute
replacement = this.__fillNumber(seconds, wildcardSize);
break;
case "S": // Fractional second
replacement = this.__fillNumber(ms, 3);
if (wildcardSize < replacement.length) {
replacement = replacement.substr(0, wildcardSize);
} else {
while (wildcardSize > replacement.length) {
// if needed, fill the remaining wildcard length with trailing zeros
replacement += "0";
}
}
break;
case "z": // Time zone
if (wildcardSize >= 1 && wildcardSize <= 4) {
replacement =
"GMT" +
(timezoneSign > 0 ? "-" : "+") +
this.__fillNumber(Math.abs(timezoneHours), 2) +
":" +
this.__fillNumber(timezoneMinutes, 2);
}
break;
case "Z": // RFC 822 time zone
if (wildcardSize >= 1 && wildcardSize <= 3) {
replacement =
(timezoneSign > 0 ? "-" : "+") +
this.__fillNumber(Math.abs(timezoneHours), 2) +
this.__fillNumber(timezoneMinutes, 2);
} else {
replacement =
"GMT" +
(timezoneSign > 0 ? "-" : "+") +
this.__fillNumber(Math.abs(timezoneHours), 2) +
":" +
this.__fillNumber(timezoneMinutes, 2);
}
break;
}
output += replacement;
}
}
return output;
},
/**
* Parses a date.
*
* @param dateStr {String} the date to parse.
* @return {Date} the parsed date.
* @throws {Error} If the format is not well formed or if the date string does not
* match to the format.
*/
parse(dateStr) {
this.__initParseFeed();
// Apply the regex
var hit = this.__parseFeed.regex.exec(dateStr);
if (hit == null) {
throw new Error(
"Date string '" +
dateStr +
"' does not match the date format: " +
this.__format
);
}
// Apply the rules
var dateValues = {
era: 1,
year: 1970,
quarter: 1,
month: 0,
day: 1,
dayOfYear: 1,
hour: 0,
ispm: false,
weekDay: 4,
weekYear: 1970,
weekOfMonth: 1,
weekOfYear: 1,
min: 0,
sec: 0,
ms: 0,
tzOffsetMins: null
};
var currGroup = 1;
var applyWeekYearAfterRule = false;
var applyDayOfYearAfterRule = false;
for (var i = 0; i < this.__parseFeed.usedRules.length; i++) {
var rule = this.__parseFeed.usedRules[i];
var value = hit[currGroup];
if (rule.field != null) {
dateValues[rule.field] = parseInt(value, 10);
} else {
rule.manipulator(dateValues, value, rule.pattern);
}
if (rule.pattern == "Y+") {
var yearRuleApplied = false;
for (var k = 0; k < this.__parseFeed.usedRules.length; k++) {
if (this.__parseFeed.usedRules[k].pattern == "y+") {
yearRuleApplied = true;
break;
}
}
if (!yearRuleApplied) {
applyWeekYearAfterRule = true;
}
}
if (rule.pattern.indexOf("D") != -1) {
var dayRuleApplied = false;
for (var k = 0; k < this.__parseFeed.usedRules.length; k++) {
if (this.__parseFeed.usedRules[k].pattern.indexOf("d") != -1) {
dayRuleApplied = true;
break;
}
}
if (!dayRuleApplied) {
applyDayOfYearAfterRule = true;
}
}
currGroup += rule.groups == null ? 1 : rule.groups;
}
if (applyWeekYearAfterRule) {
dateValues.year = this.__getYearFromWeekYearAndMonth(
dateValues.weekYear,
dateValues.month,
dateValues.day
);
}
if (applyDayOfYearAfterRule) {
var dayAndMonth = this.__getMonthAndDayFromDayOfYear(
dateValues.dayOfYear,
dateValues.year
);
dateValues.month = dayAndMonth.month;
dateValues.day = dayAndMonth.day;
}
if (dateValues.era < 0 && dateValues.year * dateValues.era < 0) {
dateValues.year = dateValues.year * dateValues.era;
}
var date;
if (this.__UTC || dateValues.tzOffsetMins !== null) {
var utcMs = Date.UTC(
dateValues.year,
dateValues.month,
dateValues.day,
dateValues.ispm ? dateValues.hour + 12 : dateValues.hour,
dateValues.min,
dateValues.sec,
dateValues.ms
);
if (dateValues.tzOffsetMins !== 0) {
utcMs += dateValues.tzOffsetMins * 60000;
}
date = new Date(utcMs);
if (
this.__UTC &&
(dateValues.month !== date.getUTCMonth() ||
dateValues.year !== date.getUTCFullYear())
) {
throw new Error(
"Error parsing date '" +
dateStr +
"': the value for day or month is too large"
);
}
} else {
date = new Date(
dateValues.year,
dateValues.month,
dateValues.day,
dateValues.ispm ? dateValues.hour + 12 : dateValues.hour,
dateValues.min,
dateValues.sec,
dateValues.ms
);
if (
dateValues.month !== date.getMonth() ||
dateValues.year !== date.getFullYear()
) {
throw new Error(
"Error parsing date '" +
dateStr +
"': the value for day or month is too large"
);
}
}
return date;
},
/**
* Helper method for {@link #format()} and {@link #parse()}.
* Parses the date format.
*
*/
__initFormatTree() {
if (this.__formatTree != null) {
return;
}
this.__formatTree = [];
var currWildcardChar;
var currWildcardSize = 0;
var currLiteral = "";
var format = this.__format;
var state = "default";
var i = 0;
while (i < format.length) {
var currChar = format.charAt(i);
switch (state) {
case "quoted_literal":
// We are now inside a quoted literal
// Check whether the current character is an escaped "'" character
if (currChar == "'") {
if (i + 1 >= format.length) {
// this is the last character
i++;
break;
}
var lookAhead = format.charAt(i + 1);
if (lookAhead == "'") {
currLiteral += currChar;
i++;
} else {
// quoted literal ends
i++;
state = "unkown";
}
} else {
currLiteral += currChar;
i++;
}
break;
case "wildcard":
// Check whether the currChar belongs to that wildcard
if (currChar == currWildcardChar) {
// It does -> Raise the size
currWildcardSize++;
i++;
} else {
// It does not -> The current wildcard is done
this.__formatTree.push({
type: "wildcard",
character: currWildcardChar,
size: currWildcardSize
});
currWildcardChar = null;
currWildcardSize = 0;
state = "default";
}
break;
default:
// We are not (any more) in a wildcard or quoted literal -> Check what's starting here
if (
(currChar >= "a" && currChar <= "z") ||
(currChar >= "A" && currChar <= "Z")
) {
// This is a letter -> All letters are wildcards
// Start a new wildcard
currWildcardChar = currChar;
state = "wildcard";
} else if (currChar == "'") {
if (i + 1 >= format.length) {
// this is the last character
currLiteral += currChar;
i++;
break;
}
var lookAhead = format.charAt(i + 1);
if (lookAhead == "'") {
currLiteral += currChar;
i++;
}
i++;
state = "quoted_literal";
} else {
state = "default";
}
if (state != "default") {
// Add the literal
if (currLiteral.length > 0) {
this.__formatTree.push({
type: "literal",
text: currLiteral
});
currLiteral = "";
}
} else {
// This is an unquoted literal -> Add it to the current literal
currLiteral += currChar;
i++;
}
break;
}
}
// Add the last wildcard or literal
if (currWildcardChar != null) {
this.__formatTree.push({
type: "wildcard",
character: currWildcardChar,
size: currWildcardSize
});
} else if (currLiteral.length > 0) {
this.__formatTree.push({
type: "literal",
text: currLiteral
});
}
},
/**
* Initializes the parse feed.
*
* The parse contains everything needed for parsing: The regular expression
* (in compiled and uncompiled form) and the used rules.
*
* @throws {Error} If the date format is malformed.
*/
__initParseFeed() {
if (this.__parseFeed != null) {
// We already have the parse feed
return;
}
var format = this.__format;
// Initialize the rules
this.__initParseRules();
this.__initFormatTree();
// Get the used rules and construct the regex pattern
var usedRules = [];
var pattern = "^";
for (var atomIdx = 0; atomIdx < this.__formatTree.length; atomIdx++) {
var currAtom = this.__formatTree[atomIdx];
if (currAtom.type == "literal") {
pattern += qx.lang.String.escapeRegexpChars(currAtom.text);
} else {
// This is a wildcard
var wildcardChar = currAtom.character;
var wildcardSize = currAtom.size;
// Get the rule for this wildcard
var wildcardRule;
for (var ruleIdx = 0; ruleIdx < this.__parseRules.length; ruleIdx++) {
var rule = this.__parseRules[ruleIdx];
if (this.__isRuleForWildcard(rule, wildcardChar, wildcardSize)) {
// We found the right rule for the wildcard
wildcardRule = rule;
break;
}
}
// Check the rule
if (wildcardRule == null) {
// We have no rule for that wildcard -> Malformed date format
var wildcardStr = "";
for (var i = 0; i < wildcardSize; i++) {
wildcardStr += wildcardChar;
}
throw new Error(
"Malformed date format: " +
format +
". Wildcard " +
wildcardStr +
" is not supported"
);
} else {
// Add the rule to the pattern
usedRules.push(wildcardRule);
pattern += wildcardRule.regex;
}
}
}
pattern += "$";
// Create the regex
var regex;
try {
regex = new RegExp(pattern);
} catch (exc) {
throw new Error("Malformed date format: " + format);
}
// Create the this.__parseFeed
this.__parseFeed = {
regex: regex,
usedRules: usedRules,
pattern: pattern
};
},
/**
* Checks whether the rule matches the wildcard or not.
* @param rule {Object} the rule we try to match with the wildcard
* @param wildcardChar {String} the character in the wildcard
* @param wildcardSize {Integer} the number of wildcardChar characters in the wildcard
* @return {Boolean} if the rule matches or not
*/
__isRuleForWildcard(rule, wildcardChar, wildcardSize) {
if (wildcardChar === "y" && rule.pattern === "y+") {
rule.regex = rule.regexFunc(wildcardSize);
return true;
} else if (wildcardChar === "Y" && rule.pattern === "Y+") {
rule.regex = rule.regexFunc(wildcardSize);
return true;
} else {
return (
wildcardChar == rule.pattern.charAt(0) &&
wildcardSize == rule.pattern.length
);
}
},
/**
* Initializes the static parse rules.
*
*/
__initParseRules() {
var DateFormat = qx.util.format.DateFormat;
var LString = qx.lang.String;
if (this.__parseRules != null) {
// The parse rules are already initialized
return;
}
var rules = (this.__parseRules = []);
var amMarker =
qx.locale.Date.getAmMarker(this.getLocale()).toString() ||
DateFormat.AM_MARKER;
var pmMarker =
qx.locale.Date.getPmMarker(this.getLocale()).toString() ||
DateFormat.PM_MARKER;
var locale = this.getLocale();
var yearManipulator = function (dateValues, value) {
value = parseInt(value, 10);
if (value >= 0) {
if (value < DateFormat.ASSUME_YEAR_2000_THRESHOLD) {
value += 2000;
} else if (value < 100) {
value += 1900;
}
}
dateValues.year = value;
};
var weekYearManipulator = function (dateValues, value) {
value = parseInt(value, 10);
if (value >= 0) {
if (value < DateFormat.ASSUME_YEAR_2000_THRESHOLD) {
value += 2000;
} else if (value < 100) {
value += 1900;
}
}
dateValues.weekYear = value;
};
var monthManipulator = function (dateValues, value) {
dateValues.month = parseInt(value, 10) - 1;
};
var localWeekDayManipulator = function (dateValues, value) {
var startOfWeek = qx.locale.Date.getWeekStart(locale);
var dayOfWeek =
parseInt(value, 10) - 1 + startOfWeek <= 6
? parseInt(value, 10) - 1 + startOfWeek
: parseInt(value, 10) - 1 + startOfWeek - 7;
dateValues.weekDay = dayOfWeek;
};
var ampmManipulator = function (dateValues, value) {
var pmMarker =
qx.locale.Date.getPmMarker(locale).toString() || DateFormat.PM_MARKER;
dateValues.ispm = value == pmMarker;
};
var noZeroHourManipulator = function (dateValues, value) {
dateValues.hour = parseInt(value, 10) % 24;
};
var noZeroAmPmHourManipulator = function (dateValues, value) {
dateValues.hour = parseInt(value, 10) % 12;
};
var timezoneManipulator = function (dateValues, value) {
var regEx = new RegExp("([+-]?)(\\d\\d)(?::?(\\d\\d))?$");
var tzResults = regEx.exec(value);
var offsetHours = parseInt(tzResults[2], 10);
var offsetMins = parseInt(tzResults[3], 10);
// basic check, hours range is -12 to +14 https://en.wikipedia.org/wiki/Category:UTC_offsets
if (offsetHours > 14) {
throw new Error("Invalid hours in time zone offset.");
}
if (offsetMins > 59) {
throw new Error("Invalid minutes in time zone offset.");
}
dateValues.tzOffsetMins = offsetHours * 60 + offsetMins;
if (tzResults[1] === "-") {
dateValues.tzOffsetMins = -dateValues.tzOffsetMins;
}
};
// var ignoreManipulator = function(dateValues, value) {
// return;
// };
var narrowEraNames = ["A", "B"];
var narrowEraNameManipulator = function (dateValues, value) {
dateValues.era = value == "A" ? 1 : -1;
};
var abbrevEraNames = ["AD", "BC"];
var abbrevEraNameManipulator = function (dateValues, value) {
dateValues.era = value == "AD" ? 1 : -1;
};
var fullEraNames = ["Anno Domini", "Before Christ"];
var fullEraNameManipulator = function (dateValues, value) {
dateValues.era = value == "Anno Domini" ? 1 : -1;
};
var abbrevQuarterNames = ["Q1", "Q2", "Q3", "Q4"];
var abbrevQuarterManipulator = function (dateValues, value) {
dateValues.quarter = abbrevQuarterNames.indexOf(value);
};
var fullQuarterNames = [
"1st quarter",
"2nd quarter",
"3rd quarter",
"4th quarter"
];
var fullQuarterManipulator = function (dateValues, value) {
dateValues.quarter = fullQuarterNames.indexOf(value);
};
var cache = {};
var dateNamesManipulator = function (pattern) {
var monthPatternLetters = ["L", "M"];
var dayPatternLetters = ["c", "e", "E"];
var firstLetterInPattern = pattern.charAt(0);
var isMonth = monthPatternLetters.indexOf(firstLetterInPattern) >= 0;
var getContext = function () {
var letters = isMonth ? monthPatternLetters : dayPatternLetters;
var context =
firstLetterInPattern === letters[0] ? "stand-alone" : "format";
var patternLength = pattern.length;
var lengthName = "abbreviated";
switch (patternLength) {
case 4:
lengthName = "wide";
break;
case 5:
lengthName = "narrow";
break;
default:
lengthName = "abbreviated";
}
return [context, lengthName];
};
if (!cache[pattern]) {
cache[pattern] = {};
var context = getContext();
var func = isMonth
? qx.locale.Date.getMonthNames
: qx.locale.Date.getDayNames;
var names = func.call(
qx.locale.Date,
context[1],
locale,
context[0],
true
);
for (var i = 0, l = names.length; i < l; i++) {
names[i] = LString.escapeRegexpChars(names[i].toString());
}
cache[pattern].data = names;
cache[pattern].func = function (dateValues, value) {
value = LString.escapeRegexpChars(value);
dateValues[isMonth ? "month" : "weekDay"] = names.indexOf(value);
};
}
return cache[pattern];
};
// Unsupported: F (Day of week in month)
rules.push({
pattern: "y+",
regexFunc(yNumber) {
var regex = "(-*";
for (var i = 0; i < yNumber; i++) {
regex += "\\d";
if (i === yNumber - 1 && i !== 1) {
regex += "+?";
}
}
regex += ")";
return regex;
},
manipulator: yearManipulator
});
rules.push({
pattern: "Y+",
regexFunc(yNumber) {
var regex = "(-*";
for (var i = 0; i < yNumber; i++) {
regex += "\\d";
if (i === yNumber - 1) {
regex += "+?";
}
}
regex += ")";
return regex;
},
manipulator: weekYearManipulator
});
rules.push({
pattern: "G",
regex: "(" + abbrevEraNames.join("|") + ")",
manipulator: abbrevEraNameManipulator
});
rules.push({
pattern: "GG",
regex: "(" + abbrevEraNames.join("|") + ")",
manipulator: abbrevEraNameManipulator
});
rules.push({
pattern: "GGG",
regex: "(" + abbrevEraNames.join("|") + ")",
manipulator: abbrevEraNameManipulator
});
rules.push({
pattern: "GGGG",
regex: "(" + fullEraNames.join("|") + ")",
manipulator: fullEraNameManipulator
});
rules.push({
pattern: "GGGGG",
regex: "(" + narrowEraNames.join("|") + ")",
manipulator: narrowEraNameManipulator
});
rules.push({
pattern: "Q",
regex: "(\\d\\d*?)",
field: "quarter"
});
rules.push({
pattern: "QQ",
regex: "(\\d\\d?)",
field: "quarter"
});
rules.push({
pattern: "QQQ",
regex: "(" + abbrevQuarterNames.join("|") + ")",
manipulator: abbrevQuarterManipulator
});
rules.push({
pattern: "QQQQ",
regex: "(" + fullQuarterNames.join("|") + ")",
manipulator: fullQuarterManipulator
});
rules.push({
pattern: "q",
regex: "(\\d\\d*?)",
field: "quarter"
});
rules.push({
pattern: "qq",
regex: "(\\d\\d?)",
field: "quarter"
});
rules.push({
pattern: "qqq",
regex: "(" + abbrevQuarterNames.join("|") + ")",
manipulator: abbrevQuarterManipulator
});
rules.push({
pattern: "qqqq",
regex: "(" + fullQuarterNames.join("|") + ")",
manipulator: fullQuarterManipulator
});
rules.push({
pattern: "M",
regex: "(\\d\\d*?)",
manipulator: monthManipulator
});
rules.push({
pattern: "MM",
regex: "(\\d\\d?)",
manipulator: monthManipulator
});
rules.push({
pattern: "MMM",
regex: "(" + dateNamesManipulator("MMM").data.join("|") + ")",
manipulator: dateNamesManipulator("MMM").func
});
rules.push({
pattern: "MMMM",
regex: "(" + dateNamesManipulator("MMMM").data.join("|") + ")",
manipulator: dateNamesManipulator("MMMM").func
});
rules.push({
pattern: "MMMMM",
regex: "(" + dateNamesManipulator("MMMMM").data.join("|") + ")",
manipulator: dateNamesManipulator("MMMMM").func
});
rules.push({
pattern: "L",
regex: "(\\d\\d*?)",
manipulator: monthManipulator
});
rules.push({
pattern: "LL",
regex: "(\\d\\d?)",
manipulator: monthManipulator
});
rules.push({
pattern: "LLL",
regex: "(" + dateNamesManipulator("LLL").data.join("|") + ")",
manipulator: dateNamesManipulator("LLL").func
});
rules.push({
pattern: "LLLL",
regex: "(" + dateNamesManipulator("LLLL").data.join("|") + ")",
manipulator: dateNamesManipulator("LLLL").func
});
rules.push({
pattern: "LLLLL",
regex: "(" + dateNamesManipulator("LLLLL").data.join("|") + ")",
manipulator: dateNamesManipulator("LLLLL").func
});
rules.push({
pattern: "dd",
regex: "(\\d\\d?)",
field: "day"
});
rules.push({
pattern: "d",
regex: "(\\d\\d*?)",
field: "day"
});
rules.push({
pattern: "D",
regex: "(\\d?)",
field: "dayOfYear"
});
rules.push({
pattern: "DD",
regex: "(\\d\\d?)",
field: "dayOfYear"
});
rules.push({
pattern: "DDD",
regex: "(\\d\\d\\d?)",
field: "dayOfYear"
});
rules.push({
pattern: "E",
regex: "(" + dateNamesManipulator("E").data.join("|") + ")",
manipulator: dateNamesManipulator("E").func
});
rules.push({
pattern: "EE",
regex: "(" + dateNamesManipulator("EE").data.join("|") + ")",
manipulator: dateNamesManipulator("EE").func
});
rules.push({
pattern: "EEE",
regex: "(" + dateNamesManipulator("EEE").data.join("|") + ")",
manipulator: dateNamesManipulator("EEE").func
});
rules.push({
pattern: "EEEE",
regex: "(" + dateNamesManipulator("EEEE").data.join("|") + ")",
manipulator: dateNamesManipulator("EEEE").func
});
rules.push({
pattern: "EEEEE",
regex: "(" + dateNamesManipulator("EEEEE").data.join("|") + ")",
manipulator: dateNamesManipulator("EEEEE").func
});
rules.push({
pattern: "e",
regex: "(\\d?)",
manipulator: localWeekDayManipulator
});
rules.push({
pattern: "ee",
regex: "(\\d\\d?)",
manipulator: localWeekDayManipulator
});
rules.push({
pattern: "eee",
regex: "(" + dateNamesManipulator("eee").data.join("|") + ")",
manipulator: dateNamesManipulator("eee").func
});
rules.push({
pattern: "eeee",
regex: "(" + dateNamesManipulator("eeee").data.join("|") + ")",
manipulator: dateNamesManipulator("eeee").func
});
rules.push({