Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_datime/src/cache/timezone_cache.rs

7.7 KiB, 32 runs

created by r1870400018:8327, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Timezone calculation caching for performance optimisation.
2//!
3//! Offset calculations and DST transitions are expensive, and the same values
4//! are asked for repeatedly.
5//!
6//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
7//! Anthropic Claude
8
9use crate::{
10 cache::LruCache,
11 time::CalClockZone,
12};
13
14use oxedyne_fe2o3_core::prelude::*;
15
16use std::{
17 sync::{Arc, OnceLock},
18 collections::HashMap,
19};
20
21#[derive(Debug, Clone, Hash, PartialEq, Eq)]
22struct TimezoneOffsetKey {
23 timezone_id: String,
24 timestamp_millis: i64,
25}
26
27#[derive(Debug, Clone, Hash, PartialEq, Eq)]
28struct DstCalculationKey {
29 timezone_id: String,
30 year: i32,
31 timestamp_millis: i64,
32}
33
34#[derive(Debug, Clone)]
35pub struct TimezoneCache {
36 offset_cache: LruCache<TimezoneOffsetKey, i32>,
37 dst_cache: LruCache<DstCalculationKey, bool>,
38 name_cache: Arc<HashMap<String, String>>,
39}
40
41static TIMEZONE_CACHE: OnceLock<TimezoneCache> = OnceLock::new();
42
43impl TimezoneCache {
44 fn new() -> Self {
45 Self {
46 offset_cache: LruCache::new(1000), // Cache up to 1000 offset calculations
47 dst_cache: LruCache::new(500), // Cache up to 500 DST calculations
48 name_cache: Arc::new(Self::build_name_cache()),
49 }
50 }
51
52 pub fn global() -> &'static TimezoneCache {
53 TIMEZONE_CACHE.get_or_init(|| TimezoneCache::new())
54 }
55
56 fn build_name_cache() -> HashMap<String, String> {
57 let mut cache = HashMap::new();
58
59 // Common timezone abbreviations
60 cache.insert("America/New_York".to_string(), "EST".to_string());
61 cache.insert("America/Chicago".to_string(), "CST".to_string());
62 cache.insert("America/Denver".to_string(), "MST".to_string());
63 cache.insert("America/Los_Angeles".to_string(), "PST".to_string());
64 cache.insert("Europe/London".to_string(), "GMT".to_string());
65 cache.insert("Europe/Paris".to_string(), "CET".to_string());
66 cache.insert("Europe/Berlin".to_string(), "CET".to_string());
67 cache.insert("Asia/Tokyo".to_string(), "JST".to_string());
68 cache.insert("Asia/Shanghai".to_string(), "CST".to_string());
69 cache.insert("Australia/Sydney".to_string(), "AEDT".to_string());
70 cache.insert("UTC".to_string(), "UTC".to_string());
71 cache.insert("GMT".to_string(), "GMT".to_string());
72
73 cache
74 }
75
76 pub fn get_timezone_offset_cached<F>(&self, zone: &CalClockZone, timestamp_millis: i64, compute_fn: F) -> i32
77 where
78 F: FnOnce() -> i32,
79 {
80 let key = TimezoneOffsetKey {
81 timezone_id: zone.id().to_string(),
82 timestamp_millis,
83 };
84
85 if let Some(cached_offset) = self.offset_cache.get(&key) {
86 cached_offset
87 } else {
88 let offset = compute_fn();
89 self.offset_cache.insert(key, offset);
90 offset
91 }
92 }
93
94 pub fn get_dst_cached<F>(&self, zone: &CalClockZone, year: i32, timestamp_millis: i64, compute_fn: F) -> bool
95 where
96 F: FnOnce() -> bool,
97 {
98 let key = DstCalculationKey {
99 timezone_id: zone.id().to_string(),
100 year,
101 timestamp_millis,
102 };
103
104 if let Some(cached_dst) = self.dst_cache.get(&key) {
105 cached_dst
106 } else {
107 let is_dst = compute_fn();
108 self.dst_cache.insert(key, is_dst);
109 is_dst
110 }
111 }
112
113 pub fn get_timezone_name(&self, timezone_id: &str) -> Option<String> {
114 self.name_cache.get(timezone_id).cloned()
115 }
116
117 pub fn stats(&self) -> TimezoneStats {
118 let (offset_hits, offset_misses, offset_ratio) = self.offset_cache.stats();
119 let (dst_hits, dst_misses, dst_ratio) = self.dst_cache.stats();
120
121 TimezoneStats {
122 offset_cache_hits: offset_hits,
123 offset_cache_misses: offset_misses,
124 offset_cache_hit_ratio: offset_ratio,
125 dst_cache_hits: dst_hits,
126 dst_cache_misses: dst_misses,
127 dst_cache_hit_ratio: dst_ratio,
128 name_cache_size: self.name_cache.len(),
129 }
130 }
131
132 pub fn clear(&self) {
133 self.offset_cache.clear();
134 self.dst_cache.clear();
135 }
136
137 pub fn preload_common_timezones(&self) {
138 // This could be enhanced to preload common timezone calculations
139 // for the current year and next year during application startup
140 // For now, it's a placeholder for future optimisation
141 }
142}
143
144#[derive(Debug, Clone)]
145pub struct TimezoneStats {
146 pub offset_cache_hits: u64,
147 pub offset_cache_misses: u64,
148 pub offset_cache_hit_ratio: f64,
149 pub dst_cache_hits: u64,
150 pub dst_cache_misses: u64,
151 pub dst_cache_hit_ratio: f64,
152 pub name_cache_size: usize,
153}
154
155impl TimezoneStats {
156 /// A percentage, not a ratio.
157 pub fn overall_efficiency(&self) -> f64 {
158 let total_hits = self.offset_cache_hits + self.dst_cache_hits;
159 let total_requests = total_hits + self.offset_cache_misses + self.dst_cache_misses;
160
161 if total_requests > 0 {
162 (total_hits as f64 / total_requests as f64) * 100.0
163 } else {
164 0.0
165 }
166 }
167
168 /// Well means a hit rate of 80% or better.
169 pub fn is_performing_well(&self) -> bool {
170 self.overall_efficiency() >= 80.0
171 }
172}
173
174pub trait CalClockZoneCached {
175 fn offset_millis_at_time_cached(&self, timestamp_millis: i64) -> Outcome<i32>;
176
177 fn in_daylight_time_cached(&self, timestamp_millis: i64) -> Outcome<bool>;
178
179 fn name_cached(&self) -> Option<String>;
180}
181
182impl CalClockZoneCached for CalClockZone {
183 fn offset_millis_at_time_cached(&self, timestamp_millis: i64) -> Outcome<i32> {
184 let cache = TimezoneCache::global();
185
186 Ok(cache.get_timezone_offset_cached(self, timestamp_millis, || {
187 // Fallback to original calculation if cache fails
188 self.offset_millis_at_time(timestamp_millis).unwrap_or(0)
189 }))
190 }
191
192 fn in_daylight_time_cached(&self, timestamp_millis: i64) -> Outcome<bool> {
193 let cache = TimezoneCache::global();
194
195 // Extract year from timestamp for cache key
196 let year = {
197 let days_since_epoch = timestamp_millis / (24 * 60 * 60 * 1000);
198 let epoch_year = 1970;
199 // Rough approximation - could be more accurate
200 epoch_year + (days_since_epoch / 365) as i32
201 };
202
203 Ok(cache.get_dst_cached(self, year, timestamp_millis, || {
204 // Fallback to original calculation if cache fails
205 self.in_daylight_time(timestamp_millis).unwrap_or(false)
206 }))
207 }
208
209 fn name_cached(&self) -> Option<String> {
210 let cache = TimezoneCache::global();
211 cache.get_timezone_name(self.id())
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use crate::time::CalClockZone;
219
220 #[test]
221 fn test_timezone_cache_basic() {
222 let cache = TimezoneCache::new();
223 let utc_zone = CalClockZone::utc();
224
225 // Test offset caching
226 let timestamp = 1640995200000; // 2022-01-01 00:00:00 UTC
227 let offset1 = cache.get_timezone_offset_cached(&utc_zone, timestamp, || 0);
228 let offset2 = cache.get_timezone_offset_cached(&utc_zone, timestamp, || 999); // Should not be called
229
230 assert_eq!(offset1, 0);
231 assert_eq!(offset2, 0); // Should come from cache
232 }
233
234 #[test]
235 fn test_timezone_name_cache() {
236 let cache = TimezoneCache::new();
237
238 // Test known timezone
239 assert_eq!(cache.get_timezone_name("America/New_York"), Some("EST".to_string()));
240 assert_eq!(cache.get_timezone_name("Europe/London"), Some("GMT".to_string()));
241
242 // Test unknown timezone
243 assert_eq!(cache.get_timezone_name("Unknown/Timezone"), None);
244 }
245
246 #[test]
247 fn test_timezone_stats() {
248 let cache = TimezoneCache::new();
249 let utc_zone = CalClockZone::utc();
250
251 // Generate some cache activity
252 let _ = cache.get_timezone_offset_cached(&utc_zone, 1000, || 0);
253 let _ = cache.get_timezone_offset_cached(&utc_zone, 1000, || 0); // Cache hit
254 let _ = cache.get_timezone_offset_cached(&utc_zone, 2000, || 0); // Cache miss
255
256 let stats = cache.stats();
257 assert_eq!(stats.offset_cache_hits, 1);
258 assert_eq!(stats.offset_cache_misses, 2);
259 assert_eq!(stats.offset_cache_hit_ratio, 1.0 / 3.0);
260 }
261
262 #[test]
263 fn test_cached_zone_extension() -> Outcome<()> {
264 let utc_zone = CalClockZone::utc();
265
266 // Test cached offset calculation
267 let offset = res!(utc_zone.offset_millis_at_time_cached(1640995200000));
268 assert_eq!(offset, 0);
269
270 // Test cached name lookup
271 let name = utc_zone.name_cached();
272 assert_eq!(name, Some("UTC".to_string()));
273
274 Ok(())
275 }
276}