1use chrono::format::StrftimeItems;
2use chrono::DateTime;
3use chrono::FixedOffset;
4use chrono::LocalResult;
5use chrono::TimeZone as _;
6use chrono::Utc;
7use jj_lib::backend::Timestamp;
8use once_cell::sync::Lazy;
9use thiserror::Error;
10
11#[derive(Clone, Debug, Eq, PartialEq)]
13pub struct FormattingItems<'a> {
14 items: Vec<chrono::format::Item<'a>>,
15}
16
17impl<'a> FormattingItems<'a> {
18 pub fn parse(format: &'a str) -> Option<Self> {
20 let items = StrftimeItems::new(format)
22 .map(|item| match item {
23 chrono::format::Item::Error => None,
24 _ => Some(item),
25 })
26 .collect::<Option<_>>()?;
27 Some(FormattingItems { items })
28 }
29
30 pub fn into_owned(self) -> FormattingItems<'static> {
31 use chrono::format::Item;
32 let items = self
33 .items
34 .into_iter()
35 .map(|item| match item {
36 Item::Literal(s) => Item::OwnedLiteral(s.into()),
37 Item::OwnedLiteral(s) => Item::OwnedLiteral(s),
38 Item::Space(s) => Item::OwnedSpace(s.into()),
39 Item::OwnedSpace(s) => Item::OwnedSpace(s),
40 Item::Numeric(spec, pad) => Item::Numeric(spec, pad),
41 Item::Fixed(spec) => Item::Fixed(spec),
42 Item::Error => Item::Error, })
44 .collect();
45 FormattingItems { items }
46 }
47}
48
49#[derive(Debug, Error)]
50#[error("Out-of-range date")]
51pub struct TimestampOutOfRange;
52
53fn datetime_from_timestamp(
54 context: &Timestamp,
55) -> Result<DateTime<FixedOffset>, TimestampOutOfRange> {
56 let utc = match Utc.timestamp_opt(
57 context.timestamp.0.div_euclid(1000),
58 (context.timestamp.0.rem_euclid(1000)) as u32 * 1000000,
59 ) {
60 LocalResult::None => {
61 return Err(TimestampOutOfRange);
62 }
63 LocalResult::Single(x) => x,
64 LocalResult::Ambiguous(y, _z) => y,
65 };
66
67 Ok(utc.with_timezone(
68 &FixedOffset::east_opt(context.tz_offset * 60)
69 .unwrap_or_else(|| FixedOffset::east_opt(0).unwrap()),
70 ))
71}
72
73pub fn format_absolute_timestamp(timestamp: &Timestamp) -> Result<String, TimestampOutOfRange> {
74 static DEFAULT_FORMAT: Lazy<FormattingItems> =
75 Lazy::new(|| FormattingItems::parse("%Y-%m-%d %H:%M:%S.%3f %:z").unwrap());
76 format_absolute_timestamp_with(timestamp, &DEFAULT_FORMAT)
77}
78
79pub fn format_absolute_timestamp_with(
80 timestamp: &Timestamp,
81 format: &FormattingItems,
82) -> Result<String, TimestampOutOfRange> {
83 let datetime = datetime_from_timestamp(timestamp)?;
84 Ok(datetime.format_with_items(format.items.iter()).to_string())
85}
86
87pub fn format_duration(
88 from: &Timestamp,
89 to: &Timestamp,
90 format: &timeago::Formatter,
91) -> Result<String, TimestampOutOfRange> {
92 let duration = datetime_from_timestamp(to)?
93 .signed_duration_since(datetime_from_timestamp(from)?)
94 .to_std()
95 .map_err(|_: chrono::OutOfRangeError| TimestampOutOfRange)?;
96 Ok(format.convert(duration))
97}