From ae017078c9cc32f5af048243e649d06eeaedfe87 Mon Sep 17 00:00:00 2001 From: Howard Hinnant Date: Fri, 9 Apr 2021 11:08:12 -0400 Subject: [PATCH] Fix Posix::time_zone for southern hemisphere * The get_info functions were not prepared for the case where the start and end times of a rule are chronologically reversed for a year. I.e. the daylight saving start comes later in the year than the daylight saving end. When save is positive, this happens in the southern hemisphere (e.g. "Australia/Sydney" -> "AEST-10AEDT,M10.1.0,M4.1.0/3"). It also happens in the nothern hemisphere when the save is negative (e.g. "Europe/Dublin" -> "IST-1GMT0,M10.5.0,M3.5.0/1"). * Added tests for Posix::time_zone. --- include/date/ptz.h | 204 +++++++++++++++++++++++++++------------- test/posix/ptz.pass.cpp | 108 +++++++++++++++++++++ 2 files changed, 246 insertions(+), 66 deletions(-) create mode 100644 test/posix/ptz.pass.cpp diff --git a/include/date/ptz.h b/include/date/ptz.h index bdb00b0..ebd6e04 100644 --- a/include/date/ptz.h +++ b/include/date/ptz.h @@ -269,8 +269,76 @@ public: std::string name() const; friend bool operator==(const time_zone& x, const time_zone& y); + +private: + date::sys_seconds get_start(date::year y) const; + date::sys_seconds get_prev_start(date::year y) const; + date::sys_seconds get_next_start(date::year y) const; + date::sys_seconds get_end(date::year y) const; + date::sys_seconds get_prev_end(date::year y) const; + date::sys_seconds get_next_end(date::year y) const; + date::sys_info contant_offset() const; }; +inline +date::sys_seconds +time_zone::get_start(date::year y) const +{ + return date::sys_seconds{(start_rule_(y) - offset_).time_since_epoch()}; +} + +inline +date::sys_seconds +time_zone::get_prev_start(date::year y) const +{ + return date::sys_seconds{(start_rule_(--y) - offset_).time_since_epoch()}; +} + +inline +date::sys_seconds +time_zone::get_next_start(date::year y) const +{ + return date::sys_seconds{(start_rule_(++y) - offset_).time_since_epoch()}; +} + +inline +date::sys_seconds +time_zone::get_end(date::year y) const +{ + return date::sys_seconds{(end_rule_(y) - (offset_ + save_)).time_since_epoch()}; +} + +inline +date::sys_seconds +time_zone::get_prev_end(date::year y) const +{ + return date::sys_seconds{(end_rule_(--y) - (offset_ + save_)).time_since_epoch()}; +} + +inline +date::sys_seconds +time_zone::get_next_end(date::year y) const +{ + return date::sys_seconds{(end_rule_(++y) - (offset_ + save_)).time_since_epoch()}; +} + +date::sys_info +time_zone::contant_offset() const +{ + using date::year; + using date::sys_info; + using date::sys_days; + using date::January; + using date::December; + using date::last; + sys_info r; + r.begin = sys_days{year::min()/January/1}; + r.end = sys_days{year::max()/December/last}; + r.abbrev = std_abbrev_; + r.offset = offset_; + return r; +} + inline time_zone::time_zone(const detail::string_t& s) { @@ -313,12 +381,10 @@ time_zone::get_info(date::sys_time st) const { using date::sys_info; using date::year_month_day; - using date::sys_seconds; using date::sys_days; using date::floor; using date::ceil; using date::days; - using date::years; using date::year; using date::January; using date::December; @@ -329,36 +395,59 @@ time_zone::get_info(date::sys_time st) const if (start_rule_.ok()) { auto y = year_month_day{floor(st)}.year(); - auto start = sys_seconds{(start_rule_(y) - offset_).time_since_epoch()}; - auto end = sys_seconds{(end_rule_(y) - (offset_ + save_)).time_since_epoch()}; - if (start <= st && st < end) + auto start = get_start(y); + auto end = get_end(y); + if (start <= end) // (northern hemisphere) { - r.begin = start; - r.end = end; - r.offset += save_; - r.save = ceil(save_); - r.abbrev = dst_abbrev_; + if (start <= st && st < end) + { + r.begin = start; + r.end = end; + r.offset += save_; + r.save = ceil(save_); + r.abbrev = dst_abbrev_; + } + else if (st < start) + { + r.begin = get_prev_end(y); + r.end = start; + r.abbrev = std_abbrev_; + } + else // st >= end + { + r.begin = end; + r.end = get_next_start(y); + r.abbrev = std_abbrev_; + } } - else if (st < start) + else // end < start (southern hemisphere) { - r.begin = sys_seconds{(end_rule_(y-years{1}) - - (offset_ + save_)).time_since_epoch()}; - r.end = start; - r.abbrev = std_abbrev_; - } - else // st >= end - { - r.begin = end; - r.end = sys_seconds{(start_rule_(y+years{1}) - offset_).time_since_epoch()}; - r.abbrev = std_abbrev_; + if (end <= st && st < start) + { + r.begin = end; + r.end = start; + r.abbrev = std_abbrev_; + } + else if (st < end) + { + r.begin = get_prev_start(y); + r.end = end; + r.offset += save_; + r.save = ceil(save_); + r.abbrev = dst_abbrev_; + } + else // st >= start + { + r.begin = start; + r.end = get_next_end(y); + r.offset += save_; + r.save = ceil(save_); + r.abbrev = dst_abbrev_; + } } } - else // constant offset - { - r.begin = sys_days{year::min()/January/1}; - r.end = sys_days{year::max()/December/last}; - r.abbrev = std_abbrev_; - } + else + r = contant_offset(); return r; } @@ -371,7 +460,6 @@ time_zone::get_info(date::local_time tp) const using date::days; using date::sys_days; using date::sys_seconds; - using date::years; using date::year; using date::ceil; using date::January; @@ -384,19 +472,25 @@ time_zone::get_info(date::local_time tp) const if (start_rule_.ok()) { auto y = year_month_day{floor(tp)}.year(); - auto start = sys_seconds{(start_rule_(y) - offset_).time_since_epoch()}; - auto end = sys_seconds{(end_rule_(y) - (offset_ + save_)).time_since_epoch()}; + auto start = get_start(y); + auto end = get_end(y); auto utcs = sys_seconds{floor(tp - offset_).time_since_epoch()}; auto utcd = sys_seconds{floor(tp - (offset_ + save_)).time_since_epoch()}; + auto northern = start <= end; if ((utcs < start) != (utcd < start)) { - r.first.begin = sys_seconds{(end_rule_(y-years{1}) - - (offset_ + save_)).time_since_epoch()}; + if (northern) + r.first.begin = get_prev_end(y); + else + r.first.begin = end; r.first.end = start; r.first.offset = offset_; r.first.abbrev = std_abbrev_; r.second.begin = start; - r.second.end = end; + if (northern) + r.second.end = end; + else + r.second.end = get_next_end(y); r.second.abbrev = dst_abbrev_; r.second.offset = offset_ + save_; r.second.save = ceil(save_); @@ -405,51 +499,29 @@ time_zone::get_info(date::local_time tp) const } else if ((utcs < end) != (utcd < end)) { - r.first.begin = start; + if (northern) + r.first.begin = start; + else + r.first.begin = get_prev_start(y); r.first.end = end; r.first.offset = offset_ + save_; r.first.save = ceil(save_); r.first.abbrev = dst_abbrev_; r.second.begin = end; - r.second.end = sys_seconds{(start_rule_(y+years{1}) - - offset_).time_since_epoch()}; + if (northern) + r.second.end = get_next_start(y); + else + r.second.end = start; r.second.abbrev = std_abbrev_; r.second.offset = offset_; r.result = save_ > seconds{0} ? local_info::ambiguous : local_info::nonexistent; } - else if (utcs < start) - { - r.first.begin = sys_seconds{(end_rule_(y-years{1}) - - (offset_ + save_)).time_since_epoch()}; - r.first.end = start; - r.first.offset = offset_; - r.first.abbrev = std_abbrev_; - } - else if (utcs < end) - { - r.first.begin = start; - r.first.end = end; - r.first.offset = offset_ + save_; - r.first.save = ceil(save_); - r.first.abbrev = dst_abbrev_; - } else - { - r.first.begin = end; - r.first.end = sys_seconds{(start_rule_(y+years{1}) - - offset_).time_since_epoch()}; - r.first.abbrev = std_abbrev_; - r.first.offset = offset_; - } - } - else // constant offset - { - r.first.begin = sys_days{year::min()/January/1}; - r.first.end = sys_days{year::max()/December/last}; - r.first.abbrev = std_abbrev_; - r.first.offset = offset_; + r.first = get_info(utcs); } + else + r.first = contant_offset(); return r; } diff --git a/test/posix/ptz.pass.cpp b/test/posix/ptz.pass.cpp new file mode 100644 index 0000000..5601c21 --- /dev/null +++ b/test/posix/ptz.pass.cpp @@ -0,0 +1,108 @@ +// The MIT License (MIT) +// +// Copyright (c) 2021 Howard Hinnant +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// Test Posix::time_zone + +#include "tz.h" +#include "ptz.h" +#include + +bool +is_equal(date::sys_info const& x, date::sys_info const& y) +{ + return x.begin == y.begin && + x.end == y.end && + x.offset == y.offset && + x.save == y.save && + x.abbrev == y.abbrev; +} + +bool +is_equal(date::local_info const& x, date::local_info const& y) +{ + return x.result == y.result && is_equal(x.first, y.first) + && is_equal(x.second, y.second); +} + +int +main() +{ + using namespace date; + using namespace std; + using namespace std::chrono; + + auto tzi = locate_zone("Australia/Sydney"); + Posix::time_zone tzp{"AEST-10AEDT,M10.1.0,M4.1.0/3"}; + auto tp = local_days{2021_y/1/1} + 0s; + assert(tzp.get_info(tp).result == local_info::unique); + assert(is_equal(tzi->get_info(tp), tzp.get_info(tp))); + + tp = local_days{2021_y/10/Sunday[1]} + 2h + 30min; + assert(tzp.get_info(tp).result == local_info::nonexistent); + assert(is_equal(tzi->get_info(tp), tzp.get_info(tp))); + + tp = local_days{2021_y/4/Sunday[1]} + 2h + 30min; + assert(tzp.get_info(tp).result == local_info::ambiguous); + assert(is_equal(tzi->get_info(tp), tzp.get_info(tp))); + + tp = local_days{2021_y/7/1}; + assert(tzp.get_info(tp).result == local_info::unique); + assert(is_equal(tzi->get_info(tp), tzp.get_info(tp))); + + + tzi = locate_zone("America/New_York"); + tzp = Posix::time_zone{"EST5EDT,M3.2.0,M11.1.0"}; + tp = local_days{2021_y/1/1}; + assert(tzp.get_info(tp).result == local_info::unique); + assert(is_equal(tzi->get_info(tp), tzp.get_info(tp))); + + tp = local_days{2021_y/3/Sunday[2]} + 2h + 30min; + assert(tzp.get_info(tp).result == local_info::nonexistent); + assert(is_equal(tzi->get_info(tp), tzp.get_info(tp))); + + tp = local_days{2021_y/11/Sunday[1]} + 1h + 30min; + assert(tzp.get_info(tp).result == local_info::ambiguous); + assert(is_equal(tzi->get_info(tp), tzp.get_info(tp))); + + tp = local_days{2021_y/7/1}; + assert(tzp.get_info(tp).result == local_info::unique); + assert(is_equal(tzi->get_info(tp), tzp.get_info(tp))); + + + tzi = locate_zone("Europe/Dublin"); + tzp = Posix::time_zone{"IST-1GMT0,M10.5.0,M3.5.0/1"}; + tp = local_days{2021_y/1/1}; + assert(tzp.get_info(tp).result == local_info::unique); + assert(is_equal(tzi->get_info(tp), tzp.get_info(tp))); + + tp = local_days{2021_y/3/Sunday[last]} + 1h + 30min; + assert(tzp.get_info(tp).result == local_info::nonexistent); + assert(is_equal(tzi->get_info(tp), tzp.get_info(tp))); + + tp = local_days{2021_y/10/Sunday[last]} + 1h + 30min; + assert(tzp.get_info(tp).result == local_info::ambiguous); + assert(is_equal(tzi->get_info(tp), tzp.get_info(tp))); + + tp = local_days{2021_y/7/1}; + assert(tzp.get_info(tp).result == local_info::unique); + assert(is_equal(tzi->get_info(tp), tzp.get_info(tp))); +}