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
pub const MAX_TIMESTAMP_WIDTH: u8 = 15;
/// Format nostr Timestamp relative to local time
/// with optional day specifier or full date depending on distance to today
pub fn relative_datetimestamp(stamp: &Timestamp) -> String {
/// Format DateTime easily comprehensible for human but unambiguous.
/// Length may vary.
pub fn format_datetime_relative(time: DateTime<Local>) -> String {
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(),
-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) {
Single(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"))
}
Single(time) => formatter(time),
_ => stamp.to_human_datetime(),
}
}
/// Format a nostr timestamp in a sensible comprehensive format
pub fn local_datetimestamp(stamp: &Timestamp) -> String {
format_stamp(stamp, "%y-%m-%d %a %H:%M")
/// Format nostr Timestamp relative to local time
/// with optional day specifier or full date depending on distance to today.
pub fn format_timestamp_relative(stamp: &Timestamp) -> String {
format_as_datetime(stamp, format_datetime_relative)
}
/// Format a nostr timestamp with the given format
pub fn format_stamp(stamp: &Timestamp, format: &str) -> String {
match Local.timestamp_opt(stamp.as_u64() as i64, 0) {
Single(time) => time.format(format).to_string(),
_ => stamp.to_human_datetime(),
/// Format nostr timestamp with the given format.
pub fn format_timestamp(stamp: &Timestamp, format: &str) -> String {
format_as_datetime(stamp, |time| time.format(format).to_string())
}
/// 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 => {
tasks.get_current_task().map_or_else(
|| 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;
}
@ -448,10 +448,11 @@ async fn main() -> Result<()> {
parse_hour(arg, 1)
.or_else(|| parse_date(arg).map(|utc| utc.with_timezone(&Local)))
.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.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)
.collect()
);

View File

@ -10,7 +10,7 @@ use itertools::Itertools;
use log::{debug, error, info, trace, warn};
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};
pub static MARKER_PARENT: &str = "parent";
@ -156,7 +156,7 @@ impl Task {
"parentid" => self.parent_id().map(|i| i.to_string()),
"name" => Some(self.event.content.clone()),
"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()),
// Dynamic
"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::time::Duration;
use chrono::Local;
use colored::Colorize;
use itertools::Itertools;
use log::{debug, error, info, trace, warn};
@ -15,7 +14,7 @@ use nostr_sdk::prelude::Marker;
use TagStandard::Hashtag;
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::task::{MARKER_DEPENDS, MARKER_PARENT, State, Task, TaskState};
@ -189,7 +188,7 @@ impl Tasks {
.join(" "));
if new != last {
// 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;
}
}
@ -206,18 +205,13 @@ impl Tasks {
let mut iter = timestamps(set.iter(), &ids).tuples();
while let Some(((start, _), (end, _))) = iter.next() {
vec.push(format!("{} - {} by {}",
local_datetimestamp(start),
// Only use full stamp when ambiguous (>1day)
if end.as_u64() - start.as_u64() > 80_000 {
local_datetimestamp(end)
} else {
format_stamp(end, "%H:%M")
},
format_timestamp_local(start),
format_timestamp_relative_to(end, start),
self.get_author(key)))
}
iter.into_buffer()
.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
}).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))
@ -409,10 +403,10 @@ impl Tasks {
writeln!(
lock,
"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,
state.get_label(),
relative_datetimestamp(&state.time)
format_timestamp_relative(&state.time)
)?;
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 {
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(
build_tracking(task)
.custom_created_at(time)