use crate::table::Entry;
use anyhow::Result;
use std::collections::HashMap;
pub struct SongNameAccessor<'a> {
db: &'a rusqlite::Connection,
fallback: &'a HashMap<String, String>,
}
impl<'a> SongNameAccessor<'a> {
pub const fn new(db: &'a rusqlite::Connection, fallback: &'a HashMap<String, String>) -> Self {
Self { db, fallback }
}
pub fn load(&self, md5: &str) -> Result<Option<String>> {
if let Some(title) = crate::db::load_song(self.db, md5)? {
Ok(Some(title))
} else {
Ok(self.fallback.get(md5).cloned())
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct TableUpdateChangelog {
full: String,
summary: String,
}
impl TableUpdateChangelog {
pub fn try_new(
song_loader: &SongNameAccessor,
old: &[Entry],
new: &[Entry],
folder_order: &[String],
symbol: &str,
) -> Result<Self> {
let raw = RawTableUpdateChangelog::new(old, new, folder_order);
Ok(Self {
full: raw.full(song_loader, symbol)?,
summary: raw.summary()?,
})
}
pub fn full(&'_ self) -> &'_ str {
&self.full
}
pub fn summary(&'_ self) -> &'_ str {
&self.summary
}
}
#[derive(Clone, Debug, Default, PartialEq)]
struct RawTableUpdateChangelog {
changed: Vec<(String, String, String)>,
deleted: Vec<(String, String)>,
new: Vec<(String, String)>,
}
impl RawTableUpdateChangelog {
fn new(old: &[Entry], new: &[Entry], folder_order: &[String]) -> Self {
use alphanumeric_sort::compare_str as acmp;
let mut changelog = Self {
changed: vec![],
deleted: vec![],
new: vec![],
};
for e in old {
#[derive(PartialEq)]
enum Found {
Not,
DifferentLv(String),
SameLv,
}
let mut found = Found::Not;
for ee in new.iter().filter(|ee| e.md5 == ee.md5) {
if e.level == ee.level {
found = Found::SameLv;
break;
}
found = Found::DifferentLv(ee.level.clone());
}
match found {
Found::Not => changelog.deleted.push((e.md5.clone(), e.level.clone())),
Found::DifferentLv(to) => {
changelog.changed.push((e.md5.clone(), e.level.clone(), to));
}
Found::SameLv => {}
}
}
for e in new {
if !old.iter().any(|ee| e.md5 == ee.md5) {
changelog.new.push((e.md5.clone(), e.level.clone()));
}
}
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
}
fn full(&self, song_loader: &SongNameAccessor, symbol: &str) -> Result<String> {
let mut out = String::new();
if !self.new.is_empty() {
for (md5, level) in &self.new {
out += "+";
out += symbol;
out += level;
out += " ";
out += song_loader.load(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 += symbol;
out += to;
out += " <- ";
out += symbol;
out += from;
out += " ";
out += song_loader.load(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 += symbol;
out += level;
out += " ";
out += song_loader.load(md5)?.as_deref().unwrap_or(md5);
out += "\n";
}
}
out.pop();
Ok(out)
}
fn 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::RawTableUpdateChangelog;
use crate::table::Table;
assert_eq!(
RawTableUpdateChangelog::new(
&Table::empty()
.with_new_entry(1, "1")
.with_new_entry(1, "1")
.with_new_entry(69, "1")
.with_new_entry(69, "2")
.with_new_entry(420, "1")
.with_new_entry(1234, "1")
.0
.entries,
&Table::empty()
.with_new_entry(0, "10")
.with_new_entry(1, "1")
.with_new_entry(2, "1")
.with_new_entry(2, "1")
.with_new_entry(69, "1")
.with_new_entry(69, "2")
.with_new_entry(420, "10")
.0
.entries,
&[]
),
RawTableUpdateChangelog {
changed: vec![(
"foodfoodfoodfoodfoodfoodfood0420".to_string(),
"1".to_string(),
"10".to_string()
)],
deleted: vec![
(
"foodfoodfoodfoodfoodfoodfood1234".to_string(),
"1".to_string()
),
],
new: vec![
(
"foodfoodfoodfoodfoodfoodfood0002".to_string(),
"1".to_string()
),
(
"foodfoodfoodfoodfoodfoodfood0002".to_string(),
"1".to_string()
),
(
"foodfoodfoodfoodfoodfoodfood0000".to_string(),
"10".to_string()
),
]
}
);
}
}