summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatthew Sotoudeh <matthewsot@outlook.com>2022-01-23 16:11:52 -0800
committerMatthew Sotoudeh <matthewsot@outlook.com>2022-01-23 16:11:52 -0800
commit3672ea38b85b077986351fd35574442aec81f579 (patch)
tree57eae1dce305ee384228b99d1f4ab10796376f7c
parenta2e2d88bcdbf191273caed442aad3d4ea99437e5 (diff)
Break main.rs into sub-files
-rw-r--r--src/html_calendar.rs192
-rw-r--r--src/main.rs365
-rw-r--r--src/parse.rs144
-rw-r--r--src/task.rs30
4 files changed, 372 insertions, 359 deletions
diff --git a/src/html_calendar.rs b/src/html_calendar.rs
new file mode 100644
index 0000000..b16b987
--- /dev/null
+++ b/src/html_calendar.rs
@@ -0,0 +1,192 @@
+use std::collections::HashMap;
+use chrono::{NaiveTime, Duration, Local};
+use crate::task::*;
+
+pub enum CalendarPrivacy {
+ Public,
+ Private,
+}
+
+pub fn tasks_to_html(tasks: &Vec<Task>, privacy: CalendarPrivacy) -> String {
+ let public_tags = HashMap::from([
+ ("busy", "I will be genuinely busy, e.g., a meeting with others."),
+ ("rough", "The nature of the event (e.g., a hike) makes it difficult to preduct the exact start/end times."),
+ ("tentative", "This event timing is only tentative."),
+ ("join-me", "This is an open event; if you're interested in joining please reach out!"),
+ ("self", "This is scheduled time for me to complete a specific work or personal task; I can usually reschedule such blocks when requested."),
+ ]);
+
+ let mut html = "<html><head><meta charset=\"UTF-8\"><title>Calendar</title><link rel=\"stylesheet\" href=\"calendar_style.css\"></link></head><body>".to_string();
+
+ let n_days = 14;
+ let start_period = Local::now().date().naive_local();
+ let end_period = start_period + Duration::days(n_days);
+ let mut week_task_ids: Vec<usize> = Vec::new();
+ for (i, task) in tasks.iter().enumerate() {
+ if task.date >= start_period && task.date < end_period {
+ week_task_ids.push(i);
+ }
+ }
+
+ let min_incr: i64 = 15;
+ let timespans_per_day = (24 * 60 ) / min_incr;
+ let mut table: Vec<Vec<Option<usize>>> = Vec::new();
+ for i in 0..timespans_per_day {
+ table.push(Vec::new());
+ for _ in 0..n_days {
+ table[i as usize].push(None);
+ }
+ }
+
+ html.push_str("<table>");
+ html.push_str("<tr><th>Time</th>");
+ for offset in 0..n_days {
+ html.push_str("<th>");
+ html.push_str(&(start_period + Duration::days(offset)).format("%a %-m/%-d/%y").to_string());
+ html.push_str("</th>");
+ }
+ html.push_str("</tr>");
+
+ week_task_ids.sort_by(|a, b| cmp_tasks(&tasks[*a], &tasks[*b]));
+
+ for i in 0..timespans_per_day {
+ let timespan_start = NaiveTime::from_hms(0, 0, 0) + Duration::minutes(i * min_incr);
+ let timespan_end = NaiveTime::from_hms(0, 0, 0) + Duration::minutes((i + 1) * min_incr);
+ for offset in 0..n_days {
+ // (1) Find all task ids that intersect this timespan on this day.
+ let this_date = start_period + Duration::days(offset);
+ let on_this_date: Vec<usize>
+ = week_task_ids.iter().map(|&idx| idx)
+ .filter(|&idx| tasks[idx].date == this_date).collect();
+ let intersecting: Vec<usize>
+ = on_this_date.iter().map(|&idx| idx)
+ .filter(|&idx| does_overlap(&timespan_start, &timespan_end, &tasks[idx])).collect();
+ // (2) Find the event ending first and place it in the table.
+ table[i as usize][offset as usize] = intersecting.iter()
+ .map(|&idx| idx)
+ .min_by_key(|&idx| tasks[idx].end_time.expect("Should have an end time at this point..."));
+ }
+ }
+ for row_idx in 0..timespans_per_day {
+ let timespan_start = NaiveTime::from_hms(0, 0, 0) + Duration::minutes(row_idx * min_incr);
+ html.push_str("<tr><td><b>");
+ html.push_str(&timespan_start.format("%l:%M %p").to_string());
+ html.push_str("</b></td>");
+ for col_idx in 0..n_days {
+ let task_idx = table[row_idx as usize][col_idx as usize];
+ match task_idx {
+ Some(idx) => {
+ if row_idx == 0 || table[(row_idx - 1) as usize][col_idx as usize] != task_idx {
+ let mut rowspan = 0;
+ for i in row_idx..timespans_per_day {
+ if table[i as usize][col_idx as usize] == task_idx {
+ rowspan += 1;
+ } else {
+ break;
+ }
+ }
+ html.push_str("<td class=\"has-task");
+ for tag in &tasks[idx].tags {
+ if public_tags.contains_key(tag.as_str()) {
+ html.push_str(" tag-");
+ html.push_str(tag.as_str());
+ }
+ }
+ html.push_str("\" rowspan=\"");
+ html.push_str(rowspan.to_string().as_str());
+ html.push_str("\">");
+ html.push_str("<a href=\"#");
+ html.push_str("task-");
+ html.push_str(idx.to_string().as_str());
+ html.push_str("\">");
+ match privacy {
+ CalendarPrivacy::Public => {
+ let mut any_yet = false;
+ for tag in &tasks[idx].tags {
+ if public_tags.contains_key(tag.as_str()) {
+ if any_yet { html.push_str(", "); }
+ html.push_str(tag.as_str());
+ any_yet = true;
+ }
+ }
+ if tasks[idx].tags.contains(&&"public".to_string()) {
+ if any_yet { html.push_str(": \""); }
+ html.push_str(&tasks[idx].details.as_str());
+ html.push_str("\"");
+ any_yet = true;
+ }
+ if !any_yet {
+ html.push_str("has-task");
+ }
+ },
+ CalendarPrivacy::Private => {
+ html.push_str(&tasks[idx].details.as_str());
+ },
+ }
+ html.push_str("</a></td>");
+ }
+ },
+ _ => {
+ html.push_str("<td></td>");
+ },
+ }
+ }
+ html.push_str("</tr>");
+ }
+ html.push_str("</table><ul>");
+ for i in week_task_ids.iter() {
+ let task = &tasks[*i];
+ let is_public = task.tags.contains(&"public".to_string());
+ match (&privacy, &task.start_time, &is_public) {
+ (CalendarPrivacy::Public, None, false) => continue,
+ _ => (),
+ }
+ html.push_str("<li id=\"task-");
+ html.push_str(i.to_string().as_str());
+ html.push_str("\">");
+ html.push_str(task.date.format("%a %-m/%-d/%y ").to_string().as_str());
+ match [task.start_time, task.end_time] {
+ [Some(start), Some(end)] => {
+ html.push_str(start.format("%l:%M%p").to_string().as_str());
+ html.push_str(" -- ");
+ html.push_str(end.format("%l:%M%p").to_string().as_str());
+ }
+ _ => (),
+ }
+ html.push_str("<ul>");
+ match privacy {
+ CalendarPrivacy::Public => {
+ if task.tags.contains(&"public".to_string()) {
+ html.push_str("<li><b>Description:</b> ");
+ html.push_str(task.details.as_str());
+ html.push_str("</li>");
+ }
+ for tag in &task.tags {
+ if public_tags.contains_key(&tag.as_str()) {
+ html.push_str("<li>Tagged <b>");
+ html.push_str(tag.as_str());
+ html.push_str(":</b> ");
+ html.push_str(public_tags.get(&tag.as_str()).expect(""));
+ html.push_str("</li>");
+ }
+ }
+ },
+ CalendarPrivacy::Private => {
+ html.push_str("<li><b>Description:</b> ");
+ html.push_str(task.details.as_str());
+ html.push_str("</li><li>Tagged: ");
+ for (i, tag) in task.tags.iter().enumerate() {
+ if i > 0 { html.push_str(", "); }
+ html.push_str("<b>");
+ html.push_str(tag.as_str());
+ html.push_str("</b>");
+ }
+ html.push_str("</li>");
+ }
+ }
+ html.push_str("</ul>");
+ html.push_str("</li>");
+ }
+ html.push_str("</ul><a href=\"https://lair.masot.net/git/wtd\">src</a></body></html>");
+ return html;
+}
diff --git a/src/main.rs b/src/main.rs
index 593d413..6dfb441 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,325 +1,12 @@
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
-use std::str::FromStr;
-use std::cmp::Ordering;
-use std::collections::HashMap;
-use chrono::{Datelike, NaiveDate, NaiveTime, Weekday, Duration, Timelike, Local};
-// use chrono::format::ParseError;
-struct Task {
- date: NaiveDate,
- start_time: Option<NaiveTime>,
- end_time: Option<NaiveTime>,
- details: String,
- tags: Vec<String>,
-}
-
-fn parse_date_line(l: &str) -> Option<NaiveDate> {
- for maybe_date_str in l.split(' ') {
- if let Ok(date) = NaiveDate::parse_from_str(maybe_date_str, "%m/%d/%y") {
- return Some(date);
- }
- };
- return None;
-}
-
-fn parse_day_line(l: &str) -> Weekday {
- let daystr = l.get(3..).expect("Day-of-week line not long enough...");
- return Weekday::from_str(daystr).expect("Misparse day-of-week str...");
-}
-
-fn parse_time(s_: &str) -> NaiveTime {
- let formats = vec!["%l:%M%p", "%H:%M"];
- let mut s = s_.to_string();
- if !s.contains(":") {
- if s.ends_with("M") {
- s.insert_str(s.len() - 2, ":00");
- } else {
- s.push_str(":00");
- }
- }
- for format in formats {
- if let Ok(parsed) = NaiveTime::parse_from_str(&s, format) {
- if !format.contains("%p") && parsed.hour() < 6 {
- return parsed + Duration::hours(12);
- }
- return parsed;
- }
- }
- panic!("Couldn't parse time {}", s);
-}
-
-fn parse_duration(s: &str) -> chrono::Duration {
- // We try to find Mm, HhMm, Hh
- if s.contains("h") && s.contains("m") {
- // TODO: Decompose this case into the two below.
- let hstr = s.split("h").collect::<Vec<&str>>().get(0).expect("").to_string();
- let mstr = s.split("h").collect::<Vec<&str>>().get(1).expect("").split("m").collect::<Vec<&str>>().get(0).expect("Expected XhYm").to_string();
- let secs = ((hstr.parse::<u64>().unwrap() * 60)
- + (mstr.parse::<u64>().unwrap())) * 60;
- return chrono::Duration::from_std(std::time::Duration::new(secs, 0)).unwrap();
- } else if s.contains("h") {
- let hstr = s.split("h").collect::<Vec<&str>>().get(0).expect("").to_string();
- let secs = hstr.parse::<u64>().unwrap() * 60 * 60;
- return chrono::Duration::from_std(std::time::Duration::new(secs, 0)).unwrap();
- } else if s.contains("m") {
- let hstr = s.split("m").collect::<Vec<&str>>().get(0).expect("").to_string();
- let secs = hstr.parse::<u64>().unwrap() * 60;
- return chrono::Duration::from_std(std::time::Duration::new(secs, 0)).unwrap();
- }
- panic!("Couldn't parse duration {}", s);
-}
-
-fn handle_task_details(l: &str, t: &mut Task) {
- for tok in l.split(' ') {
- if tok.starts_with("+") {
- let tag = tok.get(1..).expect("Unexpected");
- t.tags.push(tag.to_string());
- } else if tok.starts_with("@") {
- let timestr = tok.get(1..).expect("Unexpected");
- if timestr.contains("+") { // @Start+Duration
- let parts: Vec<&str> = timestr.split("+").collect();
- match parts[..] {
- [startstr, durstr] => {
- t.start_time = Some(parse_time(startstr));
- t.end_time = Some(t.start_time.unwrap() + parse_duration(durstr));
- },
- _ => panic!("Not 2 parts to {}\n", timestr)
- }
- } else if timestr.contains("--") { // @Start--End
- let parts: Vec<&str> = timestr.split("--").collect();
- match parts[..] {
- [startstr, endstr] => {
- t.start_time = Some(parse_time(startstr));
- t.end_time = Some(parse_time(endstr));
- if t.start_time > t.end_time {
- panic!("Start time {} interpreted as after end time {}",
- startstr, endstr);
- }
- },
- _ => panic!("Not 2 parts to {}\n", timestr)
- }
- } else {
- panic!("'{}' is not of the form Start+Duration or Start--End\n", timestr);
- }
- } else {
- if t.details.len() > 0 {
- t.details.push(' ');
- }
- t.details.push_str(tok.trim());
- }
- }
-}
-
-fn cmp_tasks(a: &Task, b: &Task) -> Ordering {
- if a.date != b.date {
- return if a.date < b.date { Ordering::Less } else { Ordering::Greater };
- }
- match [a.start_time, b.start_time] {
- [None, None] => return Ordering::Equal,
- [None, Some(_)] => return Ordering::Greater,
- [Some(_), None] => return Ordering::Less,
- [Some(atime), Some(btime)] => return if atime < btime { Ordering::Less } else { Ordering::Greater },
- }
-}
-
-fn does_overlap(timespan_start: &NaiveTime, timespan_end: &NaiveTime, task: &Task) -> bool {
- return match [task.start_time, task.end_time] {
- [Some(start), Some(end)] => (&start <= timespan_start && timespan_start < &end)
- || (&start < timespan_end && timespan_end < &end),
- _ => false,
- }
-}
-
-enum CalendarPrivacy {
- Public,
- Private,
-}
-
-fn tasks_to_html(tasks: &Vec<Task>, privacy: CalendarPrivacy) -> String {
- let public_tags = HashMap::from([
- ("busy", "I will be genuinely busy, e.g., a meeting with others."),
- ("rough", "The nature of the event (e.g., a hike) makes it difficult to preduct the exact start/end times."),
- ("tentative", "This event timing is only tentative."),
- ("join-me", "This is an open event; if you're interested in joining please reach out!"),
- ("self", "This is scheduled time for me to complete a specific work or personal task; I can usually reschedule such blocks when requested."),
- ]);
-
- let mut html = "<html><head><meta charset=\"UTF-8\"><title>Calendar</title><link rel=\"stylesheet\" href=\"calendar_style.css\"></link></head><body>".to_string();
-
- let n_days = 14;
- let start_period = Local::now().date().naive_local();
- let end_period = start_period + Duration::days(n_days);
- let mut week_task_ids: Vec<usize> = Vec::new();
- for (i, task) in tasks.iter().enumerate() {
- if task.date >= start_period && task.date < end_period {
- week_task_ids.push(i);
- }
- }
-
- let min_incr: i64 = 15;
- let timespans_per_day = (24 * 60 ) / min_incr;
- let mut table: Vec<Vec<Option<usize>>> = Vec::new();
- for i in 0..timespans_per_day {
- table.push(Vec::new());
- for _ in 0..n_days {
- table[i as usize].push(None);
- }
- }
-
- html.push_str("<table>");
- html.push_str("<tr><th>Time</th>");
- for offset in 0..n_days {
- html.push_str("<th>");
- html.push_str(&(start_period + Duration::days(offset)).format("%a %-m/%-d/%y").to_string());
- html.push_str("</th>");
- }
- html.push_str("</tr>");
-
- week_task_ids.sort_by(|a, b| cmp_tasks(&tasks[*a], &tasks[*b]));
-
- for i in 0..timespans_per_day {
- let timespan_start = NaiveTime::from_hms(0, 0, 0) + Duration::minutes(i * min_incr);
- let timespan_end = NaiveTime::from_hms(0, 0, 0) + Duration::minutes((i + 1) * min_incr);
- for offset in 0..n_days {
- // (1) Find all task ids that intersect this timespan on this day.
- let this_date = start_period + Duration::days(offset);
- let on_this_date: Vec<usize>
- = week_task_ids.iter().map(|&idx| idx)
- .filter(|&idx| tasks[idx].date == this_date).collect();
- let intersecting: Vec<usize>
- = on_this_date.iter().map(|&idx| idx)
- .filter(|&idx| does_overlap(&timespan_start, &timespan_end, &tasks[idx])).collect();
- // (2) Find the event ending first and place it in the table.
- table[i as usize][offset as usize] = intersecting.iter()
- .map(|&idx| idx)
- .min_by_key(|&idx| tasks[idx].end_time.expect("Should have an end time at this point..."));
- }
- }
- for row_idx in 0..timespans_per_day {
- let timespan_start = NaiveTime::from_hms(0, 0, 0) + Duration::minutes(row_idx * min_incr);
- html.push_str("<tr><td><b>");
- html.push_str(&timespan_start.format("%l:%M %p").to_string());
- html.push_str("</b></td>");
- for col_idx in 0..n_days {
- let task_idx = table[row_idx as usize][col_idx as usize];
- match task_idx {
- Some(idx) => {
- if row_idx == 0 || table[(row_idx - 1) as usize][col_idx as usize] != task_idx {
- let mut rowspan = 0;
- for i in row_idx..timespans_per_day {
- if table[i as usize][col_idx as usize] == task_idx {
- rowspan += 1;
- } else {
- break;
- }
- }
- html.push_str("<td class=\"has-task");
- for tag in &tasks[idx].tags {
- if public_tags.contains_key(tag.as_str()) {
- html.push_str(" tag-");
- html.push_str(tag.as_str());
- }
- }
- html.push_str("\" rowspan=\"");
- html.push_str(rowspan.to_string().as_str());
- html.push_str("\">");
- html.push_str("<a href=\"#");
- html.push_str("task-");
- html.push_str(idx.to_string().as_str());
- html.push_str("\">");
- match privacy {
- CalendarPrivacy::Public => {
- let mut any_yet = false;
- for tag in &tasks[idx].tags {
- if public_tags.contains_key(tag.as_str()) {
- if any_yet { html.push_str(", "); }
- html.push_str(tag.as_str());
- any_yet = true;
- }
- }
- if tasks[idx].tags.contains(&&"public".to_string()) {
- if any_yet { html.push_str(": \""); }
- html.push_str(&tasks[idx].details.as_str());
- html.push_str("\"");
- any_yet = true;
- }
- if !any_yet {
- html.push_str("has-task");
- }
- },
- CalendarPrivacy::Private => {
- html.push_str(&tasks[idx].details.as_str());
- },
- }
- html.push_str("</a></td>");
- }
- },
- _ => {
- html.push_str("<td></td>");
- },
- }
- }
- html.push_str("</tr>");
- }
- html.push_str("</table><ul>");
- for i in week_task_ids.iter() {
- let task = &tasks[*i];
- let is_public = task.tags.contains(&"public".to_string());
- match (&privacy, &task.start_time, &is_public) {
- (CalendarPrivacy::Public, None, false) => continue,
- _ => (),
- }
- html.push_str("<li id=\"task-");
- html.push_str(i.to_string().as_str());
- html.push_str("\">");
- html.push_str(task.date.format("%a %-m/%-d/%y ").to_string().as_str());
- match [task.start_time, task.end_time] {
- [Some(start), Some(end)] => {
- html.push_str(start.format("%l:%M%p").to_string().as_str());
- html.push_str(" -- ");
- html.push_str(end.format("%l:%M%p").to_string().as_str());
- }
- _ => (),
- }
- html.push_str("<ul>");
- match privacy {
- CalendarPrivacy::Public => {
- if task.tags.contains(&"public".to_string()) {
- html.push_str("<li><b>Description:</b> ");
- html.push_str(task.details.as_str());
- html.push_str("</li>");
- }
- for tag in &task.tags {
- if public_tags.contains_key(&tag.as_str()) {
- html.push_str("<li>Tagged <b>");
- html.push_str(tag.as_str());
- html.push_str(":</b> ");
- html.push_str(public_tags.get(&tag.as_str()).expect(""));
- html.push_str("</li>");
- }
- }
- },
- CalendarPrivacy::Private => {
- html.push_str("<li><b>Description:</b> ");
- html.push_str(task.details.as_str());
- html.push_str("</li><li>Tagged: ");
- for (i, tag) in task.tags.iter().enumerate() {
- if i > 0 { html.push_str(", "); }
- html.push_str("<b>");
- html.push_str(tag.as_str());
- html.push_str("</b>");
- }
- html.push_str("</li>");
- }
- }
- html.push_str("</ul>");
- html.push_str("</li>");
- }
- html.push_str("</ul><a href=\"https://lair.masot.net/git/wtd\">src</a></body></html>");
- return html;
-}
+mod task;
+mod parse;
+use parse::*;
+mod html_calendar;
+use html_calendar::*;
// https://doc.rust-lang.org/std/fs/struct.File.html
fn main() {
@@ -337,47 +24,7 @@ fn main() {
match file.read_to_string(&mut s) {
Err(why) => panic!("Couldn't read {}: {}", display, why),
Ok(_) => {
-
- let mut tasks = Vec::new();
- let mut start_date = None;
- let mut the_date = None;
- for l in s.split('\n') {
- if l.starts_with("# ") {
- // '# 12/27/21', starts a new week block
- start_date = parse_date_line(l);
- } else if l.starts_with("## ") {
- // '## Monday/Tuesday/...', starts a new day block
- // Need to compute the actual date, basically looking for the first one after
- // start_date.
- let dayofweek = parse_day_line(l);
- let mut current = start_date.expect("Invalid or missing '# ' date");
- the_date = loop {
- if current.weekday() == dayofweek {
- break Some(current);
- }
- current = current.succ();
- };
- } else if l.starts_with("- [ ]") || l.starts_with("- [X]") {
- // '- [ ] ...', starts a new task block
- let date = the_date.expect("No current date parsed yet...");
- tasks.push(Task {
- date: date,
- start_time: None,
- end_time: None,
- details: "".to_string(),
- tags: Vec::new(),
- });
- let details = l.get(5..).expect("").trim();
- handle_task_details(details, tasks.last_mut().expect("Unexpected error..."));
- } else if l.starts_with(" ") {
- // Extends the last task.
- handle_task_details(l, tasks.last_mut().expect("Unexpected error..."));
- } else {
- if l.trim().len() > 0 {
- eprint!("Ignoring line: {}\n", l);
- }
- }
- }
+ let tasks = parse_file_str(&s);
let public_html = tasks_to_html(&tasks, CalendarPrivacy::Public);
let private_html = tasks_to_html(&tasks, CalendarPrivacy::Private);
// https://riptutorial.com/rust/example/4276/write-in-a-file
diff --git a/src/parse.rs b/src/parse.rs
new file mode 100644
index 0000000..079e83a
--- /dev/null
+++ b/src/parse.rs
@@ -0,0 +1,144 @@
+use std::str::FromStr;
+use chrono::{Datelike, NaiveDate, NaiveTime, Weekday, Duration, Timelike};
+
+// mod task;
+use crate::task::*;
+
+pub fn parse_file_str(file_str: &str) -> Vec<Task> {
+ let mut tasks = Vec::new();
+ let mut start_date = None;
+ let mut the_date = None;
+ for l in file_str.split('\n') {
+ if l.starts_with("# ") {
+ // '# 12/27/21', starts a new week block
+ start_date = parse_date_line(l);
+ } else if l.starts_with("## ") {
+ // '## Monday/Tuesday/...', starts a new day block
+ // Need to compute the actual date, basically looking for the first one after
+ // start_date.
+ let dayofweek = parse_day_line(l);
+ let mut current = start_date.expect("Invalid or missing '# ' date");
+ the_date = loop {
+ if current.weekday() == dayofweek {
+ break Some(current);
+ }
+ current = current.succ();
+ };
+ } else if l.starts_with("- [ ]") || l.starts_with("- [X]") {
+ // '- [ ] ...', starts a new task block
+ let date = the_date.expect("No current date parsed yet...");
+ tasks.push(Task {
+ date: date,
+ start_time: None,
+ end_time: None,
+ details: "".to_string(),
+ tags: Vec::new(),
+ });
+ let details = l.get(5..).expect("").trim();
+ handle_task_details(details, tasks.last_mut().expect("Unexpected error..."));
+ } else if l.starts_with(" ") {
+ // Extends the last task.
+ handle_task_details(l, tasks.last_mut().expect("Unexpected error..."));
+ } else if l.trim().len() > 0 {
+ eprint!("Ignoring line: {}\n", l);
+ }
+ }
+ return tasks;
+}
+
+fn parse_date_line(l: &str) -> Option<NaiveDate> {
+ for maybe_date_str in l.split(' ') {
+ if let Ok(date) = NaiveDate::parse_from_str(maybe_date_str, "%m/%d/%y") {
+ return Some(date);
+ }
+ };
+ return None;
+}
+
+fn parse_day_line(l: &str) -> Weekday {
+ let daystr = l.get(3..).expect("Day-of-week line not long enough...");
+ return Weekday::from_str(daystr).expect("Misparse day-of-week str...");
+}
+
+fn parse_time(s_: &str) -> NaiveTime {
+ let formats = vec!["%l:%M%p", "%H:%M"];
+ let mut s = s_.to_string();
+ if !s.contains(":") {
+ if s.ends_with("M") {
+ s.insert_str(s.len() - 2, ":00");
+ } else {
+ s.push_str(":00");
+ }
+ }
+ for format in formats {
+ if let Ok(parsed) = NaiveTime::parse_from_str(&s, format) {
+ if !format.contains("%p") && parsed.hour() < 6 {
+ return parsed + Duration::hours(12);
+ }
+ return parsed;
+ }
+ }
+ panic!("Couldn't parse time {}", s);
+}
+
+fn parse_duration(s: &str) -> chrono::Duration {
+ // We try to find Mm, HhMm, Hh
+ if s.contains("h") && s.contains("m") {
+ // TODO: Decompose this case into the two below.
+ let hstr = s.split("h").collect::<Vec<&str>>().get(0).expect("").to_string();
+ let mstr = s.split("h").collect::<Vec<&str>>().get(1).expect("").split("m").collect::<Vec<&str>>().get(0).expect("Expected XhYm").to_string();
+ let secs = ((hstr.parse::<u64>().unwrap() * 60)
+ + (mstr.parse::<u64>().unwrap())) * 60;
+ return chrono::Duration::from_std(std::time::Duration::new(secs, 0)).unwrap();
+ } else if s.contains("h") {
+ let hstr = s.split("h").collect::<Vec<&str>>().get(0).expect("").to_string();
+ let secs = hstr.parse::<u64>().unwrap() * 60 * 60;
+ return chrono::Duration::from_std(std::time::Duration::new(secs, 0)).unwrap();
+ } else if s.contains("m") {
+ let hstr = s.split("m").collect::<Vec<&str>>().get(0).expect("").to_string();
+ let secs = hstr.parse::<u64>().unwrap() * 60;
+ return chrono::Duration::from_std(std::time::Duration::new(secs, 0)).unwrap();
+ }
+ panic!("Couldn't parse duration {}", s);
+}
+
+fn handle_task_details(l: &str, t: &mut Task) {
+ for tok in l.split(' ') {
+ if tok.starts_with("+") {
+ let tag = tok.get(1..).expect("Unexpected");
+ t.tags.push(tag.to_string());
+ } else if tok.starts_with("@") {
+ let timestr = tok.get(1..).expect("Unexpected");
+ if timestr.contains("+") { // @Start+Duration
+ let parts: Vec<&str> = timestr.split("+").collect();
+ match parts[..] {
+ [startstr, durstr] => {
+ t.start_time = Some(parse_time(startstr));
+ t.end_time = Some(t.start_time.unwrap() + parse_duration(durstr));
+ },
+ _ => panic!("Not 2 parts to {}\n", timestr)
+ }
+ } else if timestr.contains("--") { // @Start--End
+ let parts: Vec<&str> = timestr.split("--").collect();
+ match parts[..] {
+ [startstr, endstr] => {
+ t.start_time = Some(parse_time(startstr));
+ t.end_time = Some(parse_time(endstr));
+ if t.start_time > t.end_time {
+ panic!("Start time {} interpreted as after end time {}",
+ startstr, endstr);
+ }
+ },
+ _ => panic!("Not 2 parts to {}\n", timestr)
+ }
+ } else {
+ panic!("'{}' is not of the form Start+Duration or Start--End\n", timestr);
+ }
+ } else {
+ if t.details.len() > 0 {
+ t.details.push(' ');
+ }
+ t.details.push_str(tok.trim());
+ }
+ }
+}
diff --git a/src/task.rs b/src/task.rs
new file mode 100644
index 0000000..871d8ff
--- /dev/null
+++ b/src/task.rs
@@ -0,0 +1,30 @@
+use std::cmp::Ordering;
+use chrono::{NaiveDate, NaiveTime};
+
+pub struct Task {
+ pub date: NaiveDate,
+ pub start_time: Option<NaiveTime>,
+ pub end_time: Option<NaiveTime>,
+ pub details: String,
+ pub tags: Vec<String>,
+}
+
+pub fn cmp_tasks(a: &Task, b: &Task) -> Ordering {
+ if a.date != b.date {
+ return if a.date < b.date { Ordering::Less } else { Ordering::Greater };
+ }
+ match [a.start_time, b.start_time] {
+ [None, None] => return Ordering::Equal,
+ [None, Some(_)] => return Ordering::Greater,
+ [Some(_), None] => return Ordering::Less,
+ [Some(atime), Some(btime)] => return if atime < btime { Ordering::Less } else { Ordering::Greater },
+ }
+}
+
+pub fn does_overlap(timespan_start: &NaiveTime, timespan_end: &NaiveTime, task: &Task) -> bool {
+ return match [task.start_time, task.end_time] {
+ [Some(start), Some(end)] => (&start <= timespan_start && timespan_start < &end)
+ || (&start < timespan_end && timespan_end < &end),
+ _ => false,
+ }
+}
generated by cgit on debian on lair
contact matthew@masot.net with questions or feedback