1use crate::types::Rom;
2
3#[derive(Debug, Clone)]
5pub struct RomGroup {
6 pub name: String,
7 pub primary: Rom,
8 pub others: Vec<Rom>,
9}
10
11pub fn group_roms_by_name(items: &[Rom]) -> Vec<RomGroup> {
14 use std::collections::HashMap;
15 let mut by_name: HashMap<String, Vec<Rom>> = HashMap::new();
16 for rom in items {
17 by_name
18 .entry(rom.name.clone())
19 .or_default()
20 .push(rom.clone());
21 }
22 let mut groups = Vec::with_capacity(by_name.len());
23 for (name, mut roms) in by_name {
24 roms.sort_by(|a, b| {
25 let a_extra = a.fs_name.to_lowercase().contains("[update]")
26 || a.fs_name.to_lowercase().contains("[dlc]");
27 let b_extra = b.fs_name.to_lowercase().contains("[update]")
28 || b.fs_name.to_lowercase().contains("[dlc]");
29 match (a_extra, b_extra) {
30 (false, true) => std::cmp::Ordering::Less,
31 (true, false) => std::cmp::Ordering::Greater,
32 _ => std::cmp::Ordering::Equal,
33 }
34 });
35 let primary = roms.remove(0);
36 groups.push(RomGroup {
37 name,
38 primary,
39 others: roms,
40 });
41 }
42 groups.sort_by(|a, b| a.name.cmp(&b.name));
43 groups
44}
45
46pub fn format_size(bytes: u64) -> String {
48 const KB: u64 = 1024;
49 const MB: u64 = KB * 1024;
50 const GB: u64 = MB * 1024;
51 if bytes >= GB {
52 format!("{:.2} GB", bytes as f64 / GB as f64)
53 } else if bytes >= MB {
54 format!("{:.2} MB", bytes as f64 / MB as f64)
55 } else if bytes >= KB {
56 format!("{:.2} KB", bytes as f64 / KB as f64)
57 } else {
58 format!("{} B", bytes)
59 }
60}
61
62pub fn sanitize_filename(name: &str) -> String {
64 name.chars()
65 .map(|c| {
66 if c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_' || c == ' ' {
67 c
68 } else {
69 '_'
70 }
71 })
72 .collect()
73}
74
75pub fn truncate(s: &str, max: usize) -> String {
77 let s = s.trim();
78 if s.chars().count() <= max {
79 s.to_string()
80 } else {
81 format!(
82 "{}…",
83 s.chars().take(max.saturating_sub(1)).collect::<String>()
84 )
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use crate::types::Rom;
92
93 fn rom(id: u64, name: &str, fs_name: &str) -> Rom {
94 Rom {
95 id,
96 platform_id: 1,
97 platform_slug: None,
98 platform_fs_slug: None,
99 platform_custom_name: Some("NES".to_string()),
100 platform_display_name: Some("NES".to_string()),
101 fs_name: fs_name.to_string(),
102 fs_name_no_tags: name.to_string(),
103 fs_name_no_ext: name.to_string(),
104 fs_extension: "zip".to_string(),
105 fs_path: format!("/roms/{}.zip", id),
106 fs_size_bytes: 1,
107 name: name.to_string(),
108 slug: None,
109 summary: None,
110 path_cover_small: None,
111 path_cover_large: None,
112 url_cover: None,
113 is_unidentified: false,
114 is_identified: true,
115 }
116 }
117
118 #[test]
119 fn group_roms_prefers_base_file_as_primary() {
120 let input = vec![
121 rom(1, "Game A", "Game A [Update].zip"),
122 rom(2, "Game A", "Game A [DLC].zip"),
123 rom(3, "Game A", "Game A.zip"),
124 rom(4, "Game B", "Game B.zip"),
125 ];
126
127 let groups = group_roms_by_name(&input);
128 assert_eq!(groups.len(), 2);
129
130 let game_a = groups.iter().find(|g| g.name == "Game A").expect("group");
131 assert_eq!(game_a.primary.fs_name, "Game A.zip");
132 assert_eq!(game_a.others.len(), 2);
133 }
134}