Skip to content

<chrono>: Incorrect time conversion for zones where DST was observed in the past #6096

@daksh-goyal

Description

@daksh-goyal

Describe the bug

There are certain time zones that used to observe Daylight Savings Time sometime in the past but have since abolished the practice. When trying to convert the time on one of these dates from the past when DST was observed, STL produces incorrect output.

Command-line test case

C:\Temp>type repro.cpp
#include <iostream>
#include <chrono>
#include <format>
using namespace std;
using namespace std::chrono;

int main() {
    // Brazil (America/Sao_Paulo): Observed DST from ~Oct/Nov to ~Feb each year.
    // Last DST period: Nov 4, 2018 → Feb 17, 2019 (UTC-2 BRST).
    auto sp = locate_zone("America/Sao_Paulo");
    auto sp1 = local_days{year{2019}/month{1}/day{1}} + hours{13};
    cout << "=== America/Sao_Paulo (abolished DST Feb 2019) ===" << endl;
    cout << "2019-01-01 13:00 local -> " << format("{:%H:%M}", sp->to_sys(sp1)) << " UTC (expect 15:00, DST UTC-2)" << endl;
    auto si = sp->get_info(sp->to_sys(sp1));
    cout << "  offset=" << si.offset.count() << "s save=" << si.save.count() << "s" << endl;

    // Turkey: abolished DST in Sep 2016, permanent UTC+3
    // Summer 2016 had DST (UTC+3), standard was UTC+2
    auto istanbul = locate_zone("Europe/Istanbul");
    auto tur1 = local_days{year{2016}/month{7}/day{1}} + hours{12};
    auto tur2 = local_days{year{2015}/month{7}/day{1}} + hours{12};
    cout << "\n=== Europe/Istanbul (abolished DST Sep 2016) ===" << endl;
    cout << "2016-07-01 12:00 local -> " << format("{:%H:%M}", istanbul->to_sys(tur1)) << " UTC (expect 09:00, DST UTC+3)" << endl;
    cout << "2015-07-01 12:00 local -> " << format("{:%H:%M}", istanbul->to_sys(tur2)) << " UTC (expect 09:00, DST UTC+3)" << endl;
    auto ti = istanbul->get_info(istanbul->to_sys(tur1));
    cout << "  offset=" << ti.offset.count() << "s save=" << ti.save.count() << "s" << endl;

    // Russia: Summer 2010 had DST UTC+4, standard UTC+3
    auto moscow = locate_zone("Europe/Moscow");
    auto rus1 = local_days{year{2010}/month{7}/day{1}} + hours{12};
    cout << "\n=== Europe/Moscow (complex history) ===" << endl;
    cout << "2010-07-01 12:00 local -> " << format("{:%H:%M}", moscow->to_sys(rus1)) << " UTC (expect 08:00, DST UTC+4)" << endl;
    auto mi = moscow->get_info(moscow->to_sys(rus1));
    cout << "  offset=" << mi.offset.count() << "s save=" << mi.save.count() << "s" << endl;

    // Morocco: was UTC+0 with DST to UTC+1, went permanent UTC+1 in Oct 2018
    // DST was suspended during Ramadan each year, so pick dates outside Ramadan
    auto casablanca = locate_zone("Africa/Casablanca");
    auto mor1 = local_days{year{2017}/month{4}/day{15}} + hours{12};
    auto mor2 = local_days{year{2018}/month{4}/day{15}} + hours{12};
    cout << "\n=== Africa/Casablanca (permanent UTC+1 since Oct 2018) ===" << endl;
    cout << "2017-04-15 12:00 local -> " << format("{:%H:%M}", casablanca->to_sys(mor1)) << " UTC (expect 11:00, DST UTC+1)" << endl;
    cout << "2018-04-15 12:00 local -> " << format("{:%H:%M}", casablanca->to_sys(mor2)) << " UTC (expect 11:00, DST UTC+1)" << endl;
    auto ci = casablanca->get_info(casablanca->to_sys(mor1));
    cout << "  offset=" << ci.offset.count() << "s save=" << ci.save.count() << "s" << endl;

    // Control: New York (still has DST) - should be correct
    auto ny = locate_zone("America/New_York");
    auto ny1 = local_days{year{2020}/month{7}/day{1}} + hours{12};
    cout << "\n=== America/New_York (control, still has DST) ===" << endl;
    cout << "2020-07-01 12:00 local -> " << format("{:%H:%M}", ny->to_sys(ny1)) << " UTC (expect 16:00, EDT UTC-4)" << endl;
    auto ni = ny->get_info(ny->to_sys(ny1));
    cout << "  offset=" << ni.offset.count() << "s save=" << ni.save.count() << "s" << endl;

    return 0;
}

C:\Temp>cl /EHsc /W4 /WX /std:c++20 .\repro.cpp
Microsoft (R) C/C++ Optimizing Compiler Version 19.36.32522 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

/std:c++20

repro.cpp
Microsoft (R) Incremental Linker Version 14.36.32522.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:repro.exe
repro.obj

C:\Temp>.\repro.exe
=== America/Sao_Paulo (abolished DST Feb 2019) ===
2019-01-01 13:00 local -> 16:00 UTC (expect 15:00, DST UTC-2)
  offset=-10800s save=0s

=== Europe/Istanbul (abolished DST Sep 2016) ===
2016-07-01 12:00 local -> 10:00 UTC (expect 09:00, DST UTC+3)
2015-07-01 12:00 local -> 10:00 UTC (expect 09:00, DST UTC+3)
  offset=7200s save=0s

=== Europe/Moscow (complex history) ===
2010-07-01 12:00 local -> 09:00 UTC (expect 08:00, DST UTC+4)
  offset=10800s save=0s

=== Africa/Casablanca (permanent UTC+1 since Oct 2018) ===
2017-04-15 12:00 local -> 11:00 UTC (expect 11:00, DST UTC+1)
2018-04-15 12:00 local -> 11:00 UTC (expect 11:00, DST UTC+1)
  offset=3600s save=60s

=== America/New_York (control, still has DST) ===
2020-07-01 12:00 local -> 16:00 UTC (expect 16:00, EDT UTC-4)
  offset=-14400s save=60s

Expected behavior

Mentioned in the output above.

STL version

C:\Temp>cl.exe
Microsoft (R) C/C++ Optimizing Compiler Version 19.44.35222 for x86
Copyright (C) Microsoft Corporation.  All rights reserved.

usage: cl [ option... ] filename... [ /link linkoption... ]

Additional context

The problem here is because of STL's misunderstanding of ucal_inDaylightTime API from ICU. According to the documentation, it is used to

Determine if a UCalendar is currently in daylight savings time.

But STL seems to be using it to check whether the calendar was observing DST in the past, which is wrong. Relevant code from tzdb.cpp:

  const auto _Is_daylight = __icu_ucal_inDaylightTime(_Cal.get(), &_UErr);
  if (U_FAILURE(_UErr)) {
      return _Report_error(_Info, __std_tzdb_error::_Icu_error);
  }

  _Info->_Save = _Is_daylight ? __icu_ucal_get(_Cal.get(), UCalendarDateFields::UCAL_DST_OFFSET, &_UErr) : 0;
  if (U_FAILURE(_UErr)) {
      return _Report_error(_Info, __std_tzdb_error::_Icu_error);
  }

STL was probably trying to be defensive but ended up shooting itself in the foot with this one. The suggested fix would be to just not use that guard and directly obtain the DST offset all the time.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingchronoC++20 chrono

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions