feat(time): improve timezone handling by switching to jiff (#7222)

This commit is contained in:
David Knaack
2026-06-06 18:44:15 +02:00
committed by GitHub
parent 033f20b461
commit 3dd8c14144
6 changed files with 314 additions and 216 deletions
Generated
+1 -44
View File
@@ -393,19 +393,6 @@ dependencies = [
"rand_core 0.10.1",
]
[[package]]
name = "chrono"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link 0.2.1",
]
[[package]]
name = "clap"
version = "4.6.1"
@@ -538,12 +525,6 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.17"
@@ -1892,30 +1873,6 @@ dependencies = [
"typenum",
]
[[package]]
name = "iana-time-zone"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core 0.62.2",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "id-arena"
version = "2.3.0"
@@ -3380,7 +3337,6 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
name = "starship"
version = "1.25.1"
dependencies = [
"chrono",
"clap",
"clap_complete",
"clap_complete_nushell",
@@ -3391,6 +3347,7 @@ dependencies = [
"guess_host_triple",
"home",
"indexmap",
"jiff",
"jsonc-parser",
"log",
"mockall",
+1 -1
View File
@@ -35,7 +35,6 @@ config-schema = ["schemars"]
notify = ["notify-rust"]
[dependencies]
chrono = { version = "0.4.44", default-features = false, features = ["clock", "std", "wasmbind"] }
clap = { version = "4.6.1", features = ["derive", "cargo", "unicode"] }
clap_complete = "4.6.5"
clap_complete_nushell = "4.6.0"
@@ -44,6 +43,7 @@ dunce = "1.0.5"
# default feature restriction addresses https://github.com/starship/starship/issues/4251
gix = { version = "0.84.0", default-features = false, features = ["max-performance-safe", "revision", "zlib-rs", "status", "sha1"] }
indexmap = { version = "2.14.0", features = ["serde"] }
jiff = { version = "0.2.24", features = ["serde"] }
jsonc-parser = { version = "0.32.4", features = ["serde"] }
log = { version = "0.4.30", features = ["std"] }
# notify-rust is optional (on by default) because the crate doesn't currently build for darwin with nix
+17 -4
View File
@@ -4814,7 +4814,7 @@ format = 'via [$symbol$workspace]($style) '
## Time
The `time` module shows the current **local** time.
The `format` configuration value is used by the [`chrono`](https://crates.io/crates/chrono) crate to control how the time is displayed. Take a look [at the chrono strftime docs](https://docs.rs/chrono/0.4.7/chrono/format/strftime/index.html) to see what options are available.
The `format` configuration value is used by the [`jiff`](https://crates.io/crates/jiff) crate to control how the time is displayed. Take a look [at the jiff strftime docs](https://docs.rs/jiff/latest/jiff/fmt/strtime/index.html) to see what options are available.
> [!TIP]
> This module is disabled by default.
@@ -4823,12 +4823,12 @@ The `format` configuration value is used by the [`chrono`](https://crates.io/cra
### Options
| Option | Default | Description |
| ----------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| ----------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `format` | `'at [$time]($style) '` | The format string for the module. |
| `use_12hr` | `false` | Enables 12 hour formatting |
| `time_format` | see below | The [chrono format string](https://docs.rs/chrono/0.4.7/chrono/format/strftime/index.html) used to format the time. |
| `time_format` | see below | The [jiff format string](https://docs.rs/jiff/latest/jiff/fmt/strtime/index.html) used to format the time. |
| `style` | `'bold yellow'` | The style for the module time |
| `utc_time_offset` | `'local'` | Sets the UTC offset to use. Range from -24 < x < 24. Allows floats to accommodate 30/45 minute timezone offsets. |
| `utc_time_offset` | `'local'` | Sets the UTC offset to use. Either an IANA time zone name or a range from -24 < x < 24. Allows floats to accommodate 30/45 minute timezone offsets. |
| `disabled` | `true` | Disables the `time` module. |
| `time_range` | `'-'` | Sets the time range during which the module will be shown. Times must be specified in 24-hours format |
@@ -4846,6 +4846,8 @@ Manually setting `time_format` will override the `use_12hr` setting.
### Example
#### With UTC offset
```toml
# ~/.config/starship.toml
@@ -4857,6 +4859,17 @@ utc_time_offset = '-5'
time_range = '10:00:00-14:00:00'
```
#### With Timezone name
```toml
# ~/.config/starship.toml
[time]
disabled = false
time_format = '%T'
utc_time_offset = 'Europe/Berlin'
```
## Typst
The `typst` module shows the current installed version of Typst used in a project.
+11 -2
View File
@@ -1,5 +1,13 @@
use crate::config::Either;
use serde::{Deserialize, Serialize};
// Wrapper struct to enable serde serialization/deserialization for jiff::tz::TimeZone
#[derive(Clone, Deserialize, Serialize)]
#[serde(transparent)]
pub struct TimezoneWrapper(
#[serde(with = "jiff::fmt::serde::tz::required")] pub jiff::tz::TimeZone,
);
#[derive(Clone, Deserialize, Serialize)]
#[cfg_attr(
feature = "config-schema",
@@ -14,7 +22,8 @@ pub struct TimeConfig<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
pub time_format: Option<&'a str>,
pub disabled: bool,
pub utc_time_offset: &'a str,
#[cfg_attr(feature = "config-schema", schemars(with = "String"))]
pub utc_time_offset: Either<TimezoneWrapper, &'a str>,
pub time_range: &'a str,
}
@@ -26,7 +35,7 @@ impl Default for TimeConfig<'_> {
use_12hr: false,
time_format: None,
disabled: true,
utc_time_offset: "local",
utc_time_offset: Either::Second("local"),
time_range: "-",
}
}
+20 -48
View File
@@ -3,8 +3,8 @@ use std::collections::HashMap;
use std::path::PathBuf;
use std::str::FromStr;
use chrono::DateTime;
use ini::Ini;
use jiff::{Timestamp, Zoned};
use serde_json as json;
use sha1::{Digest, Sha1};
@@ -149,7 +149,7 @@ fn get_credentials_duration(
.find_map(|env_var| context.get_env(env_var))
{
// get expiration from environment variables
chrono::DateTime::parse_from_rfc3339(&expiration_date).ok()
expiration_date.parse::<Timestamp>().ok()
} else if let Some(section) =
get_creds(context, aws_creds).and_then(|creds| get_profile_creds(creds, aws_profile))
{
@@ -158,7 +158,7 @@ fn get_credentials_duration(
expiration_keys
.iter()
.find_map(|expiration_key| section.get(expiration_key))
.and_then(|expiration| DateTime::parse_from_rfc3339(expiration).ok())
.and_then(|expiration| expiration.parse::<Timestamp>().ok())
} else {
// get expiration from cached SSO credentials
let config = get_config(context, aws_config)?;
@@ -172,10 +172,10 @@ fn get_credentials_duration(
let sso_cred_json: json::Value =
json::from_str(&crate::utils::read_file(&sso_cred_path).ok()?).ok()?;
let expires_at = sso_cred_json.get("expiresAt")?.as_str();
DateTime::parse_from_rfc3339(expires_at?).ok()
expires_at?.parse::<Timestamp>().ok()
}?;
Some(expiration_date.timestamp() - chrono::Local::now().timestamp())
Some(expiration_date.as_second() - Zoned::now().timestamp().as_second())
}
fn alias_name(name: Option<String>, aliases: &HashMap<String, &str>) -> Option<String> {
@@ -332,6 +332,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
#[cfg(test)]
mod tests {
use super::*;
use crate::test::ModuleRenderer;
use nu_ansi_term::Color;
use std::fs::{File, create_dir};
@@ -736,21 +737,16 @@ credential_process = /opt/bin/awscreds-retriever
#[test]
fn expiration_date_set() {
use chrono::{DateTime, SecondsFormat, Utc};
let expiration_env_vars = ["AWS_SESSION_EXPIRATION", "AWS_CREDENTIAL_EXPIRATION"];
for env_var in expiration_env_vars {
let now_plus_half_hour: DateTime<Utc> =
DateTime::from_timestamp(chrono::Local::now().timestamp() + 1800, 0).unwrap();
let now_plus_half_hour =
Timestamp::from_second(Zoned::now().timestamp().as_second() + 1800).unwrap();
let actual = ModuleRenderer::new("aws")
.env("AWS_PROFILE", "astronauts")
.env("AWS_REGION", "ap-northeast-2")
.env("AWS_ACCESS_KEY_ID", "dummy")
.env(
env_var,
now_plus_half_hour.to_rfc3339_opts(SecondsFormat::Secs, true),
)
.env(env_var, now_plus_half_hour.to_string())
.collect();
let possible_values = [
@@ -772,16 +768,14 @@ credential_process = /opt/bin/awscreds-retriever
#[test]
fn expiration_date_set_from_file() -> io::Result<()> {
use chrono::{DateTime, Utc};
let dir = tempfile::tempdir()?;
let credentials_path = dir.path().join("credentials");
let mut file = File::create(&credentials_path)?;
let now_plus_half_hour: DateTime<Utc> =
DateTime::from_timestamp(chrono::Local::now().timestamp() + 1800, 0).unwrap();
let now_plus_half_hour =
Timestamp::from_second(Zoned::now().timestamp().as_second() + 1800).unwrap();
let expiration_date = now_plus_half_hour.to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
let expiration_date = now_plus_half_hour.to_string();
let expiration_keys = ["expiration", "x_security_token_expires"];
for key in expiration_keys {
@@ -847,10 +841,7 @@ aws_secret_access_key=dummy
#[test]
fn expiration_date_set_expired() {
use chrono::{DateTime, SecondsFormat, Utc};
let now: DateTime<Utc> =
DateTime::from_timestamp(chrono::Local::now().timestamp() - 1800, 0).unwrap();
let now = Timestamp::from_second(Zoned::now().timestamp().as_second() - 1800).unwrap();
let symbol = "!!!";
@@ -862,10 +853,7 @@ aws_secret_access_key=dummy
.env("AWS_PROFILE", "astronauts")
.env("AWS_REGION", "ap-northeast-2")
.env("AWS_ACCESS_KEY_ID", "dummy")
.env(
"AWS_SESSION_EXPIRATION",
now.to_rfc3339_opts(SecondsFormat::Secs, true),
)
.env("AWS_SESSION_EXPIRATION", now.to_string())
.collect();
let expected = Some(format!(
"on {}",
@@ -1056,8 +1044,6 @@ credential_process = /opt/bin/awscreds-for-tests
#[test]
fn sso_legacy_set() -> io::Result<()> {
use chrono::{DateTime, SecondsFormat, Utc};
let (module_renderer, dir) = ModuleRenderer::new_with_home("aws")?;
std::fs::create_dir_all(dir.path().join(".aws/sso/cache"))?;
@@ -1080,16 +1066,10 @@ sso_role_name = <AWS-ROLE-NAME>
.join(".aws/sso/cache/a47a4e57aecc96b31b4f083543924bd6f828e65a.json"),
)?;
let one_second_ago: DateTime<Utc> =
DateTime::from_timestamp(chrono::Local::now().timestamp() - 1, 0).unwrap();
let one_second_ago =
Timestamp::from_second(Zoned::now().timestamp().as_second() - 1).unwrap();
file.write_all(
format!(
r#"{{"expiresAt": "{}"}}"#,
one_second_ago.to_rfc3339_opts(SecondsFormat::Secs, true)
)
.as_bytes(),
)?;
file.write_all(format!(r#"{{"expiresAt": "{one_second_ago}"}}"#).as_bytes())?;
file.sync_all()?;
let actual = module_renderer.collect();
@@ -1104,8 +1084,6 @@ sso_role_name = <AWS-ROLE-NAME>
#[test]
fn sso_set() -> io::Result<()> {
use chrono::{DateTime, SecondsFormat, Utc};
let (module_renderer, dir) = ModuleRenderer::new_with_home("aws")?;
std::fs::create_dir_all(dir.path().join(".aws/sso/cache"))?;
@@ -1134,16 +1112,10 @@ sso_registration_scopes = sso:account:access
.join(".aws/sso/cache/7505d64a54e061b7acd54ccd58b49dc43500b635.json"),
)?;
let one_second_ago: DateTime<Utc> =
DateTime::from_timestamp(chrono::Local::now().timestamp() - 1, 0).unwrap();
let one_second_ago =
Timestamp::from_second(Zoned::now().timestamp().as_second() - 1).unwrap();
cache_file.write_all(
format!(
r#"{{"expiresAt": "{}"}}"#,
one_second_ago.to_rfc3339_opts(SecondsFormat::Secs, true)
)
.as_bytes(),
)?;
cache_file.write_all(format!(r#"{{"expiresAt": "{one_second_ago}"}}"#).as_bytes())?;
cache_file.sync_all()?;
let actual = module_renderer
+255 -108
View File
@@ -1,7 +1,12 @@
use chrono::{DateTime, FixedOffset, Local, NaiveTime, Utc};
use jiff::{
Timestamp, Zoned,
civil::Time,
tz::{Offset, TimeZone},
};
use super::{Context, Module, ModuleConfig};
use crate::configs::time::TimeConfig;
use crate::config::Either;
use crate::configs::time::{TimeConfig, TimezoneWrapper};
use crate::formatter::StringFormatter;
/// Outputs the current time
@@ -17,7 +22,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
// Hide prompt if current time is not inside time_range
let (display_start, display_end) = parse_time_range(config.time_range);
let time_now = Local::now().time();
let time_now = Zoned::now().time();
if !is_inside_time_range(time_now, display_start, display_end) {
return None;
}
@@ -27,17 +32,26 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
log::trace!("Timer module is enabled with format string: {time_format}");
let formatted_time_string = if config.utc_time_offset != "local" {
create_offset_time_string(Utc::now(), config.utc_time_offset, time_format).unwrap_or_else(
|_| {
let formatted_time_string = match &config.utc_time_offset {
Either::First(TimezoneWrapper(tz)) => {
// Use IANA timezone name
let target_time = Timestamp::now().to_zoned(tz.clone());
format_time_fixed_offset(time_format, target_time)
}
Either::Second("local") => {
// Use local timezone
format_time(time_format, Zoned::now())
}
Either::Second(utc_time_offset) => {
// Use numeric offset
create_offset_time_string(Timestamp::now(), utc_time_offset, time_format)
.unwrap_or_else(|_| {
log::warn!(
"Invalid utc_time_offset configuration provided! Falling back to \"local\"."
);
format_time(time_format, Local::now())
},
)
} else {
format_time(time_format, Local::now())
format_time(time_format, Zoned::now())
})
}
};
let parsed = StringFormatter::new(config.format).and_then(|formatter| {
@@ -65,10 +79,10 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
}
fn create_offset_time_string(
utc_time: DateTime<Utc>,
utc_time: Timestamp,
utc_time_offset_str: &str,
time_format: &str,
) -> Result<String, &'static str> {
) -> Result<String, String> {
// Using floats to allow 30/45 minute offsets: https://www.timeanddate.com/time/time-zones-interesting.html
let utc_time_offset_in_hours = utc_time_offset_str.parse::<f32>().unwrap_or(
// Passing out of range value to force falling back to "local"
@@ -76,38 +90,34 @@ fn create_offset_time_string(
);
if utc_time_offset_in_hours < 24_f32 && utc_time_offset_in_hours > -24_f32 {
let utc_offset_in_seconds: i32 = (utc_time_offset_in_hours * 3600_f32) as i32;
let Some(timezone_offset) = FixedOffset::east_opt(utc_offset_in_seconds) else {
return Err("Invalid offset");
};
let timezone_offset = Offset::from_seconds(utc_offset_in_seconds)
.map_err(|err| format!("Invalid timezone offset: {err:?}"))?;
let tz = TimeZone::fixed(timezone_offset);
log::trace!("Target timezone offset is {timezone_offset}");
let target_time = utc_time.with_timezone(&timezone_offset);
let target_time = utc_time.to_zoned(tz);
log::trace!("Time in target timezone now is {target_time}");
Ok(format_time_fixed_offset(time_format, target_time))
} else {
Err("Invalid timezone offset.")
Err("Invalid timezone offset.".to_string())
}
}
/// Format a given time into the given string. This function should be referentially
/// transparent, which makes it easy to test (unlike anything involving the actual time)
fn format_time(time_format: &str, local_time: DateTime<Local>) -> String {
local_time.format(time_format).to_string()
fn format_time(time_format: &str, local_time: Zoned) -> String {
local_time.strftime(time_format).to_string()
}
fn format_time_fixed_offset(time_format: &str, utc_time: DateTime<FixedOffset>) -> String {
utc_time.format(time_format).to_string()
fn format_time_fixed_offset(time_format: &str, zoned_time: Zoned) -> String {
zoned_time.strftime(time_format).to_string()
}
/// Returns true if `time_now` is between `time_start` and `time_end`.
/// If one of these values is not given, then it is ignored.
/// It also handles cases where `time_start` and `time_end` have a midnight in between
fn is_inside_time_range(
time_now: NaiveTime,
time_start: Option<NaiveTime>,
time_end: Option<NaiveTime>,
) -> bool {
fn is_inside_time_range(time_now: Time, time_start: Option<Time>, time_end: Option<Time>) -> bool {
match (time_start, time_end) {
(None, None) => true,
(Some(i), None) => time_now > i,
@@ -127,7 +137,7 @@ fn is_inside_time_range(
///
/// If one of the ranges is invalid or not provided, then the corresponding field in the output
/// tuple is None
fn parse_time_range(time_range: &str) -> (Option<NaiveTime>, Option<NaiveTime>) {
fn parse_time_range(time_range: &str) -> (Option<Time>, Option<Time>) {
let value = String::from(time_range);
// Check if there is exactly one hyphen, and fail otherwise
@@ -140,8 +150,8 @@ fn parse_time_range(time_range: &str) -> (Option<NaiveTime>, Option<NaiveTime>)
let end = &end[1..];
// Parse the ranges
let start_time = NaiveTime::parse_from_str(start, "%H:%M:%S").ok();
let end_time = NaiveTime::parse_from_str(end, "%H:%M:%S").ok();
let start_time = start.parse::<Time>().ok();
let end_time = end.parse::<Time>().ok();
(start_time, end_time)
}
@@ -152,140 +162,151 @@ tests become extra important */
mod tests {
use super::*;
use crate::test::ModuleRenderer;
use chrono::offset::TimeZone;
use jiff::civil::date;
const FMT_12: &str = "%r";
const FMT_24: &str = "%T";
#[test]
fn test_midnight_12hr() {
let time = Local.with_ymd_and_hms(2014, 7, 8, 0, 0, 0).unwrap();
let time = date(2014, 7, 8)
.at(0, 0, 0, 0)
.to_zoned(TimeZone::system())
.unwrap();
let formatted = format_time(FMT_12, time);
assert_eq!(formatted, "12:00:00 AM");
}
#[test]
fn test_midnight_24hr() {
let time = Local.with_ymd_and_hms(2014, 7, 8, 0, 0, 0).unwrap();
let time = date(2014, 7, 8)
.at(0, 0, 0, 0)
.to_zoned(TimeZone::system())
.unwrap();
let formatted = format_time(FMT_24, time);
assert_eq!(formatted, "00:00:00");
}
#[test]
fn test_noon_12hr() {
let time = Local.with_ymd_and_hms(2014, 7, 8, 12, 0, 0).unwrap();
let time = date(2014, 7, 8)
.at(12, 0, 0, 0)
.to_zoned(TimeZone::system())
.unwrap();
let formatted = format_time(FMT_12, time);
assert_eq!(formatted, "12:00:00 PM");
}
#[test]
fn test_noon_24hr() {
let time = Local.with_ymd_and_hms(2014, 7, 8, 12, 0, 0).unwrap();
let time = date(2014, 7, 8)
.at(12, 0, 0, 0)
.to_zoned(TimeZone::system())
.unwrap();
let formatted = format_time(FMT_24, time);
assert_eq!(formatted, "12:00:00");
}
#[test]
fn test_arbtime_12hr() {
let time = Local.with_ymd_and_hms(2014, 7, 8, 15, 36, 47).unwrap();
let time = date(2014, 7, 8)
.at(15, 36, 47, 0)
.to_zoned(TimeZone::system())
.unwrap();
let formatted = format_time(FMT_12, time);
assert_eq!(formatted, "03:36:47 PM");
assert_eq!(formatted, "3:36:47 PM");
}
#[test]
fn test_arbtime_24hr() {
let time = Local.with_ymd_and_hms(2014, 7, 8, 15, 36, 47).unwrap();
let time = date(2014, 7, 8)
.at(15, 36, 47, 0)
.to_zoned(TimeZone::system())
.unwrap();
let formatted = format_time(FMT_24, time);
assert_eq!(formatted, "15:36:47");
}
#[test]
fn test_format_with_paren() {
let time = Local.with_ymd_and_hms(2014, 7, 8, 15, 36, 47).unwrap();
let time = date(2014, 7, 8)
.at(15, 36, 47, 0)
.to_zoned(TimeZone::system())
.unwrap();
let formatted = format_time("[%T]", time);
assert_eq!(formatted, "[15:36:47]");
}
#[test]
fn test_midnight_12hr_fixed_offset() {
let timezone_offset = FixedOffset::east_opt(0).unwrap();
let time = Utc
.with_ymd_and_hms(2014, 7, 8, 0, 0, 0)
.unwrap()
.with_timezone(&timezone_offset);
let timezone_offset = Offset::from_seconds(0).unwrap();
let tz = TimeZone::fixed(timezone_offset);
let time = date(2014, 7, 8).at(0, 0, 0, 0).to_zoned(tz).unwrap();
let formatted = format_time_fixed_offset(FMT_12, time);
assert_eq!(formatted, "12:00:00 AM");
}
#[test]
fn test_midnight_24hr_fixed_offset() {
let timezone_offset = FixedOffset::east_opt(0).unwrap();
let time = Utc
.with_ymd_and_hms(2014, 7, 8, 0, 0, 0)
.unwrap()
.with_timezone(&timezone_offset);
let timezone_offset = Offset::from_seconds(0).unwrap();
let tz = TimeZone::fixed(timezone_offset);
let time = date(2014, 7, 8).at(0, 0, 0, 0).to_zoned(tz).unwrap();
let formatted = format_time_fixed_offset(FMT_24, time);
assert_eq!(formatted, "00:00:00");
}
#[test]
fn test_noon_12hr_fixed_offset() {
let timezone_offset = FixedOffset::east_opt(0).unwrap();
let time = Utc
.with_ymd_and_hms(2014, 7, 8, 12, 0, 0)
.unwrap()
.with_timezone(&timezone_offset);
let timezone_offset = Offset::from_seconds(0).unwrap();
let tz = TimeZone::fixed(timezone_offset);
let time = date(2014, 7, 8).at(12, 0, 0, 0).to_zoned(tz).unwrap();
let formatted = format_time_fixed_offset(FMT_12, time);
assert_eq!(formatted, "12:00:00 PM");
}
#[test]
fn test_noon_24hr_fixed_offset() {
let timezone_offset = FixedOffset::east_opt(0).unwrap();
let time = Utc
.with_ymd_and_hms(2014, 7, 8, 12, 0, 0)
.unwrap()
.with_timezone(&timezone_offset);
let timezone_offset = Offset::from_seconds(0).unwrap();
let tz = TimeZone::fixed(timezone_offset);
let time = date(2014, 7, 8).at(12, 0, 0, 0).to_zoned(tz).unwrap();
let formatted = format_time_fixed_offset(FMT_24, time);
assert_eq!(formatted, "12:00:00");
}
#[test]
fn test_arbtime_12hr_fixed_offset() {
let timezone_offset = FixedOffset::east_opt(0).unwrap();
let time = Utc
.with_ymd_and_hms(2014, 7, 8, 15, 36, 47)
.unwrap()
.with_timezone(&timezone_offset);
let timezone_offset = Offset::from_seconds(0).unwrap();
let tz = TimeZone::fixed(timezone_offset);
let time = date(2014, 7, 8).at(15, 36, 47, 0).to_zoned(tz).unwrap();
let formatted = format_time_fixed_offset(FMT_12, time);
assert_eq!(formatted, "03:36:47 PM");
assert_eq!(formatted, "3:36:47 PM");
}
#[test]
fn test_arbtime_24hr_fixed_offset() {
let timezone_offset = FixedOffset::east_opt(0).unwrap();
let time = Utc
.with_ymd_and_hms(2014, 7, 8, 15, 36, 47)
.unwrap()
.with_timezone(&timezone_offset);
let timezone_offset = Offset::from_seconds(0).unwrap();
let tz = TimeZone::fixed(timezone_offset);
let time = date(2014, 7, 8).at(15, 36, 47, 0).to_zoned(tz).unwrap();
let formatted = format_time_fixed_offset(FMT_24, time);
assert_eq!(formatted, "15:36:47");
}
#[test]
fn test_format_with_paren_fixed_offset() {
let timezone_offset = FixedOffset::east_opt(0).unwrap();
let time = Utc
.with_ymd_and_hms(2014, 7, 8, 15, 36, 47)
.unwrap()
.with_timezone(&timezone_offset);
let timezone_offset = Offset::from_seconds(0).unwrap();
let tz = TimeZone::fixed(timezone_offset);
let time = date(2014, 7, 8).at(15, 36, 47, 0).to_zoned(tz).unwrap();
let formatted = format_time_fixed_offset("[%T]", time);
assert_eq!(formatted, "[15:36:47]");
}
#[test]
fn test_create_formatted_time_string_with_minus_3() {
let utc_time: DateTime<Utc> = Utc.with_ymd_and_hms(2014, 7, 8, 15, 36, 47).unwrap();
let utc_time = date(2014, 7, 8)
.at(15, 36, 47, 0)
.to_zoned(TimeZone::UTC)
.unwrap()
.timestamp();
let utc_time_offset_str = "-3";
let actual = create_offset_time_string(utc_time, utc_time_offset_str, FMT_12).unwrap();
@@ -294,34 +315,50 @@ mod tests {
#[test]
fn test_create_formatted_time_string_with_plus_5() {
let utc_time: DateTime<Utc> = Utc.with_ymd_and_hms(2014, 7, 8, 15, 36, 47).unwrap();
let utc_time = date(2014, 7, 8)
.at(15, 36, 47, 0)
.to_zoned(TimeZone::UTC)
.unwrap()
.timestamp();
let utc_time_offset_str = "+5";
let actual = create_offset_time_string(utc_time, utc_time_offset_str, FMT_12).unwrap();
assert_eq!(actual, "08:36:47 PM");
assert_eq!(actual, "8:36:47 PM");
}
#[test]
fn test_create_formatted_time_string_with_plus_9_30() {
let utc_time: DateTime<Utc> = Utc.with_ymd_and_hms(2014, 7, 8, 15, 36, 47).unwrap();
let utc_time = date(2014, 7, 8)
.at(15, 36, 47, 0)
.to_zoned(TimeZone::UTC)
.unwrap()
.timestamp();
let utc_time_offset_str = "+9.5";
let actual = create_offset_time_string(utc_time, utc_time_offset_str, FMT_12).unwrap();
assert_eq!(actual, "01:06:47 AM");
assert_eq!(actual, "1:06:47 AM");
}
#[test]
fn test_create_formatted_time_string_with_plus_5_45() {
let utc_time: DateTime<Utc> = Utc.with_ymd_and_hms(2014, 7, 8, 15, 36, 47).unwrap();
let utc_time = date(2014, 7, 8)
.at(15, 36, 47, 0)
.to_zoned(TimeZone::UTC)
.unwrap()
.timestamp();
let utc_time_offset_str = "+5.75";
let actual = create_offset_time_string(utc_time, utc_time_offset_str, FMT_12).unwrap();
assert_eq!(actual, "09:21:47 PM");
assert_eq!(actual, "9:21:47 PM");
}
#[test]
fn test_create_formatted_time_string_with_plus_24() {
let utc_time: DateTime<Utc> = Utc.with_ymd_and_hms(2014, 7, 8, 15, 36, 47).unwrap();
let utc_time = date(2014, 7, 8)
.at(15, 36, 47, 0)
.to_zoned(TimeZone::UTC)
.unwrap()
.timestamp();
let utc_time_offset_str = "+24";
create_offset_time_string(utc_time, utc_time_offset_str, FMT_12)
@@ -330,7 +367,11 @@ mod tests {
#[test]
fn test_create_formatted_time_string_with_minus_24() {
let utc_time: DateTime<Utc> = Utc.with_ymd_and_hms(2014, 7, 8, 15, 36, 47).unwrap();
let utc_time = date(2014, 7, 8)
.at(15, 36, 47, 0)
.to_zoned(TimeZone::UTC)
.unwrap()
.timestamp();
let utc_time_offset_str = "-24";
create_offset_time_string(utc_time, utc_time_offset_str, FMT_12)
@@ -339,7 +380,11 @@ mod tests {
#[test]
fn test_create_formatted_time_string_with_plus_9001() {
let utc_time: DateTime<Utc> = Utc.with_ymd_and_hms(2014, 7, 8, 15, 36, 47).unwrap();
let utc_time = date(2014, 7, 8)
.at(15, 36, 47, 0)
.to_zoned(TimeZone::UTC)
.unwrap()
.timestamp();
let utc_time_offset_str = "+9001";
create_offset_time_string(utc_time, utc_time_offset_str, FMT_12)
@@ -348,7 +393,11 @@ mod tests {
#[test]
fn test_create_formatted_time_string_with_minus_4242() {
let utc_time: DateTime<Utc> = Utc.with_ymd_and_hms(2014, 7, 8, 15, 36, 47).unwrap();
let utc_time = date(2014, 7, 8)
.at(15, 36, 47, 0)
.to_zoned(TimeZone::UTC)
.unwrap()
.timestamp();
let utc_time_offset_str = "-4242";
create_offset_time_string(utc_time, utc_time_offset_str, FMT_12)
@@ -357,7 +406,11 @@ mod tests {
#[test]
fn test_create_formatted_time_string_with_invalid_string() {
let utc_time: DateTime<Utc> = Utc.with_ymd_and_hms(2014, 7, 8, 15, 36, 47).unwrap();
let utc_time = date(2014, 7, 8)
.at(15, 36, 47, 0)
.to_zoned(TimeZone::UTC)
.unwrap()
.timestamp();
let utc_time_offset_str = "completely wrong config";
create_offset_time_string(utc_time, utc_time_offset_str, FMT_12)
@@ -379,7 +432,7 @@ mod tests {
assert_eq!(
parse_time_range(time_range),
(Some(NaiveTime::from_hms_opt(10, 00, 00).unwrap()), None)
(Some(Time::new(10, 0, 0, 0).unwrap()), None)
);
}
@@ -389,7 +442,7 @@ mod tests {
assert_eq!(
parse_time_range(time_range),
(None, Some(NaiveTime::from_hms_opt(22, 00, 00).unwrap()))
(None, Some(Time::new(22, 0, 0, 0).unwrap()))
);
}
@@ -400,8 +453,8 @@ mod tests {
assert_eq!(
parse_time_range(time_range),
(
Some(NaiveTime::from_hms_opt(10, 00, 00).unwrap()),
Some(NaiveTime::from_hms_opt(16, 00, 00).unwrap())
Some(Time::new(10, 0, 0, 0).unwrap()),
Some(Time::new(16, 0, 0, 0).unwrap())
)
);
}
@@ -410,16 +463,16 @@ mod tests {
fn test_is_inside_time_range_with_no_range() {
let time_start = None;
let time_end = None;
let time_now = NaiveTime::from_hms_opt(10, 00, 00).unwrap();
let time_now = Time::new(10, 0, 0, 0).unwrap();
assert!(is_inside_time_range(time_now, time_start, time_end));
}
#[test]
fn test_is_inside_time_range_with_start_range() {
let time_start = Some(NaiveTime::from_hms_opt(10, 00, 00).unwrap());
let time_now = NaiveTime::from_hms_opt(12, 00, 00).unwrap();
let time_now2 = NaiveTime::from_hms_opt(8, 00, 00).unwrap();
let time_start = Some(Time::new(10, 0, 0, 0).unwrap());
let time_now = Time::new(12, 0, 0, 0).unwrap();
let time_now2 = Time::new(8, 0, 0, 0).unwrap();
assert!(is_inside_time_range(time_now, time_start, None));
assert!(!is_inside_time_range(time_now2, time_start, None));
@@ -427,9 +480,9 @@ mod tests {
#[test]
fn test_is_inside_time_range_with_end_range() {
let time_end = Some(NaiveTime::from_hms_opt(16, 00, 00).unwrap());
let time_now = NaiveTime::from_hms_opt(15, 00, 00).unwrap();
let time_now2 = NaiveTime::from_hms_opt(19, 00, 00).unwrap();
let time_end = Some(Time::new(16, 0, 0, 0).unwrap());
let time_now = Time::new(15, 0, 0, 0).unwrap();
let time_now2 = Time::new(19, 0, 0, 0).unwrap();
assert!(is_inside_time_range(time_now, None, time_end));
assert!(!is_inside_time_range(time_now2, None, time_end));
@@ -437,11 +490,11 @@ mod tests {
#[test]
fn test_is_inside_time_range_with_complete_range() {
let time_start = Some(NaiveTime::from_hms_opt(9, 00, 00).unwrap());
let time_end = Some(NaiveTime::from_hms_opt(18, 00, 00).unwrap());
let time_now = NaiveTime::from_hms_opt(3, 00, 00).unwrap();
let time_now2 = NaiveTime::from_hms_opt(13, 00, 00).unwrap();
let time_now3 = NaiveTime::from_hms_opt(20, 00, 00).unwrap();
let time_start = Some(Time::new(9, 0, 0, 0).unwrap());
let time_end = Some(Time::new(18, 0, 0, 0).unwrap());
let time_now = Time::new(3, 0, 0, 0).unwrap();
let time_now2 = Time::new(13, 0, 0, 0).unwrap();
let time_now3 = Time::new(20, 0, 0, 0).unwrap();
assert!(!is_inside_time_range(time_now, time_start, time_end));
assert!(is_inside_time_range(time_now2, time_start, time_end));
@@ -450,11 +503,11 @@ mod tests {
#[test]
fn test_is_inside_time_range_with_complete_range_passing_midnight() {
let time_start = Some(NaiveTime::from_hms_opt(19, 00, 00).unwrap());
let time_end = Some(NaiveTime::from_hms_opt(12, 00, 00).unwrap());
let time_now = NaiveTime::from_hms_opt(3, 00, 00).unwrap();
let time_now2 = NaiveTime::from_hms_opt(13, 00, 00).unwrap();
let time_now3 = NaiveTime::from_hms_opt(20, 00, 00).unwrap();
let time_start = Some(Time::new(19, 0, 0, 0).unwrap());
let time_end = Some(Time::new(12, 0, 0, 0).unwrap());
let time_now = Time::new(3, 0, 0, 0).unwrap();
let time_now2 = Time::new(13, 0, 0, 0).unwrap();
let time_now3 = Time::new(20, 0, 0, 0).unwrap();
assert!(is_inside_time_range(time_now, time_start, time_end));
assert!(!is_inside_time_range(time_now2, time_start, time_end));
@@ -503,4 +556,98 @@ mod tests {
assert!(actual.starts_with(&col_prefix));
assert!(actual.ends_with(&col_suffix));
}
#[test]
fn config_check_invalid_tz() {
let actual = ModuleRenderer::new("time")
.config(toml::toml! {
[time]
disabled = false
time_format = "%T"
utc_time_offset = "invalid"
})
.collect();
assert!(actual.is_some(), "Falls back to local time");
}
#[test]
fn module_tz() {
use nu_ansi_term::Color;
let actual = ModuleRenderer::new("time")
.config(toml::toml! {
[time]
disabled = false
time_format = "%:z"
utc_time_offset = "Asia/Kolkata"
})
.collect();
let expected = Some(format!("at {} ", Color::Yellow.bold().paint("+05:30")));
assert_eq!(actual, expected, "Uses timezone");
}
#[test]
fn module_offset() {
use nu_ansi_term::Color;
let actual = ModuleRenderer::new("time")
.config(toml::toml! {
[time]
disabled = false
time_format = "%:z"
utc_time_offset = "-1.75"
})
.collect();
let expected = Some(format!("at {} ", Color::Yellow.bold().paint("-01:45")));
assert_eq!(actual, expected, "Uses timezone offset");
}
#[test]
fn module_tz_abbreviation() {
let actual = ModuleRenderer::new("time")
.config(toml::toml! {
[time]
disabled = false
time_format = "%Z"
utc_time_offset = "America/New_York"
})
.collect();
// Should output a timezone abbreviation like "EST" or "EDT"
assert!(
actual.is_some(),
"Timezone abbreviation should be displayed"
);
let output = actual.unwrap();
assert!(
output.contains("EST") || output.contains("EDT"),
"Should contain timezone abbreviation"
);
}
#[test]
fn module_tz_identifier() {
use nu_ansi_term::Color;
let actual = ModuleRenderer::new("time")
.config(toml::toml! {
[time]
disabled = false
time_format = "%Q"
utc_time_offset = "America/New_York"
})
.collect();
let expected = Some(format!(
"at {} ",
Color::Yellow.bold().paint("America/New_York")
));
assert_eq!(actual, expected, "Uses IANA timezone identifier");
}
}