Skip to main content

flowsurface_data/
util.rs

1use chrono::{DateTime, Datelike, Timelike};
2use serde::{Deserialize, Deserializer};
3
4const DAY_MS: u64 = 86_400_000;
5const HOUR_MS: u64 = 3_600_000;
6const MINUTE_MS: u64 = 60_000;
7const SECOND_MS: u64 = 1_000;
8
9pub fn ok_or_default<'a, T, D>(deserializer: D) -> Result<T, D::Error>
10where
11    T: Deserialize<'a> + Default,
12    D: Deserializer<'a>,
13{
14    let v: serde_json::Value = Deserialize::deserialize(deserializer)?;
15    Ok(T::deserialize(v).unwrap_or_default())
16}
17
18pub fn abbr_large_numbers(value: f32) -> String {
19    let abs_value = value.abs();
20    let sign = if value < 0.0 { "-" } else { "" };
21
22    match abs_value {
23        v if v >= 1_000_000_000.0 => {
24            format!("{}{:.3}b", sign, v / 100_000_000.0)
25        }
26        v if v >= 1_000_000.0 => format!("{}{:.2}m", sign, v / 1_000_000.0),
27        v if v >= 10_000.0 => format!("{}{:.1}k", sign, v / 1_000.0),
28        v if v >= 1_000.0 => format!("{}{:.2}k", sign, v / 1_000.0),
29        v if v >= 100.0 => format!("{}{:.0}", sign, v),
30        v if v >= 10.0 => format!("{}{:.1}", sign, v),
31        v if v >= 1.0 => format!("{}{:.2}", sign, v),
32        v if v >= 0.001 => format!("{}{:.3}", sign, v),
33        v if v >= 0.0001 => format!("{}{:.4}", sign, v),
34        v if v >= 0.00001 => format!("{}{:.5}", sign, v),
35        _ => {
36            if abs_value == 0.0 {
37                "0".to_string()
38            } else {
39                let s = format!("{}{:.3}", sign, abs_value);
40                s.trim_end_matches('0').trim_end_matches('.').to_string()
41            }
42        }
43    }
44}
45
46pub fn format_with_commas(num: f32) -> String {
47    if num == 0.0 {
48        return "0".to_string();
49    }
50
51    let abs_num = num.abs();
52    let decimals = match abs_num {
53        n if n >= 1000.0 => 0,
54        n if n >= 100.0 => 1,
55        n if n >= 10.0 => 2,
56        _ => 3,
57    };
58
59    let is_negative = num < 0.0;
60
61    if abs_num < 1000.0 {
62        return format!(
63            "{}{:.*}",
64            if is_negative { "-" } else { "" },
65            decimals,
66            abs_num
67        );
68    }
69
70    let s = format!("{:.*}", decimals, abs_num);
71
72    let (integer_part, decimal_part) = match s.find('.') {
73        Some(pos) => (&s[..pos], Some(&s[pos..])),
74        None => (s.as_str(), None),
75    };
76
77    let mut result = {
78        let num_commas = (integer_part.len() - 1) / 3;
79        let decimal_len = decimal_part.map_or(0, str::len);
80
81        String::with_capacity(
82            usize::from(is_negative) + integer_part.len() + num_commas + decimal_len,
83        )
84    };
85
86    if is_negative {
87        result.push('-');
88    }
89
90    let digits_len = integer_part.len();
91    for (i, ch) in integer_part.chars().enumerate() {
92        result.push(ch);
93
94        let pos_from_right = digits_len - i - 1;
95        if i < digits_len - 1 && pos_from_right % 3 == 0 {
96            result.push(',');
97        }
98    }
99
100    if let Some(decimal) = decimal_part {
101        result.push_str(decimal);
102    }
103
104    result
105}
106
107pub fn round_to_tick(value: f32, tick_size: f32) -> f32 {
108    (value / tick_size).round() * tick_size
109}
110
111pub fn round_to_next_tick(value: f32, tick_size: f32, down: bool) -> f32 {
112    if down {
113        (value / tick_size).floor() * tick_size
114    } else {
115        (value / tick_size).ceil() * tick_size
116    }
117}
118
119pub fn currency_abbr(price: f32) -> String {
120    match price {
121        p if p > 1_000_000_000.0 => format!("${:.2}b", p / 1_000_000_000.0),
122        p if p > 1_000_000.0 => format!("${:.1}m", p / 1_000_000.0),
123        p if p > 1000.0 => format!("${:.2}k", p / 1000.0),
124        _ => format!("${:.2}", price),
125    }
126}
127
128pub fn pct_change(change: f32) -> String {
129    match change {
130        c if c > 0.0 => format!("+{:.2}%", c),
131        _ => format!("{:.2}%", change),
132    }
133}
134
135pub fn guesstimate_ticks(range: f32) -> f32 {
136    match range {
137        r if r > 1_000_000_000.0 => 1_000_000.0,
138        r if r > 100_000_000.0 => 100_000.0,
139        r if r > 10_000_000.0 => 10_000.0,
140        r if r > 1_000_000.0 => 1_000.0,
141        r if r > 100_000.0 => 1_000.0,
142        r if r > 10_000.0 => 100.0,
143        r if r > 1_000.0 => 10.0,
144        r if r > 100.0 => 1.0,
145        r if r > 10.0 => 0.1,
146        r if r > 1.0 => 0.01,
147        r if r > 0.1 => 0.001,
148        r if r > 0.01 => 0.0001,
149        _ => 0.00001,
150    }
151}
152
153pub fn format_duration_ms(diff_ms: u64) -> String {
154    if diff_ms >= DAY_MS {
155        let days = diff_ms / DAY_MS;
156        let hours = (diff_ms % DAY_MS) / HOUR_MS;
157        if hours > 0 {
158            format!("{}d {}h", days, hours)
159        } else {
160            format!("{}d", days)
161        }
162    } else if diff_ms >= HOUR_MS {
163        let hours = diff_ms / HOUR_MS;
164        let mins = (diff_ms % HOUR_MS) / MINUTE_MS;
165        if mins > 0 {
166            format!("{}h {}m", hours, mins)
167        } else {
168            format!("{}h", hours)
169        }
170    } else if diff_ms >= MINUTE_MS {
171        let mins = diff_ms / MINUTE_MS;
172        let secs = (diff_ms % MINUTE_MS) / SECOND_MS;
173        if secs > 0 {
174            format!("{}m {}s", mins, secs)
175        } else {
176            format!("{}m", mins)
177        }
178    } else if diff_ms >= 5_000 {
179        format!("{}s", diff_ms / SECOND_MS)
180    } else {
181        format!("{}ms", diff_ms)
182    }
183}
184
185/// Shrinks main panel if needed when adding a new panel.
186/// Ensures indicators never shrink below `MIN_PANEL_HEIGHT`
187pub fn calc_panel_splits(
188    initial_main_split: f32,
189    active_indicators: usize,
190    previous_indicators: Option<usize>,
191) -> Vec<f32> {
192    const MIN_PANEL_HEIGHT: f32 = 0.1;
193    const TOTAL_HEIGHT: f32 = 1.0;
194
195    let mut main_split = initial_main_split;
196
197    if let Some(prev_inds) = previous_indicators
198        && active_indicators > prev_inds
199    {
200        let min_space_needed_all_indis = active_indicators as f32 * MIN_PANEL_HEIGHT;
201
202        let max_main_split_if_indis_get_min =
203            (TOTAL_HEIGHT - min_space_needed_all_indis).max(MIN_PANEL_HEIGHT);
204
205        if main_split > max_main_split_if_indis_get_min {
206            main_split = max_main_split_if_indis_get_min;
207        }
208    }
209
210    let upper_bound_for_main = if active_indicators == 0 {
211        TOTAL_HEIGHT
212    } else {
213        (TOTAL_HEIGHT - active_indicators as f32 * MIN_PANEL_HEIGHT).max(MIN_PANEL_HEIGHT)
214    };
215
216    main_split = main_split.clamp(MIN_PANEL_HEIGHT, upper_bound_for_main);
217    main_split = main_split.min(TOTAL_HEIGHT);
218
219    let mut splits = vec![main_split];
220
221    if active_indicators > 1 {
222        let indicator_total_space = (TOTAL_HEIGHT - main_split).max(0.0);
223        let per_indicator_space = indicator_total_space / active_indicators as f32;
224
225        for i in 1..active_indicators {
226            let cumulative_indicator_space = per_indicator_space * i as f32;
227            let split_pos = main_split + cumulative_indicator_space;
228            splits.push(split_pos.min(TOTAL_HEIGHT));
229        }
230    }
231    splits
232}
233
234pub fn reset_to_start_of_day_utc(dt: DateTime<chrono::Utc>) -> DateTime<chrono::Utc> {
235    dt.with_hour(0)
236        .unwrap_or(dt)
237        .with_minute(0)
238        .unwrap_or(dt)
239        .with_second(0)
240        .unwrap_or(dt)
241        .with_nanosecond(0)
242        .unwrap_or(dt)
243}
244
245pub fn reset_to_start_of_month_utc(dt: DateTime<chrono::Utc>) -> DateTime<chrono::Utc> {
246    reset_to_start_of_day_utc(dt.with_day(1).unwrap_or(dt))
247}
248
249pub fn reset_to_start_of_year_utc(dt: DateTime<chrono::Utc>) -> DateTime<chrono::Utc> {
250    reset_to_start_of_month_utc(dt.with_month(1).unwrap_or(dt))
251}