1use serde_json::Value;
4
5pub enum DurationFormat {
7 Short,
9 Long,
11 LongWithSeconds,
13}
14
15pub fn format_duration_as(ms: u64, format: DurationFormat) -> String {
17 let total_secs = ms / 1000;
18 let hours = total_secs / 3600;
19 let mins = (total_secs % 3600) / 60;
20 let secs = total_secs % 60;
21
22 match format {
23 DurationFormat::Short => {
24 if hours > 0 {
25 format!("{}:{:02}:{:02}", hours, mins, secs)
26 } else {
27 format!("{}:{:02}", mins, secs)
28 }
29 }
30 DurationFormat::Long => {
31 if hours > 0 {
32 format!("{}h {}m", hours, mins)
33 } else {
34 format!("{}m", mins)
35 }
36 }
37 DurationFormat::LongWithSeconds => {
38 if hours > 0 {
39 format!("{}h {}m", hours, mins)
40 } else if mins > 0 {
41 format!("{}m {}s", mins, secs)
42 } else {
43 format!("{}s", secs)
44 }
45 }
46 }
47}
48
49pub fn format_duration(ms: u64) -> String {
52 format_duration_as(ms, DurationFormat::Short)
53}
54
55pub fn truncate(s: &str, max: usize) -> String {
57 if s.chars().count() <= max {
58 s.to_string()
59 } else {
60 format!("{}...", s.chars().take(max - 3).collect::<String>())
61 }
62}
63
64pub fn format_number(n: u64) -> String {
66 if n >= 1_000_000 {
67 format!("{:.1}M", n as f64 / 1_000_000.0)
68 } else if n >= 1_000 {
69 format!("{:.1}K", n as f64 / 1_000.0)
70 } else {
71 n.to_string()
72 }
73}
74
75pub fn get_score(item: &Value) -> i64 {
77 item.get("fuzzy_score")
78 .and_then(|v| v.as_f64())
79 .map(|s| s as i64)
80 .unwrap_or(0)
81}
82
83pub fn extract_artist_names(item: &Value) -> String {
85 item.get("artists")
86 .and_then(|v| v.as_array())
87 .map(|arr| {
88 arr.iter()
89 .filter_map(|a| a.get("name").and_then(|n| n.as_str()))
90 .collect::<Vec<_>>()
91 .join(", ")
92 })
93 .unwrap_or_else(|| "Unknown".to_string())
94}
95
96pub fn print_table(header: &str, cols: &[&str], rows: &[Vec<String>], col_widths: &[usize]) {
98 println!("\n{}:", header);
99
100 print!(" ");
101 for (i, col) in cols.iter().enumerate() {
102 if i == cols.len() - 1 {
103 print!("{:>width$}", col, width = col_widths[i]);
104 } else {
105 print!("{:<width$} ", col, width = col_widths[i]);
106 }
107 }
108 println!();
109
110 print!(" ");
111 for (i, &w) in col_widths.iter().enumerate() {
112 if i == col_widths.len() - 1 {
113 print!("{}", "-".repeat(w));
114 } else {
115 print!("{} ", "-".repeat(w));
116 }
117 }
118 println!();
119
120 for row in rows {
121 print!(" ");
122 for (i, cell) in row.iter().enumerate() {
123 if i == row.len() - 1 {
124 print!("{:>width$}", cell, width = col_widths[i]);
125 } else {
126 print!("{:<width$} ", cell, width = col_widths[i]);
127 }
128 }
129 println!();
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use serde_json::json;
137
138 #[test]
139 fn format_duration_short_minutes_only() {
140 assert_eq!(format_duration(0), "0:00");
141 assert_eq!(format_duration(1000), "0:01");
142 assert_eq!(format_duration(60000), "1:00");
143 assert_eq!(format_duration(210000), "3:30");
144 }
145
146 #[test]
147 fn format_duration_short_with_hours() {
148 assert_eq!(format_duration(3600000), "1:00:00");
149 assert_eq!(format_duration(3661000), "1:01:01");
150 assert_eq!(format_duration(7200000), "2:00:00");
151 }
152
153 #[test]
154 fn format_duration_as_short() {
155 assert_eq!(format_duration_as(180000, DurationFormat::Short), "3:00");
156 assert_eq!(
157 format_duration_as(3661000, DurationFormat::Short),
158 "1:01:01"
159 );
160 }
161
162 #[test]
163 fn format_duration_as_long() {
164 assert_eq!(format_duration_as(60000, DurationFormat::Long), "1m");
165 assert_eq!(format_duration_as(3660000, DurationFormat::Long), "1h 1m");
166 assert_eq!(format_duration_as(7200000, DurationFormat::Long), "2h 0m");
167 }
168
169 #[test]
170 fn format_duration_as_long_with_seconds() {
171 assert_eq!(
172 format_duration_as(30000, DurationFormat::LongWithSeconds),
173 "30s"
174 );
175 assert_eq!(
176 format_duration_as(90000, DurationFormat::LongWithSeconds),
177 "1m 30s"
178 );
179 assert_eq!(
180 format_duration_as(3661000, DurationFormat::LongWithSeconds),
181 "1h 1m"
182 );
183 }
184
185 #[test]
186 fn truncate_short_string() {
187 assert_eq!(truncate("hello", 10), "hello");
188 assert_eq!(truncate("short", 10), "short");
189 }
190
191 #[test]
192 fn truncate_long_string() {
193 assert_eq!(truncate("hello world", 8), "hello...");
194 assert_eq!(truncate("a very long string", 10), "a very ...");
195 }
196
197 #[test]
198 fn truncate_exact_length() {
199 assert_eq!(truncate("hello", 5), "hello");
200 }
201
202 #[test]
203 fn format_number_small() {
204 assert_eq!(format_number(0), "0");
205 assert_eq!(format_number(999), "999");
206 }
207
208 #[test]
209 fn format_number_thousands() {
210 assert_eq!(format_number(1000), "1.0K");
211 assert_eq!(format_number(1500), "1.5K");
212 assert_eq!(format_number(10000), "10.0K");
213 assert_eq!(format_number(999999), "1000.0K");
214 }
215
216 #[test]
217 fn format_number_millions() {
218 assert_eq!(format_number(1000000), "1.0M");
219 assert_eq!(format_number(1500000), "1.5M");
220 assert_eq!(format_number(10000000), "10.0M");
221 }
222
223 #[test]
224 fn get_score_present() {
225 let item = json!({ "fuzzy_score": 75.5 });
226 assert_eq!(get_score(&item), 75);
227 }
228
229 #[test]
230 fn get_score_missing() {
231 let item = json!({ "name": "test" });
232 assert_eq!(get_score(&item), 0);
233 }
234
235 #[test]
236 fn get_score_non_numeric() {
237 let item = json!({ "fuzzy_score": "not a number" });
238 assert_eq!(get_score(&item), 0);
239 }
240
241 #[test]
242 fn extract_artist_names_single() {
243 let item = json!({
244 "artists": [{ "name": "Artist One" }]
245 });
246 assert_eq!(extract_artist_names(&item), "Artist One");
247 }
248
249 #[test]
250 fn extract_artist_names_multiple() {
251 let item = json!({
252 "artists": [
253 { "name": "Artist One" },
254 { "name": "Artist Two" }
255 ]
256 });
257 assert_eq!(extract_artist_names(&item), "Artist One, Artist Two");
258 }
259
260 #[test]
261 fn extract_artist_names_empty_array() {
262 let item = json!({ "artists": [] });
263 assert_eq!(extract_artist_names(&item), "");
264 }
265
266 #[test]
267 fn extract_artist_names_missing() {
268 let item = json!({ "name": "Track" });
269 assert_eq!(extract_artist_names(&item), "Unknown");
270 }
271
272 #[test]
273 fn extract_artist_names_null() {
274 let item = json!({ "artists": null });
275 assert_eq!(extract_artist_names(&item), "Unknown");
276 }
277}