/* * This file is a part of Winix * and is distributed under the 2-Clause BSD licence. * Author: Tomasz Sowa */ /* * Copyright (c) 2012-2023, Tomasz Sowa * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * */ #include "timezone.h" #include "misc.h" namespace Winix { TimeZone::DstDate::DstDate() { Clear(); } void TimeZone::DstDate::Clear() { date_type = ordinary_date; weekday = 0; date.Clear(); } int TimeZone::DstDate::Compare(const pt::Date & utc_date) const { int res = 0; if( date_type == ordinary_date ) { res = date.Compare(utc_date, true); } else if( date_type == first_weekday ) { pt::Date tmp_date = CalculateRelativeDate(utc_date.year, date.month, true, weekday, date.hour, date.min, date.sec); res = tmp_date.Compare(utc_date, true); } else if( date_type == last_weekday ) { pt::Date tmp_date = CalculateRelativeDate(utc_date.year, date.month, false, weekday, date.hour, date.min, date.sec); res = tmp_date.Compare(utc_date, true); } return res; } pt::Date TimeZone::DstDate::CalculateRelativeDate(int year, int month, bool is_first, int weekday, int hour, int min, int sec) { pt::Date new_date; new_date.year = year; new_date.month = month; new_date.hour = hour; new_date.min = min; new_date.sec = sec; if( is_first ) { new_date.day = 1; int w = new_date.WeekDay(); if( w < weekday ) { new_date.day += (weekday - w); } else if( w > weekday ) { new_date.day += (weekday - w) + 7; } } else { new_date.day = pt::Date::MonthLen(new_date.year, new_date.month); int w = new_date.WeekDay(); if( w > weekday ) { new_date.day -= (w - weekday); } else if( w < weekday ) { new_date.day -= (w - weekday) + 7; } } return new_date; } TimeZone::Dst::Dst() { Clear(); } void TimeZone::Dst::Clear() { has_dst = false; start.Clear(); end.Clear(); offset = 0; } bool TimeZone::Dst::IsDstUsed(const pt::Date & utc_date) const { return has_dst && start.Compare(utc_date) <= 0 && end.Compare(utc_date) > 0; } TimeZone::TimeZone() { Clear(); } void TimeZone::Clear() { name.clear(); id = 0; offset = 0; dst_map.clear(); } TimeZone::Dst * TimeZone::FindDst(int year) { if( dst_map.empty() ) return 0; DstMap::iterator i = dst_map.lower_bound(year); if( i == dst_map.begin() && i->first > year ) return 0; if( i == dst_map.end() ) return &(--i)->second; if( i != dst_map.begin() && i->first > year ) return &(--i)->second; return &i->second; } time_t TimeZone::CalcLocalOffset(const pt::Date & utc_date) { time_t dst_offset = 0; Dst * dst = FindDst(utc_date.year); if( dst && dst->IsDstUsed(utc_date) ) dst_offset = dst->offset; return offset + dst_offset; } time_t TimeZone::ToLocal(time_t utc_time) { time_t offset = CalcLocalOffset(pt::Date(utc_time)); return utc_time + offset; } pt::Date TimeZone::ToLocal(const pt::Date & utc_date) { pt::Date local(utc_date); local += CalcLocalOffset(utc_date); return local; } void TimeZone::PrintOffsetPart(long val, pt::Stream & str) { if( val < 10 ) { str << '0'; } str << val; } void TimeZone::PrintOffset(time_t offset, pt::Stream & str) { bool has_sign = false; if( offset < 0 ) { has_sign = true; offset = 0 - offset; } time_t days = offset / 60 / 60 / 24; if( days == 0 ) { str << ((has_sign) ? '-' : '+'); time_t diff = offset - days * 60 * 60 * 24; long hour = int(diff / 60 / 60); long min = int((diff - hour * 60 * 60) / 60); PrintOffsetPart(hour, str); str << ':'; PrintOffsetPart(min, str); } } time_t TimeZone::CalcUTCOffset(const pt::Date & local_date) { time_t dst_offset = 0; Dst * dst = FindDst(local_date.year); if( dst && dst->has_dst ) { // dst date ranges we have in UTC pt::Date utc(local_date); utc -= (offset + dst->offset); if( dst->IsDstUsed(utc) ) dst_offset = dst->offset; } return offset + dst_offset; } time_t TimeZone::ToUTC(time_t local_time) { time_t offset = CalcUTCOffset(pt::Date(local_time)); return local_time - offset; } pt::Date TimeZone::ToUTC(const pt::Date & local_date) { time_t offset; pt::Date utc(local_date); offset = CalcUTCOffset(local_date); utc -= offset; return utc; } time_t TimeZone::ParseStrOffset(const wchar_t * str) { pt::Date date; bool is_sign = false; time_t offset = 0; str = SkipWhite(str); if( *str == '-' ) { is_sign = true; str += 1; } else if( *str == '+' ) { str += 1; } if( date.ParseTime(str) ) { offset = date.hour * 60 * 60 + date.min * 60 + date.sec; if( is_sign ) offset = -offset; } return offset; } time_t TimeZone::GetOffset(pt::Space & space) { std::wstring * offset_str = space.get_wstr(L"offset_str"); if( offset_str ) return ParseStrOffset(offset_str->c_str()); return space.to_long(L"offset"); } const wchar_t * TimeZone::SkipDstDateSeparator(const wchar_t * date_str) { date_str = pt::skip_white(date_str, false, false); // the same separators as in the pikotools library if( *date_str == '-' || *date_str == '/' || *date_str == '.' ) { ++date_str; } return pt::skip_white(date_str, false, false); } int TimeZone::ParseDstDateWeekday(const wchar_t * date_str, const wchar_t ** after_date_str) { date_str = SkipDstDateSeparator(date_str); int weekday_int = -1; struct WeekDay { const wchar_t * name; int weekday; }; static const WeekDay week_days[] = { {L"sunday", 0}, {L"monday", 1}, {L"tuesday", 2}, {L"wednesday", 3}, {L"thursday", 4}, {L"friday", 5}, {L"saturday", 6}, }; for(const WeekDay & week_day : week_days) { if( pt::is_substr(week_day.name, date_str) ) { date_str += wcslen(week_day.name); weekday_int = week_day.weekday; break; } } *after_date_str = date_str; return weekday_int; } bool TimeZone::ParseDstDate(int year, const wchar_t * date_str, DstDate & dst_date) { bool was_overflow = false; bool is_ok = false; const wchar_t * after_date_str; dst_date.Clear(); dst_date.date.year = year; date_str = pt::skip_white(date_str, false, false); dst_date.date.month = pt::to_i(date_str, 10, &after_date_str, &was_overflow, true); if( !was_overflow && after_date_str != date_str ) { date_str = SkipDstDateSeparator(after_date_str); if( pt::is_substr(L"first", date_str) ) { date_str += 5; // length of "first" dst_date.date_type = DstDate::first_weekday; dst_date.weekday = ParseDstDateWeekday(date_str, &date_str); dst_date.date.day = 1; // to return true from IsCorrectDate() at the end of this method, will not be used in the future is_ok = (dst_date.weekday >= 0); } else if( pt::is_substr(L"last", date_str) ) { date_str += 4; // length of "last" dst_date.date_type = DstDate::last_weekday; dst_date.weekday = ParseDstDateWeekday(date_str, &date_str); dst_date.date.day = 1; is_ok = (dst_date.weekday >= 0); } else { dst_date.date_type = DstDate::ordinary_date; dst_date.date.day = pt::to_i(date_str, 10, &after_date_str, &was_overflow, true); is_ok = !was_overflow && after_date_str != date_str; date_str = after_date_str; } } return is_ok && dst_date.date.ParseTime(date_str) && dst_date.date.IsCorrectDate(); } bool TimeZone::SetTzDst(pt::Space & year) { time_t h24 = pt::Date::ONE_DAY; bool result = true; Dst dst; int year_int = 0; std::wstring * year_name = year.get_wstr(L"year"); if( year_name ) year_int = Toi(*year_name); if( year_int < 1970 || year_int > 10000 ) return false; bool has_dst = year.to_bool(L"has_dst", false); std::wstring * dst_start = year.get_wstr(L"start"); std::wstring * dst_end = year.get_wstr(L"end"); if( has_dst && dst_start && dst_end ) { dst.has_dst = true; result = result && ParseDstDate(year_int, dst_start->c_str(), dst.start); result = result && ParseDstDate(year_int, dst_end->c_str(), dst.end); dst.offset = GetOffset(year); if( dst.offset < -h24 || dst.offset > h24 ) result = false; } if( result ) dst_map[year_int] = dst; return result; } bool TimeZone::SetTz(pt::Space & space) { bool result = true; name.clear(); std::wstring * space_name = space.get_wstr(L"name"); if( space_name ) name = *space_name; id = space.to_int(L"id", -1); offset = GetOffset(space); time_t h24 = pt::Date::ONE_DAY; if( offset < -h24 || offset > h24 ) result = false; pt::Space::TableType * dst = space.get_table(L"dst"); if( dst ) { for(pt::Space * dst_space : *dst) { if( !SetTzDst(*dst_space) ) { result = false; break; } } } return result; } } // namespace Winix