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,10 +102,11 @@ 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..]);
Ok(())
}
}

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 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),
}
let starttime = parse_timepart(time_parts[0])?;
let endtime: Option<NaiveDateTime> = if time_parts.len() > 1 {
Some(parse_timepart(time_parts[1])?)
} else {
endtime = None;
None
};
if let Some(endtime) = endtime {
if endtime < starttime {
return Err(ActivityError::NegativeDurationError);
}
}
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,34 +1,38 @@
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
started last. To achieve this we reverse the order of the activities before we extract the
set of descriptions and activities. Afterwards we also reverse the list of descriptions and
activities.
started last. To achieve this we reverse the order of the activities before we extract the
set of descriptions and activities. Afterwards we also reverse the list of descriptions and
activities.
e.g. if tasks have been started in this order: a, b, c, a, c the list of descriptions and
activities should have this order: b, a, c
*/
e.g. if tasks have been started in this order: a, b, c, a, c the list of descriptions and
activities should have this order: b, a, c
*/
activities.reverse();
let mut known_descriptions_and_projects: HashSet<(&String, &String)> = HashSet::new();
@ -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())
);
}
}