mecomp_storage/
util.rs

1//! Utility types and functions.
2
3use one_or_many::OneOrMany;
4#[cfg(feature = "serde")]
5use serde::{Deserialize, Serialize};
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
8#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
9#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
10pub enum MetadataConflictResolution {
11    #[default]
12    Overwrite,
13    Skip,
14}
15
16#[cfg(all(test, feature = "serde"))]
17mod metadata_conflict_resolution {
18    use rstest::rstest;
19
20    use super::*;
21
22    #[test]
23    fn test_default() {
24        assert_eq!(
25            MetadataConflictResolution::default(),
26            MetadataConflictResolution::Overwrite
27        );
28    }
29
30    #[rstest]
31    #[case::lower(MetadataConflictResolution::Overwrite, "overwrite")]
32    #[case::lower(MetadataConflictResolution::Skip, "skip")]
33    fn test_deserialize<D, 'de>(#[case] expected: MetadataConflictResolution, #[case] input: D)
34    where
35        D: serde::de::IntoDeserializer<'de>,
36    {
37        let actual: MetadataConflictResolution =
38            MetadataConflictResolution::deserialize(input.into_deserializer()).unwrap();
39        assert_eq!(actual, expected);
40    }
41}
42
43/// Splits an artist name into multiple names based on the given separators.
44///
45/// The function takes into account exceptions, which are artist names that should not be split
46/// even if they contain the separator.
47///
48/// The function returns a `OneOrMany<String>` containing the split artist names.
49///
50/// Worst Case Runtime: O(n(m + k)), where n is the length of the artist name, m is the number of separators, and k is the number of exceptions.
51///
52/// In general, it will be faster then this since the number of separators and exceptions is usually small.
53#[inline]
54#[must_use]
55pub fn split_artist_name(
56    artist: &str,
57    artist_name_separator: &OneOrMany<String>,
58    exceptions: &OneOrMany<String>,
59) -> OneOrMany<String> {
60    let mut artists = OneOrMany::None;
61
62    // for the separators with exclusions, we can use a 2 pointers approach
63    let mut left = 0;
64    let mut right = 0;
65    while right < artist.len() {
66        let rest_right = &artist[right..];
67        // are we at a separator?
68        if let Some(sep) = artist_name_separator
69            .iter()
70            .find(|sep| rest_right.starts_with(*sep))
71        {
72            let rest_left = &artist[left..];
73            // if we are at a separator, we need to check if are we at the beginning of an exception
74            if let Some(exception) = exceptions
75                .iter()
76                .find(|exception| rest_left.starts_with(*exception))
77            {
78                let exception_len = exception.len();
79                // if we are at the beginning of an exception, we need to check if after it the string ends or there is a separator
80                let after_exception = &artist[left + exception_len..];
81                // if the string ends, we can add the artist
82                if after_exception.is_empty() {
83                    break;
84                }
85                if let Some(sep) = artist_name_separator
86                    .iter()
87                    .find(|sep| after_exception.starts_with(*sep))
88                {
89                    // if there is a separator after it, we split the string there instead
90                    let new = artist[left..left + exception_len].trim().replace('\0', "");
91                    if !new.is_empty() {
92                        artists.push(new);
93                    }
94                    left += exception_len + sep.len();
95                    right = left;
96                    continue;
97                }
98            }
99            // otherwise, we can split the string at this separator
100            let new = artist[left..right].trim().replace('\0', "");
101            if !new.is_empty() {
102                artists.push(new);
103            }
104            right += sep.len();
105            left = right;
106        } else {
107            // if we are not at a separator, we just move the right pointer to the right
108            right += 1;
109            // continue incrementing the right pointer if we're not at a character boundary
110            while !artist.is_char_boundary(right) && right < artist.len() {
111                right += 1;
112            }
113        }
114    }
115
116    // add the last artist, if any
117    if left < artist.len() {
118        let new = artist[left..].trim().replace('\0', "");
119        if !new.is_empty() {
120            artists.push(new);
121        }
122    }
123    artists
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use rstest::rstest;
130
131    #[rstest]
132    #[case::no_separation("Foo & Bar", &[], &[], vec!["Foo & Bar"])]
133    #[case::redundant_separation("Foo & Bar", &["&", " ", " &", " & "], &[], vec!["Foo", "Bar"])]
134    #[case::separation_no_exclusions("Foo & BarBaz", &["&", ";"], &[], vec!["Foo","BarBaz"])]
135    #[case::separation_no_exclusions("Foo & Bar; Baz", &["&", ";"], &[], vec!["Foo", "Bar", "Baz"])]
136    #[case::separation_excluded("Foo & Bar", &["&", ";"], &["Foo & Bar"], vec!["Foo & Bar"])]
137    #[case::separation_excluded("Foo & BarBaz", &["&", ";"], &["Foo & Bar"], vec!["Foo","BarBaz"])]
138    #[case::separation_excluded("Foo & Bar; Baz", &["&", ";"], &["Foo & Bar"], vec!["Foo & Bar", "Baz"])]
139    #[case::separation_excluded("Foo & BarBaz; Zing", &["&", ";"], &["Foo & Bar"], vec!["Foo","BarBaz", "Zing"])]
140    #[case::separation_excluded("Zing; Foo & BarBaz", &["&", ";"], &["Foo & Bar"], vec!["Zing","Foo","BarBaz"])]
141    #[case::separation_excluded("Foo & Bar; Baz; Zing", &["&", ";"], &["Foo & Bar"], vec!["Foo & Bar", "Baz", "Zing"])]
142    fn test_split_artist_name(
143        #[case] artist: &str,
144        #[case] separators: &[&str],
145        #[case] exceptions: &[&str],
146        #[case] expected: Vec<&str>,
147    ) {
148        let separators = separators.iter().map(|s| (*s).to_string()).collect();
149        let exceptions = exceptions.iter().map(|s| (*s).to_string()).collect();
150        let expected = expected
151            .into_iter()
152            .map(std::string::ToString::to_string)
153            .collect::<OneOrMany<String>>();
154        let artists = split_artist_name(artist, &OneOrMany::Many(separators), &exceptions);
155        assert_eq!(artists, expected);
156    }
157}