cover/src/library.rs

249 lines
6.1 KiB
Rust

use std::{
collections::HashSet,
hash::{Hash, Hasher},
};
use nanoserde::DeJson;
#[derive(Debug, Clone, DeJson)]
pub struct Library {
artists: HashSet<Artist>,
}
impl Library {
pub fn new(artists: impl IntoIterator<Item = Artist>) -> Library {
Library {
artists: artists.into_iter().collect(),
}
}
fn all_covers(&self) -> HashSet<&Song> {
self.artists
.iter()
.fold(HashSet::new(), |mut covers, artist| {
covers.extend(artist.covered_songs());
covers
})
}
pub fn library_covers(&self) -> HashSet<&Song> {
self.all_covers()
.into_iter()
.filter(|song| song.is_library_cover(self))
.collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq, DeJson)]
pub struct Artist {
name: String,
members: HashSet<String>,
catalog: HashSet<Song>,
}
impl Hash for Artist {
fn hash<H: Hasher>(&self, state: &mut H) {
self.name.hash(state);
}
}
impl Artist {
pub fn new(
name: String,
members: impl IntoIterator<Item = String>,
catalog: impl IntoIterator<Item = Song>,
) -> Artist {
Artist {
name,
members: members.into_iter().collect(),
catalog: catalog.into_iter().collect(),
}
}
fn covered_songs(&self) -> HashSet<&Song> {
self.catalog
.iter()
.filter(|song| song.is_covered_by(self))
.collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq, DeJson)]
pub struct Song {
pub title: String,
pub performer: String,
pub writers: HashSet<String>,
}
impl Hash for Song {
fn hash<H: Hasher>(&self, state: &mut H) {
self.title.hash(state);
self.performer.hash(state);
}
}
impl Song {
pub fn new(
title: String,
performer: String,
writers: impl IntoIterator<Item = String>,
) -> Song {
Song {
title,
performer,
writers: writers.into_iter().collect(),
}
}
fn is_library_cover(&self, library: &Library) -> bool {
library
.artists
.iter()
.any(|artist| self.is_cover_of(artist))
}
fn is_covered_by(&self, artist: &Artist) -> bool {
self.performer == artist.name && self.writers.is_disjoint(&artist.members)
}
fn is_cover_of(&self, artist: &Artist) -> bool {
self.performer != artist.name && !self.writers.is_disjoint(&artist.members)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn bm_members() -> HashSet<String> {
[
"Cameron Picton".to_string(),
"Matt Kelvin".to_string(),
"Morgan Simpson".to_string(),
"Geordie Greep".to_string(),
]
.into()
}
fn bmbmbm() -> Song {
Song {
title: "bmbmbm".to_string(),
performer: "black midi".to_string(),
writers: bm_members(),
}
}
fn love_story(performer: &str) -> Song {
Song {
title: "Love Story".to_string(),
performer: performer.to_string(),
writers: ["Taylor Swift".to_string()].into(),
}
}
fn schizoid_man() -> Song {
Song {
title: "21st Century Schizoid Man".to_string(),
performer: "black midi".to_string(),
writers: [
"Greg Lake".to_string(),
"Ian McDonald".to_string(),
"Michael Giles".to_string(),
"Peter Sinfield".to_string(),
"Robert Fripp".to_string(),
]
.into(),
}
}
fn exile() -> Song {
Song {
title: "Exile".to_string(),
performer: "Taylor Swift".to_string(),
writers: [
"Taylor Swift".to_string(),
"William Bowery".to_string(),
"Justin Vernon".to_string(),
]
.into(),
}
}
fn bm() -> Artist {
Artist {
name: "black midi".to_string(),
members: bm_members(),
catalog: [bmbmbm(), love_story("black midi"), schizoid_man()].into(),
}
}
fn ts() -> Artist {
Artist {
name: "Taylor Swift".to_string(),
members: ["Taylor Swift".to_string()].into(),
catalog: [love_story("Taylor Swift"), exile()].into(),
}
}
fn bon_iver() -> Artist {
Artist {
name: "Bon Iver".to_string(),
members: [
"Justin Vernon".to_string(),
"Sean Carey".to_string(),
"Michael Noyce".to_string(),
"Matthew McCaughan".to_string(),
]
.into(),
catalog: [
Song {
title: "Skinny Love".to_string(),
performer: "Bon Iver".to_string(),
writers: ["Justin Vernon".to_string()].into(),
},
Song {
title: "Holocene".to_string(),
performer: "Bon Iver".to_string(),
writers: ["Justin Vernon".to_string()].into(),
},
Song {
title: "The Times They Are A-Changin'".to_string(),
performer: "Bon Iver".to_string(),
writers: ["Bob Dylan".to_string()].into(),
},
]
.into(),
}
}
fn library() -> Library {
Library {
artists: [bm(), ts(), bon_iver()].into(),
}
}
#[test]
fn test_covered_songs() {
assert!(bm().covered_songs() == [&love_story("black midi"), &schizoid_man()].into())
}
#[test]
fn test_covered_songs_no_covers() {
assert!(ts().covered_songs().is_empty())
}
#[test]
fn test_find_covers() {
assert!(library().library_covers() == [&love_story("black midi")].into())
}
#[test]
fn test_find_covers_one_artist() {
assert!(Library {
artists: [bon_iver()].into()
}
.library_covers()
.is_empty())
}
}