winix/winixd/core/timezone.cpp

519 lines
9.7 KiB
C++

/*
* This file is a part of Winix
* and is distributed under the 2-Clause BSD licence.
* Author: Tomasz Sowa <t.sowa@ttmath.org>
*/
/*
* 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