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 to
pub 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 time
let current_timestamp = Timestamp::now();
let current_datetime = current_timestamp.to_zoned(TimeZone::UTC).datetime();
// Calculate the difference, rounded to the largest unit
let 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 etc
let 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 unit
let selected_unit = units
.iter()
.find(|(_unit, value)| value.abs() > 0)
.map(|(unit, _value)| *unit)
.unwrap_or(Unit::Second);
// Round the span to that selected unit
let 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 value
let 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 use
let 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"