use crate::TableEntry;
use anyhow::Result;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct TableUpdateChangelog {
full: String,
summary: String,
}
impl TableUpdateChangelog {
pub fn try_new(
db: &rusqlite::Connection,
old: &[TableEntry],
new: &[TableEntry],
folder_order: &[String],
symbol: &str,
) -> Result<Self> {
let raw = RawTableUpdateChangelog::new(old, new, folder_order);
Ok(Self {
full: raw.full(db, 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: &[TableEntry], new: &[TableEntry], 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, db: &rusqlite::Connection, symbol: &str) -> Result<String> {
use crate::db::load_song;
let mut out = String::new();
if !self.new.is_empty() {
for (md5, level) in &self.new {
out += "+";
out += symbol;
out += level;
out += " ";
out += load_song(db, 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 += load_song(db, 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 += load_song(db, md5)?.as_deref().unwrap_or(md5);
out += "\n";
}
}
out.pop();
Ok(out)
}
fn summary(&self) -> String {
use std::fmt::Write as _;
let mut out = String::new();
if !self.new.is_empty() {
write!(out, "+{} ", self.new.len()).unwrap();
}
if !self.changed.is_empty() {
write!(out, "~{} ", self.changed.len()).unwrap();
}
if !self.deleted.is_empty() {
write!(out, "-{} ", self.deleted.len()).unwrap();
}
out.pop();
out
}
}
#[cfg(test)]
mod tests {
use test_log::test;
#[test]
fn calculate_diff() {
use super::RawTableUpdateChangelog;
use crate::Table;
assert_eq!(
RawTableUpdateChangelog::new(
&Table::empty()
.with_entry_leveled_plus(1, 1)
.with_entry_leveled_plus(1, 1)
.with_entry_leveled_plus(69, 1)
.with_entry_leveled_plus(69, 2)
.with_entry_leveled_plus(420, 1)
.with_entry_leveled_plus(1234, 1)
.0
.entries,
&Table::empty()
.with_entry_leveled_plus(0, 10)
.with_entry_leveled_plus(1, 1)
.with_entry_leveled_plus(2, 1)
.with_entry_leveled_plus(2, 1)
.with_entry_leveled_plus(69, 1)
.with_entry_leveled_plus(69, 2)
.with_entry_leveled_plus(420, 10)
.0
.entries,
&[]
),
RawTableUpdateChangelog {
changed: vec![(
"foodfoodfoodfoodfoodfoodfood 420".to_string(),
"1".to_string(),
"10".to_string()
)],
deleted: vec![
(
"foodfoodfoodfoodfoodfoodfood1234".to_string(),
"1".to_string()
),
],
new: vec![
(
"foodfoodfoodfoodfoodfoodfood 2".to_string(),
"1".to_string()
),
(
"foodfoodfoodfoodfoodfoodfood 2".to_string(),
"1".to_string()
),
(
"foodfoodfoodfoodfoodfoodfood 0".to_string(),
"10".to_string()
),
]
}
);
}
}