jj_cli/
time_util.rs

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/// Parsed formatting items which should never contain an error.
12#[derive(Clone, Debug, Eq, PartialEq)]
13pub struct FormattingItems<'a> {
14    items: Vec<chrono::format::Item<'a>>,
15}
16
17impl<'a> FormattingItems<'a> {
18    /// Parses strftime-like format string.
19    pub fn parse(format: &'a str) -> Option<Self> {
20        // If the parsed format contained an error, format().to_string() would panic.
21        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, // shouldn't exist, but just copy
43            })
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}