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