#include "common.hh" #include "tz_str.hh" #include namespace tz { namespace { inline bool is_alpha(char c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); } inline bool is_digit(char c) { return c >= '0' && c <= '9'; } inline bool is_alphnumeric(char c) { return is_alpha(c) || is_digit(c); } std::optional read_abbr(std::string_view str, size_t& offset) { if (offset >= str.size()) return std::nullopt; auto const start = offset; std::string_view ret; if (str[offset] == '<') { size_t end = str.find('>', offset + 1); if (end == std::string_view::npos) return std::nullopt; offset = end + 1; ret = str.substr(start + 1, end - (start + 1)); for (auto const& c : ret) { if (!(is_alphnumeric(c) || c == '+' || c == '-')) return std::nullopt; } } else { while (offset < str.size() && is_alpha(str[offset])) ++offset; ret = str.substr(start, offset - start); } if (ret.size() < 3) return std::nullopt; return ret; } std::optional read_num(std::string_view str, size_t& offset) { if (offset >= str.size() || !is_digit(str[offset])) return std::nullopt; uint32_t ret = str[offset++] - '0'; while (offset < str.size() && is_digit(str[offset])) { auto value = ret * 10 + (str[offset++] - '0'); if (value < ret) return std::nullopt; ret = value; } return ret; } std::optional read_time(std::string_view str, size_t& offset) { auto hh = read_num(str, offset); if (!hh) return std::nullopt; if (hh.value() > 24) return std::nullopt; time_t ret = hh.value() * 60 * 60; if (offset < str.size() && str[offset] == ':') { ++offset; auto mm = read_num(str, offset); if (!mm) return std::nullopt; if (mm.value() > 59) return std::nullopt; ret += mm.value() * 60; if (offset < str.size() && str[offset] == ':') { ++offset; auto ss = read_num(str, offset); if (!ss) return std::nullopt; if (ss.value() > 59) return std::nullopt; ret += ss.value(); } } return ret; } std::optional read_offset(std::string_view str, size_t& offset) { bool negative; if (offset < str.size() && (str[offset] == '+' || str[offset] == '-')) negative = str[offset++] == '+'; // Yes, this is correct. ('-' is east) else negative = true; // Yes, this is correct. (default is west) auto ret = read_time(str, offset); if (!ret) return std::nullopt; return negative ? -ret.value() : ret.value(); } inline bool leap_year(int32_t local_year) { return (local_year % 4) == 0 && ((local_year % 100) || ((local_year % 400) == 0)); } uint8_t week_day_for_day_of_month(int32_t year, uint8_t month, uint8_t day_of_month) { auto k = static_cast(day_of_month); auto m = month >= 3 ? month - 2 : 10 + month; auto C = year % 100; auto Y = year / 100; if (m > 10) --Y; return abs((k + static_cast((2.6 * m - 0.2)) - 2 * C + Y + (Y / 4) + (C / 4))) % 7; } std::optional read_date_and_time(std::string_view str, int32_t local_year, size_t& offset) { if (offset >= str.size()) return std::nullopt; time_t day_of_year; if (str[offset] == 'J') { ++offset; auto julian_day = read_num(str, offset); if (!julian_day || julian_day.value() < 1 || julian_day.value() > 365) return std::nullopt; day_of_year = julian_day.value() - 1; if (leap_year(local_year) && julian_day.value() >= 60) ++day_of_year; } else if (str[offset] == 'M') { ++offset; auto month = read_num(str, offset); if (!month || month.value() < 1 || month.value() > 12) return std::nullopt; if (offset >= str.size() || str[offset] != '.') return std::nullopt; ++offset; auto week_of_month = read_num(str, offset); if (!week_of_month || week_of_month.value() < 1 || week_of_month.value() > 5) return std::nullopt; if (offset >= str.size() || str[offset] != '.') return std::nullopt; ++offset; auto day_of_week = read_num(str, offset); if (!day_of_week || day_of_week.value() > 6) return std::nullopt; day_of_year = 0; for (size_t i = 1; i < month; ++i) { day_of_year += (i <= 7) ? ((i == 2) ? (leap_year(local_year) ? 29 : 28) : (i % 2 ? 31 : 30)) : (i % 2 ? 30 : 31); } auto week_day_for_first_day_of_month = week_day_for_day_of_month( local_year, month.value(), 1); for (size_t i = 1; i < week_of_month; ++i) day_of_year += 7; if (day_of_week.value() < week_day_for_first_day_of_month) day_of_year += 7 - week_day_for_first_day_of_month - day_of_week.value(); else day_of_year += day_of_week.value() - week_day_for_first_day_of_month; } else { auto julian_day = read_num(str, offset); if (!julian_day || julian_day.value() > 365) return std::nullopt; day_of_year = julian_day.value(); } time_t ret = day_of_year * 24 * 60 * 60; if (offset < str.size() && str[offset] == '/') { ++offset; auto time = read_time(str, offset); if (!time) return std::nullopt; ret += time.value(); } else { ret += 2 * 60 * 60; } return ret; } } // namespace std::optional get_local_time(std::string_view tz_str, time_t utc_time) { size_t offset = 0; auto std = read_abbr(tz_str, offset); if (!std) return std::nullopt; auto std_offset = read_offset(tz_str, offset); if (!std_offset) return std::nullopt; auto local_time = utc_time + std_offset.value(); if (offset == tz_str.size()) return local_time; auto dst = read_abbr(tz_str, offset); if (!dst) return std::nullopt; std::optional dst_offset; if (offset == tz_str.size() || tz_str[offset] == ',') { dst_offset = std_offset.value() + 60 * 60; } else { dst_offset = read_offset(tz_str, offset); if (!dst_offset) return std::nullopt; } if (offset == tz_str.size()) { // TODO: Can't figure out what the spec actually says about this. // They are clearly optional but it doesn't specify what the default // are. Assume no DST. return local_time; } if (tz_str[offset] != ',') return std::nullopt; ++offset; // TODO: This can't be 100% correct auto local_year = 1970 + local_time / (365.25 * 24 * 60 * 60); auto start = read_date_and_time(tz_str, local_year, offset); if (!start) return std::nullopt; if (tz_str[offset] != ',') return std::nullopt; ++offset; auto end = read_date_and_time(tz_str, local_year, offset); if (!end) return std::nullopt; // TODO: If local_year isn't correct this is definitly not. auto local_time_in_year = local_time % static_cast((365.25 * 24 * 60 * 60)); if (start.value() <= end.value()) { if (start.value() < local_time_in_year && local_time_in_year < end.value()) { return utc_time + dst_offset.value(); } } else { if (!(end.value() < local_time_in_year && local_time_in_year < start.value())) { return utc_time + dst_offset.value(); } } return local_time; } } // namespace tz