1130 lines
35 KiB
JavaScript
1130 lines
35 KiB
JavaScript
// Copyright 2006 The Closure Library Authors. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS-IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
/**
|
|
* @fileoverview Date/Time parsing library with locale support.
|
|
*/
|
|
|
|
|
|
/**
|
|
* Namespace for locale date/time parsing functions
|
|
*/
|
|
goog.provide('goog.i18n.DateTimeParse');
|
|
|
|
goog.require('goog.i18n.DateTimeFormat');
|
|
goog.require('goog.i18n.DateTimeSymbols');
|
|
|
|
|
|
/**
|
|
* DateTimeParse is for parsing date in a locale-sensitive manner. It allows
|
|
* user to use any customized patterns to parse date-time string under certain
|
|
* locale. Things varies across locales like month name, weekname, field
|
|
* order, etc.
|
|
*
|
|
* This module is the counter-part of DateTimeFormat. They use the same
|
|
* date/time pattern specification, which is borrowed from ICU/JDK.
|
|
*
|
|
* This implementation could parse partial date/time.
|
|
*
|
|
* Time Format Syntax: To specify the time format use a time pattern string.
|
|
* In this pattern, following letters are reserved as pattern letters, which
|
|
* are defined as the following:
|
|
*
|
|
* <pre>
|
|
* Symbol Meaning Presentation Example
|
|
* ------ ------- ------------ -------
|
|
* G era designator (Text) AD
|
|
* y# year (Number) 1996
|
|
* M month in year (Text & Number) July & 07
|
|
* d day in month (Number) 10
|
|
* h hour in am/pm (1~12) (Number) 12
|
|
* H hour in day (0~23) (Number) 0
|
|
* m minute in hour (Number) 30
|
|
* s second in minute (Number) 55
|
|
* S fractional second (Number) 978
|
|
* E day of week (Text) Tuesday
|
|
* D day in year (Number) 189
|
|
* a am/pm marker (Text) PM
|
|
* k hour in day (1~24) (Number) 24
|
|
* K hour in am/pm (0~11) (Number) 0
|
|
* z time zone (Text) Pacific Standard Time
|
|
* Z time zone (RFC 822) (Number) -0800
|
|
* v time zone (generic) (Text) Pacific Time
|
|
* ' escape for text (Delimiter) 'Date='
|
|
* '' single quote (Literal) 'o''clock'
|
|
* </pre>
|
|
*
|
|
* The count of pattern letters determine the format. <p>
|
|
* (Text): 4 or more pattern letters--use full form,
|
|
* less than 4--use short or abbreviated form if one exists.
|
|
* In parsing, we will always try long format, then short. <p>
|
|
* (Number): the minimum number of digits. <p>
|
|
* (Text & Number): 3 or over, use text, otherwise use number. <p>
|
|
* Any characters that not in the pattern will be treated as quoted text. For
|
|
* instance, characters like ':', '.', ' ', '#' and '@' will appear in the
|
|
* resulting time text even they are not embraced within single quotes. In our
|
|
* current pattern usage, we didn't use up all letters. But those unused
|
|
* letters are strongly discouraged to be used as quoted text without quote.
|
|
* That's because we may use other letter for pattern in future. <p>
|
|
*
|
|
* Examples Using the US Locale:
|
|
*
|
|
* Format Pattern Result
|
|
* -------------- -------
|
|
* "yyyy.MM.dd G 'at' HH:mm:ss vvvv" ->> 1996.07.10 AD at 15:08:56 Pacific Time
|
|
* "EEE, MMM d, ''yy" ->> Wed, July 10, '96
|
|
* "h:mm a" ->> 12:08 PM
|
|
* "hh 'o''clock' a, zzzz" ->> 12 o'clock PM, Pacific Daylight Time
|
|
* "K:mm a, vvv" ->> 0:00 PM, PT
|
|
* "yyyyy.MMMMM.dd GGG hh:mm aaa" ->> 01996.July.10 AD 12:08 PM
|
|
*
|
|
* <p> When parsing a date string using the abbreviated year pattern ("yy"),
|
|
* DateTimeParse must interpret the abbreviated year relative to some
|
|
* century. It does this by adjusting dates to be within 80 years before and 20
|
|
* years after the time the parse function is called. For example, using a
|
|
* pattern of "MM/dd/yy" and a DateTimeParse instance created on Jan 1, 1997,
|
|
* the string "01/11/12" would be interpreted as Jan 11, 2012 while the string
|
|
* "05/04/64" would be interpreted as May 4, 1964. During parsing, only
|
|
* strings consisting of exactly two digits, as defined by {@link
|
|
* java.lang.Character#isDigit(char)}, will be parsed into the default
|
|
* century. Any other numeric string, such as a one digit string, a three or
|
|
* more digit string will be interpreted as its face value.
|
|
*
|
|
* <p> If the year pattern does not have exactly two 'y' characters, the year is
|
|
* interpreted literally, regardless of the number of digits. So using the
|
|
* pattern "MM/dd/yyyy", "01/11/12" parses to Jan 11, 12 A.D.
|
|
*
|
|
* <p> When numeric fields abut one another directly, with no intervening
|
|
* delimiter characters, they constitute a run of abutting numeric fields. Such
|
|
* runs are parsed specially. For example, the format "HHmmss" parses the input
|
|
* text "123456" to 12:34:56, parses the input text "12345" to 1:23:45, and
|
|
* fails to parse "1234". In other words, the leftmost field of the run is
|
|
* flexible, while the others keep a fixed width. If the parse fails anywhere in
|
|
* the run, then the leftmost field is shortened by one character, and the
|
|
* entire run is parsed again. This is repeated until either the parse succeeds
|
|
* or the leftmost field is one character in length. If the parse still fails at
|
|
* that point, the parse of the run fails.
|
|
*
|
|
* <p> Now timezone parsing only support GMT:hhmm, GMT:+hhmm, GMT:-hhmm
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
* Construct a DateTimeParse based on current locale.
|
|
* @param {string|number} pattern pattern specification or pattern type.
|
|
* @constructor
|
|
*/
|
|
goog.i18n.DateTimeParse = function(pattern) {
|
|
this.patternParts_ = [];
|
|
if (typeof pattern == 'number') {
|
|
this.applyStandardPattern_(pattern);
|
|
} else {
|
|
this.applyPattern_(pattern);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Number of years prior to now that the century used to
|
|
* disambiguate two digit years will begin
|
|
*
|
|
* @type {number}
|
|
*/
|
|
goog.i18n.DateTimeParse.ambiguousYearCenturyStart = 80;
|
|
|
|
|
|
/**
|
|
* Apply a pattern to this Parser. The pattern string will be parsed and saved
|
|
* in "compiled" form.
|
|
* Note: this method is somewhat similar to the pattern parsing methold in
|
|
* datetimeformat. If you see something wrong here, you might want
|
|
* to check the other.
|
|
* @param {string} pattern It describes the format of date string that need to
|
|
* be parsed.
|
|
* @private
|
|
*/
|
|
goog.i18n.DateTimeParse.prototype.applyPattern_ = function(pattern) {
|
|
var inQuote = false;
|
|
var buf = '';
|
|
|
|
for (var i = 0; i < pattern.length; i++) {
|
|
var ch = pattern.charAt(i);
|
|
|
|
// handle space, add literal part (if exist), and add space part
|
|
if (ch == ' ') {
|
|
if (buf.length > 0) {
|
|
this.patternParts_.push({text: buf, count: 0, abutStart: false});
|
|
buf = '';
|
|
}
|
|
this.patternParts_.push({text: ' ', count: 0, abutStart: false});
|
|
while (i < pattern.length - 1 && pattern.charAt(i + 1) == ' ') {
|
|
i++;
|
|
}
|
|
} else if (inQuote) {
|
|
// inside quote, except '', just copy or exit
|
|
if (ch == '\'') {
|
|
if (i + 1 < pattern.length && pattern.charAt(i + 1) == '\'') {
|
|
// quote appeared twice continuously, interpret as one quote.
|
|
buf += '\'';
|
|
i++;
|
|
} else {
|
|
// exit quote
|
|
inQuote = false;
|
|
}
|
|
} else {
|
|
// literal
|
|
buf += ch;
|
|
}
|
|
} else if (goog.i18n.DateTimeParse.PATTERN_CHARS_.indexOf(ch) >= 0) {
|
|
// outside quote, it is a pattern char
|
|
if (buf.length > 0) {
|
|
this.patternParts_.push({text: buf, count: 0, abutStart: false});
|
|
buf = '';
|
|
}
|
|
var count = this.getNextCharCount_(pattern, i);
|
|
this.patternParts_.push({text: ch, count: count, abutStart: false});
|
|
i += count - 1;
|
|
} else if (ch == '\'') {
|
|
// Two consecutive quotes is a quote literal, inside or outside of quotes.
|
|
if (i + 1 < pattern.length && pattern.charAt(i + 1) == '\'') {
|
|
buf += '\'';
|
|
i++;
|
|
} else {
|
|
inQuote = true;
|
|
}
|
|
} else {
|
|
buf += ch;
|
|
}
|
|
}
|
|
|
|
if (buf.length > 0) {
|
|
this.patternParts_.push({text: buf, count: 0, abutStart: false});
|
|
}
|
|
|
|
this.markAbutStart_();
|
|
};
|
|
|
|
|
|
/**
|
|
* Apply a predefined pattern to this Parser.
|
|
* @param {number} formatType A constant used to identified the predefined
|
|
* pattern string stored in locale repository.
|
|
* @private
|
|
*/
|
|
goog.i18n.DateTimeParse.prototype.applyStandardPattern_ = function(formatType) {
|
|
var pattern;
|
|
// formatType constants are in consecutive numbers. So it can be used to
|
|
// index array in following way.
|
|
|
|
// if type is out of range, default to medium date/time format.
|
|
if (formatType > goog.i18n.DateTimeFormat.Format.SHORT_DATETIME) {
|
|
formatType = goog.i18n.DateTimeFormat.Format.MEDIUM_DATETIME;
|
|
}
|
|
|
|
if (formatType < 4) {
|
|
pattern = goog.i18n.DateTimeSymbols.DATEFORMATS[formatType];
|
|
} else if (formatType < 8) {
|
|
pattern = goog.i18n.DateTimeSymbols.TIMEFORMATS[formatType - 4];
|
|
} else {
|
|
pattern = goog.i18n.DateTimeSymbols.DATETIMEFORMATS[formatType - 8];
|
|
pattern = pattern.replace('{1}',
|
|
goog.i18n.DateTimeSymbols.DATEFORMATS[formatType - 8]);
|
|
pattern = pattern.replace('{0}',
|
|
goog.i18n.DateTimeSymbols.TIMEFORMATS[formatType - 8]);
|
|
}
|
|
this.applyPattern_(pattern);
|
|
};
|
|
|
|
|
|
/**
|
|
* Parse the given string and fill info into date object. This version does
|
|
* not validate the input.
|
|
* @param {string} text The string being parsed.
|
|
* @param {goog.date.DateLike} date The Date object to hold the parsed date.
|
|
* @param {number=} opt_start The position from where parse should begin.
|
|
* @return {number} How many characters parser advanced.
|
|
*/
|
|
goog.i18n.DateTimeParse.prototype.parse = function(text, date, opt_start) {
|
|
var start = opt_start || 0;
|
|
return this.internalParse_(text, date, start, false/*validation*/);
|
|
};
|
|
|
|
|
|
/**
|
|
* Parse the given string and fill info into date object. This version will
|
|
* validate the input and make sure it is a validate date/time.
|
|
* @param {string} text The string being parsed.
|
|
* @param {goog.date.DateLike} date The Date object to hold the parsed date.
|
|
* @param {number=} opt_start The position from where parse should begin.
|
|
* @return {number} How many characters parser advanced.
|
|
*/
|
|
goog.i18n.DateTimeParse.prototype.strictParse =
|
|
function(text, date, opt_start) {
|
|
var start = opt_start || 0;
|
|
return this.internalParse_(text, date, start, true/*validation*/);
|
|
};
|
|
|
|
|
|
/**
|
|
* Parse the given string and fill info into date object.
|
|
* @param {string} text The string being parsed.
|
|
* @param {goog.date.DateLike} date The Date object to hold the parsed date.
|
|
* @param {number} start The position from where parse should begin.
|
|
* @param {boolean} validation If true, input string need to be a valid
|
|
* date/time string.
|
|
* @return {number} How many characters parser advanced.
|
|
* @private
|
|
*/
|
|
goog.i18n.DateTimeParse.prototype.internalParse_ =
|
|
function(text, date, start, validation) {
|
|
var cal = new goog.i18n.DateTimeParse.MyDate_();
|
|
var parsePos = [start];
|
|
|
|
// For parsing abutting numeric fields. 'abutPat' is the
|
|
// offset into 'pattern' of the first of 2 or more abutting
|
|
// numeric fields. 'abutStart' is the offset into 'text'
|
|
// where parsing the fields begins. 'abutPass' starts off as 0
|
|
// and increments each time we try to parse the fields.
|
|
var abutPat = -1; // If >=0, we are in a run of abutting numeric fields
|
|
var abutStart = 0;
|
|
var abutPass = 0;
|
|
|
|
for (var i = 0; i < this.patternParts_.length; i++) {
|
|
if (this.patternParts_[i].count > 0) {
|
|
if (abutPat < 0 && this.patternParts_[i].abutStart) {
|
|
abutPat = i;
|
|
abutStart = start;
|
|
abutPass = 0;
|
|
}
|
|
|
|
// Handle fields within a run of abutting numeric fields. Take
|
|
// the pattern "HHmmss" as an example. We will try to parse
|
|
// 2/2/2 characters of the input text, then if that fails,
|
|
// 1/2/2. We only adjust the width of the leftmost field; the
|
|
// others remain fixed. This allows "123456" => 12:34:56, but
|
|
// "12345" => 1:23:45. Likewise, for the pattern "yyyyMMdd" we
|
|
// try 4/2/2, 3/2/2, 2/2/2, and finally 1/2/2.
|
|
if (abutPat >= 0) {
|
|
// If we are at the start of a run of abutting fields, then
|
|
// shorten this field in each pass. If we can't shorten
|
|
// this field any more, then the parse of this set of
|
|
// abutting numeric fields has failed.
|
|
var count = this.patternParts_[i].count;
|
|
if (i == abutPat) {
|
|
count -= abutPass;
|
|
abutPass++;
|
|
if (count == 0) {
|
|
// tried all possible width, fail now
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
if (!this.subParse_(text, parsePos, this.patternParts_[i], count,
|
|
cal)) {
|
|
// If the parse fails anywhere in the run, back up to the
|
|
// start of the run and retry.
|
|
i = abutPat - 1;
|
|
parsePos[0] = abutStart;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Handle non-numeric fields and non-abutting numeric fields.
|
|
else {
|
|
abutPat = -1;
|
|
if (!this.subParse_(text, parsePos, this.patternParts_[i], 0, cal)) {
|
|
return 0;
|
|
}
|
|
}
|
|
} else {
|
|
// Handle literal pattern characters. These are any
|
|
// quoted characters and non-alphabetic unquoted
|
|
// characters.
|
|
abutPat = -1;
|
|
// A run of white space in the pattern matches a run
|
|
// of white space in the input text.
|
|
if (this.patternParts_[i].text.charAt(0) == ' ') {
|
|
// Advance over run in input text
|
|
var s = parsePos[0];
|
|
this.skipSpace_(text, parsePos);
|
|
|
|
// Must see at least one white space char in input
|
|
if (parsePos[0] > s) {
|
|
continue;
|
|
}
|
|
} else if (text.indexOf(this.patternParts_[i].text, parsePos[0]) ==
|
|
parsePos[0]) {
|
|
parsePos[0] += this.patternParts_[i].text.length;
|
|
continue;
|
|
}
|
|
// We fall through to this point if the match fails
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// return progress
|
|
return cal.calcDate_(date, validation) ? parsePos[0] - start : 0;
|
|
};
|
|
|
|
|
|
/**
|
|
* Calculate character repeat count in pattern.
|
|
*
|
|
* @param {string} pattern It describes the format of date string that need to
|
|
* be parsed.
|
|
* @param {number} start The position of pattern character.
|
|
*
|
|
* @return {number} Repeat count.
|
|
* @private
|
|
*/
|
|
goog.i18n.DateTimeParse.prototype.getNextCharCount_ =
|
|
function(pattern, start) {
|
|
var ch = pattern.charAt(start);
|
|
var next = start + 1;
|
|
while (next < pattern.length && pattern.charAt(next) == ch) {
|
|
next++;
|
|
}
|
|
return next - start;
|
|
};
|
|
|
|
|
|
/**
|
|
* All acceptable pattern characters.
|
|
* @private
|
|
*/
|
|
goog.i18n.DateTimeParse.PATTERN_CHARS_ = 'GyMdkHmsSEDahKzZvQ';
|
|
|
|
|
|
/**
|
|
* Pattern characters that specify numerical field.
|
|
* @private
|
|
*/
|
|
goog.i18n.DateTimeParse.NUMERIC_FORMAT_CHARS_ = 'MydhHmsSDkK';
|
|
|
|
|
|
/**
|
|
* Check if the pattern part is a numeric field.
|
|
*
|
|
* @param {Object} part pattern part to be examined.
|
|
*
|
|
* @return {boolean} true if the pattern part is numberic field.
|
|
* @private
|
|
*/
|
|
goog.i18n.DateTimeParse.prototype.isNumericField_ = function(part) {
|
|
if (part.count <= 0) {
|
|
return false;
|
|
}
|
|
var i = goog.i18n.DateTimeParse.NUMERIC_FORMAT_CHARS_.indexOf(
|
|
part.text.charAt(0));
|
|
return i > 0 || i == 0 && part.count < 3;
|
|
};
|
|
|
|
|
|
/**
|
|
* Identify the start of an abutting numeric fields' run. Taking pattern
|
|
* "HHmmss" as an example. It will try to parse 2/2/2 characters of the input
|
|
* text, then if that fails, 1/2/2. We only adjust the width of the leftmost
|
|
* field; the others remain fixed. This allows "123456" => 12:34:56, but
|
|
* "12345" => 1:23:45. Likewise, for the pattern "yyyyMMdd" we try 4/2/2,
|
|
* 3/2/2, 2/2/2, and finally 1/2/2. The first field of connected numeric
|
|
* fields will be marked as abutStart, its width can be reduced to accomodate
|
|
* others.
|
|
*
|
|
* @private
|
|
*/
|
|
goog.i18n.DateTimeParse.prototype.markAbutStart_ = function() {
|
|
// abut parts are continuous numeric parts. abutStart is the switch
|
|
// point from non-abut to abut
|
|
var abut = false;
|
|
|
|
for (var i = 0; i < this.patternParts_.length; i++) {
|
|
if (this.isNumericField_(this.patternParts_[i])) {
|
|
// if next part is not following abut sequence, and isNumericField_
|
|
if (!abut && i + 1 < this.patternParts_.length &&
|
|
this.isNumericField_(this.patternParts_[i + 1])) {
|
|
abut = true;
|
|
this.patternParts_[i].abutStart = true;
|
|
}
|
|
} else {
|
|
abut = false;
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Skip space in the string.
|
|
*
|
|
* @param {string} text input string.
|
|
* @param {Array.<number>} pos where skip start, and return back where the skip
|
|
* stops.
|
|
* @private
|
|
*/
|
|
goog.i18n.DateTimeParse.prototype.skipSpace_ = function(text, pos) {
|
|
var m = text.substring(pos[0]).match(/^\s+/);
|
|
if (m) {
|
|
pos[0] += m[0].length;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Protected method that converts one field of the input string into a
|
|
* numeric field value.
|
|
*
|
|
* @param {string} text the time text to be parsed.
|
|
* @param {Array.<number>} pos Parse position.
|
|
* @param {Object} part the pattern part for this field.
|
|
* @param {number} digitCount when > 0, numeric parsing must obey the count.
|
|
* @param {goog.i18n.DateTimeParse.MyDate_} cal object that holds parsed value.
|
|
*
|
|
* @return {boolean} True if it parses successfully.
|
|
* @private
|
|
*/
|
|
goog.i18n.DateTimeParse.prototype.subParse_ =
|
|
function(text, pos, part, digitCount, cal) {
|
|
this.skipSpace_(text, pos);
|
|
|
|
var start = pos[0];
|
|
var ch = part.text.charAt(0);
|
|
|
|
// parse integer value if it is a numeric field
|
|
var value = -1;
|
|
if (this.isNumericField_(part)) {
|
|
if (digitCount > 0) {
|
|
if ((start + digitCount) > text.length) {
|
|
return false;
|
|
}
|
|
value = this.parseInt_(
|
|
text.substring(0, start + digitCount), pos);
|
|
} else {
|
|
value = this.parseInt_(text, pos);
|
|
}
|
|
}
|
|
|
|
switch (ch) {
|
|
case 'G': // ERA
|
|
cal.era = this.matchString_(text, pos, goog.i18n.DateTimeSymbols.ERAS);
|
|
return true;
|
|
case 'M': // MONTH
|
|
return this.subParseMonth_(text, pos, cal, value);
|
|
case 'E':
|
|
return this.subParseDayOfWeek_(text, pos, cal);
|
|
case 'a': // AM_PM
|
|
cal.ampm = this.matchString_(text, pos, goog.i18n.DateTimeSymbols.AMPMS);
|
|
return true;
|
|
case 'y': // YEAR
|
|
return this.subParseYear_(text, pos, start, value, part, cal);
|
|
case 'Q': // QUARTER
|
|
return this.subParseQuarter_(text, pos, cal, value);
|
|
case 'd': // DATE
|
|
cal.day = value;
|
|
return true;
|
|
case 'S': // FRACTIONAL_SECOND
|
|
return this.subParseFractionalSeconds_(value, pos, start, cal);
|
|
case 'h': // HOUR (1..12)
|
|
if (value == 12) {
|
|
value = 0;
|
|
}
|
|
case 'K': // HOUR (0..11)
|
|
case 'H': // HOUR_OF_DAY (0..23)
|
|
case 'k': // HOUR_OF_DAY (1..24)
|
|
cal.hours = value;
|
|
return true;
|
|
case 'm': // MINUTE
|
|
cal.minutes = value;
|
|
return true;
|
|
case 's': // SECOND
|
|
cal.seconds = value;
|
|
return true;
|
|
|
|
case 'z': // ZONE_OFFSET
|
|
case 'Z': // TIMEZONE_RFC
|
|
case 'v': // TIMEZONE_GENERIC
|
|
return this.subparseTimeZoneInGMT_(text, pos, cal);
|
|
default:
|
|
return false;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Parse year field. Year field is special because
|
|
* 1) two digit year need to be resolved.
|
|
* 2) we allow year to take a sign.
|
|
* 3) year field participate in abut processing.
|
|
*
|
|
* @param {string} text the time text to be parsed.
|
|
* @param {Array.<number>} pos Parse position.
|
|
* @param {number} start where this field start.
|
|
* @param {number} value integer value of year.
|
|
* @param {Object} part the pattern part for this field.
|
|
* @param {goog.i18n.DateTimeParse.MyDate_} cal object to hold parsed value.
|
|
*
|
|
* @return {boolean} True if successful.
|
|
* @private
|
|
*/
|
|
goog.i18n.DateTimeParse.prototype.subParseYear_ =
|
|
function(text, pos, start, value, part, cal) {
|
|
var ch;
|
|
if (value < 0) {
|
|
//possible sign
|
|
ch = text.charAt(pos[0]);
|
|
if (ch != '+' && ch != '-') {
|
|
return false;
|
|
}
|
|
pos[0]++;
|
|
value = this.parseInt_(text, pos);
|
|
if (value < 0) {
|
|
return false;
|
|
}
|
|
if (ch == '-') {
|
|
value = -value;
|
|
}
|
|
}
|
|
|
|
// only if 2 digit was actually parsed, and pattern say it has 2 digit.
|
|
if (!ch && pos[0] - start == 2 && part.count == 2) {
|
|
cal.setTwoDigitYear_(value);
|
|
} else {
|
|
cal.year = value;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
|
|
/**
|
|
* Parse Month field.
|
|
*
|
|
* @param {string} text the time text to be parsed.
|
|
* @param {Array.<number>} pos Parse position.
|
|
* @param {goog.i18n.DateTimeParse.MyDate_} cal object to hold parsed value.
|
|
* @param {number} value numeric value if this field is expressed using
|
|
* numeric pattern, or -1 if not.
|
|
*
|
|
* @return {boolean} True if parsing successful.
|
|
* @private
|
|
*/
|
|
goog.i18n.DateTimeParse.prototype.subParseMonth_ =
|
|
function(text, pos, cal, value) {
|
|
// when month is symbols, i.e., MMM or MMMM, value will be -1
|
|
if (value < 0) {
|
|
// Want to be able to parse both short and long forms.
|
|
// Try count == 4 first:
|
|
value = this.matchString_(text, pos, goog.i18n.DateTimeSymbols.MONTHS);
|
|
if (value < 0) { // count == 4 failed, now try count == 3
|
|
value = this.matchString_(text, pos,
|
|
goog.i18n.DateTimeSymbols.SHORTMONTHS);
|
|
}
|
|
if (value < 0) {
|
|
return false;
|
|
}
|
|
cal.month = value;
|
|
return true;
|
|
} else {
|
|
cal.month = value - 1;
|
|
return true;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Parse Quarter field.
|
|
*
|
|
* @param {string} text the time text to be parsed.
|
|
* @param {Array.<number>} pos Parse position.
|
|
* @param {goog.i18n.DateTimeParse.MyDate_} cal object to hold parsed value.
|
|
* @param {number} value numeric value if this field is expressed using
|
|
* numeric pattern, or -1 if not.
|
|
*
|
|
* @return {boolean} True if parsing successful.
|
|
* @private
|
|
*/
|
|
goog.i18n.DateTimeParse.prototype.subParseQuarter_ =
|
|
function(text, pos, cal, value) {
|
|
// value should be -1, since this is a non-numeric field.
|
|
if (value < 0) {
|
|
// Want to be able to parse both short and long forms.
|
|
// Try count == 4 first:
|
|
value = this.matchString_(text, pos, goog.i18n.DateTimeSymbols.QUARTERS);
|
|
if (value < 0) { // count == 4 failed, now try count == 3
|
|
value = this.matchString_(text, pos,
|
|
goog.i18n.DateTimeSymbols.SHORTQUARTERS);
|
|
}
|
|
if (value < 0) {
|
|
return false;
|
|
}
|
|
cal.month = value * 3; // First month of quarter.
|
|
cal.day = 1;
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
|
|
/**
|
|
* Parse Day of week field.
|
|
* @param {string} text the time text to be parsed.
|
|
* @param {Array.<number>} pos Parse position.
|
|
* @param {goog.i18n.DateTimeParse.MyDate_} cal object to hold parsed value.
|
|
*
|
|
* @return {boolean} True if successful.
|
|
* @private
|
|
*/
|
|
goog.i18n.DateTimeParse.prototype.subParseDayOfWeek_ =
|
|
function(text, pos, cal) {
|
|
// Handle both short and long forms.
|
|
// Try count == 4 (DDDD) first:
|
|
var value = this.matchString_(text, pos, goog.i18n.DateTimeSymbols.WEEKDAYS);
|
|
if (value < 0) {
|
|
value = this.matchString_(text, pos,
|
|
goog.i18n.DateTimeSymbols.SHORTWEEKDAYS);
|
|
}
|
|
if (value < 0) {
|
|
return false;
|
|
}
|
|
cal.dayOfWeek = value;
|
|
return true;
|
|
};
|
|
|
|
|
|
/**
|
|
* Parse fractional seconds field.
|
|
*
|
|
* @param {number} value parsed numberic value.
|
|
* @param {Array.<number>} pos current parse position.
|
|
* @param {number} start where this field start.
|
|
* @param {goog.i18n.DateTimeParse.MyDate_} cal object to hold parsed value.
|
|
*
|
|
* @return {boolean} True if successful.
|
|
* @private
|
|
*/
|
|
goog.i18n.DateTimeParse.prototype.subParseFractionalSeconds_ =
|
|
function(value, pos, start, cal) {
|
|
// Fractional seconds left-justify
|
|
var len = pos[0] - start;
|
|
cal.milliseconds = len < 3 ? value * Math.pow(10, 3 - len) :
|
|
Math.round(value / Math.pow(10, len - 3));
|
|
return true;
|
|
};
|
|
|
|
|
|
/**
|
|
* Parse GMT type timezone.
|
|
*
|
|
* @param {string} text the time text to be parsed.
|
|
* @param {Array.<number>} pos Parse position.
|
|
* @param {goog.i18n.DateTimeParse.MyDate_} cal object to hold parsed value.
|
|
*
|
|
* @return {boolean} True if successful.
|
|
* @private
|
|
*/
|
|
goog.i18n.DateTimeParse.prototype.subparseTimeZoneInGMT_ =
|
|
function(text, pos, cal) {
|
|
// First try to parse generic forms such as GMT-07:00. Do this first
|
|
// in case localized DateFormatZoneData contains the string "GMT"
|
|
// for a zone; in that case, we don't want to match the first three
|
|
// characters of GMT+/-HH:MM etc.
|
|
|
|
// For time zones that have no known names, look for strings
|
|
// of the form:
|
|
// GMT[+-]hours:minutes or
|
|
// GMT[+-]hhmm or
|
|
// GMT.
|
|
if (text.indexOf('GMT', pos[0]) == pos[0]) {
|
|
pos[0] += 3; // 3 is the length of GMT
|
|
return this.parseTimeZoneOffset_(text, pos, cal);
|
|
}
|
|
|
|
// TODO(user): check for named time zones by looking through the locale
|
|
// data from the DateFormatZoneData strings. should parse both short and long
|
|
// forms.
|
|
// subParseZoneString(text, start, cal);
|
|
|
|
// As a last resort, look for numeric timezones of the form
|
|
// [+-]hhmm as specified by RFC 822. This code is actually
|
|
// a little more permissive than RFC 822. It will try to do
|
|
// its best with numbers that aren't strictly 4 digits long.
|
|
return this.parseTimeZoneOffset_(text, pos, cal);
|
|
};
|
|
|
|
|
|
/**
|
|
* Parse time zone offset.
|
|
*
|
|
* @param {string} text the time text to be parsed.
|
|
* @param {Array.<number>} pos Parse position.
|
|
* @param {goog.i18n.DateTimeParse.MyDate_} cal object to hold parsed value.
|
|
*
|
|
* @return {boolean} True if successful.
|
|
* @private
|
|
*/
|
|
goog.i18n.DateTimeParse.prototype.parseTimeZoneOffset_ =
|
|
function(text, pos, cal) {
|
|
if (pos[0] >= text.length) {
|
|
cal.tzOffset = 0;
|
|
return true;
|
|
}
|
|
|
|
var sign = 1;
|
|
switch (text.charAt(pos[0])) {
|
|
case '-': sign = -1; // fall through
|
|
case '+': pos[0]++;
|
|
}
|
|
|
|
// Look for hours:minutes or hhmm.
|
|
var st = pos[0];
|
|
var value = this.parseInt_(text, pos);
|
|
if (value == 0 && pos[0] == st) {
|
|
return false;
|
|
}
|
|
|
|
var offset;
|
|
if (pos[0] < text.length && text.charAt(pos[0]) == ':') {
|
|
// This is the hours:minutes case
|
|
offset = value * 60;
|
|
pos[0]++;
|
|
st = pos[0];
|
|
value = this.parseInt_(text, pos);
|
|
if (value == 0 && pos[0] == st) {
|
|
return false;
|
|
}
|
|
offset += value;
|
|
} else {
|
|
// This is the hhmm case.
|
|
offset = value;
|
|
// Assume "-23".."+23" refers to hours.
|
|
if (offset < 24 && (pos[0] - st) <= 2) {
|
|
offset *= 60;
|
|
} else {
|
|
// todo: this looks questionable, should have more error checking
|
|
offset = offset % 100 + offset / 100 * 60;
|
|
}
|
|
}
|
|
|
|
offset *= sign;
|
|
cal.tzOffset = -offset;
|
|
return true;
|
|
};
|
|
|
|
|
|
/**
|
|
* Parse a integer string and return integer value.
|
|
*
|
|
* @param {string} text string being parsed.
|
|
* @param {Array.<number>} pos parse position.
|
|
*
|
|
* @return {number} Converted integer value.
|
|
* @private
|
|
*/
|
|
goog.i18n.DateTimeParse.prototype.parseInt_ = function(text, pos) {
|
|
// Delocalizes the string containing native digits specified by the locale,
|
|
// replaces the natvie digits with ASCII digits. Leaves other characters.
|
|
// This is the reverse operation of localizeNumbers_ in datetimeformat.js.
|
|
if (goog.i18n.DateTimeSymbols.ZERODIGIT) {
|
|
var parts = [];
|
|
for (var i = pos[0]; i < text.length; i++) {
|
|
var c = text.charCodeAt(i) - goog.i18n.DateTimeSymbols.ZERODIGIT;
|
|
parts.push((0 <= c && c <= 9) ?
|
|
String.fromCharCode(c + 0x30) :
|
|
text.charAt(i));
|
|
}
|
|
text = parts.join('');
|
|
} else {
|
|
text = text.substring(pos[0]);
|
|
}
|
|
|
|
var m = text.match(/^\d+/);
|
|
if (!m) {
|
|
return -1;
|
|
}
|
|
pos[0] += m[0].length;
|
|
return parseInt(m[0], 10);
|
|
};
|
|
|
|
|
|
/**
|
|
* Attempt to match the text at a given position against an array of strings.
|
|
* Since multiple strings in the array may match (for example, if the array
|
|
* contains "a", "ab", and "abc", all will match the input string "abcd") the
|
|
* longest match is returned.
|
|
*
|
|
* @param {string} text The string to match to.
|
|
* @param {Array.<number>} pos parsing position.
|
|
* @param {Array.<string>} data The string array of matching patterns.
|
|
*
|
|
* @return {number} the new start position if matching succeeded; a negative
|
|
* number indicating matching failure.
|
|
* @private
|
|
*/
|
|
goog.i18n.DateTimeParse.prototype.matchString_ = function(text, pos, data) {
|
|
// There may be multiple strings in the data[] array which begin with
|
|
// the same prefix (e.g., Cerven and Cervenec (June and July) in Czech).
|
|
// We keep track of the longest match, and return that. Note that this
|
|
// unfortunately requires us to test all array elements.
|
|
var bestMatchLength = 0;
|
|
var bestMatch = -1;
|
|
var lower_text = text.substring(pos[0]).toLowerCase();
|
|
for (var i = 0; i < data.length; i++) {
|
|
var len = data[i].length;
|
|
// Always compare if we have no match yet; otherwise only compare
|
|
// against potentially better matches (longer strings).
|
|
if (len > bestMatchLength &&
|
|
lower_text.indexOf(data[i].toLowerCase()) == 0) {
|
|
bestMatch = i;
|
|
bestMatchLength = len;
|
|
}
|
|
}
|
|
if (bestMatch >= 0) {
|
|
pos[0] += bestMatchLength;
|
|
}
|
|
return bestMatch;
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
* This class hold the intermediate parsing result. After all fields are
|
|
* consumed, final result will be resolved from this class.
|
|
* @constructor
|
|
* @private
|
|
*/
|
|
goog.i18n.DateTimeParse.MyDate_ = function() {};
|
|
|
|
|
|
/**
|
|
* The date's era.
|
|
* @type {?number}
|
|
*/
|
|
goog.i18n.DateTimeParse.MyDate_.prototype.era;
|
|
|
|
|
|
/**
|
|
* The date's year.
|
|
* @type {?number}
|
|
*/
|
|
goog.i18n.DateTimeParse.MyDate_.prototype.year;
|
|
|
|
|
|
/**
|
|
* The date's month.
|
|
* @type {?number}
|
|
*/
|
|
goog.i18n.DateTimeParse.MyDate_.prototype.month;
|
|
|
|
|
|
/**
|
|
* The date's day of month.
|
|
* @type {?number}
|
|
*/
|
|
goog.i18n.DateTimeParse.MyDate_.prototype.day;
|
|
|
|
|
|
/**
|
|
* The date's hour.
|
|
* @type {?number}
|
|
*/
|
|
goog.i18n.DateTimeParse.MyDate_.prototype.hours;
|
|
|
|
|
|
/**
|
|
* The date's before/afternoon denominator.
|
|
* @type {?number}
|
|
*/
|
|
goog.i18n.DateTimeParse.MyDate_.prototype.ampm;
|
|
|
|
|
|
/**
|
|
* The date's minutes.
|
|
* @type {?number}
|
|
*/
|
|
goog.i18n.DateTimeParse.MyDate_.prototype.minutes;
|
|
|
|
|
|
/**
|
|
* The date's seconds.
|
|
* @type {?number}
|
|
*/
|
|
goog.i18n.DateTimeParse.MyDate_.prototype.seconds;
|
|
|
|
|
|
/**
|
|
* The date's milliseconds.
|
|
* @type {?number}
|
|
*/
|
|
goog.i18n.DateTimeParse.MyDate_.prototype.milliseconds;
|
|
|
|
|
|
/**
|
|
* The date's timezone offset.
|
|
* @type {?number}
|
|
*/
|
|
goog.i18n.DateTimeParse.MyDate_.prototype.tzOffset;
|
|
|
|
|
|
/**
|
|
* The date's day of week. Sunday is 0, Saturday is 6.
|
|
* @type {?number}
|
|
*/
|
|
goog.i18n.DateTimeParse.MyDate_.prototype.dayOfWeek;
|
|
|
|
|
|
/**
|
|
* 2 digit year special handling. Assuming for example that the
|
|
* defaultCenturyStart is 6/18/1903. This means that two-digit years will be
|
|
* forced into the range 6/18/1903 to 6/17/2003. As a result, years 00, 01, and
|
|
* 02 correspond to 2000, 2001, and 2002. Years 04, 05, etc. correspond
|
|
* to 1904, 1905, etc. If the year is 03, then it is 2003 if the
|
|
* other fields specify a date before 6/18, or 1903 if they specify a
|
|
* date afterwards. As a result, 03 is an ambiguous year. All other
|
|
* two-digit years are unambiguous.
|
|
*
|
|
* @param {number} year 2 digit year value before adjustment.
|
|
* @return {number} disambiguated year.
|
|
* @private
|
|
*/
|
|
goog.i18n.DateTimeParse.MyDate_.prototype.setTwoDigitYear_ = function(year) {
|
|
var now = new Date();
|
|
var defaultCenturyStartYear =
|
|
now.getFullYear() - goog.i18n.DateTimeParse.ambiguousYearCenturyStart;
|
|
var ambiguousTwoDigitYear = defaultCenturyStartYear % 100;
|
|
this.ambiguousYear = (year == ambiguousTwoDigitYear);
|
|
year += Math.floor(defaultCenturyStartYear / 100) * 100 +
|
|
(year < ambiguousTwoDigitYear ? 100 : 0);
|
|
return this.year = year;
|
|
};
|
|
|
|
|
|
/**
|
|
* Based on the fields set, fill a Date object. For those fields that not
|
|
* set, use the passed in date object's value.
|
|
*
|
|
* @param {goog.date.DateLike} date Date object to be filled.
|
|
* @param {boolean} validation If true, input string will be checked to make
|
|
* sure it is valid.
|
|
*
|
|
* @return {boolean} false if fields specify a invalid date.
|
|
* @private
|
|
*/
|
|
goog.i18n.DateTimeParse.MyDate_.prototype.calcDate_ =
|
|
function(date, validation) {
|
|
// year 0 is 1 BC, and so on.
|
|
if (this.era != undefined && this.year != undefined &&
|
|
this.era == 0 && this.year > 0) {
|
|
this.year = -(this.year - 1);
|
|
}
|
|
|
|
if (this.year != undefined) {
|
|
date.setFullYear(this.year);
|
|
}
|
|
|
|
// The setMonth and setDate logic is a little tricky. We need to make sure
|
|
// day of month is smaller enough so that it won't cause a month switch when
|
|
// setting month. For example, if data in date is Nov 30, when month is set
|
|
// to Feb, because there is no Feb 30, JS adjust it to Mar 2. So Feb 12 will
|
|
// become Mar 12.
|
|
var orgDate = date.getDate();
|
|
|
|
// Every month has a 1st day, this can actually be anything less than 29.
|
|
date.setDate(1);
|
|
|
|
if (this.month != undefined) {
|
|
date.setMonth(this.month);
|
|
}
|
|
|
|
if (this.day != undefined) {
|
|
date.setDate(this.day);
|
|
} else {
|
|
date.setDate(orgDate);
|
|
}
|
|
|
|
if (goog.isFunction(date.setHours)) {
|
|
if (this.hours == undefined) {
|
|
this.hours = date.getHours();
|
|
}
|
|
// adjust ampm
|
|
if (this.ampm != undefined && this.ampm > 0 && this.hours < 12) {
|
|
this.hours += 12;
|
|
}
|
|
date.setHours(this.hours);
|
|
}
|
|
|
|
if (goog.isFunction(date.setMinutes) && this.minutes != undefined) {
|
|
date.setMinutes(this.minutes);
|
|
}
|
|
|
|
if (goog.isFunction(date.setSeconds) && this.seconds != undefined) {
|
|
date.setSeconds(this.seconds);
|
|
}
|
|
|
|
if (goog.isFunction(date.setMilliseconds) &&
|
|
this.milliseconds != undefined) {
|
|
date.setMilliseconds(this.milliseconds);
|
|
}
|
|
|
|
// If validation is needed, verify that the uncalculated date fields
|
|
// match the calculated date fields. We do this before we set the
|
|
// timezone offset, which will skew all of the dates.
|
|
//
|
|
// Don't need to check the day of week as it is guaranteed to be
|
|
// correct or return false below.
|
|
if (validation &&
|
|
(this.year != undefined && this.year != date.getFullYear() ||
|
|
this.month != undefined && this.month != date.getMonth() ||
|
|
this.day != undefined && this.day != date.getDate() ||
|
|
this.hours >= 24 || this.minutes >= 60 || this.seconds >= 60 ||
|
|
this.milliseconds >= 1000)) {
|
|
return false;
|
|
}
|
|
|
|
// adjust time zone
|
|
if (this.tzOffset != undefined) {
|
|
var offset = date.getTimezoneOffset();
|
|
date.setTime(date.getTime() + (this.tzOffset - offset) * 60 * 1000);
|
|
}
|
|
|
|
// resolve ambiguous year if needed
|
|
if (this.ambiguousYear) { // the two-digit year == the default start year
|
|
var defaultCenturyStart = new Date();
|
|
defaultCenturyStart.setFullYear(
|
|
defaultCenturyStart.getFullYear() -
|
|
goog.i18n.DateTimeParse.ambiguousYearCenturyStart);
|
|
if (date.getTime() < defaultCenturyStart.getTime()) {
|
|
date.setFullYear(defaultCenturyStart.getFullYear() + 100);
|
|
}
|
|
}
|
|
|
|
// dayOfWeek, validation only
|
|
if (this.dayOfWeek != undefined) {
|
|
if (this.day == undefined) {
|
|
// adjust to the nearest day of the week
|
|
var adjustment = (7 + this.dayOfWeek - date.getDay()) % 7;
|
|
if (adjustment > 3) {
|
|
adjustment -= 7;
|
|
}
|
|
var orgMonth = date.getMonth();
|
|
date.setDate(date.getDate() + adjustment);
|
|
|
|
// don't let it switch month
|
|
if (date.getMonth() != orgMonth) {
|
|
date.setDate(date.getDate() + (adjustment > 0 ? -7 : 7));
|
|
}
|
|
} else if (this.dayOfWeek != date.getDay()) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|