diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a754a14d3..4ae64c91a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -639,6 +639,9 @@ for the list of changes made since `v2.0.0-alpha.1`. - `isDate` now works properly with dates passed across iframes [#754](https://github.com/date-fns/date-fns/pull/754). +- Fix a few bugs that appear in timezones with offsets that include seconds (e.g. GMT+00:57:44). + See PR [#789](https://github.com/date-fns/date-fns/pull/789). + ## [1.28.5] - 2017-05-19 ### Fixed diff --git a/src/_lib/addUTCMinutes/index.js b/src/_lib/addUTCMinutes/index.js deleted file mode 100644 index f4d7d651f0..0000000000 --- a/src/_lib/addUTCMinutes/index.js +++ /dev/null @@ -1,15 +0,0 @@ -import toInteger from '../toInteger/index.js' -import toDate from '../../toDate/index.js' - -// This function will be a part of public API when UTC function will be implemented. -// See issue: https://github.com/date-fns/date-fns/issues/376 -export default function addUTCMinutes (dirtyDate, dirtyAmount, dirtyOptions) { - if (arguments.length < 2) { - throw new TypeError('2 arguments required, but only ' + arguments.length + ' present') - } - - var date = toDate(dirtyDate, dirtyOptions) - var amount = toInteger(dirtyAmount) - date.setUTCMinutes(date.getUTCMinutes() + amount) - return date -} diff --git a/src/_lib/addUTCMinutes/test.js b/src/_lib/addUTCMinutes/test.js deleted file mode 100644 index 6dbd77cd47..0000000000 --- a/src/_lib/addUTCMinutes/test.js +++ /dev/null @@ -1,61 +0,0 @@ -// @flow -/* eslint-env mocha */ - -import assert from 'power-assert' -import addUTCMinutes from '.' - -describe('addUTCMinutes', function () { - it('adds the given number of minutes', function () { - var result = addUTCMinutes(new Date(Date.UTC(2014, 6 /* Jul */, 10, 12, 0)), 30) - assert.deepEqual(result, new Date(Date.UTC(2014, 6 /* Jul */, 10, 12, 30))) - }) - - it('accepts a string', function () { - var result = addUTCMinutes( - new Date(Date.UTC(2014, 6 /* Jul */, 10, 12, 0)).toISOString(), 20 - ) - assert.deepEqual(result, new Date(Date.UTC(2014, 6 /* Jul */, 10, 12, 20))) - }) - - it('accepts a timestamp', function () { - var result = addUTCMinutes( - new Date(Date.UTC(2014, 6 /* Jul */, 10, 12, 0)).getTime(), 20 - ) - assert.deepEqual(result, new Date(Date.UTC(2014, 6 /* Jul */, 10, 12, 20))) - }) - - it('converts a fractional number to an integer', function () { - var result = addUTCMinutes(new Date(Date.UTC(2014, 6 /* Jul */, 10, 12, 0)), 30.5) - assert.deepEqual(result, new Date(Date.UTC(2014, 6 /* Jul */, 10, 12, 30))) - }) - - it('implicitly converts number arguments', function () { - var result = addUTCMinutes(new Date(Date.UTC(2014, 6 /* Jul */, 10, 12, 0)), '30') - assert.deepEqual(result, new Date(Date.UTC(2014, 6 /* Jul */, 10, 12, 30))) - }) - - it('does not mutate the original date', function () { - var date = new Date(Date.UTC(2014, 6 /* Jul */, 10, 12, 0)) - addUTCMinutes(date, 25) - assert.deepEqual(date, new Date(Date.UTC(2014, 6 /* Jul */, 10, 12, 0))) - }) - - it('returns `Invalid Date` if the given date is invalid', function () { - var result = addUTCMinutes(new Date(NaN), 30) - assert(result instanceof Date && isNaN(result)) - }) - - it('returns `Invalid Date` if the given amount is NaN', function () { - var result = addUTCMinutes(new Date(2014, 6 /* Jul */, 10, 12, 0), NaN) - assert(result instanceof Date && isNaN(result)) - }) - - it('throws `RangeError` if `options.additionalDigits` is not convertable to 0, 1, 2 or undefined', function () { - var block = addUTCMinutes.bind(null, new Date(2014, 6 /* Jul */, 10, 12, 0), 30, {additionalDigits: NaN}) - assert.throws(block, RangeError) - }) - - it('throws TypeError exception if passed less than 1 argument', function () { - assert.throws(addUTCMinutes.bind(null, 1), TypeError) - }) -}) diff --git a/src/_lib/getTimezoneOffsetInMilliseconds/index.js b/src/_lib/getTimezoneOffsetInMilliseconds/index.js new file mode 100644 index 0000000000..2136c78368 --- /dev/null +++ b/src/_lib/getTimezoneOffsetInMilliseconds/index.js @@ -0,0 +1,21 @@ +var MILLISECONDS_IN_MINUTE = 60000 + +/** + * Google Chrome as of 67.0.3396.87 introduced timezones with offset that includes seconds. + * They usually appear for dates that denote time before the timezones were introduced + * (e.g. for 'Europe/Prague' timezone the offset is GMT+00:57:44 before 1 October 1891 + * and GMT+01:00:00 after that date) + * + * Date#getTimezoneOffset returns the offset in minutes and would return 57 for the example above, + * which would lead to incorrect calculations. + * + * This function returns the timezone offset in milliseconds that takes seconds in account. + */ +export default function getTimezoneOffsetInMilliseconds (dirtyDate) { + var date = new Date(dirtyDate.getTime()) + var baseTimezoneOffset = date.getTimezoneOffset() + date.setSeconds(0, 0) + var millisecondsPartOfTimezoneOffset = date.getTime() % MILLISECONDS_IN_MINUTE + + return baseTimezoneOffset * MILLISECONDS_IN_MINUTE + millisecondsPartOfTimezoneOffset +} diff --git a/src/_lib/getTimezoneOffsetInMilliseconds/test.js b/src/_lib/getTimezoneOffsetInMilliseconds/test.js new file mode 100644 index 0000000000..821e16f72b --- /dev/null +++ b/src/_lib/getTimezoneOffsetInMilliseconds/test.js @@ -0,0 +1,28 @@ +// @flow +/* eslint-env mocha */ + +import assert from 'power-assert' +import getTimezoneOffsetInMilliseconds from '.' + +describe('getTimezoneOffsetInMilliseconds', function () { + it('works for a modern date', function () { + var date = new Date(2018, 0 /* Jan */, 1, 12, 34, 56, 789) + var result = date.getTime() - getTimezoneOffsetInMilliseconds(date) + var expectedResult = Date.UTC(2018, 0 /* Jan */, 1, 12, 34, 56, 789) + assert(result === expectedResult) + }) + + it('works for a date before standardized timezones', function () { + var date = new Date(1800, 0 /* Jan */, 1, 12, 34, 56, 789) + var result = date.getTime() - getTimezoneOffsetInMilliseconds(date) + var expectedResult = Date.UTC(1800, 0 /* Jan */, 1, 12, 34, 56, 789) + assert(result === expectedResult) + }) + + it('works for a date BC', function () { + var date = new Date(-500, 0 /* Jan */, 1, 12, 34, 56, 789) + var result = date.getTime() - getTimezoneOffsetInMilliseconds(date) + var expectedResult = Date.UTC(-500, 0 /* Jan */, 1, 12, 34, 56, 789) + assert(result === expectedResult) + }) +}) diff --git a/src/differenceInCalendarDays/index.js b/src/differenceInCalendarDays/index.js index 4b97189848..e053891fc9 100644 --- a/src/differenceInCalendarDays/index.js +++ b/src/differenceInCalendarDays/index.js @@ -1,6 +1,6 @@ +import getTimezoneOffsetInMilliseconds from '../_lib/getTimezoneOffsetInMilliseconds/index.js' import startOfDay from '../startOfDay/index.js' -var MILLISECONDS_IN_MINUTE = 60000 var MILLISECONDS_IN_DAY = 86400000 /** @@ -45,9 +45,9 @@ export default function differenceInCalendarDays (dirtyDateLeft, dirtyDateRight, var startOfDayRight = startOfDay(dirtyDateRight, dirtyOptions) var timestampLeft = startOfDayLeft.getTime() - - startOfDayLeft.getTimezoneOffset() * MILLISECONDS_IN_MINUTE + getTimezoneOffsetInMilliseconds(startOfDayLeft) var timestampRight = startOfDayRight.getTime() - - startOfDayRight.getTimezoneOffset() * MILLISECONDS_IN_MINUTE + getTimezoneOffsetInMilliseconds(startOfDayRight) // Round the number of days to the nearest integer // because the number of milliseconds in a day is not constant diff --git a/src/differenceInCalendarISOWeeks/index.js b/src/differenceInCalendarISOWeeks/index.js index cd8036d372..be4dfbdfbb 100644 --- a/src/differenceInCalendarISOWeeks/index.js +++ b/src/differenceInCalendarISOWeeks/index.js @@ -1,6 +1,6 @@ +import getTimezoneOffsetInMilliseconds from '../_lib/getTimezoneOffsetInMilliseconds/index.js' import startOfISOWeek from '../startOfISOWeek/index.js' -var MILLISECONDS_IN_MINUTE = 60000 var MILLISECONDS_IN_WEEK = 604800000 /** @@ -38,9 +38,9 @@ export default function differenceInCalendarISOWeeks (dirtyDateLeft, dirtyDateRi var startOfISOWeekRight = startOfISOWeek(dirtyDateRight, dirtyOptions) var timestampLeft = startOfISOWeekLeft.getTime() - - startOfISOWeekLeft.getTimezoneOffset() * MILLISECONDS_IN_MINUTE + getTimezoneOffsetInMilliseconds(startOfISOWeekLeft) var timestampRight = startOfISOWeekRight.getTime() - - startOfISOWeekRight.getTimezoneOffset() * MILLISECONDS_IN_MINUTE + getTimezoneOffsetInMilliseconds(startOfISOWeekRight) // Round the number of days to the nearest integer // because the number of milliseconds in a week is not constant diff --git a/src/differenceInCalendarWeeks/index.js b/src/differenceInCalendarWeeks/index.js index d81465320c..4cb73f081f 100644 --- a/src/differenceInCalendarWeeks/index.js +++ b/src/differenceInCalendarWeeks/index.js @@ -1,6 +1,6 @@ +import getTimezoneOffsetInMilliseconds from '../_lib/getTimezoneOffsetInMilliseconds/index.js' import startOfWeek from '../startOfWeek/index.js' -var MILLISECONDS_IN_MINUTE = 60000 var MILLISECONDS_IN_WEEK = 604800000 /** @@ -49,9 +49,9 @@ export default function differenceInCalendarWeeks (dirtyDateLeft, dirtyDateRight var startOfWeekRight = startOfWeek(dirtyDateRight, dirtyOptions) var timestampLeft = startOfWeekLeft.getTime() - - startOfWeekLeft.getTimezoneOffset() * MILLISECONDS_IN_MINUTE + getTimezoneOffsetInMilliseconds(startOfWeekLeft) var timestampRight = startOfWeekRight.getTime() - - startOfWeekRight.getTimezoneOffset() * MILLISECONDS_IN_MINUTE + getTimezoneOffsetInMilliseconds(startOfWeekRight) // Round the number of days to the nearest integer // because the number of milliseconds in a week is not constant diff --git a/src/format/index.js b/src/format/index.js index 01c3dc5c3f..19dafff248 100644 --- a/src/format/index.js +++ b/src/format/index.js @@ -1,10 +1,11 @@ import toInteger from '../_lib/toInteger/index.js' +import getTimezoneOffsetInMilliseconds from '../_lib/getTimezoneOffsetInMilliseconds/index.js' import toDate from '../toDate/index.js' import isValid from '../isValid/index.js' import defaultLocale from '../locale/en-US/index.js' import formatters from './_lib/formatters/index.js' import longFormatters from './_lib/longFormatters/index.js' -import addUTCMinutes from '../_lib/addUTCMinutes/index.js' +import subMilliseconds from '../subMilliseconds/index.js' // This RegExp consists of three parts separated by `|`: // - [yYQqMLwIdDecihHKkms]o matches any available ordinal number token @@ -359,8 +360,8 @@ export default function format (dirtyDate, dirtyFormatStr, dirtyOptions) { // Convert the date in system timezone to the same date in UTC+00:00 timezone. // This ensures that when UTC functions will be implemented, locales will be compatible with them. // See an issue about UTC functions: https://github.com/date-fns/date-fns/issues/376 - var timezoneOffset = originalDate.getTimezoneOffset() - var utcDate = addUTCMinutes(originalDate, -timezoneOffset, options) + var timezoneOffset = getTimezoneOffsetInMilliseconds(originalDate) + var utcDate = subMilliseconds(originalDate, timezoneOffset, options) var formatterOptions = { firstWeekContainsDate: firstWeekContainsDate, diff --git a/src/formatDistance/index.js b/src/formatDistance/index.js index 5ef72034f4..bad82e68d7 100644 --- a/src/formatDistance/index.js +++ b/src/formatDistance/index.js @@ -1,3 +1,4 @@ +import getTimezoneOffsetInMilliseconds from '../_lib/getTimezoneOffsetInMilliseconds/index.js' import compareAsc from '../compareAsc/index.js' import toDate from '../toDate/index.js' import differenceInSeconds from '../differenceInSeconds/index.js' @@ -130,8 +131,8 @@ export default function formatDistance (dirtyDate, dirtyBaseDate, dirtyOptions) } var seconds = differenceInSeconds(dateRight, dateLeft, options) - var offset = dateRight.getTimezoneOffset() - dateLeft.getTimezoneOffset() - var minutes = Math.round(seconds / 60) - offset + var offsetInSeconds = (getTimezoneOffsetInMilliseconds(dateRight) - getTimezoneOffsetInMilliseconds(dateLeft)) / 1000 + var minutes = Math.round((seconds - offsetInSeconds) / 60) var months // 0 up to 2 mins diff --git a/src/formatDistanceStrict/index.js b/src/formatDistanceStrict/index.js index c4451ab5fd..1a76d8e02d 100644 --- a/src/formatDistanceStrict/index.js +++ b/src/formatDistanceStrict/index.js @@ -1,3 +1,4 @@ +import getTimezoneOffsetInMilliseconds from '../_lib/getTimezoneOffsetInMilliseconds/index.js' import compareAsc from '../compareAsc/index.js' import toDate from '../toDate/index.js' import differenceInSeconds from '../differenceInSeconds/index.js' @@ -145,8 +146,8 @@ export default function formatDistanceStrict (dirtyDate, dirtyBaseDate, dirtyOpt } var seconds = differenceInSeconds(dateRight, dateLeft, dirtyOptions) - var offset = dateRight.getTimezoneOffset() - dateLeft.getTimezoneOffset() - var minutes = roundingMethodFn(seconds / 60) - offset + var offsetInSeconds = (getTimezoneOffsetInMilliseconds(dateRight) - getTimezoneOffsetInMilliseconds(dateLeft)) / 1000 + var minutes = roundingMethodFn((seconds - offsetInSeconds) / 60) var unit if (options.unit == null) { diff --git a/src/formatRelative/index.js b/src/formatRelative/index.js index 3fba394ffa..aa9bcf1faf 100644 --- a/src/formatRelative/index.js +++ b/src/formatRelative/index.js @@ -1,8 +1,9 @@ +import getTimezoneOffsetInMilliseconds from '../_lib/getTimezoneOffsetInMilliseconds/index.js' import toDate from '../toDate/index.js' import format from '../format/index.js' import differenceInCalendarDays from '../differenceInCalendarDays/index.js' import defaultLocale from '../locale/en-US/index.js' -import subMinutes from '../subMinutes/index.js' +import subMilliseconds from '../subMilliseconds/index.js' /** * @name formatRelative @@ -78,8 +79,8 @@ export default function formatRelative (dirtyDate, dirtyBaseDate, dirtyOptions) token = 'other' } - var utcDate = subMinutes(date, date.getTimezoneOffset(), options) - var utcBaseDate = subMinutes(baseDate, date.getTimezoneOffset(), options) + var utcDate = subMilliseconds(date, getTimezoneOffsetInMilliseconds(date), options) + var utcBaseDate = subMilliseconds(baseDate, getTimezoneOffsetInMilliseconds(baseDate), options) var formatStr = locale.formatRelative(token, utcDate, utcBaseDate, options) return format(date, formatStr, options) } diff --git a/src/parse/index.js b/src/parse/index.js index b709f90d0f..cc72f32cab 100644 --- a/src/parse/index.js +++ b/src/parse/index.js @@ -1,6 +1,7 @@ import toInteger from '../_lib/toInteger/index.js' +import getTimezoneOffsetInMilliseconds from '../_lib/getTimezoneOffsetInMilliseconds/index.js' import toDate from '../toDate/index.js' -import subMinutes from '../subMinutes/index.js' +import subMilliseconds from '../subMilliseconds/index.js' import defaultLocale from '../locale/en-US/index.js' import parsers from './_lib/parsers/index.js' @@ -416,7 +417,7 @@ export default function parse (dirtyDateString, dirtyFormatString, dirtyBaseDate // Convert the date in system timezone to the same date in UTC+00:00 timezone. // This ensures that when UTC functions will be implemented, locales will be compatible with them. // See an issue about UTC functions: https://github.com/date-fns/date-fns/issues/37 - var utcDate = subMinutes(date, date.getTimezoneOffset()) + var utcDate = subMilliseconds(date, getTimezoneOffsetInMilliseconds(date)) for (i = 0; i < uniquePrioritySetters.length; i++) { var setter = uniquePrioritySetters[i] diff --git a/src/toDate/index.js b/src/toDate/index.js index 12810e6492..6e08e4496c 100644 --- a/src/toDate/index.js +++ b/src/toDate/index.js @@ -1,4 +1,5 @@ import toInteger from '../_lib/toInteger/index.js' +import getTimezoneOffsetInMilliseconds from '../_lib/getTimezoneOffsetInMilliseconds/index.js' var MILLISECONDS_IN_HOUR = 3600000 var MILLISECONDS_IN_MINUTE = 60000 @@ -128,11 +129,11 @@ export default function toDate (argument, dirtyOptions) { offset = parseTimezone(dateStrings.timezone) } else { // get offset accurate to hour in timezones that change offset - offset = new Date(timestamp + time).getTimezoneOffset() - offset = new Date(timestamp + time + offset * MILLISECONDS_IN_MINUTE).getTimezoneOffset() + offset = getTimezoneOffsetInMilliseconds(new Date(timestamp + time)) + offset = getTimezoneOffsetInMilliseconds(new Date(timestamp + time + offset)) } - return new Date(timestamp + time + offset * MILLISECONDS_IN_MINUTE) + return new Date(timestamp + time + offset) } else { return new Date(NaN) } @@ -314,14 +315,14 @@ function parseTimezone (timezoneString) { // ±hh token = patterns.timezoneHH.exec(timezoneString) if (token) { - absoluteOffset = parseInt(token[2], 10) * 60 + absoluteOffset = parseInt(token[2], 10) * MILLISECONDS_IN_HOUR return (token[1] === '+') ? -absoluteOffset : absoluteOffset } // ±hh:mm or ±hhmm token = patterns.timezoneHHMM.exec(timezoneString) if (token) { - absoluteOffset = parseInt(token[2], 10) * 60 + parseInt(token[3], 10) + absoluteOffset = parseInt(token[2], 10) * MILLISECONDS_IN_HOUR + parseInt(token[3], 10) * MILLISECONDS_IN_MINUTE return (token[1] === '+') ? -absoluteOffset : absoluteOffset }