feat: revamp timestamp formatting helpers

This commit is contained in:
xeruf 2024-08-21 11:56:15 +03:00
parent 3dca6a4b23
commit ed1f482707
4 changed files with 63 additions and 49 deletions

View File

@ -57,41 +57,60 @@ pub fn parse_tracking_stamp(str: &str) -> Option<Timestamp> {
}) })
} }
// For use in format strings but not possible, so need global find-replace /// Format DateTime easily comprehensible for human but unambiguous.
pub const MAX_TIMESTAMP_WIDTH: u8 = 15; /// Length may vary.
/// Format nostr Timestamp relative to local time pub fn format_datetime_relative(time: DateTime<Local>) -> String {
/// with optional day specifier or full date depending on distance to today let date = time.date_naive();
pub fn relative_datetimestamp(stamp: &Timestamp) -> String { let prefix =
match Local::now()
.date_naive()
.signed_duration_since(date)
.num_days() {
-1 => "tomorrow ".into(),
0 => "".into(),
1 => "yesterday ".into(),
-3..=3 => date.format("%a ").to_string(),
//-10..=10 => date.format("%d. %a ").to_string(),
-100..=100 => date.format("%b %d ").to_string(),
_ => date.format("%y-%m-%d ").to_string(),
};
format!("{}{}", prefix, time.format("%H:%M"))
}
/// Format a nostr timestamp with the given formatting function.
pub fn format_as_datetime<F>(stamp: &Timestamp, formatter: F) -> String
where
F: Fn(DateTime<Local>) -> String,
{
match Local.timestamp_opt(stamp.as_u64() as i64, 0) { match Local.timestamp_opt(stamp.as_u64() as i64, 0) {
Single(time) => { Single(time) => formatter(time),
let date = time.date_naive();
let prefix = match Local::now()
.date_naive()
.signed_duration_since(date)
.num_days()
{
-1 => "tomorrow ".into(),
0 => "".into(),
1 => "yesterday ".into(),
2..=6 => date.format("last %a ").to_string(),
_ => date.format("%y-%m-%d ").to_string(),
};
format!("{}{}", prefix, time.format("%H:%M"))
}
_ => stamp.to_human_datetime(), _ => stamp.to_human_datetime(),
} }
} }
/// Format a nostr timestamp in a sensible comprehensive format /// Format nostr Timestamp relative to local time
pub fn local_datetimestamp(stamp: &Timestamp) -> String { /// with optional day specifier or full date depending on distance to today.
format_stamp(stamp, "%y-%m-%d %a %H:%M") pub fn format_timestamp_relative(stamp: &Timestamp) -> String {
format_as_datetime(stamp, format_datetime_relative)
} }
/// Format a nostr timestamp with the given format /// Format nostr timestamp with the given format.
pub fn format_stamp(stamp: &Timestamp, format: &str) -> String { pub fn format_timestamp(stamp: &Timestamp, format: &str) -> String {
match Local.timestamp_opt(stamp.as_u64() as i64, 0) { format_as_datetime(stamp, |time| time.format(format).to_string())
Single(time) => time.format(format).to_string(), }
_ => stamp.to_human_datetime(),
/// Format nostr timestamp in a sensible comprehensive format with consistent length and consistent sorting.
///
/// Currently: 18 characters
pub fn format_timestamp_local(stamp: &Timestamp) -> String {
format_timestamp(stamp, "%y-%m-%d %a %H:%M")
}
pub fn format_timestamp_relative_to(stamp: &Timestamp, reference: &Timestamp) -> String {
// Rough difference in days
match (stamp.as_u64() as i64 - reference.as_u64() as i64) / 80_000 {
0 => format_timestamp(stamp, "%H:%M"),
-3..=3 => format_timestamp(stamp, "%a %H:%M"),
_ => format_timestamp_local(stamp),
} }
} }

View File

