diff --git a/Cargo.toml b/Cargo.toml index ca09507..d4ea53f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "syracuse" -version = "2.3.0" +version = "2.3.1" edition = "2021" [dependencies] diff --git a/src/algorithms.rs b/src/algorithms.rs index ae5823b..89692d8 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -9,53 +9,67 @@ pub fn smith_waterman(seq_1: &str, seq_2: &str) -> f64 { // initialization, seq_1 on the left and seq_2 on the top let seq_1: Vec = seq_1.chars().collect(); let seq_2: Vec = seq_2.chars().collect(); - let mut matrix : Vec> = vec![vec![0; seq_2.len()+1]; seq_1.len()+1]; + let mut matrix: Vec> = vec![vec![0; seq_2.len() + 1]; seq_1.len() + 1]; // matrix filling let mut total_score: i16 = 0; - let mut largest_score_index: (usize, usize) = (0,0); - for i in 1..seq_1.len()+1 { for j in 1..seq_2.len()+1 { - matrix[i][j] = { - let score = [ - 0, - matrix[i][j-1] + gap_penalty, - matrix[i-1][j] + gap_penalty, - if seq_1[i-1] == seq_2[j-1] { - matrix[i-1][j-1] + match_score - } else { - matrix[i-1][j-1] + mismatch_penalty - }, - ].into_iter().max().unwrap(); // safe unwrap - if score > total_score { - total_score = score; - largest_score_index = (i, j); + let mut largest_score_index: (usize, usize) = (0, 0); + for i in 1..seq_1.len() + 1 { + for j in 1..seq_2.len() + 1 { + matrix[i][j] = { + let score = [ + 0, + matrix[i][j - 1] + gap_penalty, + matrix[i - 1][j] + gap_penalty, + if seq_1[i - 1] == seq_2[j - 1] { + matrix[i - 1][j - 1] + match_score + } else { + matrix[i - 1][j - 1] + mismatch_penalty + }, + ] + .into_iter() + .max() + .unwrap(); // safe unwrap + if score > total_score { + total_score = score; + largest_score_index = (i, j); + } + score } - score } - }} + } // traceback let (mut i, mut j) = largest_score_index; let mut score = total_score; while score != 0 { score = [ - (i, j-1, matrix[i][j-1]), - (i-1, j, matrix[i-1][j]), - (i-1, j-1, matrix[i-1][j-1]) - ].into_iter().fold(0, |acc, (_i, _j, val)| { + (i, j - 1, matrix[i][j - 1]), + (i - 1, j, matrix[i - 1][j]), + (i - 1, j - 1, matrix[i - 1][j - 1]), + ] + .into_iter() + .fold(0, |acc, (_i, _j, val)| { if val > acc { - i = _i; j = _j; + i = _i; + j = _j; val - } else {acc} + } else { + acc + } }); total_score += score; } // normalisation let max_score = { - let val = [match_score, mismatch_penalty, gap_penalty].into_iter().max().unwrap().abs(); + let val = [match_score, mismatch_penalty, gap_penalty] + .into_iter() + .max() + .unwrap() + .abs(); let n = std::cmp::min(seq_1.len(), seq_2.len()) as i16 * val; - (n*(n+1))/2 + (n * (n + 1)) / 2 }; total_score as f64 / max_score as f64 @@ -70,44 +84,55 @@ pub fn needleman_wunsch(seq_1: &str, seq_2: &str) -> f64 { // initialization, seq_1 on the left and seq_2 on the top let seq_1: Vec = seq_1.chars().collect(); let seq_2: Vec = seq_2.chars().collect(); - let mut matrix : Vec> = vec![vec![0; seq_2.len()+1]; seq_1.len()+1]; + let mut matrix: Vec> = vec![vec![0; seq_2.len() + 1]; seq_1.len() + 1]; // matrix filling - for j in 1..seq_2.len()+1 {matrix[0][j] = j as i16 * gap_penalty} - for i in 1..seq_1.len()+1 {matrix[i][0] = i as i16 * gap_penalty} - for i in 1..seq_1.len()+1 { for j in 1..seq_2.len()+1 { - matrix[i][j] = { - [ - matrix[i][j-1] + gap_penalty, - matrix[i-1][j] + gap_penalty, - if seq_1[i-1] == seq_2[j-1] { - matrix[i-1][j-1] + match_score - } else { - matrix[i-1][j-1] + mismatch_penalty - } - ].into_iter().max().unwrap() // safe unwrap - } - }} + for j in 1..seq_2.len() + 1 { + matrix[0][j] = j as i16 * gap_penalty + } + for i in 1..seq_1.len() + 1 { + matrix[i][0] = i as i16 * gap_penalty + } + for i in 1..seq_1.len() + 1 { + for j in 1..seq_2.len() + 1 { + matrix[i][j] = { + [ + matrix[i][j - 1] + gap_penalty, + matrix[i - 1][j] + gap_penalty, + if seq_1[i - 1] == seq_2[j - 1] { + matrix[i - 1][j - 1] + match_score + } else { + matrix[i - 1][j - 1] + mismatch_penalty + }, + ] + .into_iter() + .max() + .unwrap() // safe unwrap + } + } + } // traceback let (mut i, mut j) = (seq_1.len(), seq_2.len()); let mut total_score: i16 = matrix[i][j]; while j != 0 && i != 0 { - if seq_1[i-1] == seq_2[j-1] { - i -= 1; j -= 1; - } - else { + if seq_1[i - 1] == seq_2[j - 1] { + i -= 1; + j -= 1; + } else { // this only considers a single path, gives priority to going 'upwards' - if matrix[i][j-1] > matrix[i-1][j] { + if matrix[i][j - 1] > matrix[i - 1][j] { j -= 1; - } else {i-=1} + } else { + i -= 1 + } } total_score += matrix[i][j] } // goes to (0, 0) when reaching an edge let i_gp = i as i16 * gap_penalty; let j_gp = j as i16 * gap_penalty; - total_score += i_gp*(i_gp+1)/2 + j_gp*(j_gp+1)/2; + total_score += i_gp * (i_gp + 1) / 2 + j_gp * (j_gp + 1) / 2; // normalisation // paranoid, but this should mathematically ensure the result is between -1 and 1 even if the user does something absurd like set the match_score to a negative integer @@ -115,20 +140,28 @@ pub fn needleman_wunsch(seq_1: &str, seq_2: &str) -> f64 { match total_score { 1.. => { let max_pos_score = { - let val = [match_score, mismatch_penalty, gap_penalty].into_iter().max().unwrap().abs(); + let val = [match_score, mismatch_penalty, gap_penalty] + .into_iter() + .max() + .unwrap() + .abs(); let n = std::cmp::max(seq_1.len(), seq_2.len()) as i16 * val; - (n*(n+1))/2 + (n * (n + 1)) / 2 }; total_score as f64 / max_pos_score as f64 - }, + } 0 => 0.0, ..=-1 => { let max_neg_score = { - let val = [match_score, mismatch_penalty, gap_penalty].into_iter().min().unwrap().abs(); + let val = [match_score, mismatch_penalty, gap_penalty] + .into_iter() + .min() + .unwrap() + .abs(); let n = std::cmp::max(seq_1.len(), seq_2.len()) as i16 * val; - (n*(n+1))/2 + (n * (n + 1)) / 2 }; total_score as f64 / max_neg_score as f64 - } + } } -} \ No newline at end of file +} diff --git a/src/animation.rs b/src/animation.rs index 2527ebd..c306d99 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -1,5 +1,5 @@ -use std::io::{self, Write}; use crossterm::style::Stylize; +use std::io::{self, Write}; use crate::warn; @@ -12,28 +12,47 @@ pub struct Animation { } impl Animation { - pub fn construct(builder: AnimationBuilder, max_focus_len: usize, min_focus_len: usize) -> Self { - let max_frame_len = builder.iter().map(|(l, r)| l.len() + r.len()).max().unwrap_or(0_usize) + max_focus_len; + pub fn construct( + builder: AnimationBuilder, + max_focus_len: usize, + min_focus_len: usize, + ) -> Self { + let max_frame_len = builder + .iter() + .map(|(l, r)| l.len() + r.len()) + .max() + .unwrap_or(0_usize) + + max_focus_len; Self { index: 0, - frames: builder.into_iter() - .map(|(l, r)| { - let num_repeats = max_frame_len - min_focus_len - l.len() - r.len(); - ("\r".to_string() + l.as_str(), r + " ".repeat(num_repeats).as_str()) - }) - .collect() + frames: builder + .into_iter() + .map(|(l, r)| { + let num_repeats = max_frame_len - min_focus_len - l.len() - r.len(); + ( + "\r".to_string() + l.as_str(), + r + " ".repeat(num_repeats).as_str(), + ) + }) + .collect(), } } pub fn step(&mut self, stdout: &mut io::Stdout, focus: &str) { if let Some((l, r)) = self.frames.get(self.index) { - let _ = stdout.write_all( - &[l.as_bytes(), focus.as_bytes(), r.as_bytes()].concat() - ).map_err(|err| {warn!("failed to write animation to stdout, {}", err);}); - let _ = stdout.flush().map_err(|err| {warn!("failed to flush stdout, {}", err);}); - if self.index < self.frames.len()-1 { + let _ = stdout + .write_all(&[l.as_bytes(), focus.as_bytes(), r.as_bytes()].concat()) + .map_err(|err| { + warn!("failed to write animation to stdout, {}", err); + }); + let _ = stdout.flush().map_err(|err| { + warn!("failed to flush stdout, {}", err); + }); + if self.index < self.frames.len() - 1 { self.index += 1 - } else {self.index = 0} + } else { + self.index = 0 + } } } -} \ No newline at end of file +} diff --git a/src/data/graphing.rs b/src/data/graphing.rs index 40e56fb..09c1673 100644 --- a/src/data/graphing.rs +++ b/src/data/graphing.rs @@ -4,7 +4,10 @@ use itertools::Itertools; use plotters::prelude::*; use std::path::PathBuf; -use super::{internal::{Entries, Entry}, syrtime::SyrDate}; +use super::{ + internal::{Entries, Entry}, + syrtime::SyrDate, +}; use crate::{config::Config, info, warn}; trait GraphMethods { @@ -13,15 +16,19 @@ trait GraphMethods { impl GraphMethods for Entry { fn get_points(&self, dates: &[SyrDate]) -> Vec<(f64, f64)> { - dates.iter().enumerate().map(|(idx, date)| { - match self.blocs.get(date) { - Some(nanos) => { - // idx + 1 since we pad our graph and 0 is not used - ((idx+1) as f64, *nanos as f64 / 36e11) - }, - None => ((idx+1) as f64, 0_f64), - } - }).collect_vec() + dates + .iter() + .enumerate() + .map(|(idx, date)| { + match self.blocs.get(date) { + Some(nanos) => { + // idx + 1 since we pad our graph and 0 is not used + ((idx + 1) as f64, *nanos as f64 / 36e11) + } + None => ((idx + 1) as f64, 0_f64), + } + }) + .collect_vec() } } @@ -33,21 +40,31 @@ pub fn graph(entries: Entries, start_date: SyrDate, end_date: SyrDate) -> anyhow let fine_grid_rgb = rgb_translate(Config::get().graph_fine_grid_rgb); let marker_size = Config::get().graph_marker_size; - let marker_color_wheel: Vec = Config::get().graph_marker_rgb.clone().into_iter().map(rgb_translate).collect(); + let marker_color_wheel: Vec = Config::get() + .graph_marker_rgb + .clone() + .into_iter() + .map(rgb_translate) + .collect(); let mcw_len = marker_color_wheel.len(); let mut mcw_idx: usize = 0; if marker_color_wheel.is_empty() { - Err(crate::error::Error{}).context("please provide at least one color in graph_marker_colors")?; + Err(crate::error::Error {}) + .context("please provide at least one color in graph_marker_colors")?; } let dates = SyrDate::expand_from_bounds(start_date, end_date); if dates.len() < 3 { - Err(crate::error::Error{}).context("at minimum, a span of three days is required to build a graph")? + Err(crate::error::Error {}) + .context("at minimum, a span of three days is required to build a graph")? } - let mut superpoints: Vec<(String, Vec<(f64, f64)>)> = entries.iter().map(|entry| (entry.name.clone(), entry.get_points(&dates))).collect(); + let mut superpoints: Vec<(String, Vec<(f64, f64)>)> = entries + .iter() + .map(|entry| (entry.name.clone(), entry.get_points(&dates))) + .collect(); let mut sum_points: Vec<(f64, f64)> = superpoints[0].1.clone(); for (_, points) in superpoints.iter().skip(1) { @@ -55,7 +72,16 @@ pub fn graph(entries: Entries, start_date: SyrDate, end_date: SyrDate) -> anyhow sum_points[idx].1 += point.1 } } - let max_y = sum_points.iter().map(|&(_, a)| a).max_by(|a, b| a.total_cmp(b)).unwrap_or(6.0).ceil(); + let max_y = sum_points + .iter() + .map(|&(_, a)| a) + .max_by(|a, b| a.total_cmp(b)) + .unwrap_or(6.0) + .ceil(); + if max_y == 0.0 { + warn!("no entries found within the given date range, returning early"); + return Ok(()); + } let image_width: u32 = dates.len() as u32 * 100 + 500; let image_height: u32 = 1080; @@ -76,8 +102,7 @@ pub fn graph(entries: Entries, start_date: SyrDate, end_date: SyrDate) -> anyhow }; info!("drawing..."); - let root = - BitMapBackend::new(&filepath, (image_width, image_height)).into_drawing_area(); + let root = BitMapBackend::new(&filepath, (image_width, image_height)).into_drawing_area(); root.fill::(&bg_rgb)?; let mut ctx = ChartBuilder::on(&root) @@ -86,7 +111,7 @@ pub fn graph(entries: Entries, start_date: SyrDate, end_date: SyrDate) -> anyhow .set_label_area_size(LabelAreaPosition::Left, 50) .set_label_area_size(LabelAreaPosition::Bottom, 50) // ignore 0 and pad by 2 to the right - .build_cartesian_2d(0_f64..(dates.len()+2) as f64, 0_f64..max_y)?; + .build_cartesian_2d(0_f64..(dates.len() + 2) as f64, 0_f64..max_y)?; ctx.configure_mesh() .axis_style(ShapeStyle { @@ -104,7 +129,7 @@ pub fn graph(entries: Entries, start_date: SyrDate, end_date: SyrDate) -> anyhow } }) // due to padding (1 on the left, 2 on the right) - .x_labels(dates.len()+3) + .x_labels(dates.len() + 3) .bold_line_style(coarse_grid_rgb.to_rgba().stroke_width(2)) .light_line_style(fine_grid_rgb.to_rgba().stroke_width(1)) .draw()?; @@ -116,7 +141,7 @@ pub fn graph(entries: Entries, start_date: SyrDate, end_date: SyrDate) -> anyhow .into_iter() .map(|coord| Circle::new(coord, 0, fg_rgb.stroke_width(2))), )?; - }, + } interpolation::InterpolationMethod::Makima => { ctx.draw_series( interpolation::makima(sum_points) @@ -136,13 +161,7 @@ pub fn graph(entries: Entries, start_date: SyrDate, end_date: SyrDate) -> anyhow ctx.draw_series( points .into_iter() - .map(|coord| - Circle::new( - coord, - marker_size, - color.stroke_width(2) - ) - ), + .map(|coord| Circle::new(coord, marker_size, color.stroke_width(2))), )? .label(name) .legend(move |point| Circle::new(point, marker_size, color.stroke_width(2))); @@ -159,13 +178,7 @@ pub fn graph(entries: Entries, start_date: SyrDate, end_date: SyrDate) -> anyhow ctx.draw_series( points .into_iter() - .map(|coord| - TriangleMarker::new( - coord, - marker_size, - color.stroke_width(2) - ) - ), + .map(|coord| TriangleMarker::new(coord, marker_size, color.stroke_width(2))), )? .label(name) .legend(move |coord| TriangleMarker::new(coord, marker_size, color.stroke_width(2))); @@ -182,13 +195,7 @@ pub fn graph(entries: Entries, start_date: SyrDate, end_date: SyrDate) -> anyhow ctx.draw_series( points .into_iter() - .map(|coord| - Cross::new( - coord, - marker_size, - color.stroke_width(2) - ) - ), + .map(|coord| Cross::new(coord, marker_size, color.stroke_width(2))), )? .label(name) .legend(move |coord| Cross::new(coord, marker_size, color.stroke_width(2))); @@ -245,46 +252,46 @@ pub mod interpolation { let step_size = (x_ip1 - x_i) / nb_points as f64; let mut x = x_i; while x < x_ip1 { - local_points.push((x, (y_i*(x_ip1 - x) + y_ip1*(x - x_i))/(x_ip1 - x_i))); + local_points.push((x, (y_i * (x_ip1 - x) + y_ip1 * (x - x_i)) / (x_ip1 - x_i))); x += step_size; } local_points - }).fold(Vec::new(), |mut global_points, local_points| { - global_points.extend(local_points); + }) + .fold(Vec::new(), |mut global_points, local_points| { + global_points.extend(local_points); global_points }) } - pub(super) fn makima(points: Vec<(f64, f64)>) -> Vec<(f64, f64)> { if points.len() < 5 { warn!("failed to interpolate data using makima, defaulting to linear, {} out of the 5 required points met", points.len()); return linear(points); - } + } let n = points.len() - 1; // slope between each point let m: Vec = points .iter() .tuple_windows() - .map(|(&(x_i, y_i), &(x_ip1, y_ip1))| { - (y_ip1 - y_i) / (x_ip1 - x_i) - }) + .map(|(&(x_i, y_i), &(x_ip1, y_ip1))| (y_ip1 - y_i) / (x_ip1 - x_i)) .collect(); // spline slopes let mut s: Vec = Vec::new(); // deals with the two first spline slopes - s.push(m[0]); s.push((m[0] + m[1])/2.0); - for i in 2..points.len()-2 { + s.push(m[0]); + s.push((m[0] + m[1]) / 2.0); + for i in 2..points.len() - 2 { s.push({ - let w_1 = (m[i+1] - m[i]).abs() + (m[i+1] + m[i]).abs()/2.0; - let w_2 = (m[i-1] - m[i-2]).abs() + (m[i-1] + m[i-2]).abs()/2.0; - (w_1 / (w_1 + w_2)) * m[i-1] + (w_2 / (w_1 + w_2)) * m[i] + let w_1 = (m[i + 1] - m[i]).abs() + (m[i + 1] + m[i]).abs() / 2.0; + let w_2 = (m[i - 1] - m[i - 2]).abs() + (m[i - 1] + m[i - 2]).abs() / 2.0; + (w_1 / (w_1 + w_2)) * m[i - 1] + (w_2 / (w_1 + w_2)) * m[i] }); } // deals with the last two spline slopes - s.push((m[n-3] + m[n-2])/2.0); s.push(m[n-1]); + s.push((m[n - 3] + m[n - 2]) / 2.0); + s.push(m[n - 1]); let nb_points = Config::get().graph_nb_interpolated_points; @@ -295,8 +302,8 @@ pub mod interpolation { .map(|(i, ((x_i, y_i), (x_ip1, _)))| { let a_i = y_i; let b_i = s[i]; - let c_i = (3.0*m[i] - 2.0*s[i] - s[i+1]) / (x_ip1 - x_i); - let d_i = (s[i] + s[i+1] - 2.0*m[i]) / (x_ip1 - x_i)*(x_ip1 - x_i); + let c_i = (3.0 * m[i] - 2.0 * s[i] - s[i + 1]) / (x_ip1 - x_i); + let d_i = (s[i] + s[i + 1] - 2.0 * m[i]) / (x_ip1 - x_i) * (x_ip1 - x_i); let mut local_points: Vec<(f64, f64)> = Vec::new(); @@ -304,17 +311,18 @@ pub mod interpolation { let step_size = (x_ip1 - x_i) / nb_points as f64; let mut x = x_i; while x < x_ip1 { - let y = a_i + b_i * (x-x_i) + c_i * (x-x_i)*(x-x_i) + d_i * (x-x_i)*(x-x_i)*(x-x_i); - local_points.push( - (x, if y < 0.0 {0.0} else {y}) - ); + let y = a_i + + b_i * (x - x_i) + + c_i * (x - x_i) * (x - x_i) + + d_i * (x - x_i) * (x - x_i) * (x - x_i); + local_points.push((x, if y < 0.0 { 0.0 } else { y })); x += step_size; } local_points }) .fold(Vec::new(), |mut global_points, local_points| { - global_points.extend(local_points); + global_points.extend(local_points); global_points }) } -} \ No newline at end of file +} diff --git a/src/data/syrtime.rs b/src/data/syrtime.rs index 38b78c8..dd281cd 100644 --- a/src/data/syrtime.rs +++ b/src/data/syrtime.rs @@ -1,11 +1,11 @@ -use std::collections::BTreeMap; use anyhow::Context; -use serde::{Serialize, Deserialize, de::Visitor}; use itertools::Itertools; +use serde::{de::Visitor, Deserialize, Serialize}; +use std::collections::BTreeMap; // u128 representing nanoseconds #[derive(Clone, Default, Serialize, Deserialize)] -pub(super) struct Blocs (BTreeMap); +pub(super) struct Blocs(BTreeMap); impl std::ops::Deref for Blocs { type Target = BTreeMap; @@ -93,7 +93,7 @@ impl SyrDate { impl std::fmt::Display for SyrDate { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( - f, + f, "{:0>2}/{:0>2}/{:0>4}", self.day(), self.month() as u8, @@ -110,10 +110,15 @@ impl From for SyrDate { impl TryFrom<&str> for SyrDate { type Error = anyhow::Error; - fn try_from(value: &str) -> Result - { - let Some(split_char) = ['/', '.', '-', '_'].into_iter().filter(|char| value.contains(*char)).nth(0) else { - Err(crate::error::Error{}).context("failed to parse date, no separator character detected, (e.g. '/', '.', '-', '_')")? + fn try_from(value: &str) -> Result { + let Some(split_char) = ['/', '.', '-', '_'] + .into_iter() + .filter(|char| value.contains(*char)) + .nth(0) + else { + Err(crate::error::Error {}).context( + "failed to parse date, no separator character detected, (e.g. '/', '.', '-', '_')", + )? }; let input: Vec<&str> = value.split(split_char).collect(); if input.len() != 3 { @@ -127,7 +132,6 @@ impl TryFrom<&str> for SyrDate { } } - impl Serialize for SyrDate { fn serialize(&self, serializer: S) -> Result where @@ -172,13 +176,17 @@ impl<'a> Visitor<'a> for SyrDateVisitor { where E: serde::de::Error, { - SyrDate::try_from(v).or(Err(E::custom("failed to parse date, invalid date format, expected dd/mm/yyyy"))) + SyrDate::try_from(v).or(Err(E::custom( + "failed to parse date, invalid date format, expected dd/mm/yyyy", + ))) } fn visit_string(self, v: String) -> Result where E: serde::de::Error, { - SyrDate::try_from(v.as_str()).or(Err(E::custom("ffailed to parse date, invalid date format, expected dd/mm/yyyy"))) + SyrDate::try_from(v.as_str()).or(Err(E::custom( + "ffailed to parse date, invalid date format, expected dd/mm/yyyy", + ))) } }