| // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| // @dart=2.9 |
| |
| part of intl; |
| |
| /// Given a month and day number, return the day of the year, all one-based. |
| /// |
| /// For example, |
| /// * January 2nd (1, 2) -> 2. |
| /// * February 5th (2, 5) -> 36. |
| /// * March 1st of a non-leap year (3, 1) -> 60. |
| int _dayOfYear(int month, int day, bool leapYear) { |
| if (month == 1) return day; |
| if (month == 2) return day + 31; |
| return ordinalDayFromMarchFirst(month, day) + 59 + (leapYear ? 1 : 0); |
| } |
| |
| /// Return true if this is a leap year. Rely on [DateTime] to do the |
| /// underlying calculation, even though it doesn't expose the test to us. |
| bool _isLeapYear(DateTime date) { |
| var feb29 = DateTime(date.year, 2, 29); |
| return feb29.month == 2; |
| } |
| |
| /// Return the day of the year counting March 1st as 1, after which the |
| /// number of days per month is constant, so it's easier to calculate. |
| /// Formula from http://en.wikipedia.org/wiki/Ordinal_date |
| int ordinalDayFromMarchFirst(int month, int day) => |
| ((30.6 * month) - 91.4).floor() + day; |
| |
| /// A class for holding onto the data for a date so that it can be built |
| /// up incrementally. |
| class _DateBuilder { |
| // Default the date values to the EPOCH so that there's a valid date |
| // in case the format doesn't set them. |
| int year = 1970, |
| month = 1, |
| day = 1, |
| dayOfYear = 0, |
| hour = 0, |
| minute = 0, |
| second = 0, |
| fractionalSecond = 0; |
| bool pm = false; |
| bool utc = false; |
| |
| /// Whether the century portion of [year] is ambiguous. |
| /// |
| /// Ignored if `year < 0` or `year >= 100`. |
| bool _hasAmbiguousCentury = false; |
| |
| /// The locale, kept for logging purposes when there's an error. |
| final String _locale; |
| |
| /// The date result produced from [asDate]. |
| /// |
| /// Kept as a field to cache the result and to reduce the possibility of error |
| /// after we've verified. |
| DateTime _date; |
| |
| /// The number of times we've retried, for error reporting. |
| int _retried = 0; |
| |
| /// Is this constructing a pure date. |
| /// |
| /// This is important because some locales change times at midnight, |
| /// e.g. Brazil. So if we try to create a DateTime representing a date at |
| /// midnight on the day of transition it will jump forward or back 1 hour. If |
| /// it jumps forward that's mostly harmless if we only care about the |
| /// date. But if it jumps backwards that will change the date, which is |
| /// bad. Compensate by adjusting the time portion forward. But only do that |
| /// when we're explicitly trying to construct a date, which we can tell from |
| /// the format. |
| |
| // We do set it, the analyzer just can't tell. |
| // ignore: prefer_final_fields |
| var _dateOnly = false; |
| |
| /// The function we will call to create a DateTime from its component pieces. |
| /// |
| /// This is normally only modified in tests that want to introduce errors. |
| final _DateTimeConstructor _dateTimeConstructor; |
| |
| _DateBuilder(this._locale, this._dateTimeConstructor); |
| |
| // Functions that exist just to be closurized so we can pass them to a general |
| // method. |
| void setYear(int x) { |
| year = x; |
| } |
| |
| /// Sets whether [year] should be treated as ambiguous because it lacks a |
| /// century. |
| void setHasAmbiguousCentury(bool isAmbiguous) { |
| _hasAmbiguousCentury = isAmbiguous; |
| } |
| |
| void setMonth(int x) { |
| month = x; |
| } |
| |
| void setDay(int x) { |
| day = x; |
| } |
| |
| void setDayOfYear(int x) { |
| dayOfYear = x; |
| } |
| |
| /// If [dayOfYear] has been set, return it, otherwise return [day], indicating |
| /// the day of the month. |
| int get dayOrDayOfYear => dayOfYear == 0 ? day : dayOfYear; |
| |
| void setHour(int x) { |
| hour = x; |
| } |
| |
| void setMinute(int x) { |
| minute = x; |
| } |
| |
| void setSecond(int x) { |
| second = x; |
| } |
| |
| void setFractionalSecond(int x) { |
| fractionalSecond = x; |
| } |
| |
| int get hour24 => pm ? hour + 12 : hour; |
| |
| /// Verify that we correspond to a valid date. This will reject out of |
| /// range values, even if the DateTime constructor would accept them. An |
| /// invalid message will result in throwing a [FormatException]. |
| void verify(String s) { |
| _verify(month, 1, 12, 'month', s); |
| _verify(hour24, 0, 23, 'hour', s); |
| _verify(minute, 0, 59, 'minute', s); |
| _verify(second, 0, 59, 'second', s); |
| _verify(fractionalSecond, 0, 999, 'fractional second', s); |
| // Verifying the day is tricky, because it depends on the month. Create |
| // our resulting date and then verify that our values agree with it |
| // as an additional verification. And since we're doing that, also |
| // check the year, which we otherwise can't verify, and the hours, |
| // which will catch cases like '14:00:00 PM'. |
| var date = asDate(); |
| // On rare occasions, possibly related to DST boundaries, a parsed date will |
| // come out as 1:00am. We compensate for the case of going backwards in |
| // _correctForErrors, but we may not be able to compensate for a midnight |
| // that doesn't exist. So tolerate an hour value of zero or one in these |
| // cases. |
| var minimumDate = _dateOnly && date.hour == 1 ? 0 : date.hour; |
| _verify(hour24, minimumDate, date.hour, 'hour', s, date); |
| if (dayOfYear > 0) { |
| // We have an ordinal date, compute the corresponding date for the result |
| // and compare to that. |
| var leapYear = _isLeapYear(date); |
| var correspondingDay = _dayOfYear(date.month, date.day, leapYear); |
| _verify( |
| dayOfYear, correspondingDay, correspondingDay, 'dayOfYear', s, date); |
| } else { |
| // We have the day of the month, compare directly. |
| _verify(day, date.day, date.day, 'day', s, date); |
| } |
| _verify(year, date.year, date.year, 'year', s, date); |
| } |
| |
| void _verify(int value, int min, int max, String desc, String originalInput, |
| [DateTime parsed]) { |
| if (value < min || value > max) { |
| var parsedDescription = parsed == null ? '' : ' Date parsed as $parsed.'; |
| var errorDescription = |
| 'Error parsing $originalInput, invalid $desc value: $value' |
| ' in $_locale' |
| ' with time zone offset ${parsed?.timeZoneOffset ?? 'unknown'}.' |
| ' Expected value between $min and $max.$parsedDescription.'; |
| if (_retried > 0) { |
| errorDescription += ' Failed after $_retried retries.'; |
| } |
| throw FormatException(errorDescription); |
| } |
| } |
| |
| /// Offsets a [DateTime] by a specified number of years. |
| /// |
| /// All other fields of the [DateTime] normally will remain unaffected. An |
| /// exception is if the resulting [DateTime] otherwise would represent an |
| /// invalid date (e.g. February 29 of a non-leap year). |
| DateTime _offsetYear(DateTime dateTime, int offsetYears) => |
| _dateTimeConstructor( |
| dateTime.year + offsetYears, |
| dateTime.month, |
| dateTime.day, |
| dateTime.hour, |
| dateTime.minute, |
| dateTime.second, |
| dateTime.millisecond, |
| dateTime.isUtc); |
| |
| /// Return a date built using our values. If no date portion is set, |
| /// use the 'Epoch' of January 1, 1970. |
| DateTime asDate({int retries = 3}) { |
| // TODO(alanknight): Validate the date, especially for things which |
| // can crash the VM, e.g. large month values. |
| if (_date != null) return _date; |
| |
| DateTime preliminaryResult; |
| final hasCentury = !_hasAmbiguousCentury || year < 0 || year >= 100; |
| if (hasCentury) { |
| preliminaryResult = _dateTimeConstructor(year, month, dayOrDayOfYear, |
| hour24, minute, second, fractionalSecond, utc); |
| } else { |
| var now = clock.now(); |
| if (utc) { |
| now = now.toUtc(); |
| } |
| |
| const lookBehindYears = 80; |
| var lowerDate = _offsetYear(now, -lookBehindYears); |
| var upperDate = _offsetYear(now, 100 - lookBehindYears); |
| var lowerCentury = (lowerDate.year ~/ 100) * 100; |
| var upperCentury = (upperDate.year ~/ 100) * 100; |
| preliminaryResult = _dateTimeConstructor(upperCentury + year, month, |
| dayOrDayOfYear, hour24, minute, second, fractionalSecond, utc); |
| |
| // Our interval must be half-open since there otherwise could be ambiguity |
| // for a date that is exactly 20 years in the future or exactly 80 years |
| // in the past (mod 100). We'll treat the lower-bound date as the |
| // exclusive bound because: |
| // * It's farther away from the present, and we're less likely to care |
| // about it. |
| // * By the time this function exits, time will have advanced to favor |
| // the upper-bound date. |
| // |
| // We don't actually need to check both bounds. |
| if (preliminaryResult.compareTo(upperDate) <= 0) { |
| // Within range. |
| assert(preliminaryResult.compareTo(lowerDate) > 0); |
| } else { |
| preliminaryResult = _dateTimeConstructor(lowerCentury + year, month, |
| dayOrDayOfYear, hour24, minute, second, fractionalSecond, utc); |
| } |
| } |
| |
| if (utc && hasCentury) { |
| _date = preliminaryResult; |
| } else { |
| _date = _correctForErrors(preliminaryResult, retries); |
| } |
| return _date; |
| } |
| |
| /// Given a local DateTime, check for errors and try to compensate for them if |
| /// possible. |
| DateTime _correctForErrors(DateTime result, int retries) { |
| // There are 3 kinds of errors that we know of |
| // |
| // 1 - Issue 15560, sometimes we get UTC even when we asked for local, or |
| // they get constructed as if in UTC and then have the offset subtracted. |
| // Retry, possibly several times, until we get something that looks valid, |
| // or we give up. |
| // |
| // 1a) - It appears that sometimes we get incorrect timezone offsets that |
| // are not directly related to UTC. Also check for those and retry or |
| // compensate. |
| // |
| // 2 - Timezone transitions. If we ask for the time during a timezone |
| // transition then it will offset it by that transition. This is |
| // particularly a problem if the timezone transition happens at midnight, |
| // and we're looking for a date with no time component. This happens in |
| // Brazil, and we can end up with 11:00pm the previous day. Add time to |
| // compensate. |
| // |
| // 3 - Invalid input which the constructor nevertheless accepts. Just return |
| // what it created, and verify will catch it if we're in strict mode. |
| |
| // If we've exhausted our retries, just return the input - it's not just a |
| // flaky result. |
| if (retries <= 0) { |
| return result; |
| } |
| |
| var leapYear = _isLeapYear(result); |
| var resultDayOfYear = _dayOfYear(result.month, result.day, leapYear); |
| |
| // Check for the UTC failure. Are we expecting to produce a local time, but |
| // the result is UTC. However, the local time might happen to be the same as |
| // UTC. To be thorough, check if either the hour/day don't agree with what |
| // we expect, or is a new DateTime in a non-UTC timezone. |
| if (!utc && |
| result.isUtc && |
| (result.hour != hour24 || |
| result.day != resultDayOfYear || |
| !DateTime.now().isUtc)) { |
| // This may be a UTC failure. Retry and if the result doesn't look |
| // like it's in the UTC time zone, use that instead. |
| _retried++; |
| return asDate(retries: retries - 1); |
| } |
| |
| if (_dateOnly && result.hour != 0) { |
| // This could be a flake, try again. |
| var tryAgain = asDate(retries: retries - 1); |
| if (tryAgain != result) { |
| // Trying again gave a different answer, so presumably it worked. |
| return tryAgain; |
| } |
| |
| // Trying again didn't work, try to force the offset. |
| var expectedDayOfYear = |
| dayOfYear == 0 ? _dayOfYear(month, day, leapYear) : dayOfYear; |
| |
| // If we're _dateOnly, then hours should be zero, but might have been |
| // offset to e.g. 11:00pm the previous day. Add that time back in. This |
| // might be because of an erratic error, but it might also be because of a |
| // time zone (Brazil) where there is no midnight at a daylight savings |
| // time transition. In that case we will retry, but eventually give up and |
| // return 1:00am on the correct date. |
| var daysPrevious = expectedDayOfYear - resultDayOfYear; |
| // For example, if it's the day before at 11:00pm, we offset by (24 - 23), |
| // so +1. If it's the same day at 1:00am, we offset by (0 - 1), so -1. |
| var offset = (daysPrevious * 24) - result.hour; |
| var adjusted = result.add(Duration(hours: offset)); |
| // Check if the adjustment worked. This can fail on a time zone transition |
| // where midnight doesn't exist. |
| if (adjusted.hour == 0) { |
| return adjusted; |
| } |
| // Adjusting did not work. Just check if the adjusted date is right. And |
| // if it's not, just give up and return [result]. The scenario where this |
| // might correctly happen is if we're in a Brazil time zone, jump forward |
| // to 1:00 am because of a DST transition, and trying to go backwards 1 |
| // hour takes us back to 11:00pm the day before. In that case the 1:00am |
| // answer on the correct date is preferable. |
| var adjustedDayOfYear = |
| _dayOfYear(adjusted.month, adjusted.day, leapYear); |
| if (adjustedDayOfYear != expectedDayOfYear) { |
| return result; |
| } |
| return adjusted; |
| } |
| // None of our corrections applied, just return the uncorrected date. |
| return result; |
| } |
| } |
| |
| /// A simple and not particularly general stream class to make parsing |
| /// dates from strings simpler. It is general enough to operate on either |
| /// lists or strings. |
| // TODO(alanknight): With the improvements to the collection libraries |
| // since this was written we might be able to get rid of it entirely |
| // in favor of e.g. aString.split('') giving us an iterable of one-character |
| // strings, or else make the implementation trivial. And consider renaming, |
| // as _Stream is now just confusing with the system Streams. |
| class _Stream { |
| dynamic contents; |
| int index = 0; |
| |
| _Stream(this.contents); |
| |
| bool atEnd() => index >= contents.length; |
| |
| dynamic next() => contents[index++]; |
| |
| /// Return the next [howMany] items, or as many as there are remaining. |
| /// Advance the stream by that many positions. |
| dynamic read([int howMany = 1]) { |
| var result = peek(howMany); |
| index += howMany; |
| return result; |
| } |
| |
| /// Does the input start with the given string, if we start from the |
| /// current position. |
| bool startsWith(String pattern) { |
| if (contents is String) return contents.startsWith(pattern, index); |
| return pattern == peek(pattern.length); |
| } |
| |
| /// Return the next [howMany] items, or as many as there are remaining. |
| /// Does not modify the stream position. |
| dynamic peek([int howMany = 1]) { |
| dynamic result; |
| if (contents is String) { |
| String stringContents = contents; |
| result = stringContents.substring( |
| index, min(index + howMany, stringContents.length)); |
| } else { |
| // Assume List |
| result = contents.sublist(index, index + howMany); |
| } |
| return result; |
| } |
| |
| /// Return the remaining contents of the stream |
| dynamic rest() => peek(contents.length - index); |
| |
| /// Find the index of the first element for which [f] returns true. |
| /// Advances the stream to that position. |
| int findIndex(bool Function(dynamic) f) { |
| while (!atEnd()) { |
| if (f(next())) return index - 1; |
| } |
| return null; |
| } |
| |
| /// Find the indexes of all the elements for which [f] returns true. |
| /// Leaves the stream positioned at the end. |
| List<dynamic> findIndexes(bool Function(dynamic) f) { |
| var results = []; |
| while (!atEnd()) { |
| if (f(next())) results.add(index - 1); |
| } |
| return results; |
| } |
| |
| /// Assuming that the contents are characters, read as many digits as we |
| /// can see and then return the corresponding integer, advancing the receiver. |
| /// |
| /// For non-ascii digits, the optional arguments are a regular expression |
| /// [digitMatcher] to find the next integer, and the codeUnit of the local |
| /// zero [zeroDigit]. |
| int nextInteger({RegExp digitMatcher, int zeroDigit}) { |
| var string = |
| (digitMatcher ?? DateFormat._asciiDigitMatcher).stringMatch(rest()); |
| if (string == null || string.isEmpty) return null; |
| read(string.length); |
| if (zeroDigit != null && zeroDigit != DateFormat._asciiZeroCodeUnit) { |
| // Trying to optimize this, as it might get called a lot. |
| var oldDigits = string.codeUnits; |
| var newDigits = List<int>(string.length); |
| for (var i = 0; i < string.length; i++) { |
| newDigits[i] = oldDigits[i] - zeroDigit + DateFormat._asciiZeroCodeUnit; |
| } |
| string = String.fromCharCodes(newDigits); |
| } |
| return int.parse(string); |
| } |
| } |