2 * Copyright (C) 1999-2000 Harri Porten (porten@kde.org)
3 * Copyright (C) 2006, 2007 Apple Inc. All rights reserved.
4 * Copyright (C) 2009 Google Inc. All rights reserved.
5 * Copyright (C) 2007-2009 Torch Mobile, Inc.
6 * Copyright (C) 2010 &yet, LLC. (nate@andyet.net)
8 * The Original Code is Mozilla Communicator client code, released
11 * The Initial Developer of the Original Code is
12 * Netscape Communications Corporation.
13 * Portions created by the Initial Developer are Copyright (C) 1998
14 * the Initial Developer. All Rights Reserved.
16 * This library is free software; you can redistribute it and/or
17 * modify it under the terms of the GNU Lesser General Public
18 * License as published by the Free Software Foundation; either
19 * version 2.1 of the License, or (at your option) any later version.
21 * This library is distributed in the hope that it will be useful,
22 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
24 * Lesser General Public License for more details.
26 * You should have received a copy of the GNU Lesser General Public
27 * License along with this library; if not, write to the Free Software
28 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
30 * Alternatively, the contents of this file may be used under the terms
31 * of either the Mozilla Public License Version 1.1, found at
32 * http://www.mozilla.org/MPL/ (the "MPL") or the GNU General Public
33 * License Version 2.0, found at http://www.fsf.org/copyleft/gpl.html
34 * (the "GPL"), in which case the provisions of the MPL or the GPL are
35 * applicable instead of those above. If you wish to allow use of your
36 * version of this file only under the terms of one of those two
37 * licenses (the MPL or the GPL) and not to allow others to use your
38 * version of this file under the LGPL, indicate your decision by
39 * deletingthe provisions above and replace them with the notice and
40 * other provisions required by the MPL or the GPL, as the case may be.
41 * If you do not delete the provisions above, a recipient may use your
42 * version of this file under any of the LGPL, the MPL or the GPL.
44 * Copyright 2006-2008 the V8 project authors. All rights reserved.
45 * Redistribution and use in source and binary forms, with or without
46 * modification, are permitted provided that the following conditions are
49 * * Redistributions of source code must retain the above copyright
50 * notice, this list of conditions and the following disclaimer.
51 * * Redistributions in binary form must reproduce the above
52 * copyright notice, this list of conditions and the following
53 * disclaimer in the documentation and/or other materials provided
54 * with the distribution.
55 * * Neither the name of Google Inc. nor the names of its
56 * contributors may be used to endorse or promote products derived
57 * from this software without specific prior written permission.
59 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
60 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
61 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
62 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
63 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
64 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
65 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
66 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
67 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
68 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
69 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
75 #include "Assertions.h"
76 #include "ASCIICType.h"
77 #include "CurrentTime.h"
78 #include "MathExtras.h"
79 #include "StdLibExtras.h"
80 #include "StringExtras.h"
88 #include "wtf/text/StringBuilder.h"
104 static const double hoursPerDay = 24.0;
105 static const double secondsPerDay = 24.0 * 60.0 * 60.0;
107 static const double maxUnixTime = 2145859200.0; // 12/31/2037
109 // Day of year for the first day of each month, where index 0 is January, and day 0 is January 1.
110 // First for non-leap years, then for leap years.
111 static const int firstDayOfMonth[2][12] = {
112 {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334},
113 {0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335}
116 static inline void getLocalTime(const time_t* localTime, struct tm* localTM)
119 localtime_s(localTM, localTime);
121 localtime_r(localTime, localTM);
125 bool isLeapYear(int year)
136 static inline int daysInYear(int year)
138 return 365 + isLeapYear(year);
141 static inline double daysFrom1970ToYear(int year)
143 // The Gregorian Calendar rules for leap years:
144 // Every fourth year is a leap year. 2004, 2008, and 2012 are leap years.
145 // However, every hundredth year is not a leap year. 1900 and 2100 are not leap years.
146 // Every four hundred years, there's a leap year after all. 2000 and 2400 are leap years.
148 static const int leapDaysBefore1971By4Rule = 1970 / 4;
149 static const int excludedLeapDaysBefore1971By100Rule = 1970 / 100;
150 static const int leapDaysBefore1971By400Rule = 1970 / 400;
152 const double yearMinusOne = year - 1;
153 const double yearsToAddBy4Rule = floor(yearMinusOne / 4.0) - leapDaysBefore1971By4Rule;
154 const double yearsToExcludeBy100Rule = floor(yearMinusOne / 100.0) - excludedLeapDaysBefore1971By100Rule;
155 const double yearsToAddBy400Rule = floor(yearMinusOne / 400.0) - leapDaysBefore1971By400Rule;
157 return 365.0 * (year - 1970) + yearsToAddBy4Rule - yearsToExcludeBy100Rule + yearsToAddBy400Rule;
160 static double msToDays(double ms)
162 return floor(ms / msPerDay);
165 static void appendTwoDigitNumber(StringBuilder& builder, int number)
167 ASSERT(number >= 0 && number < 100);
170 builder.appendNumber(number);
173 int msToYear(double ms)
175 int approxYear = static_cast<int>(floor(ms / (msPerDay * 365.2425)) + 1970);
176 double msFromApproxYearTo1970 = msPerDay * daysFrom1970ToYear(approxYear);
177 if (msFromApproxYearTo1970 > ms)
178 return approxYear - 1;
179 if (msFromApproxYearTo1970 + msPerDay * daysInYear(approxYear) <= ms)
180 return approxYear + 1;
184 int dayInYear(double ms, int year)
186 return static_cast<int>(msToDays(ms) - daysFrom1970ToYear(year));
189 static inline double msToMilliseconds(double ms)
191 double result = fmod(ms, msPerDay);
197 int monthFromDayInYear(int dayInYear, bool leapYear)
199 const int d = dayInYear;
204 step += (leapYear ? 29 : 28);
207 if (d < (step += 31))
209 if (d < (step += 30))
211 if (d < (step += 31))
213 if (d < (step += 30))
215 if (d < (step += 31))
217 if (d < (step += 31))
219 if (d < (step += 30))
221 if (d < (step += 31))
223 if (d < (step += 30))
228 static inline bool checkMonth(int dayInYear, int& startDayOfThisMonth, int& startDayOfNextMonth, int daysInThisMonth)
230 startDayOfThisMonth = startDayOfNextMonth;
231 startDayOfNextMonth += daysInThisMonth;
232 return (dayInYear <= startDayOfNextMonth);
235 int dayInMonthFromDayInYear(int dayInYear, bool leapYear)
237 const int d = dayInYear;
243 const int daysInFeb = (leapYear ? 29 : 28);
244 if (checkMonth(d, step, next, daysInFeb))
246 if (checkMonth(d, step, next, 31))
248 if (checkMonth(d, step, next, 30))
250 if (checkMonth(d, step, next, 31))
252 if (checkMonth(d, step, next, 30))
254 if (checkMonth(d, step, next, 31))
256 if (checkMonth(d, step, next, 31))
258 if (checkMonth(d, step, next, 30))
260 if (checkMonth(d, step, next, 31))
262 if (checkMonth(d, step, next, 30))
268 int dayInYear(int year, int month, int day)
270 return firstDayOfMonth[isLeapYear(year)][month] + day - 1;
273 double dateToDaysFrom1970(int year, int month, int day)
283 double yearday = floor(daysFrom1970ToYear(year));
284 ASSERT((year >= 1970 && yearday >= 0) || (year < 1970 && yearday < 0));
285 return yearday + dayInYear(year, month, day);
288 // There is a hard limit at 2038 that we currently do not have a workaround
289 // for (rdar://problem/5052975).
290 static inline int maximumYearForDST()
295 static inline double jsCurrentTime()
297 // JavaScript doesn't recognize fractions of a millisecond.
298 return floor(WTF::currentTimeMS());
301 static inline int minimumYearForDST()
303 // Because of the 2038 issue (see maximumYearForDST) if the current year is
304 // greater than the max year minus 27 (2010), we want to use the max year
305 // minus 27 instead, to ensure there is a range of 28 years that all years
307 return std::min(msToYear(jsCurrentTime()), maximumYearForDST() - 27) ;
311 * Find an equivalent year for the one given, where equivalence is deterined by
312 * the two years having the same leapness and the first day of the year, falling
313 * on the same day of the week.
315 * This function returns a year between this current year and 2037, however this
316 * function will potentially return incorrect results if the current year is after
317 * 2010, (rdar://problem/5052975), if the year passed in is before 1900 or after
318 * 2100, (rdar://problem/5055038).
320 static int equivalentYearForDST(int year)
322 // It is ok if the cached year is not the current year as long as the rules
323 // for DST did not change between the two years; if they did the app would need
325 static int minYear = minimumYearForDST();
326 int maxYear = maximumYearForDST();
330 difference = minYear - year;
331 else if (year < minYear)
332 difference = maxYear - year;
336 int quotient = difference / 28;
337 int product = (quotient) * 28;
340 ASSERT((year >= minYear && year <= maxYear) || (product - year == static_cast<int>(std::numeric_limits<double>::quiet_NaN())));
344 static double calculateUTCOffset()
347 TIME_ZONE_INFORMATION timeZoneInformation;
348 GetTimeZoneInformation(&timeZoneInformation);
349 int32_t bias = timeZoneInformation.Bias + timeZoneInformation.StandardBias;
350 return -bias * 60 * 1000;
352 time_t localTime = time(0);
354 getLocalTime(&localTime, &localt);
356 // tm_gmtoff includes any daylight savings offset, so subtract it.
357 return static_cast<double>(localt.tm_gmtoff * msPerSecond - (localt.tm_isdst > 0 ? msPerHour : 0));
362 * Get the DST offset for the time passed in.
364 static double calculateDSTOffsetSimple(double localTimeSeconds, double utcOffset)
366 if (localTimeSeconds > maxUnixTime)
367 localTimeSeconds = maxUnixTime;
368 else if (localTimeSeconds < 0) // Go ahead a day to make localtime work (does not work with 0)
369 localTimeSeconds += secondsPerDay;
371 // FIXME: time_t has a potential problem in 2038
372 time_t localTime = static_cast<time_t>(localTimeSeconds);
375 getLocalTime(&localTime, &localTM);
377 return localTM.tm_isdst > 0 ? msPerHour : 0;
380 // Get the DST offset, given a time in UTC
381 static double calculateDSTOffset(double ms, double utcOffset)
383 // On Mac OS X, the call to localtime (see calculateDSTOffsetSimple) will return historically accurate
384 // DST information (e.g. New Zealand did not have DST from 1946 to 1974) however the JavaScript
385 // standard explicitly dictates that historical information should not be considered when
386 // determining DST. For this reason we shift away from years that localtime can handle but would
387 // return historically accurate information.
388 int year = msToYear(ms);
389 int equivalentYear = equivalentYearForDST(year);
390 if (year != equivalentYear) {
391 bool leapYear = isLeapYear(year);
392 int dayInYearLocal = dayInYear(ms, year);
393 int dayInMonth = dayInMonthFromDayInYear(dayInYearLocal, leapYear);
394 int month = monthFromDayInYear(dayInYearLocal, leapYear);
395 double day = dateToDaysFrom1970(equivalentYear, month, dayInMonth);
396 ms = (day * msPerDay) + msToMilliseconds(ms);
399 return calculateDSTOffsetSimple(ms / msPerSecond, utcOffset);
402 void initializeDates()
405 static bool alreadyInitialized;
406 ASSERT(!alreadyInitialized);
407 alreadyInitialized = true;
410 equivalentYearForDST(2000); // Need to call once to initialize a static used in this function.
413 static inline double ymdhmsToSeconds(int year, long mon, long day, long hour, long minute, double second)
415 double days = (day - 32075)
416 + floor(1461 * (year + 4800.0 + (mon - 14) / 12) / 4)
417 + 367 * (mon - 2 - (mon - 14) / 12 * 12) / 12
418 - floor(3 * ((year + 4900.0 + (mon - 14) / 12) / 100) / 4)
420 return ((days * hoursPerDay + hour) * minutesPerHour + minute) * secondsPerMinute + second;
423 // We follow the recommendation of RFC 2822 to consider all
424 // obsolete time zones not listed here equivalent to "-0000".
425 static const struct KnownZone {
444 inline static void skipSpacesAndComments(const char*& s)
449 if (!isASCIISpace(ch)) {
452 else if (ch == ')' && nesting > 0)
454 else if (nesting == 0)
461 // returns 0-11 (Jan-Dec); -1 on failure
462 static int findMonth(const char* monthStr)
466 for (int i = 0; i < 3; ++i) {
469 needle[i] = static_cast<char>(toASCIILower(*monthStr++));
472 const char *haystack = "janfebmaraprmayjunjulaugsepoctnovdec";
473 const char *str = strstr(haystack, needle);
475 int position = static_cast<int>(str - haystack);
476 if (position % 3 == 0)
482 static bool parseInt(const char* string, char** stopPosition, int base, int* result)
484 long longResult = strtol(string, stopPosition, base);
485 // Avoid the use of errno as it is not available on Windows CE
486 if (string == *stopPosition || longResult <= std::numeric_limits<int>::min() || longResult >= std::numeric_limits<int>::max())
488 *result = static_cast<int>(longResult);
492 static bool parseLong(const char* string, char** stopPosition, int base, long* result)
494 *result = strtol(string, stopPosition, base);
495 // Avoid the use of errno as it is not available on Windows CE
496 if (string == *stopPosition || *result == std::numeric_limits<long>::min() || *result == std::numeric_limits<long>::max())
501 // Odd case where 'exec' is allowed to be 0, to accomodate a caller in WebCore.
502 static double parseDateFromNullTerminatedCharacters(const char* dateString, bool& haveTZ, int& offset)
507 // This parses a date in the form:
508 // Tuesday, 09-Nov-99 23:12:40 GMT
510 // Sat, 01-Jan-2000 08:00:00 GMT
512 // Sat, 01 Jan 2000 08:00:00 GMT
514 // 01 Jan 99 22:00 +0100 (exceptions in rfc822/rfc2822)
515 // ### non RFC formats, added for Javascript:
516 // [Wednesday] January 09 1999 23:12:40 GMT
517 // [Wednesday] January 09 23:12:40 GMT 1999
519 // We ignore the weekday.
521 // Skip leading space
522 skipSpacesAndComments(dateString);
525 const char *wordStart = dateString;
526 // Check contents of first words if not number
527 while (*dateString && !isASCIIDigit(*dateString)) {
528 if (isASCIISpace(*dateString) || *dateString == '(') {
529 if (dateString - wordStart >= 3)
530 month = findMonth(wordStart);
531 skipSpacesAndComments(dateString);
532 wordStart = dateString;
537 // Missing delimiter between month and day (like "January29")?
538 if (month == -1 && wordStart != dateString)
539 month = findMonth(wordStart);
541 skipSpacesAndComments(dateString);
544 return std::numeric_limits<double>::quiet_NaN();
546 // ' 09-Nov-99 23:12:40 GMT'
549 if (!parseLong(dateString, &newPosStr, 10, &day))
550 return std::numeric_limits<double>::quiet_NaN();
551 dateString = newPosStr;
554 return std::numeric_limits<double>::quiet_NaN();
557 return std::numeric_limits<double>::quiet_NaN();
561 // ### where is the boundary and what happens below?
562 if (*dateString != '/')
563 return std::numeric_limits<double>::quiet_NaN();
564 // looks like a YYYY/MM/DD date
566 return std::numeric_limits<double>::quiet_NaN();
567 if (day <= std::numeric_limits<int>::min() || day >= std::numeric_limits<int>::max())
568 return std::numeric_limits<double>::quiet_NaN();
569 year = static_cast<int>(day);
570 if (!parseLong(dateString, &newPosStr, 10, &month))
571 return std::numeric_limits<double>::quiet_NaN();
573 dateString = newPosStr;
574 if (*dateString++ != '/' || !*dateString)
575 return std::numeric_limits<double>::quiet_NaN();
576 if (!parseLong(dateString, &newPosStr, 10, &day))
577 return std::numeric_limits<double>::quiet_NaN();
578 dateString = newPosStr;
579 } else if (*dateString == '/' && month == -1) {
581 // This looks like a MM/DD/YYYY date, not an RFC date.
582 month = day - 1; // 0-based
583 if (!parseLong(dateString, &newPosStr, 10, &day))
584 return std::numeric_limits<double>::quiet_NaN();
585 if (day < 1 || day > 31)
586 return std::numeric_limits<double>::quiet_NaN();
587 dateString = newPosStr;
588 if (*dateString == '/')
591 return std::numeric_limits<double>::quiet_NaN();
593 if (*dateString == '-')
596 skipSpacesAndComments(dateString);
598 if (*dateString == ',')
601 if (month == -1) { // not found yet
602 month = findMonth(dateString);
604 return std::numeric_limits<double>::quiet_NaN();
606 while (*dateString && *dateString != '-' && *dateString != ',' && !isASCIISpace(*dateString))
610 return std::numeric_limits<double>::quiet_NaN();
612 // '-99 23:12:40 GMT'
613 if (*dateString != '-' && *dateString != '/' && *dateString != ',' && !isASCIISpace(*dateString))
614 return std::numeric_limits<double>::quiet_NaN();
619 if (month < 0 || month > 11)
620 return std::numeric_limits<double>::quiet_NaN();
623 if (year <= 0 && *dateString) {
624 if (!parseInt(dateString, &newPosStr, 10, &year))
625 return std::numeric_limits<double>::quiet_NaN();
628 // Don't fail if the time is missing.
633 dateString = newPosStr;
636 if (!(isASCIISpace(*newPosStr) || *newPosStr == ',')) {
637 if (*newPosStr != ':')
638 return std::numeric_limits<double>::quiet_NaN();
639 // There was no year; the number was the hour.
642 // in the normal case (we parsed the year), advance to the next number
643 dateString = ++newPosStr;
644 skipSpacesAndComments(dateString);
647 parseLong(dateString, &newPosStr, 10, &hour);
648 // Do not check for errno here since we want to continue
649 // even if errno was set becasue we are still looking
652 // Read a number? If not, this might be a timezone name.
653 if (newPosStr != dateString) {
654 dateString = newPosStr;
656 if (hour < 0 || hour > 23)
657 return std::numeric_limits<double>::quiet_NaN();
660 return std::numeric_limits<double>::quiet_NaN();
663 if (*dateString++ != ':')
664 return std::numeric_limits<double>::quiet_NaN();
666 if (!parseLong(dateString, &newPosStr, 10, &minute))
667 return std::numeric_limits<double>::quiet_NaN();
668 dateString = newPosStr;
670 if (minute < 0 || minute > 59)
671 return std::numeric_limits<double>::quiet_NaN();
674 if (*dateString && *dateString != ':' && !isASCIISpace(*dateString))
675 return std::numeric_limits<double>::quiet_NaN();
677 // seconds are optional in rfc822 + rfc2822
678 if (*dateString ==':') {
681 if (!parseLong(dateString, &newPosStr, 10, &second))
682 return std::numeric_limits<double>::quiet_NaN();
683 dateString = newPosStr;
685 if (second < 0 || second > 59)
686 return std::numeric_limits<double>::quiet_NaN();
689 skipSpacesAndComments(dateString);
691 if (strncasecmp(dateString, "AM", 2) == 0) {
693 return std::numeric_limits<double>::quiet_NaN();
697 skipSpacesAndComments(dateString);
698 } else if (strncasecmp(dateString, "PM", 2) == 0) {
700 return std::numeric_limits<double>::quiet_NaN();
704 skipSpacesAndComments(dateString);
709 // The year may be after the time but before the time zone.
710 if (isASCIIDigit(*dateString) && year == -1) {
711 if (!parseInt(dateString, &newPosStr, 10, &year))
712 return std::numeric_limits<double>::quiet_NaN();
713 dateString = newPosStr;
714 skipSpacesAndComments(dateString);
717 // Don't fail if the time zone is missing.
718 // Some websites omit the time zone (4275206).
720 if (strncasecmp(dateString, "GMT", 3) == 0 || strncasecmp(dateString, "UTC", 3) == 0) {
725 if (*dateString == '+' || *dateString == '-') {
727 if (!parseInt(dateString, &newPosStr, 10, &o))
728 return std::numeric_limits<double>::quiet_NaN();
729 dateString = newPosStr;
731 if (o < -9959 || o > 9959)
732 return std::numeric_limits<double>::quiet_NaN();
734 int sgn = (o < 0) ? -1 : 1;
736 if (*dateString != ':') {
738 offset = ((o / 100) * 60 + (o % 100)) * sgn;
740 offset = o * 60 * sgn;
741 } else { // GMT+05:00
742 ++dateString; // skip the ':'
744 if (!parseInt(dateString, &newPosStr, 10, &o2))
745 return std::numeric_limits<double>::quiet_NaN();
746 dateString = newPosStr;
747 offset = (o * 60 + o2) * sgn;
751 for (size_t i = 0; i < WTF_ARRAY_LENGTH(known_zones); ++i) {
752 if (0 == strncasecmp(dateString, known_zones[i].tzName, strlen(known_zones[i].tzName))) {
753 offset = known_zones[i].tzOffset;
754 dateString += strlen(known_zones[i].tzName);
762 skipSpacesAndComments(dateString);
764 if (*dateString && year == -1) {
765 if (!parseInt(dateString, &newPosStr, 10, &year))
766 return std::numeric_limits<double>::quiet_NaN();
767 dateString = newPosStr;
768 skipSpacesAndComments(dateString);
773 return std::numeric_limits<double>::quiet_NaN();
775 // Y2K: Handle 2 digit years.
776 if (year >= 0 && year < 100) {
783 return ymdhmsToSeconds(year, month + 1, day, hour, minute, second) * msPerSecond;
786 double parseDateFromNullTerminatedCharacters(const char* dateString)
790 double ms = parseDateFromNullTerminatedCharacters(dateString, haveTZ, offset);
792 return std::numeric_limits<double>::quiet_NaN();
794 // fall back to local timezone
796 double utcOffset = calculateUTCOffset();
797 double dstOffset = calculateDSTOffset(ms, utcOffset);
798 offset = static_cast<int>((utcOffset + dstOffset) / msPerMinute);
800 return ms - (offset * msPerMinute);
803 // See http://tools.ietf.org/html/rfc2822#section-3.3 for more information.
804 String makeRFC2822DateString(unsigned dayOfWeek, unsigned day, unsigned month, unsigned year, unsigned hours, unsigned minutes, unsigned seconds, int utcOffset)
806 StringBuilder stringBuilder;
807 stringBuilder.append(weekdayName[dayOfWeek]);
808 stringBuilder.appendLiteral(", ");
809 stringBuilder.appendNumber(day);
810 stringBuilder.append(' ');
811 stringBuilder.append(monthName[month]);
812 stringBuilder.append(' ');
813 stringBuilder.appendNumber(year);
814 stringBuilder.append(' ');
816 appendTwoDigitNumber(stringBuilder, hours);
817 stringBuilder.append(':');
818 appendTwoDigitNumber(stringBuilder, minutes);
819 stringBuilder.append(':');
820 appendTwoDigitNumber(stringBuilder, seconds);
821 stringBuilder.append(' ');
823 stringBuilder.append(utcOffset > 0 ? '+' : '-');
824 int absoluteUTCOffset = abs(utcOffset);
825 appendTwoDigitNumber(stringBuilder, absoluteUTCOffset / 60);
826 appendTwoDigitNumber(stringBuilder, absoluteUTCOffset % 60);
828 return stringBuilder.toString();
831 double convertToLocalTime(double ms)
833 double utcOffset = calculateUTCOffset();
834 double dstOffset = calculateDSTOffset(ms, utcOffset);
835 return (ms + utcOffset + dstOffset);