249 lines
6.1 KiB
Rust
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())
|
|
}
|
|
}
|