use crate::{data, ui};
use ratatui::{prelude::*, widgets::*};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct NoteEnvStatistics {
pub id: String,
match_score: i64,
inlinks_global: usize,
inlinks_local: usize,
outlinks_local: usize,
outlinks_global: usize,
broken_links: usize,
}
impl NoteEnvStatistics {
fn new_empty(id: String, match_score: i64) -> Self {
Self {
id,
match_score,
inlinks_global: 0,
inlinks_local: 0,
outlinks_local: 0,
outlinks_global: 0,
broken_links: 0,
}
}
fn to_row(&self, index: data::NoteIndexContainer, styles: &ui::UiStyles) -> Option<Row<'_>> {
index.borrow().get(&self.id).map(|note| {
Row::new(vec![
note.display_name.clone(),
format!("{:7}", note.words),
format!("{:7}", note.characters),
format!("{:7}", self.outlinks_global),
format!("{:7}", self.outlinks_local),
format!("{:7}", self.inlinks_global),
format!("{:7}", self.inlinks_local),
])
.style(styles.text_style)
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum SortingMode {
#[default]
Name,
Words,
Chars,
GlobalOutLinks,
LocalOutLinks,
GlobalInLinks,
LocalInLinks,
Score,
Broken,
}
#[derive(Debug, Clone)]
pub struct EnvironmentStats {
word_count_total: usize,
char_count_total: usize,
note_count_total: usize,
tag_count_total: usize,
local_local_links: usize,
local_global_links: usize,
global_local_links: usize,
filtered_stats: Vec<NoteEnvStatistics>,
broken_links: usize,
}
impl EnvironmentStats {
pub fn new_with_filter(index: &super::NoteIndexContainer, filter: data::Filter) -> Self {
let index = index.borrow();
let mut filtered_index = index
.inner
.iter()
.filter_map(|(id, note)| {
filter.apply(note, &index).map(|score| {
(
id.clone(),
(NoteEnvStatistics::new_empty(id.clone(), score), note),
)
})
})
.collect::<HashMap<_, _>>();
for (id, note) in index.inner.iter() {
let local_source = filtered_index.contains_key(id);
let mut local_targets = 0;
let mut global_targets = 0;
for link in ¬e.links {
if index.inner.contains_key(link) {
global_targets += 1;
if let Some((target, _)) = filtered_index.get_mut(link) {
target.inlinks_global += 1;
if local_source {
target.inlinks_local += 1;
}
local_targets += 1;
}
}
}
if let Some((source, _)) = filtered_index.get_mut(id) {
source.outlinks_local += local_targets;
source.outlinks_global += global_targets;
source.broken_links = note.links.len() - global_targets;
}
}
Self {
word_count_total: filtered_index.values().map(|(_, stats)| stats.words).sum(),
char_count_total: filtered_index
.values()
.map(|(_, stats)| stats.characters)
.sum(),
note_count_total: filtered_index.len(),
tag_count_total: filtered_index
.values()
.flat_map(|(_, stats)| &stats.tags)
.collect::<std::collections::HashSet<_>>()
.len(),
local_local_links: filtered_index
.values()
.map(|(env_stats, _)| env_stats.outlinks_local)
.sum(),
local_global_links: filtered_index
.values()
.map(|(env_stats, _)| env_stats.outlinks_global)
.sum(),
global_local_links: filtered_index
.values()
.map(|(env_stats, _)| env_stats.inlinks_global)
.sum(),
broken_links: filtered_index
.values()
.map(|(env_stats, _)| env_stats.broken_links)
.sum(),
filtered_stats: {
let mut fs = filtered_index
.into_values()
.map(|(env_stats, _)| env_stats)
.collect::<Vec<_>>();
fs.sort_by_cached_key(|env_stats| env_stats.match_score);
fs.reverse();
fs
},
}
}
pub fn get_selected(&self, index: usize) -> Option<&NoteEnvStatistics> {
self.filtered_stats.get(index)
}
pub fn sort(&mut self, index: data::NoteIndexContainer, mode: SortingMode, ascending: bool) {
self.filtered_stats
.sort_by_cached_key(|env_stats| env_stats.id.clone());
if mode != SortingMode::Name {
if !ascending {
self.filtered_stats.reverse();
}
self.filtered_stats.sort_by_cached_key(|env_stats| {
if let Some(note) = index.borrow().get(&env_stats.id) {
match mode {
SortingMode::Name => 0,
SortingMode::Words => note.words,
SortingMode::Chars => note.characters,
SortingMode::GlobalOutLinks => env_stats.outlinks_global,
SortingMode::LocalOutLinks => env_stats.outlinks_local,
SortingMode::GlobalInLinks => env_stats.inlinks_global,
SortingMode::LocalInLinks => env_stats.inlinks_local,
SortingMode::Score => env_stats.match_score as usize,
SortingMode::Broken => env_stats.broken_links,
}
} else {
0
}
})
}
if !ascending {
self.filtered_stats.reverse();
}
}
pub fn len(&self) -> usize {
self.filtered_stats.len()
}
pub fn to_note_table(
&self,
index: data::NoteIndexContainer,
styles: &ui::UiStyles,
) -> Table<'_> {
let notes_table_widths = [
Constraint::Min(25),
Constraint::Length(8),
Constraint::Length(8),
Constraint::Length(10),
Constraint::Length(10),
Constraint::Length(10),
Constraint::Length(10),
];
let notes_rows = self
.filtered_stats
.iter()
.flat_map(|note_env| note_env.to_row(index.clone(), styles))
.collect::<Vec<Row>>();
Table::new(notes_rows, notes_table_widths).column_spacing(1)
}
pub fn to_global_stats_table(&self, styles: &ui::UiStyles) -> Table<'_> {
let stats_widths = [
Constraint::Length(20),
Constraint::Length(16),
Constraint::Length(20),
Constraint::Length(16),
Constraint::Min(0),
];
let global_stats_rows = [
Row::new(vec![
Cell::from("Total notes:").style(styles.text_style),
Cell::from(format!("{:7}", self.note_count_total)).style(styles.text_style),
Cell::from("Total words:").style(styles.text_style),
Cell::from(format!("{:7}", self.word_count_total)).style(styles.text_style),
]),
Row::new(vec![
Cell::from("Total unique tags:").style(styles.text_style),
Cell::from(format!("{:7}", self.tag_count_total)).style(styles.text_style),
Cell::from("Total characters:").style(styles.text_style),
Cell::from(format!("{:7}", self.char_count_total)).style(styles.text_style),
]),
Row::new(vec![
Cell::from("Total links:").style(styles.text_style),
Cell::from(format!("{:7}", self.local_local_links)).style(styles.text_style),
Cell::from("Broken links:").style(styles.text_style),
Cell::from(format!("{:7}", self.broken_links)).style(styles.text_style),
]),
];
Table::new(global_stats_rows, stats_widths).column_spacing(1)
}
pub fn to_local_stats_table(&self, global: &Self, styles: &ui::UiStyles) -> Table<'_> {
let stats_widths = [
Constraint::Length(20),
Constraint::Length(16),
Constraint::Length(20),
Constraint::Length(16),
Constraint::Min(0),
];
let local_stats_rows = [
Row::new(vec![
Cell::from("Total notes:").style(styles.text_style),
Cell::from(format!(
"{:7} ({:3}%)",
self.note_count_total,
self.note_count_total * 100 / global.note_count_total.max(1)
))
.style(styles.text_style),
Cell::from("Total words:").style(styles.text_style),
Cell::from(format!(
"{:7} ({:3}%)",
self.word_count_total,
self.word_count_total * 100 / global.word_count_total.max(1)
))
.style(styles.text_style),
]),
Row::new(vec![
Cell::from("Total unique tags:").style(styles.text_style),
Cell::from(format!(
"{:7} ({:3}%)",
self.tag_count_total,
self.tag_count_total * 100 / global.tag_count_total.max(1)
))
.style(styles.text_style),
Cell::from("Total characters:").style(styles.text_style),
Cell::from(format!(
"{:7} ({:3}%)",
self.char_count_total,
self.char_count_total * 100 / global.char_count_total.max(1)
))
.style(styles.text_style),
]),
Row::new(vec![
Cell::from("Incoming links:").style(styles.text_style),
Cell::from(format!(
"{:7} ({:3}%)",
self.global_local_links,
self.global_local_links * 100 / global.local_local_links.max(1),
))
.style(styles.text_style),
Cell::from("Outgoing links:").style(styles.text_style),
Cell::from(format!(
"{:7} ({:3}%)",
self.local_global_links,
self.local_global_links * 100 / global.local_local_links.max(1),
))
.style(styles.text_style),
]),
Row::new(vec![
Cell::from("Internal links:").style(styles.text_style),
Cell::from(format!(
"{:7} ({:3}%)",
self.local_local_links,
self.local_local_links * 100 / global.local_local_links.max(1),
))
.style(styles.text_style),
Cell::from("Broken links:").style(styles.text_style),
Cell::from(format!(
"{:7} ({:3}%)",
self.broken_links,
self.broken_links * 100 / global.broken_links.max(1)
))
.style(styles.text_style),
]),
];
Table::new(local_stats_rows, stats_widths).column_spacing(1)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{data, io};
#[test]
fn test_env_stats_1_tags_any() {
let config = crate::Config::default();
let tracker = io::FileTracker::new(&config, std::path::PathBuf::from("./tests")).unwrap();
let builder = io::HtmlBuilder::new(&config, std::path::PathBuf::from("./tests"));
let index = data::NoteIndex::new(tracker, builder).0;
assert_eq!(index.inner.len(), 12);
let index = std::rc::Rc::new(std::cell::RefCell::new(index));
let filter1 = data::Filter {
any: true,
tags: vec![
("#topology".to_string(), true),
("#diffgeo".to_string(), true),
],
links: vec![],
blinks: vec![],
title: String::new(),
full_text: None,
};
let env1 = EnvironmentStats::new_with_filter(&index, filter1);
assert_eq!(env1.note_count_total, 5);
assert_eq!(env1.tag_count_total, 3);
assert_eq!(env1.local_local_links, 10);
assert_eq!(env1.local_global_links, 11);
assert_eq!(env1.global_local_links, 13);
assert_eq!(env1.broken_links, 1);
}
#[test]
fn test_env_stats_2_tags_all() {
let config = crate::Config::default();
let tracker = io::FileTracker::new(&config, std::path::PathBuf::from("./tests")).unwrap();
let builder = io::HtmlBuilder::new(&config, std::path::PathBuf::from("./tests"));
let index = data::NoteIndex::new(tracker, builder).0;
assert_eq!(index.inner.len(), 12);
let index = std::rc::Rc::new(std::cell::RefCell::new(index));
let filter2 = data::Filter {
any: false,
tags: vec![
("#topology".to_string(), true),
("#diffgeo".to_string(), true),
],
links: vec![],
blinks: vec![],
title: String::new(),
full_text: None,
};
let env2 = EnvironmentStats::new_with_filter(&index, filter2);
assert_eq!(env2.note_count_total, 2);
assert_eq!(env2.tag_count_total, 2);
assert_eq!(env2.local_local_links, 1);
assert_eq!(env2.local_global_links, 5);
assert_eq!(env2.global_local_links, 6);
assert_eq!(env2.broken_links, 1);
env2.filtered_stats
.iter()
.filter(|env_stats| env_stats.id == "manifold")
.for_each(|ma| {
assert_eq!(ma.inlinks_global, 5);
assert_eq!(ma.inlinks_local, 1);
assert_eq!(ma.outlinks_local, 0);
assert_eq!(ma.outlinks_global, 4);
assert_eq!(ma.broken_links, 0);
});
}
#[test]
fn test_env_stats_3_title() {
let config = crate::Config::default();
let tracker = io::FileTracker::new(&config, std::path::PathBuf::from("./tests")).unwrap();
let builder = io::HtmlBuilder::new(&config, std::path::PathBuf::from("./tests"));
let index = data::NoteIndex::new(tracker, builder).0;
assert_eq!(index.inner.len(), 12);
let index = std::rc::Rc::new(std::cell::RefCell::new(index));
let filter3 = data::Filter {
any: false,
tags: vec![],
links: vec![],
blinks: vec![],
title: "operating".to_string(),
full_text: None,
};
let env3 = EnvironmentStats::new_with_filter(&index, filter3);
assert_eq!(env3.note_count_total, 1);
assert_eq!(env3.tag_count_total, 1);
assert_eq!(env3.local_local_links, 0);
assert_eq!(env3.local_global_links, 6);
assert_eq!(env3.global_local_links, 0);
assert_eq!(env3.broken_links, 0);
}
#[test]
fn test_env_stats_4_blinks() {
let config = crate::Config::default();
let tracker = io::FileTracker::new(&config, std::path::PathBuf::from("./tests")).unwrap();
let builder = io::HtmlBuilder::new(&config, std::path::PathBuf::from("./tests"));
let index = data::NoteIndex::new(tracker, builder).0;
assert_eq!(index.inner.len(), 12);
let index = std::rc::Rc::new(std::cell::RefCell::new(index));
let filter4 = data::Filter {
any: true,
tags: vec![],
links: vec![],
blinks: vec![("atlas".to_string(), true)],
title: String::new(),
full_text: None,
};
let env4 = EnvironmentStats::new_with_filter(&index, filter4);
assert_eq!(env4.note_count_total, 3);
assert_eq!(env4.tag_count_total, 2);
assert_eq!(env4.local_local_links, 2);
assert_eq!(env4.local_global_links, 5);
assert_eq!(env4.global_local_links, 9);
assert_eq!(env4.broken_links, 1);
}
#[test]
fn test_env_stats_5_links_blinks() {
let config = crate::Config::default();
let tracker = io::FileTracker::new(&config, std::path::PathBuf::from("./tests")).unwrap();
let builder = io::HtmlBuilder::new(&config, std::path::PathBuf::from("./tests"));
let index = data::NoteIndex::new(tracker, builder).0;
assert_eq!(index.inner.len(), 12);
let index = std::rc::Rc::new(std::cell::RefCell::new(index));
let filter5 = data::Filter {
any: true,
tags: vec![],
links: vec![("smooth-map".to_string(), true)],
blinks: vec![("atlas".to_string(), true)],
title: String::new(),
full_text: None,
};
let env5 = EnvironmentStats::new_with_filter(&index, filter5);
assert_eq!(env5.note_count_total, 4);
assert_eq!(env5.tag_count_total, 3);
assert_eq!(env5.local_local_links, 5);
assert_eq!(env5.local_global_links, 8);
assert_eq!(env5.global_local_links, 10);
assert_eq!(env5.broken_links, 1);
}
}