use std::collections::{BTreeSet, HashMap};
use crate::classify::VersionDraft;
use crate::credits::Credit;
use crate::types::{Artist, ArtistRole, RawVersion, VersionDelimiter};
use crate::words::dedup_key;
fn precedence(role: ArtistRole) -> u8 {
match role {
ArtistRole::Primary => 2,
ArtistRole::Featured => 1,
ArtistRole::Producer | ArtistRole::Remixer => 0,
}
}
pub(crate) fn to_artist(c: Credit) -> Artist {
Artist { name: c.name, role: c.role, joiner: c.joiner, source: c.source }
}
struct Entry {
a: Artist,
list: usize,
alive: bool,
}
struct Lists(HashMap<usize, BTreeSet<usize>>);
impl Lists {
fn new(entries: &[Entry]) -> Lists {
let mut lists: HashMap<usize, BTreeSet<usize>> = HashMap::new();
for (i, e) in entries.iter().enumerate() {
lists.entry(e.list).or_default().insert(i);
}
Lists(lists)
}
fn remove(&mut self, list: usize, e: usize) {
if let Some(members) = self.0.get_mut(&list) {
members.remove(&e);
}
}
fn insert(&mut self, list: usize, e: usize) {
self.0.entry(list).or_default().insert(e);
}
fn heir(&self, list: usize, e: usize) -> Option<usize> {
let members = self.0.get(&list)?;
if members.first() != Some(&e) {
return None;
}
members.range(e + 1..).next().copied()
}
}
fn pass_joiner(entries: &mut [Entry], lists: &Lists, e: usize, joiner: Option<String>) {
let Some(list) = entries.get(e).map(|x| x.list) else { return };
if let Some(next) = lists.heir(list, e).and_then(|i| entries.get_mut(i)) {
next.a.joiner = joiner;
}
}
pub(crate) fn dedup_artists(credits: Vec<Credit>) -> Vec<Artist> {
let mut entries: Vec<Entry> = credits
.into_iter()
.map(|c| {
let list = c.list;
Entry { a: to_artist(c), list, alive: true }
})
.collect();
let mut lists = Lists::new(&entries);
let mut index: HashMap<String, usize> = HashMap::new();
for i in 0..entries.len() {
let Some(e) = entries.get(i) else { continue };
let class = if e.a.role == ArtistRole::Producer { "p" } else { "c" };
let key = format!("{class}:{}", dedup_key(&e.a.name));
let (e_role, e_joiner, e_source, e_list) =
(e.a.role, e.a.joiner.clone(), e.a.source, e.list);
let Some(&k) = index.get(&key) else {
index.insert(key, i);
continue;
};
let Some((kept_role, kept_joiner, kept_list)) =
entries.get(k).map(|x| (x.a.role, x.a.joiner.clone(), x.list))
else {
continue;
};
if precedence(e_role) > precedence(kept_role) {
pass_joiner(&mut entries, &lists, k, kept_joiner);
if let Some(kept) = entries.get_mut(k) {
kept.a.role = e_role;
kept.a.joiner = e_joiner;
kept.a.source = e_source;
kept.list = e_list;
}
lists.remove(kept_list, k);
lists.insert(e_list, k);
} else {
pass_joiner(&mut entries, &lists, i, e_joiner);
}
if let Some(e) = entries.get_mut(i) {
e.alive = false;
}
lists.remove(e_list, i);
}
entries.into_iter().filter(|e| e.alive).map(|e| e.a).collect()
}
pub(crate) fn to_version(v: VersionDraft) -> RawVersion {
RawVersion {
vtype: v.vtype,
raw: v.raw,
artists: v.artists.into_iter().map(to_artist).collect(),
modifiers: v.modifiers,
descriptor: v.descriptor,
year: v.year,
unknown_artist: v.unknown_artist,
delimiter: v.delimiter,
}
}
pub(crate) fn render_version(raw: &str, delimiter: VersionDelimiter) -> String {
match delimiter {
VersionDelimiter::Paren => format!("({raw})"),
VersionDelimiter::Square => format!("[{raw}]"),
VersionDelimiter::Curly => format!("{{{raw}}}"),
VersionDelimiter::Dash => format!("- {raw}"),
}
}
pub(crate) fn full_title_of(title: &str, versions: &[RawVersion]) -> String {
std::iter::once(title.to_string())
.chain(versions.iter().map(|v| render_version(&v.raw, v.delimiter)))
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ")
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
mod tests {
use super::*;
use crate::types::ArtistSource;
fn reference(credits: Vec<Credit>) -> Vec<Artist> {
fn pass(entries: &mut [Entry], e: usize, joiner: Option<String>) {
let list = entries[e].list;
let mut members =
entries.iter_mut().enumerate().filter(|(_, x)| x.alive && x.list == list);
if members.next().map(|(i, _)| i) != Some(e) {
return;
}
if let Some((_, next)) = members.next() {
next.a.joiner = joiner;
}
}
let mut entries: Vec<Entry> = credits
.into_iter()
.map(|c| {
let list = c.list;
Entry { a: to_artist(c), list, alive: true }
})
.collect();
let mut index: HashMap<String, usize> = HashMap::new();
for i in 0..entries.len() {
let e = &entries[i];
let class = if e.a.role == ArtistRole::Producer { "p" } else { "c" };
let key = format!("{class}:{}", dedup_key(&e.a.name));
let (role, joiner, source, list) = (e.a.role, e.a.joiner.clone(), e.a.source, e.list);
let Some(&k) = index.get(&key) else {
index.insert(key, i);
continue;
};
if precedence(role) > precedence(entries[k].a.role) {
let kept_joiner = entries[k].a.joiner.clone();
pass(&mut entries, k, kept_joiner);
let kept = &mut entries[k];
kept.a.role = role;
kept.a.joiner = joiner;
kept.a.source = source;
kept.list = list;
} else {
pass(&mut entries, i, joiner);
}
entries[i].alive = false;
}
entries.into_iter().filter(|e| e.alive).map(|e| e.a).collect()
}
#[test]
fn dedup_matches_the_plain_algorithm() {
let mut state = 0x2545_f491_u32;
let mut below = |n: u32| {
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
(state % n) as usize
};
let roles =
[ArtistRole::Primary, ArtistRole::Featured, ArtistRole::Producer, ArtistRole::Remixer];
let sources = [ArtistSource::Artist, ArtistSource::Title, ArtistSource::Version];
let names = ["a", "A", "b", "c", "é", "d"];
for _ in 0..20_000 {
let lists = 1 + below(4);
let credits: Vec<Credit> = (0..below(14))
.map(|i| Credit {
name: names[below(names.len() as u32)].to_string(),
role: roles[below(4)],
joiner: [None, Some("&".to_string()), Some(format!("j{i}"))][below(3)].clone(),
source: sources[below(3)],
pos: i as i64,
list: below(lists as u32),
})
.collect();
assert_eq!(dedup_artists(credits.clone()), reference(credits.clone()), "{credits:?}");
}
}
}