Skip to content

[libc++][TZDB] Implements time_zone get_info(local_time). #89537

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions libcxx/include/__chrono/time_zone.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
// Enable the contents of the header only when libc++ was built with experimental features enabled.
#if !defined(_LIBCPP_HAS_NO_EXPERIMENTAL_TZDB)

# include <__chrono/calendar.h>
# include <__chrono/duration.h>
# include <__chrono/local_info.h>
# include <__chrono/sys_info.h>
# include <__chrono/system_clock.h>
# include <__compare/strong_order.h>
Expand Down Expand Up @@ -63,12 +65,18 @@ class _LIBCPP_AVAILABILITY_TZDB time_zone {
return __get_info(chrono::time_point_cast<seconds>(__time));
}

template <class _Duration>
[[nodiscard]] _LIBCPP_HIDE_FROM_ABI local_info get_info(const local_time<_Duration>& __time) const {
return __get_info(chrono::time_point_cast<seconds>(__time));
}

[[nodiscard]] _LIBCPP_HIDE_FROM_ABI const __impl& __implementation() const noexcept { return *__impl_; }

private:
[[nodiscard]] _LIBCPP_EXPORTED_FROM_ABI string_view __name() const noexcept;

[[nodiscard]] _LIBCPP_AVAILABILITY_TZDB _LIBCPP_EXPORTED_FROM_ABI sys_info __get_info(sys_seconds __time) const;
[[nodiscard]] _LIBCPP_AVAILABILITY_TZDB _LIBCPP_EXPORTED_FROM_ABI local_info __get_info(local_seconds __time) const;

unique_ptr<__impl> __impl_;
};
Expand Down
3 changes: 3 additions & 0 deletions libcxx/include/chrono
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,9 @@ class time_zone {

template<class Duration>
sys_info get_info(const sys_time<Duration>& st) const;

template<class Duration>
local_info get_info(const local_time<Duration>& tp) const;
};
bool operator==(const time_zone& x, const time_zone& y) noexcept; // C++20
strong_ordering operator<=>(const time_zone& x, const time_zone& y) noexcept; // C++20
Expand Down
147 changes: 147 additions & 0 deletions libcxx/src/time_zone.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#include <chrono>
#include <expected>
#include <map>
#include <numeric>
#include <ranges>

#include "include/tzdb/time_zone_private.h"
Expand Down Expand Up @@ -903,6 +904,152 @@ time_zone::__get_info(sys_seconds __time) const {
std::__throw_runtime_error("tzdb: corrupt db");
}

// Is the "__local_time" present in "__first" and "__second". If so the
// local_info has an ambiguous result.
[[nodiscard]] static bool
__is_ambiguous(local_seconds __local_time, const sys_info& __first, const sys_info& __second) {
std::chrono::local_seconds __end_first{__first.end.time_since_epoch() + __first.offset};
std::chrono::local_seconds __begin_second{__second.begin.time_since_epoch() + __second.offset};

return __local_time < __end_first && __local_time >= __begin_second;
}

// Determines the result of the "__local_time". This expects the object
// "__first" to be earlier in time than "__second".
[[nodiscard]] static local_info
__get_info(local_seconds __local_time, const sys_info& __first, const sys_info& __second) {
std::chrono::local_seconds __end_first{__first.end.time_since_epoch() + __first.offset};
std::chrono::local_seconds __begin_second{__second.begin.time_since_epoch() + __second.offset};

if (__local_time < __end_first) {
if (__local_time >= __begin_second)
// |--------|
// |------|
// ^
return {local_info::ambiguous, __first, __second};

// |--------|
// |------|
// ^
return {local_info::unique, __first, sys_info{}};
}

if (__local_time < __begin_second)
// |--------|
// |------|
// ^
return {local_info::nonexistent, __first, __second};

// |--------|
// |------|
// ^
return {local_info::unique, __second, sys_info{}};
}

