summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatthew Sotoudeh <matthewsot@outlook.com>2021-12-27 03:02:42 -0800
committerMatthew Sotoudeh <matthewsot@outlook.com>2021-12-27 03:02:42 -0800
commit8c8ea69902c0bdff3d4d9c8d562689ea45da79c2 (patch)
tree2e558207c5a79ab35d2d786ac8c4886fdb9b3796
Generates a simple calendar table
-rw-r--r--.gitignore4
-rw-r--r--Cargo.lock91
-rw-r--r--Cargo.toml10
-rw-r--r--README.md7
-rw-r--r--src/main.rs260
-rw-r--r--stylesheet.css9
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;
+}
generated by cgit on debian on lair
contact matthew@masot.net with questions or feedback