mirror of
https://github.com/brazilofmux/tinymux
synced 2026-08-13 00:23:11 -04:00
Add tz_name to timezone cache entries so $Z reads the cached name instead of calling localtime() again. Re-enable ±1 week probing on cache miss — three well-chosen localtime() calls cover a two-week span, so subsequent queries are pure cache hits. In functions.cpp, replace UTC2Local() + subtraction with a single queryLocalOffsetAtUTC() call that returns offset, DST, and timezone name from the cache. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
788 lines
35 KiB
C++
788 lines
35 KiB
C++
/*! \file timezone.cpp
|
|
* \brief Timezone-related helper functions (Modernized C++14 using specific time types).
|
|
*
|
|
* This contains conversions between local and UTC timezones using CLinearTimeAbsolute,
|
|
* CLinearTimeDelta, and standard library features, relying only on localtime() for
|
|
* system timezone information.
|
|
*/
|
|
|
|
#include <vector>
|
|
#include <ctime>
|
|
#include <algorithm>
|
|
#include <limits>
|
|
#include <cstdint>
|
|
#include <mutex>
|
|
#include <numeric>
|
|
#include <stdexcept>
|
|
#include <array>
|
|
#include <cstring>
|
|
|
|
#include "copyright.h"
|
|
#include "autoconf.h"
|
|
#include "config.h"
|
|
#include "core.h"
|
|
|
|
// --- Configuration & Constants ---
|
|
|
|
namespace TimezoneCache {
|
|
namespace Detail {
|
|
|
|
// Define isLeapYear if somehow missed by externs.h - unlikely
|
|
#ifndef isLeapYear
|
|
bool isLeapYear(int year) {
|
|
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
|
|
}
|
|
#endif // isLeapYear
|
|
|
|
// Assume time_1w is defined externally representing one week
|
|
#ifndef time_1w
|
|
// Provide a fallback definition if needed, though it should come from externs.h
|
|
const CLinearTimeDelta time_1w(7 * 24 * 60 * 60);
|
|
#endif
|
|
|
|
// Cache entry structure
|
|
struct OffsetEntry {
|
|
CLinearTimeAbsolute start_lta;
|
|
CLinearTimeAbsolute end_lta;
|
|
CLinearTimeDelta offset_ltd;
|
|
int touched_count; // For LRU
|
|
bool is_dst;
|
|
std::string tz_name;
|
|
|
|
// Need comparison operators for sorting/lower_bound
|
|
// Compare OffsetEntry < CLinearTimeAbsolute
|
|
bool operator<(const CLinearTimeAbsolute& t) const { return start_lta < t; }
|
|
// Compare CLinearTimeAbsolute < OffsetEntry
|
|
friend bool operator<(const CLinearTimeAbsolute& t, const OffsetEntry& e) { return t < e.start_lta; }
|
|
// Compare OffsetEntry < OffsetEntry (needed for sorting/min_element)
|
|
bool operator<(const OffsetEntry& other) const { return start_lta < other.start_lta; }
|
|
};
|
|
|
|
// Maximum size of the offset cache
|
|
constexpr std::size_t MAX_OFFSETS = 50;
|
|
// Minimum interval to merge cache entries
|
|
const CLinearTimeDelta MIN_MERGE_INTERVAL = time_1w; // Use external definition
|
|
|
|
// Encapsulated state
|
|
struct CacheState {
|
|
CLinearTimeAbsolute lower_bound_lta;
|
|
CLinearTimeAbsolute upper_bound_lta;
|
|
CLinearTimeDelta standard_offset_ltd;
|
|
std::array<int16_t, 15> nearest_year_of_type; // Index 0 unused, 1-7 non-leap, 8-14 leap
|
|
std::vector<OffsetEntry> offset_table;
|
|
int touch_counter = 0;
|
|
bool initialized = false;
|
|
std::once_flag init_flag;
|
|
std::mutex cache_mutex;
|
|
|
|
CacheState() {
|
|
nearest_year_of_type.fill(-1);
|
|
offset_table.reserve(MAX_OFFSETS);
|
|
}
|
|
};
|
|
|
|
// Singleton instance of the state
|
|
CacheState& getState() {
|
|
static CacheState state;
|
|
return state;
|
|
}
|
|
|
|
|
|
// --- Time Conversion Helpers ---
|
|
|
|
// Convert struct tm to FIELDEDTIME (adapted from original)
|
|
void setFieldedTimeFromStructTm(FIELDEDTIME* ft, const struct tm* ptm)
|
|
{
|
|
ft->iYear = static_cast<short>(ptm->tm_year + 1900);
|
|
ft->iMonth = static_cast<unsigned short>(ptm->tm_mon + 1);
|
|
ft->iDayOfMonth = static_cast<unsigned short>(ptm->tm_mday);
|
|
ft->iDayOfWeek = static_cast<unsigned short>(ptm->tm_wday);
|
|
ft->iDayOfYear = static_cast<unsigned short>(ptm->tm_yday + 1);
|
|
ft->iHour = static_cast<unsigned short>(ptm->tm_hour);
|
|
ft->iMinute = static_cast<unsigned short>(ptm->tm_min);
|
|
ft->iSecond = static_cast<unsigned short>(ptm->tm_sec);
|
|
ft->iMillisecond = 0;
|
|
ft->iMicrosecond = 0;
|
|
ft->iNanosecond = 0;
|
|
}
|
|
|
|
static time_t time_t_largest(void)
|
|
{
|
|
time_t t;
|
|
if (sizeof(int64_t) <= sizeof(time_t))
|
|
{
|
|
t = static_cast<time_t>(INT64_MAX);
|
|
}
|
|
else
|
|
{
|
|
t = static_cast<time_t>(INT32_MAX);
|
|
}
|
|
|
|
#if defined(TIMEUTIL_TIME_T_MAX_VALUE)
|
|
int64_t t64 = static_cast<int64_t>(t);
|
|
if (TIMEUTIL_TIME_T_MAX_VALUE < t64)
|
|
{
|
|
t = static_cast<time_t>(TIMEUTIL_TIME_T_MAX_VALUE);
|
|
}
|
|
#endif
|
|
#if defined(LOCALTIME_TIME_T_MAX_VALUE)
|
|
// Windows cannot handle negative time_t values, and some versions have
|
|
// an upper limit as well. Values which are too large cause an assert.
|
|
//
|
|
// In VS 2003, the limit is 0x100000000000i64 (beyond the size of a
|
|
// time_t). In VS 2005, the limit is December 31, 2999, 23:59:59 UTC
|
|
// (or 32535215999).
|
|
//
|
|
if (LOCALTIME_TIME_T_MAX_VALUE < t)
|
|
{
|
|
t = static_cast<time_t>(LOCALTIME_TIME_T_MAX_VALUE);
|
|
}
|
|
#endif
|
|
return t;
|
|
}
|
|
|
|
static time_t time_t_smallest(void)
|
|
{
|
|
time_t t;
|
|
if (sizeof(int64_t) <= sizeof(time_t))
|
|
{
|
|
t = static_cast<time_t>(INT64_MIN);
|
|
}
|
|
else
|
|
{
|
|
t = static_cast<time_t>(INT32_MIN);
|
|
}
|
|
#if defined(TIMEUTIL_TIME_T_MIN_VALUE)
|
|
int64_t t64 = static_cast<int64_t>(t);
|
|
if (t64 < TIMEUTIL_TIME_T_MIN_VALUE)
|
|
{
|
|
t = static_cast<time_t>(TIMEUTIL_TIME_T_MIN_VALUE);
|
|
}
|
|
#endif
|
|
#if defined(LOCALTIME_TIME_T_MIN_VALUE)
|
|
if (t < LOCALTIME_TIME_T_MIN_VALUE)
|
|
{
|
|
t = static_cast<time_t>(LOCALTIME_TIME_T_MIN_VALUE);
|
|
}
|
|
#endif
|
|
return t;
|
|
}
|
|
|
|
// Safely convert CLinearTimeAbsolute to time_t for localtime()
|
|
// Handles potential mismatch between CLinearTimeAbsolute internal representation and time_t
|
|
time_t to_time_t(const CLinearTimeAbsolute& lta) {
|
|
// Assuming ReturnSeconds() returns a type like int64_t or similar wide enough type
|
|
auto seconds_count = lta.ReturnSeconds();
|
|
|
|
// Clamp to time_t limits if necessary
|
|
time_t t_max = time_t_largest();
|
|
time_t t_min = time_t_smallest();
|
|
|
|
// Cast appropriately for comparison
|
|
if (seconds_count > static_cast<int64_t>(t_max)) return t_max;
|
|
if (seconds_count < static_cast<int64_t>(t_min)) return t_min;
|
|
|
|
return static_cast<time_t>(seconds_count);
|
|
}
|
|
|
|
// Wrapper for platform-specific localtime (adapted from original mux_localtime)
|
|
bool safe_localtime(const time_t* timer, struct tm* result) {
|
|
#if defined(WINDOWS_TIME) && !defined(__INTEL_COMPILER) && (_MSC_VER >= 1400)
|
|
// MS specific secure version
|
|
return (_localtime64_s(result, timer) == 0);
|
|
#elif defined(HAVE_LOCALTIME_R)
|
|
// POSIX reentrant version
|
|
return (localtime_r(timer, result) != nullptr);
|
|
#else
|
|
// Fallback to non-thread-safe localtime - requires external locking
|
|
// This mutex protects the call to standard localtime itself.
|
|
static std::mutex localtime_mutex;
|
|
std::lock_guard<std::mutex> lock(localtime_mutex);
|
|
struct tm* ptm = localtime(timer);
|
|
if (ptm) {
|
|
// Copy the result from the static internal buffer used by localtime
|
|
*result = *ptm;
|
|
return true;
|
|
}
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
// --- Initialization Logic ---
|
|
|
|
// Determines the type of year based on leap status and starting weekday (original logic)
|
|
int getYearType(int iYear)
|
|
{
|
|
FIELDEDTIME ft;
|
|
// Use memset for POD initialization consistency with C style if preferred,
|
|
// otherwise C++ zero-initialization FIELDEDTIME ft{}; might suffice if it's simple enough.
|
|
std::memset(&ft, 0, sizeof(FIELDEDTIME));
|
|
ft.iYear = static_cast<short>(iYear);
|
|
ft.iMonth = 1;
|
|
ft.iDayOfMonth = 1;
|
|
// Other fields (like time) should ideally be set to a neutral value (e.g., noon)
|
|
// to avoid potential DST boundary issues if SetFields is sensitive to it.
|
|
// Assuming SetFields defaults them or handles 0 appropriately.
|
|
|
|
CLinearTimeAbsolute ltaJan1;
|
|
// SetFields likely calculates iDayOfWeek internally based on date.
|
|
// If SetFields fails for the given year, this function might return unexpected results.
|
|
// Assume SetFields is robust within reasonable year ranges.
|
|
if (!ltaJan1.SetFields(&ft)) {
|
|
// Handle error: Year might be invalid for SetFields. Return an error code.
|
|
return 0; // 0 indicates error/unknown type
|
|
}
|
|
|
|
// After SetFields, ft.iDayOfWeek should be populated.
|
|
if (isLeapYear(iYear))
|
|
{
|
|
// Original logic: 8-14 for leap years (Sun=0 -> 8, Sat=6 -> 14)
|
|
return ft.iDayOfWeek + 8;
|
|
}
|
|
else
|
|
{
|
|
// Original logic: 1-7 for non-leap years (Sun=0 -> 1, Sat=6 -> 7)
|
|
return ft.iDayOfWeek + 1;
|
|
}
|
|
// Ensure result is within 1-14 range? The logic assumes 0-6 input for iDayOfWeek.
|
|
}
|
|
|
|
|
|
// Helper for midpoint calculation to avoid overflow (from original)
|
|
time_t time_t_midpoint(time_t tLower, time_t tUpper)
|
|
{
|
|
// Be careful with subtraction near limits.
|
|
// Ensure tUpper >= tLower + 2 before subtraction.
|
|
if (tUpper < tLower + 2) {
|
|
return tLower; // Or handle as edge case, maybe return tLower+1 if tUpper==tLower+1?
|
|
}
|
|
// Calculate diff = (tUpper - 1) - (tLower + 1) = tUpper - tLower - 2
|
|
// Use unsigned arithmetic for division if intermediate diff can be large?
|
|
// Or rely on the fact that time_t diff should fit in time_t if operands are valid.
|
|
time_t tDiff = (tUpper - 1) - tLower; // Calculate diff carefully
|
|
return tLower + tDiff / 2 + 1;
|
|
}
|
|
|
|
|
|
// Finds the actual usable range of localtime and the standard offset
|
|
void perform_time_t_tests() {
|
|
CacheState& state = getState();
|
|
struct tm temp_tm;
|
|
|
|
// Use smallest/largest helpers from original code if available, otherwise use numeric_limits
|
|
// Assuming they handle platform specifics like TIMEUTIL_TIME_T_MAX_VALUE etc.
|
|
time_t time_min = time_t_smallest();
|
|
time_t time_max = time_t_largest();
|
|
|
|
// --- Search for the highest supported value ---
|
|
time_t upper_t = time_max;
|
|
time_t lower_t = 0;
|
|
time_t mid_t = 0;
|
|
time_t highest_valid = 0; // Initialize to 0
|
|
|
|
// Adjust initial range if 0 itself fails localtime
|
|
if (!safe_localtime(&lower_t, &temp_tm)) {
|
|
// If 0 fails, the valid range might not exist or start higher. Bail out?
|
|
// For now, assume 0 is valid as per original logic.
|
|
highest_valid = lower_t; // If 0 is the highest valid, unlikely but possible
|
|
}
|
|
else {
|
|
highest_valid = lower_t; // Start assuming 0 is valid
|
|
}
|
|
|
|
|
|
while (lower_t < upper_t) {
|
|
// Use careful midpoint logic
|
|
mid_t = time_t_midpoint(lower_t + 1, upper_t);
|
|
if (mid_t <= lower_t) break; // Avoid infinite loop if midpoint doesn't advance
|
|
|
|
if (safe_localtime(&mid_t, &temp_tm)) {
|
|
highest_valid = mid_t; // Found a new higher valid time
|
|
lower_t = mid_t; // Search in the upper half [mid_t, upper_t]
|
|
}
|
|
else {
|
|
upper_t = mid_t - 1; // Search in the lower half [lower_t, mid_t - 1]
|
|
}
|
|
}
|
|
// After loop, highest_valid holds the largest value for which safe_localtime succeeded.
|
|
state.upper_bound_lta.SetSeconds(highest_valid);
|
|
|
|
|
|
// --- Search for the lowest supported value ---
|
|
upper_t = 0; // Upper limit of search is now 0
|
|
lower_t = time_min;
|
|
mid_t = 0;
|
|
time_t lowest_valid = 0; // Initialize to 0
|
|
|
|
// Check if 0 is valid first, as it's the upper bound now
|
|
if (safe_localtime(&upper_t, &temp_tm)) {
|
|
lowest_valid = upper_t; // 0 is valid
|
|
}
|
|
else {
|
|
// If 0 isn't valid, something is odd. The original loop implies 0 should be tested.
|
|
// The original loop structure might be slightly different here. Let's re-check.
|
|
// Original: `while (tLower < tUpper) { tMid = time_t_midpoint(tLower, tUpper-1); ... }`
|
|
// This suggests the range is [tLower, tUpper-1].
|
|
// If 0 fails, the loop won't run if tLower starts >= 0.
|
|
// Let's stick to finding the lowest valid value >= time_min.
|
|
// Reset lowest_valid and proceed with search.
|
|
lowest_valid = 0; // Re-initialize potential lowest found
|
|
}
|
|
|
|
while (lower_t < upper_t) {
|
|
// Midpoint of [lower_t, upper_t - 1]
|
|
mid_t = time_t_midpoint(lower_t, upper_t - 1);
|
|
if (mid_t >= upper_t) break; // Should not happen if upper_t > lower_t
|
|
|
|
if (safe_localtime(&mid_t, &temp_tm)) {
|
|
lowest_valid = mid_t; // Found a new lower valid time
|
|
upper_t = mid_t; // Search in lower half [lower_t, mid_t]
|
|
}
|
|
else {
|
|
lower_t = mid_t + 1; // Search in upper half [mid_t + 1, upper_t]
|
|
}
|
|
}
|
|
// After loop, lowest_valid holds the smallest value >= time_min for which safe_localtime succeeded.
|
|
state.lower_bound_lta.SetSeconds(lowest_valid);
|
|
|
|
|
|
// --- Find standard offset near the lower bound ---
|
|
time_t current_t = lowest_valid;
|
|
CLinearTimeAbsolute current_lta;
|
|
bool standard_offset_found = false;
|
|
|
|
// Search forward from the lowest valid time
|
|
// Limit search iterations to avoid excessive checks if DST is always on/unknown
|
|
for (int i = 0; i < 24; ++i) { // Check approx 2 years worth of months max
|
|
current_lta.SetSeconds(current_t);
|
|
if (current_lta > state.upper_bound_lta) break; // Don't exceed upper bound
|
|
|
|
if (!safe_localtime(¤t_t, &temp_tm)) {
|
|
// Should not happen if current_t is within found bounds, but handle defensively
|
|
// Advance by approx 1 month and try again
|
|
current_t += 30 * 24 * 60 * 60; // Approx 1 month
|
|
continue;
|
|
}
|
|
|
|
if (temp_tm.tm_isdst <= 0) { // DST not in effect or unknown
|
|
FIELDEDTIME ft_local;
|
|
setFieldedTimeFromStructTm(&ft_local, &temp_tm);
|
|
|
|
CLinearTimeAbsolute lta_local;
|
|
// SetFields might fail if tm contains invalid date/time combinations
|
|
// But since it came from localtime, it should be valid. Assume success.
|
|
lta_local.SetFields(&ft_local);
|
|
|
|
CLinearTimeAbsolute lta_utc;
|
|
lta_utc.SetSeconds(current_t);
|
|
|
|
state.standard_offset_ltd = lta_local - lta_utc;
|
|
standard_offset_found = true;
|
|
break; // Found it
|
|
}
|
|
|
|
// Advance time by approx 1 month
|
|
// Use CLinearTimeDelta if available for safer time addition
|
|
// current_lta += CLinearTimeDelta(30 * 24 * 60 * 60);
|
|
// current_t = to_time_t(current_lta);
|
|
// Simpler: stick to time_t addition
|
|
current_t += 30 * 24 * 60 * 60; // Approx 1 month
|
|
}
|
|
|
|
// If no non-DST time found, standard_offset_ltd might remain uninitialized (default constructor).
|
|
// The original didn't explicitly handle this; it assumed it would find one.
|
|
// We might need a default fallback if the loop finishes without success.
|
|
if (!standard_offset_found) {
|
|
// Default to zero offset? Or log a warning?
|
|
state.standard_offset_ltd = CLinearTimeDelta(0); // Assign a default
|
|
// Log warning here if logging facility exists
|
|
}
|
|
}
|
|
|
|
// Fills the NearestYearOfType table
|
|
void populate_year_table() {
|
|
CacheState& state = getState();
|
|
|
|
FIELDEDTIME ft_upper;
|
|
// Get the year from the upper bound time
|
|
if (!state.upper_bound_lta.ReturnFields(&ft_upper)) {
|
|
// If ReturnFields fails (e.g., time is zero/invalid), use a fallback.
|
|
// Use a year known to be within typical 32-bit time_t limits.
|
|
ft_upper.iYear = 2037;
|
|
}
|
|
|
|
int start_year = ft_upper.iYear;
|
|
int types_found = 0;
|
|
const int total_types = 14; // 1-14
|
|
|
|
// Search backwards from the year before the upper bound year
|
|
// Limit search depth to avoid excessive loops if year types repeat rarely
|
|
for (int year = start_year - 1; types_found < total_types && year > start_year - 200; --year) {
|
|
int type = getYearType(year); // Needs CLinearTimeAbsolute/FIELDEDTIME
|
|
if (type >= 1 && type <= 14) { // Ensure type is valid (1-14)
|
|
if (state.nearest_year_of_type[type] == -1) { // Check if not already found
|
|
state.nearest_year_of_type[type] = static_cast<int16_t>(year);
|
|
types_found++;
|
|
}
|
|
}
|
|
// Add check for lower bound year? Stop searching if year goes below lower_bound_lta year?
|
|
// Might be useful if lower_bound_lta is significantly after year 0.
|
|
}
|
|
// Remaining types in nearest_year_of_type stay -1 if not found within search range.
|
|
}
|
|
|
|
// Initialization function (called via std::call_once)
|
|
void initialize_internal() {
|
|
CacheState& state = getState();
|
|
if (state.initialized) return;
|
|
|
|
#ifdef HAVE_TZSET
|
|
// Assuming mux_tzset() is the allowed interface from externs.h
|
|
mux_tzset();
|
|
#endif
|
|
|
|
perform_time_t_tests();
|
|
populate_year_table();
|
|
|
|
state.initialized = true;
|
|
}
|
|
|
|
|
|
// --- Cache Management Logic ---
|
|
|
|
// Finds iterator to cache entry whose start_lta <= lta, or end() if none.
|
|
std::vector<OffsetEntry>::iterator find_entry_iter(const CLinearTimeAbsolute& lta) {
|
|
CacheState& state = getState();
|
|
|
|
// lower_bound finds first element >= lta (when comparing element.start_lta < lta)
|
|
// We want the element *before* that, if it exists and its start <= lta.
|
|
auto it = std::lower_bound(state.offset_table.begin(), state.offset_table.end(), lta);
|
|
|
|
// If it == begin(), no element starts <= lta
|
|
if (it == state.offset_table.begin()) {
|
|
// Unless the first element *exactly* starts at lta
|
|
if (it != state.offset_table.end() && it->start_lta == lta) {
|
|
return it;
|
|
}
|
|
return state.offset_table.end(); // No entry starts at or before lta
|
|
}
|
|
|
|
// Otherwise, 'it' points to the first element > lta, or end().
|
|
// The element we want is the one *before* 'it'.
|
|
auto prev_it = std::prev(it);
|
|
|
|
// Check if this previous element actually starts at or before lta
|
|
if (prev_it->start_lta <= lta) {
|
|
return prev_it;
|
|
}
|
|
else {
|
|
// This case shouldn't happen with sorted data and lower_bound logic
|
|
// but handle defensively.
|
|
return state.offset_table.end();
|
|
}
|
|
}
|
|
|
|
|
|
// Updates the cache with a new data point (lta, offset, is_dst)
|
|
void update_offset_table(const CLinearTimeAbsolute& lta, const CLinearTimeDelta& offset, bool is_dst, const std::string& tz_name)
|
|
{
|
|
CacheState& state = getState();
|
|
// Lock the mutex for cache modification
|
|
std::lock_guard<std::mutex> lock(state.cache_mutex);
|
|
|
|
state.touch_counter++;
|
|
|
|
// Find iterator to entry potentially covering lta, or the one just preceding it.
|
|
auto it = find_entry_iter(lta);
|
|
|
|
// Case 1: Found an existing entry that covers this time point
|
|
if (it != state.offset_table.end() && lta >= it->start_lta && lta <= it->end_lta) {
|
|
// Check for consistency. If data matches, just update touch count.
|
|
if (it->offset_ltd == offset && it->is_dst == is_dst && it->tz_name == tz_name) {
|
|
it->touched_count = state.touch_counter;
|
|
return; // Cache hit, data consistent
|
|
}
|
|
else {
|
|
// Data mismatch within an interval! This indicates an issue.
|
|
// Original code didn't explicitly handle splitting.
|
|
// Simplest approach: Just update the touch count, acknowledging potential inaccuracy.
|
|
// A more robust approach would involve splitting the interval [start, lta-1] and [lta, end].
|
|
it->touched_count = state.touch_counter; // Update touch, but data is potentially stale
|
|
// Log warning here if logging exists.
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Case 2: lta falls outside existing intervals or between them.
|
|
// Need to potentially insert, extend, or merge.
|
|
|
|
bool merged_or_extended = false;
|
|
|
|
// Try extending the preceding entry ('it') if it exists, matches, and is close enough
|
|
if (it != state.offset_table.end() && // 'it' points to the entry starting <= lta
|
|
it->offset_ltd == offset && it->is_dst == is_dst &&
|
|
lta > it->end_lta && // Ensure lta is actually after the current end
|
|
lta <= it->end_lta + MIN_MERGE_INTERVAL)
|
|
{
|
|
it->end_lta = lta; // Extend end time
|
|
it->touched_count = state.touch_counter;
|
|
merged_or_extended = true;
|
|
}
|
|
|
|
// Try extending the succeeding entry backwards ('next_it') if it exists, matches, and is close enough
|
|
// 'next_it' is the element *after* 'it' (if 'it' is valid), or the beginning if 'it' was end().
|
|
auto next_it = (it == state.offset_table.end()) ? state.offset_table.begin() : std::next(it);
|
|
|
|
if (next_it != state.offset_table.end() &&
|
|
next_it->offset_ltd == offset && next_it->is_dst == is_dst &&
|
|
lta < next_it->start_lta && // Ensure lta is actually before the current start
|
|
next_it->start_lta <= lta + MIN_MERGE_INTERVAL) // Check closeness
|
|
{
|
|
if (merged_or_extended) {
|
|
// Already extended 'it'. Now check if 'it' and 'next_it' can merge.
|
|
// This happens if the gap between the newly extended 'it' and 'next_it' is small enough.
|
|
// Note: The check should be between it->end_lta and next_it->start_lta
|
|
if (next_it->start_lta <= it->end_lta + MIN_MERGE_INTERVAL) {
|
|
// Merge 'next_it' into 'it'
|
|
it->end_lta = next_it->end_lta;
|
|
it->touched_count = state.touch_counter; // Update touch count again
|
|
state.offset_table.erase(next_it); // Remove the merged 'next_it'
|
|
}
|
|
// If they can't merge after extension, merged_or_extended remains true,
|
|
// but we don't modify next_it here.
|
|
}
|
|
else {
|
|
// Only potentially extend 'next_it' backwards
|
|
next_it->start_lta = lta;
|
|
next_it->touched_count = state.touch_counter;
|
|
merged_or_extended = true;
|
|
}
|
|
}
|
|
|
|
// Case 3: No merge or extension happened, insert a new point/interval.
|
|
if (!merged_or_extended) {
|
|
// Evict LRU entry if cache is full
|
|
if (state.offset_table.size() >= MAX_OFFSETS) {
|
|
auto lru_it = std::min_element(state.offset_table.begin(), state.offset_table.end(),
|
|
[](const OffsetEntry& a, const OffsetEntry& b) {
|
|
return a.touched_count < b.touched_count;
|
|
});
|
|
// Erase the LRU element. Need to be careful if it affects iterators,
|
|
// but since we re-find the insertion point, it's okay.
|
|
if (lru_it != state.offset_table.end()) { // Ensure not trying to erase end()
|
|
state.offset_table.erase(lru_it);
|
|
}
|
|
}
|
|
|
|
// Find correct insertion position *again* after potential eviction, to maintain sort order.
|
|
auto insert_pos = std::lower_bound(state.offset_table.begin(), state.offset_table.end(), lta);
|
|
|
|
// Insert the new entry as a single point interval [lta, lta]
|
|
state.offset_table.insert(insert_pos, { lta, lta, offset, state.touch_counter, is_dst, tz_name });
|
|
|
|
// After insertion, could we now merge this new entry with neighbors?
|
|
// This logic can get complex. The original code handled merging after extending.
|
|
// Let's stick to the extend/merge logic *before* insertion for simplicity,
|
|
// matching the original's apparent strategy more closely.
|
|
// Re-checking merges post-insertion would require finding the inserted element
|
|
// and its neighbors again.
|
|
}
|
|
// The logic here tries to mirror the original's merge approach.
|
|
// Revisit if specific merge scenarios aren't handled correctly.
|
|
}
|
|
|
|
// --- Core Query Logic ---
|
|
|
|
// Internal query: Performs the actual time conversion and calculation
|
|
CLinearTimeDelta queryLocalOffsetAt_Internal(CLinearTimeAbsolute utc_lta, bool* is_dst)
|
|
{
|
|
CacheState& state = getState();
|
|
*is_dst = false; // Default
|
|
|
|
CLinearTimeAbsolute query_lta = utc_lta; // The time point used for localtime query, possibly mapped
|
|
|
|
// Handle times beyond the reliable upper bound using year mapping (Original Logic)
|
|
if (query_lta > state.upper_bound_lta) {
|
|
FIELDEDTIME ft_query;
|
|
if (!query_lta.ReturnFields(&ft_query)) {
|
|
// Failed to get fields, cannot map year. Return standard offset.
|
|
return state.standard_offset_ltd;
|
|
}
|
|
|
|
int original_year = ft_query.iYear;
|
|
int year_type = getYearType(original_year);
|
|
int mapped_year = -1;
|
|
|
|
if (year_type >= 1 && year_type <= 14) {
|
|
mapped_year = state.nearest_year_of_type[year_type];
|
|
}
|
|
|
|
if (mapped_year != -1 && mapped_year >= 1) { // Ensure mapped year is valid
|
|
// Modify the fielded time to use the mapped year
|
|
ft_query.iYear = static_cast<short>(mapped_year);
|
|
// Use SetFields to get the CLinearTimeAbsolute for the mapped date/time
|
|
// This recalculates the absolute time based on the new year.
|
|
CLinearTimeAbsolute mapped_lta;
|
|
if (!mapped_lta.SetFields(&ft_query)) {
|
|
// SetFields failed for the mapped year, fallback.
|
|
return state.standard_offset_ltd;
|
|
}
|
|
query_lta = mapped_lta;
|
|
|
|
}
|
|
else {
|
|
// No valid mapped year found, can only return the standard offset guess.
|
|
return state.standard_offset_ltd;
|
|
}
|
|
}
|
|
|
|
// Ensure query_lta is within detected bounds after potential mapping.
|
|
// Clamping might be needed if year mapping produced a time outside bounds.
|
|
if (query_lta < state.lower_bound_lta) query_lta = state.lower_bound_lta;
|
|
// The upper bound check might be redundant if mapping always targets years below upper_bound_lta,
|
|
// but keep for safety.
|
|
if (query_lta > state.upper_bound_lta) query_lta = state.upper_bound_lta;
|
|
|
|
// Use safe_localtime with the seconds from the (potentially mapped) query_lta
|
|
time_t query_t = to_time_t(query_lta);
|
|
struct tm local_tm;
|
|
if (!safe_localtime(&query_t, &local_tm)) {
|
|
// localtime failed even within bounds - rare. Return standard offset.
|
|
return state.standard_offset_ltd;
|
|
}
|
|
|
|
*is_dst = (local_tm.tm_isdst > 0);
|
|
|
|
// Capture timezone name from the localtime result
|
|
char tz_buf[64];
|
|
strftime(tz_buf, sizeof(tz_buf), "%Z", &local_tm);
|
|
std::string tz_name_str(tz_buf);
|
|
|
|
// Calculate the offset: Local time represented by local_tm - UTC time represented by query_lta
|
|
FIELDEDTIME ft_local;
|
|
setFieldedTimeFromStructTm(&ft_local, &local_tm);
|
|
|
|
CLinearTimeAbsolute lta_local;
|
|
if (!lta_local.SetFields(&ft_local)) {
|
|
// Should not fail if ft_local came from valid tm, but handle defensively.
|
|
return state.standard_offset_ltd; // Fallback
|
|
}
|
|
|
|
// The UTC time corresponding to the query we made
|
|
CLinearTimeAbsolute lta_utc_query;
|
|
lta_utc_query.SetSeconds(query_t); // Use the actual time_t used for the query
|
|
|
|
CLinearTimeDelta offset = lta_local - lta_utc_query;
|
|
|
|
// Update the cache with the result for the *original* utc_lta
|
|
// Note: update_offset_table handles locking internally
|
|
update_offset_table(utc_lta, offset, *is_dst, tz_name_str);
|
|
return offset;
|
|
}
|
|
|
|
} // namespace Detail
|
|
|
|
// --- Public API ---
|
|
|
|
// Initialize the timezone cache system (thread-safe)
|
|
void initialize() {
|
|
std::call_once(Detail::getState().init_flag, Detail::initialize_internal);
|
|
}
|
|
|
|
// Query the local time offset from UTC at a specific UTC time point
|
|
CLinearTimeDelta queryLocalOffsetAtUTC(const CLinearTimeAbsolute& utc_lta, bool* is_dst, std::string* tz_name)
|
|
{
|
|
initialize(); // Ensure initialized (call_once handles subsequent calls)
|
|
|
|
Detail::CacheState& state = Detail::getState();
|
|
*is_dst = false; // Default
|
|
if (tz_name) tz_name->clear();
|
|
|
|
// Handle times before the known lower bound (assume standard offset, no DST)
|
|
if (utc_lta < state.lower_bound_lta) {
|
|
return state.standard_offset_ltd;
|
|
}
|
|
|
|
CLinearTimeDelta offset_result;
|
|
bool dst_result;
|
|
std::string name_result;
|
|
bool found_in_cache = false;
|
|
|
|
{ // Scope for cache read lock
|
|
std::lock_guard<std::mutex> lock(state.cache_mutex);
|
|
state.touch_counter++; // Increment touch counter even for reads to help LRU? Maybe only on hit.
|
|
|
|
auto it = Detail::find_entry_iter(utc_lta);
|
|
|
|
if (it != state.offset_table.end() && utc_lta >= it->start_lta && utc_lta <= it->end_lta) {
|
|
// Cache hit!
|
|
offset_result = it->offset_ltd;
|
|
dst_result = it->is_dst;
|
|
name_result = it->tz_name;
|
|
it->touched_count = state.touch_counter; // Update LRU counter on hit
|
|
found_in_cache = true;
|
|
}
|
|
} // Release cache lock
|
|
|
|
if (found_in_cache) {
|
|
*is_dst = dst_result;
|
|
if (tz_name) *tz_name = name_result;
|
|
return offset_result;
|
|
}
|
|
else {
|
|
// Cache miss: Call the internal query logic.
|
|
// This internal call will perform the localtime query and update the cache (including locking).
|
|
offset_result = Detail::queryLocalOffsetAt_Internal(utc_lta, is_dst);
|
|
|
|
// Probe nearby times to pre-populate the cache so that subsequent
|
|
// queries within this time range are cache hits. Without this,
|
|
// the cache stores a point interval [lta, lta] and the very next
|
|
// second is another miss.
|
|
//
|
|
bool dont_care_dst;
|
|
CLinearTimeAbsolute probe_before = utc_lta - Detail::MIN_MERGE_INTERVAL;
|
|
if (probe_before >= state.lower_bound_lta) {
|
|
Detail::queryLocalOffsetAt_Internal(probe_before, &dont_care_dst);
|
|
}
|
|
CLinearTimeAbsolute probe_after = utc_lta + Detail::MIN_MERGE_INTERVAL;
|
|
Detail::queryLocalOffsetAt_Internal(probe_after, &dont_care_dst);
|
|
|
|
// Read the timezone name back from the newly-cached entry
|
|
if (tz_name) {
|
|
std::lock_guard<std::mutex> lock(state.cache_mutex);
|
|
auto it = Detail::find_entry_iter(utc_lta);
|
|
if (it != state.offset_table.end() && utc_lta >= it->start_lta && utc_lta <= it->end_lta) {
|
|
*tz_name = it->tz_name;
|
|
}
|
|
}
|
|
|
|
return offset_result;
|
|
}
|
|
}
|
|
|
|
// Helper to get the current offset
|
|
// Requires CLinearTimeAbsolute to have a method to get current UTC time.
|
|
// Assuming a static method or a constructor. Let's assume GetUTC().
|
|
CLinearTimeDelta getCurrentLocalOffset(bool* is_dst) {
|
|
// Assuming CLinearTimeAbsolute has a way to get current time, e.g., static GetUTC()
|
|
// or default constructor initializes to now, or a SetNow() method.
|
|
// Adjust based on actual CLinearTimeAbsolute interface.
|
|
// Example: CLinearTimeAbsolute ltaNow = CLinearTimeAbsolute::GetUTC();
|
|
// Example: CLinearTimeAbsolute ltaNow; // If default constructor is current time
|
|
// Example: CLinearTimeAbsolute ltaNow; ltaNow.SetUTC(); // If requires explicit set
|
|
|
|
// Placeholder - replace with actual mechanism for getting current time
|
|
CLinearTimeAbsolute ltaNow;
|
|
#ifdef HAVE_CLINEARTIMEABSOLUTE_SETUTC // Example hypothetical check
|
|
ltaNow.SetUTC();
|
|
#else
|
|
// Fallback: Use time() if absolutely necessary and allowed as lowest common denominator
|
|
// This adds a slight dependency but might be unavoidable if CLinearTimeAbsolute can't get 'now'.
|
|
time_t now_t;
|
|
time(&now_t); // Standard C function to get current time_t
|
|
ltaNow.SetSeconds(now_t);
|
|
#endif
|
|
|
|
return queryLocalOffsetAtUTC(ltaNow, is_dst);
|
|
}
|
|
} // namespace TimezoneCache
|