Add sanity check for start- and endtime of activities

This commit is contained in:
Lukas Dietrich 2022-03-31 22:30:30 +02:00
parent fa7af0809a
commit 1c3f3ff495
4 changed files with 170 additions and 61 deletions

View file

@ -18,7 +18,11 @@ pub fn list_running(file_name: &str) -> Result<()> {
// lists tracked activities
//
// the activities will be ordered chronologically.
pub fn list(file_name: &str, filter: getter::ActivityFilter, do_group_activities: bool) -> Result<()> {
pub fn list(
file_name: &str,
filter: getter::ActivityFilter,
do_group_activities: bool,
) -> Result<()> {
let file_content = bartib_file::get_file_content(file_name)?;
let activities = getter::get_activities(&file_content);
let mut filtered_activities: Vec<&activity::Activity> =
@ -26,18 +30,17 @@ pub fn list(file_name: &str, filter: getter::ActivityFilter, do_group_activities
filtered_activities.sort_by_key(|activity| activity.start);
let first_element = filtered_activities.len().saturating_sub(filter.number_of_activities.unwrap_or(filtered_activities.len()));
let first_element = filtered_activities.len().saturating_sub(
filter
.number_of_activities
.unwrap_or(filtered_activities.len()),
);
if do_group_activities {
list::list_activities_grouped_by_date(
&filtered_activities[first_element..],
);
list::list_activities_grouped_by_date(&filtered_activities[first_element..]);
} else {
let with_start_dates = filter.date.is_none();
list::list_activities(
&filtered_activities[first_element..],
with_start_dates,
);
list::list_activities(&filtered_activities[first_element..], with_start_dates);
}
Ok(())
@ -47,7 +50,8 @@ pub fn list(file_name: &str, filter: getter::ActivityFilter, do_group_activities
pub fn check(file_name: &str) -> Result<()> {
let file_content = bartib_file::get_file_content(file_name)?;
let number_of_errors = file_content.iter()
let number_of_errors = file_content
.iter()
.filter(|line| line.activity.is_err())
.count();
@ -58,11 +62,17 @@ pub fn check(file_name: &str) -> Result<()> {
println!("Found {} line(s) with parsing errors", number_of_errors);
file_content.iter()
file_content
.iter()
.filter(|line| line.activity.is_err() && line.plaintext.is_some())
.for_each(|line| {
if let Err(e) = &line.activity {
println!("\n{}\n -> {} (Line: {})", line.plaintext.as_ref().unwrap(), e.to_string(), line.line_number.unwrap_or(0));
println!(
"\n{}\n -> {} (Line: {})",
line.plaintext.as_ref().unwrap(),
e.to_string(),
line.line_number.unwrap_or(0)
);
}
});
@ -92,7 +102,8 @@ pub fn list_projects(file_name: &str, current: bool) -> Result<()> {
pub fn list_last_activities(file_name: &str, number: usize) -> Result<()> {
let file_content = bartib_file::get_file_content(file_name)?;
let descriptions_and_projects : Vec<(&String, &String)> = getter::get_descriptions_and_projects(&file_content);
let descriptions_and_projects: Vec<(&String, &String)> =
getter::get_descriptions_and_projects(&file_content);
let first_element = descriptions_and_projects.len().saturating_sub(number);
list::list_descriptions_and_projects(&descriptions_and_projects[first_element..]);

View file

@ -20,6 +20,10 @@ pub enum ActivityError {
DateTimeParseError,
#[error("could not parse activity")]
GeneralParseError,
#[error("the activity ended before it started")]
NegativeDurationError,
#[error("the activity was started before the previous ended")]
InvalidOrderError,
}
impl Activity {
@ -47,6 +51,26 @@ impl Activity {
Local::now().naive_local().signed_duration_since(self.start)
}
}
pub fn parse_with_preceeding(
plaintext: &str,
preceeding: Option<&Activity>,
) -> Result<Activity, ActivityError> {
let activity = Activity::from_str(plaintext)?;
if let Some(preceeding) = preceeding {
match preceeding.end {
Some(preceeding_end) => {
if preceeding_end > activity.start {
return Err(ActivityError::InvalidOrderError);
}
}
None => return Err(ActivityError::InvalidOrderError),
}
}
Ok(activity)
}
}
impl fmt::Display for Activity {
@ -76,7 +100,7 @@ impl fmt::Display for Activity {
// escapes the pipe character, so we can use it to separate the distinct parts of a activity
fn escape_special_chars(s: &str) -> String {
s.replace("\\", "\\\\").replace("|", "\\|")
s.replace('\\', "\\\\").replace('|', "\\|")
}
impl FromStr for Activity {
@ -91,22 +115,17 @@ impl FromStr for Activity {
let time_parts: Vec<&str> = parts[0].split(" - ").collect();
let starttime =
match NaiveDateTime::parse_from_str(time_parts[0].trim(), conf::FORMAT_DATETIME) {
Ok(t) => t,
Err(_) => return Err(ActivityError::DateTimeParseError),
let starttime = parse_timepart(time_parts[0])?;
let endtime: Option<NaiveDateTime> = if time_parts.len() > 1 {
Some(parse_timepart(time_parts[1])?)
} else {
None
};
let endtime: Option<NaiveDateTime>;
if time_parts.len() > 1 {
endtime =
match NaiveDateTime::parse_from_str(time_parts[1].trim(), conf::FORMAT_DATETIME) {
Ok(t) => Some(t),
Err(_) => return Err(ActivityError::DateTimeParseError),
if let Some(endtime) = endtime {
if endtime < starttime {
return Err(ActivityError::NegativeDurationError);
}
} else {
endtime = None;
}
let project = parts[1].trim();
@ -123,6 +142,11 @@ impl FromStr for Activity {
}
}
fn parse_timepart(time_part: &str) -> Result<NaiveDateTime, ActivityError> {
NaiveDateTime::parse_from_str(time_part.trim(), conf::FORMAT_DATETIME)
.map_err(|_| ActivityError::DateTimeParseError)
}
/**
* an iterator for splitting strings at the pipe character "|"
*
@ -316,5 +340,29 @@ mod tests {
let t = Activity::from_str("asb - 2021- | project");
assert!(matches!(t, Err(ActivityError::DateTimeParseError)));
let t = Activity::from_str("2022-03-31 21:31 - 2022-03-31 21:15 | project");
assert!(matches!(t, Err(ActivityError::NegativeDurationError)));
}
#[test]
fn parse_with_preceeding_errors() {
let p = Activity::from_str("2022-03-31 21:31 - 2022-03-31 21:35 | project").unwrap();
let t = Activity::parse_with_preceeding("2022-03-31 21:30 | project", Some(&p));
assert!(matches!(t, Err(ActivityError::InvalidOrderError)));
let p = Activity::from_str("2022-03-31 21:31 | project").unwrap();
let t = Activity::parse_with_preceeding("2022-03-31 21:30 | project", Some(&p));
assert!(matches!(t, Err(ActivityError::InvalidOrderError)));
}
#[test]
fn parse_with_preceeding_ok() {
let t = Activity::parse_with_preceeding("2022-03-31 21:30 | project", None);
assert!(t.is_ok());
let p = Activity::from_str("2022-03-31 21:31 - 2022-03-31 21:35 | project").unwrap();
let t = Activity::parse_with_preceeding("2022-03-31 21:35 | project", Some(&p));
assert!(t.is_ok());
}
}

View file

@ -2,7 +2,6 @@ use anyhow::{Context, Result};
use std::fs::{File, OpenOptions};
use std::io;
use std::io::{BufRead, BufReader, Write};
use std::str::FromStr;
use crate::data::activity;
@ -28,11 +27,15 @@ pub struct Line {
impl Line {
// creates a new line struct from plaintext
pub fn new(plaintext: &str, line_number: usize) -> Line {
pub fn new(plaintext: &str, line_number: usize, preceeding_line: Option<&Line>) -> Line {
let preceeding_activity = preceeding_line
.map(|line| line.activity.as_ref().ok())
.flatten();
Line {
plaintext: Some(plaintext.trim().to_string()),
line_number: Some(line_number),
activity: activity::Activity::from_str(plaintext),
activity: activity::Activity::parse_with_preceeding(plaintext, preceeding_activity),
status: LineStatus::Unchanged,
}
}
@ -63,8 +66,14 @@ pub fn get_file_content(file_name: &str) -> Result<Vec<Line>> {
.lines()
.filter_map(|line_result| line_result.ok())
.enumerate()
.map(|(line_number, line)| Line::new(&line, line_number.saturating_add(1)))
.collect();
.fold(Vec::new(), |mut lines, (line_number, line)| {
lines.push(Line::new(
&line,
line_number.saturating_add(1),
lines.last(),
));
lines
});
Ok(lines)
}
@ -81,7 +90,7 @@ pub fn write_to_file(file_name: &str, file_content: &[Line]) -> Result<(), io::E
} else {
write!(&file_handler, "{}", line.activity.as_ref().unwrap())?
}
},
}
LineStatus::Changed => write!(&file_handler, "{}", line.activity.as_ref().unwrap())?,
}
}

View file

@ -1,24 +1,28 @@
use std::collections::HashSet;
use chrono::{naive, NaiveDate};
use std::collections::HashSet;
use crate::data::activity;
use crate::data::bartib_file;
use crate::data::activity::Activity;
use crate::data::bartib_file;
pub struct ActivityFilter<'a> {
pub number_of_activities: Option<usize>,
pub from_date: Option<NaiveDate>,
pub to_date: Option<NaiveDate>,
pub date: Option<NaiveDate>,
pub project: Option<&'a str>
pub project: Option<&'a str>,
}
pub fn get_descriptions_and_projects(file_content: &[bartib_file::Line]) -> Vec<(&String, &String)> {
let mut activities : Vec<&activity::Activity> = get_activities(file_content).collect();
pub fn get_descriptions_and_projects(
file_content: &[bartib_file::Line],
) -> Vec<(&String, &String)> {
let mut activities: Vec<&activity::Activity> = get_activities(file_content).collect();
get_descriptions_and_projects_from_activities(&mut activities)
}
fn get_descriptions_and_projects_from_activities<'a>(activities: &mut [&'a Activity]) -> Vec<(&'a String, &'a String)> {
fn get_descriptions_and_projects_from_activities<'a>(
activities: &mut [&'a Activity],
) -> Vec<(&'a String, &'a String)> {
activities.sort_by_key(|activity| activity.start);
/* each activity should be placed in the list in the descending order of when it had been
@ -51,11 +55,21 @@ pub fn get_running_activities(file_content: &[bartib_file::Line]) -> Vec<&activi
.collect()
}
pub fn get_activities(file_content: &[bartib_file::Line]) -> impl Iterator<Item = &activity::Activity> {
pub fn get_activities(
file_content: &[bartib_file::Line],
) -> impl Iterator<Item = &activity::Activity> {
file_content
.iter()
.map(|line| line.activity.as_ref())
.filter_map(|activity_result| activity_result.ok())
.filter_map(|line: &bartib_file::Line| match &line.activity {
Ok(activity) => Some(activity),
Err(_) => {
println!(
"Warning: Ignoring line {}. Please see `bartib check` for further information",
line.line_number.unwrap_or(0),
);
None
}
})
}
pub fn filter_activities<'a>(
@ -74,17 +88,30 @@ pub fn filter_activities<'a>(
}
activities
.filter(move |activity| {activity.start.date() >= from_date && activity.start.date() <= to_date})
.filter(move |activity| filter.project.map(|p| activity.project == *p).unwrap_or(true))
.filter(move |activity| {
activity.start.date() >= from_date && activity.start.date() <= to_date
})
.filter(move |activity| {
filter
.project
.map(|p| activity.project == *p)
.unwrap_or(true)
})
}
pub fn get_last_activity_by_end(file_content: &[bartib_file::Line]) -> Option<&activity::Activity> {
get_activities(file_content)
.filter(|activity| activity.is_stopped())
.max_by_key(|activity| activity.end.unwrap_or_else(||naive::MIN_DATE.and_hms(0, 0, 0)))
.max_by_key(|activity| {
activity
.end
.unwrap_or_else(|| naive::MIN_DATE.and_hms(0, 0, 0))
})
}
pub fn get_last_activity_by_start(file_content: &[bartib_file::Line]) -> Option<&activity::Activity> {
pub fn get_last_activity_by_start(
file_content: &[bartib_file::Line],
) -> Option<&activity::Activity> {
get_activities(file_content).max_by_key(|activity| activity.start)
}
@ -99,11 +126,18 @@ mod tests {
let a3 = activity::Activity::start("p2".to_string(), "d1".to_string(), None);
let mut activities = vec![&a1, &a2, &a3];
let descriptions_and_projects = get_descriptions_and_projects_from_activities(&mut activities);
let descriptions_and_projects =
get_descriptions_and_projects_from_activities(&mut activities);
assert_eq!(descriptions_and_projects.len(), 2);
assert_eq!(*descriptions_and_projects.get(0).unwrap(), (&"d1".to_string(), &"p1".to_string()));
assert_eq!(*descriptions_and_projects.get(1).unwrap(), (&"d1".to_string(), &"p2".to_string()));
assert_eq!(
*descriptions_and_projects.get(0).unwrap(),
(&"d1".to_string(), &"p1".to_string())
);
assert_eq!(
*descriptions_and_projects.get(1).unwrap(),
(&"d1".to_string(), &"p2".to_string())
);
}
#[test]
@ -113,10 +147,17 @@ mod tests {
let a3 = activity::Activity::start("p1".to_string(), "d1".to_string(), None);
let mut activities = vec![&a1, &a2, &a3];
let descriptions_and_projects = get_descriptions_and_projects_from_activities(&mut activities);
let descriptions_and_projects =
get_descriptions_and_projects_from_activities(&mut activities);
assert_eq!(descriptions_and_projects.len(), 2);
assert_eq!(*descriptions_and_projects.get(0).unwrap(), (&"d1".to_string(), &"p2".to_string()));
assert_eq!(*descriptions_and_projects.get(1).unwrap(), (&"d1".to_string(), &"p1".to_string()));
assert_eq!(
*descriptions_and_projects.get(0).unwrap(),
(&"d1".to_string(), &"p2".to_string())
);
assert_eq!(
*descriptions_and_projects.get(1).unwrap(),
(&"d1".to_string(), &"p1".to_string())
);
}
}