From a4700becde93ea709605cfa28c52d192d0e61bad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 25 Sep 2026 23:52:42 -0400 Subject: [PATCH] T16 review fixes: an unknown system zone stops instead of reading as UTC, the view window refits when data shrinks, the volume profile counts narrow and flat candles whole, Fibonacci labels stay in their pane, the DOM ladder shows its tick's decimals, the tape flashes on each new trade, typed numbers reach their owner before a press, and a smaller loss than expected reads as a beat --- src/finance/blotter.rs | 29 ++++++++++++++++-------- src/finance/calendars.rs | 45 ++++++++++++++++++++++++++++--------- src/finance/candles.rs | 28 +++++++++++++++++++++-- src/finance/labels.rs | 10 ++++++++- src/finance/market.rs | 22 +++++++++++++----- src/finance/renko.rs | 17 +++++++++----- src/finance/stage.rs | 24 ++++++++++++++++++++ src/finance/tape.rs | 21 ++++++++++------- src/finance/wallet.rs | 18 ++++++++++----- src/forms/date/mod.rs | 8 +++---- src/forms/number.rs | 42 ++++++++++++++++++++++++---------- src/forms/tests/mod.rs | 20 +++++++++++++++++ src/typography/format.rs | 7 ++++++ src/typography/formatted.rs | 4 +--- 14 files changed, 230 insertions(+), 65 deletions(-) diff --git a/src/finance/blotter.rs b/src/finance/blotter.rs index aecef28..70c428d 100644 --- a/src/finance/blotter.rs +++ b/src/finance/blotter.rs @@ -16,7 +16,10 @@ use crate::{ motion::Flash, tables::{Aggregate, Cell, Column, DataTable, Row}, theme::{ActiveTheme, TextSize}, - typography::{format, tabular}, + typography::{ + format::{self, system_zone}, + tabular, + }, }; /// A held position: its symbol, how many, what each cost on average, and the last price. @@ -115,7 +118,7 @@ pub struct Working { pub struct OrderTable { id: ElementId, orders: Vec, - zone: TimeZone, + zone: Option, } impl OrderTable { @@ -132,13 +135,13 @@ impl OrderTable { Self { id: id.into(), orders, - zone: TimeZone::system(), + zone: None, } } /// The time zone its times read in; the system's unless set. pub fn zone(mut self, zone: TimeZone) -> Self { - self.zone = zone; + self.zone = Some(zone); self } } @@ -153,6 +156,10 @@ fn side_tag(side: Side) -> Cell { impl RenderOnce for OrderTable { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { + let zone = self + .zone + .clone() + .unwrap_or_else(|| system_zone("OrderTable")); let columns = [ Column::new("time", "Placed"), Column::new("symbol", "Symbol"), @@ -175,7 +182,7 @@ impl RenderOnce for OrderTable { }; let time: SharedString = working .time - .to_zoned(self.zone.clone()) + .to_zoned(zone.clone()) .strftime("%H:%M:%S") .to_string() .into(); @@ -214,7 +221,7 @@ pub struct Fill { pub struct TradeHistoryTable { id: ElementId, fills: Vec, - zone: TimeZone, + zone: Option, } impl TradeHistoryTable { @@ -224,19 +231,23 @@ impl TradeHistoryTable { Self { id: id.into(), fills, - zone: TimeZone::system(), + zone: None, } } /// The time zone its times read in; the system's unless set. pub fn zone(mut self, zone: TimeZone) -> Self { - self.zone = zone; + self.zone = Some(zone); self } } impl RenderOnce for TradeHistoryTable { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { + let zone = self + .zone + .clone() + .unwrap_or_else(|| system_zone("TradeHistoryTable")); let columns = [ Column::new("time", "Time"), Column::new("symbol", "Symbol"), @@ -261,7 +272,7 @@ impl RenderOnce for TradeHistoryTable { let total = signed * fill.quantity * fill.price - fill.fee; let time: SharedString = fill .time - .to_zoned(self.zone.clone()) + .to_zoned(zone.clone()) .strftime("%b %-d %H:%M") .to_string() .into(); diff --git a/src/finance/calendars.rs b/src/finance/calendars.rs index 712c3db..dda092e 100644 --- a/src/finance/calendars.rs +++ b/src/finance/calendars.rs @@ -11,7 +11,11 @@ use crate::{ forms::flag, tables::{Cell, Column, DataTable, Row}, theme::{ActiveTheme, TextSize}, - typography::{Ellipsis, format, tabular}, + typography::{ + Ellipsis, + format::{self, system_zone}, + tabular, + }, }; /// An economic release: when, for which region, what, how much it moves markets from one to three, and its figure as it came, as forecast and as before. @@ -31,7 +35,7 @@ pub struct Release { #[derive(IntoElement)] pub struct EconomicCalendar { releases: Vec, - zone: TimeZone, + zone: Option, } impl EconomicCalendar { @@ -45,18 +49,22 @@ impl EconomicCalendar { ); Self { releases, - zone: TimeZone::system(), + zone: None, } } pub fn zone(mut self, zone: TimeZone) -> Self { - self.zone = zone; + self.zone = Some(zone); self } } impl RenderOnce for EconomicCalendar { fn render(mut self, _: &mut Window, cx: &mut App) -> impl IntoElement { + let zone = self + .zone + .clone() + .unwrap_or_else(|| system_zone("EconomicCalendar")); let theme = cx.theme(); let colors = theme.colors.clone(); self.releases.sort_by_key(|release| release.time); @@ -68,7 +76,7 @@ impl RenderOnce for EconomicCalendar { let mut out: Vec = Vec::new(); let mut day: Option = None; for release in &self.releases { - let zoned = release.time.to_zoned(self.zone.clone()); + let zoned = release.time.to_zoned(zone.clone()); if day != Some(zoned.date()) { day = Some(zoned.date()); out.push( @@ -179,6 +187,11 @@ pub struct Earnings { pub reported: Option, } +/// How far a report beat its estimate, as a share of the estimate's size: positive when better, whatever the sign. +pub(crate) fn surprise(estimate: f64, reported: f64) -> f64 { + (reported - estimate) / estimate.abs() +} + /// Earnings by date: each company, when in the day it reports, what is expected, and once reported, how far it beat or missed. #[derive(IntoElement)] pub struct EarningsCalendar { @@ -222,7 +235,7 @@ impl RenderOnce for EarningsCalendar { ); let surprise = match earnings.reported { Some(reported) if earnings.estimate != 0.0 => { - let share = reported / earnings.estimate - 1.0; + let share = surprise(earnings.estimate, reported); Cell::Tag( format::percent(share, 1, true).into(), if share >= 0.0 { @@ -270,7 +283,7 @@ type OnStory = Rc; pub struct NewsFeed { id: ElementId, stories: Vec, - zone: TimeZone, + zone: Option, on_open: Option, } @@ -279,13 +292,13 @@ impl NewsFeed { Self { id: id.into(), stories: stories.into_iter().collect(), - zone: TimeZone::system(), + zone: None, on_open: None, } } pub fn zone(mut self, zone: TimeZone) -> Self { - self.zone = zone; + self.zone = Some(zone); self } @@ -298,6 +311,7 @@ impl NewsFeed { impl RenderOnce for NewsFeed { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + let zone = self.zone.clone().unwrap_or_else(|| system_zone("NewsFeed")); let theme = cx.theme(); let colors = theme.colors.clone(); let mut order: Vec = (0..self.stories.len()).collect(); @@ -315,7 +329,7 @@ impl RenderOnce for NewsFeed { let open = self.on_open.clone(); let when = story .time - .to_zoned(self.zone.clone()) + .to_zoned(zone.clone()) .strftime("%b %-d, %H:%M") .to_string(); div() @@ -376,3 +390,14 @@ impl RenderOnce for NewsFeed { })) } } + +#[cfg(test)] +mod tests { + use super::surprise; + + #[test] + fn a_smaller_loss_than_expected_is_a_beat() { + assert_eq!(surprise(-1.0, -0.5), 0.5); + assert_eq!(surprise(2.0, 1.5), -0.25); + } +} diff --git a/src/finance/candles.rs b/src/finance/candles.rs index 1fd26c0..378778b 100644 --- a/src/finance/candles.rs +++ b/src/finance/candles.rs @@ -162,14 +162,22 @@ pub(crate) fn profile( (low, high): (f64, f64), bands: usize, ) -> Vec<(f64, f64, f64)> { + assert!(bands > 0 && high > low, "a profile needs bands over a span"); let step = (high - low) / bands as f64; let mut traded = vec![0.0; bands]; for candle in candles { - let reach = (candle.high - candle.low).max(step); + let span = candle.high - candle.low; + if span <= 0.0 { + let band = ((candle.close - low) / step) + .floor() + .clamp(0.0, (bands - 1) as f64); + traded[band as usize] += candle.volume; + continue; + } for (band, volume) in traded.iter_mut().enumerate() { let (from, to) = (low + step * band as f64, low + step * (band + 1) as f64); let overlap = (to.min(candle.high) - from.max(candle.low)).max(0.0); - *volume += candle.volume * overlap / reach; + *volume += candle.volume * overlap / span; } } traded @@ -250,4 +258,20 @@ mod tests { ); assert_eq!((bands[3].0, bands[3].1), (13.0, 14.0)); } + + #[test] + fn narrow_and_flat_candles_count_all_their_volume() { + let narrow = Candle::new(Timestamp::UNIX_EPOCH, (10.2, 10.3, 10.1, 10.2), 100.0); + let flat = Candle::new(Timestamp::UNIX_EPOCH, (12.5, 12.5, 12.5, 12.5), 40.0); + let traded: Vec = profile(&[narrow, flat], (10.0, 14.0), 4) + .iter() + .map(|band| band.2) + .collect(); + assert!( + (traded[0] - 100.0).abs() < 1e-9, + "{traded:?}: a narrow candle's whole volume" + ); + assert_eq!(traded[2], 40.0, "a flat candle lands in its band"); + assert!((traded.iter().sum::() - 140.0).abs() < 1e-9); + } } diff --git a/src/finance/labels.rs b/src/finance/labels.rs index 81a8885..a66233c 100644 --- a/src/finance/labels.rs +++ b/src/finance/labels.rs @@ -225,7 +225,15 @@ pub(crate) fn labels( continue; }; let x = axes.visible.x(from.0.min(to.0), (main.x, main.w)); - for (share, value) in Drawing::levels(from, to) { + if x > main.x + main.w { + continue; + } + let x = x.max(main.x); + let inside = |y: f32| y >= main.y && y <= main.y + main.h; + for (share, value) in Drawing::levels(from, to) + .into_iter() + .filter(|(_, value)| inside(price.at(*value))) + { let words = format!( "{} {}", format::percent(share, 1, false), diff --git a/src/finance/market.rs b/src/finance/market.rs index 54a69f3..5557a8d 100644 --- a/src/finance/market.rs +++ b/src/finance/market.rs @@ -19,6 +19,7 @@ use super::{ use crate::{ charts::{Linear, Rect, measure}, theme::ActiveTheme, + typography::format::system_zone, }; /// How a market chart draws its prices. @@ -57,7 +58,7 @@ pub struct CandlestickChart { compare: Option<(SharedString, Rc>)>, sync: Option>, red_up: bool, - zone: TimeZone, + zone: Option, drawings: Vec, tool: Option, on_draw: Option, @@ -85,7 +86,7 @@ impl CandlestickChart { compare: None, sync: None, red_up: false, - zone: TimeZone::system(), + zone: None, drawings: Vec::new(), tool: None, on_draw: None, @@ -181,7 +182,7 @@ impl CandlestickChart { /// The time zone its times read in; the system's unless set. pub fn zone(mut self, zone: TimeZone) -> Self { - self.zone = zone; + self.zone = Some(zone); self } } @@ -194,6 +195,10 @@ impl Styled for CandlestickChart { impl RenderOnce for CandlestickChart { fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let zone = self + .zone + .clone() + .unwrap_or_else(|| system_zone("CandlestickChart")); let sync = self.sync.clone(); let stage: Entity = window.use_keyed_state((self.id.clone(), "stage"), cx, |_, cx| Stage { @@ -236,11 +241,18 @@ impl RenderOnce for CandlestickChart { { visible.pan((total - seen) as f64, total) } - Some(visible) => visible, + Some(visible) => visible.fitted(total), None => Visible::latest(total, f64::from(main.w / pixels(sizes.candle)).max(1.0)), }; if seen != total { + log::debug!("market chart: {seen} candles to {total}"); stage.update(cx, |stage, _| stage.seen = total); + if held.0.is_some() { + match &self.sync { + Some(sync) => sync.update(cx, |sync, _| sync.visible = Some(visible)), + None => stage.update(cx, |stage, _| stage.visible = Some(visible)), + } + } } let range = visible.range(total); let shown = Rc::new(if self.kind == ChartKind::HeikinAshi { @@ -362,7 +374,7 @@ impl RenderOnce for CandlestickChart { range: range.clone(), hover, row, - zone: self.zone.clone(), + zone: zone.clone(), title: self.title.clone(), compare: self.compare.as_ref().map(|(name, _)| name.clone()), drawings: marks.clone(), diff --git a/src/finance/renko.rs b/src/finance/renko.rs index a4a2c69..c671cba 100644 --- a/src/finance/renko.rs +++ b/src/finance/renko.rs @@ -17,7 +17,10 @@ use crate::{ ChartTooltip, Linear, Rect, Tiles, anchored, at, finish, measure, place, ring, tracked, }, theme::{ActiveTheme, TextSize}, - typography::{format, tabular}, + typography::{ + format::{self, system_zone}, + tabular, + }, }; /// A mark on a price-only chart: its slot, the prices it spans, whether it rose, and its words. @@ -221,7 +224,7 @@ pub struct RenkoChart { id: ElementId, candles: Rc>, size: f64, - zone: TimeZone, + zone: Option, red_up: bool, } @@ -236,7 +239,7 @@ impl RenkoChart { id: id.into(), candles: candles.into(), size, - zone: TimeZone::system(), + zone: None, red_up: false, } } @@ -248,7 +251,7 @@ impl RenkoChart { /// The time zone its dates read in; the system's unless set. pub fn zone(mut self, zone: TimeZone) -> Self { - self.zone = zone; + self.zone = Some(zone); self } } @@ -261,13 +264,17 @@ impl Styled for RenkoChart { impl RenderOnce for RenkoChart { fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let zone = self + .zone + .clone() + .unwrap_or_else(|| system_zone("RenkoChart")); let marks = renko(&self.candles, self.size) .into_iter() .enumerate() .map(|(slot, brick)| { let day = self.candles[brick.at] .time - .to_zoned(self.zone.clone()) + .to_zoned(zone.clone()) .strftime("%b %-d, %Y") .to_string(); let reading = format!( diff --git a/src/finance/stage.rs b/src/finance/stage.rs index 6e81241..ace0e57 100644 --- a/src/finance/stage.rs +++ b/src/finance/stage.rs @@ -26,6 +26,15 @@ impl Visible { .kept(total) } + /// Held to `total` candles after the data changed: no wider than they allow, and in reach of them. + pub(crate) fn fitted(self, total: usize) -> Self { + let count = self.count.clamp( + CLOSEST.min(total as f64).max(1.0), + (total as f64).max(1.0) * 1.1, + ); + Self { count, ..self }.kept(total) + } + /// Kept to what `total` candles allow: never before the first, at most a quarter window past the last. fn kept(self, total: usize) -> Self { let last = (total as f64 - self.count * 0.75).max(0.0); @@ -107,6 +116,21 @@ pub(crate) fn decimals(step: f64) -> usize { mod tests { use super::*; + #[test] + fn a_window_fits_data_that_shrank_under_it() { + let window = Visible { + start: 150.0, + count: 50.0, + }; + let fitted = window.fitted(12); + assert!(fitted.count <= 12.0 * 1.1); + let range = fitted.range(12); + assert!( + !range.is_empty() && range.end == 12, + "{range:?} shows the newest candles" + ); + } + #[test] fn a_window_maps_candles_across_its_frame() { let window = Visible { diff --git a/src/finance/tape.rs b/src/finance/tape.rs index 47b8b72..5963a39 100644 --- a/src/finance/tape.rs +++ b/src/finance/tape.rs @@ -9,12 +9,13 @@ use jiff::tz::TimeZone; use super::{ book::{Side, Trade, cell, heading, level}, quotes::{moves, price}, + stage::decimals, }; use crate::{ charts::compact, motion::Flash, theme::{ActiveTheme, Density, TextSize}, - typography::tabular, + typography::{format::system_zone, tabular}, }; /// Trades as they print, newest on top: the time, price and size tinted by the side that took them; trades of at least `large` stand bold, and the newest flashes in. @@ -25,7 +26,7 @@ pub struct TimeAndSales { large: f64, rows: usize, places: usize, - zone: TimeZone, + zone: Option, red_up: bool, } @@ -38,7 +39,7 @@ impl TimeAndSales { large: f64::MAX, rows: 12, places: 2, - zone: TimeZone::system(), + zone: None, red_up: false, } } @@ -55,7 +56,7 @@ impl TimeAndSales { } pub fn zone(mut self, zone: TimeZone) -> Self { - self.zone = zone; + self.zone = Some(zone); self } @@ -67,10 +68,13 @@ impl TimeAndSales { impl RenderOnce for TimeAndSales { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + let zone = self + .zone + .clone() + .unwrap_or_else(|| system_zone("TimeAndSales")); let (rise, fall) = moves(self.red_up, cx); let theme = cx.theme(); let colors = theme.colors.clone(); - let newest = self.trades.len(); let rows = self .trades .iter() @@ -81,7 +85,7 @@ impl RenderOnce for TimeAndSales { let ink = if trade.side == Side::Buy { rise } else { fall }; let time = trade .time - .to_zoned(self.zone.clone()) + .to_zoned(zone.clone()) .strftime("%H:%M:%S") .to_string(); let row = div() @@ -104,7 +108,7 @@ impl RenderOnce for TimeAndSales { .child(cell(price(trade.price, self.places), ink)) .child(cell(compact(trade.size), colors.fg)); if ix == 0 { - Flash::new((self.id.clone(), "newest"), newest) + Flash::new((self.id.clone(), "newest"), *trade) .child(row) .into_any_element() } else { @@ -192,6 +196,7 @@ fn rungs(last: f64, tick: f64, rows: usize) -> Vec { impl RenderOnce for DomLadder { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + let places = self.places.max(decimals(self.tick)); let (rise, fall) = moves(self.red_up, cx); let theme = cx.theme(); let colors = theme.colors.clone(); @@ -255,7 +260,7 @@ impl RenderOnce for DomLadder { .font_weight(FontWeight::SEMIBOLD) }) .when(!marked, |price| price.text_color(colors.fg_muted)) - .child(price(at, self.places)), + .child(price(at, places)), ) .child(side_cell(ask, Side::Sell, fall)) }); diff --git a/src/finance/wallet.rs b/src/finance/wallet.rs index 147b1dc..a332b4d 100644 --- a/src/finance/wallet.rs +++ b/src/finance/wallet.rs @@ -13,7 +13,11 @@ use crate::{ forms::{Combobox, NumberInput, TextInput}, primitives::{Icon, IconName}, theme::{ActiveTheme, IconSize, Radius, TextSize}, - typography::{Ellipsis, format, tabular}, + typography::{ + Ellipsis, + format::{self, system_zone}, + tabular, + }, }; type OnConvert = Rc; @@ -309,7 +313,7 @@ pub struct Transfer { #[derive(IntoElement)] pub struct TransactionList { transfers: Vec, - zone: TimeZone, + zone: Option, } impl TransactionList { @@ -318,13 +322,13 @@ impl TransactionList { transfers.sort_by_key(|transfer| Reverse(transfer.time)); Self { transfers, - zone: TimeZone::system(), + zone: None, } } /// The time zone its times read in; the system's unless set. pub fn zone(mut self, zone: TimeZone) -> Self { - self.zone = zone; + self.zone = Some(zone); self } } @@ -342,6 +346,10 @@ fn amount(value: f64, unit: &str) -> String { impl RenderOnce for TransactionList { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + let zone = self + .zone + .clone() + .unwrap_or_else(|| system_zone("TransactionList")); let theme = cx.theme(); let colors = theme.colors.clone(); div() @@ -397,7 +405,7 @@ impl RenderOnce for TransactionList { .child( transfer .time - .to_zoned(self.zone.clone()) + .to_zoned(zone.clone()) .strftime("%b %-d, %H:%M") .to_string(), ), diff --git a/src/forms/date/mod.rs b/src/forms/date/mod.rs index 785e557..188be98 100644 --- a/src/forms/date/mod.rs +++ b/src/forms/date/mod.rs @@ -11,9 +11,10 @@ mod zone; use jiff::{ Timestamp, Zoned, civil::{Date, Time}, - tz::TimeZone, }; +use crate::typography::format::system_zone; + pub use calendar::Calendar; pub use cron::CronEditor; pub use period::{MonthPicker, QuarterPicker, YearPicker}; @@ -26,10 +27,7 @@ pub use zone::TimezoneSelect; /// The system's clock in its own zone. An unknown zone stops here, not in UTC. pub(crate) fn zoned_now() -> Zoned { - let zone = TimeZone::try_system().unwrap_or_else(|error| { - panic!("the system time zone is unknown; pass a fixed date: {error}") - }); - Timestamp::now().to_zoned(zone) + Timestamp::now().to_zoned(system_zone("date pickers")) } /// A day as "Sep 25, 2026". diff --git a/src/forms/number.rs b/src/forms/number.rs index b3f23bf..b67b8fd 100644 --- a/src/forms/number.rs +++ b/src/forms/number.rs @@ -102,6 +102,7 @@ fn commit(state: &Entity, next: f64, window: &mut Window, cx: &mut App) ) }; let value = settle(next, min, max, precision); + state.update(cx, |numeric, _| numeric.value = value); show(&input, on_change.as_ref(), value, precision, window, cx); } @@ -112,19 +113,36 @@ fn numeric(id: &ElementId, window: &mut Window, cx: &mut App) -> Entity .filter(|ch| ch.is_ascii_digit() || matches!(ch, '.' | ',' | '-' | format::MINUS)) }); let events = cx.subscribe_in(&input, window, |numeric, input, event, window, cx| { - if !matches!(event, InputEvent::Blur | InputEvent::Submit) { - return; - } let (min, max, _, precision) = numeric.limits; - let value = settle(numeric.current(cx), min, max, precision); - show( - input, - numeric.on_change.as_ref(), - value, - precision, - window, - cx, - ); + match event { + InputEvent::Blur | InputEvent::Submit => { + let value = settle(numeric.current(cx), min, max, precision); + numeric.value = value; + show( + input, + numeric.on_change.as_ref(), + value, + precision, + window, + cx, + ); + } + InputEvent::Changed if input.read(cx).focus().is_focused(window) => { + let Some(typed) = parse(input.read(cx).text()) else { + return; + }; + let settled = typed == settle(typed, min, max, precision); + if typed == numeric.value || !settled { + return; + } + log::info!("number input: typed {typed}"); + numeric.value = typed; + if let Some(on_change) = numeric.on_change.clone() { + on_change(typed, window, cx); + } + } + _ => {} + } }); Numeric { input, diff --git a/src/forms/tests/mod.rs b/src/forms/tests/mod.rs index 9ad2638..ea497ba 100644 --- a/src/forms/tests/mod.rs +++ b/src/forms/tests/mod.rs @@ -183,6 +183,26 @@ fn stepping_starts_from_the_typed_number(cx: &mut TestAppContext) { assert_eq!(view.read_with(cx, |view, _| view.value), 21.0); } +#[gpui::test] +fn a_typed_number_reaches_the_owner_before_any_blur(cx: &mut TestAppContext) { + setup(cx); + let (view, cx) = cx.add_window_view(|_, _| Stepped { value: 100.0 }); + cx.update(|window, _| window.focus_next()); + cx.simulate_keystrokes("cmd-a"); + cx.simulate_input("250"); + assert_eq!( + view.read_with(cx, |view, _| view.value), + 250.0, + "a press elsewhere reads the typed number" + ); + cx.simulate_input("."); + assert_eq!( + view.read_with(cx, |view, _| view.value), + 250.0, + "a half-typed number waits" + ); +} + struct Pin { code: String, } diff --git a/src/typography/format.rs b/src/typography/format.rs index 7ebf0ee..b357dd9 100644 --- a/src/typography/format.rs +++ b/src/typography/format.rs @@ -173,6 +173,13 @@ pub fn relative(then: Timestamp, now: Timestamp) -> String { } } +/// The system's time zone for `user`. An unknown zone stops here rather than reading as UTC. +pub(crate) fn system_zone(user: &str) -> TimeZone { + TimeZone::try_system().unwrap_or_else(|error| { + panic!("{user}: the system time zone is unknown ({error}); pass a zone") + }) +} + /// strftime in `zone`; a bad pattern is an error. pub fn datetime(at: Timestamp, zone: &TimeZone, pattern: &str) -> Result { strtime::format(pattern, &at.to_zoned(zone.clone())) diff --git a/src/typography/formatted.rs b/src/typography/formatted.rs index ffb66c2..00c2246 100644 --- a/src/typography/formatted.rs +++ b/src/typography/formatted.rs @@ -229,9 +229,7 @@ impl RenderOnce for DateTimeText { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { let zone = match self.zone { Some(zone) => zone, - None => TimeZone::try_system().unwrap_or_else(|error| { - panic!("DateTimeText: system time zone unknown ({error}); pass .zone()") - }), + None => format::system_zone("DateTimeText"), }; let text = format::datetime(self.at, &zone, self.pattern) .unwrap_or_else(|error| panic!("DateTimeText pattern {:?}: {error}", self.pattern));