Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 additions & 1 deletion src/theme_engine/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -485,9 +485,64 @@ fn templates_apply_numeric_character_formats() {
format_template("Used {claude.session.percentage:0.0}%", &context),
"Used 73.5%"
);
assert_eq!(format_template("{reset:duration_short}", &context), "4d");
assert_eq!(format_template("{reset:duration_short}", &context), "5d");
}

#[test]
fn duration_short_precision_scale_and_half_steps() {
let mut context = DataContext::default();
context.insert_string("i18n.day_suffix", "д");
context.insert_string("i18n.hour_suffix", "ч");
context.insert_string("i18n.minute_suffix", "м");
context.insert_string("i18n.second_suffix", "с");
context.insert_string("i18n.now", "сейчас");

let check = |secs: f64, expected: &str, ctx: &DataContext| {
let mut c = ctx.clone();
c.insert("val", secs);
assert_eq!(format_template("{val:duration_short}", &c), expected);
};

// Minutes under 1 hour (< 3600s)
check(3580.0, "59м", &context); // 59m 40s must clamp to 59m, never 60m
check(3300.0, "55м", &context);
check(2700.0, "45м", &context);
check(900.0, "15м", &context);
check(60.0, "1м", &context);

// Hours under 10 hours (1h .. 10h): 0.5 step rounding with dot
check(3600.0, "1ч", &context);
check(3900.0, "1ч", &context); // 1h 05m -> 1ч
check(5400.0, "1.5ч", &context); // 1h 30m -> 1.5ч
check(7800.0, "2ч", &context); // 2h 10m -> 2ч
check(9300.0, "2.5ч", &context); // 2h 35m -> 2.5ч
check(10500.0, "3ч", &context); // 2h 55m -> 3ч
check(34200.0, "9.5ч", &context); // 9h 30m -> 9.5ч

// Hours >= 10 hours: integer math rounding
check(36000.0, "10ч", &context);
check(43200.0, "12ч", &context);
check(66300.0, "18ч", &context); // 18h 25m -> 18ч
check(85500.0, "1д", &context); // 23h 45m -> rolls over to 1д

// Days < 10 days: 0.5 step rounding with dot
check(86400.0, "1д", &context);
check(129600.0, "1.5д", &context); // 1d 12h -> 1.5д
check(216000.0, "2.5д", &context); // 2d 12h -> 2.5д
check(532800.0, "6д", &context); // 6d 4h -> strictly 6д
check(565200.0, "6.5д", &context); // 6d 13h -> 6.5д
check(604800.0, "7д", &context); // 7d -> 7д

// Days >= 10 days: integer math rounding
check(892800.0, "10д", &context); // 10d 8h -> 10д
check(1036800.0, "12д", &context); // 12d -> 12д

// Seconds and now
check(45.0, "45с", &context);
check(0.0, "сейчас", &context);
}


#[test]
fn text_templates_allow_if_to_return_quoted_strings() {
let mut context = DataContext::default();
Expand Down
60 changes: 46 additions & 14 deletions src/theme_engine/theme_expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -582,26 +582,58 @@ pub(super) fn localized<'a>(context: &'a DataContext, name: &str, fallback: &'a
context.get_string(name).unwrap_or(fallback)
}

fn format_half_step(val: f64, suffix: &str) -> String {
let rounded = (val * 2.0).round() / 2.0;
if rounded.fract().abs() < 1e-4 {
format!("{:.0}{suffix}", rounded)
} else {
format!("{:.1}{suffix}", rounded)
}
}

pub(super) fn format_duration_short(seconds: u64, context: &DataContext) -> String {
let day_suffix = localized(context, "i18n.day_suffix", "d");
let hour_suffix = localized(context, "i18n.hour_suffix", "h");
let minute_suffix = localized(context, "i18n.minute_suffix", "m");
let second_suffix = localized(context, "i18n.second_suffix", "s");

if seconds >= 86_400 {
if seconds >= 864_000 {
let days = (seconds + 43_200) / 86_400;
format!("{days}{day_suffix}")
} else {
let days = seconds as f64 / 86_400.0;
format_half_step(days, day_suffix)
}
} else if seconds >= 3_600 {
if seconds >= 36_000 {
let hours = (seconds + 1800) / 3600;
if hours >= 24 {
format!("1{day_suffix}")
} else {
format!("{hours}{hour_suffix}")
}
} else {
let hours = seconds as f64 / 3600.0;
format_half_step(hours, hour_suffix)
}
} else if seconds >= 60 {
let minutes = ((seconds + 30) / 60).min(59);
format!("{minutes}{minute_suffix}")
} else if seconds > 0 {
format!("{seconds}{second_suffix}")
} else {
localized(context, "i18n.now", "now").to_string()
}
}

pub(super) fn format_value(value: f64, format: &str, context: &DataContext) -> String {
if let Some(value) = format_timestamp(value, format, context) {
return value;
}
if format.eq_ignore_ascii_case("duration_short") {
let seconds = value.max(0.0).round() as u64;
let days = seconds / 86_400;
let hours = seconds / 3_600;
let minutes = seconds / 60;
return if days > 0 {
format!("{days}{}", localized(context, "i18n.day_suffix", "d"))
} else if hours > 0 {
format!("{hours}{}", localized(context, "i18n.hour_suffix", "h"))
} else if minutes > 0 {
format!("{minutes}{}", localized(context, "i18n.minute_suffix", "m"))
} else if seconds > 0 {
format!("{seconds}{}", localized(context, "i18n.second_suffix", "s"))
} else {
localized(context, "i18n.now", "now").to_string()
};
return format_duration_short(seconds, context);
}
if format.eq_ignore_ascii_case("duration") {
let seconds = value.max(0.0).round() as u64;
Expand Down
Loading