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 Crosshair { show_millis: bool },
25 Custom(&'a str),
27}
28
29impl UserTimezone {
30 pub fn to_user_datetime(
31 &self,
32 datetime: DateTime<chrono::Utc>,
33 ) -> DateTime<chrono::FixedOffset> {
34 self.with_user_timezone(datetime, |time_with_zone| time_with_zone)
35 }
36
37 pub fn format_with_kind(&self, timestamp_ms: i64, kind: TimeLabelKind<'_>) -> Option<String> {
39 DateTime::from_timestamp_millis(timestamp_ms).map(|datetime| {
40 self.with_user_timezone(datetime, |time_with_zone| match kind {
41 TimeLabelKind::Axis { timeframe } => {
42 Self::format_by_timeframe(&time_with_zone, timeframe)
43 }
44 TimeLabelKind::Crosshair { show_millis } => {
45 if show_millis {
46 time_with_zone.format("%H:%M:%S.%3f").to_string()
47 } else {
48 time_with_zone.format("%a %b %-d %H:%M").to_string()
49 }
50 }
51 TimeLabelKind::Custom(fmt) => time_with_zone.format(fmt).to_string(),
52 })
53 })
54 }
55
56 fn with_user_timezone<T>(
59 &self,
60 datetime: DateTime<chrono::Utc>,
61 formatter: impl FnOnce(DateTime<chrono::FixedOffset>) -> T,
62 ) -> T {
63 let time_with_zone = match self {
64 UserTimezone::Local => datetime.with_timezone(&chrono::Local).fixed_offset(),
65 UserTimezone::Utc => datetime.fixed_offset(),
66 };
67
68 formatter(time_with_zone)
69 }
70
71 fn format_by_timeframe(
77 datetime: &DateTime<chrono::FixedOffset>,
78 timeframe: exchange::Timeframe,
79 ) -> String {
80 let interval = timeframe.to_milliseconds();
81
82 if interval < 10_000 {
83 datetime.format("%M:%S").to_string()
84 } else {
85 datetime.format("%H:%M").to_string()
86 }
87 }
88
89 fn local_date(&self, datetime: DateTime<chrono::Utc>) -> chrono::NaiveDate {
91 match self {
92 UserTimezone::Utc => datetime.date_naive(),
93 UserTimezone::Local => datetime.with_timezone(&chrono::Local).date_naive(),
94 }
95 }
96
97 pub(crate) fn local_midnight_utc_ms(&self, date: chrono::NaiveDate) -> Option<u64> {
101 let midnight = date.and_hms_opt(0, 0, 0)?;
102 let utc = match self {
103 UserTimezone::Utc => midnight.and_utc(),
104 UserTimezone::Local => chrono::Local
105 .from_local_datetime(&midnight)
106 .earliest()?
107 .with_timezone(&chrono::Utc),
108 };
109 utc.timestamp_millis().try_into().ok()
110 }
111
112 pub(crate) fn start_of_local_day_utc_ms(&self, timestamp_ms: u64) -> Option<u64> {
114 let datetime = DateTime::from_timestamp_millis(timestamp_ms as i64)?;
115 self.local_midnight_utc_ms(self.local_date(datetime))
116 }
117
118 pub(crate) fn start_of_local_month_utc_ms(&self, timestamp_ms: u64) -> Option<u64> {
120 let datetime = DateTime::from_timestamp_millis(timestamp_ms as i64)?;
121 self.local_midnight_utc_ms(self.local_date(datetime).with_day(1)?)
122 }
123
124 pub(crate) fn start_of_local_year_utc_ms(&self, timestamp_ms: u64) -> Option<u64> {
126 let datetime = DateTime::from_timestamp_millis(timestamp_ms as i64)?;
127 self.local_midnight_utc_ms(self.local_date(datetime).with_month(1)?.with_day(1)?)
128 }
129
130 pub(crate) fn next_local_day_utc_ms(&self, timestamp_ms: u64) -> Option<u64> {
132 let datetime = DateTime::from_timestamp_millis(timestamp_ms as i64)?;
133 self.local_midnight_utc_ms(self.local_date(datetime).succ_opt()?)
134 }
135
136 pub(crate) fn next_local_month_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(
140 self.local_date(datetime)
141 .checked_add_months(Months::new(1))?,
142 )
143 }
144
145 pub(crate) fn next_local_year_utc_ms(&self, timestamp_ms: u64) -> Option<u64> {
147 let datetime = DateTime::from_timestamp_millis(timestamp_ms as i64)?;
148 self.local_midnight_utc_ms(
149 self.local_date(datetime)
150 .checked_add_months(Months::new(12))?,
151 )
152 }
153
154 pub(crate) fn align_to_skip_grid(&self, timestamp_ms: u64, skip_days: u64) -> Option<u64> {
162 let origin = chrono::NaiveDate::from_ymd_opt(ORIGIN_YEAR, 1, 1)?;
163 let datetime = DateTime::from_timestamp_millis(timestamp_ms as i64)?;
164 let date = self.local_date(datetime);
165 let days = date.signed_duration_since(origin).num_days();
166 let advance = advance_to_grid(skip_days, days);
167 self.local_midnight_utc_ms(date.checked_add_days(chrono::Days::new(advance))?)
168 }
169
170 pub(crate) fn align_to_month_grid(&self, timestamp_ms: u64, step_months: u64) -> Option<u64> {
173 let datetime = DateTime::from_timestamp_millis(timestamp_ms as i64)?;
174 let date = self.local_date(datetime);
175 let months = (date.year() - ORIGIN_YEAR) as i64 * 12 + i64::from(date.month0());
176 let first = date.with_day(1)?;
177 self.local_midnight_utc_ms(
178 first.checked_add_months(Months::new(advance_to_grid(step_months, months) as u32))?,
179 )
180 }
181
182 pub(crate) fn align_to_year_grid(&self, timestamp_ms: u64, step_years: u64) -> Option<u64> {
185 let datetime = DateTime::from_timestamp_millis(timestamp_ms as i64)?;
186 let date = self.local_date(datetime);
187 let years = (date.year() - ORIGIN_YEAR) as i64;
188 let first = date.with_month(1)?.with_day(1)?;
189 self.local_midnight_utc_ms(first.checked_add_months(Months::new(
190 (advance_to_grid(step_years, years) * 12) as u32,
191 ))?)
192 }
193}
194
195fn advance_to_grid(step: u64, offset: i64) -> u64 {
198 let step = step.max(1) as i64;
199 ((step - offset.rem_euclid(step)) % step) as u64
200}
201
202impl fmt::Display for UserTimezone {
203 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204 match self {
205 UserTimezone::Utc => write!(f, "UTC"),
206 UserTimezone::Local => {
207 let local_offset = chrono::Local::now().offset().local_minus_utc();
208 let hours = local_offset / 3600;
209 let minutes = (local_offset % 3600) / 60;
210 write!(f, "Local (UTC {hours:+03}:{minutes:02})")
211 }
212 }
213 }
214}
215
216impl<'de> Deserialize<'de> for UserTimezone {
217 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
218 where
219 D: serde::Deserializer<'de>,
220 {
221 let timezone_str = String::deserialize(deserializer)?;
222 match timezone_str.to_lowercase().as_str() {
223 "utc" => Ok(UserTimezone::Utc),
224 "local" => Ok(UserTimezone::Local),
225 _ => Err(serde::de::Error::custom("Invalid UserTimezone")),
226 }
227 }
228}
229
230impl Serialize for UserTimezone {
231 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
232 where
233 S: serde::Serializer,
234 {
235 match self {
236 UserTimezone::Utc => serializer.serialize_str("UTC"),
237 UserTimezone::Local => serializer.serialize_str("Local"),
238 }
239 }
240}