use crate::table::Entry;
use anyhow::Result;
use std::collections::HashMap;
pub trait SongNameAccessor {
fn load(&self, md5: &str) -> Result<Option<String>>;
}
pub struct DbSongNameAccessor<'a> {
pub db: &'a rusqlite::Connection,
}
impl SongNameAccessor for DbSongNameAccessor<'_> {
fn load(&self, md5: &str) -> Result<Option<String>> {
crate::db::load_song(self.db, md5)
}
}
pub struct FallbackSongNameAccessor<'a> {
pub fallback: &'a HashMap<String, String>,
}
impl SongNameAccessor for FallbackSongNameAccessor<'_> {
fn load(&self, md5: &str) -> Result<Option<String>> {
Ok(self.fallback.get(md5).cloned())
}
}
fn load_first(accessors: &[&dyn SongNameAccessor], md5: &str) -> Result<Option<String>> {
for accessor in accessors {
if let Some(title) = accessor.load(md5)? {
return Ok(Some(title));
}
}
Ok(None)
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct TableDiff {
pub changed: Vec<(String, String, String)>,
pub deleted: Vec<(String, String)>,
pub new: Vec<(String, String)>,
}
impl TableDiff {
#[must_use]
pub fn new(old: &[Entry], new: &[Entry], folder_order: &[String]) -> Self {
use alphanumeric_sort::compare_str as acmp;
use itertools::Itertools;
let mut changes = std::collections::HashMap::<String, (Vec<String>, Vec<String>)>::new();
for e in old {
if !new.contains(e) {
changes
.entry(e.md5.clone())
.or_default()
.0
.push(e.level.clone());
}
}
for e in new {
if !old.contains(e) {
changes
.entry(e.md5.clone())
.or_default()
.1
.push(e.level.clone());
}
}
let mut changelog = Self {
changed: vec![],
deleted: vec![],
new: vec![],
};
for (md5, (deleted, added)) in changes {
for pair in deleted.into_iter().zip_longest(added) {
match pair {
itertools::EitherOrBoth::Both(from, to) => {
changelog.changed.push((md5.clone(), from, to));
}
itertools::EitherOrBoth::Left(deleted) => {
changelog.deleted.push((md5.clone(), deleted));
}
itertools::EitherOrBoth::Right(added) => {
changelog.new.push((md5.clone(), added));
}
}
}
}
let key_by_order = |e: &str| {
folder_order
.iter()
.position(|o| *o == *e)
.unwrap_or(usize::MAX)
};
let ocmp = |a: &String, b: &String| -> std::cmp::Ordering {
key_by_order(a).cmp(&key_by_order(b))
};
changelog.new.sort_unstable_by(|(lh, ll), (rh, rl)| {
ocmp(ll, rl)
.then_with(|| acmp(ll, rl))
.then_with(|| lh.cmp(rh))
});
changelog.deleted.sort_unstable_by(|(lh, ll), (rh, rl)| {
ocmp(ll, rl)
.then_with(|| acmp(ll, rl))
.then_with(|| lh.cmp(rh))
});
changelog
.changed
.sort_unstable_by(|(lh, _, ll), (rh, _, rl)| {
ocmp(ll, rl)
.then_with(|| acmp(ll, rl))
.then_with(|| lh.cmp(rh))
});
changelog
}
pub fn format_full(
&self,
accessors: &[&dyn SongNameAccessor],
symbol: &str,
user_symbol: Option<&str>,
) -> Result<String> {
let effective_symbol = user_symbol.unwrap_or(symbol);
let mut out = String::new();
if !self.new.is_empty() {
for (md5, level) in &self.new {
out += "+";
out += effective_symbol;
out += level;
out += " ";
out += load_first(accessors, md5)?.as_deref().unwrap_or(md5);
out += "\n";
}
}
if !self.changed.is_empty() {
if !out.is_empty() {
out += "\n";
}
for (md5, from, to) in &self.changed {
out += "~";
out += effective_symbol;
out += to;
out += " <- ";
out += effective_symbol;
out += from;
out += " ";
out += load_first(accessors, md5)?.as_deref().unwrap_or(md5);
out += "\n";
}
}
if !self.deleted.is_empty() {
if !out.is_empty() {
out += "\n";
}
for (md5, level) in &self.deleted {
out += "-";
out += effective_symbol;
out += level;
out += " ";
out += load_first(accessors, md5)?.as_deref().unwrap_or(md5);
out += "\n";
}
}
out.pop();
Ok(out)
}
pub fn format_summary(&self) -> Result<String> {
use std::fmt::Write as _;
let mut out = String::new();
if !self.new.is_empty() {
write!(out, "+{} ", self.new.len())?;
}
if !self.changed.is_empty() {
write!(out, "~{} ", self.changed.len())?;
}
if !self.deleted.is_empty() {
write!(out, "-{} ", self.deleted.len())?;
}
out.pop();
Ok(out)
}
}
#[cfg(test)]
mod tests {
use test_log::test;
#[test]
fn calculate_diff() {
use super::TableDiff;
use crate::table::Table;
assert_eq!(
TableDiff::new(
&Table::empty()
.with_entry(1, "1")
.with_entry(1, "1")
.with_entry(69, "1")
.with_entry(69, "2")
.with_entry(0x420, "1")
.with_entry(0x1234, "1")
.with_entry(0xbeef, "one")
.with_entry(0xbeef, "two")
.0
.entries,
&Table::empty()
.with_entry(0, "10")
.with_entry(1, "1")
.with_entry(2, "1")
.with_entry(2, "1")
.with_entry(69, "1")
.with_entry(69, "2")
.with_entry(0x420, "10")
.with_entry(0xbeef, "one")
.0
.entries,
&[]
),
TableDiff {
changed: vec![(
"foodfoodfoodfoodfoodfoodfood0420".to_string(),
"1".to_string(),
"10".to_string()
)],
deleted: vec![
(
"foodfoodfoodfoodfoodfoodfood1234".to_string(),
"1".to_string()
),
(
"foodfoodfoodfoodfoodfoodfoodbeef".to_string(),
"two".to_string()
)
],
new: vec![
(
"foodfoodfoodfoodfoodfoodfood0002".to_string(),
"1".to_string()
),
(
"foodfoodfoodfoodfoodfoodfood0002".to_string(),
"1".to_string()
),
(
"foodfoodfoodfoodfoodfoodfood0000".to_string(),
"10".to_string()
),
]
}
);
}
#[test]
fn format() {
let db = crate::db::tests::create_lr2_song_db();
crate::db::tests::insert_lr2_song(&db, "dbodfoodfoodfoodfoodfoodfoodfood");
let mut fallback = std::collections::HashMap::new();
fallback.insert(
"fbodfoodfoodfoodfoodfoodfoodfood".to_string(),
"fallback-name".to_string(),
);
let accessors: &[&dyn super::SongNameAccessor] = &[
&super::DbSongNameAccessor { db: &db },
&super::FallbackSongNameAccessor {
fallback: &fallback,
},
];
let changelog = super::TableDiff {
changed: vec![
(
"dbodfoodfoodfoodfoodfoodfoodfood".to_string(),
"from".to_string(),
"to".to_string(),
),
(
"fbodfoodfoodfoodfoodfoodfoodfood".to_string(),
"from".to_string(),
"to".to_string(),
),
(
"00odfoodfoodfoodfoodfoodfoodfood".to_string(),
"from".to_string(),
"to".to_string(),
),
],
deleted: vec![
(
"dbodfoodfoodfoodfoodfoodfoodfood".to_string(),
"del".to_string(),
),
(
"fbodfoodfoodfoodfoodfoodfoodfood".to_string(),
"del".to_string(),
),
(
"00odfoodfoodfoodfoodfoodfoodfood".to_string(),
"del".to_string(),
),
],
new: vec![
(
"dbodfoodfoodfoodfoodfoodfoodfood".to_string(),
"new".to_string(),
),
(
"fbodfoodfoodfoodfoodfoodfoodfood".to_string(),
"new".to_string(),
),
(
"00odfoodfoodfoodfoodfoodfoodfood".to_string(),
"new".to_string(),
),
],
};
assert_eq!(changelog.format_summary().unwrap(), "+3 ~3 -3");
assert_eq!(
changelog.format_full(accessors, "X", None).unwrap(),
"+Xnew 3y3s
+Xnew fallback-name
+Xnew 00odfoodfoodfoodfoodfoodfoodfood
~Xto <- Xfrom 3y3s
~Xto <- Xfrom fallback-name
~Xto <- Xfrom 00odfoodfoodfoodfoodfoodfoodfood
-Xdel 3y3s
-Xdel fallback-name
-Xdel 00odfoodfoodfoodfoodfoodfoodfood"
);
assert_eq!(
changelog.format_full(accessors, "X", Some("Z")).unwrap(),
"+Znew 3y3s
+Znew fallback-name
+Znew 00odfoodfoodfoodfoodfoodfoodfood
~Zto <- Zfrom 3y3s
~Zto <- Zfrom fallback-name
~Zto <- Zfrom 00odfoodfoodfoodfoodfoodfoodfood
-Zdel 3y3s
-Zdel fallback-name
-Zdel 00odfoodfoodfoodfoodfoodfoodfood"
);
}
}