@ -392,7 +392,7 @@ async fn main() -> Result<()> {
None => { None => {
tasks.get_current_task().map_or_else( tasks.get_current_task().map_or_else(
|| info!("With a task selected, use ,NOTE to attach NOTE and , to list all its notes"), || info!("With a task selected, use ,NOTE to attach NOTE and , to list all its notes"),
|task| println!("{}", task.description_events().map(|e| format!("{} {}", local_datetimestamp(&e.created_at), e.content)).join("\n")), |task| println!("{}", task.description_events().map(|e| format!("{} {}", format_timestamp_local(&e.created_at), e.content)).join("\n")),
); );
continue; continue;
} }
@ -448,10 +448,11 @@ async fn main() -> Result<()> {
parse_hour(arg, 1) parse_hour(arg, 1)
.or_else(|| parse_date(arg).map(|utc| utc.with_timezone(&Local))) .or_else(|| parse_date(arg).map(|utc| utc.with_timezone(&Local)))
.map(|time| { .map(|time| {
info!("Filtering for tasks created after {}", time); info!("Filtering for tasks created after {}", format_datetime_relative(time));
let threshold = time.to_utc().timestamp();
tasks.set_filter( tasks.set_filter(
tasks.filtered_tasks(tasks.get_position_ref()) tasks.filtered_tasks(tasks.get_position_ref())
.filter(|t| t.event.created_at.as_u64() as i64 > time.to_utc().timestamp()) .filter(|t| t.event.created_at.as_u64() as i64 > threshold)
.map(|t| t.event.id) .map(|t| t.event.id)
.collect() .collect()
); );

View File

@ -10,7 +10,7 @@ use itertools::Itertools;
use log::{debug, error, info, trace, warn}; use log::{debug, error, info, trace, warn};
use nostr_sdk::{Event, EventId, Kind, Tag, TagStandard, Timestamp}; use nostr_sdk::{Event, EventId, Kind, Tag, TagStandard, Timestamp};
use crate::helpers::{local_datetimestamp, some_non_empty}; use crate::helpers::{format_timestamp_local, some_non_empty};
use crate::kinds::{is_hashtag, TASK_KIND}; use crate::kinds::{is_hashtag, TASK_KIND};
pub static MARKER_PARENT: &str = "parent"; pub static MARKER_PARENT: &str = "parent";
@ -156,7 +156,7 @@ impl Task {
"parentid" => self.parent_id().map(|i| i.to_string()), "parentid" => self.parent_id().map(|i| i.to_string()),
"name" => Some(self.event.content.clone()), "name" => Some(self.event.content.clone()),
"pubkey" => Some(self.event.pubkey.to_string()), "pubkey" => Some(self.event.pubkey.to_string()),
"created" => Some(local_datetimestamp(&self.event.created_at)), "created" => Some(format_timestamp_local(&self.event.created_at)),
"kind" => Some(self.event.kind.to_string()), "kind" => Some(self.event.kind.to_string()),
// Dynamic // Dynamic
"status" => self.state_label().map(|c| c.to_string()), "status" => self.state_label().map(|c| c.to_string()),

View File

@ -6,7 +6,6 @@ use std::ops::{Div, Rem};
use std::str::FromStr; use std::str::FromStr;
use std::time::Duration; use std::time::Duration;
use chrono::Local;
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};
@ -15,7 +14,7 @@ use nostr_sdk::prelude::Marker;
use TagStandard::Hashtag; use TagStandard::Hashtag;
use crate::{EventSender, MostrMessage}; use crate::{EventSender, MostrMessage};
use crate::helpers::{format_stamp, local_datetimestamp, parse_tracking_stamp, relative_datetimestamp, some_non_empty}; use crate::helpers::{format_timestamp_local, format_timestamp_relative, format_timestamp_relative_to, parse_tracking_stamp, some_non_empty};
use crate::kinds::*; use crate::kinds::*;
use crate::task::{MARKER_DEPENDS, MARKER_PARENT, State, Task, TaskState}; use crate::task::{MARKER_DEPENDS, MARKER_PARENT, State, Task, TaskState};
@ -189,7 +188,7 @@ impl Tasks {
.join(" ")); .join(" "));
if new != last { if new != last {
// TODO alternate color with grey between days // TODO alternate color with grey between days
full.push(format!("{:>15} {}", relative_datetimestamp(&event.created_at), new.as_ref().unwrap_or(&"---".to_string()))); full.push(format!("{} {}", format_timestamp_local(&event.created_at), new.as_ref().unwrap_or(&"---".to_string())));
last = new; last = new;
} }
} }
@ -206,18 +205,13 @@ impl Tasks {
let mut iter = timestamps(set.iter(), &ids).tuples(); let mut iter = timestamps(set.iter(), &ids).tuples();
while let Some(((start, _), (end, _))) = iter.next() { while let Some(((start, _), (end, _))) = iter.next() {
vec.push(format!("{} - {} by {}", vec.push(format!("{} - {} by {}",
local_datetimestamp(start), format_timestamp_local(start),
// Only use full stamp when ambiguous (>1day) format_timestamp_relative_to(end, start),
if end.as_u64() - start.as_u64() > 80_000 {
local_datetimestamp(end)
} else {
format_stamp(end, "%H:%M")
},
self.get_author(key))) self.get_author(key)))
} }
iter.into_buffer() iter.into_buffer()
.for_each(|(stamp, _)| .for_each(|(stamp, _)|
vec.push(format!("{} started by {}", local_datetimestamp(stamp), self.get_author(key)))); vec.push(format!("{} started by {}", format_timestamp_local(stamp), self.get_author(key))));
vec vec
}).sorted_unstable(); // TODO sorting depends on timestamp format - needed to interleave different people }).sorted_unstable(); // TODO sorting depends on timestamp format - needed to interleave different people
(format!("Times Tracked on {:?}", self.get_task_title(&id)), Box::from(history)) (format!("Times Tracked on {:?}", self.get_task_title(&id)), Box::from(history))
@ -409,10 +403,10 @@ impl Tasks {
writeln!( writeln!(
lock, lock,
"Tracking since {} (total tracked time {}m) - {} since {}", "Tracking since {} (total tracked time {}m) - {} since {}",
tracking_stamp.map_or("?".to_string(), |t| relative_datetimestamp(&t)), tracking_stamp.map_or("?".to_string(), |t| format_timestamp_relative(&t)),
self.time_tracked(*t.get_id()) / 60, self.time_tracked(*t.get_id()) / 60,
state.get_label(), state.get_label(),
relative_datetimestamp(&state.time) format_timestamp_relative(&state.time)
)?; )?;
writeln!(lock, "{}", t.descriptions().join("\n"))?; writeln!(lock, "{}", t.descriptions().join("\n"))?;
} }
@ -736,7 +730,7 @@ impl Tasks {
} }
pub(crate) fn track_at(&mut self, time: Timestamp, task: Option<EventId>) -> EventId { pub(crate) fn track_at(&mut self, time: Timestamp, task: Option<EventId>) -> EventId {
info!("{} from {}", task.map_or(String::from("Stopping time-tracking"), |id| format!("Tracking \"{}\"", self.get_task_title(&id))), relative_datetimestamp(&time)); info!("{} from {}", task.map_or(String::from("Stopping time-tracking"), |id| format!("Tracking \"{}\"", self.get_task_title(&id))), format_timestamp_relative(&time));
self.submit( self.submit(
build_tracking(task) build_tracking(task)
.custom_created_at(time) .custom_created_at(time)