date-fns-tz icon indicating copy to clipboard operation
date-fns-tz copied to clipboard

formatInTimeZone gives wrong results around DST

Open Gadzy opened this issue 2 years ago • 5 comments

This issue covers 2 problems in 1:

  • formatInTimeZone gives wrong results around DST
  • formatInTimeZone gives inconsistent results around DST depending on host time zone

Testing code:

describe('Europe/Paris', function () {
  var tests = [
    {
      name: 'summer time CEST',
      date: '2021-08-01T00:30:00Z',
      expected: '2021-08-01T02:30:00+02:00',
    },
    {
      name: 'winter time CET',
      date: '2021-01-01T01:30:00Z',
      expected: '2021-01-01T02:30:00+01:00',
    },
    {
      name: 'before DST changeover (CEST to CET)',
      date: '2021-10-31T00:30:00Z',
      expected: '2021-10-31T02:30:00+02:00',
    },
    {
      name: 'after DST changeover (CEST to CET)',
      date: '2021-10-31T01:30:00Z',
      expected: '2021-10-31T02:30:00+01:00',
    },
    {
      name: 'before DST changeover (CET to CEST)',
      date: '2021-03-28T00:30:00Z',
      expected: '2021-03-28T01:30:00+01:00',
    },
    {
      name: 'after DST changeover (CET to CEST)',
      date: '2021-03-28T01:30:00Z',
      expected: '2021-03-28T03:30:00+02:00',
    },
  ]

  tests.forEach(({ name, date, expected }) => {
    it(`${name}: ${date}`, function () {
      var timeZone = 'Europe/Paris'
      var result = formatInTimeZone(date, timeZone, "yyyy-MM-dd'T'HH:mm:ssxxx")
      assert(result === expected)
      assert(new Date(result).getTime() === new Date(date).getTime()) // Equivalent to previous assertion
    })
  })

Testing results:

  • first with host TZ set to Europe/Paris,
  • then with host TZ set to UTC
$ cmd.exe '/c' 'tzutil /g'
Romance Standard Time

$ yarn test

SUMMARY:
✔ 5 tests completed
✖ 1 test failed

FAILED TESTS:
  Europe/Paris
    ✖ after DST changeover (CEST to CET): 2021-10-31T01:30:00Z
      Chrome 98.0.4758 (Windows 10.0.0)
    AssertionError:   # src\formatInTimeZone\test.js:64
      
      assert(result === expected)
             |      |   |        
             |      |   "2021-10-31T02:30:00+01:00"
             |      false        
             "2021-10-31T02:30:00+02:00"
      
      --- [string] expected
      +++ [string] result
      @@ -14,12 +14,12 @@
       :30:00+0
      -1
      +2
       :00

$ cmd.exe '/c' 'tzutil /s UTC'

$ cmd.exe '/c' 'tzutil /g'
UTC

$ yarn test

SUMMARY:
✔ 4 tests completed
✖ 2 tests failed

FAILED TESTS:
  Europe/Paris
    ✖ before DST changeover (CEST to CET): 2021-10-31T00:30:00Z
      Chrome 98.0.4758 (Windows 10.0.0)
    AssertionError:   # src\formatInTimeZone\test.js:64
      
      assert(result === expected)
             |      |   |        
             |      |   "2021-10-31T02:30:00+02:00"
             |      false        
             "2021-10-31T02:30:00+01:00"
      
      --- [string] expected
      +++ [string] result
      @@ -14,12 +14,12 @@
       :30:00+0
      -2
      +1
       :00

    ✖ before DST changeover (CET to CEST): 2021-03-28T00:30:00Z
      Chrome 98.0.4758 (Windows 10.0.0)
    AssertionError:   # src\formatInTimeZone\test.js:64
      
      assert(result === expected)
             |      |   |        
             |      |   "2021-03-28T01:30:00+01:00"
             |      false        
             "2021-03-28T01:30:00+02:00"

      --- [string] expected
      +++ [string] result
      @@ -14,12 +14,12 @@
       :30:00+0
      -1
      +2
       :00

Gadzy avatar Mar 02 '22 15:03 Gadzy

Moreover, and maybe related, these tests in getTimezoneOffset/test.js are wrong by design. DST changeover happens respectively at 02:00 (AEST to AEDT) and 03:00 (AEDT to AEST) LOCAL TIME and not UTC time.

  describe('near DST changeover (AEST to AEDT)', function () {
    it('one day before', function () {
      var date = new Date('2020-10-04T00:45:00.000Z')
      assert.equal(getTimezoneOffset('Australia/Melbourne', date), 10 * hours)
    })

    it('15 minutes before', function () {
      var date = new Date('2020-10-04T01:45:00.000Z')
      assert.equal(getTimezoneOffset('Australia/Melbourne', date), 10 * hours)
    })

    it('15 minutes after', function () {
      var date = new Date('2020-10-04T03:15:00.000Z')
      assert.equal(getTimezoneOffset('Australia/Melbourne', date), 11 * hours)
    })
  })

Gadzy avatar Mar 02 '22 16:03 Gadzy

I assume it's the same issue but I saw this trying to format some parsed dates as UTC. You can see the issue even if the input dates are UTC already as shown below. The output depends on my local timezone (Europe/Berlin).

describe('formatInTimeZone', () => {
  test.each([
    { input: '2023-03-26T00:11:00.000Z', expected: '2023-03-26 00:11:00 UTC' },
    { input: '2023-03-26T01:11:01.000Z', expected: '2023-03-26 01:11:01 UTC' },
    { input: '2023-03-26T02:11:02.000Z', expected: '2023-03-26 02:11:02 UTC' },
    { input: '2023-03-26T03:11:03.000Z', expected: '2023-03-26 03:11:03 UTC' },
  ])('formatInTimeZone($input) == $expected', ({ input, expected }) => {
    expect(formatInTimeZone(input, 'UTC', 'yyyy-MM-dd HH:mm:ss zzz')).toEqual(
      expected,
    )
  })
})
  foo
    ✓ formatInTimeZone(2023-03-26T00:11:00.000Z) == 2023-03-26 00:11:00 UTC (16 ms)
    ✓ formatInTimeZone(2023-03-26T01:11:01.000Z) == 2023-03-26 01:11:01 UTC (1 ms)
    ✕ formatInTimeZone(2023-03-26T02:11:02.000Z) == 2023-03-26 02:11:02 UTC (4 ms)
    ✓ formatInTimeZone(2023-03-26T03:11:03.000Z) == 2023-03-26 03:11:03 UTC (1 ms)

  ● formatInTimeZone › formatInTimeZone(2023-03-26T02:11:02.000Z) == 2023-03-26 02:11:02 UTC

    expect(received).toEqual(expected) // deep equality

    Expected: "2023-03-26 02:11:02 UTC"
    Received: "2023-03-26 03:11:02 UTC"

      30 |     { input: '2023-03-26T03:11:03.000Z', expected: '2023-03-26 03:11:03 UTC' },
      31 |   ])('formatInTimeZone($input) == $expected', ({ input, expected }) => {
    > 32 |     expect(formatInTimeZone(input, 'UTC', 'yyyy-MM-dd HH:mm:ss zzz')).toEqual(
         |                                                                       ^
      33 |       expected,
      34 |     )
      35 |   })

      at redacted.test.tsx:32:71

RazerM avatar Apr 12 '23 16:04 RazerM

This change should fix it: https://github.com/marnusw/date-fns-tz/pull/247

asennikov avatar Aug 24 '23 09:08 asennikov

This change should fix it: #247

It doesn't pass the test cases I provided above.

Gadzy avatar Nov 22 '23 22:11 Gadzy

This change should fix it: #247

It doesn't pass the test cases I provided above.

That's because I'm not touching getTimezoneOffset, I'm fixing formatInTimeZone, and the fix makes the tests from the description of this issue pass. Feel free to open another PR to fix the issues with getTimezoneOffset.

asennikov avatar Nov 23 '23 10:11 asennikov