diff options
author | Matthew Sotoudeh <matthewsot@outlook.com> | 2021-12-27 03:02:42 -0800 |
---|---|---|
committer | Matthew Sotoudeh <matthewsot@outlook.com> | 2021-12-27 03:02:42 -0800 |
commit | 8c8ea69902c0bdff3d4d9c8d562689ea45da79c2 (patch) | |
tree | 2e558207c5a79ab35d2d786ac8c4886fdb9b3796 |
Generates a simple calendar table
-rw-r--r-- | .gitignore | 4 | ||||
-rw-r--r-- | Cargo.lock | 91 | ||||
-rw-r--r-- | Cargo.toml | 10 | ||||
-rw-r--r-- | README.md | 7 | ||||
-rw-r--r-- | src/main.rs | 260 | ||||
-rw-r--r-- | stylesheet.css | 9 |
6 files changed, 381 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ef768c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +target +.*.sw* +*.html +wtd.md diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..6c56970 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,91 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time", + "winapi", +] + +[[package]] +name = "libc" +version = "0.2.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi", + "winapi", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "wtd" +version = "0.1.0" +dependencies = [ + "chrono", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..66ed18f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "wtd" +version = "0.1.0" +authors = ["Matthew Sotoudeh <matthewsot@outlook.com>"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +chrono = "0.4" diff --git a/README.md b/README.md new file mode 100644 index 0000000..ef07666 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# goal +Basically a big list of tasks, organized into days. Tasks can be tagged with +times (@9AM, @10AM+2hr, @9:45AM--11AM etc.), and with arbitrary tags (+public, ++busy, +weekly, etc.). +- `wtd carryover` finds `+weekly` tags from the current week and copies those + events to the upcoming week. +- `wtd publish` publishes a calendar on my website showing my availability diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..04d335e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,260 @@ +use std::fs::File; +use std::io::prelude::*; +use std::path::Path; +use std::str::FromStr; +use std::convert::TryInto; +use std::cmp::Ordering; +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(' ') { + match NaiveDate::parse_from_str(maybe_date_str, "%m/%d/%y") { + Err(_) => continue, + Ok(date) => { 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!["%H:%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 { + match NaiveTime::parse_from_str(&s, format) { + Err(_) => continue, + Ok(parsed) => { + 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) { + if t.details.len() > 0 { + t.details.push(' '); + } + t.details.push_str(l.trim()); + 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); + } + } + } +} + +fn cmp_tasks(a: &Task, b: &Task) -> Ordering { + if a.date < b.date { + return Ordering::Less; + } else if b.date < a.date { + return 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 tasks_to_html(tasks: &Vec<Task>) -> String { + let mut html = "<html><head><title>Calendar</title><link rel=\"stylesheet\" href=\"stylesheet.css\"></link></head><body>".to_string(); + + let today = Local::now().date().naive_local(); + let start_of_week = today - Duration::days(today.weekday().num_days_from_monday().try_into().unwrap()); + let start_of_next_week = today + (Duration::days(7) - Duration::days(today.weekday().num_days_from_monday().try_into().unwrap())); + let mut week_task_ids: Vec<usize> = Vec::new(); + for (i, task) in tasks.iter().enumerate() { + if task.date >= start_of_week && task.date < start_of_next_week { + week_task_ids.push(i); + } + } + + html.push_str("<table><tr>"); + html.push_str("<th>Time</th>"); + for day_of_week in 0..7 { + html.push_str("<th>"); + html.push_str(&(start_of_week + Duration::days(day_of_week)).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])); + + let min_incr = 15; + let mut time = NaiveTime::from_hms(0, 0, 0); + loop { + html.push_str("<tr><td>"); + html.push_str(&time.format("%l:%M %p").to_string()); + html.push_str("</td>"); + for day_of_week in 0..7 { + // TODO: Use a smarter data structure for this. + let mut any_task = false; + for i in week_task_ids.iter() { + let task = &tasks[*i]; + let task_day = task.date.weekday().num_days_from_monday(); + if task_day > day_of_week { + break; + } else if task_day < day_of_week { + continue; + } + match [task.start_time, task.end_time] { + [Some(start), Some(end)] => { + if time >= start && time < end { + any_task = true; + break; + } + } + _ => continue + } + } + if any_task { + html.push_str("<td class=\"busy\"></td>"); + } else { + html.push_str("<td></td>"); + } + } + html.push_str("</tr>"); + + time = time + Duration::minutes(min_incr); + if time == NaiveTime::from_hms(0, 0, 0) { + break; + } + } + html.push_str("</table></body></html>"); + return html; +} + +// https://doc.rust-lang.org/std/fs/struct.File.html +fn main() { + let path = Path::new("wtd.md"); + let display = path.display(); + + // Open the path in read-only mode, returns `io::Result<File>` + let mut file = match File::open(&path) { + Err(why) => panic!("Error opening {}: {}", display, why), + Ok(file) => file, + }; + + // Read the file contents into a string, returns `io::Result<usize>` + let mut s = String::new(); + 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("- [ ]") { + // '- [ ] ...', 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 { + print!("Ignoring line: {}\n", l); + } + } + } + print!("{}\n", tasks_to_html(&tasks)); + } + } +} diff --git a/stylesheet.css b/stylesheet.css new file mode 100644 index 0000000..ee55c50 --- /dev/null +++ b/stylesheet.css @@ -0,0 +1,9 @@ +table, th, td { + border: 1px solid black; + border-collapse: collapse; + padding: 3px; +} + +.busy { + background-color: gray; +} |