diff options
author | Devtools Arcadia <arcadia-devtools@yandex-team.ru> | 2022-02-07 18:08:42 +0300 |
---|---|---|
committer | Devtools Arcadia <arcadia-devtools@mous.vla.yp-c.yandex.net> | 2022-02-07 18:08:42 +0300 |
commit | 1110808a9d39d4b808aef724c861a2e1a38d2a69 (patch) | |
tree | e26c9fed0de5d9873cce7e00bc214573dc2195b7 /library/cpp/timezone_conversion | |
download | ydb-1110808a9d39d4b808aef724c861a2e1a38d2a69.tar.gz |
intermediate changes
ref:cde9a383711a11544ce7e107a78147fb96cc4029
Diffstat (limited to 'library/cpp/timezone_conversion')
-rw-r--r-- | library/cpp/timezone_conversion/README.md | 27 | ||||
-rw-r--r-- | library/cpp/timezone_conversion/civil-inl.h | 63 | ||||
-rw-r--r-- | library/cpp/timezone_conversion/civil.cpp | 234 | ||||
-rw-r--r-- | library/cpp/timezone_conversion/civil.h | 338 | ||||
-rw-r--r-- | library/cpp/timezone_conversion/convert.cpp | 93 | ||||
-rw-r--r-- | library/cpp/timezone_conversion/convert.h | 60 | ||||
-rw-r--r-- | library/cpp/timezone_conversion/ut/civil_ut.cpp | 157 | ||||
-rw-r--r-- | library/cpp/timezone_conversion/ut/convert_ut.cpp | 204 | ||||
-rw-r--r-- | library/cpp/timezone_conversion/ut/ya.make | 15 | ||||
-rw-r--r-- | library/cpp/timezone_conversion/ya.make | 22 |
10 files changed, 1213 insertions, 0 deletions
diff --git a/library/cpp/timezone_conversion/README.md b/library/cpp/timezone_conversion/README.md new file mode 100644 index 00000000000..828f1880bc9 --- /dev/null +++ b/library/cpp/timezone_conversion/README.md @@ -0,0 +1,27 @@ +A library for translating between absolute times (i.e., `TInstant`) and civil times (i.e., +`NDatetime::TSimpleTM`) using the rules defined by a time zone (i.e., `NDatetime::TTimeZone`). + +(the terms `absolute` and `civil` come from [cctz#fundamental-concepts][cctz-fundamental-concepts]) + +This is basically a wrapper around [CCTZ][cctz] with one important change: the time zone database is +in Arcadia and is compiled with the library (which means your executable will end up ~2MB larger). + +See [contrib/libs/cctz/README][update] if you think zone database is outdated. + +Quick start: +============ +``` +#include <library/cpp/timezone_conversion/convert.h> + +// NDatetime::{GetLocalTimeZone(),GetUtcTimeZone()} are also available. +NDatetime::TTimeZone msk = NDatetime::GetTimeZone("Europe/Moscow"); +TInstant now = TInstant::Now(); +NDatetime::TSimpleTM civil = NDatetime::ToCivilTime(now, msk); +Cout << "Local time in Moscow is " << civil.ToString() << Endl; +TInstant absolute = NDatetime::ToAbsoluteTime(civil, msk); +Cout << "The current UNIX time is " << absolute.Seconds() << Endl; +``` + +[cctz-fundamental-concepts]: https://github.com/google/cctz#fundamental-concepts +[cctz]: https://github.com/google/cctz +[update]: https://a.yandex-team.ru/arc/trunk/arcadia/contrib/libs/cctz/tzdata/README?rev=2286180 diff --git a/library/cpp/timezone_conversion/civil-inl.h b/library/cpp/timezone_conversion/civil-inl.h new file mode 100644 index 00000000000..32afbccb642 --- /dev/null +++ b/library/cpp/timezone_conversion/civil-inl.h @@ -0,0 +1,63 @@ +#pragma once + +#include "civil.h" + +namespace NDatetime { + namespace NDetail { + template <typename T> + struct TGetCivilUnit; + + template <> + struct TGetCivilUnit<TCivilSecond> { + static constexpr ECivilUnit Value = ECivilUnit::Second; + }; + template <> + struct TGetCivilUnit<TCivilMinute> { + static constexpr ECivilUnit Value = ECivilUnit::Minute; + }; + template <> + struct TGetCivilUnit<TCivilHour> { + static constexpr ECivilUnit Value = ECivilUnit::Hour; + }; + template <> + struct TGetCivilUnit<TCivilDay> { + static constexpr ECivilUnit Value = ECivilUnit::Day; + }; + template <> + struct TGetCivilUnit<TCivilMonth> { + static constexpr ECivilUnit Value = ECivilUnit::Month; + }; + template <> + struct TGetCivilUnit<TCivilYear> { + static constexpr ECivilUnit Value = ECivilUnit::Year; + }; + + template <ECivilUnit Unit> + struct TGetCivilTime; + + template <> + struct TGetCivilTime<ECivilUnit::Second> { + using TResult = TCivilSecond; + }; + template <> + struct TGetCivilTime<ECivilUnit::Minute> { + using TResult = TCivilMinute; + }; + template <> + struct TGetCivilTime<ECivilUnit::Hour> { + using TResult = TCivilHour; + }; + template <> + struct TGetCivilTime<ECivilUnit::Day> { + using TResult = TCivilDay; + }; + template <> + struct TGetCivilTime<ECivilUnit::Month> { + using TResult = TCivilMonth; + }; + template <> + struct TGetCivilTime<ECivilUnit::Year> { + using TResult = TCivilYear; + }; + } +} diff --git a/library/cpp/timezone_conversion/civil.cpp b/library/cpp/timezone_conversion/civil.cpp new file mode 100644 index 00000000000..5986318b9a8 --- /dev/null +++ b/library/cpp/timezone_conversion/civil.cpp @@ -0,0 +1,234 @@ +#include "civil.h" + +#include <util/stream/output.h> +#include <util/stream/format.h> +#include <util/string/ascii.h> + +namespace { + bool TryParseInt(TStringBuf& s, int& dst, size_t maxDigits) { + int res = 0; + size_t i = 0; + while (i < maxDigits && !s.empty() && IsAsciiDigit(s[0])) { + res = res * 10 + (s[0] - '0'); + ++i; + s.Skip(1); + } + if (i == 0) { + return false; + } + dst = res; + return true; + } + + bool TryParseUTCOffsetTimezone(TStringBuf name, int& offset) { + static constexpr TStringBuf OFFSET_PREFIX = "UTC"; + if (!name.SkipPrefix(OFFSET_PREFIX)) { + return false; + } + if (name.empty()) { + return false; + } + bool negative; + if (name[0] == '+') { + negative = false; + } else if (name[0] == '-') { + negative = true; + } else { + return false; + } + name.Skip(1); + int hour; + int minute = 0; + if (!TryParseInt(name, hour, 2) || hour > 24) { + return false; + } + if (!name.empty()) { + if (name[0] == ':') { + name.Skip(1); + } + if (!TryParseInt(name, minute, 2) || minute >= 60) { + return false; + } + if (!name.empty()) { + return false; + } + } + if (hour == 24 && minute != 0) { + return false; + } + offset = (hour * 60 + minute) * 60; + if (negative) + offset = -offset; + return true; + } +} // anonymous namespace + +namespace NDatetime { + TTimeZone GetTimeZone(TStringBuf name) { + int offset; + if (TryParseUTCOffsetTimezone(name, offset)) { + return GetFixedTimeZone(offset); + } + TTimeZone result; + if (!cctz::load_time_zone(static_cast<std::string>(name), &result)) { + ythrow TInvalidTimezone() << "Failed to load time zone " << name << ", " << result.name(); + } + return result; + } + + TTimeZone GetFixedTimeZone(const long offset) { + return cctz::fixed_time_zone(std::chrono::seconds(offset)); + } + + TCivilSecond Convert(const TInstant& absTime, const TTimeZone& tz) { + return cctz::convert(TSystemClock::from_time_t(absTime.TimeT()), tz); + } + + TCivilSecond Convert(const TInstant& absTime, TStringBuf tzName) { + TTimeZone tz = GetTimeZone(tzName); + return cctz::convert(TSystemClock::from_time_t(absTime.TimeT()), tz); + } + + TInstant Convert(const TCivilSecond& tp, const TTimeZone& tz) { + return TInstant::Seconds(cctz::convert(tp, tz).time_since_epoch().count()); + } + + TCivilSecond AddYears(const TCivilSecond& tp, TDiff diff) { + TCivilYear newYear = Calc<TCivilYear>(tp, diff); + return NDatetime::TCivilSecond(newYear.year(), tp.month(), tp.day(), tp.hour(), tp.minute(), tp.second()); + } + + TCivilSecond AddMonths(const TCivilSecond& tp, TDiff diff) { + TCivilMonth newMonth = Calc<TCivilMonth>(tp, diff); + return NDatetime::TCivilSecond(newMonth.year(), newMonth.month(), tp.day(), tp.hour(), tp.minute(), tp.second()); + } + + TCivilSecond AddDays(const TCivilSecond& tp, TDiff diff) { + TCivilDay newDay = Calc<TCivilDay>(tp, diff); + return NDatetime::TCivilSecond(newDay.year(), newDay.month(), newDay.day(), tp.hour(), tp.minute(), tp.second()); + } + + TCivilSecond AddHours(const TCivilSecond& tp, TDiff diff) { + TCivilHour newHour = Calc<TCivilHour>(tp, diff); + return NDatetime::TCivilSecond(newHour.year(), newHour.month(), newHour.day(), newHour.hour(), tp.minute(), tp.second()); + } + + TCivilSecond AddMinutes(const TCivilSecond& tp, TDiff diff) { + TCivilMinute newMinute = Calc<TCivilMinute>(tp, diff); + return NDatetime::TCivilSecond(newMinute.year(), newMinute.month(), newMinute.day(), newMinute.hour(), newMinute.minute(), tp.second()); + } + + TCivilSecond AddSeconds(const TCivilSecond& tp, TDiff diff) { + return Calc<TCivilSecond>(tp, diff); + } + + TCivilSecond AddCivil(const TCivilSecond& tp, TCivilDiff diff) { + switch (diff.Unit) { + case ECivilUnit::Second: { + return AddSeconds(tp, diff.Value); + } + case ECivilUnit::Minute: { + return AddMinutes(tp, diff.Value); + } + case ECivilUnit::Hour: { + return AddHours(tp, diff.Value); + } + case ECivilUnit::Day: { + return AddDays(tp, diff.Value); + } + case ECivilUnit::Month: { + return AddMonths(tp, diff.Value); + } + case ECivilUnit::Year: { + return AddYears(tp, diff.Value); + } + default: { + ythrow yexception() << "Unexpected civil unit value " << static_cast<int>(diff.Unit); + } + } + } + + TCivilDiff GetCivilDiff(const TCivilSecond& tpX, const TCivilSecond& tpY, ECivilUnit unit) { + switch (unit) { + case ECivilUnit::Second: { + return {tpX - tpY, unit}; + } + case ECivilUnit::Minute: { + return {static_cast<TCivilMinute>(tpX) - static_cast<TCivilMinute>(tpY), unit}; + } + case ECivilUnit::Hour: { + return {static_cast<TCivilHour>(tpX) - static_cast<TCivilHour>(tpY), unit}; + } + case ECivilUnit::Day: { + return {static_cast<TCivilDay>(tpX) - static_cast<TCivilDay>(tpY), unit}; + } + case ECivilUnit::Month: { + return {static_cast<TCivilMonth>(tpX) - static_cast<TCivilMonth>(tpY), unit}; + } + case ECivilUnit::Year: { + return {static_cast<TCivilYear>(tpX) - static_cast<TCivilYear>(tpY), unit}; + } + default: { + ythrow yexception() << "Unexpected civil unit value " << static_cast<int>(unit); + } + } + } +} + +template <> +void Out<NDatetime::TCivilYear>(IOutputStream& out, const NDatetime::TCivilYear& y) { + out << y.year(); +} + +template <> +void Out<NDatetime::TCivilMonth>(IOutputStream& out, const NDatetime::TCivilMonth& m) { + out << NDatetime::TCivilYear(m) << '-' << LeftPad(m.month(), 2, '0'); +} + +template <> +void Out<NDatetime::TCivilDay>(IOutputStream& out, const NDatetime::TCivilDay& d) { + out << NDatetime::TCivilMonth(d) << '-' << LeftPad(d.day(), 2, '0'); +} + +template <> +void Out<NDatetime::TCivilHour>(IOutputStream& out, const NDatetime::TCivilHour& h) { + out << NDatetime::TCivilDay(h) << 'T' << LeftPad(h.hour(), 2, '0'); +} + +template <> +void Out<NDatetime::TCivilMinute>(IOutputStream& out, const NDatetime::TCivilMinute& m) { + out << NDatetime::TCivilHour(m) << ':' << LeftPad(m.minute(), 2, '0'); +} + +template <> +void Out<NDatetime::TCivilSecond>(IOutputStream& out, const NDatetime::TCivilSecond& s) { + out << NDatetime::TCivilMinute(s) << ':' << LeftPad(s.second(), 2, '0'); +} + +template <> +void Out<NDatetime::TWeekday>(IOutputStream& out, NDatetime::TWeekday wd) { + using namespace cctz; + switch (wd) { + case weekday::monday: + out << TStringBuf("Monday"); + break; + case weekday::tuesday: + out << TStringBuf("Tuesday"); + break; + case weekday::wednesday: + out << TStringBuf("Wednesday"); + break; + case weekday::thursday: + out << TStringBuf("Thursday"); + break; + case weekday::friday: + out << TStringBuf("Friday"); + break; + case weekday::saturday: + out << TStringBuf("Saturday"); + break; + case weekday::sunday: + out << TStringBuf("Sunday"); + break; + } +} diff --git a/library/cpp/timezone_conversion/civil.h b/library/cpp/timezone_conversion/civil.h new file mode 100644 index 00000000000..0e95b807ed3 --- /dev/null +++ b/library/cpp/timezone_conversion/civil.h @@ -0,0 +1,338 @@ +#pragma once + +#include <util/datetime/base.h> + +#include <contrib/libs/cctz/include/cctz/civil_time.h> +#include <contrib/libs/cctz/include/cctz/time_zone.h> + +#include <util/generic/strbuf.h> +#include <util/generic/string.h> +#include <util/generic/yexception.h> + +#if __clang__ && __cpp_constexpr >= 201304 +#define CONSTEXPR_M constexpr +#else +#define CONSTEXPR_M inline +#endif + +namespace NDatetime { + /** Exception class which throws when time zone is not valid + */ + class TInvalidTimezone: public yexception { + }; + + using TSystemClock = std::chrono::system_clock; + using TTimePoint = std::chrono::time_point<TSystemClock>; + + /* + * An opaque class representing past, present, and future rules of + * mapping between absolute and civil times in a given region. + * It is very lightweight and may be passed by value. + */ + using TTimeZone = cctz::time_zone; + + using TCivilYear = cctz::civil_year; + using TCivilMonth = cctz::civil_month; + using TCivilDay = cctz::civil_day; + using TCivilHour = cctz::civil_hour; + using TCivilMinute = cctz::civil_minute; + using TCivilSecond = cctz::civil_second; + using TWeekday = cctz::weekday; + using TDiff = cctz::diff_t; + + using TYear = cctz::year_t; + using TMonth = cctz::detail::month_t; + + enum class ECivilUnit : int { + Second = 0, + Minute = 1, + Hour = 2, + Day = 3, + Month = 4, + Year = 5 + }; + + namespace NDetail { + template <typename T> + struct TGetCivilUnit; + + template <ECivilUnit Unit> + struct TGetCivilTime; + } + + template <typename T> + CONSTEXPR_M ECivilUnit GetCivilUnit(const T& = {}) { + return NDetail::TGetCivilUnit<T>::Value; + } + + template <ECivilUnit Unit> + using TCivilTime = typename NDetail::TGetCivilTime<Unit>::TResult; + + /** + * Class with variable unit diff. + */ + struct TCivilDiff { + TDiff Value = 0; + ECivilUnit Unit = ECivilUnit::Second; + + TCivilDiff() = default; + TCivilDiff(TDiff value, ECivilUnit unit) + : Value(value) + , Unit(unit) + { + } + + /** + * Straightfoward implementation of operators <, == and unit conversions + * can be potentially misleading (e.g. 1 month == 30 days?); + * we leave it to user to implement it properly for each application. + */ + }; + + /** + * Gets the time zone by an IANA name. + * @param name A name in IANA format (e.g., "Europe/Moscow"). + * @note After you request a time zone for the first time, it is cached + * in a (thread-safe) cache, so subsequent requests to the + * same time zone should be extremely fast. + * @throw TInvalidTimezone if the name is invalid. + * @see http://www.iana.org/time-zones + */ + TTimeZone GetTimeZone(TStringBuf name); + + /** + * Returns a time zone that is a fixed offset (seconds east) from UTC. + * Note: If the absolute value of the offset is greater than 24 hours + * you'll get UTC (i.e., zero offset) instead. + */ + TTimeZone GetFixedTimeZone(const long offset); + + /** Convert civil time from one timezone to another + * @param[in] src is source time with 'from' timezone + * @param[in] from is a initial timezone + * @param[in] from is a destination timezone + * @return a civil time + */ + template <typename T> + T Convert(const T& src, const TTimeZone& from, const TTimeZone& to) { + return cctz::convert(cctz::convert(src, from), to); + } + + /** Convert absolute time to civil time by rules from timezone. + * @param[in] absTime is an absolute time which is used to convert + * @param[in] tz is a loaded timezone + * @return a civil time + * + * Note: This function doesn't work properly for dates before 1 Jan 1970! + */ + TCivilSecond Convert(const TInstant& absTime, const TTimeZone& tz); + + /** Convert absolute time to civil time by rules from timezone which will be loaded. + * @throw InvalidTimezone if the name is invalid. + * @param[in] absTime is an absolute time which is used to convert + * @param[in] tzName is a timezone name which will be loaded + * @return a civil time + * + * Note: This function doesn't work properly for dates before 1 Jan 1970! + */ + TCivilSecond Convert(const TInstant& absTime, TStringBuf tzName); + + /** Convert a civil time to absolute by using rules from timezone + * + * Note: This function doesn't work properly for dates before 1 Jan 1970! + */ + TInstant Convert(const TCivilSecond& s, const TTimeZone& tz); + + /** Just to simply calculations between dates/times. + * NDatetime::Calc<TCivilDay>(TCivilSecond(2001, 1, 1, 10, 10, 10), 5); // returns TCivilDay(2001, 1, 6); + * @param[in] tp is a timepoint with which calc will be + * @param[in] diff is quantity of which will be added (of type T) to the tp + * @return the calculated T type + */ + template <typename T, typename S> + inline T Calc(const S& tp, TDiff diff) { + return T(tp) + diff; + } + + /** Non-template methods for adding dates/times. + * @param[in] tp is a timepoint with which calc will be + * @param[in] diff is quantity of which will be added to the tp + * @return the calculated TCivilSecond object + */ + TCivilSecond AddYears(const TCivilSecond& tp, TDiff diff); + TCivilSecond AddMonths(const TCivilSecond& tp, TDiff diff); + TCivilSecond AddDays(const TCivilSecond& tp, TDiff diff); + TCivilSecond AddHours(const TCivilSecond& tp, TDiff diff); + TCivilSecond AddMinutes(const TCivilSecond& tp, TDiff diff); + TCivilSecond AddSeconds(const TCivilSecond& tp, TDiff diff); + + /** Method to add TCivilDiff + * @param[in] tp is a timepoint with which calc will be + * @param[in] diff is quantity of which will be added to the tp + * @return the calculated TCivilSecond object + */ + TCivilSecond AddCivil(const TCivilSecond& tp, TCivilDiff diff); + + /** Method to subtract to civil dates/times and get TCivilDiff. + * First casts to unit, then subtracts; + * e.g. GetCivilDiff(2017-10-01, 2017-09-30, Month) = 1. + * + * @param[in] tpX is a timepoint + * @param[in] tpY is a timepoint to subtract from tpX + * @param[in] unit is a civil time unit to use in subtraction + * @return the calculated diff as TCivilDiff object + */ + TCivilDiff GetCivilDiff(const TCivilSecond& tpX, const TCivilSecond& tpY, ECivilUnit unit); + + /** Formats the given TimePoint in the given TTimeZone according to + * the provided format string. Uses strftime()-like formatting options, + * with the following extensions: + * + * - %Ez - RFC3339-compatible numeric time zone (+hh:mm or -hh:mm) + * - %E#S - Seconds with # digits of fractional precision + * - %E*S - Seconds with full fractional precision (a literal '*') + * - %E#f - Fractional seconds with # digits of precision + * - %E*f - Fractional seconds with full precision (a literal '*') + * - %E4Y - Four-character years (-999 ... -001, 0000, 0001 ... 9999) + * + * Note that %E0S behaves like %S, and %E0f produces no characters. In + * contrast %E*f always produces at least one digit, which may be '0'. + * + * Note that %Y produces as many characters as it takes to fully render the + * year. A year outside of [-999:9999] when formatted with %E4Y will produce + * more than four characters, just like %Y. + * + * Tip: Format strings should include the UTC offset (e.g., %z or %Ez) so that + * the resultng string uniquely identifies an absolute time. + * + * Example: + * NDatetime::TTimeZone lax = NDatetime::GetTimeZone("America/Los_Angeles"); + * NDatetime::TCivilSecond tp(2013, 1, 2, 3, 4, 5); + * TString f = NDatetime::Format("%H:%M:%S", tp, lax); // "03:04:05" + * TString f = NDatetime::Format("%H:%M:%E3S", tp, lax); //"03:04:05.000" + */ + template <typename TP> + TString Format(TStringBuf fmt, const TP& tp, const TTimeZone& tz) { + return TString(cctz::format(static_cast<std::string>(fmt), TTimePoint(cctz::convert(tp, tz)), tz)); + } + + /** Returns the weekday by day. + * @param[in] day is a given day + * @return a weekday (enum) + */ + CONSTEXPR_M TWeekday GetWeekday(const TCivilDay& day) noexcept { + return cctz::get_weekday(day); + } + + /** Returns the TCivilDay that strictly follows or precedes the given + * civil_day, and that falls on the given weekday. + * @code + For example, given: + + August 2015 + Su Mo Tu We Th Fr Sa + 1 + 2 3 4 5 6 7 8 + 9 10 11 12 13 14 15 + 16 17 18 19 20 21 22 + 23 24 25 26 27 28 29 + 30 31 + + TCivilDay a(2015, 8, 13); // GetWeekday(a) == TWeekday::thursday + TCivilDay b = NextWeekday(a, TWeekday::thursday); // b = 2015-08-20 + TCivilDay c = PrevWeekday(a, TWeekday::thursday); // c = 2015-08-06 + TCivilDay d = NearestWeekday(a, TWeekday::thursday); // d = 2015-08-13 + + TCivilDay e = ... + // Gets the following Thursday if e is not already Thursday + TCivilDay thurs1 = PrevWeekday(e, TWeekday::thursday) + 7; + // Gets the previous Thursday if e is not already Thursday + TCivilDay thurs2 = NextWeekday(e, TWeekday::thursday) - 7; + * @endcode + * @see PrevWeekday() + * @see NearestWeekday() + * @param[in] cd is a current day + * @param[in] wd is a weekday which wanetd for find on next week + * @return a civil day on weekday next week + */ + CONSTEXPR_M TCivilDay NextWeekday(const TCivilDay& cd, TWeekday wd) noexcept { + return cctz::next_weekday(cd, wd); + } + + /** Returns prev weekday. See the description of NextWeekday(). + * @see NextWeekday() + * @see NearestWeekday() + * @param[in] cd is a current day + * @param[in] wd is a weekday which is looking for (backward) + * @return a first occurence of the given weekday (backward) + */ + CONSTEXPR_M TCivilDay PrevWeekday(const TCivilDay& cd, TWeekday wd) noexcept { + return cctz::prev_weekday(cd, wd); + } + + /** Find a nearest occurence of the given weekday forward (could be current day). + * @see NextWeekday() + * @param[in] cd is a current day + * @param[in] wd is a weekday which is looking for (current day or later) + * @return first occurence (including current day) of the given weekday + */ + CONSTEXPR_M TCivilDay NearestWeekday(const TCivilDay& cd, TWeekday wd) noexcept { + return get_weekday(cd) != wd ? next_weekday(cd, wd) : cd; + } + + /** Find the date of the given weekday within the given week. + * @param[in] cd is a current day + * @param[in] wd is a requested week day + * @return day within a week of a given day + */ + CONSTEXPR_M TCivilDay WeekdayOnTheWeek(const TCivilDay& cd, TWeekday wd) noexcept { + const auto d = get_weekday(cd); + if (d == wd) + return cd; + + return d < wd ? NextWeekday(cd, wd) : PrevWeekday(cd, wd); + } + + /** Returns an absolute day of year by given day. + */ + CONSTEXPR_M int GetYearDay(const TCivilDay& cd) noexcept { + return cctz::get_yearday(cd); + } + + CONSTEXPR_M int DaysPerMonth(TYear year, TMonth month) noexcept { + return cctz::detail::impl::days_per_month(year, month); + } + + CONSTEXPR_M int DaysPerYear(TYear year, TMonth month) noexcept { + return cctz::detail::impl::days_per_year(year, month); + } + + /** Calculate week number for the given date + * @param[in] cd is a current day + * @param[in] usePreviousYear (optional) true if calculate week number from previous year + * + * The week number starts from 1 for the first week, where Thursday exist (see ISO8601), i.e. + * Jan 2021 + * week# mo tu we th fr sa su + * 53 1 2 3 + * 01 4 5 6 7 8 9 10 + * 02 11 ... + * Jan 2020 + * week# mo tu we th fr sa su + * 01 1 2 3 4 5 + * 02 6 7 8 9 10 11... + * + * In case if you received zero value, you may call function again with usePreviousYear=true + * Also you may use usePreviousYear to calculate week difference between two dates in different year + */ + CONSTEXPR_M int GetYearWeek(const TCivilDay& cd, bool usePreviousYear = false) noexcept { + const auto jan1 = NDatetime::GetWeekday(NDatetime::TCivilDay{cd.year() - (usePreviousYear ? 1 : 0), 1, 1}); + const auto daysCount = GetYearDay(cd) + (usePreviousYear ? DaysPerYear(cd.year()-1, 1) : 0); + + return (daysCount + static_cast<int>(jan1) - 1) / 7 + (jan1 == cctz::weekday::monday || jan1 == cctz::weekday::tuesday || jan1 == cctz::weekday::wednesday); + } +} + +#include "civil-inl.h" + +#undef CONSTEXPR_M diff --git a/library/cpp/timezone_conversion/convert.cpp b/library/cpp/timezone_conversion/convert.cpp new file mode 100644 index 00000000000..490bb4e2700 --- /dev/null +++ b/library/cpp/timezone_conversion/convert.cpp @@ -0,0 +1,93 @@ +#include "convert.h" + +#include <contrib/libs/cctz/include/cctz/civil_time.h> + +#include <util/generic/yexception.h> + +#include <chrono> + +namespace NDatetime { + static constexpr i64 TM_YEAR_OFFSET = 1900; + using TSystemClock = std::chrono::system_clock; + using TTimePoint = std::chrono::time_point<TSystemClock>; + + static TSimpleTM CivilToTM(const cctz::civil_second& cs, const cctz::time_zone::absolute_lookup& al) { + cctz::civil_day cd(cs); + TSimpleTM tm; + tm.GMTOff = al.offset; + tm.Year = cs.year() - TM_YEAR_OFFSET; + tm.YDay = cctz::get_yearday(cd); + tm.Mon = cs.month() - 1; + tm.MDay = cs.day(); + tm.Hour = cs.hour(); + tm.Min = cs.minute(); + tm.Sec = cs.second(); + tm.IsDst = al.is_dst; + tm.IsLeap = LeapYearAD(cs.year()); + + switch (cctz::get_weekday(cd)) { + case cctz::weekday::monday: + tm.WDay = 1; + break; + case cctz::weekday::tuesday: + tm.WDay = 2; + break; + case cctz::weekday::wednesday: + tm.WDay = 3; + break; + case cctz::weekday::thursday: + tm.WDay = 4; + break; + case cctz::weekday::friday: + tm.WDay = 5; + break; + case cctz::weekday::saturday: + tm.WDay = 6; + break; + case cctz::weekday::sunday: + tm.WDay = 0; + break; + } + + return tm; + } + + /* + TTimeZone GetTimeZone(const TString& name) { + TTimeZone result; + if (!cctz::load_time_zone(name, &result)) { + ythrow yexception() << "Failed to load time zone " << name << ", " << result.name(); + } + return result; + } + */ + + TTimeZone GetUtcTimeZone() { + return cctz::utc_time_zone(); + } + + TTimeZone GetLocalTimeZone() { + return cctz::local_time_zone(); + } + + TSimpleTM ToCivilTime(const TInstant& absoluteTime, const TTimeZone& tz) { + TTimePoint tp = TSystemClock::from_time_t(absoluteTime.TimeT()); + return CivilToTM(cctz::convert(tp, tz), tz.lookup(tp)); + } + + TSimpleTM CreateCivilTime(const TTimeZone& tz, ui32 year, ui32 mon, ui32 day, ui32 h, ui32 m, ui32 s) { + cctz::civil_second cs(year, mon, day, h, m, s); + return CivilToTM(cs, tz.lookup(tz.lookup(cs).pre)); + } + + TInstant ToAbsoluteTime(const TSimpleTM& civilTime, const TTimeZone& tz) { + cctz::civil_second cs( + civilTime.Year + TM_YEAR_OFFSET, + civilTime.Mon + 1, + civilTime.MDay, + civilTime.Hour, + civilTime.Min, + civilTime.Sec); + return TInstant::Seconds(TSystemClock::to_time_t(tz.lookup(cs).pre)); + } +} diff --git a/library/cpp/timezone_conversion/convert.h b/library/cpp/timezone_conversion/convert.h new file mode 100644 index 00000000000..768a9e110f2 --- /dev/null +++ b/library/cpp/timezone_conversion/convert.h @@ -0,0 +1,60 @@ +#pragma once + +#include "civil.h" + +#include <contrib/libs/cctz/include/cctz/time_zone.h> +#include <util/datetime/base.h> +#include <util/draft/datetime.h> + +namespace NDatetime { + /** + * @return The mother of all time zones. + * @see https://en.wikipedia.org/wiki/Coordinated_Universal_Time + */ + TTimeZone GetUtcTimeZone(); + + /** + * @return The time zone that is curently set on your machine. + */ + TTimeZone GetLocalTimeZone(); + + /** + * @param absoluteTime A TInstant representing a number of seconds elapsed + * since The Epoch (the microsecond part is ignored). + * @param tz The time zone to use for conversion. + * @return The civil time corresponding to absoluteTime. + * @note This conversion is always well-defined (i.e., there + * is exactly one civil time which corresponds to + * absoluteTime). + * @see https://en.wikipedia.org/wiki/Unix_time + */ + TSimpleTM ToCivilTime(const TInstant& absoluteTime, const TTimeZone& tz); + + /** + * Creates civil time in place with respect of given timezone. + * @param[in] tz The time zone to use for creation. + * @param[in] year The year of the creation time. + * @param[in] mon The month of the creation time. + * @param[in] day The day of the creation time. + * @param[in] h The hour of the creation time. + * @param[in] m The minute of the creation time. + * @param[in] s The second of the creation time. + * @return a civil time + */ + TSimpleTM CreateCivilTime(const TTimeZone& tz, ui32 year, ui32 mon, ui32 day, ui32 h = 0, ui32 m = 0, ui32 s = 0); + + /** + * @param civilTime A human-readable date and time (the following fields + * are used by this function: {Year,Mon,MDay,Hour,Min,Sec}). + * @param tz The time zone to use for conversion. + * @return Some absolute time corresponding to civilTime. + * @note If multiple absolute times match civilTime, the earliest + * if returned. + * If civilTime doesn't exist due to discontinuity in time + * (e.g., DST happened) we pretend the discontinuity isn't + * there (i.e., if we skipped from 1:59AM to 3:00AM then + * ToAbsoluteTime(2:30AM) == ToAbsoluteTime(3:30AM)). + * @see https://en.wikipedia.org/wiki/Daylight_saving_time + */ + TInstant ToAbsoluteTime(const TSimpleTM& civilTime, const TTimeZone& tz); +} diff --git a/library/cpp/timezone_conversion/ut/civil_ut.cpp b/library/cpp/timezone_conversion/ut/civil_ut.cpp new file mode 100644 index 00000000000..a21bd4bd7df --- /dev/null +++ b/library/cpp/timezone_conversion/ut/civil_ut.cpp @@ -0,0 +1,157 @@ +#include <library/cpp/timezone_conversion/civil.h> +#include <library/cpp/testing/unittest/registar.h> +#include <library/cpp/testing/unittest/tests_data.h> + +#include <util/stream/str.h> + +namespace NDatetime { + inline bool operator==(const NDatetime::TCivilDiff& x, const NDatetime::TCivilDiff& y) { + return x.Unit == y.Unit && x.Value == y.Value; + } +} + +template <> +inline void Out<NDatetime::TCivilDiff>(IOutputStream& out, const NDatetime::TCivilDiff& diff) { + out << "(" << diff.Value << "," << diff.Unit << ")"; +} + +Y_UNIT_TEST_SUITE(DateTime) { + Y_UNIT_TEST(Calc) { + NDatetime::TCivilSecond s(2017, 2, 1, 10, 12, 9); + UNIT_ASSERT_VALUES_EQUAL(NDatetime::Calc<NDatetime::TCivilDay>(s, 2), NDatetime::TCivilDay(2017, 2, 3)); + UNIT_ASSERT_VALUES_EQUAL(NDatetime::Calc<NDatetime::TCivilDay>(s, -2), NDatetime::TCivilDay(2017, 1, 30)); + } + Y_UNIT_TEST(Adds) { + NDatetime::TCivilSecond s(2017, 2, 1, 10, 12, 9); + UNIT_ASSERT_VALUES_EQUAL(NDatetime::AddDays(s, 2), NDatetime::TCivilSecond(2017, 2, 3, 10, 12, 9)); + UNIT_ASSERT_VALUES_EQUAL(NDatetime::AddMonths(s, -2), NDatetime::TCivilSecond(2016, 12, 1, 10, 12, 9)); + UNIT_ASSERT_VALUES_EQUAL(NDatetime::AddYears(s, -55), NDatetime::TCivilSecond(1962, 2, 1, 10, 12, 9)); + UNIT_ASSERT_VALUES_EQUAL(NDatetime::AddHours(s, 40), NDatetime::TCivilSecond(2017, 2, 3, 2, 12, 9)); + } + Y_UNIT_TEST(Convert) { + TInstant absTime = TInstant::Seconds(1500299239); + NDatetime::TTimeZone lax = NDatetime::GetTimeZone("America/Los_Angeles"); + NDatetime::TCivilSecond dt1 = NDatetime::Convert(absTime, lax); + NDatetime::TCivilSecond dt2(2017, 7, 17, 6, 47, 19); + UNIT_ASSERT_VALUES_EQUAL(dt1, dt2); + UNIT_ASSERT_VALUES_EQUAL(absTime, NDatetime::Convert(dt2, lax)); + UNIT_ASSERT_EXCEPTION(NDatetime::Convert(absTime, "Unknown time zone"), NDatetime::TInvalidTimezone); + } + Y_UNIT_TEST(UTCOffsetTimezone) { + NDatetime::TTimeZone lax = NDatetime::GetTimeZone("UTC+12"); + auto lookup = lax.lookup(std::chrono::system_clock::from_time_t(0)); + UNIT_ASSERT_VALUES_EQUAL(12 * 60 * 60, lookup.offset); + lax = NDatetime::GetTimeZone("UTC-10"); + lookup = lax.lookup(std::chrono::system_clock::from_time_t(0)); + UNIT_ASSERT_VALUES_EQUAL(-10 * 60 * 60, lookup.offset); + lax = NDatetime::GetTimeZone("UTC"); + lookup = lax.lookup(std::chrono::system_clock::from_time_t(0)); + UNIT_ASSERT_VALUES_EQUAL(0, lookup.offset); + lax = NDatetime::GetTimeZone("UTC+0"); + lookup = lax.lookup(std::chrono::system_clock::from_time_t(0)); + UNIT_ASSERT_VALUES_EQUAL(0, lookup.offset); + lax = NDatetime::GetTimeZone("UTC-2"); + lookup = lax.lookup(std::chrono::system_clock::from_time_t(0)); + UNIT_ASSERT_VALUES_EQUAL(-2 * 60 * 60, lookup.offset); + lax = NDatetime::GetTimeZone("UTC-00:30"); + lookup = lax.lookup(std::chrono::system_clock::from_time_t(0)); + UNIT_ASSERT_VALUES_EQUAL(-30 * 60, lookup.offset); + lax = NDatetime::GetTimeZone("UTC-0241"); + lookup = lax.lookup(std::chrono::system_clock::from_time_t(0)); + UNIT_ASSERT_VALUES_EQUAL(-(2 * 60 + 41) * 60, lookup.offset); + UNIT_ASSERT_EXCEPTION(NDatetime::GetTimeZone("UTCUnknown"), NDatetime::TInvalidTimezone); + UNIT_ASSERT_EXCEPTION(NDatetime::GetTimeZone("UTC+:"), NDatetime::TInvalidTimezone); + UNIT_ASSERT_EXCEPTION(NDatetime::GetTimeZone("UTC+24:01"), NDatetime::TInvalidTimezone); + UNIT_ASSERT_EXCEPTION(NDatetime::GetTimeZone("UTC+20:"), NDatetime::TInvalidTimezone); + UNIT_ASSERT_EXCEPTION(NDatetime::GetTimeZone("UTC+20:60"), NDatetime::TInvalidTimezone); + UNIT_ASSERT_EXCEPTION(NDatetime::GetTimeZone("UTC+20:30:"), NDatetime::TInvalidTimezone); + } + Y_UNIT_TEST(Format) { + NDatetime::TTimeZone lax = NDatetime::GetTimeZone("America/Los_Angeles"); + NDatetime::TCivilSecond tp(2013, 1, 2, 3, 4, 5); + UNIT_ASSERT_VALUES_EQUAL(NDatetime::Format("%H:%M:%S", tp, lax), "03:04:05"); + UNIT_ASSERT_VALUES_EQUAL(NDatetime::Format("%H:%M:%E3S", tp, lax), "03:04:05.000"); + } + Y_UNIT_TEST(Weekday) { + NDatetime::TCivilDay d(2013, 1, 2); + NDatetime::TWeekday wd = NDatetime::GetWeekday(d); + UNIT_ASSERT_VALUES_EQUAL(wd, NDatetime::TWeekday::wednesday); + UNIT_ASSERT_VALUES_EQUAL(NDatetime::NextWeekday(d, NDatetime::TWeekday::monday), NDatetime::TCivilDay(2013, 1, 7)); + UNIT_ASSERT_VALUES_EQUAL(NDatetime::PrevWeekday(d, NDatetime::TWeekday::monday), NDatetime::TCivilDay(2012, 12, 31)); + UNIT_ASSERT_VALUES_EQUAL(NDatetime::WeekdayOnTheWeek(d, NDatetime::TWeekday::monday), NDatetime::TCivilDay(2012, 12, 31)); + UNIT_ASSERT_VALUES_EQUAL(NDatetime::WeekdayOnTheWeek(d, NDatetime::TWeekday::wednesday), NDatetime::TCivilDay(2013, 1, 2)); + UNIT_ASSERT_VALUES_EQUAL(NDatetime::WeekdayOnTheWeek(d, NDatetime::TWeekday::friday), NDatetime::TCivilDay(2013, 1, 4)); + } + Y_UNIT_TEST(CivilUnit) { + using namespace NDatetime; + + UNIT_ASSERT_VALUES_EQUAL(GetCivilUnit<TCivilMonth>(), ECivilUnit::Month); + UNIT_ASSERT_VALUES_EQUAL(GetCivilUnit(TCivilHour{}), ECivilUnit::Hour); + + UNIT_ASSERT_VALUES_EQUAL(TCivilTime<ECivilUnit::Day>(2017, 1, 11), TCivilDay(2017, 1, 11)); + + NDatetime::TCivilSecond s(2017, 2, 1, 10, 12, 9); + + UNIT_ASSERT_VALUES_EQUAL( + NDatetime::AddCivil(s, TCivilDiff{2, ECivilUnit::Day}), + NDatetime::AddDays(s, 2)); + UNIT_ASSERT_VALUES_EQUAL( + NDatetime::AddCivil(s, TCivilDiff{-2, ECivilUnit::Month}), + NDatetime::AddMonths(s, -2)); + UNIT_ASSERT_VALUES_EQUAL( + NDatetime::AddCivil(s, TCivilDiff{-55, ECivilUnit::Year}), + NDatetime::AddYears(s, -55)); + UNIT_ASSERT_VALUES_EQUAL( + NDatetime::AddCivil(s, TCivilDiff{40, ECivilUnit::Hour}), + NDatetime::AddHours(s, 40)); + + UNIT_ASSERT_VALUES_EQUAL( + GetCivilDiff(TCivilSecond(2017, 10), TCivilSecond(2017, 7), ECivilUnit::Month), + TCivilDiff(3, ECivilUnit::Month)); + + UNIT_ASSERT_VALUES_EQUAL( + GetCivilDiff(TCivilSecond(2017, 10, 1), TCivilSecond(2017, 9, 30), ECivilUnit::Month), + TCivilDiff(1, ECivilUnit::Month)); + + UNIT_ASSERT_VALUES_EQUAL( + GetCivilDiff(TCivilSecond(2017, 10, 1), TCivilSecond(2017, 9, 31), ECivilUnit::Month), + TCivilDiff(0, ECivilUnit::Month)); + } + + Y_UNIT_TEST(TestYearWeekNmb) { + + // YEAR 2021 - start from Friday, first dates (1-3) will have week# 0 + UNIT_ASSERT_VALUES_EQUAL(NDatetime::GetYearWeek(NDatetime::TCivilDay{2021, 1, 1}), 0); + UNIT_ASSERT_VALUES_EQUAL(NDatetime::GetYearWeek(NDatetime::TCivilDay{2021, 1, 2}), 0); + UNIT_ASSERT_VALUES_EQUAL(NDatetime::GetYearWeek(NDatetime::TCivilDay{2021, 1, 3}), 0); + UNIT_ASSERT_VALUES_EQUAL(NDatetime::GetYearWeek(NDatetime::TCivilDay{2021, 1, 4}), 1); + + UNIT_ASSERT_VALUES_EQUAL(NDatetime::GetYearWeek(NDatetime::TCivilDay{2021, 1, 1}, true), 53); + UNIT_ASSERT_VALUES_EQUAL(NDatetime::GetYearWeek(NDatetime::TCivilDay{2021, 1, 2}, true), 53); + UNIT_ASSERT_VALUES_EQUAL(NDatetime::GetYearWeek(NDatetime::TCivilDay{2021, 1, 3}, true), 53); + UNIT_ASSERT_VALUES_EQUAL(NDatetime::GetYearWeek(NDatetime::TCivilDay{2021, 1, 4}, true), 54); + + UNIT_ASSERT_VALUES_EQUAL(NDatetime::GetYearWeek(NDatetime::TCivilDay{2021, 2, 28}), 8); + UNIT_ASSERT_VALUES_EQUAL(NDatetime::GetYearWeek(NDatetime::TCivilDay{2021, 2, 29}), 9); // <- this is invalid date, should be normalized to March 1 + UNIT_ASSERT_VALUES_EQUAL(NDatetime::GetYearWeek(NDatetime::TCivilDay{2021, 3, 1}), 9); + + UNIT_ASSERT_VALUES_EQUAL(NDatetime::GetYearWeek(NDatetime::TCivilDay{2021, 12, 26}), 51); + UNIT_ASSERT_VALUES_EQUAL(NDatetime::GetYearWeek(NDatetime::TCivilDay{2021, 12, 27}), 52); + UNIT_ASSERT_VALUES_EQUAL(NDatetime::GetYearWeek(NDatetime::TCivilDay{2021, 12, 31}), 52); + + // YEAR 2020 - start from Wednesday, all dates start from week# 1 + UNIT_ASSERT_VALUES_EQUAL(NDatetime::GetYearWeek(NDatetime::TCivilDay{2020, 1, 1}), 1); + UNIT_ASSERT_VALUES_EQUAL(NDatetime::GetYearWeek(NDatetime::TCivilDay{2020, 1, 5}), 1); + UNIT_ASSERT_VALUES_EQUAL(NDatetime::GetYearWeek(NDatetime::TCivilDay{2020, 1, 6}), 2); + + UNIT_ASSERT_VALUES_EQUAL(NDatetime::GetYearWeek(NDatetime::TCivilDay{2020, 2, 28}), 9); + UNIT_ASSERT_VALUES_EQUAL(NDatetime::GetYearWeek(NDatetime::TCivilDay{2020, 2, 29}), 9); + UNIT_ASSERT_VALUES_EQUAL(NDatetime::GetYearWeek(NDatetime::TCivilDay{2020, 3, 1}), 9); + UNIT_ASSERT_VALUES_EQUAL(NDatetime::GetYearWeek(NDatetime::TCivilDay{2020, 3, 2}), 10); + + UNIT_ASSERT_VALUES_EQUAL(NDatetime::GetYearWeek(NDatetime::TCivilDay{2020, 12, 31}), 53); + + // Max possible delta - calcuate week # for 31 Dec 2021 from 1 Jan 2020 + UNIT_ASSERT_VALUES_EQUAL(NDatetime::GetYearWeek(NDatetime::TCivilDay{2021, 12, 31}, true), 105); + } +} diff --git a/library/cpp/timezone_conversion/ut/convert_ut.cpp b/library/cpp/timezone_conversion/ut/convert_ut.cpp new file mode 100644 index 00000000000..bbf9e9b8263 --- /dev/null +++ b/library/cpp/timezone_conversion/ut/convert_ut.cpp @@ -0,0 +1,204 @@ +#include <library/cpp/timezone_conversion/convert.h> +#include <library/cpp/testing/unittest/gtest.h> + +using namespace NDatetime; + +template <> +void Out<TSimpleTM>(IOutputStream& os, TTypeTraits<TSimpleTM>::TFuncParam value) { + os << value.ToString() << ", dst: " << int(value.IsDst); +} + +TSimpleTM ZonedTm(i32 utcHours, bool isDst, ui32 year, ui32 mon, ui32 day, ui32 h, ui32 m, ui32 s) { + TSimpleTM res(year, mon, day, h, m, s); + res.GMTOff = utcHours * 60 * 60; + res.IsDst = isDst; + return res; +} + +void CompareCivilTimes(const TSimpleTM& expected, const TSimpleTM& actual) { + EXPECT_EQ(expected.GMTOff, actual.GMTOff); + EXPECT_EQ(expected.Year, actual.Year); + EXPECT_EQ(expected.Mon, actual.Mon); + EXPECT_EQ(expected.MDay, actual.MDay); + EXPECT_EQ(expected.WDay, actual.WDay); + EXPECT_EQ(expected.Hour, actual.Hour); + EXPECT_EQ(expected.Min, actual.Min); + EXPECT_EQ(expected.Sec, actual.Sec); + EXPECT_EQ(expected.IsDst, actual.IsDst); + EXPECT_EQ(expected.IsLeap, actual.IsLeap); +} + +#define CHECK_ROUND_TRIP(tz, unixTime, civil) \ + EXPECT_EQ( \ + TInstant::Seconds(unixTime), \ + ToAbsoluteTime(civil, tz)); \ + CompareCivilTimes( \ + civil, \ + ToCivilTime(TInstant::Seconds(unixTime), tz)); + +// Tests only unambiguous civil times (i.e., those that occurred exactly once). +TEST(TimeZoneConversion, Simple) { + TTimeZone msk = GetTimeZone("Europe/Moscow"); + // Before and after the temporary switch to UTC+3 in 2010. + CHECK_ROUND_TRIP( + msk, + 1288475999, + ZonedTm(+4, true, 2010, 10, 31, 1, 59, 59)); + CHECK_ROUND_TRIP( + msk, + 1288475999 + 3 * 60 * 60, + ZonedTm(+3, false, 2010, 10, 31, 3, 59, 59)); + + // Before and after the permanent switch to UTC+4 in 2011. + CHECK_ROUND_TRIP( + msk, + 1301180399, + ZonedTm(+3, false, 2011, 3, 27, 1, 59, 59)); + CHECK_ROUND_TRIP( + msk, + 1301180399 + 60 * 60, + ZonedTm(+4, false, 2011, 3, 27, 3, 59, 59)); + + // Some random moment between 2011 and 2014 when UTC+4 (no DST) was in place. + CHECK_ROUND_TRIP( + msk, + 1378901234, + ZonedTm(+4, false, 2013, 9, 11, 16, 7, 14)); + + // As of right now (i.e., as I'm writing this) Moscow is in UTC+3 (no DST). + CHECK_ROUND_TRIP( + msk, + 1458513396, + ZonedTm(+3, false, 2016, 3, 21, 1, 36, 36)); + + // Please add a new test if the current president moves Moscow back to UTC+4 + // or introduces DST again. +} + +TEST(TimeZoneConversion, TestRepeatedDate) { + TTimeZone ekb = GetTimeZone("Asia/Yekaterinburg"); + + CompareCivilTimes( + ZonedTm(+6, true, 2010, 10, 31, 2, 30, 0), + ToCivilTime(TInstant::Seconds(1288470600), ekb)); + + CompareCivilTimes( + ZonedTm(+5, false, 2010, 10, 31, 2, 30, 0), + ToCivilTime(TInstant::Seconds(1288474200), ekb)); + + CompareCivilTimes( + ZonedTm(+5, false, 2016, 5, 10, 9, 8, 7), + CreateCivilTime(ekb, 2016, 5, 10, 9, 8, 7)); + + CompareCivilTimes( + ZonedTm(+6, true, 2010, 10, 31, 2, 30, 0), + CreateCivilTime(ekb, 2010, 10, 31, 2, 30, 0)); + + // The earlier timestamp should be chosen. + EXPECT_EQ( + TInstant::Seconds(1288470600), + ToAbsoluteTime(TSimpleTM(2010, 10, 31, 2, 30, 0), ekb)); +} + +TEST(TimeZoneConversion, TestSkippedDate) { + TTimeZone nsk = GetTimeZone("Asia/Novosibirsk"); + + CompareCivilTimes( + ZonedTm(+6, false, 2011, 3, 27, 1, 30, 0), + ToCivilTime(TInstant::Seconds(1301167800), nsk)); + + CompareCivilTimes( + ZonedTm(+7, false, 2011, 3, 27, 3, 30, 0), + ToCivilTime(TInstant::Seconds(1301171400), nsk)); + + EXPECT_EQ( + TInstant::Seconds(1301171400), + ToAbsoluteTime(TSimpleTM(2011, 3, 27, 2, 30, 0), nsk)); + + EXPECT_EQ( + TInstant::Seconds(1301171400), + ToAbsoluteTime(TSimpleTM(2011, 3, 27, 3, 30, 0), nsk)); +} + +TEST(TimeZoneConversion, Utc) { + CHECK_ROUND_TRIP( + GetUtcTimeZone(), + 1451703845, + ZonedTm(0, false, 2016, 1, 2, 3, 4, 5)); +} + +TEST(TimeZoneConversion, Local) { + TTimeZone local = GetLocalTimeZone(); + auto nowAbsolute = TInstant::Now(); + auto nowCivilLocal = ToCivilTime(nowAbsolute, local); + EXPECT_EQ(nowAbsolute.Seconds(), ToAbsoluteTime(nowCivilLocal, local).Seconds()); +} + +TEST(TimeZoneConversion, BeforeEpoch) { + { + //NOTE: This test will not work because NDatetime::Convert() with TInstant does not work properly for dates before 1/1/1970 + NDatetime::TCivilSecond civilTime = NDatetime::TCivilSecond{1969, 12, 1, 0, 0, 0}; + TInstant absTime = NDatetime::Convert(civilTime, NDatetime::GetUtcTimeZone()); + NDatetime::TCivilSecond civilTime2 = NDatetime::Convert(absTime, NDatetime::GetUtcTimeZone()); + EXPECT_NE(civilTime2, civilTime); // ERROR. Must be EXPECT_EQ, but Convert() functions with TInstant doesnot wotk properly for dates before EPOCH + } + + // Right test + NDatetime::TCivilSecond civilTime = NDatetime::TCivilSecond{1969, 12, 1, 0, 0, 0}; + NDatetime::TCivilSecond civilTime2 = Convert<NDatetime::TCivilSecond>(civilTime, NDatetime::GetUtcTimeZone(), NDatetime::GetUtcTimeZone()); + EXPECT_EQ(civilTime2, civilTime); + +} + +TEST(TimeZoneConversion, InvalidTimeZone) { + EXPECT_THROW(GetTimeZone("Europe/Mscow"), yexception); + EXPECT_THROW(GetTimeZone(""), yexception); +} + +TEST(TimeZoneConversion, TestSaratov) { + TTimeZone saratov = GetTimeZone("Europe/Saratov"); + + CompareCivilTimes( + ZonedTm(+4, false, 2016, 12, 5, 1, 55, 35), + ToCivilTime(TInstant::Seconds(1480888535), saratov)); + + CompareCivilTimes( + ZonedTm(+3, false, 2016, 12, 1, 0, 55, 35), + ToCivilTime(TInstant::Seconds(1480542935), saratov)); +} + +TEST(TimeZoneConversion, TestFutureDstChanges) { + TTimeZone london = GetTimeZone("Europe/London"); + + // This test assumes the British won't cancel DST before 2025. + // I don't think they will, but then again, nobody really expected Brexit. + + // DST is still in effect in early October 2025, meaning we are in UTC+1. + CHECK_ROUND_TRIP( + london, + 1760124660, + ZonedTm(+1, true, 2025, 10, 10, 20, 31, 0)); + + // 31 days later we're back to UTC+0 again. + CHECK_ROUND_TRIP( + london, + 1760124660 + 31 * 24 * 60 * 60, + ZonedTm(+0, false, 2025, 11, 10, 19, 31, 0)); +} + +TEST(TimeZoneConversion, TWDay) { + TTimeZone nsk = GetTimeZone("Asia/Novosibirsk"); + + for (time_t e = 1301167800, to = 1301167800 + 86400 * 7, dow = 0; e < to; e += 86400, ++dow) { + EXPECT_EQ(dow, ToCivilTime(TInstant::Seconds(e), nsk).WDay); + } +} + +TEST(TimeZoneConversion, TestBaikonur) { + // Yes, the Baikonur spaceport is located in Kyzylorda Region. + const auto baikonur = GetTimeZone("Asia/Qyzylorda"); + + CompareCivilTimes( + ZonedTm(+5, false, 2019, 1, 11, 23, 55, 23), + ToCivilTime(TInstant::Seconds(1547232923), baikonur)); +} diff --git a/library/cpp/timezone_conversion/ut/ya.make b/library/cpp/timezone_conversion/ut/ya.make new file mode 100644 index 00000000000..781a57da9fc --- /dev/null +++ b/library/cpp/timezone_conversion/ut/ya.make @@ -0,0 +1,15 @@ +UNITTEST() + +OWNER(dfyz) + +PEERDIR( + library/cpp/testing/unittest + library/cpp/timezone_conversion +) + +SRCS( + convert_ut.cpp + civil_ut.cpp +) + +END() diff --git a/library/cpp/timezone_conversion/ya.make b/library/cpp/timezone_conversion/ya.make new file mode 100644 index 00000000000..f99ebe73eee --- /dev/null +++ b/library/cpp/timezone_conversion/ya.make @@ -0,0 +1,22 @@ +LIBRARY() + +OWNER( + dfyz + petrk +) + +PEERDIR( + contrib/libs/cctz/tzdata + util/draft +) + +SRCS( + convert.cpp + civil.cpp +) + +GENERATE_ENUM_SERIALIZATION(civil.h) + +END() + +RECURSE_FOR_TESTS(ut) |