1use chrono::{DateTime, Datelike, Timelike};
2use serde::{Deserialize, Deserializer};
3
4pub(crate) fn stable_hash(bytes: &[u8]) -> u64 {
5 bytes.iter().fold(0xcbf2_9ce4_8422_2325, |hash, byte| {
6 hash.wrapping_mul(0x0000_0100_0000_01b3)
7 .wrapping_add(u64::from(*byte))
8 })
9}
10
11pub fn reorder_vec<T>(items: &mut Vec<T>, index: usize, target: usize) -> Option<()> {
12 if index >= items.len() || target > items.len() {
13 return None;
14 }
15
16 if target != index {
17 let item = items.remove(index);
18 items.insert(if index < target { target - 1 } else { target }, item);
19 }
20
21 Some(())
22}
23
24pub fn unique_name<'a>(
25 proposed: &str,
26 existing: impl Iterator<Item = &'a str>,
27 max_chars: usize,
28) -> String {
29 let existing = existing.collect::<Vec<_>>();
30 let proposed = proposed.trim();
31 let mut name = proposed.chars().take(max_chars).collect::<String>();
32 let mut suffix = 2;
33
34 while existing.contains(&name.as_str()) {
35 let ending = format!(" ({suffix})");
36 let prefix = proposed
37 .chars()
38 .take(max_chars.saturating_sub(ending.len()))
39 .collect::<String>();
40 name = format!("{prefix}{ending}");
41 suffix += 1;
42 }
43
44 name
45}
46
47pub fn ok_or_default<'a, T, D>(deserializer: D) -> Result<T, D::Error>
48where
49 T: Deserialize<'a> + Default,
50 D: Deserializer<'a>,
51{
52 let v: serde_json::Value = Deserialize::deserialize(deserializer)?;
53 Ok(T::deserialize(v).unwrap_or_default())
54}
55
56pub fn abbr_large_numbers(value: f64) -> String {
57 let abs_value = value.abs();
58 let sign = if value < 0.0 { "-" } else { "" };
59
60 match abs_value {
61 v if v >= 1_000_000_000.0 => {
62 format!("{}{:.3}b", sign, v / 1_000_000_000.0)
63 }
64 v if v >= 1_000_000.0 => format!("{}{:.2}m", sign, v / 1_000_000.0),
65 v if v >= 10_000.0 => format!("{}{:.1}k", sign, v / 1_000.0),
66 v if v >= 1_000.0 => format!("{}{:.2}k", sign, v / 1_000.0),
67 v if v >= 100.0 => format!("{}{:.0}", sign, v),
68 v if v >= 10.0 => format!("{}{:.1}", sign, v),
69 v if v >= 1.0 => format!("{}{:.2}", sign, v),
70 v if v >= 0.001 => format!("{}{:.3}", sign, v),
71 v if v >= 0.0001 => format!("{}{:.4}", sign, v),
72 v if v >= 0.00001 => format!("{}{:.5}", sign, v),
73 _ => {
74 if abs_value == 0.0 {
75 "0".to_string()
76 } else {
77 let s = format!("{}{:.3}", sign, abs_value);
78 s.trim_end_matches('0').trim_end_matches('.').to_string()
79 }
80 }
81 }
82}
83
84pub fn format_with_commas(num: f64) -> String {
85 if num == 0.0 {
86 return "0".to_string();
87 }
88
89 let abs_num = num.abs();
90 let decimals = match abs_num {
91 n if n >= 1000.0 => 0,
92 n if n >= 100.0 => 1,
93 n if n >= 10.0 => 2,
94 _ => 3,
95 };
96
97 let is_negative = num < 0.0;
98
99 if abs_num < 1000.0 {
100 return format!(
101 "{}{:.*}",
102 if is_negative { "-" } else { "" },
103 decimals,
104 abs_num
105 );
106 }
107
108 let s = format!("{:.*}", decimals, abs_num);
109
110 let (integer_part, decimal_part) = match s.find('.') {
111 Some(pos) => (&s[..pos], Some(&s[pos..])),
112 None => (s.as_str(), None),
113 };
114
115 let mut result = {
116 let num_commas = (integer_part.len() - 1) / 3;
117 let decimal_len = decimal_part.map_or(0, str::len);
118
119 String::with_capacity(
120 usize::from(is_negative) + integer_part.len() + num_commas + decimal_len,
121 )
122 };
123
124 if is_negative {
125 result.push('-');
126 }
127
128 let digits_len = integer_part.len();
129 for (i, ch) in integer_part.chars().enumerate() {
130 result.push(ch);
131
132 let pos_from_right = digits_len - i - 1;
133 if i < digits_len - 1 && pos_from_right % 3 == 0 {
134 result.push(',');
135 }
136 }
137
138 if let Some(decimal) = decimal_part {
139 result.push_str(decimal);
140 }
141
142 result
143}
144
145pub fn round_to_next_tick(value: f32, tick_size: f32, down: bool) -> f32 {
146 if down {
147 (value / tick_size).floor() * tick_size
148 } else {
149 (value / tick_size).ceil() * tick_size
150 }
151}
152
153pub fn currency_abbr(price: f32) -> String {
154 match price {
155 p if p > 1_000_000_000.0 => format!("${:.2}b", p / 1_000_000_000.0),
156 p if p > 1_000_000.0 => format!("${:.1}m", p / 1_000_000.0),
157 p if p > 1000.0 => format!("${:.2}k", p / 1000.0),
158 _ => format!("${:.2}", price),
159 }
160}
161
162pub fn pct_change(change: f32) -> String {
163 match change {
164 c if c > 0.0 => format!("+{:.2}%", c),
165 _ => format!("{:.2}%", change),
166 }
167}
168
169pub fn format_duration_ms(diff_ms: u64) -> String {
170 const DAY_MS: u64 = 86_400_000;
171 const HOUR_MS: u64 = 3_600_000;
172 const MINUTE_MS: u64 = 60_000;
173 const SECOND_MS: u64 = 1_000;
174
175 if diff_ms >= DAY_MS {
176 let days = diff_ms / DAY_MS;
177 let hours = (diff_ms % DAY_MS) / HOUR_MS;
178 if hours > 0 {
179 format!("{}d {}h", days, hours)
180 } else {
181 format!("{}d", days)
182 }
183 } else if diff_ms >= HOUR_MS {
184 let hours = diff_ms / HOUR_MS;
185 let mins = (diff_ms % HOUR_MS) / MINUTE_MS;
186 if mins > 0 {
187 format!("{}h {}m", hours, mins)
188 } else {
189 format!("{}h", hours)
190 }
191 } else if diff_ms >= MINUTE_MS {
192 let mins = diff_ms / MINUTE_MS;
193 let secs = (diff_ms % MINUTE_MS) / SECOND_MS;
194 if secs > 0 {
195 format!("{}m {}s", mins, secs)
196 } else {
197 format!("{}m", mins)
198 }
199 } else if diff_ms >= 5_000 {
200 format!("{}s", diff_ms / SECOND_MS)
201 } else {
202 format!("{}ms", diff_ms)
203 }
204}
205
206pub fn round_125(v: f64) -> f64 {
208 if !v.is_finite() || v <= 0.0 {
209 return 1.0;
210 }
211
212 let base = 10.0f64.powf(v.log10().floor());
213 let fraction = v / base;
214 let mult = if fraction <= 1.0 {
215 1.0
216 } else if fraction <= 2.0 {
217 2.0
218 } else if fraction <= 5.0 {
219 5.0
220 } else {
221 10.0
222 };
223 mult * base
224}
225
226pub fn reset_to_start_of_day_utc(dt: DateTime<chrono::Utc>) -> DateTime<chrono::Utc> {
227 dt.with_hour(0)
228 .unwrap_or(dt)
229 .with_minute(0)
230 .unwrap_or(dt)
231 .with_second(0)
232 .unwrap_or(dt)
233 .with_nanosecond(0)
234 .unwrap_or(dt)
235}
236
237pub fn reset_to_start_of_month_utc(dt: DateTime<chrono::Utc>) -> DateTime<chrono::Utc> {
238 reset_to_start_of_day_utc(dt.with_day(1).unwrap_or(dt))
239}
240
241pub fn reset_to_start_of_year_utc(dt: DateTime<chrono::Utc>) -> DateTime<chrono::Utc> {
242 reset_to_start_of_month_utc(dt.with_month(1).unwrap_or(dt))
243}
244
245pub fn relative_time_label(updated: chrono::DateTime<chrono::Utc>) -> String {
248 let elapsed = chrono::Utc::now()
249 .signed_duration_since(updated)
250 .to_std()
251 .unwrap_or_default();
252 let secs = elapsed.as_secs();
253
254 match secs {
255 0..=59 => "just now".to_string(),
256 60..=3_599 => format!("{} min ago", secs / 60),
257 3_600..=86_399 => format!("{} h ago", secs / 3_600),
258 _ => format!("{} d ago", secs / 86_400),
259 }
260}