summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatthew Sotoudeh <matthewsot@outlook.com>2021-12-27 09:41:26 -0800
committerMatthew Sotoudeh <matthewsot@outlook.com>2021-12-27 09:41:26 -0800
commitbde392442b3a7314cf386cd36b96af53e1c4e3d2 (patch)
treeaae779e51445af417635332f924366d5feb7fa65
parentf0b5ed9e20eadb4595a88d1e79053c82aa2d6aa2 (diff)
Support for blocks
-rw-r--r--src/main.rs165
-rw-r--r--stylesheet.css35
2 files changed, 137 insertions, 63 deletions
diff --git a/src/main.rs b/src/main.rs
index e55ed85..84f95bc 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,7 +2,6 @@ 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 std::collections::HashSet;
use std::collections::HashMap;
@@ -132,95 +131,137 @@ fn cmp_tasks(a: &Task, b: &Task) -> Ordering {
}
}
+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,
+ }
+}
+
fn tasks_to_html(tasks: &Vec<Task>) -> 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 attending with me 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><title>Calendar</title><link rel=\"stylesheet\" href=\"stylesheet.css\"></link></head><body>".to_string();
+ let mut html = "<html><head><meta charset=\"UTF-8\"><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 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_of_week && task.date < start_of_next_week {
+ if task.date >= start_period && task.date < end_period {
week_task_ids.push(i);
}
}
- html.push_str("<table><tr>");
- html.push_str("<th>Time</th>");
- for day_of_week in 0..7 {
+ let min_incr: i64 = 15;
+ let timespans_per_day = (24 * 60 ) / min_incr;
+ let mut table: Vec<Vec<Option<usize>>> = Vec::new();
+ let mut table_tags: Vec<Vec<Vec<&String>>> = Vec::new();
+ for i in 0..timespans_per_day {
+ table.push(Vec::new());
+ table_tags.push(Vec::new());
+ for _ in 0..n_days {
+ table[i as usize].push(None);
+ table_tags[i as usize].push(vec![]);
+ }
+ }
+
+ 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_of_week + Duration::days(day_of_week)).format("%A %-m/%-d/%y").to_string());
+ 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]));
- 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 cell_tasks = Vec::new();
- let mut cell_public_tags = HashSet::new();
- 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;
+ 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..."));
+ // (3) Collect all the (public) tags used.
+ let mut span_public_tags: HashSet<&String> = HashSet::new();
+ for idx in intersecting {
+ span_public_tags.extend(tasks[idx].tags.iter().map(|t| t));
+ }
+ for tag in span_public_tags {
+ if public_tags.contains_key(tag.as_str()) {
+ table_tags[i as usize][offset as usize].push(tag);
}
- match [task.start_time, task.end_time] {
- [Some(start), Some(end)] => {
- if time >= start && time < end {
- cell_tasks.push(i);
- for tag in &task.tags {
- if public_tags.contains_key(&tag.as_str()) {
- cell_public_tags.insert(tag.to_string());
- }
+ }
+ table_tags[i as usize][offset as usize].sort();
+ }
+ }
+ 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];
+ let all_tags = &table_tags[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 rowspan..timespans_per_day {
+ if table[i as usize][col_idx as usize] == task_idx {
+ rowspan += 1;
}
}
+ html.push_str("<td class=\"has-task");
+ for tag in all_tags {
+ 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("\">");
+ if all_tags.len() == 0 {
+ html.push_str("has-task");
+ }
+ let mut any_yet = false;
+ for tag in all_tags {
+ if any_yet {
+ html.push_str(", ");
+ }
+ html.push_str(tag.as_str());
+ any_yet = true;
+ }
+ html.push_str("</a></td>");
}
- _ => continue
- }
- }
- if cell_tasks.len() > 0 {
- html.push_str("<td class=\"has-task");
- for tag in &cell_public_tags {
- html.push_str(" tag-");
- html.push_str(tag.as_str());
- }
- html.push_str("\">");
- html.push_str("<a href=\"#");
- html.push_str("task-");
- html.push_str(cell_tasks.first().expect("").to_string().as_str());
- html.push_str("\">has-task");
- for tag in &cell_public_tags {
- html.push_str(", ");
- html.push_str(tag.as_str());
- }
- html.push_str("</a></td>");
- } else {
- html.push_str("<td></td>");
+ },
+ _ => {
+ 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><ul>");
for i in week_task_ids.iter() {
@@ -228,7 +269,7 @@ fn tasks_to_html(tasks: &Vec<Task>) -> String {
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());
+ 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());
diff --git a/stylesheet.css b/stylesheet.css
index 03dd613..b70e5b5 100644
--- a/stylesheet.css
+++ b/stylesheet.css
@@ -4,15 +4,41 @@ table {
}
th, td {
border: 1px solid black;
- border-collapse: collapse;
padding: 0;
}
+ td {
+ border-collapse: collapse;
+ }
th {
padding: 3px;
+ background-color: #ccc;
+ /* https://stackoverflow.com/questions/41882616/why-border-is-not-visible-with-position-sticky-when-background-color-exists */
+ background-clip: padding-box;
+}
+ th:after{
+ content:'';
+ position:absolute;
+ left: 0;
+ bottom: 0;
+ width:100%;
+ border-bottom: 1px solid black;
+ }
+tr:first-child {
+ position: sticky;
+ top: -1px;
+ border: 1px solid #000;
+ z-index: 100;
+}
+tr > td:first-child {
+ padding-left: 2px;
+ padding-right: 2px;
+ background-color: #ccc;
}
.has-task {
background-color: #aaa;
+ position: relative;
+ background-clip: padding-box;
}
.tag-busy {
background-color: red;
@@ -28,11 +54,18 @@ th {
.tag-join-me {
background-color: #2a2;
}
+.tag-self {
+ background-color: #22aa9d6e;
+}
td a {
display: block;
text-decoration: none;
color: inherit;
padding: 3px;
+ position: absolute;
+ top: 0;
+ height: 100%;
+ width: 100%;
}
li:target {
generated by cgit on debian on lair
contact matthew@masot.net with questions or feedback