flowsurface_data/config/
timezone.rs1use chrono::{DateTime, Datelike, Months, TimeZone};
2use serde::{Deserialize, Serialize};
3use std::fmt;
4
5const ORIGIN_YEAR: i32 = 2000;
6
7#[derive(Debug, Clone, Copy, PartialEq, Default)]
8pub enum UserTimezone {
9 #[default]
10 Utc,
11 Local,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum TimeLabelKind<'a> {
18 Axis { timeframe: exchange::Timeframe },
21 AxisStepMs { step_ms: u64 },
23 Crosshair { show_millis: bool },
27 Custom(&'a str),
29}
30
31impl UserTimezone {
32 pub fn to_user_datetime(
33 &self,
34 datetime: DateTime<chrono::Utc>,
35 ) -> DateTime<chrono::FixedOffset> {
36 self.with_user_timezone(datetime, |time_with_zone| time_with_zone)
37 }
38
39 pub fn format_with_kind(&self, timestamp_ms: i64, kind: TimeLabelKind<'_>) -> Option<String> {
41 DateTime::from_timestamp_millis(timestamp_ms).map(|datetime| {
42 self.with_user_timezone(datetime, |time_with_zone| match kind {
43 TimeLabelKind::Axis { timeframe } => {
44 Self::format_by_timeframe(&time_with_zone, timeframe)
45 }
46 TimeLabelKind::AxisStepMs { step_ms } => {
47 Self::format_by_step_ms(&time_with_zone, step_ms)
48 }
49 TimeLabelKind::Crosshair { show_millis } => {
50 if show_millis {
51 time_with_zone.format("%H:%M:%S.%3f").to_string()
52 } else {
53 time_with_zone.format("%a %b %-d %H:%M").to_string()
54 }
55 }
56 TimeLabelKind::Custom(fmt) => time_with_zone.format(fmt).to_string(),
57 })
58 })
59 }
60
61 fn with_user_timezone<T>(
64 &self,
65 datetime: DateTime<chrono::Utc>,
66 formatter: impl FnOnce(DateTime<chrono::FixedOffset>) -> T,
67 ) -> T {
68 let time_with_zone = match self {
69 UserTimezone::Local => datetime.with_timezone(&chrono::Local).fixed_offset(),
70 UserTimezone::Utc => datetime.fixed_offset(),
71 };
72
73 formatter(time_with_zone)
74 }
75
76 fn format_by_timeframe(
82 datetime: &DateTime<chrono::FixedOffset>,
83 timeframe: exchange::Timeframe,
84 ) -> String {
85 let interval = timeframe.to_milliseconds();
86
87 if interval < 10_000 {
88 datetime.format("%M:%S").to_string()
89 } else {
90 datetime.format("%H:%M").to_string()
91 }
92 }
93
94 fn format_by_step_ms(datetime: &DateTime<chrono::FixedOffset>, step_ms: u64) -> String {
95 const S: u64 = 1_000;
96 const M: u64 = 60 * S;
97 const H: u64 = 60 * M;
98 const D: u64 = 24 * H;
99
100 if step_ms < M {
101 datetime.format("%H:%M:%S").to_string()
102 } else if step_ms < D {
103 datetime.format("%H:%M").to_string()
104 } else if step_ms < 7 * D {
105 datetime.format("%b %d").to_string()
106 } else if step_ms < 365 * D {
107 datetime.format("%Y-%m").to_string()
108 } else {
109 datetime.format("%Y").to_string()
110 }
111 }
112
113 fn local_date(&self, datetime: DateTime<chrono::Utc>) -> chrono::NaiveDate {
115 match self {
116 UserTimezone::Utc => datetime.date_naive(),
117 UserTimezone::Local => datetime.with_timezone(&chrono::Local).date_naive(),
118 }
119 }
120
121 pub(crate) fn local_midnight_utc_ms(&self, date: chrono::NaiveDate) -> Option<u64> {
125 let midnight = date.and_hms_opt(0, 0, 0)?;
126 let utc = match self {
127 UserTimezone::Utc => midnight.and_utc(),
128 UserTimezone::Local => chrono::Local
129 .from_local_datetime(&midnight)
130 .earliest()?
131 .with_timezone(&chrono::Utc),
132 };
133 utc.timestamp_millis().try_into().ok()
134 }
135
136 pub(crate) fn start_of_local_day_utc_ms(&self, timestamp_ms: u64) -> Option<u64> {
138 let datetime = DateTime::from_timestamp_millis(timestamp_ms as i64)?;
139 self.local_midnight_utc_ms(self.local_date(datetime))
140 }
141
142 pub(crate) fn start_of_local_month_utc_ms(&self, timestamp_ms: u64) -> Option<u64> {
144 let datetime = DateTime::from_timestamp_millis(timestamp_ms as i64)?;
145 self.local_midnight_utc_ms(self.local_date(datetime).with_day(1)?)
146 }
147
148 pub(crate) fn start_of_local_year_utc_ms(&self, timestamp_ms: u64) -> Option<u64> {
150 let datetime = DateTime::from_timestamp_millis(timestamp_ms as i64)?;
151 self.local_midnight_utc_ms(self.local_date(datetime).with_month(1)?.with_day(1)?)
152 }
153
154 pub(crate) fn next_local_day_utc_ms(&self, timestamp_ms: u64) -> Option<u64> {
156 let datetime = DateTime::from_timestamp_millis(timestamp_ms as i64)?;
157 self.local_midnight_utc_ms(self.local_date(datetime).succ_opt()?)
158 }
159
160 pub(crate) fn next_local_month_utc_ms(&self, timestamp_ms: u64) -> Option<u64> {
162 let datetime = DateTime::from_timestamp_millis(timestamp_ms as i64)?;
163 self.local_midnight_utc_ms(
164 self.local_date(datetime)
165 .checked_add_months(Months::new(1))?,
166 )
167 }
168
169 pub(crate) fn next_local_year_utc_ms(&self, timestamp_ms: u64) -> Option<u64> {
171 let datetime = DateTime::from_timestamp_millis(timestamp_ms as i64)?;
172 self.local_midnight_utc_ms(
173 self.local_date(datetime)
174 .checked_add_months(Months::new(12))?,
175 )
176 }
177
178 pub(crate) fn align_to_skip_grid(&self, timestamp_ms: u64, skip_days: u64) -> Option<u64> {
186 let origin = chrono::NaiveDate::from_ymd_opt(ORIGIN_YEAR, 1, 1)?;
187 let datetime = DateTime::from_timestamp_millis(timestamp_ms as i64)?;
188 let date = self.local_date(datetime);
189 let days = date.signed_duration_since(origin).num_days();
190 let advance = advance_to_grid(skip_days, days);
191 self.local_midnight_utc_ms(date.checked_add_days(chrono::Days::new(advance))?)
192 }
193
194 pub(crate) fn align_to_month_grid(&self, timestamp_ms: u64, step_months: u64) -> Option<u64> {
197 let datetime = DateTime::from_timestamp_millis(timestamp_ms as i64)?;
198 let date = self.local_date(datetime);
199 let months = (date.year() - ORIGIN_YEAR) as i64 * 12 + i64::from(date.month0());
200 let first = date.with_day(1)?;
201 self.local_midnight_utc_ms(
202 first.checked_add_months(Months::new(advance_to_grid(step_months, months) as u32))?,
203 )
204 }
205
206 pub(crate) fn align_to_year_grid(&self, timestamp_ms: u64, step_years: u64) -> Option<u64> {
209 let datetime = DateTime::from_timestamp_millis(timestamp_ms as i64)?;
210 let date = self.local_date(datetime);
211 let years = (date.year() - ORIGIN_YEAR) as i64;
212 let first = date.with_month(1)?.with_day(1)?;
213 self.local_midnight_utc_ms(first.checked_add_months(Months::new(
214 (advance_to_grid(step_years, years) * 12) as u32,
215 ))?)
216 }
217}
218
219fn advance_to_grid(step: u64, offset: i64) -> u64 {
222 let step = step.max(1) as i64;
223 ((step - offset.rem_euclid(step)) % step) as u64
224}
225
226impl fmt::Display for UserTimezone {
227 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228 match self {
229 UserTimezone::Utc => write!(f, "UTC"),
230 UserTimezone::Local => {
231 let local_offset = chrono::Local::now().offset().local_minus_utc();
232 let hours = local_offset / 3600;
233 let minutes = (local_offset % 3600) / 60;
234 write!(f, "Local (UTC {hours:+03}:{minutes:02})")
235 }
236 }
237 }
238}
239
240impl<'de> Deserialize<'de> for UserTimezone {
241 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
242 where
243 D: serde::Deserializer<'de>,
244 {
245 let timezone_str = String::deserialize(deserializer)?;
246 match timezone_str.to_lowercase().as_str() {
247 "utc" => Ok(UserTimezone::Utc),
248 "local" => Ok(UserTimezone::Local),
249 _ => Err(serde::de::Error::custom("Invalid UserTimezone")),
250 }
251 }
252}
253
254impl Serialize for UserTimezone {
255 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
256 where
257 S: serde::Serializer,
258 {
259 match self {
260 UserTimezone::Utc => serializer.serialize_str("UTC"),
261 UserTimezone::Local => serializer.serialize_str("Local"),
262 }
263 }
264}