mirror of
https://github.com/nikolassv/bartib
synced 2024-11-28 06:20:28 +00:00
Add sanity check for start- and endtime of activities
This commit is contained in:
parent
fa7af0809a
commit
1c3f3ff495
4 changed files with 170 additions and 61 deletions
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())?,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue