Compare commits

...

2 Commits
main ... dev

Author SHA1 Message Date
xeruf 4769c12336 feat: relative hourstamp parsing 2024-12-23 02:48:31 +01:00
xeruf 1b065c434f feat(helpers): parse HHMM timestamps 2024-12-18 14:22:13 +01:00
3 changed files with 86 additions and 26 deletions

View File

@ -1,5 +1,3 @@
use std::ops::Sub;
use chrono::LocalResult::Single; use chrono::LocalResult::Single;
use chrono::{DateTime, Local, NaiveTime, TimeDelta, TimeZone, Utc}; use chrono::{DateTime, Local, NaiveTime, TimeDelta, TimeZone, Utc};
use log::{debug, error, info, trace, warn}; use log::{debug, error, info, trace, warn};
@ -34,16 +32,24 @@ impl<T: TimeZone> ToTimestamp for DateTime<T> {
} }
} }
/// Parses the hour from a plain number in the String,
/// Parses the hour optionally with minute from a plain number in a String,
/// with max of max_future hours into the future. /// with max of max_future hours into the future.
// TODO parse HHMM as well
pub fn parse_hour(str: &str, max_future: i64) -> Option<DateTime<Local>> { pub fn parse_hour(str: &str, max_future: i64) -> Option<DateTime<Local>> {
str.parse::<u32>().ok().and_then(|hour| { parse_hour_after(str, Local::now() - TimeDelta::hours(24 - max_future))
let now = Local::now(); }
/// Parses the hour optionally with minute from a plain number in a String.
pub fn parse_hour_after<T: TimeZone>(str: &str, after: DateTime<T>) -> Option<DateTime<T>> {
str.parse::<u32>().ok().and_then(|number| {
#[allow(deprecated)] #[allow(deprecated)]
now.date().and_hms_opt(hour, 0, 0).map(|time| { after.date().and_hms_opt(
if time - now > TimeDelta::hours(max_future) { if number > 23 { number / 100 } else { number },
time.sub(TimeDelta::days(1)) if number > 23 { number % 100 } else { 0 },
0,
).map(|time| {
if time < after {
time + TimeDelta::days(1)
} else { } else {
time time
} }
@ -52,11 +58,15 @@ pub fn parse_hour(str: &str, max_future: i64) -> Option<DateTime<Local>> {
} }
pub fn parse_date(str: &str) -> Option<DateTime<Utc>> { 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 // 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()), Ok(date) => Some(date.to_utc()),
Err(e) => { 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()), Ok(date) => Some(date.to_utc()),
Err(_) => { Err(_) => {
warn!("Could not parse date from \"{str}\": {e}"); warn!("Could not parse date from \"{str}\": {e}");
@ -79,8 +89,8 @@ pub fn parse_date(str: &str) -> Option<DateTime<Utc>> {
/// - Plain number as hour, 18 hours back or 6 hours forward /// - Plain number as hour, 18 hours back or 6 hours forward
/// - Number with prefix as minute offset /// - Number with prefix as minute offset
/// - Otherwise try to parse a relative date /// - Otherwise try to parse a relative date
pub fn parse_tracking_stamp(str: &str) -> Option<Timestamp> { pub fn parse_tracking_stamp(str: &str, after: Option<DateTime<Local>>) -> Option<Timestamp> {
if let Some(num) = parse_hour(str, 6) { if let Some(num) = parse_hour_after(str, after.unwrap_or(Local::now() - TimeDelta::hours(18))) {
return Some(num.to_timestamp()); return Some(num.to_timestamp());
} }
let stripped = str.trim().trim_start_matches('+').trim_start_matches("in "); let stripped = str.trim().trim_start_matches('+').trim_start_matches("in ");
@ -160,3 +170,49 @@ pub fn format_timestamp_relative_to(stamp: &Timestamp, reference: &Timestamp) ->
_ => format_timestamp_local(stamp), _ => format_timestamp_local(stamp),
} }
} }
mod test {
use super::*;
use chrono::{FixedOffset, NaiveDate, Timelike};
use interim::datetime::DateTime;
#[test]
fn parse_hours() {
let now = Local::now();
#[allow(deprecated)]
let date = now.date();
if now.hour() > 2 {
assert_eq!(
parse_hour("23", 22).unwrap(),
date.and_hms_opt(23, 0, 0).unwrap()
);
}
if now.hour() < 22 {
assert_eq!(
parse_hour("02", 2).unwrap(),
date.and_hms_opt(2, 0, 0).unwrap()
);
assert_eq!(
parse_hour("2301", 1).unwrap(),
(date - TimeDelta::days(1)).and_hms_opt(23, 01, 0).unwrap()
);
}
let date = NaiveDate::from_ymd_opt(2020, 10, 10).unwrap();
let time = Utc.from_utc_datetime(
&date.and_hms_opt(10, 1,0).unwrap()
);
assert_eq!(parse_hour_after("2201", time).unwrap(), Utc.from_utc_datetime(&date.and_hms_opt(22, 1, 0).unwrap()));
assert_eq!(parse_hour_after("10", time).unwrap(), Utc.from_utc_datetime(&(date + TimeDelta::days(1)).and_hms_opt(10, 0, 0).unwrap()));
// 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()
);
}
}

View File

@ -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::kinds::{format_tag_basic, match_event_tag, Prio, BASIC_KINDS, PROPERTY_COLUMNS, PROP_KINDS};
use crate::task::{State, StateChange, Task, MARKER_PROPERTY}; use crate::task::{State, StateChange, Task, MARKER_PROPERTY};
use crate::tasks::{referenced_event, PropertyCollection, StateFilter, TasksRelay}; use crate::tasks::{referenced_event, PropertyCollection, StateFilter, TasksRelay};
use chrono::Local; use chrono::{DateTime, Local, TimeZone, Utc};
use colored::Colorize; use colored::Colorize;
use directories::ProjectDirs; use directories::ProjectDirs;
use env_logger::{Builder, Target, WriteStyle}; use env_logger::{Builder, Target, WriteStyle};
@ -650,7 +650,8 @@ async fn main() -> Result<()> {
match arg { match arg {
None => tasks.move_to(None), None => tasks.move_to(None),
Some(arg) => { 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)); println!("{}", tasks.times_tracked(15));
} }
// So the error message is not covered up // So the error message is not covered up

View File

@ -4,13 +4,6 @@ mod tests;
mod children_traversal; mod children_traversal;
mod durations; 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::event_sender::{EventSender, MostrMessage};
use crate::hashtag::Hashtag; use crate::hashtag::Hashtag;
use crate::helpers::{ 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::children_traversal::ChildrenTraversal;
use crate::tasks::durations::{referenced_events, timestamps, Durations}; use crate::tasks::durations::{referenced_events, timestamps, Durations};
pub use crate::tasks::nostr_users::NostrUsers; pub use crate::tasks::nostr_users::NostrUsers;
use chrono::{Local, TimeDelta};
use colored::Colorize; use colored::Colorize;
use itertools::Itertools; use itertools::Itertools;
use log::{debug, error, info, trace, warn}; use log::{debug, error, info, trace, warn};
@ -30,6 +25,12 @@ use nostr_sdk::{
SingleLetterTag, Tag, TagKind, Timestamp, Url, SingleLetterTag, Tag, TagKind, Timestamp, Url,
}; };
use regex::bytes::Regex; 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; use tokio::sync::mpsc::Sender;
const DEFAULT_PRIO: Prio = 25; const DEFAULT_PRIO: Prio = 25;
@ -240,6 +241,10 @@ impl TasksRelay {
self.get_position_at(now()).1 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 { fn sorting_key(&self, task: &Task) -> impl Ord {
self.sorting self.sorting
.iter() .iter()
@ -277,9 +282,7 @@ impl TasksRelay {
label label
} else { } else {
format!("{}{}", format!("{}{}",
if limit > times.len() || limit == usize::MAX { "All ".to_string() } if limit > times.len() || limit == usize::MAX { "All ".to_string() } else if limit < 20 { "Recent ".to_string() } else { format!("Latest {limit} Entries of ") },
else if limit < 20 { "Recent ".to_string() }
else { format!("Latest {limit} Entries of ") },
label) label)
}.italic(), }.italic(),
&times[times.len().saturating_sub(limit)..].join("\n")) &times[times.len().saturating_sub(limit)..].join("\n"))
@ -1160,7 +1163,7 @@ impl TasksRelay {
/// ///
/// Returns false and prints a message if parsing failed /// Returns false and prints a message if parsing failed
pub(crate) fn track_from(&mut self, str: &str) -> bool { 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())) .and_then(|stamp| self.track_at(stamp, self.get_position()))
.is_some() .is_some()
} }