1use crate::Points;
4
5#[inline]
23#[must_use]
24pub fn format_bytes(bytes: u64) -> String {
25 const KB: u64 = 1024;
26 const MB: u64 = KB * 1024;
27 const GB: u64 = MB * 1024;
28 const TB: u64 = GB * 1024;
29
30 if bytes >= TB {
31 format!("{:.2} TB", bytes as f64 / TB as f64)
32 } else if bytes >= GB {
33 format!("{:.2} GB", bytes as f64 / GB as f64)
34 } else if bytes >= MB {
35 format!("{:.2} MB", bytes as f64 / MB as f64)
36 } else if bytes >= KB {
37 format!("{:.2} KB", bytes as f64 / KB as f64)
38 } else {
39 format!("{} B", bytes)
40 }
41}
42
43#[inline]
61#[must_use]
62pub fn format_duration_ms(ms: u64) -> String {
63 const SEC: u64 = 1000;
64 const MIN: u64 = SEC * 60;
65 const HOUR: u64 = MIN * 60;
66 const DAY: u64 = HOUR * 24;
67
68 if ms >= DAY {
69 format!("{:.1} days", ms as f64 / DAY as f64)
70 } else if ms >= HOUR {
71 format!("{:.1} hours", ms as f64 / HOUR as f64)
72 } else if ms >= MIN {
73 format!("{:.1} minutes", ms as f64 / MIN as f64)
74 } else if ms >= SEC {
75 format!("{:.1} seconds", ms as f64 / SEC as f64)
76 } else {
77 format!("{} ms", ms)
78 }
79}
80
81#[inline]
83#[must_use]
84pub fn truncate_string(s: &str, max_len: usize) -> String {
85 if s.len() <= max_len {
86 s.to_string()
87 } else {
88 let mut truncated = s
89 .chars()
90 .take(max_len.saturating_sub(3))
91 .collect::<String>();
92 truncated.push_str("...");
93 truncated
94 }
95}
96
97#[inline]
99#[must_use]
100pub fn sanitize_tag(tag: &str) -> String {
101 tag.trim().to_lowercase()
102}
103
104#[must_use]
106pub fn sanitize_tags(tags: &[String]) -> Vec<String> {
107 tags.iter()
108 .map(|t| sanitize_tag(t))
109 .filter(|t| !t.is_empty())
110 .collect()
111}
112
113#[inline]
115#[must_use]
116pub fn format_points(points: Points) -> String {
117 let s = points.to_string();
118 let mut result = String::new();
119 let chars: Vec<char> = s.chars().collect();
120
121 for (i, c) in chars.iter().enumerate() {
122 if i > 0 && (chars.len() - i) % 3 == 0 {
123 result.push(',');
124 }
125 result.push(*c);
126 }
127
128 result
129}
130
131#[inline]
133#[must_use]
134pub fn sanitize_string(s: &str) -> String {
135 s.chars()
136 .filter(|c| !c.is_control() || c.is_whitespace())
137 .collect()
138}
139
140pub fn normalize_tag(tag: &str) -> String {
142 tag.trim()
143 .to_lowercase()
144 .chars()
145 .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
146 .collect()
147}
148
149pub fn format_bandwidth(bps: u64) -> String {
151 if bps >= 1_000_000_000 {
152 format!("{:.2} Gbps", bps as f64 / 1_000_000_000.0)
153 } else if bps >= 1_000_000 {
154 format!("{:.2} Mbps", bps as f64 / 1_000_000.0)
155 } else if bps >= 1_000 {
156 format!("{:.2} Kbps", bps as f64 / 1_000.0)
157 } else {
158 format!("{} bps", bps)
159 }
160}
161
162pub fn format_ratio_as_percentage(numerator: u64, denominator: u64) -> String {
164 if denominator == 0 {
165 return "N/A".to_string();
166 }
167 format!("{:.2}%", (numerator as f64 / denominator as f64) * 100.0)
168}
169
170pub fn generate_slug(text: &str, max_len: usize) -> String {
173 let mut slug = text
174 .to_lowercase()
175 .chars()
176 .map(|c| {
177 if c.is_ascii_alphanumeric() {
178 c
179 } else if c.is_whitespace() || c == '_' || c == '-' {
180 '-'
181 } else {
182 '\0' }
184 })
185 .filter(|&c| c != '\0')
186 .collect::<String>();
187
188 while slug.contains("--") {
190 slug = slug.replace("--", "-");
191 }
192
193 slug = slug.trim_matches('-').to_string();
195
196 if slug.len() > max_len {
198 slug.truncate(max_len);
199 slug = slug.trim_end_matches('-').to_string();
200 }
201
202 slug
203}
204
205pub fn cid_to_short_id(cid: &str) -> String {
208 if cid.starts_with("Qm") && cid.len() > 10 {
209 cid[2..10].to_string()
210 } else if cid.len() >= 8 {
211 cid[..8].to_string()
212 } else {
213 cid.to_string()
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn test_format_bytes() {
223 assert_eq!(format_bytes(512), "512 B");
224 assert_eq!(format_bytes(1024), "1.00 KB");
225 assert_eq!(format_bytes(1024 * 1024), "1.00 MB");
226 assert_eq!(format_bytes(1024 * 1024 * 1024), "1.00 GB");
227 assert_eq!(format_bytes(1024_u64 * 1024 * 1024 * 1024), "1.00 TB");
228 }
229
230 #[test]
231 fn test_format_duration() {
232 assert_eq!(format_duration_ms(500), "500 ms");
233 assert_eq!(format_duration_ms(1000), "1.0 seconds");
234 assert_eq!(format_duration_ms(60_000), "1.0 minutes");
235 assert_eq!(format_duration_ms(3_600_000), "1.0 hours");
236 assert_eq!(format_duration_ms(86_400_000), "1.0 days");
237 }
238
239 #[test]
240 fn test_truncate_string() {
241 assert_eq!(truncate_string("hello", 10), "hello");
242 assert_eq!(truncate_string("hello world", 8), "hello...");
243 assert_eq!(truncate_string("hi", 5), "hi");
244 }
245
246 #[test]
247 fn test_sanitize_tag() {
248 assert_eq!(sanitize_tag(" Hello World "), "hello world");
249 assert_eq!(sanitize_tag("Rust"), "rust");
250 }
251
252 #[test]
253 fn test_sanitize_tags() {
254 let tags = vec![
255 " Rust ".to_string(),
256 "GAME".to_string(),
257 " ".to_string(),
258 "3D".to_string(),
259 ];
260 let sanitized = sanitize_tags(&tags);
261 assert_eq!(sanitized, vec!["rust", "game", "3d"]);
262 }
263
264 #[test]
265 fn test_format_points() {
266 assert_eq!(format_points(0), "0");
267 assert_eq!(format_points(999), "999");
268 assert_eq!(format_points(1000), "1,000");
269 assert_eq!(format_points(1_000_000), "1,000,000");
270 }
271
272 #[test]
273 fn test_sanitize_string() {
274 assert_eq!(sanitize_string("hello world"), "hello world");
275 assert_eq!(sanitize_string("hello\nworld"), "hello\nworld");
276 assert_eq!(sanitize_string("hello\x00world"), "helloworld");
277 }
278
279 #[test]
280 fn test_normalize_tag() {
281 assert_eq!(normalize_tag(" Hello World "), "helloworld");
282 assert_eq!(normalize_tag("Rust-2024"), "rust-2024");
283 assert_eq!(normalize_tag("Tag_Name"), "tag_name");
284 assert_eq!(normalize_tag("Tag@#$Name"), "tagname");
285 }
286
287 #[test]
288 fn test_format_bandwidth() {
289 assert_eq!(format_bandwidth(500), "500 bps");
290 assert_eq!(format_bandwidth(10_000), "10.00 Kbps");
291 assert_eq!(format_bandwidth(10_000_000), "10.00 Mbps");
292 assert_eq!(format_bandwidth(1_000_000_000), "1.00 Gbps");
293 }
294
295 #[test]
296 fn test_format_ratio_as_percentage() {
297 assert_eq!(format_ratio_as_percentage(1, 4), "25.00%");
298 assert_eq!(format_ratio_as_percentage(3, 4), "75.00%");
299 assert_eq!(format_ratio_as_percentage(5, 0), "N/A");
300 }
301
302 #[test]
303 fn test_generate_slug() {
304 assert_eq!(generate_slug("Hello World", 100), "hello-world");
305 assert_eq!(generate_slug("Rust Programming!", 100), "rust-programming");
306 assert_eq!(
307 generate_slug("Test_With_Underscores", 100),
308 "test-with-underscores"
309 );
310 assert_eq!(generate_slug(" Trim Spaces ", 100), "trim-spaces");
311 assert_eq!(
312 generate_slug("Remove@Special#Chars$", 100),
313 "removespecialchars"
314 );
315 assert_eq!(generate_slug("Multiple---Hyphens", 100), "multiple-hyphens");
316 assert_eq!(
317 generate_slug("Very Long Title That Exceeds Maximum Length", 20),
318 "very-long-title-that"
319 );
320 assert_eq!(
321 generate_slug("---Leading-Trailing---", 100),
322 "leading-trailing"
323 );
324 }
325
326 #[test]
327 fn test_cid_to_short_id() {
328 assert_eq!(cid_to_short_id("QmExampleCID123456"), "ExampleC");
329 assert_eq!(cid_to_short_id("bafybeigdyrzt"), "bafybeig");
330 assert_eq!(cid_to_short_id("short"), "short");
331 assert_eq!(cid_to_short_id("QmTest"), "QmTest");
332 }
333}