Includes a simple algorithm for creating human-readable relative timestamps, which definitely has some room for improvement but seems like a good starting point.
BFL2Y7GN6NBXXNAUSD4M6T6CIVQ2OLERPE2CAFSLRF377WFFTVCQC HHJDRLLNN36UNIA7STAXEEVBCEMPJNB7SJQOS3TJLLYN4AEZ4MHQC HCGVXOF7P3KKS2IMGVJWI2POVOZQFPXH26YVBJZRSOYSUM4CHUBQC YNEOCYMGMSHQGCL5TOIGWDDKHE4BZ5M7FGY5I6B2V6JO6ZRCLETAC WBI5HFOBBUMDSGKY2RX3YA6N7YDCJEP23JNEJ7PG5VZXHLYIRJRQC O77KA6C4UJGZXVGPEA7WCRQH6XYQJPWETSPDXI3VOKOSRQND7JEQC ROSR4HD5ENPQU3HH5IVYSOA5YM72W77CHVQARSD3T67BUNYG7KZQC F5LG7WENUUDRSCTDMA4M6BAC5RWTGQO45C4ZEBZDX6FHCTTHBVGQC VNSHGQYNPGKGGPYNVP4Z2RWD7JCSDJVYAADD6UXWBYL6ZRXKLE4AC UKFEFT6LSI4K7X6UHQFZYD52DILKXMZMYSO2UYS2FCHNPXIF4BEQC JZXXFWQKOYAFQLQZDRALXG4KGEDR7JKO3AZ5Q5X7IQTS7BCJP3QAC SHNZZSZGIBTTD4IV5SMW5BIN5DORUWQVTVTNB5RMRD5CTFNOMJ6AC V5S5K33ALIEG5ZABUSAPO4ULHEBFDB2PLTW27A4BFS342SJG7URQC RLX6XPNZKD6GIRLWKYXFH2RNIU4ZNXLMHXLOMID3E6H53QXXXNZQC 56F2YE6HUZ76U4QBPUDJ2VQLJ75TQYNTVQIOX4QBOZ2H6GJKRGUQC use fixed_decimal::{FixedDecimal, FloatPrecision};use icu_experimental::relativetime::{options::Numeric, RelativeTimeFormatter, RelativeTimeFormatterOptions,};use icu_locid::{langid, LanguageIdentifier};use icu_provider::DataLocale;use jiff::{tz::TimeZone, SpanRound, Timestamp, Unit};/// Allow the formatter to use non-numeric output (e.g. "tomorrow", "yesterday")const FORMATTER_OPTIONS: RelativeTimeFormatterOptions = RelativeTimeFormatterOptions {numeric: Numeric::Auto,};/// The locale to fall back topub const DEFAULT_LOCALE: LanguageIdentifier = langid!("en-US");/// A time relative to the system clock (either past or future)pub struct RelativeTime(Timestamp);impl RelativeTime {pub fn new(timestamp: Timestamp) -> Self {Self(timestamp)}}impl crate::Localize for RelativeTime {const CANONICAL_LOCALE: LanguageIdentifier = DEFAULT_LOCALE;fn localize(&self) -> String {// Get the current timelet current_timestamp = Timestamp::now();let current_datetime = current_timestamp.to_zoned(TimeZone::UTC).datetime();// Calculate the difference, rounded to the largest unitlet unformatted_span = current_timestamp.since(self.0).unwrap()// Make sure the span is rounded to the largest available unit.round(SpanRound::new().largest(Unit::Year).relative(current_datetime),).unwrap();// Find the largest "component": year, month, week etclet units: [(Unit, i64); 7] = [(Unit::Year, unformatted_span.get_years() as i64),(Unit::Month, unformatted_span.get_months() as i64),(Unit::Week, unformatted_span.get_weeks() as i64),(Unit::Day, unformatted_span.get_days() as i64),(Unit::Hour, unformatted_span.get_hours() as i64),(Unit::Minute, unformatted_span.get_minutes()),(Unit::Second, unformatted_span.get_seconds()),];// Use the largest non-zero unitlet selected_unit = units.iter().find(|(_unit, value)| value.abs() > 0).map(|(unit, _value)| *unit).unwrap_or(Unit::Second);// Round the span to that selected unitlet rounding_options = SpanRound::new().smallest(selected_unit).largest(selected_unit).relative(current_datetime);let formatted_span = current_timestamp.since(self.0).unwrap().round(rounding_options).unwrap();// We can finally get the actual rounded valuelet selected_value = match selected_unit {Unit::Year => formatted_span.get_years() as i64,Unit::Month => formatted_span.get_months() as i64,Unit::Week => formatted_span.get_weeks() as i64,Unit::Day => formatted_span.get_days() as i64,Unit::Hour => formatted_span.get_hours() as i64,Unit::Minute => formatted_span.get_minutes(),Unit::Second => formatted_span.get_seconds(),_ => unreachable!(),};// Select which formatter to uselet locale = DataLocale::from(&Self::CANONICAL_LOCALE);let formatter = match selected_unit {Unit::Year => RelativeTimeFormatter::try_new_long_year(&locale, FORMATTER_OPTIONS),Unit::Month => RelativeTimeFormatter::try_new_long_month(&locale, FORMATTER_OPTIONS),Unit::Week => RelativeTimeFormatter::try_new_long_week(&locale, FORMATTER_OPTIONS),Unit::Day => RelativeTimeFormatter::try_new_long_day(&locale, FORMATTER_OPTIONS),Unit::Hour => RelativeTimeFormatter::try_new_long_hour(&locale, FORMATTER_OPTIONS),Unit::Minute => RelativeTimeFormatter::try_new_long_minute(&locale, FORMATTER_OPTIONS),Unit::Second => RelativeTimeFormatter::try_new_long_second(&locale, FORMATTER_OPTIONS),_ => unreachable!(),}.unwrap();let decimal =FixedDecimal::try_from_f64(selected_value as f64, FloatPrecision::Integer).unwrap();formatter.format(decimal).to_string()}}
icu_locid = "1.4.0"icu_plurals = "1.4.0"icu_provider = "1.4.0"
fixed_decimal = { version = "0.5.6", features = ["ryu"] }icu_experimental = "0.1.0"icu_locid = "1.5.0"icu_plurals = "1.5.0"icu_provider = "1.5.0"jiff = { version = "0.1.2", default-features = false, features = ["std"] }
][[package]]name = "icu_collections"version = "1.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"dependencies = ["displaydoc","yoke","zerofrom","zerovec",][[package]]name = "icu_decimal"version = "1.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "fb8fd98f86ec0448d85e1edf8884e4e318bb2e121bd733ec929a05c0a5e8b0eb"dependencies = ["displaydoc","fixed_decimal","icu_decimal_data","icu_locid_transform","icu_provider","writeable",
name = "icu_decimal_data"version = "1.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "8d424c994071c6f5644f999925fc868c85fec82295326e75ad5017bc94b41523"[[package]]name = "icu_experimental"version = "0.1.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "844ad7b682a165c758065d694bc4d74ac67f176da1c499a04d85d492c0f193b7"dependencies = ["displaydoc","fixed_decimal","icu_collections","icu_decimal","icu_experimental_data","icu_locid","icu_locid_transform","icu_normalizer","icu_pattern","icu_plurals","icu_properties","icu_provider","litemap","num-bigint","num-rational","num-traits","smallvec","tinystr","writeable","zerofrom","zerotrie","zerovec",][[package]]name = "icu_experimental_data"version = "0.1.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9c178b9a34083fca5bd70d61f647575335e9c197d0f30c38e8ccd187babc69d0"[[package]]
version = "1.4.0"
version = "1.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"[[package]]name = "icu_normalizer"version = "1.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"dependencies = ["displaydoc","icu_collections","icu_normalizer_data","icu_properties","icu_provider","smallvec","utf16_iter","utf8_iter","write16","zerovec",][[package]]name = "icu_normalizer_data"version = "1.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"[[package]]name = "icu_pattern"version = "0.2.0"
checksum = "c3acd5f1f2f988ed2dae9316c3d3560dfe4e03a7516d142b4b89b92252ada41a"
checksum = "9e3e8f775b215d45838814a090a2227247a7431d74e9156407d9c37f6ef0f208"[[package]]name = "icu_properties"version = "1.5.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"dependencies = ["displaydoc","icu_collections","icu_locid_transform","icu_properties_data","icu_provider","tinystr","zerovec",][[package]]name = "icu_properties_data"version = "1.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
][[package]]name = "num-bigint"version = "0.4.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"dependencies = ["num-integer","num-traits",][[package]]name = "num-integer"version = "0.1.46"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"dependencies = ["num-traits",][[package]]name = "num-rational"version = "0.4.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"dependencies = ["num-bigint","num-integer","num-traits",][[package]]name = "num-traits"version = "0.2.19"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"dependencies = ["autocfg",
[[package]]name = "utf16_iter"version = "1.0.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"[[package]]name = "utf8_iter"version = "1.0.4"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"