extern crate chrono;
extern crate clap;
extern crate larry;
extern crate pidgin;
extern crate regex;
extern crate serde_json;
use crate::configure::Configuration;
use crate::util::{duration_string, log_path};
use chrono::{Datelike, Duration, Local, NaiveDate, NaiveDateTime, Timelike};
use clap::ArgMatches;
use larry::Larry;
use pidgin::{Grammar, Matcher};
use regex::{Regex, RegexSet};
use std::fs::{File, OpenOptions};
use std::io::{BufRead, BufReader, Lines, Write};
use std::path::PathBuf;
lazy_static! {
#[doc(hidden)]
pub static ref LOG_LINES: Grammar = grammar!{
TOP -> r(r"\A") <log_item> r(r"\z")
log_item -> <timestamped_item> | <blank> | <comment>
blank -> r(r"\s*")
comment -> r(r"\s*#.*")
timestamped_item -> <timestamp> <ti_continuation>
timestamp -> r(r"\s*[1-9]\d{3}(?:\s+\d{1,2}){5}\s*")
ti_continuation -> <taggable> | <done>
taggable -> <tag_separator> <tags> (":") <description>
tag_separator -> <event> | <note>
event -> (":")
note -> ("<NOTE>")
done -> r(r":DONE\s*")
tags -> r(r"(?:\\.|[^:<\\])*")
description -> r(r".*")
};
pub static ref MATCHER: Matcher = LOG_LINES.matcher().unwrap();
}
pub fn parse_line(line: &str, offset: usize) -> Item {
if let Some(ast) = MATCHER.parse(line) {
if let Some(timestamp) = ast.name("timestamp") {
let timestamp = parse_timestamp(timestamp.as_str());
if ast.has("done") {
Item::Done(Done(timestamp), offset)
} else {
let tags = parse_tags(ast.name("tags").unwrap().as_str());
let description = ast.name("description").unwrap().as_str();
if ast.has("event") {
Item::Event(
Event {
start: timestamp,
start_overlap: false,
end: None,
end_overlap: false,
description: description.to_owned(),
tags: tags,
vacation: false,
vacation_type: None,
},
offset,
)
} else {
Item::Note(
Note {
time: timestamp,
description: description.to_owned(),
tags: tags,
},
offset,
)
}
}
} else if ast.has("blank") {
Item::Blank(offset)
} else {
Item::Comment(offset)
}
} else {
Item::Error(String::from("unexpected line format"), offset)
}
}
pub struct LogController {
pub larry: Larry,
pub path: String,
}
impl LogController {
pub fn new(
log: Option<PathBuf>,
conf: &Configuration,
) -> Result<LogController, std::io::Error> {
let log = log.unwrap_or(log_path(conf.directory()));
let path = log.as_path().to_str();
Larry::new(log.as_path()).and_then(|log| {
Ok(LogController {
larry: log,
path: path.unwrap().to_owned(),
})
})
}
pub fn find_line(&mut self, time: &NaiveDateTime) -> Option<Item> {
if let Some(start) = self.get_after(0) {
let end = self.get_before(self.larry.len() - 1);
let time = start.advance(time);
Some(self.narrow_in(&time, start, end))
} else {
None
}
}
pub fn first_timestamp(&self) -> Option<NaiveDateTime> {
let item = ItemsAfter::new(0, &self.path).find(|i| i.has_time());
item.and_then(|i| Some(i.time().unwrap().0.clone()))
}
pub fn last_timestamp(&mut self) -> Option<NaiveDateTime> {
let item = ItemsBefore::new(self.larry.len(), self).find(|i| i.has_time());
item.and_then(|i| Some(i.time().unwrap().0.clone()))
}
fn narrow_in(&mut self, time: &NaiveDateTime, start: Item, end: Item) -> Item {
let start = self.advance_to_first(start);
let (t1, mut o1) = start.time().unwrap();
if t1 == time {
return start;
}
let (t2, o2) = end.time().unwrap();
if t2 == time {
return end;
} else if t1 == t2 {
return start;
}
let mut o3 = self.estimate(time, t1, o1, t2, o2);
if o3 == o1 {
return start;
}
loop {
let next = self.get_before(o3);
if next == start {
o1 = o3;
o3 = self.estimate(time, t1, o1, t2, o2);
if o3 == o1 {
return start;
}
} else {
if let Some((t, _)) = next.time() {
if t == time {
return next;
} else if t < time {
return self.narrow_in(time, next, end);
} else {
return self.narrow_in(time, start, next);
}
} else {
unreachable!()
}
}
}
}
fn estimate(
&self,
time: &NaiveDateTime,
t1: &NaiveDateTime,
o1: usize,
t2: &NaiveDateTime,
o2: usize,
) -> usize {
let line_delta = o2 - o1;
match line_delta {
1 => o1,
2 => o1 + 1,
_ => {
if line_delta <= 16 {
return o1 + line_delta / 2;
}
let time_delta = t2.timestamp() - t1.timestamp();
let lines_per_second = (line_delta as f64) / (time_delta as f64);
let seconds = (time.timestamp() - t1.timestamp()) as f64;
let additional_lines = (lines_per_second * seconds) as usize;
let additional_lines = if additional_lines == 0 {
1
} else if additional_lines == line_delta {
line_delta - 1
} else {
additional_lines
};
o1 + additional_lines
}
}
}
fn get_after(&mut self, i: usize) -> Option<Item> {
for i in i..self.larry.len() {
let item = parse_line(self.larry.get(i).unwrap(), i);
let t = item.time();
if let Some((_, _)) = t {
return Some(item);
}
}
None
}
pub fn items_before(&mut self, offset: usize) -> ItemsBefore {
ItemsBefore::new(offset, self)
}
fn get_before(&mut self, i: usize) -> Item {
let mut i = i;
if i >= self.larry.len() {
i = self.larry.len() - 1;
}
loop {
let item = parse_line(self.larry.get(i).unwrap(), i);
match item {
Item::Done(_, _) | Item::Note(_, _) | Item::Event(_, _) => return item,
_ => (),
}
if i == 0 {
break;
}
i -= 1;
}
unreachable!()
}
fn advance_to_first(&mut self, item: Item) -> Item {
let (time, mut i) = item.time().unwrap();
let mut ptr = item.clone();
while i > 0 {
i -= 1;
let next = parse_line(self.larry.get(i).unwrap(), i);
let next_time = next.time();
if let Some((next_time, _)) = next_time {
if time == next_time {
ptr = next;
} else if time > next_time {
return ptr;
}
}
}
ptr
}
pub fn events_from_the_end(&mut self) -> EventsBefore {
EventsBefore::new(self.larry.len(), self)
}
pub fn notes_from_the_end(&mut self) -> NotesBefore {
NotesBefore::new(self.larry.len(), self)
}
pub fn events_from_the_beginning(self) -> EventsAfter {
EventsAfter::new(0, &self)
}
pub fn notes_from_the_beginning(self) -> NotesAfter {
NotesAfter::new(0, &self)
}
pub fn events_in_range(&mut self, start: &NaiveDateTime, end: &NaiveDateTime) -> Vec<Event> {
let mut ret = vec![];
if let Some(item) = self.find_line(start) {
for e in EventsAfter::new(item.offset(), self) {
if &e.start < end {
ret.push(e);
} else {
break;
}
}
}
ret
}
pub fn notes_in_range(&mut self, start: &NaiveDateTime, end: &NaiveDateTime) -> Vec<Note> {
let mut ret = vec![];
if let Some(item) = self.find_line(start) {
for n in NotesAfter::new(item.offset(), self) {
if &n.time < end {
ret.push(n);
} else {
break;
}
}
}
ret
}
pub fn last_event(&mut self) -> Option<Event> {
self.events_from_the_end().find(|_| true)
}
pub fn forgot_to_end_last_event(&mut self) -> bool {
if let Some(event) = self.last_event() {
if event.ongoing() {
let now = Local::now().naive_local();
event.start.date() != now.date()
} else {
false
}
} else {
false
}
}
fn needs_newline(&mut self) -> bool {
if self.larry.len() > 0 {
let last_line = self
.larry
.get(self.larry.len() - 1)
.expect("could not obtain last line of log");
let last_char = last_line.bytes().last().unwrap();
!(last_char == 0x0D || last_char == 0x0A)
} else {
false
}
}
pub fn append_event(&mut self, description: String, tags: Vec<String>) -> (Event, usize) {
let event = Event::coin(description, tags);
self.append_to_log(event, "could not append event to log")
}
pub fn append_note(&mut self, description: String, tags: Vec<String>) -> (Note, usize) {
let note = Note::coin(description, tags);
self.append_to_log(note, "could not append note to log")
}
pub fn close_event(&mut self) -> (Done, usize) {
let done = Done(Local::now().naive_local());
self.append_to_log(done, "could not append DONE line to log")
}
pub fn append_to_log<T: LogLine>(&mut self, item: T, error_message: &str) -> (T, usize) {
let mut log = OpenOptions::new()
.write(true)
.append(true)
.open(&self.path)
.unwrap();
if self.needs_newline() {
writeln!(log, "").expect("could not append to log file");
}
let now = Local::today().naive_local();
if let Some(ts) = self.last_timestamp() {
if ts.date() != now {
writeln!(log, "# {}/{}/{}", now.year(), now.month(), now.day())
.expect("could not append date comment to log");
}
} else {
writeln!(log, "# {}/{}/{}", now.year(), now.month(), now.day())
.expect("could not append date comment to log");
}
writeln!(log, "{}", &item.to_line()).expect(error_message);
(item, self.larry.len())
}
pub fn items(&self) -> ItemsAfter {
ItemsAfter::new(0, &self.path)
}
}
pub struct ItemsBefore<'a> {
offset: Option<usize>,
larry: &'a mut Larry,
}
impl<'a> ItemsBefore<'a> {
fn new(offset: usize, reader: &mut LogController) -> ItemsBefore {
ItemsBefore {
offset: if offset == 0 { None } else { Some(offset) },
larry: &mut reader.larry,
}
}
}
impl<'a> Iterator for ItemsBefore<'a> {
type Item = Item;
fn next(&mut self) -> Option<Item> {
if let Some(o) = self.offset {
let o2 = o - 1;
let line = self.larry.get(o2).unwrap();
let item = parse_line(line, o);
self.offset = if o2 > 0 { Some(o2) } else { None };
Some(item)
} else {
None
}
}
}
pub struct ItemsAfter {
offset: usize,
bufreader: Lines<BufReader<File>>,
}
impl ItemsAfter {
pub fn new(offset: usize, path: &str) -> ItemsAfter {
let mut bufreader =
BufReader::new(File::open(path).expect("could not open log file")).lines();
for _ in 0..offset {
bufreader.next();
}
ItemsAfter { offset, bufreader }
}
}
impl Iterator for ItemsAfter {
type Item = Item;
fn next(&mut self) -> Option<Item> {
if let Some(res) = self.bufreader.next() {
let line = res.expect("could not read log line");
let item = parse_line(&line, self.offset);
self.offset += 1;
Some(item)
} else {
None
}
}
}
pub struct NotesBefore<'a> {
item_iterator: ItemsBefore<'a>,
}
impl<'a> NotesBefore<'a> {
fn new(offset: usize, reader: &mut LogController) -> NotesBefore {
NotesBefore {
item_iterator: ItemsBefore::new(offset, reader),
}
}
}
impl<'a> Iterator for NotesBefore<'a> {
type Item = Note;
fn next(&mut self) -> Option<Note> {
loop {
let item = self.item_iterator.next();
if let Some(item) = item {
match item {
Item::Note(n, _) => return Some(n),
_ => (),
}
} else {
return None;
}
}
}
}
pub struct NotesAfter {
item_iterator: ItemsAfter,
}
impl NotesAfter {
fn new(offset: usize, reader: &LogController) -> NotesAfter {
NotesAfter {
item_iterator: ItemsAfter::new(offset, &reader.path),
}
}
}
impl Iterator for NotesAfter {
type Item = Note;
fn next(&mut self) -> Option<Note> {
loop {
let item = self.item_iterator.next();
if let Some(item) = item {
match item {
Item::Note(n, _) => return Some(n),
_ => (),
}
} else {
return None;
}
}
}
}
pub struct EventsBefore<'a> {
last_time: Option<NaiveDateTime>,
item_iterator: ItemsBefore<'a>,
}
impl<'a> EventsBefore<'a> {
fn new(offset: usize, reader: &mut LogController) -> EventsBefore {
let items_after = ItemsAfter::new(offset, &reader.path);
let timed_item = items_after
.filter(|i| match i {
Item::Event(_, _) | Item::Done(_, _) => true,
_ => false,
})
.find(|i| i.time().is_some());
let last_time = if let Some(i) = timed_item {
Some(i.time().unwrap().0.to_owned())
} else {
None
};
EventsBefore {
last_time,
item_iterator: ItemsBefore::new(offset, reader),
}
}
}
impl<'a> Iterator for EventsBefore<'a> {
type Item = Event;
fn next(&mut self) -> Option<Event> {
let mut last_time = self.last_time;
let mut event: Option<Event> = None;
loop {
if let Some(i) = self.item_iterator.next() {
match i {
Item::Event(e, _) => {
event = Some(e.bounded_time(last_time));
break;
}
Item::Done(d, _) => {
last_time = Some(d.0);
}
_ => (),
}
} else {
break;
}
}
self.last_time = if event.is_some() {
Some(event.as_ref().unwrap().start.clone())
} else {
last_time
};
event
}
}
pub struct EventsAfter {
next_item: Option<Event>,
item_iterator: ItemsAfter,
}
impl EventsAfter {
fn new(offset: usize, reader: &LogController) -> EventsAfter {
EventsAfter {
next_item: None,
item_iterator: ItemsAfter::new(offset, &reader.path),
}
}
fn get_end_time(&mut self) -> Option<NaiveDateTime> {
self.next_item = None;
loop {
if let Some(i) = self.item_iterator.next() {
match i {
Item::Event(e, _) => {
let time = e.start.clone();
self.next_item = Some(e);
return Some(time);
}
Item::Done(d, _) => return Some(d.0),
_ => (),
}
} else {
return None;
}
}
}
}
impl Iterator for EventsAfter {
type Item = Event;
fn next(&mut self) -> Option<Event> {
if let Some(event) = &self.next_item {
return Some(event.clone().bounded_time(self.get_end_time()));
}
loop {
if let Some(i) = self.item_iterator.next() {
match i {
Item::Event(e, _) => return Some(e.bounded_time(self.get_end_time())),
_ => (),
}
} else {
return None;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Duration;
use rand::seq::SliceRandom;
use rand::{thread_rng, Rng};
use std::fs::File;
use std::io::LineWriter;
use std::ops::AddAssign;
use std::str::FromStr;
enum Need {
E,
N,
B,
C,
Error,
}
fn random_tag() -> String {
let choices = ["foo", "bar", "baz", "plugh", "work", "play", "tedium"];
choices[rand::thread_rng().gen_range(0, choices.len())].to_owned()
}
fn random_words(min: usize, max: usize) -> Vec<String> {
(0..(rand::thread_rng().gen_range(min, max + 1)))
.map(|_| random_tag())
.collect()
}
fn random_tags() -> Vec<String> {
let mut tags = random_words(0, 5);
tags.sort_unstable();
tags.dedup();
tags
}
fn random_text() -> String {
let mut words = random_words(5, 15);
let mut word = words.remove(0);
for w in words {
word += " ";
word.push_str(&w);
}
word
}
fn random_line(
time: &mut NaiveDateTime,
open_event: bool,
offset: usize,
need: Option<Need>,
) -> Item {
let n = rand::thread_rng().gen_range(0, 100);
let need = if let Some(need) = need {
need
} else {
if n < 4 {
Need::B
} else if n < 10 {
Need::C
} else if n < 11 {
Need::Error
} else if n < 20 {
Need::N
} else {
Need::E
}
};
match need {
Need::B => Item::Blank(offset),
Need::C => {
let mut comment = String::from("# ");
comment.push_str(&random_text());
Item::Comment(offset)
}
Need::Error => Item::Error(random_text(), offset),
Need::N => {
time.add_assign(Duration::seconds(rand::thread_rng().gen_range(1, 1000)));
Item::Note(
Note {
time: time.clone(),
description: random_text(),
tags: random_tags(),
},
offset,
)
}
Need::E => {
time.add_assign(Duration::seconds(rand::thread_rng().gen_range(1, 1000)));
if open_event && n < 30 {
Item::Done(Done(time.clone()), offset)
} else {
Item::Event(
Event {
start: time.clone(),
start_overlap: false,
end: None,
end_overlap: false,
tags: random_tags(),
description: random_text(),
vacation: false,
vacation_type: None,
},
offset,
)
}
}
}
}
fn random_log(length: usize, need: Vec<Need>, disambiguator: &str) -> (Vec<Item>, String) {
let mut initial_time = NaiveDate::from_ymd(2019, 12, 22).and_hms(9, 39, 30);
let mut items: Vec<Item> = Vec::with_capacity(length);
let mut open_event = false;
let path = format!(
"{}-{}-{}.log",
disambiguator,
length,
Local::now().naive_local().timestamp_millis()
);
let file = File::create(path.clone()).unwrap();
let mut file = LineWriter::new(file);
let mut need: Vec<(usize, Need)> = if need.is_empty() {
vec![]
} else {
let mut indices: Vec<usize> = (0..length).collect();
indices.shuffle(&mut thread_rng());
let mut need = need;
need.shuffle(&mut thread_rng());
let mut need = need
.into_iter()
.map(|n| (indices.remove(0), n))
.collect::<Vec<_>>();
need.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
need
};
for offset in 0..length {
let t = if let Some((i, _)) = need.get(0) {
if i == &offset {
let t = need.remove(0).1;
Some(t)
} else {
None
}
} else {
None
};
let item = random_line(&mut initial_time, open_event, offset, t);
open_event = match item {
Item::Done(_, _) => false,
Item::Event(_, _) => true,
_ => open_event,
};
let line = match &item {
Item::Event(e, _) => e.to_line(),
Item::Note(n, _) => n.to_line(),
Item::Done(d, _) => d.to_line(),
Item::Blank(_) => String::new(),
Item::Comment(_) => {
let mut s = String::from("# ");
s.push_str(&random_text());
s
}
Item::Error(s, _) => s.clone(),
};
file.write_all(line.as_ref()).unwrap();
file.write_all("\n".as_ref()).unwrap();
if item.has_time() {
items.push(item);
}
}
(items, path)
}
fn closed_events(mut items: Vec<Item>) -> Vec<Event> {
items.reverse();
let mut ret = Vec::with_capacity(items.len());
let mut last_time: Option<NaiveDateTime> = None;
for i in items.iter() {
match i {
Item::Done(Done(t), _) => last_time = Some(t.clone()),
Item::Event(e, _) => {
let mut e = e.clone();
if last_time.is_some() {
e.end = last_time;
}
last_time = Some(e.start.clone());
ret.push(e);
}
_ => (),
}
}
ret.reverse();
ret
}
fn notes(items: Vec<Item>) -> Vec<Note> {
let mut ret = Vec::with_capacity(items.len());
for i in items.iter() {
match i {
Item::Note(n, _) => {
ret.push(n.clone());
}
_ => (),
}
}
ret
}
fn test_configuration(path: &str) -> (String, Configuration) {
let conf_path = format!("{}_conf", path);
File::create(
PathBuf::from_str(&conf_path)
.expect(&format!("could not create path {}", conf_path))
.as_path(),
)
.expect(&format!("could not create file {}", conf_path));
let pb = PathBuf::from_str(&conf_path)
.expect(&format!("could not form path from {}", conf_path));
let conf = Configuration::read(Some(pb), None);
(conf_path, conf)
}
fn cleanup(paths: &[&str]) {
for p in paths {
let pb = PathBuf::from_str(p).expect(&format!("cannot form a path from {}", p));
if pb.as_path().exists() {
std::fs::remove_file(p).expect(&format!("failed to remove {}", p))
}
}
}
#[test]
fn test_notes_in_range() {
let (items, path) = random_log(100, vec![Need::N, Need::N], "test_notes_in_range");
let notes = notes(items);
assert!(notes.len() > 1, "found more than one note");
let (conf_path, conf) = test_configuration("test_notes_in_range");
let mut log_reader =
LogController::new(Some(PathBuf::from_str(&path).unwrap()), &conf).unwrap();
for i in 0..notes.len() - 1 {
for j in i..notes.len() {
let found_notes = log_reader.notes_in_range(¬es[i].time, ¬es[j].time);
assert!(
j - i == found_notes.len(),
"found as many events as expected"
);
for offset in 0..found_notes.len() {
let k = i + offset;
assert_eq!(notes[k].time, found_notes[offset].time, "same time");
assert_eq!(notes[k].tags, found_notes[offset].tags, "same tags");
assert_eq!(
notes[k].description, found_notes[offset].description,
"same description"
);
}
}
}
cleanup(&[&path, &conf_path]);
}
#[test]
fn test_events_in_range() {
let (items, path) = random_log(20, vec![Need::E, Need::E], "test_events_in_range");
let events = closed_events(items);
assert!(events.len() > 1, "found more than one event");
let (conf_path, conf) = test_configuration("test_events_in_range");
let mut log_reader =
LogController::new(Some(PathBuf::from_str(&path).unwrap()), &conf).unwrap();
for i in 0..events.len() - 1 {
for j in i..events.len() {
let found_events = log_reader.events_in_range(&events[i].start, &events[j].start);
assert!(
j - i <= found_events.len(),
"found at least as many events as expected"
);
for offset in 0..found_events.len() {
let k = i + offset;
assert_eq!(events[k].start, found_events[offset].start, "same start");
assert_eq!(events[k].end, found_events[offset].end, "same end");
assert_eq!(events[k].tags, found_events[offset].tags, "same tags");
assert_eq!(
events[k].description, found_events[offset].description,
"same description"
);
}
}
}
cleanup(&[&path, &conf_path]);
}
#[test]
fn test_notes_from_end() {
let (items, path) = random_log(100, vec![Need::N], "test_notes_from_end");
let mut notes = notes(items);
notes.reverse();
let (conf_path, conf) = test_configuration("test_notes_from_end");
let mut log_reader =
LogController::new(Some(PathBuf::from_str(&path).unwrap()), &conf).unwrap();
let found_notes = log_reader.notes_from_the_end().collect::<Vec<_>>();
assert_eq!(
notes.len(),
found_notes.len(),
"found the right number of notes"
);
for (i, e) in notes.iter().enumerate() {
assert_eq!(e.time, found_notes[i].time, "they occur at the same time");
assert_eq!(e.tags, found_notes[i].tags, "they have the same tags");
assert_eq!(
e.description, found_notes[i].description,
"they have the same text"
);
}
cleanup(&[&path, &conf_path]);
}
#[test]
fn test_notes_from_beginning() {
let (items, path) = random_log(103, vec![Need::N], "test_notes_from_beginning");
let notes = notes(items);
let (conf_path, conf) = test_configuration("test_notes_from_beginning");
let log_reader =
LogController::new(Some(PathBuf::from_str(&path).unwrap()), &conf).unwrap();
let found_notes = log_reader.notes_from_the_beginning().collect::<Vec<_>>();
assert_eq!(
notes.len(),
found_notes.len(),
"found the right number of notes"
);
for (i, n) in notes.iter().enumerate() {
assert_eq!(n.time, found_notes[i].time, "they occur at the same time");
assert_eq!(n.tags, found_notes[i].tags, "they have the same tags");
assert_eq!(
n.description, found_notes[i].description,
"they have the same text"
);
}
cleanup(&[&path, &conf_path]);
}
#[test]
fn test_events_from_end() {
let (items, path) = random_log(107, vec![Need::E], "test_events_from_end");
let mut events = closed_events(items);
events.reverse();
let (conf_path, conf) = test_configuration("test_events_from_end");
let mut log_reader =
LogController::new(Some(PathBuf::from_str(&path).unwrap()), &conf).unwrap();
let found_events = log_reader.events_from_the_end().collect::<Vec<_>>();
assert_eq!(
events.len(),
found_events.len(),
"found the right number of events"
);
for (i, e) in events.iter().enumerate() {
assert_eq!(
e.start, found_events[i].start,
"they start at the same time"
);
assert_eq!(e.end, found_events[i].end, "they end at the same time");
assert_eq!(e.tags, found_events[i].tags, "they have the same tags");
assert_eq!(
e.description, found_events[i].description,
"they have the same description"
);
}
cleanup(&[&path, &conf_path]);
}
#[test]
fn test_events_from_beginning() {
let (items, path) = random_log(100, vec![Need::E], "test_events_from_beginning");
let events = closed_events(items);
let (conf_path, conf) = test_configuration("test_events_from_beginning");
let log_reader =
LogController::new(Some(PathBuf::from_str(&path).unwrap()), &conf).unwrap();
let found_events = log_reader.events_from_the_beginning().collect::<Vec<_>>();
assert_eq!(
events.len(),
found_events.len(),
"found the right number of events"
);
for (i, e) in events.iter().enumerate() {
assert_eq!(
e.start, found_events[i].start,
"they start at the same time"
);
assert_eq!(e.end, found_events[i].end, "they end at the same time");
assert_eq!(e.tags, found_events[i].tags, "they have the same tags");
assert_eq!(
e.description, found_events[i].description,
"they have the same description"
);
}
cleanup(&[&path, &conf_path]);
}
fn test_log(length: usize, disambiguator: &str) {
let (items, path) = random_log(length, vec![], disambiguator);
if items.is_empty() {
println!("empty file; skipping...");
} else {
let (conf_path, conf) = test_configuration(&path);
let mut log_reader =
LogController::new(Some(PathBuf::from_str(&path).unwrap()), &conf).unwrap();
let mut last_timed_item: Option<Item> = None;
for item in items {
let (time, offset) = item.time().unwrap();
let found_item = log_reader.find_line(time);
if let Some(found_item) = found_item {
assert_eq!(offset, found_item.offset());
if let Some(lti) = last_timed_item.clone() {
let (t1, _) = lti.time().unwrap();
let (t2, _) = found_item.time().unwrap();
let d = *t2 - *t1;
if d.num_seconds() > 1 {
let intermediate_time = t1
.checked_add_signed(Duration::seconds(d.num_seconds() / 2))
.unwrap();
let should_be_found = log_reader.find_line(&intermediate_time);
if let Some(should_be_found) = should_be_found {
assert_eq!(last_timed_item.unwrap(), should_be_found);
} else {
assert!(false, format!("failed to revert to found time when looking for missing intermediate time {}", intermediate_time));
}
}
}
last_timed_item = Some(found_item);
} else {
assert!(false, format!("could not find item at offset {}", offset));
}
cleanup(&[&conf_path]);
}
}
cleanup(&[&path]);
}
#[test]
fn test_empty_file() {
test_log(0, "test_empty_file");
}
#[test]
fn test_100_tiny_files() {
for i in 0..100 {
test_log(5, &format!("test_100_tiny_files_{}", i));
}
}
#[test]
fn test_10_small_files() {
for i in 0..10 {
test_log(100, &format!("test_10_small_files_{}", i));
}
}
#[test]
fn test_large_file() {
test_log(10000, "test_large_file");
}
#[test]
fn test_event() {
match parse_line("2019 12 1 16 3 30::an event with no tags", 0) {
Item::Event(
Event {
start,
tags,
description,
..
},
_,
) => {
assert_eq!(2019, start.year());
assert_eq!(12, start.month());
assert_eq!(1, start.day());
assert_eq!(16, start.hour());
assert_eq!(3, start.minute());
assert_eq!(30, start.second());
assert!(tags.is_empty(), "there are no tags");
assert_eq!(
"an event with no tags", &description,
"got correct description"
)
}
_ => assert!(false, "failed to parse an event line"),
};
match parse_line("2019 12 1 16 3 30:foo bar:an event with some tags", 0) {
Item::Event(
Event {
start,
tags,
description,
..
},
_,
) => {
assert_eq!(2019, start.year());
assert_eq!(12, start.month());
assert_eq!(1, start.day());
assert_eq!(16, start.hour());
assert_eq!(3, start.minute());
assert_eq!(30, start.second());
assert_eq!(2, tags.len(), "there are some tags");
for t in vec!["foo", "bar"] {
assert!(tags.contains(&t.to_owned()));
}
assert_eq!(
"an event with some tags", &description,
"got correct description"
)
}
_ => assert!(false, "failed to parse an event line"),
};
match parse_line("2019 12 22 12 49 24:foo:plugh baz baz foo play play work baz tedium foo tedium foo work bar", 0) {
Item::Event(
Event {
start,
tags,
description,
..
},
_,
) => {
assert_eq!(2019, start.year());
assert_eq!(12, start.month());
assert_eq!(22, start.day());
assert_eq!(12, start.hour());
assert_eq!(49, start.minute());
assert_eq!(24, start.second());
assert_eq!(1, tags.len(), "there are some tags");
for t in vec!["foo"] {
assert!(tags.contains(&t.to_owned()));
}
assert_eq!(
"plugh baz baz foo play play work baz tedium foo tedium foo work bar", &description,
"got correct description"
)
}
_ => assert!(false, "failed to parse an event line"),
};
}
#[test]
fn test_note() {
match parse_line("2019 12 1 16 3 30<NOTE>:a note with no tags", 0) {
Item::Note(
Note {
time,
tags,
description,
},
_,
) => {
assert_eq!(2019, time.year());
assert_eq!(12, time.month());
assert_eq!(1, time.day());
assert_eq!(16, time.hour());
assert_eq!(3, time.minute());
assert_eq!(30, time.second());
assert!(tags.is_empty(), "there are no tags");
assert_eq!(
"a note with no tags", &description,
"got correct description"
)
}
_ => assert!(false, "failed to parse a NOTE line"),
};
match parse_line("2019 12 1 16 3 30<NOTE>foo bar:a short note", 0) {
Item::Note(
Note {
time,
tags,
description,
},
_,
) => {
assert_eq!(2019, time.year());
assert_eq!(12, time.month());
assert_eq!(1, time.day());
assert_eq!(16, time.hour());
assert_eq!(3, time.minute());
assert_eq!(30, time.second());
assert_eq!(tags.len(), 2, "there are two tags");
for t in vec!["foo", "bar"] {
assert!(tags.contains(&t.to_owned()));
}
assert_eq!("a short note", &description, "got correct description")
}
_ => assert!(false, "failed to parse a NOTE line"),
};
match parse_line(
r"2019 12 1 16 3 30<NOTE>f\:oo b\<ar b\ az pl\\ugh:a short note",
0,
) {
Item::Note(
Note {
time,
tags,
description,
},
_,
) => {
assert_eq!(2019, time.year());
assert_eq!(12, time.month());
assert_eq!(1, time.day());
assert_eq!(16, time.hour());
assert_eq!(3, time.minute());
assert_eq!(30, time.second());
assert_eq!(tags.len(), 4, "there are two tags");
for t in vec!["f:oo", "b<ar", "b az", r"pl\ugh"] {
assert!(tags.contains(&t.to_owned()), "escaping worked");
}
assert_eq!("a short note", &description, "got correct description")
}
_ => assert!(false, "failed to parse a NOTE line"),
};
match parse_line("2019 12 1 16 3 30<NOTE>foo bar bar:a short note", 0) {
Item::Note(
Note {
time,
tags,
description,
},
_,
) => {
assert_eq!(2019, time.year());
assert_eq!(12, time.month());
assert_eq!(1, time.day());
assert_eq!(16, time.hour());
assert_eq!(3, time.minute());
assert_eq!(30, time.second());
assert_eq!(tags.len(), 2, "there are two tags");
for t in vec!["foo", "bar"] {
assert!(tags.contains(&t.to_owned()));
}
assert_eq!("a short note", &description, "got correct description")
}
_ => assert!(false, "failed to parse a NOTE line"),
};
match parse_line("2019 12 1 16 3 30<NOTE> foo bar :a short note", 0) {
Item::Note(
Note {
time,
tags,
description,
},
_,
) => {
assert_eq!(2019, time.year());
assert_eq!(12, time.month());
assert_eq!(1, time.day());
assert_eq!(16, time.hour());
assert_eq!(3, time.minute());
assert_eq!(30, time.second());
assert_eq!(tags.len(), 2, "there are two tags");
for t in vec!["foo", "bar"] {
assert!(tags.contains(&t.to_owned()));
}
assert_eq!("a short note", &description, "got correct description")
}
_ => assert!(false, "failed to parse a NOTE line"),
};
match parse_line("2019 12 22 9 59 34<NOTE>foo play tedium work:baz tedium baz tedium foo plugh bar foo bar play plugh foo baz play baz tedium work work play play bar", 0) {
Item::Note(
Note {
time,
tags,
description,
},
_,
) => {
assert_eq!(2019, time.year());
assert_eq!(12, time.month());
assert_eq!(22, time.day());
assert_eq!(9, time.hour());
assert_eq!(59, time.minute());
assert_eq!(34, time.second());
assert_eq!(tags.len(), 4, "there are three tags");
for t in vec!["foo", "play", "tedium", "work"] {
assert!(tags.contains(&t.to_owned()));
}
assert_eq!("baz tedium baz tedium foo plugh bar foo bar play plugh foo baz play baz tedium work work play play bar", &description, "got correct description")
}
_ => assert!(false, "failed to parse a NOTE line"),
};
match parse_line(
"2019 12 22 12 8 0<NOTE>bar:tedium plugh baz play tedium baz play work",
0,
) {
Item::Note(
Note {
time,
tags,
description,
},
_,
) => {
assert_eq!(2019, time.year());
assert_eq!(12, time.month());
assert_eq!(22, time.day());
assert_eq!(12, time.hour());
assert_eq!(8, time.minute());
assert_eq!(0, time.second());
assert_eq!(tags.len(), 1, "there is one tag");
for t in vec!["bar"] {
assert!(tags.contains(&t.to_owned()));
}
assert_eq!(
"tedium plugh baz play tedium baz play work", &description,
"got correct description"
)
}
_ => assert!(false, "failed to parse a NOTE line"),
};
}
#[test]
fn test_done() {
match parse_line("2019 12 1 16 3 30:DONE", 0) {
Item::Done(Done(time), _) => {
assert_eq!(2019, time.year());
assert_eq!(12, time.month());
assert_eq!(1, time.day());
assert_eq!(16, time.hour());
assert_eq!(3, time.minute());
assert_eq!(30, time.second());
}
_ => assert!(false, "failed to parse a DONE line"),
};
match parse_line(" 2019 12 1 16 3 30 :DONE", 0) {
Item::Done(Done(time), _) => {
assert_eq!(2019, time.year(), "space doesn't matter");
assert_eq!(12, time.month(), "space doesn't matter");
assert_eq!(1, time.day(), "space doesn't matter");
assert_eq!(16, time.hour(), "space doesn't matter");
assert_eq!(3, time.minute(), "space doesn't matter");
assert_eq!(30, time.second(), "space doesn't matter");
}
_ => assert!(false, "failed to parse a DONE line"),
};
}
#[test]
fn test_zero_padding() {
match parse_line("2019 12 01 16 03 30:DONE", 0) {
Item::Done(Done(time), _) => {
assert_eq!(1, time.day());
assert_eq!(3, time.minute());
}
_ => assert!(false, "failed to parse a DONE line"),
};
}
#[test]
fn test_comment() {
let success = match parse_line("#foo", 0) {
Item::Comment(_) => true,
_ => false,
};
assert!(success, "recognized '#foo' as a comment line");
let success = match parse_line(" #foo", 0) {
Item::Comment(_) => true,
_ => false,
};
assert!(success, "comments can have leading space");
}
#[test]
fn test_error() {
let success = match parse_line("foo", 0) {
Item::Error(_, _) => true,
_ => false,
};
assert!(success, "recognized 'foo' as a malformed log line");
}
#[test]
fn test_blank() {
let success = match parse_line("", 0) {
Item::Blank(_) => true,
_ => false,
};
assert!(success, "recognized an empty line as a blank");
let success = match parse_line(" ", 0) {
Item::Blank(_) => true,
_ => false,
};
assert!(success, "recognized a whitespace line as a blank");
}
#[test]
fn stack_overflow_regression() {
let (items, path) = random_log(23, vec![Need::E, Need::E], "stack_overflow_regression");
let events = closed_events(items);
assert!(events.len() > 1, "found more than one event");
let (conf_path, conf) = test_configuration("stack_overflow_regression");
let mut log_reader =
LogController::new(Some(PathBuf::from_str(&path).unwrap()), &conf).unwrap();
let e = events.first().unwrap();
let false_start = e.start - Duration::days(1);
let found_events = log_reader.events_in_range(&false_start, e.end.as_ref().unwrap());
assert_eq!(1, found_events.len(), "found one event");
assert_eq!(e.start, found_events[0].start, "same start");
assert_eq!(e.end, found_events[0].end, "same end");
assert_eq!(e.tags, found_events[0].tags, "same tags");
assert_eq!(
e.description, found_events[0].description,
"same description"
);
cleanup(&[&path, &conf_path]);
}
}
#[derive(Debug, Clone)]
pub enum Item {
Event(Event, usize),
Note(Note, usize),
Done(Done, usize),
Blank(usize),
Comment(usize),
Error(String, usize),
}
impl Item {
fn advance(&self, time: &NaiveDateTime) -> NaiveDateTime {
match self {
Item::Event(e, _) => {
if time < &e.start {
e.start.clone()
} else {
time.clone()
}
}
Item::Note(n, _) => {
if time < &n.time {
n.time.clone()
} else {
time.clone()
}
}
Item::Done(d, _) => {
if time < &d.0 {
d.0.clone()
} else {
time.clone()
}
}
_ => time.clone(),
}
}
pub fn time(&self) -> Option<(&NaiveDateTime, usize)> {
match self {
Item::Event(e, offset) => Some((&e.start, *offset)),
Item::Note(n, offset) => Some((&n.time, *offset)),
Item::Done(d, offset) => Some((&d.0, *offset)),
_ => None,
}
}
pub fn has_time(&self) -> bool {
match self {
Item::Event(_, _) | Item::Note(_, _) | Item::Done(_, _) => true,
_ => false,
}
}
pub fn offset(&self) -> usize {
match self {
Item::Event(_, i) => *i,
Item::Note(_, i) => *i,
Item::Done(_, i) => *i,
Item::Blank(i) => *i,
Item::Comment(i) => *i,
Item::Error(_, i) => *i,
}
}
}
impl PartialEq for Item {
fn eq(&self, other: &Item) -> bool {
self.offset() == other.offset()
}
}
impl PartialOrd for Item {
fn partial_cmp(&self, other: &Item) -> Option<std::cmp::Ordering> {
self.offset().partial_cmp(&other.offset())
}
}
pub fn parse_timestamp(timestamp: &str) -> NaiveDateTime {
lazy_static! {
static ref RE: Regex = Regex::new(r"\d+").unwrap();
}
let numbers: Vec<_> = RE.find_iter(timestamp).map(|m| m.as_str()).collect();
let year = numbers[0].parse::<i32>().unwrap();
let month = numbers[1].parse::<u32>().unwrap();
let day = numbers[2].parse::<u32>().unwrap();
let hour = numbers[3].parse::<u32>().unwrap();
let minute = numbers[4].parse::<u32>().unwrap();
let second = numbers[5].parse::<u32>().unwrap();
NaiveDate::from_ymd(year, month, day).and_hms(hour, minute, second)
}
pub fn timestamp(ts: &NaiveDateTime) -> String {
format!(
"{} {:>2} {:>2} {:>2} {:>2} {:>2}",
ts.year(),
ts.month(),
ts.day(),
ts.hour(),
ts.minute(),
ts.second()
)
}
pub fn parse_tags(tags: &str) -> Vec<String> {
let mut parsed = vec![];
let mut escaped = false;
let mut current = String::with_capacity(tags.len());
for c in tags.chars() {
if c == '\\' {
if escaped {
current.push(c);
} else {
escaped = true;
}
} else if c == ' ' {
if escaped {
current.push(c);
} else {
if current.len() > 0 && !parsed.contains(¤t) {
parsed.push(current.clone());
}
current.clear();
}
escaped = false;
} else {
current.push(c);
escaped = false;
}
}
if current.len() > 0 && !parsed.contains(¤t) {
parsed.push(current);
}
parsed
}
pub fn tags(tags: &Vec<String>) -> String {
let mut v = tags.clone();
v.sort_unstable();
v.dedup();
let mut s = String::new();
for (i, tag) in v.iter().enumerate() {
if i > 0 {
s.push(' ');
}
for c in tag.chars() {
match c {
':' | '\\' | '<' => s.push('\\'),
_ => (),
}
s.push(if c.is_whitespace() { ' ' } else { c });
}
}
s
}
#[derive(Debug, Clone)]
pub struct Event {
pub start: NaiveDateTime,
pub start_overlap: bool,
pub end: Option<NaiveDateTime>,
pub end_overlap: bool,
pub description: String,
pub tags: Vec<String>,
pub vacation: bool,
pub vacation_type: Option<String>,
}
impl Event {
pub fn coin(description: String, mut tags: Vec<String>) -> Event {
tags.sort_unstable();
tags.dedup();
Event {
start: Local::now().naive_local(),
start_overlap: false,
end: None,
end_overlap: false,
description: description,
tags: tags,
vacation: false,
vacation_type: None,
}
}
fn bounded_time(self, end: Option<NaiveDateTime>) -> Self {
Event {
start: self.start,
start_overlap: self.start_overlap,
end: end,
end_overlap: self.end_overlap,
description: self.description,
tags: self.tags,
vacation: self.vacation,
vacation_type: self.vacation_type,
}
}
pub fn ongoing(&self) -> bool {
self.end.is_none()
}
pub fn duration(&self, now: &NaiveDateTime) -> f32 {
let end = self.end.as_ref().unwrap_or(now);
(end.timestamp() - self.start.timestamp()) as f32
}
fn split(self, time: NaiveDateTime) -> (Self, Self) {
assert!(time > self.start);
assert!(self.end.is_none() || self.end.unwrap() > time);
let mut start = self;
let mut end = start.clone();
start.end_overlap = true;
start.end = Some(time.clone());
end.start = time;
end.end_overlap = true;
(start, end)
}
pub fn gather_by_day(events: Vec<Event>, end_date: &NaiveDateTime) -> Vec<Event> {
let mut ret = vec![];
let mut end_date = end_date;
let now = Local::now().naive_local();
if &now < &end_date {
end_date = &now;
}
for mut e in events {
if &e.start >= end_date {
break;
}
loop {
match e.end.as_ref() {
Some(&time) => {
if time.date() == e.start.date() {
ret.push(e);
break;
}
let split_date = e.start.date().and_hms(0, 0, 0) + Duration::days(1);
let (e1, e2) = e.split(split_date);
e = e2;
ret.push(e1);
}
None => {
if e.start.date() == end_date.date() {
ret.push(e);
break;
} else {
let split_date = e.start.date().and_hms(0, 0, 0) + Duration::days(1);
let (e1, e2) = e.split(split_date);
e = e2;
ret.push(e1);
}
}
}
}
}
ret
}
fn mergeable(&self, other: &Self) -> bool {
self.end.is_some() && self.end.unwrap() == other.start && self.tags == other.tags
}
fn merge(&mut self, other: Self) {
self.description = self.description.clone() + "; " + &other.description;
self.end = other.end;
self.end_overlap = other.end_overlap;
}
pub fn gather_by_day_and_merge(events: Vec<Event>, end_date: &NaiveDateTime) -> Vec<Event> {
let mut events = Self::gather_by_day(events, end_date);
if events.is_empty() {
return events;
}
let mut ret = vec![];
ret.push(events.remove(0));
for e in events {
let i = ret.len() - 1;
if ret[i].mergeable(&e) {
ret[i].merge(e);
} else {
ret.push(e);
}
}
ret
}
pub fn to_json(&self, now: &NaiveDateTime, conf: &Configuration) -> String {
let end = if let Some(time) = self.end {
serde_json::to_string(&format!("{}", time)).unwrap()
} else {
"null".to_owned()
};
format!(
r#"{{"type":"Event","start":{},"end":{},"duration":{},{}"tags":{},"description":{}}}"#,
serde_json::to_string(&format!("{}", self.start)).unwrap(),
end,
duration_string(self.duration(now), conf),
if let Some(t) = &self.vacation_type {
format!("\"vacation\":\"{}\",", if t == "" { "ordinary" } else { t })
} else {
"".to_owned()
},
serde_json::to_string(&self.tags).unwrap(),
serde_json::to_string(&self.description).unwrap()
)
}
}
impl Searchable for Event {
fn text(&self) -> &str {
&self.description
}
fn tags(&self) -> Vec<&str> {
self.tags.iter().map(|s| s.as_str()).collect()
}
}
#[derive(Debug, Clone)]
pub struct Note {
pub time: NaiveDateTime,
pub description: String,
pub tags: Vec<String>,
}
impl Note {
pub fn coin(description: String, mut tags: Vec<String>) -> Note {
tags.sort_unstable();
tags.dedup();
Note {
time: Local::now().naive_local(),
description: description,
tags: tags,
}
}
pub fn to_json(&self, _now: &NaiveDateTime, _conf: &Configuration) -> String {
format!(
r#"{{"type":"Note","time":{},"tags":{},"description":{}}}"#,
serde_json::to_string(&format!("{}", self.time)).unwrap(),
serde_json::to_string(&self.tags).unwrap(),
serde_json::to_string(&self.description).unwrap()
)
}
}
impl Searchable for Note {
fn text(&self) -> &str {
&self.description
}
fn tags(&self) -> Vec<&str> {
self.tags.iter().map(|s| s.as_str()).collect()
}
}
#[derive(Debug, Clone)]
pub struct Done(pub NaiveDateTime);
impl Done {
pub fn coin() -> Done {
Done(Local::now().naive_local())
}
}
pub enum Direction {
Forward,
Back,
}
pub trait LogLine {
fn to_line(&self) -> String;
}
impl LogLine for Done {
fn to_line(&self) -> String {
let mut ts = timestamp(&self.0);
ts += ":DONE";
ts
}
}
impl LogLine for Note {
fn to_line(&self) -> String {
let mut ts = timestamp(&self.time);
ts += "<NOTE>";
let tags = tags(&self.tags);
ts += &tags;
ts.push(':');
ts += &self.description;
ts
}
}
impl LogLine for Event {
fn to_line(&self) -> String {
let mut ts = timestamp(&self.start);
ts.push(':');
let tags = tags(&self.tags);
ts += &tags;
ts.push(':');
ts += &self.description;
ts
}
}
pub trait Searchable {
fn tags(&self) -> Vec<&str>;
fn text(&self) -> &str;
}
pub struct Filter<'a> {
all_tags: Option<Vec<&'a str>>,
no_tags: Option<Vec<&'a str>>,
some_tags: Option<Vec<&'a str>>,
some_patterns: Option<RegexSet>,
no_patterns: Option<RegexSet>,
}
impl<'a> Filter<'a> {
pub fn dummy() -> Filter<'a> {
Filter {
all_tags: None,
no_tags: None,
some_tags: None,
some_patterns: None,
no_patterns: None,
}
}
pub fn new(matches: &'a ArgMatches) -> Filter<'a> {
let all_tags = matches
.values_of("tag")
.and_then(|values| Some(values.collect()));
let no_tags = matches
.values_of("tag-none")
.and_then(|values| Some(values.collect()));
let some_tags = matches
.values_of("tag-some")
.and_then(|values| Some(values.collect()));
let some_patterns = matches
.values_of("rx")
.and_then(|values| Some(RegexSet::new(values).unwrap()));
let no_patterns = matches
.values_of("rx-not")
.and_then(|values| Some(RegexSet::new(values).unwrap()));
Filter {
all_tags,
no_tags,
some_tags,
some_patterns,
no_patterns,
}
}
pub fn matches<T: Searchable>(&self, filterable: &T) -> bool {
let tags = filterable.tags();
let text = filterable.text();
if tags.is_empty() {
if !(self.all_tags.is_none() && self.some_tags.is_none()) {
return false;
}
} else {
if self.some_tags.is_some()
&& !self
.some_tags
.as_ref()
.unwrap()
.iter()
.any(|t| tags.contains(t))
{
return false;
}
if self.all_tags.is_some()
&& self
.all_tags
.as_ref()
.unwrap()
.iter()
.any(|t| !tags.contains(t))
{
return false;
}
if self.no_tags.is_some()
&& self
.no_tags
.as_ref()
.unwrap()
.iter()
.any(|t| tags.contains(t))
{
return false;
}
}
if let Some(rx_set) = self.some_patterns.as_ref() {
if !rx_set.is_match(text) {
return false;
}
}
if let Some(rx_set) = self.no_patterns.as_ref() {
if rx_set.is_match(text) {
return false;
}
}
true
}
}