datetimeparse.js 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167
  1. // Copyright 2006 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview Date/Time parsing library with locale support.
  16. */
  17. /**
  18. * Namespace for locale date/time parsing functions
  19. */
  20. goog.provide('goog.i18n.DateTimeParse');
  21. goog.require('goog.asserts');
  22. goog.require('goog.date');
  23. goog.require('goog.i18n.DateTimeFormat');
  24. goog.require('goog.i18n.DateTimeSymbols');
  25. /**
  26. * DateTimeParse is for parsing date in a locale-sensitive manner. It allows
  27. * user to use any customized patterns to parse date-time string under certain
  28. * locale. Things varies across locales like month name, weekname, field
  29. * order, etc.
  30. *
  31. * This module is the counter-part of DateTimeFormat. They use the same
  32. * date/time pattern specification, which is borrowed from ICU/JDK.
  33. *
  34. * This implementation could parse partial date/time.
  35. *
  36. * Time Format Syntax: To specify the time format use a time pattern string.
  37. * In this pattern, following letters are reserved as pattern letters, which
  38. * are defined as the following:
  39. *
  40. * <pre>
  41. * Symbol Meaning Presentation Example
  42. * ------ ------- ------------ -------
  43. * G era designator (Text) AD
  44. * y# year (Number) 1996
  45. * M month in year (Text & Number) July & 07
  46. * d day in month (Number) 10
  47. * h hour in am/pm (1~12) (Number) 12
  48. * H hour in day (0~23) (Number) 0
  49. * m minute in hour (Number) 30
  50. * s second in minute (Number) 55
  51. * S fractional second (Number) 978
  52. * E day of week (Text) Tuesday
  53. * D day in year (Number) 189
  54. * a am/pm marker (Text) PM
  55. * k hour in day (1~24) (Number) 24
  56. * K hour in am/pm (0~11) (Number) 0
  57. * z time zone (Text) Pacific Standard Time
  58. * Z time zone (RFC 822) (Number) -0800
  59. * v time zone (generic) (Text) Pacific Time
  60. * ' escape for text (Delimiter) 'Date='
  61. * '' single quote (Literal) 'o''clock'
  62. * </pre>
  63. *
  64. * The count of pattern letters determine the format. <p>
  65. * (Text): 4 or more pattern letters--use full form,
  66. * less than 4--use short or abbreviated form if one exists.
  67. * In parsing, we will always try long format, then short. <p>
  68. * (Number): the minimum number of digits. <p>
  69. * (Text & Number): 3 or over, use text, otherwise use number. <p>
  70. * Any characters that not in the pattern will be treated as quoted text. For
  71. * instance, characters like ':', '.', ' ', '#' and '@' will appear in the
  72. * resulting time text even they are not embraced within single quotes. In our
  73. * current pattern usage, we didn't use up all letters. But those unused
  74. * letters are strongly discouraged to be used as quoted text without quote.
  75. * That's because we may use other letter for pattern in future. <p>
  76. *
  77. * Examples Using the US Locale:
  78. *
  79. * Format Pattern Result
  80. * -------------- -------
  81. * "yyyy.MM.dd G 'at' HH:mm:ss vvvv" ->> 1996.07.10 AD at 15:08:56 Pacific Time
  82. * "EEE, MMM d, ''yy" ->> Wed, July 10, '96
  83. * "h:mm a" ->> 12:08 PM
  84. * "hh 'o''clock' a, zzzz" ->> 12 o'clock PM, Pacific Daylight Time
  85. * "K:mm a, vvv" ->> 0:00 PM, PT
  86. * "yyyyy.MMMMM.dd GGG hh:mm aaa" ->> 01996.July.10 AD 12:08 PM
  87. *
  88. * <p> When parsing a date string using the abbreviated year pattern ("yy"),
  89. * DateTimeParse must interpret the abbreviated year relative to some
  90. * century. It does this by adjusting dates to be within 80 years before and 20
  91. * years after the time the parse function is called. For example, using a
  92. * pattern of "MM/dd/yy" and a DateTimeParse instance created on Jan 1, 1997,
  93. * the string "01/11/12" would be interpreted as Jan 11, 2012 while the string
  94. * "05/04/64" would be interpreted as May 4, 1964. During parsing, only
  95. * strings consisting of exactly two digits, as defined by {@link
  96. * java.lang.Character#isDigit(char)}, will be parsed into the default
  97. * century. Any other numeric string, such as a one digit string, a three or
  98. * more digit string will be interpreted as its face value.
  99. *
  100. * <p> If the year pattern does not have exactly two 'y' characters, the year is
  101. * interpreted literally, regardless of the number of digits. So using the
  102. * pattern "MM/dd/yyyy", "01/11/12" parses to Jan 11, 12 A.D.
  103. *
  104. * <p> When numeric fields abut one another directly, with no intervening
  105. * delimiter characters, they constitute a run of abutting numeric fields. Such
  106. * runs are parsed specially. For example, the format "HHmmss" parses the input
  107. * text "123456" to 12:34:56, parses the input text "12345" to 1:23:45, and
  108. * fails to parse "1234". In other words, the leftmost field of the run is
  109. * flexible, while the others keep a fixed width. If the parse fails anywhere in
  110. * the run, then the leftmost field is shortened by one character, and the
  111. * entire run is parsed again. This is repeated until either the parse succeeds
  112. * or the leftmost field is one character in length. If the parse still fails at
  113. * that point, the parse of the run fails.
  114. *
  115. * <p> Now timezone parsing only support GMT:hhmm, GMT:+hhmm, GMT:-hhmm
  116. */
  117. /**
  118. * Construct a DateTimeParse based on current locale.
  119. * @param {string|number} pattern pattern specification or pattern type.
  120. * @param {!Object=} opt_dateTimeSymbols Optional symbols to use for this
  121. * instance rather than the global symbols.
  122. * @constructor
  123. * @final
  124. */
  125. goog.i18n.DateTimeParse = function(pattern, opt_dateTimeSymbols) {
  126. goog.asserts.assert(
  127. goog.isDef(opt_dateTimeSymbols) || goog.isDef(goog.i18n.DateTimeSymbols),
  128. 'goog.i18n.DateTimeSymbols or explicit symbols must be defined');
  129. this.patternParts_ = [];
  130. /**
  131. * Data structure with all the locale info needed for date formatting.
  132. * (day/month names, most common patterns, rules for week-end, etc.)
  133. * @const @private {!goog.i18n.DateTimeSymbolsType}
  134. */
  135. this.dateTimeSymbols_ = /** @type {!goog.i18n.DateTimeSymbolsType} */ (
  136. opt_dateTimeSymbols || goog.i18n.DateTimeSymbols);
  137. if (typeof pattern == 'number') {
  138. this.applyStandardPattern_(pattern);
  139. } else {
  140. this.applyPattern_(pattern);
  141. }
  142. };
  143. /**
  144. * Number of years prior to now that the century used to
  145. * disambiguate two digit years will begin
  146. *
  147. * @type {number}
  148. */
  149. goog.i18n.DateTimeParse.ambiguousYearCenturyStart = 80;
  150. /**
  151. * Apply a pattern to this Parser. The pattern string will be parsed and saved
  152. * in "compiled" form.
  153. * Note: this method is somewhat similar to the pattern parsing method in
  154. * datetimeformat. If you see something wrong here, you might want
  155. * to check the other.
  156. * @param {string} pattern It describes the format of date string that need to
  157. * be parsed.
  158. * @private
  159. */
  160. goog.i18n.DateTimeParse.prototype.applyPattern_ = function(pattern) {
  161. var inQuote = false;
  162. var buf = '';
  163. for (var i = 0; i < pattern.length; i++) {
  164. var ch = pattern.charAt(i);
  165. // handle space, add literal part (if exist), and add space part
  166. if (ch == ' ') {
  167. if (buf.length > 0) {
  168. this.patternParts_.push({text: buf, count: 0, abutStart: false});
  169. buf = '';
  170. }
  171. this.patternParts_.push({text: ' ', count: 0, abutStart: false});
  172. while (i < pattern.length - 1 && pattern.charAt(i + 1) == ' ') {
  173. i++;
  174. }
  175. } else if (inQuote) {
  176. // inside quote, except '', just copy or exit
  177. if (ch == '\'') {
  178. if (i + 1 < pattern.length && pattern.charAt(i + 1) == '\'') {
  179. // quote appeared twice continuously, interpret as one quote.
  180. buf += '\'';
  181. i++;
  182. } else {
  183. // exit quote
  184. inQuote = false;
  185. }
  186. } else {
  187. // literal
  188. buf += ch;
  189. }
  190. } else if (goog.i18n.DateTimeParse.PATTERN_CHARS_.indexOf(ch) >= 0) {
  191. // outside quote, it is a pattern char
  192. if (buf.length > 0) {
  193. this.patternParts_.push({text: buf, count: 0, abutStart: false});
  194. buf = '';
  195. }
  196. var count = this.getNextCharCount_(pattern, i);
  197. this.patternParts_.push({text: ch, count: count, abutStart: false});
  198. i += count - 1;
  199. } else if (ch == '\'') {
  200. // Two consecutive quotes is a quote literal, inside or outside of quotes.
  201. if (i + 1 < pattern.length && pattern.charAt(i + 1) == '\'') {
  202. buf += '\'';
  203. i++;
  204. } else {
  205. inQuote = true;
  206. }
  207. } else {
  208. buf += ch;
  209. }
  210. }
  211. if (buf.length > 0) {
  212. this.patternParts_.push({text: buf, count: 0, abutStart: false});
  213. }
  214. this.markAbutStart_();
  215. };
  216. /**
  217. * Apply a predefined pattern to this Parser.
  218. * @param {number} formatType A constant used to identified the predefined
  219. * pattern string stored in locale repository.
  220. * @private
  221. */
  222. goog.i18n.DateTimeParse.prototype.applyStandardPattern_ = function(formatType) {
  223. var pattern;
  224. // formatType constants are in consecutive numbers. So it can be used to
  225. // index array in following way.
  226. // if type is out of range, default to medium date/time format.
  227. if (formatType > goog.i18n.DateTimeFormat.Format.SHORT_DATETIME) {
  228. formatType = goog.i18n.DateTimeFormat.Format.MEDIUM_DATETIME;
  229. }
  230. if (formatType < 4) {
  231. pattern = this.dateTimeSymbols_.DATEFORMATS[formatType];
  232. } else if (formatType < 8) {
  233. pattern = this.dateTimeSymbols_.TIMEFORMATS[formatType - 4];
  234. } else {
  235. pattern = this.dateTimeSymbols_.DATETIMEFORMATS[formatType - 8];
  236. pattern = pattern.replace(
  237. '{1}', this.dateTimeSymbols_.DATEFORMATS[formatType - 8]);
  238. pattern = pattern.replace(
  239. '{0}', this.dateTimeSymbols_.TIMEFORMATS[formatType - 8]);
  240. }
  241. this.applyPattern_(pattern);
  242. };
  243. /**
  244. * Parse the given string and fill info into date object. This version does
  245. * not validate the input.
  246. * @param {string} text The string being parsed.
  247. * @param {goog.date.DateLike} date The Date object to hold the parsed date.
  248. * @param {number=} opt_start The position from where parse should begin.
  249. * @return {number} How many characters parser advanced.
  250. */
  251. goog.i18n.DateTimeParse.prototype.parse = function(text, date, opt_start) {
  252. var start = opt_start || 0;
  253. return this.internalParse_(text, date, start, false /*validation*/);
  254. };
  255. /**
  256. * Parse the given string and fill info into date object. This version will
  257. * validate the input and make sure it is a valid date/time.
  258. * @param {string} text The string being parsed.
  259. * @param {goog.date.DateLike} date The Date object to hold the parsed date.
  260. * @param {number=} opt_start The position from where parse should begin.
  261. * @return {number} How many characters parser advanced.
  262. */
  263. goog.i18n.DateTimeParse.prototype.strictParse = function(
  264. text, date, opt_start) {
  265. var start = opt_start || 0;
  266. return this.internalParse_(text, date, start, true /*validation*/);
  267. };
  268. /**
  269. * Parse the given string and fill info into date object.
  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} start The position from where parse should begin.
  273. * @param {boolean} validation If true, input string need to be a valid
  274. * date/time string.
  275. * @return {number} How many characters parser advanced.
  276. * @private
  277. */
  278. goog.i18n.DateTimeParse.prototype.internalParse_ = function(
  279. text, date, start, validation) {
  280. var cal = new goog.i18n.DateTimeParse.MyDate_();
  281. var parsePos = [start];
  282. // For parsing abutting numeric fields. 'abutPat' is the
  283. // offset into 'pattern' of the first of 2 or more abutting
  284. // numeric fields. 'abutStart' is the offset into 'text'
  285. // where parsing the fields begins. 'abutPass' starts off as 0
  286. // and increments each time we try to parse the fields.
  287. var abutPat = -1; // If >=0, we are in a run of abutting numeric fields
  288. var abutStart = 0;
  289. var abutPass = 0;
  290. for (var i = 0; i < this.patternParts_.length; i++) {
  291. if (this.patternParts_[i].count > 0) {
  292. if (abutPat < 0 && this.patternParts_[i].abutStart) {
  293. abutPat = i;
  294. abutStart = start;
  295. abutPass = 0;
  296. }
  297. // Handle fields within a run of abutting numeric fields. Take
  298. // the pattern "HHmmss" as an example. We will try to parse
  299. // 2/2/2 characters of the input text, then if that fails,
  300. // 1/2/2. We only adjust the width of the leftmost field; the
  301. // others remain fixed. This allows "123456" => 12:34:56, but
  302. // "12345" => 1:23:45. Likewise, for the pattern "yyyyMMdd" we
  303. // try 4/2/2, 3/2/2, 2/2/2, and finally 1/2/2.
  304. if (abutPat >= 0) {
  305. // If we are at the start of a run of abutting fields, then
  306. // shorten this field in each pass. If we can't shorten
  307. // this field any more, then the parse of this set of
  308. // abutting numeric fields has failed.
  309. var count = this.patternParts_[i].count;
  310. if (i == abutPat) {
  311. count -= abutPass;
  312. abutPass++;
  313. if (count == 0) {
  314. // tried all possible width, fail now
  315. return 0;
  316. }
  317. }
  318. if (!this.subParse_(
  319. text, parsePos, this.patternParts_[i], count, cal)) {
  320. // If the parse fails anywhere in the run, back up to the
  321. // start of the run and retry.
  322. i = abutPat - 1;
  323. parsePos[0] = abutStart;
  324. continue;
  325. }
  326. }
  327. // Handle non-numeric fields and non-abutting numeric fields.
  328. else {
  329. abutPat = -1;
  330. if (!this.subParse_(text, parsePos, this.patternParts_[i], 0, cal)) {
  331. return 0;
  332. }
  333. }
  334. } else {
  335. // Handle literal pattern characters. These are any
  336. // quoted characters and non-alphabetic unquoted
  337. // characters.
  338. abutPat = -1;
  339. // A run of white space in the pattern matches a run
  340. // of white space in the input text.
  341. if (this.patternParts_[i].text.charAt(0) == ' ') {
  342. // Advance over run in input text
  343. var s = parsePos[0];
  344. this.skipSpace_(text, parsePos);
  345. // Must see at least one white space char in input
  346. if (parsePos[0] > s) {
  347. continue;
  348. }
  349. } else if (
  350. text.indexOf(this.patternParts_[i].text, parsePos[0]) ==
  351. parsePos[0]) {
  352. parsePos[0] += this.patternParts_[i].text.length;
  353. continue;
  354. }
  355. // We fall through to this point if the match fails
  356. return 0;
  357. }
  358. }
  359. // return progress
  360. return cal.calcDate_(date, validation) ? parsePos[0] - start : 0;
  361. };
  362. /**
  363. * Calculate character repeat count in pattern.
  364. *
  365. * @param {string} pattern It describes the format of date string that need to
  366. * be parsed.
  367. * @param {number} start The position of pattern character.
  368. *
  369. * @return {number} Repeat count.
  370. * @private
  371. */
  372. goog.i18n.DateTimeParse.prototype.getNextCharCount_ = function(pattern, start) {
  373. var ch = pattern.charAt(start);
  374. var next = start + 1;
  375. while (next < pattern.length && pattern.charAt(next) == ch) {
  376. next++;
  377. }
  378. return next - start;
  379. };
  380. /**
  381. * All acceptable pattern characters.
  382. * @private
  383. */
  384. goog.i18n.DateTimeParse.PATTERN_CHARS_ = 'GyMdkHmsSEDahKzZvQL';
  385. /**
  386. * Pattern characters that specify numerical field.
  387. * @private
  388. */
  389. goog.i18n.DateTimeParse.NUMERIC_FORMAT_CHARS_ = 'MydhHmsSDkK';
  390. /**
  391. * Check if the pattern part is a numeric field.
  392. *
  393. * @param {Object} part pattern part to be examined.
  394. *
  395. * @return {boolean} true if the pattern part is numeric field.
  396. * @private
  397. */
  398. goog.i18n.DateTimeParse.prototype.isNumericField_ = function(part) {
  399. if (part.count <= 0) {
  400. return false;
  401. }
  402. var i = goog.i18n.DateTimeParse.NUMERIC_FORMAT_CHARS_.indexOf(
  403. part.text.charAt(0));
  404. return i > 0 || i == 0 && part.count < 3;
  405. };
  406. /**
  407. * Identify the start of an abutting numeric fields' run. Taking pattern
  408. * "HHmmss" as an example. It will try to parse 2/2/2 characters of the input
  409. * text, then if that fails, 1/2/2. We only adjust the width of the leftmost
  410. * field; the others remain fixed. This allows "123456" => 12:34:56, but
  411. * "12345" => 1:23:45. Likewise, for the pattern "yyyyMMdd" we try 4/2/2,
  412. * 3/2/2, 2/2/2, and finally 1/2/2. The first field of connected numeric
  413. * fields will be marked as abutStart, its width can be reduced to accommodate
  414. * others.
  415. *
  416. * @private
  417. */
  418. goog.i18n.DateTimeParse.prototype.markAbutStart_ = function() {
  419. // abut parts are continuous numeric parts. abutStart is the switch
  420. // point from non-abut to abut
  421. var abut = false;
  422. for (var i = 0; i < this.patternParts_.length; i++) {
  423. if (this.isNumericField_(this.patternParts_[i])) {
  424. // if next part is not following abut sequence, and isNumericField_
  425. if (!abut && i + 1 < this.patternParts_.length &&
  426. this.isNumericField_(this.patternParts_[i + 1])) {
  427. abut = true;
  428. this.patternParts_[i].abutStart = true;
  429. }
  430. } else {
  431. abut = false;
  432. }
  433. }
  434. };
  435. /**
  436. * Skip space in the string.
  437. *
  438. * @param {string} text input string.
  439. * @param {Array<number>} pos where skip start, and return back where the skip
  440. * stops.
  441. * @private
  442. */
  443. goog.i18n.DateTimeParse.prototype.skipSpace_ = function(text, pos) {
  444. var m = text.substring(pos[0]).match(/^\s+/);
  445. if (m) {
  446. pos[0] += m[0].length;
  447. }
  448. };
  449. /**
  450. * Protected method that converts one field of the input string into a
  451. * numeric field value.
  452. *
  453. * @param {string} text the time text to be parsed.
  454. * @param {Array<number>} pos Parse position.
  455. * @param {Object} part the pattern part for this field.
  456. * @param {number} digitCount when > 0, numeric parsing must obey the count.
  457. * @param {goog.i18n.DateTimeParse.MyDate_} cal object that holds parsed value.
  458. *
  459. * @return {boolean} True if it parses successfully.
  460. * @private
  461. */
  462. goog.i18n.DateTimeParse.prototype.subParse_ = function(
  463. text, pos, part, digitCount, cal) {
  464. this.skipSpace_(text, pos);
  465. var start = pos[0];
  466. var ch = part.text.charAt(0);
  467. // parse integer value if it is a numeric field
  468. var value = -1;
  469. if (this.isNumericField_(part)) {
  470. if (digitCount > 0) {
  471. if ((start + digitCount) > text.length) {
  472. return false;
  473. }
  474. value = this.parseInt_(text.substring(0, start + digitCount), pos);
  475. } else {
  476. value = this.parseInt_(text, pos);
  477. }
  478. }
  479. switch (ch) {
  480. case 'G': // ERA
  481. value = this.matchString_(text, pos, this.dateTimeSymbols_.ERAS);
  482. if (value >= 0) {
  483. cal.era = value;
  484. }
  485. return true;
  486. case 'M': // MONTH
  487. case 'L': // STANDALONEMONTH
  488. return this.subParseMonth_(text, pos, cal, value);
  489. case 'E':
  490. return this.subParseDayOfWeek_(text, pos, cal);
  491. case 'a': // AM_PM
  492. value = this.matchString_(text, pos, this.dateTimeSymbols_.AMPMS);
  493. if (value >= 0) {
  494. cal.ampm = value;
  495. }
  496. return true;
  497. case 'y': // YEAR
  498. return this.subParseYear_(text, pos, start, value, part, cal);
  499. case 'Q': // QUARTER
  500. return this.subParseQuarter_(text, pos, cal, value);
  501. case 'd': // DATE
  502. if (value >= 0) {
  503. cal.day = value;
  504. }
  505. return true;
  506. case 'S': // FRACTIONAL_SECOND
  507. return this.subParseFractionalSeconds_(value, pos, start, cal);
  508. case 'h': // HOUR (1..12)
  509. if (value == 12) {
  510. value = 0;
  511. }
  512. case 'K': // HOUR (0..11)
  513. case 'H': // HOUR_OF_DAY (0..23)
  514. case 'k': // HOUR_OF_DAY (1..24)
  515. if (value >= 0) {
  516. cal.hours = value;
  517. }
  518. return true;
  519. case 'm': // MINUTE
  520. if (value >= 0) {
  521. cal.minutes = value;
  522. }
  523. return true;
  524. case 's': // SECOND
  525. if (value >= 0) {
  526. cal.seconds = value;
  527. }
  528. return true;
  529. case 'z': // ZONE_OFFSET
  530. case 'Z': // TIMEZONE_RFC
  531. case 'v': // TIMEZONE_GENERIC
  532. return this.subparseTimeZoneInGMT_(text, pos, cal);
  533. default:
  534. return false;
  535. }
  536. };
  537. /**
  538. * Parse year field. Year field is special because
  539. * 1) two digit year need to be resolved.
  540. * 2) we allow year to take a sign.
  541. * 3) year field participate in abut processing.
  542. *
  543. * @param {string} text the time text to be parsed.
  544. * @param {Array<number>} pos Parse position.
  545. * @param {number} start where this field start.
  546. * @param {number} value integer value of year.
  547. * @param {Object} part the pattern part for this field.
  548. * @param {goog.i18n.DateTimeParse.MyDate_} cal object to hold parsed value.
  549. *
  550. * @return {boolean} True if successful.
  551. * @private
  552. */
  553. goog.i18n.DateTimeParse.prototype.subParseYear_ = function(
  554. text, pos, start, value, part, cal) {
  555. var ch;
  556. if (value < 0) {
  557. // possible sign
  558. ch = text.charAt(pos[0]);
  559. if (ch != '+' && ch != '-') {
  560. return false;
  561. }
  562. pos[0]++;
  563. value = this.parseInt_(text, pos);
  564. if (value < 0) {
  565. return false;
  566. }
  567. if (ch == '-') {
  568. value = -value;
  569. }
  570. }
  571. // only if 2 digit was actually parsed, and pattern say it has 2 digit.
  572. if (!ch && pos[0] - start == 2 && part.count == 2) {
  573. cal.setTwoDigitYear_(value);
  574. } else {
  575. cal.year = value;
  576. }
  577. return true;
  578. };
  579. /**
  580. * Parse Month field.
  581. *
  582. * @param {string} text the time text to be parsed.
  583. * @param {Array<number>} pos Parse position.
  584. * @param {goog.i18n.DateTimeParse.MyDate_} cal object to hold parsed value.
  585. * @param {number} value numeric value if this field is expressed using
  586. * numeric pattern, or -1 if not.
  587. *
  588. * @return {boolean} True if parsing successful.
  589. * @private
  590. */
  591. goog.i18n.DateTimeParse.prototype.subParseMonth_ = function(
  592. text, pos, cal, value) {
  593. // when month is symbols, i.e., MMM, MMMM, LLL or LLLL, value will be -1
  594. if (value < 0) {
  595. // Want to be able to parse both short and long forms.
  596. // Try count == 4 first
  597. var months = this.dateTimeSymbols_.MONTHS
  598. .concat(this.dateTimeSymbols_.STANDALONEMONTHS)
  599. .concat(this.dateTimeSymbols_.SHORTMONTHS)
  600. .concat(this.dateTimeSymbols_.STANDALONESHORTMONTHS);
  601. value = this.matchString_(text, pos, months);
  602. if (value < 0) {
  603. return false;
  604. }
  605. // The months variable is multiple of 12, so we have to get the actual
  606. // month index by modulo 12.
  607. cal.month = (value % 12);
  608. return true;
  609. } else {
  610. cal.month = value - 1;
  611. return true;
  612. }
  613. };
  614. /**
  615. * Parse Quarter field.
  616. *
  617. * @param {string} text the time text to be parsed.
  618. * @param {Array<number>} pos Parse position.
  619. * @param {goog.i18n.DateTimeParse.MyDate_} cal object to hold parsed value.
  620. * @param {number} value numeric value if this field is expressed using
  621. * numeric pattern, or -1 if not.
  622. *
  623. * @return {boolean} True if parsing successful.
  624. * @private
  625. */
  626. goog.i18n.DateTimeParse.prototype.subParseQuarter_ = function(
  627. text, pos, cal, value) {
  628. // value should be -1, since this is a non-numeric field.
  629. if (value < 0) {
  630. // Want to be able to parse both short and long forms.
  631. // Try count == 4 first:
  632. value = this.matchString_(text, pos, this.dateTimeSymbols_.QUARTERS);
  633. if (value < 0) { // count == 4 failed, now try count == 3
  634. value = this.matchString_(text, pos, this.dateTimeSymbols_.SHORTQUARTERS);
  635. }
  636. if (value < 0) {
  637. return false;
  638. }
  639. cal.month = value * 3; // First month of quarter.
  640. cal.day = 1;
  641. return true;
  642. }
  643. return false;
  644. };
  645. /**
  646. * Parse Day of week field.
  647. * @param {string} text the time text to be parsed.
  648. * @param {Array<number>} pos Parse position.
  649. * @param {goog.i18n.DateTimeParse.MyDate_} cal object to hold parsed value.
  650. *
  651. * @return {boolean} True if successful.
  652. * @private
  653. */
  654. goog.i18n.DateTimeParse.prototype.subParseDayOfWeek_ = function(
  655. text, pos, cal) {
  656. // Handle both short and long forms.
  657. // Try count == 4 (DDDD) first:
  658. var value = this.matchString_(text, pos, this.dateTimeSymbols_.WEEKDAYS);
  659. if (value < 0) {
  660. value = this.matchString_(text, pos, this.dateTimeSymbols_.SHORTWEEKDAYS);
  661. }
  662. if (value < 0) {
  663. return false;
  664. }
  665. cal.dayOfWeek = value;
  666. return true;
  667. };
  668. /**
  669. * Parse fractional seconds field.
  670. *
  671. * @param {number} value parsed numeric value.
  672. * @param {Array<number>} pos current parse position.
  673. * @param {number} start where this field start.
  674. * @param {goog.i18n.DateTimeParse.MyDate_} cal object to hold parsed value.
  675. *
  676. * @return {boolean} True if successful.
  677. * @private
  678. */
  679. goog.i18n.DateTimeParse.prototype.subParseFractionalSeconds_ = function(
  680. value, pos, start, cal) {
  681. // Fractional seconds left-justify
  682. var len = pos[0] - start;
  683. cal.milliseconds = len < 3 ? value * Math.pow(10, 3 - len) :
  684. Math.round(value / Math.pow(10, len - 3));
  685. return true;
  686. };
  687. /**
  688. * Parse GMT type timezone.
  689. *
  690. * @param {string} text the time text to be parsed.
  691. * @param {Array<number>} pos Parse position.
  692. * @param {goog.i18n.DateTimeParse.MyDate_} cal object to hold parsed value.
  693. *
  694. * @return {boolean} True if successful.
  695. * @private
  696. */
  697. goog.i18n.DateTimeParse.prototype.subparseTimeZoneInGMT_ = function(
  698. text, pos, cal) {
  699. // First try to parse generic forms such as GMT-07:00. Do this first
  700. // in case localized DateFormatZoneData contains the string "GMT"
  701. // for a zone; in that case, we don't want to match the first three
  702. // characters of GMT+/-HH:MM etc.
  703. // For time zones that have no known names, look for strings
  704. // of the form:
  705. // GMT[+-]hours:minutes or
  706. // GMT[+-]hhmm or
  707. // GMT.
  708. if (text.indexOf('GMT', pos[0]) == pos[0]) {
  709. pos[0] += 3; // 3 is the length of GMT
  710. return this.parseTimeZoneOffset_(text, pos, cal);
  711. }
  712. // TODO(user): check for named time zones by looking through the locale
  713. // data from the DateFormatZoneData strings. Should parse both short and long
  714. // forms.
  715. // subParseZoneString(text, start, cal);
  716. // As a last resort, look for numeric timezones of the form
  717. // [+-]hhmm as specified by RFC 822. This code is actually
  718. // a little more permissive than RFC 822. It will try to do
  719. // its best with numbers that aren't strictly 4 digits long.
  720. return this.parseTimeZoneOffset_(text, pos, cal);
  721. };
  722. /**
  723. * Parse time zone offset.
  724. *
  725. * @param {string} text the time text to be parsed.
  726. * @param {Array<number>} pos Parse position.
  727. * @param {goog.i18n.DateTimeParse.MyDate_} cal object to hold parsed value.
  728. *
  729. * @return {boolean} True if successful.
  730. * @private
  731. */
  732. goog.i18n.DateTimeParse.prototype.parseTimeZoneOffset_ = function(
  733. text, pos, cal) {
  734. if (pos[0] >= text.length) {
  735. cal.tzOffset = 0;
  736. return true;
  737. }
  738. var sign = 1;
  739. switch (text.charAt(pos[0])) {
  740. case '-':
  741. sign = -1; // fall through
  742. case '+':
  743. pos[0]++;
  744. }
  745. // Look for hours:minutes or hhmm.
  746. var st = pos[0];
  747. var value = this.parseInt_(text, pos);
  748. if (value < 0) {
  749. return false;
  750. }
  751. var offset;
  752. if (pos[0] < text.length && text.charAt(pos[0]) == ':') {
  753. // This is the hours:minutes case
  754. offset = value * 60;
  755. pos[0]++;
  756. value = this.parseInt_(text, pos);
  757. if (value < 0) {
  758. return false;
  759. }
  760. offset += value;
  761. } else {
  762. // This is the hhmm case.
  763. offset = value;
  764. // Assume "-23".."+23" refers to hours.
  765. if (offset < 24 && (pos[0] - st) <= 2) {
  766. offset *= 60;
  767. } else {
  768. // todo: this looks questionable, should have more error checking
  769. offset = offset % 100 + offset / 100 * 60;
  770. }
  771. }
  772. offset *= sign;
  773. cal.tzOffset = -offset;
  774. return true;
  775. };
  776. /**
  777. * Parse an integer string and return integer value.
  778. *
  779. * @param {string} text string being parsed.
  780. * @param {Array<number>} pos parse position.
  781. *
  782. * @return {number} Converted integer value or -1 if the integer cannot be
  783. * parsed.
  784. * @private
  785. */
  786. goog.i18n.DateTimeParse.prototype.parseInt_ = function(text, pos) {
  787. // Delocalizes the string containing native digits specified by the locale,
  788. // replaces the native digits with ASCII digits. Leaves other characters.
  789. // This is the reverse operation of localizeNumbers_ in datetimeformat.js.
  790. if (this.dateTimeSymbols_.ZERODIGIT) {
  791. var parts = [];
  792. for (var i = pos[0]; i < text.length; i++) {
  793. var c = text.charCodeAt(i) - this.dateTimeSymbols_.ZERODIGIT;
  794. parts.push(
  795. (0 <= c && c <= 9) ? String.fromCharCode(c + 0x30) : text.charAt(i));
  796. }
  797. text = parts.join('');
  798. } else {
  799. text = text.substring(pos[0]);
  800. }
  801. var m = text.match(/^\d+/);
  802. if (!m) {
  803. return -1;
  804. }
  805. pos[0] += m[0].length;
  806. return parseInt(m[0], 10);
  807. };
  808. /**
  809. * Attempt to match the text at a given position against an array of strings.
  810. * Since multiple strings in the array may match (for example, if the array
  811. * contains "a", "ab", and "abc", all will match the input string "abcd") the
  812. * longest match is returned.
  813. *
  814. * @param {string} text The string to match to.
  815. * @param {Array<number>} pos parsing position.
  816. * @param {Array<string>} data The string array of matching patterns.
  817. *
  818. * @return {number} the new start position if matching succeeded; a negative
  819. * number indicating matching failure.
  820. * @private
  821. */
  822. goog.i18n.DateTimeParse.prototype.matchString_ = function(text, pos, data) {
  823. // There may be multiple strings in the data[] array which begin with
  824. // the same prefix (e.g., Cerven and Cervenec (June and July) in Czech).
  825. // We keep track of the longest match, and return that. Note that this
  826. // unfortunately requires us to test all array elements.
  827. var bestMatchLength = 0;
  828. var bestMatch = -1;
  829. var lower_text = text.substring(pos[0]).toLowerCase();
  830. for (var i = 0; i < data.length; i++) {
  831. var len = data[i].length;
  832. // Always compare if we have no match yet; otherwise only compare
  833. // against potentially better matches (longer strings).
  834. if (len > bestMatchLength &&
  835. lower_text.indexOf(data[i].toLowerCase()) == 0) {
  836. bestMatch = i;
  837. bestMatchLength = len;
  838. }
  839. }
  840. if (bestMatch >= 0) {
  841. pos[0] += bestMatchLength;
  842. }
  843. return bestMatch;
  844. };
  845. /**
  846. * This class hold the intermediate parsing result. After all fields are
  847. * consumed, final result will be resolved from this class.
  848. * @constructor
  849. * @private
  850. */
  851. goog.i18n.DateTimeParse.MyDate_ = function() {};
  852. /**
  853. * The date's era.
  854. * @type {?number}
  855. */
  856. goog.i18n.DateTimeParse.MyDate_.prototype.era;
  857. /**
  858. * The date's year.
  859. * @type {?number}
  860. */
  861. goog.i18n.DateTimeParse.MyDate_.prototype.year;
  862. /**
  863. * The date's month.
  864. * @type {?number}
  865. */
  866. goog.i18n.DateTimeParse.MyDate_.prototype.month;
  867. /**
  868. * The date's day of month.
  869. * @type {?number}
  870. */
  871. goog.i18n.DateTimeParse.MyDate_.prototype.day;
  872. /**
  873. * The date's hour.
  874. * @type {?number}
  875. */
  876. goog.i18n.DateTimeParse.MyDate_.prototype.hours;
  877. /**
  878. * The date's before/afternoon denominator.
  879. * @type {?number}
  880. */
  881. goog.i18n.DateTimeParse.MyDate_.prototype.ampm;
  882. /**
  883. * The date's minutes.
  884. * @type {?number}
  885. */
  886. goog.i18n.DateTimeParse.MyDate_.prototype.minutes;
  887. /**
  888. * The date's seconds.
  889. * @type {?number}
  890. */
  891. goog.i18n.DateTimeParse.MyDate_.prototype.seconds;
  892. /**
  893. * The date's milliseconds.
  894. * @type {?number}
  895. */
  896. goog.i18n.DateTimeParse.MyDate_.prototype.milliseconds;
  897. /**
  898. * The date's timezone offset.
  899. * @type {?number}
  900. */
  901. goog.i18n.DateTimeParse.MyDate_.prototype.tzOffset;
  902. /**
  903. * The date's day of week. Sunday is 0, Saturday is 6.
  904. * @type {?number}
  905. */
  906. goog.i18n.DateTimeParse.MyDate_.prototype.dayOfWeek;
  907. /**
  908. * 2 digit year special handling. Assuming for example that the
  909. * defaultCenturyStart is 6/18/1903. This means that two-digit years will be
  910. * forced into the range 6/18/1903 to 6/17/2003. As a result, years 00, 01, and
  911. * 02 correspond to 2000, 2001, and 2002. Years 04, 05, etc. correspond
  912. * to 1904, 1905, etc. If the year is 03, then it is 2003 if the
  913. * other fields specify a date before 6/18, or 1903 if they specify a
  914. * date afterwards. As a result, 03 is an ambiguous year. All other
  915. * two-digit years are unambiguous.
  916. *
  917. * @param {number} year 2 digit year value before adjustment.
  918. * @return {number} disambiguated year.
  919. * @private
  920. */
  921. goog.i18n.DateTimeParse.MyDate_.prototype.setTwoDigitYear_ = function(year) {
  922. var now = new Date();
  923. var defaultCenturyStartYear =
  924. now.getFullYear() - goog.i18n.DateTimeParse.ambiguousYearCenturyStart;
  925. var ambiguousTwoDigitYear = defaultCenturyStartYear % 100;
  926. this.ambiguousYear = (year == ambiguousTwoDigitYear);
  927. year += Math.floor(defaultCenturyStartYear / 100) * 100 +
  928. (year < ambiguousTwoDigitYear ? 100 : 0);
  929. return this.year = year;
  930. };
  931. /**
  932. * Based on the fields set, fill a Date object. For those fields that not
  933. * set, use the passed in date object's value.
  934. *
  935. * @param {goog.date.DateLike} date Date object to be filled.
  936. * @param {boolean} validation If true, input string will be checked to make
  937. * sure it is valid.
  938. *
  939. * @return {boolean} false if fields specify a invalid date.
  940. * @private
  941. */
  942. goog.i18n.DateTimeParse.MyDate_.prototype.calcDate_ = function(
  943. date, validation) {
  944. // Throw exception if date if null.
  945. if (date == null) {
  946. throw Error('Parameter \'date\' should not be null.');
  947. }
  948. // year 0 is 1 BC, and so on.
  949. if (this.era != undefined && this.year != undefined && this.era == 0 &&
  950. this.year > 0) {
  951. this.year = -(this.year - 1);
  952. }
  953. if (this.year != undefined) {
  954. date.setFullYear(this.year);
  955. }
  956. // The setMonth and setDate logic is a little tricky. We need to make sure
  957. // day of month is smaller enough so that it won't cause a month switch when
  958. // setting month. For example, if data in date is Nov 30, when month is set
  959. // to Feb, because there is no Feb 30, JS adjust it to Mar 2. So Feb 12 will
  960. // become Mar 12.
  961. var orgDate = date.getDate();
  962. // Every month has a 1st day, this can actually be anything less than 29.
  963. date.setDate(1);
  964. if (this.month != undefined) {
  965. date.setMonth(this.month);
  966. }
  967. if (this.day != undefined) {
  968. date.setDate(this.day);
  969. } else {
  970. var maxDate =
  971. goog.date.getNumberOfDaysInMonth(date.getFullYear(), date.getMonth());
  972. date.setDate(orgDate > maxDate ? maxDate : orgDate);
  973. }
  974. if (goog.isFunction(date.setHours)) {
  975. if (this.hours == undefined) {
  976. this.hours = date.getHours();
  977. }
  978. // adjust ampm
  979. if (this.ampm != undefined && this.ampm > 0 && this.hours < 12) {
  980. this.hours += 12;
  981. }
  982. date.setHours(this.hours);
  983. }
  984. if (goog.isFunction(date.setMinutes) && this.minutes != undefined) {
  985. date.setMinutes(this.minutes);
  986. }
  987. if (goog.isFunction(date.setSeconds) && this.seconds != undefined) {
  988. date.setSeconds(this.seconds);
  989. }
  990. if (goog.isFunction(date.setMilliseconds) && this.milliseconds != undefined) {
  991. date.setMilliseconds(this.milliseconds);
  992. }
  993. // If validation is needed, verify that the uncalculated date fields
  994. // match the calculated date fields. We do this before we set the
  995. // timezone offset, which will skew all of the dates.
  996. //
  997. // Don't need to check the day of week as it is guaranteed to be
  998. // correct or return false below.
  999. if (validation &&
  1000. (this.year != undefined && this.year != date.getFullYear() ||
  1001. this.month != undefined && this.month != date.getMonth() ||
  1002. this.day != undefined && this.day != date.getDate() ||
  1003. this.hours >= 24 || this.minutes >= 60 || this.seconds >= 60 ||
  1004. this.milliseconds >= 1000)) {
  1005. return false;
  1006. }
  1007. // adjust time zone
  1008. if (this.tzOffset != undefined) {
  1009. var offset = date.getTimezoneOffset();
  1010. date.setTime(date.getTime() + (this.tzOffset - offset) * 60 * 1000);
  1011. }
  1012. // resolve ambiguous year if needed
  1013. if (this.ambiguousYear) { // the two-digit year == the default start year
  1014. var defaultCenturyStart = new Date();
  1015. defaultCenturyStart.setFullYear(
  1016. defaultCenturyStart.getFullYear() -
  1017. goog.i18n.DateTimeParse.ambiguousYearCenturyStart);
  1018. if (date.getTime() < defaultCenturyStart.getTime()) {
  1019. date.setFullYear(defaultCenturyStart.getFullYear() + 100);
  1020. }
  1021. }
  1022. // dayOfWeek, validation only
  1023. if (this.dayOfWeek != undefined) {
  1024. if (this.day == undefined) {
  1025. // adjust to the nearest day of the week
  1026. var adjustment = (7 + this.dayOfWeek - date.getDay()) % 7;
  1027. if (adjustment > 3) {
  1028. adjustment -= 7;
  1029. }
  1030. var orgMonth = date.getMonth();
  1031. date.setDate(date.getDate() + adjustment);
  1032. // don't let it switch month
  1033. if (date.getMonth() != orgMonth) {
  1034. date.setDate(date.getDate() + (adjustment > 0 ? -7 : 7));
  1035. }
  1036. } else if (this.dayOfWeek != date.getDay()) {
  1037. return false;
  1038. }
  1039. }
  1040. return true;
  1041. };