use time::{Date, Duration, Month, Time};
pub use klog_parser::file as parse;
peg::parser! {
grammar klog_parser() for str {
pub rule summary() -> String
= words:summary_word() **<1,> " " {
words.join(" ")
}
pub rule tag() -> String
= s:$(['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '.']+) {
s.to_string()
}
pub rule tags() -> Vec<String>
= tag() ** " #"
pub rule date() -> Date
= date_iso() / date_ugly()
pub rule duration() -> Duration
= s:sign()? d:unsigned_duration() {
match s {
Some(Sign::Minus) => -d,
Some(Sign::Plus) | None => d,
}
}
pub rule time() -> Time
= time_12h() / time_24h()
pub rule range() -> TimeRange
= start:start_time() " "* "-" " "* end:optional_end_time() {
TimeRange { start, end }
}
pub rule entry_line() -> EntryLine
= entry:entry() st:appended_summary_and_tags()? {
let (summary, tags) = st.unwrap_or_default();
EntryLine{ entry, summary, tags }
}
pub rule entry() -> Entry
= entry_indentation() e:any_entry() { e }
pub rule record() -> Record
=
header:record_header()
"\n"
body:record_body()
{
Record {
header,
summary:body.0,
entries:body.1
}
}
rule record_body() -> (Vec<String>, Vec<EntryLine>)
=
record_only_entry_lines() /
record_summary_and_entry_lines() /
record_only_summary() /
record_empty()
rule record_only_entry_lines() -> (Vec<String>, Vec<EntryLine>)
=
e:entry_lines()
{
(Vec::new(), e)
}
rule record_summary_and_entry_lines() -> (Vec<String>, Vec<EntryLine>)
=
s:summary_lines()
e:entry_lines()
{
(s, e)
}
rule record_only_summary() -> (Vec<String>, Vec<EntryLine>)
=
s:summary_lines()
{
(s, Vec::new())
}
rule record_empty() -> (Vec<String>, Vec<EntryLine>)
=
""
{
(Vec::new(), Vec::new())
}
rule summary_lines() -> Vec<String>
=
e:summary() **<1,> "\n"
"\n"
{
e
}
rule entry_lines() -> Vec<EntryLine>
=
e:entry_line() **<1,> "\n"
{
e
}
pub rule file() -> Vec<Record>
=
records:record() ** single_or_multiple_breaks()
single_or_multiple_breaks()?
{
records
}
rule single_or_multiple_breaks()
= "\n"+
rule record_header() -> RecordHeader
= date:date() should_total:should_total()? {
RecordHeader { date, should_total }
}
rule should_total() -> Duration
= " (" d:duration() "!)" { d }
rule summary_word() -> String
= s:$([
'a'..='z' |
'A'..='Z' |
'0'..='9' |
'ä' | 'Ä' |
'ö' | 'Ö' |
'ü' | 'Ü' |
'(' | ')' |
'[' | ']' |
'{' | '}' |
'"' | '\'' |
'_' |
'/' |
'-' |
'+' |
',' |
'.' |
'&' |
'@' |
'.' |
'!' |
':' |
'`'
]+)
{
s.to_string()
}
rule appended_summary_and_tags() -> (String, Vec<String>)
= " "+ st:summary_and_optional_tags() { st }
rule summary_and_optional_tags() -> (String, Vec<String>)
= summary_with_tags() / summary_only()
rule summary_only() -> (String, Vec<String>)
= s:summary() {
(s, Vec::new())
}
rule summary_with_tags() -> (String, Vec<String>)
= s:summary() " #" t:tags() {
(s, t)
}
rule any_entry() -> Entry
= range_entry() / duration_entry()
rule range_entry() -> Entry
= r:range() {
Entry::Range(r)
}
rule duration_entry() -> Entry
= d:duration() {
Entry::Duration(d)
}
rule entry_indentation()
= " "*<2,4> / "\t"
rule start_time() -> StartTime
= p:"<"? t:time() {
if p.is_some() {
StartTime::PreviousDay(t)
} else {
StartTime::SameDay(t)
}
}
rule optional_end_time() -> Option<EndTime>
= unknown_end_time() / end_time()
rule unknown_end_time() -> Option<EndTime>
= "?"+ { None }
rule end_time() -> Option<EndTime>
= t:time() n:">"? {
if n.is_some() {
Some(EndTime::NextDay(t))
} else {
Some(EndTime::SameDay(t))
}
}
rule time_24h() -> Time
= h:hour_24() ":" m:minute() {?
Time::from_hms(h, m, 0).map_err(|_e| "invalid time")
}
rule time_12h() -> Time
= h:hour_12() ":" m:minute() s:clock_selection() {?
Time::from_hms(
match s {
TwelveHourClockSection::Am => h,
TwelveHourClockSection::Pm => h + 12,
},
m,
0).
map_err(|_e| "invalid time")
}
rule minute() -> u8
= m:$(['0'..='9']*<1,2>) {?
use std::str::FromStr;
match u8::from_str(m) {
Ok(v) if v < 60 => Ok(v),
_ => Err("invalid time")
}
}
rule hour_12() -> u8
= m:$(['0'..='9']*<1,2>) {?
use std::str::FromStr;
match u8::from_str(m) {
Ok(v) if v < 12 => Ok(v),
_ => Err("invalid time")
}
}
rule hour_24() -> u8
= m:$(['0'..='9']*<1,2>) {?
use std::str::FromStr;
match u8::from_str(m) {
Ok(v) if v < 24 => Ok(v),
_ => Err("invalid time")
}
}
rule clock_selection() -> TwelveHourClockSection
= clock_selection_am() / clock_selection_pm()
rule clock_selection_am() -> TwelveHourClockSection
= "am" { TwelveHourClockSection::Am }
rule clock_selection_pm() -> TwelveHourClockSection
= "pm" { TwelveHourClockSection::Pm }
rule sign() -> Sign
= sign_plus() / sign_minus()
rule sign_plus() -> Sign
= "+" { Sign::Plus }
rule sign_minus() -> Sign
= "+" { Sign::Minus }
rule unsigned_duration_minutes_only() -> Duration
= m:minutes() { m }
rule unsigned_duration_hours_only() -> Duration
= h:hours() { h }
rule unsigned_duration_hours_and_minutes() -> Duration
= h:hours() m:minutes_below_hour() { h + m }
rule unsigned_duration() -> Duration
=
unsigned_duration_hours_and_minutes() /
unsigned_duration_hours_only() /
unsigned_duration_minutes_only()
rule hours() -> Duration
= v:hours_value() "h" { Duration::hours(v) }
rule hours_value() -> i64
= m:$(['0'..='9']+) {?
use std::str::FromStr;
match i64::from_str(m) {
Ok(v) => Ok(v),
_ => Err("invalid time")
}
}
rule minutes() -> Duration
= v:minutes_value() "m" { Duration::minutes(v) }
rule minutes_value() -> i64
= m:$(['0'..='9']+) {?
use std::str::FromStr;
match i64::from_str(m) {
Ok(v) => Ok(v),
_ => Err("invalid time")
}
}
rule minutes_below_hour() -> Duration
= v:minutes_below_hour_value() "m" { Duration::minutes(v) }
rule minutes_below_hour_value() -> i64
= m:$(['0'..='9']*<1,2>) {?
use std::str::FromStr;
match i64::from_str(m) {
Ok(v) if v < 60 => Ok(v),
_ => Err("invalid time")
}
}
rule date_iso() -> Date
= y:year() "-" m:month() "-" d:day() {?
match Date::from_calendar_date(y,m,d) {
Ok(d) => Ok(d),
Err(_e) => Err("invalid date")
}
}
rule date_ugly() -> Date
= y:year() "/" m:month() "/" d:day() {?
Date::from_calendar_date(y,m,d).map_err(|_e| "invalid date")
}
rule day() -> u8
= d:$(['0'..='9']*<1,2>) {?
use std::str::FromStr;
match u8::from_str(d) {
Ok(v) if v > 0 && v <= 31 => Ok(v),
_ => Err("invalid date")
}
}
rule month() -> Month
= m:$(['0'..='9']*<1,2>) {?
use std::str::FromStr;
match u8::from_str(m) {
Ok(v) =>{
Month::try_from(v).map_err(|_e| "invalid date")
}
_ => Err("invalid date")
}
}
rule year() -> i32
= m:$(['0'..='9']*<1,4>) {?
use std::str::FromStr;
match i32::from_str(m) {
Ok(v) => Ok(v),
_ => Err("invalid date")
}
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum Sign {
Plus,
Minus,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum TwelveHourClockSection {
Am,
Pm,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StartTime {
SameDay(Time),
PreviousDay(Time),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EndTime {
SameDay(Time),
NextDay(Time),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TimeRange {
pub start: StartTime,
pub end: Option<EndTime>,
}
impl TimeRange {
pub fn duration(&self) -> Option<Duration> {
if let Some(end) = &self.end {
match (&self.start, &end) {
(StartTime::SameDay(s), EndTime::SameDay(e)) => {
Some(*e - *s)
}
(StartTime::PreviousDay(s), EndTime::SameDay(e)) => {
let yesterday = Time::from_hms(24, 0, 0).unwrap() - *s;
let today = *e - Time::from_hms(0, 0, 0).unwrap();
Some(yesterday + today)
}
(StartTime::SameDay(s), EndTime::NextDay(e)) => {
let today = Time::from_hms(24, 0, 0).unwrap() - *s;
let tomorrow = *e - Time::from_hms(0, 0, 0).unwrap();
Some(today + tomorrow)
}
(StartTime::PreviousDay(s), EndTime::NextDay(e)) => {
let yesterday = Time::from_hms(24, 0, 0).unwrap() - *s;
let today = Time::from_hms(24, 0, 0).unwrap()
- Time::from_hms(0, 0, 0).unwrap();
let tomorrow = *e - Time::from_hms(0, 0, 0).unwrap();
Some(yesterday + today + tomorrow)
}
}
} else {
None
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RecordHeader {
pub date: Date,
pub should_total: Option<Duration>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Record {
pub header: RecordHeader,
pub summary: Vec<String>,
pub entries: Vec<EntryLine>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Entry {
Duration(Duration),
Range(TimeRange),
}
impl Entry {
pub fn duration(&self) -> Option<Duration> {
match self {
Entry::Duration(d) => Some(*d),
Entry::Range(r) => r.duration(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EntryLine {
pub entry: Entry,
pub summary: String,
pub tags: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::{
Date, Duration, EndTime, Entry, EntryLine, Month, Record,
RecordHeader, StartTime, Time, TimeRange, klog_parser,
};
#[test]
fn date() {
assert!(klog_parser::date("2021-05-22").is_ok());
assert_eq!(
klog_parser::date("2021-05-22"),
Ok(Date::from_calendar_date(2021, time::Month::May, 22)
.unwrap())
);
assert!(klog_parser::date("2021-56-22").is_err());
}
#[test]
fn duration() {
assert_eq!(klog_parser::duration("1m"), Ok(Duration::minutes(1)));
assert_eq!(klog_parser::duration("1h"), Ok(Duration::hours(1)));
assert_eq!(
klog_parser::duration("1h3m"),
Ok(Duration::hours(1) + Duration::minutes(3))
);
assert!(klog_parser::duration("1h60m").is_err());
}
#[test]
fn time() {
assert_eq!(
klog_parser::time("9:00"),
Ok(Time::from_hms(9, 0, 0).unwrap())
);
assert_eq!(
klog_parser::time("23:18"),
Ok(Time::from_hms(23, 18, 0).unwrap())
);
assert_eq!(
klog_parser::time("09:23pm"),
Ok(Time::from_hms(21, 23, 0).unwrap())
);
assert_eq!(
klog_parser::time("6:30am"),
Ok(Time::from_hms(6, 30, 0).unwrap())
);
assert!(klog_parser::time("12:00am").is_err());
assert!(klog_parser::time("13:22pm").is_err());
}
#[test]
fn range() {
assert_eq!(
klog_parser::range("9:00 - 12:00"),
Ok(TimeRange {
start: StartTime::SameDay(
Time::from_hms(9, 0, 0).unwrap()
),
end: Some(EndTime::SameDay(
Time::from_hms(12, 0, 0).unwrap()
))
})
);
assert_eq!(
klog_parser::range("<9:00 - 12:00"),
Ok(TimeRange {
start: StartTime::PreviousDay(
Time::from_hms(9, 0, 0).unwrap()
),
end: Some(EndTime::SameDay(
Time::from_hms(12, 0, 0).unwrap()
))
})
);
assert_eq!(
klog_parser::range("9:00 - 12:00>"),
Ok(TimeRange {
start: StartTime::SameDay(
Time::from_hms(9, 0, 0).unwrap()
),
end: Some(EndTime::NextDay(
Time::from_hms(12, 0, 0).unwrap()
))
})
);
assert_eq!(
klog_parser::range("9:00 - ?"),
Ok(TimeRange {
start: StartTime::SameDay(
Time::from_hms(9, 0, 0).unwrap()
),
end: None,
})
);
assert_eq!(
klog_parser::range("9:20 - ?????????"),
Ok(TimeRange {
start: StartTime::SameDay(
Time::from_hms(9, 20, 0).unwrap()
),
end: None
})
);
}
#[test]
fn tag() {
assert_eq!(klog_parser::tag("hello"), Ok("hello".to_string()));
assert_eq!(
klog_parser::tag("_123hello"),
Ok("_123hello".to_string())
);
}
#[test]
fn tags() {
assert_eq!(
klog_parser::tags("hello #world"),
Ok(vec!["hello".to_string(), "world".to_string()])
);
}
#[test]
fn entry() {
assert_eq!(
klog_parser::entry(" 12:00 - 13:14"),
Ok(Entry::Range(TimeRange {
start: StartTime::SameDay(
Time::from_hms(12, 0, 0).unwrap()
),
end: Some(EndTime::SameDay(
Time::from_hms(13, 14, 0).unwrap()
))
}),)
);
}
#[test]
fn entry_line() {
assert_eq!(
klog_parser::entry_line(
" 12:00 - 13:14 Doing something #something #else"
),
Ok(EntryLine {
entry: Entry::Range(TimeRange {
start: StartTime::SameDay(
Time::from_hms(12, 0, 0).unwrap()
),
end: Some(EndTime::SameDay(
Time::from_hms(13, 14, 0).unwrap()
))
}),
summary: "Doing something".to_string(),
tags: vec!["something".to_string(), "else".to_string()]
})
);
}
#[test]
fn summary() {
assert_eq!(
klog_parser::summary("hello world"),
Ok("hello world".to_string())
);
}
#[test]
fn record() {
let s = r"2021-07-03
This was a crazy day
08:00 - 12:00 Doing some work #mywork #interesting
12:30 - 13:00 Went for a short sleep #powernap
13:00 - 17:00 Some more work #mywork #boring";
assert_eq!(
klog_parser::record(s),
Ok(Record {
header: RecordHeader {
date: Date::from_calendar_date(2021, Month::July, 3)
.unwrap(),
should_total: None,
},
summary: vec!["This was a crazy day".to_string()],
entries: vec![
EntryLine {
entry: Entry::Range(TimeRange {
start: StartTime::SameDay(
Time::from_hms(8, 0, 0).unwrap()
),
end: Some(EndTime::SameDay(
Time::from_hms(12, 0, 0).unwrap()
))
}),
summary: "Doing some work".to_string(),
tags: vec![
"mywork".to_string(),
"interesting".to_string()
]
},
EntryLine {
entry: Entry::Range(TimeRange {
start: StartTime::SameDay(
Time::from_hms(12, 30, 0).unwrap()
),
end: Some(EndTime::SameDay(
Time::from_hms(13, 0, 0).unwrap()
))
}),
summary: "Went for a short sleep".to_string(),
tags: vec!["powernap".to_string()]
},
EntryLine {
entry: Entry::Range(TimeRange {
start: StartTime::SameDay(
Time::from_hms(13, 0, 0).unwrap()
),
end: Some(EndTime::SameDay(
Time::from_hms(17, 0, 0).unwrap()
))
}),
summary: "Some more work".to_string(),
tags: vec![
"mywork".to_string(),
"boring".to_string()
]
}
]
})
);
}
#[test]
fn record_summary_only() {
let s = r"2021-07-03
This was a crazy day
Nothing happened at all
";
assert_eq!(
klog_parser::record(s),
Ok(Record {
header: RecordHeader {
date: Date::from_calendar_date(2021, Month::July, 3)
.unwrap(),
should_total: None,
},
summary: vec![
"This was a crazy day".to_string(),
"Nothing happened at all".to_string()
],
entries: vec![]
})
);
}
#[test]
fn record_entries_only() {
let s = r"2021-07-03
08:00 - 12:00 Doing some work #mywork #interesting
12:30 - 13:00 Went for a short sleep #powernap
13:00 - 17:00 Some more work #mywork #boring";
assert_eq!(
klog_parser::record(s),
Ok(Record {
header: RecordHeader {
date: Date::from_calendar_date(2021, Month::July, 3)
.unwrap(),
should_total: None,
},
summary: vec![],
entries: vec![
EntryLine {
entry: Entry::Range(TimeRange {
start: StartTime::SameDay(
Time::from_hms(8, 0, 0).unwrap()
),
end: Some(EndTime::SameDay(
Time::from_hms(12, 0, 0).unwrap()
))
}),
summary: "Doing some work".to_string(),
tags: vec![
"mywork".to_string(),
"interesting".to_string()
]
},
EntryLine {
entry: Entry::Range(TimeRange {
start: StartTime::SameDay(
Time::from_hms(12, 30, 0).unwrap()
),
end: Some(EndTime::SameDay(
Time::from_hms(13, 0, 0).unwrap()
))
}),
summary: "Went for a short sleep".to_string(),
tags: vec!["powernap".to_string()]
},
EntryLine {
entry: Entry::Range(TimeRange {
start: StartTime::SameDay(
Time::from_hms(13, 0, 0).unwrap()
),
end: Some(EndTime::SameDay(
Time::from_hms(17, 0, 0).unwrap()
))
}),
summary: "Some more work".to_string(),
tags: vec![
"mywork".to_string(),
"boring".to_string()
]
}
]
})
);
}
#[test]
fn file() {
let s = r"2021-07-03
This was a crazy day
08:00 - 12:00 Doing some work #mywork #interesting
12:30 - 13:00 Went for a short sleep #powernap
13:00 - 17:00 Some more work #mywork #boring
2021-07-04
This day was less crazy
Not so much happened
11:00 - 12:00 Had some lunch #lunch
";
assert_eq!(
klog_parser::file(s),
Ok(vec![
Record {
header: RecordHeader {
date: Date::from_calendar_date(
2021,
Month::July,
3
)
.unwrap(),
should_total: None,
},
summary: vec!["This was a crazy day".to_string()],
entries: vec![
EntryLine {
entry: Entry::Range(TimeRange {
start: StartTime::SameDay(
Time::from_hms(8, 0, 0).unwrap()
),
end: Some(EndTime::SameDay(
Time::from_hms(12, 0, 0).unwrap()
))
}),
summary: "Doing some work".to_string(),
tags: vec![
"mywork".to_string(),
"interesting".to_string()
]
},
EntryLine {
entry: Entry::Range(TimeRange {
start: StartTime::SameDay(
Time::from_hms(12, 30, 0).unwrap()
),
end: Some(EndTime::SameDay(
Time::from_hms(13, 0, 0).unwrap()
))
}),
summary: "Went for a short sleep".to_string(),
tags: vec!["powernap".to_string()]
},
EntryLine {
entry: Entry::Range(TimeRange {
start: StartTime::SameDay(
Time::from_hms(13, 0, 0).unwrap()
),
end: Some(EndTime::SameDay(
Time::from_hms(17, 0, 0).unwrap()
))
}),
summary: "Some more work".to_string(),
tags: vec![
"mywork".to_string(),
"boring".to_string()
]
}
]
},
Record {
header: RecordHeader {
date: Date::from_calendar_date(
2021,
Month::July,
4
)
.unwrap(),
should_total: None,
},
summary: vec![
"This day was less crazy".to_string(),
"Not so much happened".to_string()
],
entries: vec![EntryLine {
entry: Entry::Range(TimeRange {
start: StartTime::SameDay(
Time::from_hms(11, 0, 0).unwrap()
),
end: Some(EndTime::SameDay(
Time::from_hms(12, 0, 0).unwrap()
))
}),
summary: "Had some lunch".to_string(),
tags: vec!["lunch".to_string(),]
},]
},
])
);
}
}