Path: blob/trunk/third_party/closure/goog/i18n/datetimeparse.js
2868 views
// Copyright 2006 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.1314/**15* @fileoverview Date/Time parsing library with locale support.16*/171819/**20* Namespace for locale date/time parsing functions21*/22goog.provide('goog.i18n.DateTimeParse');2324goog.require('goog.asserts');25goog.require('goog.date');26goog.require('goog.i18n.DateTimeFormat');27goog.require('goog.i18n.DateTimeSymbols');282930/**31* DateTimeParse is for parsing date in a locale-sensitive manner. It allows32* user to use any customized patterns to parse date-time string under certain33* locale. Things varies across locales like month name, weekname, field34* order, etc.35*36* This module is the counter-part of DateTimeFormat. They use the same37* date/time pattern specification, which is borrowed from ICU/JDK.38*39* This implementation could parse partial date/time.40*41* Time Format Syntax: To specify the time format use a time pattern string.42* In this pattern, following letters are reserved as pattern letters, which43* are defined as the following:44*45* <pre>46* Symbol Meaning Presentation Example47* ------ ------- ------------ -------48* G era designator (Text) AD49* y# year (Number) 199650* M month in year (Text & Number) July & 0751* d day in month (Number) 1052* h hour in am/pm (1~12) (Number) 1253* H hour in day (0~23) (Number) 054* m minute in hour (Number) 3055* s second in minute (Number) 5556* S fractional second (Number) 97857* E day of week (Text) Tuesday58* D day in year (Number) 18959* a am/pm marker (Text) PM60* k hour in day (1~24) (Number) 2461* K hour in am/pm (0~11) (Number) 062* z time zone (Text) Pacific Standard Time63* Z time zone (RFC 822) (Number) -080064* v time zone (generic) (Text) Pacific Time65* ' escape for text (Delimiter) 'Date='66* '' single quote (Literal) 'o''clock'67* </pre>68*69* The count of pattern letters determine the format. <p>70* (Text): 4 or more pattern letters--use full form,71* less than 4--use short or abbreviated form if one exists.72* In parsing, we will always try long format, then short. <p>73* (Number): the minimum number of digits. <p>74* (Text & Number): 3 or over, use text, otherwise use number. <p>75* Any characters that not in the pattern will be treated as quoted text. For76* instance, characters like ':', '.', ' ', '#' and '@' will appear in the77* resulting time text even they are not embraced within single quotes. In our78* current pattern usage, we didn't use up all letters. But those unused79* letters are strongly discouraged to be used as quoted text without quote.80* That's because we may use other letter for pattern in future. <p>81*82* Examples Using the US Locale:83*84* Format Pattern Result85* -------------- -------86* "yyyy.MM.dd G 'at' HH:mm:ss vvvv" ->> 1996.07.10 AD at 15:08:56 Pacific Time87* "EEE, MMM d, ''yy" ->> Wed, July 10, '9688* "h:mm a" ->> 12:08 PM89* "hh 'o''clock' a, zzzz" ->> 12 o'clock PM, Pacific Daylight Time90* "K:mm a, vvv" ->> 0:00 PM, PT91* "yyyyy.MMMMM.dd GGG hh:mm aaa" ->> 01996.July.10 AD 12:08 PM92*93* <p> When parsing a date string using the abbreviated year pattern ("yy"),94* DateTimeParse must interpret the abbreviated year relative to some95* century. It does this by adjusting dates to be within 80 years before and 2096* years after the time the parse function is called. For example, using a97* pattern of "MM/dd/yy" and a DateTimeParse instance created on Jan 1, 1997,98* the string "01/11/12" would be interpreted as Jan 11, 2012 while the string99* "05/04/64" would be interpreted as May 4, 1964. During parsing, only100* strings consisting of exactly two digits, as defined by {@link101* java.lang.Character#isDigit(char)}, will be parsed into the default102* century. Any other numeric string, such as a one digit string, a three or103* more digit string will be interpreted as its face value.104*105* <p> If the year pattern does not have exactly two 'y' characters, the year is106* interpreted literally, regardless of the number of digits. So using the107* pattern "MM/dd/yyyy", "01/11/12" parses to Jan 11, 12 A.D.108*109* <p> When numeric fields abut one another directly, with no intervening110* delimiter characters, they constitute a run of abutting numeric fields. Such111* runs are parsed specially. For example, the format "HHmmss" parses the input112* text "123456" to 12:34:56, parses the input text "12345" to 1:23:45, and113* fails to parse "1234". In other words, the leftmost field of the run is114* flexible, while the others keep a fixed width. If the parse fails anywhere in115* the run, then the leftmost field is shortened by one character, and the116* entire run is parsed again. This is repeated until either the parse succeeds117* or the leftmost field is one character in length. If the parse still fails at118* that point, the parse of the run fails.119*120* <p> Now timezone parsing only support GMT:hhmm, GMT:+hhmm, GMT:-hhmm121*/122123124125/**126* Construct a DateTimeParse based on current locale.127* @param {string|number} pattern pattern specification or pattern type.128* @param {!Object=} opt_dateTimeSymbols Optional symbols to use for this129* instance rather than the global symbols.130* @constructor131* @final132*/133goog.i18n.DateTimeParse = function(pattern, opt_dateTimeSymbols) {134goog.asserts.assert(135goog.isDef(opt_dateTimeSymbols) || goog.isDef(goog.i18n.DateTimeSymbols),136'goog.i18n.DateTimeSymbols or explicit symbols must be defined');137138this.patternParts_ = [];139140/**141* Data structure with all the locale info needed for date formatting.142* (day/month names, most common patterns, rules for week-end, etc.)143* @const @private {!goog.i18n.DateTimeSymbolsType}144*/145this.dateTimeSymbols_ = /** @type {!goog.i18n.DateTimeSymbolsType} */ (146opt_dateTimeSymbols || goog.i18n.DateTimeSymbols);147if (typeof pattern == 'number') {148this.applyStandardPattern_(pattern);149} else {150this.applyPattern_(pattern);151}152};153154155/**156* Number of years prior to now that the century used to157* disambiguate two digit years will begin158*159* @type {number}160*/161goog.i18n.DateTimeParse.ambiguousYearCenturyStart = 80;162163164/**165* Apply a pattern to this Parser. The pattern string will be parsed and saved166* in "compiled" form.167* Note: this method is somewhat similar to the pattern parsing method in168* datetimeformat. If you see something wrong here, you might want169* to check the other.170* @param {string} pattern It describes the format of date string that need to171* be parsed.172* @private173*/174goog.i18n.DateTimeParse.prototype.applyPattern_ = function(pattern) {175var inQuote = false;176var buf = '';177178for (var i = 0; i < pattern.length; i++) {179var ch = pattern.charAt(i);180181// handle space, add literal part (if exist), and add space part182if (ch == ' ') {183if (buf.length > 0) {184this.patternParts_.push({text: buf, count: 0, abutStart: false});185buf = '';186}187this.patternParts_.push({text: ' ', count: 0, abutStart: false});188while (i < pattern.length - 1 && pattern.charAt(i + 1) == ' ') {189i++;190}191} else if (inQuote) {192// inside quote, except '', just copy or exit193if (ch == '\'') {194if (i + 1 < pattern.length && pattern.charAt(i + 1) == '\'') {195// quote appeared twice continuously, interpret as one quote.196buf += '\'';197i++;198} else {199// exit quote200inQuote = false;201}202} else {203// literal204buf += ch;205}206} else if (goog.i18n.DateTimeParse.PATTERN_CHARS_.indexOf(ch) >= 0) {207// outside quote, it is a pattern char208if (buf.length > 0) {209this.patternParts_.push({text: buf, count: 0, abutStart: false});210buf = '';211}212var count = this.getNextCharCount_(pattern, i);213this.patternParts_.push({text: ch, count: count, abutStart: false});214i += count - 1;215} else if (ch == '\'') {216// Two consecutive quotes is a quote literal, inside or outside of quotes.217if (i + 1 < pattern.length && pattern.charAt(i + 1) == '\'') {218buf += '\'';219i++;220} else {221inQuote = true;222}223} else {224buf += ch;225}226}227228if (buf.length > 0) {229this.patternParts_.push({text: buf, count: 0, abutStart: false});230}231232this.markAbutStart_();233};234235236/**237* Apply a predefined pattern to this Parser.238* @param {number} formatType A constant used to identified the predefined239* pattern string stored in locale repository.240* @private241*/242goog.i18n.DateTimeParse.prototype.applyStandardPattern_ = function(formatType) {243var pattern;244// formatType constants are in consecutive numbers. So it can be used to245// index array in following way.246247// if type is out of range, default to medium date/time format.248if (formatType > goog.i18n.DateTimeFormat.Format.SHORT_DATETIME) {249formatType = goog.i18n.DateTimeFormat.Format.MEDIUM_DATETIME;250}251252if (formatType < 4) {253pattern = this.dateTimeSymbols_.DATEFORMATS[formatType];254} else if (formatType < 8) {255pattern = this.dateTimeSymbols_.TIMEFORMATS[formatType - 4];256} else {257pattern = this.dateTimeSymbols_.DATETIMEFORMATS[formatType - 8];258pattern = pattern.replace(259'{1}', this.dateTimeSymbols_.DATEFORMATS[formatType - 8]);260pattern = pattern.replace(261'{0}', this.dateTimeSymbols_.TIMEFORMATS[formatType - 8]);262}263this.applyPattern_(pattern);264};265266267/**268* Parse the given string and fill info into date object. This version does269* not validate the input.270* @param {string} text The string being parsed.271* @param {goog.date.DateLike} date The Date object to hold the parsed date.272* @param {number=} opt_start The position from where parse should begin.273* @return {number} How many characters parser advanced.274*/275goog.i18n.DateTimeParse.prototype.parse = function(text, date, opt_start) {276var start = opt_start || 0;277return this.internalParse_(text, date, start, false /*validation*/);278};279280281/**282* Parse the given string and fill info into date object. This version will283* validate the input and make sure it is a valid date/time.284* @param {string} text The string being parsed.285* @param {goog.date.DateLike} date The Date object to hold the parsed date.286* @param {number=} opt_start The position from where parse should begin.287* @return {number} How many characters parser advanced.288*/289goog.i18n.DateTimeParse.prototype.strictParse = function(290text, date, opt_start) {291var start = opt_start || 0;292return this.internalParse_(text, date, start, true /*validation*/);293};294295296/**297* Parse the given string and fill info into date object.298* @param {string} text The string being parsed.299* @param {goog.date.DateLike} date The Date object to hold the parsed date.300* @param {number} start The position from where parse should begin.301* @param {boolean} validation If true, input string need to be a valid302* date/time string.303* @return {number} How many characters parser advanced.304* @private305*/306goog.i18n.DateTimeParse.prototype.internalParse_ = function(307text, date, start, validation) {308var cal = new goog.i18n.DateTimeParse.MyDate_();309var parsePos = [start];310311// For parsing abutting numeric fields. 'abutPat' is the312// offset into 'pattern' of the first of 2 or more abutting313// numeric fields. 'abutStart' is the offset into 'text'314// where parsing the fields begins. 'abutPass' starts off as 0315// and increments each time we try to parse the fields.316var abutPat = -1; // If >=0, we are in a run of abutting numeric fields317var abutStart = 0;318var abutPass = 0;319320for (var i = 0; i < this.patternParts_.length; i++) {321if (this.patternParts_[i].count > 0) {322if (abutPat < 0 && this.patternParts_[i].abutStart) {323abutPat = i;324abutStart = start;325abutPass = 0;326}327328// Handle fields within a run of abutting numeric fields. Take329// the pattern "HHmmss" as an example. We will try to parse330// 2/2/2 characters of the input text, then if that fails,331// 1/2/2. We only adjust the width of the leftmost field; the332// others remain fixed. This allows "123456" => 12:34:56, but333// "12345" => 1:23:45. Likewise, for the pattern "yyyyMMdd" we334// try 4/2/2, 3/2/2, 2/2/2, and finally 1/2/2.335if (abutPat >= 0) {336// If we are at the start of a run of abutting fields, then337// shorten this field in each pass. If we can't shorten338// this field any more, then the parse of this set of339// abutting numeric fields has failed.340var count = this.patternParts_[i].count;341if (i == abutPat) {342count -= abutPass;343abutPass++;344if (count == 0) {345// tried all possible width, fail now346return 0;347}348}349350if (!this.subParse_(351text, parsePos, this.patternParts_[i], count, cal)) {352// If the parse fails anywhere in the run, back up to the353// start of the run and retry.354i = abutPat - 1;355parsePos[0] = abutStart;356continue;357}358}359360// Handle non-numeric fields and non-abutting numeric fields.361else {362abutPat = -1;363if (!this.subParse_(text, parsePos, this.patternParts_[i], 0, cal)) {364return 0;365}366}367} else {368// Handle literal pattern characters. These are any369// quoted characters and non-alphabetic unquoted370// characters.371abutPat = -1;372// A run of white space in the pattern matches a run373// of white space in the input text.374if (this.patternParts_[i].text.charAt(0) == ' ') {375// Advance over run in input text376var s = parsePos[0];377this.skipSpace_(text, parsePos);378379// Must see at least one white space char in input380if (parsePos[0] > s) {381continue;382}383} else if (384text.indexOf(this.patternParts_[i].text, parsePos[0]) ==385parsePos[0]) {386parsePos[0] += this.patternParts_[i].text.length;387continue;388}389// We fall through to this point if the match fails390return 0;391}392}393394// return progress395return cal.calcDate_(date, validation) ? parsePos[0] - start : 0;396};397398399/**400* Calculate character repeat count in pattern.401*402* @param {string} pattern It describes the format of date string that need to403* be parsed.404* @param {number} start The position of pattern character.405*406* @return {number} Repeat count.407* @private408*/409goog.i18n.DateTimeParse.prototype.getNextCharCount_ = function(pattern, start) {410var ch = pattern.charAt(start);411var next = start + 1;412while (next < pattern.length && pattern.charAt(next) == ch) {413next++;414}415return next - start;416};417418419/**420* All acceptable pattern characters.421* @private422*/423goog.i18n.DateTimeParse.PATTERN_CHARS_ = 'GyMdkHmsSEDahKzZvQL';424425426/**427* Pattern characters that specify numerical field.428* @private429*/430goog.i18n.DateTimeParse.NUMERIC_FORMAT_CHARS_ = 'MydhHmsSDkK';431432433/**434* Check if the pattern part is a numeric field.435*436* @param {Object} part pattern part to be examined.437*438* @return {boolean} true if the pattern part is numeric field.439* @private440*/441goog.i18n.DateTimeParse.prototype.isNumericField_ = function(part) {442if (part.count <= 0) {443return false;444}445var i = goog.i18n.DateTimeParse.NUMERIC_FORMAT_CHARS_.indexOf(446part.text.charAt(0));447return i > 0 || i == 0 && part.count < 3;448};449450451/**452* Identify the start of an abutting numeric fields' run. Taking pattern453* "HHmmss" as an example. It will try to parse 2/2/2 characters of the input454* text, then if that fails, 1/2/2. We only adjust the width of the leftmost455* field; the others remain fixed. This allows "123456" => 12:34:56, but456* "12345" => 1:23:45. Likewise, for the pattern "yyyyMMdd" we try 4/2/2,457* 3/2/2, 2/2/2, and finally 1/2/2. The first field of connected numeric458* fields will be marked as abutStart, its width can be reduced to accommodate459* others.460*461* @private462*/463goog.i18n.DateTimeParse.prototype.markAbutStart_ = function() {464// abut parts are continuous numeric parts. abutStart is the switch465// point from non-abut to abut466var abut = false;467468for (var i = 0; i < this.patternParts_.length; i++) {469if (this.isNumericField_(this.patternParts_[i])) {470// if next part is not following abut sequence, and isNumericField_471if (!abut && i + 1 < this.patternParts_.length &&472this.isNumericField_(this.patternParts_[i + 1])) {473abut = true;474this.patternParts_[i].abutStart = true;475}476} else {477abut = false;478}479}480};481482483/**484* Skip space in the string.485*486* @param {string} text input string.487* @param {Array<number>} pos where skip start, and return back where the skip488* stops.489* @private490*/491goog.i18n.DateTimeParse.prototype.skipSpace_ = function(text, pos) {492var m = text.substring(pos[0]).match(/^\s+/);493if (m) {494pos[0] += m[0].length;495}496};497498499/**500* Protected method that converts one field of the input string into a501* numeric field value.502*503* @param {string} text the time text to be parsed.504* @param {Array<number>} pos Parse position.505* @param {Object} part the pattern part for this field.506* @param {number} digitCount when > 0, numeric parsing must obey the count.507* @param {goog.i18n.DateTimeParse.MyDate_} cal object that holds parsed value.508*509* @return {boolean} True if it parses successfully.510* @private511*/512goog.i18n.DateTimeParse.prototype.subParse_ = function(513text, pos, part, digitCount, cal) {514this.skipSpace_(text, pos);515516var start = pos[0];517var ch = part.text.charAt(0);518519// parse integer value if it is a numeric field520var value = -1;521if (this.isNumericField_(part)) {522if (digitCount > 0) {523if ((start + digitCount) > text.length) {524return false;525}526value = this.parseInt_(text.substring(0, start + digitCount), pos);527} else {528value = this.parseInt_(text, pos);529}530}531532switch (ch) {533case 'G': // ERA534value = this.matchString_(text, pos, this.dateTimeSymbols_.ERAS);535if (value >= 0) {536cal.era = value;537}538return true;539case 'M': // MONTH540case 'L': // STANDALONEMONTH541return this.subParseMonth_(text, pos, cal, value);542case 'E':543return this.subParseDayOfWeek_(text, pos, cal);544case 'a': // AM_PM545value = this.matchString_(text, pos, this.dateTimeSymbols_.AMPMS);546if (value >= 0) {547cal.ampm = value;548}549return true;550case 'y': // YEAR551return this.subParseYear_(text, pos, start, value, part, cal);552case 'Q': // QUARTER553return this.subParseQuarter_(text, pos, cal, value);554case 'd': // DATE555if (value >= 0) {556cal.day = value;557}558return true;559case 'S': // FRACTIONAL_SECOND560return this.subParseFractionalSeconds_(value, pos, start, cal);561case 'h': // HOUR (1..12)562if (value == 12) {563value = 0;564}565case 'K': // HOUR (0..11)566case 'H': // HOUR_OF_DAY (0..23)567case 'k': // HOUR_OF_DAY (1..24)568if (value >= 0) {569cal.hours = value;570}571return true;572case 'm': // MINUTE573if (value >= 0) {574cal.minutes = value;575}576return true;577case 's': // SECOND578if (value >= 0) {579cal.seconds = value;580}581return true;582583case 'z': // ZONE_OFFSET584case 'Z': // TIMEZONE_RFC585case 'v': // TIMEZONE_GENERIC586return this.subparseTimeZoneInGMT_(text, pos, cal);587default:588return false;589}590};591592593/**594* Parse year field. Year field is special because595* 1) two digit year need to be resolved.596* 2) we allow year to take a sign.597* 3) year field participate in abut processing.598*599* @param {string} text the time text to be parsed.600* @param {Array<number>} pos Parse position.601* @param {number} start where this field start.602* @param {number} value integer value of year.603* @param {Object} part the pattern part for this field.604* @param {goog.i18n.DateTimeParse.MyDate_} cal object to hold parsed value.605*606* @return {boolean} True if successful.607* @private608*/609goog.i18n.DateTimeParse.prototype.subParseYear_ = function(610text, pos, start, value, part, cal) {611var ch;612if (value < 0) {613// possible sign614ch = text.charAt(pos[0]);615if (ch != '+' && ch != '-') {616return false;617}618pos[0]++;619value = this.parseInt_(text, pos);620if (value < 0) {621return false;622}623if (ch == '-') {624value = -value;625}626}627628// only if 2 digit was actually parsed, and pattern say it has 2 digit.629if (!ch && pos[0] - start == 2 && part.count == 2) {630cal.setTwoDigitYear_(value);631} else {632cal.year = value;633}634return true;635};636637638/**639* Parse Month field.640*641* @param {string} text the time text to be parsed.642* @param {Array<number>} pos Parse position.643* @param {goog.i18n.DateTimeParse.MyDate_} cal object to hold parsed value.644* @param {number} value numeric value if this field is expressed using645* numeric pattern, or -1 if not.646*647* @return {boolean} True if parsing successful.648* @private649*/650goog.i18n.DateTimeParse.prototype.subParseMonth_ = function(651text, pos, cal, value) {652// when month is symbols, i.e., MMM, MMMM, LLL or LLLL, value will be -1653if (value < 0) {654// Want to be able to parse both short and long forms.655// Try count == 4 first656var months = this.dateTimeSymbols_.MONTHS657.concat(this.dateTimeSymbols_.STANDALONEMONTHS)658.concat(this.dateTimeSymbols_.SHORTMONTHS)659.concat(this.dateTimeSymbols_.STANDALONESHORTMONTHS);660value = this.matchString_(text, pos, months);661if (value < 0) {662return false;663}664// The months variable is multiple of 12, so we have to get the actual665// month index by modulo 12.666cal.month = (value % 12);667return true;668} else {669cal.month = value - 1;670return true;671}672};673674675/**676* Parse Quarter field.677*678* @param {string} text the time text to be parsed.679* @param {Array<number>} pos Parse position.680* @param {goog.i18n.DateTimeParse.MyDate_} cal object to hold parsed value.681* @param {number} value numeric value if this field is expressed using682* numeric pattern, or -1 if not.683*684* @return {boolean} True if parsing successful.685* @private686*/687goog.i18n.DateTimeParse.prototype.subParseQuarter_ = function(688text, pos, cal, value) {689// value should be -1, since this is a non-numeric field.690if (value < 0) {691// Want to be able to parse both short and long forms.692// Try count == 4 first:693value = this.matchString_(text, pos, this.dateTimeSymbols_.QUARTERS);694if (value < 0) { // count == 4 failed, now try count == 3695value = this.matchString_(text, pos, this.dateTimeSymbols_.SHORTQUARTERS);696}697if (value < 0) {698return false;699}700cal.month = value * 3; // First month of quarter.701cal.day = 1;702return true;703}704return false;705};706707708/**709* Parse Day of week field.710* @param {string} text the time text to be parsed.711* @param {Array<number>} pos Parse position.712* @param {goog.i18n.DateTimeParse.MyDate_} cal object to hold parsed value.713*714* @return {boolean} True if successful.715* @private716*/717goog.i18n.DateTimeParse.prototype.subParseDayOfWeek_ = function(718text, pos, cal) {719// Handle both short and long forms.720// Try count == 4 (DDDD) first:721var value = this.matchString_(text, pos, this.dateTimeSymbols_.WEEKDAYS);722if (value < 0) {723value = this.matchString_(text, pos, this.dateTimeSymbols_.SHORTWEEKDAYS);724}725if (value < 0) {726return false;727}728cal.dayOfWeek = value;729return true;730};731732733/**734* Parse fractional seconds field.735*736* @param {number} value parsed numeric value.737* @param {Array<number>} pos current parse position.738* @param {number} start where this field start.739* @param {goog.i18n.DateTimeParse.MyDate_} cal object to hold parsed value.740*741* @return {boolean} True if successful.742* @private743*/744goog.i18n.DateTimeParse.prototype.subParseFractionalSeconds_ = function(745value, pos, start, cal) {746// Fractional seconds left-justify747var len = pos[0] - start;748cal.milliseconds = len < 3 ? value * Math.pow(10, 3 - len) :749Math.round(value / Math.pow(10, len - 3));750return true;751};752753754/**755* Parse GMT type timezone.756*757* @param {string} text the time text to be parsed.758* @param {Array<number>} pos Parse position.759* @param {goog.i18n.DateTimeParse.MyDate_} cal object to hold parsed value.760*761* @return {boolean} True if successful.762* @private763*/764goog.i18n.DateTimeParse.prototype.subparseTimeZoneInGMT_ = function(765text, pos, cal) {766// First try to parse generic forms such as GMT-07:00. Do this first767// in case localized DateFormatZoneData contains the string "GMT"768// for a zone; in that case, we don't want to match the first three769// characters of GMT+/-HH:MM etc.770771// For time zones that have no known names, look for strings772// of the form:773// GMT[+-]hours:minutes or774// GMT[+-]hhmm or775// GMT.776if (text.indexOf('GMT', pos[0]) == pos[0]) {777pos[0] += 3; // 3 is the length of GMT778return this.parseTimeZoneOffset_(text, pos, cal);779}780781// TODO(user): check for named time zones by looking through the locale782// data from the DateFormatZoneData strings. Should parse both short and long783// forms.784// subParseZoneString(text, start, cal);785786// As a last resort, look for numeric timezones of the form787// [+-]hhmm as specified by RFC 822. This code is actually788// a little more permissive than RFC 822. It will try to do789// its best with numbers that aren't strictly 4 digits long.790return this.parseTimeZoneOffset_(text, pos, cal);791};792793794/**795* Parse time zone offset.796*797* @param {string} text the time text to be parsed.798* @param {Array<number>} pos Parse position.799* @param {goog.i18n.DateTimeParse.MyDate_} cal object to hold parsed value.800*801* @return {boolean} True if successful.802* @private803*/804goog.i18n.DateTimeParse.prototype.parseTimeZoneOffset_ = function(805text, pos, cal) {806if (pos[0] >= text.length) {807cal.tzOffset = 0;808return true;809}810811var sign = 1;812switch (text.charAt(pos[0])) {813case '-':814sign = -1; // fall through815case '+':816pos[0]++;817}818819// Look for hours:minutes or hhmm.820var st = pos[0];821var value = this.parseInt_(text, pos);822if (value < 0) {823return false;824}825826var offset;827if (pos[0] < text.length && text.charAt(pos[0]) == ':') {828// This is the hours:minutes case829offset = value * 60;830pos[0]++;831value = this.parseInt_(text, pos);832if (value < 0) {833return false;834}835offset += value;836} else {837// This is the hhmm case.838offset = value;839// Assume "-23".."+23" refers to hours.840if (offset < 24 && (pos[0] - st) <= 2) {841offset *= 60;842} else {843// todo: this looks questionable, should have more error checking844offset = offset % 100 + offset / 100 * 60;845}846}847848offset *= sign;849cal.tzOffset = -offset;850return true;851};852853854/**855* Parse an integer string and return integer value.856*857* @param {string} text string being parsed.858* @param {Array<number>} pos parse position.859*860* @return {number} Converted integer value or -1 if the integer cannot be861* parsed.862* @private863*/864goog.i18n.DateTimeParse.prototype.parseInt_ = function(text, pos) {865// Delocalizes the string containing native digits specified by the locale,866// replaces the native digits with ASCII digits. Leaves other characters.867// This is the reverse operation of localizeNumbers_ in datetimeformat.js.868if (this.dateTimeSymbols_.ZERODIGIT) {869var parts = [];870for (var i = pos[0]; i < text.length; i++) {871var c = text.charCodeAt(i) - this.dateTimeSymbols_.ZERODIGIT;872parts.push(873(0 <= c && c <= 9) ? String.fromCharCode(c + 0x30) : text.charAt(i));874}875text = parts.join('');876} else {877text = text.substring(pos[0]);878}879880var m = text.match(/^\d+/);881if (!m) {882return -1;883}884pos[0] += m[0].length;885return parseInt(m[0], 10);886};887888889/**890* Attempt to match the text at a given position against an array of strings.891* Since multiple strings in the array may match (for example, if the array892* contains "a", "ab", and "abc", all will match the input string "abcd") the893* longest match is returned.894*895* @param {string} text The string to match to.896* @param {Array<number>} pos parsing position.897* @param {Array<string>} data The string array of matching patterns.898*899* @return {number} the new start position if matching succeeded; a negative900* number indicating matching failure.901* @private902*/903goog.i18n.DateTimeParse.prototype.matchString_ = function(text, pos, data) {904// There may be multiple strings in the data[] array which begin with905// the same prefix (e.g., Cerven and Cervenec (June and July) in Czech).906// We keep track of the longest match, and return that. Note that this907// unfortunately requires us to test all array elements.908var bestMatchLength = 0;909var bestMatch = -1;910var lower_text = text.substring(pos[0]).toLowerCase();911for (var i = 0; i < data.length; i++) {912var len = data[i].length;913// Always compare if we have no match yet; otherwise only compare914// against potentially better matches (longer strings).915if (len > bestMatchLength &&916lower_text.indexOf(data[i].toLowerCase()) == 0) {917bestMatch = i;918bestMatchLength = len;919}920}921if (bestMatch >= 0) {922pos[0] += bestMatchLength;923}924return bestMatch;925};926927928929/**930* This class hold the intermediate parsing result. After all fields are931* consumed, final result will be resolved from this class.932* @constructor933* @private934*/935goog.i18n.DateTimeParse.MyDate_ = function() {};936937938/**939* The date's era.940* @type {?number}941*/942goog.i18n.DateTimeParse.MyDate_.prototype.era;943944945/**946* The date's year.947* @type {?number}948*/949goog.i18n.DateTimeParse.MyDate_.prototype.year;950951952/**953* The date's month.954* @type {?number}955*/956goog.i18n.DateTimeParse.MyDate_.prototype.month;957958959/**960* The date's day of month.961* @type {?number}962*/963goog.i18n.DateTimeParse.MyDate_.prototype.day;964965966/**967* The date's hour.968* @type {?number}969*/970goog.i18n.DateTimeParse.MyDate_.prototype.hours;971972973/**974* The date's before/afternoon denominator.975* @type {?number}976*/977goog.i18n.DateTimeParse.MyDate_.prototype.ampm;978979980/**981* The date's minutes.982* @type {?number}983*/984goog.i18n.DateTimeParse.MyDate_.prototype.minutes;985986987/**988* The date's seconds.989* @type {?number}990*/991goog.i18n.DateTimeParse.MyDate_.prototype.seconds;992993994/**995* The date's milliseconds.996* @type {?number}997*/998goog.i18n.DateTimeParse.MyDate_.prototype.milliseconds;99910001001/**1002* The date's timezone offset.1003* @type {?number}1004*/1005goog.i18n.DateTimeParse.MyDate_.prototype.tzOffset;100610071008/**1009* The date's day of week. Sunday is 0, Saturday is 6.1010* @type {?number}1011*/1012goog.i18n.DateTimeParse.MyDate_.prototype.dayOfWeek;101310141015/**1016* 2 digit year special handling. Assuming for example that the1017* defaultCenturyStart is 6/18/1903. This means that two-digit years will be1018* forced into the range 6/18/1903 to 6/17/2003. As a result, years 00, 01, and1019* 02 correspond to 2000, 2001, and 2002. Years 04, 05, etc. correspond1020* to 1904, 1905, etc. If the year is 03, then it is 2003 if the1021* other fields specify a date before 6/18, or 1903 if they specify a1022* date afterwards. As a result, 03 is an ambiguous year. All other1023* two-digit years are unambiguous.1024*1025* @param {number} year 2 digit year value before adjustment.1026* @return {number} disambiguated year.1027* @private1028*/1029goog.i18n.DateTimeParse.MyDate_.prototype.setTwoDigitYear_ = function(year) {1030var now = new Date();1031var defaultCenturyStartYear =1032now.getFullYear() - goog.i18n.DateTimeParse.ambiguousYearCenturyStart;1033var ambiguousTwoDigitYear = defaultCenturyStartYear % 100;1034this.ambiguousYear = (year == ambiguousTwoDigitYear);1035year += Math.floor(defaultCenturyStartYear / 100) * 100 +1036(year < ambiguousTwoDigitYear ? 100 : 0);1037return this.year = year;1038};103910401041/**1042* Based on the fields set, fill a Date object. For those fields that not1043* set, use the passed in date object's value.1044*1045* @param {goog.date.DateLike} date Date object to be filled.1046* @param {boolean} validation If true, input string will be checked to make1047* sure it is valid.1048*1049* @return {boolean} false if fields specify a invalid date.1050* @private1051*/1052goog.i18n.DateTimeParse.MyDate_.prototype.calcDate_ = function(1053date, validation) {1054// year 0 is 1 BC, and so on.1055if (this.era != undefined && this.year != undefined && this.era == 0 &&1056this.year > 0) {1057this.year = -(this.year - 1);1058}10591060if (this.year != undefined) {1061date.setFullYear(this.year);1062}10631064// The setMonth and setDate logic is a little tricky. We need to make sure1065// day of month is smaller enough so that it won't cause a month switch when1066// setting month. For example, if data in date is Nov 30, when month is set1067// to Feb, because there is no Feb 30, JS adjust it to Mar 2. So Feb 12 will1068// become Mar 12.1069var orgDate = date.getDate();10701071// Every month has a 1st day, this can actually be anything less than 29.1072date.setDate(1);10731074if (this.month != undefined) {1075date.setMonth(this.month);1076}10771078if (this.day != undefined) {1079date.setDate(this.day);1080} else {1081var maxDate =1082goog.date.getNumberOfDaysInMonth(date.getFullYear(), date.getMonth());1083date.setDate(orgDate > maxDate ? maxDate : orgDate);1084}10851086if (goog.isFunction(date.setHours)) {1087if (this.hours == undefined) {1088this.hours = date.getHours();1089}1090// adjust ampm1091if (this.ampm != undefined && this.ampm > 0 && this.hours < 12) {1092this.hours += 12;1093}1094date.setHours(this.hours);1095}10961097if (goog.isFunction(date.setMinutes) && this.minutes != undefined) {1098date.setMinutes(this.minutes);1099}11001101if (goog.isFunction(date.setSeconds) && this.seconds != undefined) {1102date.setSeconds(this.seconds);1103}11041105if (goog.isFunction(date.setMilliseconds) && this.milliseconds != undefined) {1106date.setMilliseconds(this.milliseconds);1107}11081109// If validation is needed, verify that the uncalculated date fields1110// match the calculated date fields. We do this before we set the1111// timezone offset, which will skew all of the dates.1112//1113// Don't need to check the day of week as it is guaranteed to be1114// correct or return false below.1115if (validation &&1116(this.year != undefined && this.year != date.getFullYear() ||1117this.month != undefined && this.month != date.getMonth() ||1118this.day != undefined && this.day != date.getDate() ||1119this.hours >= 24 || this.minutes >= 60 || this.seconds >= 60 ||1120this.milliseconds >= 1000)) {1121return false;1122}11231124// adjust time zone1125if (this.tzOffset != undefined) {1126var offset = date.getTimezoneOffset();1127date.setTime(date.getTime() + (this.tzOffset - offset) * 60 * 1000);1128}11291130// resolve ambiguous year if needed1131if (this.ambiguousYear) { // the two-digit year == the default start year1132var defaultCenturyStart = new Date();1133defaultCenturyStart.setFullYear(1134defaultCenturyStart.getFullYear() -1135goog.i18n.DateTimeParse.ambiguousYearCenturyStart);1136if (date.getTime() < defaultCenturyStart.getTime()) {1137date.setFullYear(defaultCenturyStart.getFullYear() + 100);1138}1139}11401141// dayOfWeek, validation only1142if (this.dayOfWeek != undefined) {1143if (this.day == undefined) {1144// adjust to the nearest day of the week1145var adjustment = (7 + this.dayOfWeek - date.getDay()) % 7;1146if (adjustment > 3) {1147adjustment -= 7;1148}1149var orgMonth = date.getMonth();1150date.setDate(date.getDate() + adjustment);11511152// don't let it switch month1153if (date.getMonth() != orgMonth) {1154date.setDate(date.getDate() + (adjustment > 0 ? -7 : 7));1155}1156} else if (this.dayOfWeek != date.getDay()) {1157return false;1158}1159}1160return true;1161};116211631164