use std::fs::File;
use std::io::{self, BufRead, BufReader};
use std::path::PathBuf;
use chrono::prelude::*;
use thiserror::Error;
use crate::Activity;
#[derive(Error, Debug)]
pub enum ParseError {
#[error(transparent)]
FileError(#[from] io::Error),
#[error(transparent)]
CannotParseDateTime(#[from] chrono::format::ParseError),
#[error("no line to parse")]
NoLinetoParse,
}
pub fn parse_line(line: String) -> Result<Activity, ParseError> {
if line.is_empty() {
return Err(ParseError::NoLinetoParse);
}
let parts = line
.split('#')
.map(|part| part.trim())
.collect::<Vec<&str>>();
let (start, end) = match parts[0].split_once(" - ") {
Some(v) => (
v.0.trim().parse::<DateTime<Utc>>()?,
Some(v.1.trim().parse::<DateTime<Utc>>()?),
),
None => (parts[0].parse::<DateTime<Utc>>()?, None),
};
let tags = match parts.get(1) {
Some(v) => {
if v.trim() == "" {
vec![]
} else {
v.split(' ').map(|tag| tag.to_string()).collect()
}
}
None => vec![],
};
let description = match parts.get(2) {
Some(v) => {
if v.trim() == "" {
None
} else {
Some(v.to_string())
}
}
None => None,
};
Ok(Activity::new(start, end, tags, description))
}
pub fn parse_file(path: &PathBuf) -> Result<Vec<Activity>, ParseError> {
let f = match File::open(path) {
Ok(v) => v,
Err(e) => return Err(ParseError::FileError(e)),
};
let mut activities = vec![];
for line in BufReader::new(f).lines() {
let line = match line {
Ok(v) => v,
Err(e) => return Err(ParseError::FileError(e)),
};
activities.push(parse_line(line)?);
}
Ok(activities)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils;
use crate::Config;
#[test]
fn parse_line_none_if_empty() {
assert!(matches!(
parse_line("".to_string()),
Err(ParseError::NoLinetoParse)
))
}
#[test]
fn parse_line_errs_if_bad_dt_string() {
assert!(matches!(
parse_line("not_a_dt # #".to_string()),
Err(ParseError::CannotParseDateTime(_))
))
}
#[test]
fn parse_line_creates_activity_with_only_start() {
let line = format!("{:?} # #", Utc::now());
let activity = parse_line(line).unwrap();
assert!(activity.end.is_none());
assert!(activity.tags.is_empty());
assert!(activity.description.is_none());
}
#[test]
fn parse_line_creates_activity_with_start_and_end() {
let line = format!("{:?} - {:?} # #", Utc::now(), Utc::now());
let activity = parse_line(line).unwrap();
assert!(activity.end.is_some());
assert!(activity.tags.is_empty());
assert!(activity.description.is_none());
}
#[test]
fn parse_line_parse_string_correctly_with_start() {
let activity = parse_line("2022-06-25T18:14:00.324310806Z # #".to_string()).unwrap();
assert_eq!(
activity.start.to_string(),
"2022-06-25 18:14:00.324310806 UTC".to_string()
);
assert!(activity.end.is_none());
assert!(activity.tags.is_empty());
assert!(activity.description.is_none());
}
#[test]
fn parse_line_parse_string_correctly_with_start_and_end() {
let activity = parse_line(
"2022-06-25T18:14:00.324310806Z - 2022-06-25T19:14:00.324310806Z # #".to_string(),
)
.unwrap();
assert_eq!(
activity.start.to_string(),
"2022-06-25 18:14:00.324310806 UTC".to_string()
);
assert_eq!(
activity.end.unwrap().to_string(),
"2022-06-25 19:14:00.324310806 UTC".to_string()
);
assert!(activity.tags.is_empty());
assert!(activity.description.is_none());
}
#[test]
fn parse_line_creates_activity_with_single_tag() {
let line = format!("{:?} - {:?} # work #", Utc::now(), Utc::now());
let activity = parse_line(line).unwrap();
assert!(!activity.tags.is_empty());
}
#[test]
fn parse_line_creates_activity_with_multiple_tags() {
let line = format!("{:?} - {:?} # work pleasure #", Utc::now(), Utc::now());
let activity = parse_line(line).unwrap();
assert!(!activity.tags.is_empty());
}
#[test]
fn parse_line_creates_activity_with_single_tag_in_vec() {
let line = format!("{:?} - {:?} # work #", Utc::now(), Utc::now());
let activity = parse_line(line).unwrap();
assert_eq!(activity.tags, vec!["work".to_string()])
}
#[test]
fn parse_line_creates_activity_with_multiple_tags_in_vec() {
let line = format!("{:?} - {:?} # work pleasure #", Utc::now(), Utc::now());
let activity = parse_line(line).unwrap();
assert_eq!(
activity.tags,
vec!["work".to_string(), "pleasure".to_string()]
)
}
#[test]
fn parse_line_creates_activity_with_tags_surrounding_white_space_trimmed() {
let line = format!("{:?} - {:?} # work pleasure # ", Utc::now(), Utc::now());
let activity = parse_line(line).unwrap();
assert_eq!(
activity.tags,
vec!["work".to_string(), "pleasure".to_string()]
)
}
#[test]
fn parse_line_creates_activity_with_description_and_no_tags() {
let line = format!(
"{:?} - {:?} # # This is a description.",
Utc::now(),
Utc::now()
);
let activity = parse_line(line).unwrap();
assert!(activity.tags.is_empty());
assert_eq!(
activity.description.unwrap(),
"This is a description.".to_owned()
);
}
#[test]
fn parse_file_errs_if_no_file() {
assert!(matches!(
parse_file(&PathBuf::from("notapath.txt")),
Err(ParseError::FileError(_))
));
}
#[test]
fn parsing_empty_file_returns_empty_vector() {
test_utils::clean_up();
let config = Config::setup();
let lines = parse_file(&config.activities_path);
assert!(lines.unwrap().is_empty());
}
#[test]
fn parsing_file_returns_vector_with_correct_number_of_lines() {
test_utils::clean_up();
let config = Config::setup();
test_utils::create_activities(3);
let lines = parse_file(&config.activities_path);
assert_eq!(lines.unwrap().len(), 3);
}
}