From 4769c1233617af145dc7985852814c0a48113240 Mon Sep 17 00:00:00 2001
From: xeruf <27jf@pm.me>
Date: Mon, 23 Dec 2024 02:48:31 +0100
Subject: [PATCH] feat: relative hourstamp parsing

---
 src/helpers.rs | 22 +++++++++++++++++-----
 src/main.rs    |  5 +++--
 src/tasks.rs   | 25 ++++++++++++++-----------
 3 files changed, 34 insertions(+), 18 deletions(-)

diff --git a/src/helpers.rs b/src/helpers.rs
index a35b367..712a837 100644
--- a/src/helpers.rs
+++ b/src/helpers.rs
@@ -58,11 +58,15 @@ pub fn parse_hour_after<T: TimeZone>(str: &str, after: DateTime<T>) -> Option<Da
 }
 
 pub fn parse_date(str: &str) -> Option<DateTime<Utc>> {
+    parse_date_with_ref(str, Local::now())
+}
+
+pub fn parse_date_with_ref(str: &str, reference: DateTime<Local>) -> Option<DateTime<Utc>> {
     // Using two libraries for better exhaustiveness, see https://github.com/uutils/parse_datetime/issues/84
-    match interim::parse_date_string(str, Local::now(), interim::Dialect::Us) {
+    match interim::parse_date_string(str, reference, interim::Dialect::Us) {
         Ok(date) => Some(date.to_utc()),
         Err(e) => {
-            match parse_datetime::parse_datetime_at_date(Local::now(), str) {
+            match parse_datetime::parse_datetime_at_date(reference, str) {
                 Ok(date) => Some(date.to_utc()),
                 Err(_) => {
                     warn!("Could not parse date from \"{str}\": {e}");
@@ -85,8 +89,8 @@ pub fn parse_date(str: &str) -> Option<DateTime<Utc>> {
 /// - Plain number as hour, 18 hours back or 6 hours forward
 /// - Number with prefix as minute offset
 /// - Otherwise try to parse a relative date
-pub fn parse_tracking_stamp(str: &str) -> Option<Timestamp> {
-    if let Some(num) = parse_hour(str, 6) {
+pub fn parse_tracking_stamp(str: &str, after: Option<DateTime<Local>>) -> Option<Timestamp> {
+    if let Some(num) = parse_hour_after(str, after.unwrap_or(Local::now() - TimeDelta::hours(18))) {
         return Some(num.to_timestamp());
     }
     let stripped = str.trim().trim_start_matches('+').trim_start_matches("in ");
@@ -169,7 +173,7 @@ pub fn format_timestamp_relative_to(stamp: &Timestamp, reference: &Timestamp) ->
 
 mod test {
     use super::*;
-    use chrono::{NaiveDate, NaiveDateTime, Timelike};
+    use chrono::{FixedOffset, NaiveDate, Timelike};
     use interim::datetime::DateTime;
 
     #[test]
@@ -203,4 +207,12 @@ mod test {
 
         // TODO test timezone offset issues
     }
+
+    #[test]
+    fn test_timezone() {
+        assert_eq!(
+            FixedOffset::east_opt(7200).unwrap().timestamp_millis_opt(1000).unwrap().time(),
+            NaiveTime::from_hms_opt(2, 0, 1).unwrap()
+        );
+    }
 }
\ No newline at end of file
diff --git a/src/main.rs b/src/main.rs
index 15ade0f..5953294 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -14,7 +14,7 @@ use crate::helpers::*;
 use crate::kinds::{format_tag_basic, match_event_tag, Prio, BASIC_KINDS, PROPERTY_COLUMNS, PROP_KINDS};
 use crate::task::{State, StateChange, Task, MARKER_PROPERTY};
 use crate::tasks::{referenced_event, PropertyCollection, StateFilter, TasksRelay};
-use chrono::Local;
+use chrono::{DateTime, Local, TimeZone, Utc};
 use colored::Colorize;
 use directories::ProjectDirs;
 use env_logger::{Builder, Target, WriteStyle};
@@ -650,7 +650,8 @@ async fn main() -> Result<()> {
                         match arg {
                             None => tasks.move_to(None),
                             Some(arg) => {
-                                if parse_tracking_stamp(arg).and_then(|stamp| tasks.track_at(stamp, None)).is_some() {
+                                if parse_tracking_stamp(arg, Local.timestamp_millis_opt(tasks.get_position_timestamped().0.as_u64() as i64 * 1000).earliest())
+                                    .and_then(|stamp| tasks.track_at(stamp, None)).is_some() {
                                     println!("{}", tasks.times_tracked(15));
                                 }
                                 // So the error message is not covered up
diff --git a/src/tasks.rs b/src/tasks.rs
index 6c8c893..eafae69 100644
--- a/src/tasks.rs
+++ b/src/tasks.rs
@@ -4,13 +4,6 @@ mod tests;
 mod children_traversal;
 mod durations;
 
-use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
-use std::fmt::{Display, Formatter};
-use std::iter::{empty, once, FusedIterator};
-use std::ops::{Deref, Div, Rem};
-use std::str::FromStr;
-use std::time::Duration;
-
 use crate::event_sender::{EventSender, MostrMessage};
 use crate::hashtag::Hashtag;
 use crate::helpers::{
@@ -22,6 +15,8 @@ use crate::task::{State, StateChange, Task, MARKER_DEPENDS, MARKER_PARENT, MARKE
 use crate::tasks::children_traversal::ChildrenTraversal;
 use crate::tasks::durations::{referenced_events, timestamps, Durations};
 pub use crate::tasks::nostr_users::NostrUsers;
+
+use chrono::{Local, TimeDelta};
 use colored::Colorize;
 use itertools::Itertools;
 use log::{debug, error, info, trace, warn};
@@ -30,6 +25,12 @@ use nostr_sdk::{
     SingleLetterTag, Tag, TagKind, Timestamp, Url,
 };
 use regex::bytes::Regex;
+use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
+use std::fmt::{Display, Formatter};
+use std::iter::{empty, once, FusedIterator};
+use std::ops::{Deref, Div, Rem};
+use std::str::FromStr;
+use std::time::Duration;
 use tokio::sync::mpsc::Sender;
 
 const DEFAULT_PRIO: Prio = 25;
@@ -240,6 +241,10 @@ impl TasksRelay {
         self.get_position_at(now()).1
     }
 
+    pub(crate) fn get_position_timestamped(&self) -> (Timestamp, Option<EventId>) {
+        self.get_position_at(now())
+    }
+
     fn sorting_key(&self, task: &Task) -> impl Ord {
         self.sorting
             .iter()
@@ -277,9 +282,7 @@ impl TasksRelay {
                     label
                 } else {
                     format!("{}{}",
-                            if limit > times.len() || limit == usize::MAX { "All ".to_string() }
-                            else if limit < 20 { "Recent ".to_string() }
-                            else { format!("Latest {limit} Entries of ") },
+                            if limit > times.len() || limit == usize::MAX { "All ".to_string() } else if limit < 20 { "Recent ".to_string() } else { format!("Latest {limit} Entries of ") },
                             label)
                 }.italic(),
                 &times[times.len().saturating_sub(limit)..].join("\n"))
@@ -1160,7 +1163,7 @@ impl TasksRelay {
     ///
     /// Returns false and prints a message if parsing failed
     pub(crate) fn track_from(&mut self, str: &str) -> bool {
-        parse_tracking_stamp(str)
+        parse_tracking_stamp(str, None)
             .and_then(|stamp| self.track_at(stamp, self.get_position()))
             .is_some()
     }