aboutsummaryrefslogtreecommitdiffstats
path: root/library/cpp/timezone_conversion
diff options
context:
space:
mode:
authorDevtools Arcadia <arcadia-devtools@yandex-team.ru>2022-02-07 18:08:42 +0300
committerDevtools Arcadia <arcadia-devtools@mous.vla.yp-c.yandex.net>2022-02-07 18:08:42 +0300
commit1110808a9d39d4b808aef724c861a2e1a38d2a69 (patch)
treee26c9fed0de5d9873cce7e00bc214573dc2195b7 /library/cpp/timezone_conversion
downloadydb-1110808a9d39d4b808aef724c861a2e1a38d2a69.tar.gz
intermediate changes
ref:cde9a383711a11544ce7e107a78147fb96cc4029
Diffstat (limited to 'library/cpp/timezone_conversion')
-rw-r--r--library/cpp/timezone_conversion/README.md27
-rw-r--r--library/cpp/timezone_conversion/civil-inl.h63
-rw-r--r--library/cpp/timezone_conversion/civil.cpp234
-rw-r--r--library/cpp/timezone_conversion/civil.h338
-rw-r--r--library/cpp/timezone_conversion/convert.cpp93
-rw-r--r--library/cpp/timezone_conversion/convert.h60
-rw-r--r--library/cpp/timezone_conversion/ut/civil_ut.cpp157
-rw-r--r--library/cpp/timezone_conversion/ut/convert_ut.cpp204
-rw-r--r--library/cpp/timezone_conversion/ut/ya.make15
-rw-r--r--library/cpp/timezone_conversion/ya.make22
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)