Skip to main content

flowsurface_data/
util.rs

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