[[nodiscard]] _LIBCPP_AVAILABILITY_TZDB _LIBCPP_EXPORTED_FROM_ABI local_info
time_zone::__get_info(local_seconds __local_time) const {
seconds __local_seconds = __local_time.time_since_epoch();

/* An example of a typical year with a DST switch displayed in local time.
*
* At the first of April the time goes forward one hour. This means the
* time marked with ~~ is not a valid local time. This is represented by the
* nonexistent value in local_info.result.
*
* At the first of November the time goes backward one hour. This means the
* time marked with ^^ happens twice. This is represented by the ambiguous
* value in local_info.result.
*
* 2020.11.01 2021.04.01 2021.11.01
* offset +05 offset +05 offset +05
* save 0s save 1h save 0s
* |------------//----------|
* |---------//--------------|
* |-------------
* ~~ ^^
*
* These shifts can happen due to changes in the current time zone for a
* location. For example, Indian/Kerguelen switched only once. In 1950 from an
* offset of 0 hours to an offset of +05 hours.
*
* During all these shifts the UTC time will not have gaps.
*/

// The code needs to determine the system time for the local time. There is no
// information available. Assume the offset between system time and local time
// is 0s. This gives an initial estimate.
sys_seconds __guess{__local_seconds};
sys_info __info = __get_info(__guess);

// At this point the offset can be used to determine an estimate for the local
// time. Before doing that, determine the offset and validate whether the
// local time is the range [chrono::local_seconds::min(),
// chrono::local_seconds::max()).
if (__local_seconds < 0s && __info.offset > 0s)
if (__local_seconds - chrono::local_seconds::min().time_since_epoch() < __info.offset)
return {-1, __info, {}};

if (__local_seconds > 0s && __info.offset < 0s)
if (chrono::local_seconds::max().time_since_epoch() - __local_seconds < -__info.offset)
return {-2, __info, {}};

// Based on the information found in the sys_info, the local time can be
// converted to a system time. This resulting time can be in the following
// locations of the sys_info:
//
// |---------//--------------|
// 1 2.1 2.2 2.3 3
//
// 1. The estimate is before the returned sys_info object.
// The result is either non-existent or unique in the previous sys_info.
// 2. The estimate is in the sys_info object
// - If the sys_info begin is not sys_seconds::min(), then it might be at
// 2.1 and could be ambiguous with the previous or unique.
// - If sys_info end is not sys_seconds::max(), then it might be at 2.3
// and could be ambiguous with the next or unique.
// - Else it is at 2.2 and always unique. This case happens when a
// time zone has no transitions. For example, UTC or GMT+1.
// 3. The estimate is after the returned sys_info object.
// The result is either non-existent or unique in the next sys_info.
//
// There is no specification where the "middle" starts. Similar issues can
// happen when sys_info objects are "short", then "unique in the next" could
// become "ambiguous in the next and the one following". Theoretically there
// is the option of the following time-line
//
// |------------|
// |----|
// |-----------------|
//
// However the local_info object only has 2 sys_info objects, so this option
// is not tested.

sys_seconds __sys_time{__local_seconds - __info.offset};
if (__sys_time < __info.begin)
// Case 1 before __info
return chrono::__get_info(__local_time, __get_info(__info.begin - 1s), __info);

if (__sys_time >= __info.end)
// Case 3 after __info
return chrono::__get_info(__local_time, __info, __get_info(__info.end));

// Case 2 in __info
if (__info.begin != sys_seconds::min()) {
// Case 2.1 Not at the beginning, when not ambiguous the result should test
// case 2.3.
sys_info __prev = __get_info(__info.begin - 1s);
if (__is_ambiguous(__local_time, __prev, __info))
return {local_info::ambiguous, __prev, __info};
}

if (__info.end == sys_seconds::max())
// At the end so it's case 2.2
return {local_info::unique, __info, sys_info{}};

// This tests case 2.2 or case 2.3.
return chrono::__get_info(__local_time, __info, __get_info(__info.end));
}

} // namespace chrono

_LIBCPP_END_NAMESPACE_STD
2 changes: 2 additions & 0 deletions libcxx/test/libcxx/diagnostics/chrono.nodiscard.verify.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,10 @@ void test() {

{
std::chrono::sys_seconds s{};
std::chrono::local_seconds l{};
tz.name(); // expected-warning {{ignoring return value of function declared with 'nodiscard' attribute}}
tz.get_info(s); // expected-warning {{ignoring return value of function declared with 'nodiscard' attribute}}
tz.get_info(l); // expected-warning {{ignoring return value of function declared with 'nodiscard' attribute}}
operator==(tz, tz); // expected-warning {{ignoring return value of function declared with 'nodiscard' attribute}}
operator<=>(tz, tz); // expected-warning {{ignoring return value of function declared with 'nodiscard' attribute}}
}
Expand Down
Loading
Loading