1use derive_more::Display;
3use ecolor::Color32;
4use egui::{Button, Key, RichText, Ui};
5use egui_remixicon::icons;
6use emath::{Align2, Pos2};
7use enum_iterator::Sequence;
8use epaint::{FontId, Stroke};
9use ftr_parser::types::Timescale;
10use itertools::Itertools;
11use num::{BigInt, BigRational, ToPrimitive, Zero};
12use pure_rust_locales::{Locale, locale_match};
13use serde::{Deserialize, Serialize};
14use std::sync::OnceLock;
15use sys_locale::get_locale;
16
17use crate::config::SurferConfig;
18use crate::viewport::Viewport;
19use crate::wave_data::WaveData;
20use crate::{
21 Message, SystemState,
22 translation::group_n_chars,
23 view::{DrawConfig, DrawingContext},
24};
25
26#[derive(Serialize, Deserialize, Clone)]
27pub struct TimeScale {
28 pub unit: TimeUnit,
29 pub multiplier: Option<u32>,
30}
31
32#[derive(Debug, Clone, Copy, Display, Eq, PartialEq, Serialize, Deserialize, Sequence)]
33pub enum TimeUnit {
34 #[display("zs")]
35 ZeptoSeconds,
36
37 #[display("as")]
38 AttoSeconds,
39
40 #[display("fs")]
41 FemtoSeconds,
42
43 #[display("ps")]
44 PicoSeconds,
45
46 #[display("ns")]
47 NanoSeconds,
48
49 #[display("μs")]
50 MicroSeconds,
51
52 #[display("ms")]
53 MilliSeconds,
54
55 #[display("s")]
56 Seconds,
57
58 #[display("No unit")]
59 None,
60
61 #[display("Auto")]
63 Auto,
64}
65
66pub const DEFAULT_TIMELINE_NAME: &str = "Time";
67const THIN_SPACE: &str = "\u{2009}";
68
69pub const TICK_STEPS: [f64; 8] = [1., 2., 2.5, 5., 10., 20., 25., 50.];
71
72struct LocaleFormatCache {
74 grouping: &'static [i64],
75 thousands_sep: String,
76 decimal_point: String,
77}
78
79static LOCALE_FORMAT_CACHE: OnceLock<LocaleFormatCache> = OnceLock::new();
80
81fn get_locale_format_cache() -> &'static LocaleFormatCache {
83 LOCALE_FORMAT_CACHE.get_or_init(|| {
84 let locale = get_locale()
85 .unwrap_or_else(|| "en-US".to_string())
86 .as_str()
87 .try_into()
88 .unwrap_or(Locale::en_US);
89 create_cache(locale)
90 })
91}
92
93fn create_cache(locale: Locale) -> LocaleFormatCache {
94 let grouping = locale_match!(locale => LC_NUMERIC::GROUPING);
95 let thousands_sep =
96 locale_match!(locale => LC_NUMERIC::THOUSANDS_SEP).replace('\u{202f}', THIN_SPACE);
97 let decimal_point = locale_match!(locale => LC_NUMERIC::DECIMAL_POINT).to_string();
98
99 LocaleFormatCache {
100 grouping,
101 thousands_sep,
102 decimal_point,
103 }
104}
105
106impl From<wellen::TimescaleUnit> for TimeUnit {
107 fn from(timescale: wellen::TimescaleUnit) -> Self {
108 match timescale {
109 wellen::TimescaleUnit::ZeptoSeconds => TimeUnit::ZeptoSeconds,
110 wellen::TimescaleUnit::AttoSeconds => TimeUnit::AttoSeconds,
111 wellen::TimescaleUnit::FemtoSeconds => TimeUnit::FemtoSeconds,
112 wellen::TimescaleUnit::PicoSeconds => TimeUnit::PicoSeconds,
113 wellen::TimescaleUnit::NanoSeconds => TimeUnit::NanoSeconds,
114 wellen::TimescaleUnit::MicroSeconds => TimeUnit::MicroSeconds,
115 wellen::TimescaleUnit::MilliSeconds => TimeUnit::MilliSeconds,
116 wellen::TimescaleUnit::Seconds => TimeUnit::Seconds,
117 wellen::TimescaleUnit::Unknown => TimeUnit::None,
118 }
119 }
120}
121
122impl From<ftr_parser::types::Timescale> for TimeUnit {
123 fn from(timescale: Timescale) -> Self {
124 match timescale {
125 Timescale::Fs => TimeUnit::FemtoSeconds,
126 Timescale::Ps => TimeUnit::PicoSeconds,
127 Timescale::Ns => TimeUnit::NanoSeconds,
128 Timescale::Us => TimeUnit::MicroSeconds,
129 Timescale::Ms => TimeUnit::MilliSeconds,
130 Timescale::S => TimeUnit::Seconds,
131 Timescale::Unit => TimeUnit::None,
132 Timescale::None => TimeUnit::None,
133 }
134 }
135}
136
137impl TimeUnit {
138 fn exponent(self) -> i8 {
140 match self {
141 TimeUnit::ZeptoSeconds => -21,
142 TimeUnit::AttoSeconds => -18,
143 TimeUnit::FemtoSeconds => -15,
144 TimeUnit::PicoSeconds => -12,
145 TimeUnit::NanoSeconds => -9,
146 TimeUnit::MicroSeconds => -6,
147 TimeUnit::MilliSeconds => -3,
148 TimeUnit::Seconds => 0,
149 TimeUnit::None => 0,
150 TimeUnit::Auto => 0,
151 }
152 }
153 fn from_exponent(exponent: i8) -> Option<Self> {
155 match exponent {
156 -21 => Some(TimeUnit::ZeptoSeconds),
157 -18 => Some(TimeUnit::AttoSeconds),
158 -15 => Some(TimeUnit::FemtoSeconds),
159 -12 => Some(TimeUnit::PicoSeconds),
160 -9 => Some(TimeUnit::NanoSeconds),
161 -6 => Some(TimeUnit::MicroSeconds),
162 -3 => Some(TimeUnit::MilliSeconds),
163 0 => Some(TimeUnit::Seconds),
164 _ => None,
165 }
166 }
167}
168
169pub fn timeunit_menu(ui: &mut Ui, msgs: &mut Vec<Message>, wanted_timeunit: &TimeUnit) {
171 for timeunit in enum_iterator::all::<TimeUnit>() {
172 if ui
173 .radio(*wanted_timeunit == timeunit, timeunit.to_string())
174 .clicked()
175 {
176 msgs.push(Message::SetTimeUnit(timeunit));
177 }
178 }
179}
180
181#[derive(Debug, Deserialize, Serialize, Clone)]
183pub struct TimeFormat {
184 format: TimeStringFormatting,
186 show_space: bool,
188 show_unit: bool,
190}
191
192impl Default for TimeFormat {
193 fn default() -> Self {
194 TimeFormat {
195 format: TimeStringFormatting::No,
196 show_space: true,
197 show_unit: true,
198 }
199 }
200}
201
202impl TimeFormat {
203 #[must_use]
205 pub fn new(format: TimeStringFormatting, show_space: bool, show_unit: bool) -> Self {
206 TimeFormat {
207 format,
208 show_space,
209 show_unit,
210 }
211 }
212
213 #[must_use]
215 pub fn with_format(mut self, format: TimeStringFormatting) -> Self {
216 self.format = format;
217 self
218 }
219
220 #[must_use]
222 pub fn with_space(mut self, show_space: bool) -> Self {
223 self.show_space = show_space;
224 self
225 }
226
227 #[must_use]
229 pub fn with_unit(mut self, show_unit: bool) -> Self {
230 self.show_unit = show_unit;
231 self
232 }
233}
234
235pub fn timeformat_menu(ui: &mut Ui, msgs: &mut Vec<Message>, current_timeformat: &TimeFormat) {
237 for time_string_format in enum_iterator::all::<TimeStringFormatting>() {
238 if ui
239 .radio(
240 current_timeformat.format == time_string_format,
241 if time_string_format == TimeStringFormatting::Locale {
242 format!(
243 "{time_string_format} ({locale})",
244 locale = get_locale().unwrap_or_else(|| "unknown".to_string())
245 )
246 } else {
247 time_string_format.to_string()
248 },
249 )
250 .clicked()
251 {
252 msgs.push(Message::SetTimeStringFormatting(Some(time_string_format)));
253 }
254 }
255}
256
257#[derive(Debug, Clone, Copy, Display, Eq, PartialEq, Serialize, Deserialize, Sequence)]
259pub enum TimeStringFormatting {
260 No,
262
263 Locale,
265
266 SI,
269}
270
271fn strip_trailing_zeros_and_period(time: String) -> String {
274 if !time.contains('.') {
275 return time;
276 }
277 time.trim_end_matches('0').trim_end_matches('.').to_string()
278}
279
280fn split_and_format_number(time: &str, format: TimeStringFormatting) -> String {
283 match format {
284 TimeStringFormatting::No => time.to_string(),
285 TimeStringFormatting::Locale => format_locale(time, get_locale_format_cache()),
286 TimeStringFormatting::SI => format_si(time),
287 }
288}
289
290fn format_si(time: &str) -> String {
291 if let Some((integer_part, fractional_part)) = time.split_once('.') {
292 let integer_result = if integer_part.len() > 4 {
293 group_n_chars(integer_part, 3).join(THIN_SPACE)
294 } else {
295 integer_part.to_string()
296 };
297 if fractional_part.len() > 4 {
298 let reversed = fractional_part.chars().rev().collect::<String>();
299 let reversed_fractional_parts = group_n_chars(&reversed, 3).join(THIN_SPACE);
300 let fractional_result = reversed_fractional_parts.chars().rev().collect::<String>();
301 format!("{integer_result}.{fractional_result}")
302 } else {
303 format!("{integer_result}.{fractional_part}")
304 }
305 } else if time.len() > 4 {
306 group_n_chars(time, 3).join(THIN_SPACE)
307 } else {
308 time.to_string()
309 }
310}
311
312fn format_locale(time: &str, cache: &LocaleFormatCache) -> String {
313 if cache.grouping[0] > 0 {
314 if let Some((integer_part, fractional_part)) = time.split_once('.') {
315 let integer_result = group_n_chars(integer_part, cache.grouping[0] as usize)
316 .join(cache.thousands_sep.as_str());
317 format!(
318 "{integer_result}{decimal_point}{fractional_part}",
319 decimal_point = &cache.decimal_point
320 )
321 } else {
322 group_n_chars(time, cache.grouping[0] as usize).join(cache.thousands_sep.as_str())
323 }
324 } else {
325 time.to_string()
326 }
327}
328
329fn find_auto_scale(time: &BigInt, timescale: &TimeScale) -> TimeUnit {
331 if matches!(timescale.unit, TimeUnit::Seconds) {
334 return TimeUnit::Seconds;
335 }
336 let multiplier_digits = timescale.multiplier.unwrap_or(1).ilog10();
337 let start_digits = -timescale.unit.exponent();
338 for e in (3..=start_digits).step_by(3).rev() {
339 if (time % pow10(e as u32 - multiplier_digits)).is_zero()
340 && let Some(unit) = TimeUnit::from_exponent(e - start_digits)
341 {
342 return unit;
343 }
344 }
345 timescale.unit
346}
347
348pub struct TimeFormatter {
351 timescale: TimeScale,
352 wanted_unit: TimeUnit,
353 time_format: TimeFormat,
354 exponent_diff: i8,
356 unit_string: String,
358 space_string: String,
360}
361
362impl TimeFormatter {
363 #[must_use]
365 pub(crate) fn new(
366 timescale: &TimeScale,
367 wanted_unit: &TimeUnit,
368 time_format: &TimeFormat,
369 ) -> Self {
370 let (exponent_diff, unit_string) = if *wanted_unit == TimeUnit::Auto {
372 (0i8, String::new())
374 } else {
375 let wanted_exponent = wanted_unit.exponent();
376 let data_exponent = timescale.unit.exponent();
377 let exponent_diff = wanted_exponent - data_exponent;
378
379 let unit_string = if time_format.show_unit {
380 wanted_unit.to_string()
381 } else {
382 String::new()
383 };
384
385 (exponent_diff, unit_string)
386 };
387
388 TimeFormatter {
389 timescale: timescale.clone(),
390 wanted_unit: *wanted_unit,
391 time_format: time_format.clone(),
392 exponent_diff,
393 unit_string,
394 space_string: if time_format.show_space {
395 " ".to_string()
396 } else {
397 String::new()
398 },
399 }
400 }
401
402 #[must_use]
404 pub(crate) fn format(&self, time: &BigInt) -> String {
405 if self.wanted_unit == TimeUnit::None {
406 return split_and_format_number(&time.to_string(), self.time_format.format);
407 }
408
409 let (exponent_diff, unit_string) = if self.wanted_unit == TimeUnit::Auto {
411 let auto_unit = find_auto_scale(time, &self.timescale);
412 let wanted_exponent = auto_unit.exponent();
413 let data_exponent = self.timescale.unit.exponent();
414 let exp_diff = wanted_exponent - data_exponent;
415
416 let unit_str = if self.time_format.show_unit {
417 auto_unit.to_string()
418 } else {
419 String::new()
420 };
421
422 (exp_diff, unit_str)
423 } else {
424 (self.exponent_diff, self.unit_string.clone())
425 };
426
427 let timestring = if exponent_diff >= 0 {
428 let precision = exponent_diff as usize;
429 strip_trailing_zeros_and_period(format!(
430 "{scaledtime:.precision$}",
431 scaledtime = BigRational::new(
432 time * self.timescale.multiplier.unwrap_or(1),
433 pow10(exponent_diff as u32)
434 )
435 .to_f64()
436 .unwrap_or(f64::NAN)
437 ))
438 } else {
439 (time * self.timescale.multiplier.unwrap_or(1) * pow10(-exponent_diff as u32))
440 .to_string()
441 };
442
443 format!(
444 "{scaledtime}{space}{unit}",
445 scaledtime = split_and_format_number(×tring, self.time_format.format),
446 space = if unit_string.is_empty() {
447 ""
448 } else {
449 &self.space_string
450 },
451 unit = &unit_string
452 )
453 }
454}
455
456fn pow10(exp: u32) -> BigInt {
459 match exp {
460 0 => BigInt::from(1),
461 1 => BigInt::from(10),
462 2 => BigInt::from(100),
463 3 => BigInt::from(1000),
464 6 => BigInt::from(1_000_000),
465 9 => BigInt::from(1_000_000_000),
466 12 => BigInt::from(1_000_000_000_000i64),
467 15 => BigInt::from(1_000_000_000_000_000i64),
468 18 => BigInt::from(1_000_000_000_000_000_000i64),
469 21 => BigInt::from(1_000_000_000_000_000_000_000i128),
470 _ => BigInt::from(10).pow(exp),
471 }
472}
473
474#[must_use]
477pub(crate) fn time_string(
478 time: &BigInt,
479 timescale: &TimeScale,
480 wanted_timeunit: &TimeUnit,
481 wanted_time_format: &TimeFormat,
482) -> String {
483 let formatter = TimeFormatter::new(timescale, wanted_timeunit, wanted_time_format);
484 formatter.format(time)
485}
486
487fn parse_time_input(input: &str) -> (String, Option<TimeUnit>) {
492 let sorted_units =
493 [
495 ("zs", TimeUnit::ZeptoSeconds),
496 ("as", TimeUnit::AttoSeconds),
497 ("fs", TimeUnit::FemtoSeconds),
498 ("ps", TimeUnit::PicoSeconds),
499 ("ns", TimeUnit::NanoSeconds),
500 ("μs", TimeUnit::MicroSeconds),
501 ("us", TimeUnit::MicroSeconds), ("ms", TimeUnit::MilliSeconds),
503 ("s", TimeUnit::Seconds),
504 ];
505
506 let trimmed = input.trim();
507
508 for (unit_str, unit) in sorted_units {
509 if trimmed.ends_with(unit_str) {
511 let after_number = trimmed.len() - unit_str.len();
512
513 if after_number > 0 {
515 let numeric = trimmed[..after_number].trim_end();
516 if let Some(last_char) = numeric.chars().next_back()
517 && (last_char.is_ascii_digit() || last_char == '.')
518 {
519 return (numeric.to_string(), Some(unit));
520 }
521 }
522 }
523 }
524
525 (trimmed.to_string(), None)
526}
527
528fn split_numeric_parts(numeric_str: &str) -> Result<(String, String), String> {
532 let trimmed = numeric_str.trim();
533 if trimmed.is_empty() {
534 return Err("Empty input".to_string());
535 }
536 if trimmed.starts_with('-') {
537 return Err("Negative numbers not supported".to_string());
538 }
539 let normalized = trimmed.strip_prefix('+').unwrap_or(trimmed);
540 let mut parts = normalized.split('.');
541 let integer_part = parts.next().unwrap_or("");
542 let fractional_part = parts.next().unwrap_or("");
543 if parts.next().is_some() {
544 return Err("Invalid number: multiple decimal points".to_string());
545 }
546
547 let all_valid =
549 (integer_part.chars().chain(fractional_part.chars())).all(|c| c.is_ascii_digit());
550 if !all_valid {
551 return Err(format!("Failed to parse '{numeric_str}' as number"));
552 }
553
554 let integer = if integer_part.is_empty() {
555 "0".to_string()
556 } else {
557 integer_part.to_string()
558 };
559 Ok((integer, fractional_part.to_string()))
560}
561
562fn normalize_numeric_with_unit(
567 numeric_str: &str,
568 unit: TimeUnit,
569) -> Result<(BigInt, TimeUnit), String> {
570 let (integer_part, mut fractional_part) = split_numeric_parts(numeric_str)?;
571
572 while fractional_part.ends_with('0') {
574 fractional_part.pop();
575 }
576
577 if fractional_part.is_empty() {
578 let value =
579 BigInt::parse_bytes(integer_part.as_bytes(), 10).unwrap_or_else(|| BigInt::from(0));
580 return Ok((value, unit));
581 }
582
583 let fractional_len = fractional_part.len();
584 if fractional_len > 21 {
586 return Err("Too many decimal places (max 21 supported)".to_string());
587 }
588
589 let steps = fractional_len.div_ceil(3) as i8; let new_exponent = unit.exponent() - (steps * 3);
591 let new_unit = TimeUnit::from_exponent(new_exponent)
592 .ok_or_else(|| "Too much precision for available time units".to_string())?;
593
594 let mut combined = integer_part;
595 combined.push_str(&fractional_part);
596 let mut value = BigInt::parse_bytes(combined.as_bytes(), 10).unwrap_or_else(|| BigInt::from(0));
597
598 let extra_zeros = (steps as usize * 3).saturating_sub(fractional_len);
599 if extra_zeros > 0 {
600 let scale = pow10(extra_zeros as u32);
601 value *= scale;
602 }
603
604 Ok((value, new_unit))
605}
606
607#[derive(Clone, Debug)]
611pub(crate) struct TimeInputState {
612 input_text: String,
614 parsed_value: Option<BigInt>,
616 input_unit: Option<TimeUnit>,
618 normalized_unit: Option<TimeUnit>,
620 selected_unit: TimeUnit,
622 error: Option<String>,
624}
625
626impl Default for TimeInputState {
627 fn default() -> Self {
628 Self {
629 input_text: String::new(),
630 parsed_value: None,
631 input_unit: None,
632 normalized_unit: None,
633 selected_unit: TimeUnit::NanoSeconds,
634 error: None,
635 }
636 }
637}
638
639impl TimeInputState {
640 #[cfg(test)]
642 pub(crate) fn new() -> Self {
643 Self::default()
644 }
645
646 fn update_input(&mut self, input: String) {
648 self.input_text = input;
649 let (numeric_str, unit) = parse_time_input(&self.input_text);
650 self.input_unit = unit;
651
652 let base_unit = self.input_unit.unwrap_or(self.selected_unit);
653 match normalize_numeric_with_unit(&numeric_str, base_unit) {
654 Ok((val, normalized_unit)) => {
655 self.parsed_value = Some(val);
656 self.normalized_unit = Some(normalized_unit);
657 self.error = None;
658 }
659 Err(e) => {
660 self.parsed_value = None;
661 self.normalized_unit = None;
662 self.error = Some(e);
663 }
664 }
665 }
666
667 fn effective_unit(&self) -> TimeUnit {
669 self.normalized_unit
670 .or(self.input_unit)
671 .unwrap_or(self.selected_unit)
672 }
673
674 fn to_timescale_ticks(&self, timescale: &TimeScale) -> Option<BigInt> {
678 let value = self.parsed_value.clone()?;
679 let unit = self.effective_unit();
680 let base_unit = if unit == TimeUnit::None {
681 timescale.unit
682 } else {
683 unit
684 };
685 let unit_exp = base_unit.exponent();
686 let data_exp = timescale.unit.exponent();
687 let diff = unit_exp - data_exp;
688
689 let mut result = value;
690 if diff > 0 {
691 let scale = pow10(diff as u32);
692 result *= scale;
693 } else if diff < 0 {
694 let scale = pow10((-diff) as u32);
695 result /= scale;
696 }
697
698 let multiplier = timescale.multiplier.unwrap_or(1);
699 if multiplier != 1 {
700 let mult = BigInt::from(multiplier);
701 result /= mult;
702 }
703
704 Some(result)
705 }
706}
707
708pub(crate) fn time_input_widget(
723 ui: &mut Ui,
724 waves: &WaveData,
725 msgs: &mut Vec<Message>,
726 state: &mut TimeInputState,
727 request_focus: bool,
728) {
729 ui.horizontal(|ui| {
730 let mut input = state.input_text.clone();
732 let text_response = ui.add(
733 egui::TextEdit::singleline(&mut input)
734 .desired_width(100.0)
735 .hint_text("e.g., 1.5ms"),
736 );
737
738 if request_focus && !text_response.has_focus() {
739 text_response.request_focus();
740 msgs.push(Message::SetRequestTimeEditFocus(false));
741 }
742
743 if text_response.changed() {
744 state.update_input(input);
745 }
746
747 let dropdown_enabled = state.input_unit.is_none();
749
750 if dropdown_enabled {
751 egui::ComboBox::new("Unit", "")
752 .width(32.0)
753 .selected_text(state.selected_unit.to_string())
754 .show_ui(ui, |ui| {
755 for unit in enum_iterator::all::<TimeUnit>() {
756 if !matches!(unit, TimeUnit::Auto | TimeUnit::None) {
758 ui.selectable_value(&mut state.selected_unit, unit, unit.to_string());
759 }
760 }
761 });
762 }
763
764 if text_response.gained_focus() {
766 msgs.push(Message::SetTimeEditFocused(true));
767 }
768 if text_response.lost_focus() {
769 if text_response.ctx.input(|i| i.key_pressed(Key::Enter)) {
770 if let Some(time_stamp) =
772 state.to_timescale_ticks(&waves.inner.metadata().timescale)
773 {
774 msgs.push(Message::GoToTime(Some(time_stamp), 0));
775 }
776 }
777 msgs.push(Message::SetTimeEditFocused(false));
778 }
779
780 let button_enabled = state.parsed_value.is_some();
782 let goto_button = Button::new(RichText::new(icons::TARGET_FILL).heading()).frame(false);
783 if ui
784 .add_enabled(button_enabled, goto_button)
785 .on_hover_text("Go to time")
786 .clicked()
787 && let Some(time_stamp) = state.to_timescale_ticks(&waves.inner.metadata().timescale)
788 {
789 msgs.push(Message::GoToTime(Some(time_stamp), 0));
790 }
791 let cursor_button = Button::new(RichText::new(icons::CURSOR_FILL).heading()).frame(false);
792 if ui
793 .add_enabled(button_enabled, cursor_button)
794 .on_hover_text("Set cursor at time")
795 .clicked()
796 && let Some(time_stamp) = state.to_timescale_ticks(&waves.inner.metadata().timescale)
797 {
798 msgs.push(Message::CursorSet(time_stamp));
799 }
800 });
801}
802
803impl WaveData {
804 pub(crate) fn draw_tick_line(&self, x: f32, ctx: &mut DrawingContext, stroke: &Stroke) {
805 let Pos2 {
806 x: x_pos,
807 y: y_start,
808 } = (ctx.to_screen)(x, 0.);
809 ctx.painter.vline(
810 x_pos,
811 (y_start)..=(y_start + ctx.cfg.canvas_size.y),
812 *stroke,
813 );
814 }
815 pub(crate) fn draw_ticks(
817 &self,
818 color: Color32,
819 ticks: &[(String, f32, i64)],
820 ctx: &DrawingContext<'_>,
821 y_offset: f32,
822 align: Align2,
823 ) {
824 for (tick_text, x, _) in ticks {
825 ctx.painter.text(
826 (ctx.to_screen)(*x, y_offset),
827 align,
828 tick_text,
829 FontId::proportional(ctx.cfg.text_size),
830 color,
831 );
832 }
833 }
834
835 pub fn draw_divider_text(
837 &self,
838 color: Option<Color32>,
839 text: String,
840 ticks: &[(String, f32, i64)],
841 ctx: &DrawingContext<'_>,
842 y_offset: f32,
843 config: &SurferConfig,
844 ) {
845 let font = FontId::monospace(ctx.cfg.text_size);
846 let color = color.unwrap_or(config.theme.foreground);
847
848 let layout = ctx
849 .painter
850 .layout_no_wrap(text.clone(), font.clone(), color);
851 let text_width = layout.rect.width() + (font.size * 2.);
852
853 let (next_tick, next_stamp) = ticks
854 .get(1)
855 .map_or_else(|| (1., 1), |&(_, dist, stamp)| (dist, stamp));
856
857 let (first_tick, first_stamp) = ticks
858 .first()
859 .map_or_else(|| (0., 0), |&(_, dist, stamp)| (dist, stamp));
860
861 let tick_delta = (next_tick - first_tick).abs();
862 let stamp_delta = next_stamp - first_stamp;
863 let tick_stride = (text_width / tick_delta).ceil();
864 let stamp_stride = stamp_delta * tick_stride as i64;
865 let elapsed = first_stamp / stamp_stride;
866 let mut last_stamp = (elapsed * stamp_stride) - (stamp_stride / 2);
867
868 for (_, x, stamp) in ticks {
869 if (*stamp < last_stamp + stamp_stride) || *stamp < 0 {
870 continue;
871 }
872 last_stamp = *stamp;
873
874 ctx.painter.text(
875 (ctx.to_screen)(*x, y_offset),
876 Align2::CENTER_TOP,
877 text.clone(),
878 font.clone(),
879 color,
880 );
881 }
882 }
883}
884
885impl SystemState {
886 pub(crate) fn get_time_format(&self) -> TimeFormat {
887 let time_format = self.user.config.default_time_format.clone();
888 if let Some(time_string_format) = self.user.time_string_format {
889 time_format.with_format(time_string_format)
890 } else {
891 time_format
892 }
893 }
894
895 pub(crate) fn get_ticks_for_viewport_idx(
896 &self,
897 waves: &WaveData,
898 viewport_idx: usize,
899 cfg: &DrawConfig,
900 ) -> Vec<(String, f32, i64)> {
901 self.get_ticks_for_viewport(waves, &waves.viewports[viewport_idx], cfg)
902 }
903
904 pub(crate) fn get_ticks_for_viewport(
905 &self,
906 waves: &WaveData,
907 viewport: &Viewport,
908 cfg: &DrawConfig,
909 ) -> Vec<(String, f32, i64)> {
910 get_ticks_internal(
911 viewport,
912 &waves.inner.metadata().timescale,
913 cfg.canvas_size.x,
914 cfg.text_size,
915 &self.user.wanted_timeunit,
916 &self.get_time_format(),
917 self.user.config.theme.ticks.density,
918 &waves.safe_num_timestamps(),
919 )
920 }
921}
922
923#[allow(clippy::too_many_arguments)]
927#[must_use]
928fn get_ticks_internal(
929 viewport: &Viewport,
930 timescale: &TimeScale,
931 frame_width: f32,
932 text_size: f32,
933 wanted_timeunit: &TimeUnit,
934 time_format: &TimeFormat,
935 density: f32,
936 num_timestamps: &BigInt,
937) -> Vec<(String, f32, i64)> {
938 let char_width = text_size * (20. / 31.);
939 let rightexp = viewport
940 .curr_right
941 .absolute(num_timestamps)
942 .inner()
943 .abs()
944 .log10()
945 .round() as i16;
946 let leftexp = viewport
947 .curr_left
948 .absolute(num_timestamps)
949 .inner()
950 .abs()
951 .log10()
952 .round() as i16;
953 let max_labelwidth = f32::from(rightexp.max(leftexp) + 3) * char_width;
954 let max_labels = ((frame_width * density) / max_labelwidth).floor() + 2.;
955 let scale = 10.0f64.powf(
956 ((viewport.curr_right - viewport.curr_left)
957 .absolute(num_timestamps)
958 .inner()
959 / f64::from(max_labels))
960 .log10()
961 .floor(),
962 );
963
964 let mut ticks: Vec<(String, f32, i64)> = [].to_vec();
965 for step in &TICK_STEPS {
966 let scaled_step = scale * step;
967 let rounded_min_label_time =
968 (viewport.curr_left.absolute(num_timestamps).inner() / scaled_step).floor()
969 * scaled_step;
970 let high = ((viewport.curr_right.absolute(num_timestamps).inner() - rounded_min_label_time)
971 / scaled_step)
972 .ceil() as f32
973 + 1.;
974 if high <= max_labels {
975 let time_formatter = TimeFormatter::new(timescale, wanted_timeunit, time_format);
976 ticks = (0..high as i16)
977 .map(|v| {
978 BigInt::from((f64::from(v) * scaled_step + rounded_min_label_time) as i128)
979 })
980 .unique()
981 .map(|tick| {
982 (
983 time_formatter.format(&tick),
985 viewport.pixel_from_time(&tick, frame_width, num_timestamps),
987 tick.to_i64().unwrap_or_default(),
989 )
990 })
991 .collect::<Vec<(String, f32, i64)>>();
992 break;
993 }
994 }
995 ticks
996}
997
998pub(crate) fn parse_time_string_to_ticks(input: &str, timescale: &TimeScale) -> Option<BigInt> {
1005 let (numeric_str, unit_opt) = parse_time_input(input);
1006
1007 let base_unit = unit_opt.unwrap_or(TimeUnit::None);
1008 let (value, normalized_unit) = normalize_numeric_with_unit(&numeric_str, base_unit).ok()?;
1009
1010 if normalized_unit == TimeUnit::None {
1011 return Some(value);
1013 }
1014
1015 let unit_exp = normalized_unit.exponent();
1016 let data_exp = timescale.unit.exponent();
1017 let diff = unit_exp - data_exp;
1018
1019 let mut result = value;
1020 if diff > 0 {
1021 let scale = pow10(diff as u32);
1022 result *= scale;
1023 } else if diff < 0 {
1024 let scale = pow10((-diff) as u32);
1025 result /= scale;
1026 }
1027
1028 let multiplier = timescale.multiplier.unwrap_or(1);
1029 if multiplier != 1 {
1030 result /= BigInt::from(multiplier);
1031 }
1032
1033 Some(result)
1034}
1035
1036#[cfg(test)]
1037mod test {
1038 use num::BigInt;
1039
1040 use crate::time::{TimeFormat, TimeScale, TimeStringFormatting, TimeUnit, time_string};
1041
1042 #[test]
1043 fn print_time_standard() {
1044 assert_eq!(
1045 time_string(
1046 &BigInt::from(103),
1047 &TimeScale {
1048 multiplier: Some(1),
1049 unit: TimeUnit::FemtoSeconds
1050 },
1051 &TimeUnit::FemtoSeconds,
1052 &TimeFormat::default()
1053 ),
1054 "103 fs"
1055 );
1056 assert_eq!(
1057 time_string(
1058 &BigInt::from(2200),
1059 &TimeScale {
1060 multiplier: Some(1),
1061 unit: TimeUnit::MicroSeconds
1062 },
1063 &TimeUnit::MicroSeconds,
1064 &TimeFormat::default()
1065 ),
1066 "2200 μs"
1067 );
1068 assert_eq!(
1069 time_string(
1070 &BigInt::from(2200),
1071 &TimeScale {
1072 multiplier: Some(1),
1073 unit: TimeUnit::MicroSeconds
1074 },
1075 &TimeUnit::MilliSeconds,
1076 &TimeFormat::default()
1077 ),
1078 "2.2 ms"
1079 );
1080 assert_eq!(
1081 time_string(
1082 &BigInt::from(2200),
1083 &TimeScale {
1084 multiplier: Some(1),
1085 unit: TimeUnit::MicroSeconds
1086 },
1087 &TimeUnit::NanoSeconds,
1088 &TimeFormat::default()
1089 ),
1090 "2200000 ns"
1091 );
1092 assert_eq!(
1093 time_string(
1094 &BigInt::from(2200),
1095 &TimeScale {
1096 multiplier: Some(1),
1097 unit: TimeUnit::NanoSeconds
1098 },
1099 &TimeUnit::PicoSeconds,
1100 &TimeFormat {
1101 format: TimeStringFormatting::No,
1102 show_space: false,
1103 show_unit: true
1104 }
1105 ),
1106 "2200000ps"
1107 );
1108 assert_eq!(
1109 time_string(
1110 &BigInt::from(2200),
1111 &TimeScale {
1112 multiplier: Some(10),
1113 unit: TimeUnit::MicroSeconds
1114 },
1115 &TimeUnit::MicroSeconds,
1116 &TimeFormat {
1117 format: TimeStringFormatting::No,
1118 show_space: false,
1119 show_unit: false
1120 }
1121 ),
1122 "22000"
1123 );
1124 }
1125 #[test]
1126 fn print_time_si() {
1127 assert_eq!(
1128 time_string(
1129 &BigInt::from(123456789010i128),
1130 &TimeScale {
1131 multiplier: Some(1),
1132 unit: TimeUnit::MicroSeconds
1133 },
1134 &TimeUnit::Seconds,
1135 &TimeFormat {
1136 format: TimeStringFormatting::SI,
1137 show_space: true,
1138 show_unit: true
1139 }
1140 ),
1141 "123\u{2009}456.789\u{2009}01 s"
1142 );
1143 assert_eq!(
1144 time_string(
1145 &BigInt::from(1456789100i128),
1146 &TimeScale {
1147 multiplier: Some(1),
1148 unit: TimeUnit::MicroSeconds
1149 },
1150 &TimeUnit::Seconds,
1151 &TimeFormat {
1152 format: TimeStringFormatting::SI,
1153 show_space: true,
1154 show_unit: true
1155 }
1156 ),
1157 "1456.7891 s"
1158 );
1159 assert_eq!(
1160 time_string(
1161 &BigInt::from(2200),
1162 &TimeScale {
1163 multiplier: Some(1),
1164 unit: TimeUnit::MicroSeconds
1165 },
1166 &TimeUnit::MicroSeconds,
1167 &TimeFormat {
1168 format: TimeStringFormatting::SI,
1169 show_space: true,
1170 show_unit: true
1171 }
1172 ),
1173 "2200 μs"
1174 );
1175 assert_eq!(
1176 time_string(
1177 &BigInt::from(22200),
1178 &TimeScale {
1179 multiplier: Some(1),
1180 unit: TimeUnit::MicroSeconds
1181 },
1182 &TimeUnit::MicroSeconds,
1183 &TimeFormat {
1184 format: TimeStringFormatting::SI,
1185 show_space: true,
1186 show_unit: true
1187 }
1188 ),
1189 "22\u{2009}200 μs"
1190 );
1191 }
1192 #[test]
1193 fn print_time_auto() {
1194 assert_eq!(
1195 time_string(
1196 &BigInt::from(2200),
1197 &TimeScale {
1198 multiplier: Some(1),
1199 unit: TimeUnit::MicroSeconds
1200 },
1201 &TimeUnit::Auto,
1202 &TimeFormat {
1203 format: TimeStringFormatting::SI,
1204 show_space: true,
1205 show_unit: true
1206 }
1207 ),
1208 "2200 μs"
1209 );
1210 assert_eq!(
1211 time_string(
1212 &BigInt::from(22000),
1213 &TimeScale {
1214 multiplier: Some(1),
1215 unit: TimeUnit::MicroSeconds
1216 },
1217 &TimeUnit::Auto,
1218 &TimeFormat {
1219 format: TimeStringFormatting::SI,
1220 show_space: true,
1221 show_unit: true
1222 }
1223 ),
1224 "22 ms"
1225 );
1226 assert_eq!(
1227 time_string(
1228 &BigInt::from(1500000000),
1229 &TimeScale {
1230 multiplier: Some(1),
1231 unit: TimeUnit::PicoSeconds
1232 },
1233 &TimeUnit::Auto,
1234 &TimeFormat {
1235 format: TimeStringFormatting::SI,
1236 show_space: true,
1237 show_unit: true
1238 }
1239 ),
1240 "1500 μs"
1241 );
1242 assert_eq!(
1243 time_string(
1244 &BigInt::from(22000),
1245 &TimeScale {
1246 multiplier: Some(10),
1247 unit: TimeUnit::MicroSeconds
1248 },
1249 &TimeUnit::Auto,
1250 &TimeFormat {
1251 format: TimeStringFormatting::SI,
1252 show_space: true,
1253 show_unit: true
1254 }
1255 ),
1256 "220 ms"
1257 );
1258 assert_eq!(
1259 time_string(
1260 &BigInt::from(220000),
1261 &TimeScale {
1262 multiplier: Some(100),
1263 unit: TimeUnit::MicroSeconds
1264 },
1265 &TimeUnit::Auto,
1266 &TimeFormat {
1267 format: TimeStringFormatting::SI,
1268 show_space: true,
1269 show_unit: true
1270 }
1271 ),
1272 "22 s"
1273 );
1274 assert_eq!(
1275 time_string(
1276 &BigInt::from(22000),
1277 &TimeScale {
1278 multiplier: Some(10),
1279 unit: TimeUnit::Seconds
1280 },
1281 &TimeUnit::Auto,
1282 &TimeFormat {
1283 format: TimeStringFormatting::No,
1284 show_space: true,
1285 show_unit: true
1286 }
1287 ),
1288 "220000 s"
1289 );
1290 }
1291 #[test]
1292 fn print_time_none() {
1293 assert_eq!(
1294 time_string(
1295 &BigInt::from(2200),
1296 &TimeScale {
1297 multiplier: Some(1),
1298 unit: TimeUnit::MicroSeconds
1299 },
1300 &TimeUnit::None,
1301 &TimeFormat {
1302 format: TimeStringFormatting::No,
1303 show_space: true,
1304 show_unit: true
1305 }
1306 ),
1307 "2200"
1308 );
1309 assert_eq!(
1310 time_string(
1311 &BigInt::from(220),
1312 &TimeScale {
1313 multiplier: Some(10),
1314 unit: TimeUnit::MicroSeconds
1315 },
1316 &TimeUnit::None,
1317 &TimeFormat {
1318 format: TimeStringFormatting::No,
1319 show_space: true,
1320 show_unit: true
1321 }
1322 ),
1323 "220"
1324 );
1325 }
1326
1327 #[test]
1328 fn test_strip_trailing_zeros_and_period() {
1329 use crate::time::strip_trailing_zeros_and_period;
1330
1331 assert_eq!(strip_trailing_zeros_and_period("123.000".into()), "123");
1332 assert_eq!(strip_trailing_zeros_and_period("123.450".into()), "123.45");
1333 assert_eq!(strip_trailing_zeros_and_period("123.456".into()), "123.456");
1334 assert_eq!(strip_trailing_zeros_and_period("123.".into()), "123");
1335 assert_eq!(strip_trailing_zeros_and_period("123".into()), "123");
1336 assert_eq!(strip_trailing_zeros_and_period("0.000".into()), "0");
1337 assert_eq!(strip_trailing_zeros_and_period("0.100".into()), "0.1");
1338 assert_eq!(strip_trailing_zeros_and_period(String::new()), "");
1339 }
1340
1341 #[test]
1342 fn test_format_si() {
1343 use crate::time::format_si;
1344
1345 assert_eq!(format_si("1234.56"), "1234.56");
1347 assert_eq!(format_si("123.4"), "123.4");
1348
1349 assert_eq!(format_si("12345.67"), "12\u{2009}345.67");
1351 assert_eq!(format_si("1234567.89"), "1\u{2009}234\u{2009}567.89");
1352 assert_eq!(format_si("12345"), "12\u{2009}345");
1354 assert_eq!(format_si("123"), "123");
1355
1356 assert_eq!(format_si("0.123"), "0.123");
1358 assert_eq!(format_si(""), "");
1359
1360 assert_eq!(format_si("123.4567890"), "123.456\u{2009}789\u{2009}0");
1362 }
1363
1364 #[test]
1365 fn test_time_unit_exponent() {
1366 assert_eq!(TimeUnit::Seconds.exponent(), 0);
1368 assert_eq!(TimeUnit::MilliSeconds.exponent(), -3);
1369 assert_eq!(TimeUnit::MicroSeconds.exponent(), -6);
1370 assert_eq!(TimeUnit::NanoSeconds.exponent(), -9);
1371 assert_eq!(TimeUnit::PicoSeconds.exponent(), -12);
1372 assert_eq!(TimeUnit::FemtoSeconds.exponent(), -15);
1373 assert_eq!(TimeUnit::AttoSeconds.exponent(), -18);
1374 assert_eq!(TimeUnit::ZeptoSeconds.exponent(), -21);
1375
1376 for unit in [
1378 TimeUnit::Seconds,
1379 TimeUnit::MilliSeconds,
1380 TimeUnit::MicroSeconds,
1381 TimeUnit::NanoSeconds,
1382 TimeUnit::PicoSeconds,
1383 TimeUnit::FemtoSeconds,
1384 TimeUnit::AttoSeconds,
1385 TimeUnit::ZeptoSeconds,
1386 ] {
1387 assert_eq!(TimeUnit::from_exponent(unit.exponent()), Some(unit));
1388 }
1389
1390 assert_eq!(TimeUnit::from_exponent(-5), None);
1392 assert_eq!(TimeUnit::from_exponent(1), None);
1393 }
1394
1395 #[test]
1396 fn test_time_string_zero() {
1397 assert_eq!(
1399 time_string(
1400 &BigInt::from(0),
1401 &TimeScale {
1402 multiplier: Some(1),
1403 unit: TimeUnit::MicroSeconds
1404 },
1405 &TimeUnit::MicroSeconds,
1406 &TimeFormat::default()
1407 ),
1408 "0 μs"
1409 );
1410
1411 assert_eq!(
1412 time_string(
1413 &BigInt::from(0),
1414 &TimeScale {
1415 multiplier: Some(1),
1416 unit: TimeUnit::Seconds
1417 },
1418 &TimeUnit::Auto,
1419 &TimeFormat::default()
1420 ),
1421 "0 s"
1422 );
1423 }
1424
1425 #[test]
1426 fn test_time_string_large_numbers() {
1427 assert_eq!(
1429 time_string(
1430 &BigInt::from(999_999_999_999i64),
1431 &TimeScale {
1432 multiplier: Some(1),
1433 unit: TimeUnit::NanoSeconds
1434 },
1435 &TimeUnit::Seconds,
1436 &TimeFormat {
1437 format: TimeStringFormatting::SI,
1438 show_space: true,
1439 show_unit: true
1440 }
1441 ),
1442 "999.999\u{2009}999\u{2009}999 s"
1443 );
1444 }
1445
1446 #[test]
1447 fn test_time_string_no_multiplier() {
1448 assert_eq!(
1450 time_string(
1451 &BigInt::from(1234),
1452 &TimeScale {
1453 multiplier: None,
1454 unit: TimeUnit::NanoSeconds
1455 },
1456 &TimeUnit::NanoSeconds,
1457 &TimeFormat::default()
1458 ),
1459 "1234 ns"
1460 );
1461 }
1462
1463 #[test]
1464 fn test_time_format_variations() {
1465 let value = BigInt::from(123456);
1466 let scale = TimeScale {
1467 multiplier: Some(1),
1468 unit: TimeUnit::NanoSeconds,
1469 };
1470
1471 assert_eq!(
1473 time_string(
1474 &value,
1475 &scale,
1476 &TimeUnit::NanoSeconds,
1477 &TimeFormat {
1478 format: TimeStringFormatting::No,
1479 show_space: true,
1480 show_unit: true
1481 }
1482 ),
1483 "123456 ns"
1484 );
1485
1486 assert_eq!(
1487 time_string(
1488 &value,
1489 &scale,
1490 &TimeUnit::NanoSeconds,
1491 &TimeFormat {
1492 format: TimeStringFormatting::No,
1493 show_space: false,
1494 show_unit: true
1495 }
1496 ),
1497 "123456ns"
1498 );
1499
1500 assert_eq!(
1501 time_string(
1502 &value,
1503 &scale,
1504 &TimeUnit::NanoSeconds,
1505 &TimeFormat {
1506 format: TimeStringFormatting::No,
1507 show_space: true,
1508 show_unit: false
1509 }
1510 ),
1511 "123456"
1512 );
1513
1514 assert_eq!(
1515 time_string(
1516 &value,
1517 &scale,
1518 &TimeUnit::NanoSeconds,
1519 &TimeFormat {
1520 format: TimeStringFormatting::SI,
1521 show_space: true,
1522 show_unit: true
1523 }
1524 ),
1525 "123\u{2009}456 ns"
1526 );
1527 }
1528
1529 #[test]
1530 fn test_find_auto_scale_seconds_passthrough() {
1531 use crate::time::find_auto_scale;
1532
1533 let ts = TimeScale {
1534 unit: TimeUnit::Seconds,
1535 multiplier: Some(1),
1536 };
1537 assert_eq!(find_auto_scale(&BigInt::from(1), &ts), TimeUnit::Seconds);
1538 assert_eq!(
1539 find_auto_scale(&BigInt::from(1_234_567), &ts),
1540 TimeUnit::Seconds
1541 );
1542 }
1543
1544 #[test]
1545 fn test_find_auto_scale_nanoseconds() {
1546 use crate::time::find_auto_scale;
1547
1548 let ts = TimeScale {
1549 unit: TimeUnit::NanoSeconds,
1550 multiplier: Some(1),
1551 };
1552
1553 assert_eq!(
1555 find_auto_scale(&BigInt::from(1_000_000_000i64), &ts),
1556 TimeUnit::Seconds
1557 );
1558 assert_eq!(
1560 find_auto_scale(&BigInt::from(1_000_000), &ts),
1561 TimeUnit::MilliSeconds
1562 );
1563 assert_eq!(
1565 find_auto_scale(&BigInt::from(1_000), &ts),
1566 TimeUnit::MicroSeconds
1567 );
1568 assert_eq!(
1570 find_auto_scale(&BigInt::from(1234), &ts),
1571 TimeUnit::NanoSeconds
1572 );
1573 }
1574
1575 #[test]
1576 fn test_find_auto_scale_microseconds_with_multiplier() {
1577 use crate::time::find_auto_scale;
1578
1579 let ts_none = TimeScale {
1581 unit: TimeUnit::MicroSeconds,
1582 multiplier: None,
1583 };
1584 assert_eq!(
1585 find_auto_scale(&BigInt::from(1_000_000), &ts_none),
1586 TimeUnit::Seconds
1587 );
1588 assert_eq!(
1589 find_auto_scale(&BigInt::from(1_000), &ts_none),
1590 TimeUnit::MilliSeconds
1591 );
1592 assert_eq!(
1593 find_auto_scale(&BigInt::from(123), &ts_none),
1594 TimeUnit::MicroSeconds
1595 );
1596
1597 let ts_mul10 = TimeScale {
1599 unit: TimeUnit::MicroSeconds,
1600 multiplier: Some(10),
1601 };
1602 assert_eq!(
1603 find_auto_scale(&BigInt::from(100_000), &ts_mul10),
1604 TimeUnit::Seconds
1605 );
1606 assert_eq!(
1607 find_auto_scale(&BigInt::from(100), &ts_mul10),
1608 TimeUnit::MilliSeconds
1609 );
1610 assert_eq!(
1611 find_auto_scale(&BigInt::from(123), &ts_mul10),
1612 TimeUnit::MicroSeconds
1613 );
1614 }
1615
1616 #[test]
1617 fn test_find_auto_scale_femtoseconds() {
1618 use crate::time::find_auto_scale;
1619
1620 let ts = TimeScale {
1621 unit: TimeUnit::FemtoSeconds,
1622 multiplier: Some(1),
1623 };
1624 assert_eq!(
1626 find_auto_scale(&BigInt::from(10_i128.pow(15)), &ts),
1627 TimeUnit::Seconds
1628 );
1629 assert_eq!(
1631 find_auto_scale(&BigInt::from(10_i128.pow(12)), &ts),
1632 TimeUnit::MilliSeconds
1633 );
1634 assert_eq!(
1636 find_auto_scale(&BigInt::from(10_i128.pow(9)), &ts),
1637 TimeUnit::MicroSeconds
1638 );
1639 assert_eq!(
1641 find_auto_scale(&BigInt::from(10_i128.pow(6)), &ts),
1642 TimeUnit::NanoSeconds
1643 );
1644 assert_eq!(
1646 find_auto_scale(&BigInt::from(10_i128.pow(3)), &ts),
1647 TimeUnit::PicoSeconds
1648 );
1649 assert_eq!(
1651 find_auto_scale(&BigInt::from(1), &ts),
1652 TimeUnit::FemtoSeconds
1653 );
1654 }
1655
1656 #[test]
1657 fn test_locale_cache_en_us() {
1658 use crate::time::{create_cache, format_locale};
1659 use pure_rust_locales::Locale;
1660
1661 let locale = Locale::en_US;
1662 let cache = create_cache(locale);
1663
1664 let result = format_locale("1234567.89", &cache);
1666 assert_eq!(result, "1,234,567.89");
1667 }
1668
1669 #[test]
1670 fn test_locale_cache_de_de() {
1671 use crate::time::{create_cache, format_locale};
1672 use pure_rust_locales::Locale;
1673
1674 let locale = Locale::de_DE;
1675 let cache = create_cache(locale);
1676
1677 let result = format_locale("1234567.89", &cache);
1678 assert_eq!(result, "1.234.567,89");
1679 }
1680
1681 #[test]
1682 fn test_locale_cache_fr_fr() {
1683 use crate::time::{create_cache, format_locale};
1684 use pure_rust_locales::Locale;
1685
1686 let locale = Locale::fr_FR;
1687 let cache = create_cache(locale);
1688
1689 let result = format_locale("1234567.89", &cache);
1691 assert_eq!(result, "1\u{2009}234\u{2009}567,89");
1693 }
1694
1695 #[test]
1696 fn test_locale_cache_small_numbers() {
1697 use crate::time::{create_cache, format_locale};
1698 use pure_rust_locales::Locale;
1699
1700 let locale = Locale::en_US;
1701 let cache = create_cache(locale);
1702
1703 assert_eq!(format_locale("123", &cache), "123");
1705 assert_eq!(format_locale("12.34", &cache), "12.34");
1706 assert_eq!(format_locale("0", &cache), "0");
1707 }
1708
1709 #[test]
1710 fn test_locale_cache_consistency_across_locales() {
1711 use crate::time::create_cache;
1712 use pure_rust_locales::Locale;
1713
1714 let cache1 = create_cache(Locale::en_US);
1716 let cache2 = create_cache(Locale::en_US);
1717
1718 assert_eq!(cache1.thousands_sep, cache2.thousands_sep);
1719 assert_eq!(cache1.decimal_point, cache2.decimal_point);
1720 assert_eq!(cache1.grouping, cache2.grouping);
1721 }
1722
1723 #[test]
1724 fn test_create_cache_from_various_locales() {
1725 use crate::time::{create_cache, format_locale};
1726 use pure_rust_locales::Locale;
1727
1728 let locales = vec![
1730 Locale::en_US,
1731 Locale::de_DE,
1732 Locale::fr_FR,
1733 Locale::es_ES,
1734 Locale::it_IT,
1735 Locale::pt_BR,
1736 Locale::pt_PT,
1737 Locale::ja_JP,
1738 Locale::zh_CN,
1739 Locale::zh_TW,
1740 Locale::ru_RU,
1741 Locale::ko_KR,
1742 Locale::pl_PL,
1743 Locale::tr_TR,
1744 Locale::nl_NL,
1745 Locale::sv_SE,
1746 Locale::da_DK,
1747 Locale::fi_FI,
1748 Locale::el_GR,
1749 Locale::hu_HU,
1750 Locale::cs_CZ,
1751 Locale::ro_RO,
1752 Locale::th_TH,
1753 Locale::vi_VN,
1754 Locale::ar_SA,
1755 Locale::he_IL,
1756 Locale::id_ID,
1757 Locale::uk_UA,
1758 Locale::en_GB,
1759 Locale::en_AU,
1760 Locale::en_CA,
1761 Locale::en_NZ,
1762 Locale::en_IN,
1763 Locale::fr_CA,
1764 Locale::de_AT,
1765 Locale::de_CH,
1766 Locale::fr_CH,
1767 Locale::it_CH,
1768 Locale::es_MX,
1769 Locale::es_AR,
1770 ];
1771
1772 for locale in locales {
1773 let cache = create_cache(locale);
1774 assert!(
1776 !format_locale("1234567.89", &cache).is_empty(),
1777 "Failed for {locale:?}"
1778 );
1779 }
1780 }
1781}
1782
1783#[cfg(test)]
1784mod get_ticks_tests {
1785 use super::*;
1786 use itertools::Itertools;
1787 use num::BigInt;
1788
1789 #[test]
1792 fn get_ticks_basic() {
1793 let vp = crate::viewport::Viewport::default();
1794 let timescale = TimeScale {
1795 unit: TimeUnit::MicroSeconds,
1796 multiplier: Some(1),
1797 };
1798 let frame_width = 800.0_f32;
1799 let text_size = 12.0_f32;
1800 let wanted = TimeUnit::MicroSeconds;
1801 let time_format = TimeFormat::default();
1802 let config = crate::config::SurferConfig::default();
1803 let num_timestamps = BigInt::from(1_000_000i64);
1804
1805 let ticks = get_ticks_internal(
1806 &vp,
1807 ×cale,
1808 frame_width,
1809 text_size,
1810 &wanted,
1811 &time_format,
1812 config.theme.ticks.density,
1813 &num_timestamps,
1814 );
1815
1816 assert!(!ticks.is_empty(), "expected at least one tick");
1817
1818 let mut last_x = -1.0_f32;
1820 let mut labels: Vec<String> = Vec::with_capacity(ticks.len());
1821 for (label, x, _) in &ticks {
1822 assert!(
1823 *x >= last_x,
1824 "tick x not monotonic: {x} < {last_x} for label {label}"
1825 );
1826 last_x = *x;
1827 assert!(*x >= 0.0, "tick x < 0: {x}");
1828 assert!(
1829 *x <= frame_width,
1830 "tick x > frame_width: {x} > {frame_width}"
1831 );
1832 labels.push(label.clone());
1833 }
1834 let unique_labels = labels.iter().unique().count();
1836 assert_eq!(labels.len(), unique_labels, "duplicate tick labels found");
1837 }
1838
1839 #[test]
1842 fn get_ticks_respects_frame_width_and_density() {
1843 let mut vp = crate::viewport::Viewport::default();
1844 vp.curr_left = crate::viewport::Relative(0.0);
1846 vp.curr_right = crate::viewport::Relative(0.1);
1847
1848 let timescale = TimeScale {
1849 unit: TimeUnit::NanoSeconds,
1850 multiplier: Some(1),
1851 };
1852 let frame_width = 200.0_f32;
1853 let text_size = 10.0_f32;
1854 let wanted = TimeUnit::Auto;
1855 let time_format = TimeFormat {
1856 format: TimeStringFormatting::SI,
1857 show_space: true,
1858 show_unit: true,
1859 };
1860
1861 let mut config = crate::config::SurferConfig::default();
1862 config.theme.ticks.density = 1.0;
1864
1865 let num_timestamps = BigInt::from(1_000_000i64);
1866
1867 let ticks = get_ticks_internal(
1868 &vp,
1869 ×cale,
1870 frame_width,
1871 text_size,
1872 &wanted,
1873 &time_format,
1874 config.theme.ticks.density,
1875 &num_timestamps,
1876 );
1877
1878 assert!(!ticks.is_empty(), "expected ticks even for narrow view");
1879 assert!(ticks.len() < 200, "too many ticks: {}", ticks.len());
1881
1882 let mut last_x = -1.0_f32;
1884 let mut labels: Vec<String> = Vec::with_capacity(ticks.len());
1885 for (label, x, _) in &ticks {
1886 assert!(
1887 *x >= last_x,
1888 "tick x not monotonic: {x} < {last_x} for label {label}"
1889 );
1890 last_x = *x;
1891 assert!(*x >= 0.0, "tick x < 0: {x}");
1892 assert!(
1893 *x <= frame_width,
1894 "tick x > frame_width: {x} > {frame_width}"
1895 );
1896 labels.push(label.clone());
1897 }
1898 let unique_labels = labels.iter().unique().count();
1899 assert_eq!(labels.len(), unique_labels, "duplicate tick labels found");
1900 }
1901}
1902
1903#[cfg(test)]
1904mod time_input_tests {
1905 use super::*;
1906
1907 #[test]
1908 fn test_parse_time_input_simple() {
1909 let (num, unit) = parse_time_input("100");
1910 assert_eq!(num, "100");
1911 assert_eq!(unit, None);
1912 }
1913
1914 #[test]
1915 fn test_parse_time_input_unit_only_no_panic() {
1916 let (num, unit) = parse_time_input("ns");
1918 assert_eq!(num, "ns");
1919 assert_eq!(unit, None);
1920 }
1921
1922 #[test]
1923 fn test_parse_time_input_with_unit_no_space() {
1924 let (num, unit) = parse_time_input("100ns");
1925 assert_eq!(num, "100");
1926 assert_eq!(unit, Some(TimeUnit::NanoSeconds));
1927
1928 let (num, unit) = parse_time_input("50ps");
1929 assert_eq!(num, "50");
1930 assert_eq!(unit, Some(TimeUnit::PicoSeconds));
1931
1932 let (num, unit) = parse_time_input("1.5ms");
1933 assert_eq!(num, "1.5");
1934 assert_eq!(unit, Some(TimeUnit::MilliSeconds));
1935 }
1936
1937 #[test]
1938 fn test_parse_time_input_with_unit_space() {
1939 let (num, unit) = parse_time_input("100 ns");
1940 assert_eq!(num, "100");
1941 assert_eq!(unit, Some(TimeUnit::NanoSeconds));
1942
1943 let (num, unit) = parse_time_input("1.5 ms");
1944 assert_eq!(num, "1.5");
1945 assert_eq!(unit, Some(TimeUnit::MilliSeconds));
1946
1947 let (num, unit) = parse_time_input("100\tms");
1948 assert_eq!(num, "100");
1949 assert_eq!(unit, Some(TimeUnit::MilliSeconds));
1950
1951 let (num, unit) = parse_time_input("100 ns");
1952 assert_eq!(num, "100");
1953 assert_eq!(unit, Some(TimeUnit::NanoSeconds));
1954 }
1955
1956 #[test]
1957 fn test_parse_time_input_microseconds_unicode() {
1958 let (num, unit) = parse_time_input("100μs");
1959 assert_eq!(num, "100");
1960 assert_eq!(unit, Some(TimeUnit::MicroSeconds));
1961
1962 let (num, unit) = parse_time_input("50 μs");
1963 assert_eq!(num, "50");
1964 assert_eq!(unit, Some(TimeUnit::MicroSeconds));
1965 }
1966
1967 #[test]
1968 fn test_parse_time_input_microseconds_ascii() {
1969 let (num, unit) = parse_time_input("100us");
1971 assert_eq!(num, "100");
1972 assert_eq!(unit, Some(TimeUnit::MicroSeconds));
1973 }
1974
1975 #[test]
1976 fn test_parse_time_input_seconds() {
1977 let (num, unit) = parse_time_input("10s");
1978 assert_eq!(num, "10");
1979 assert_eq!(unit, Some(TimeUnit::Seconds));
1980
1981 let (num, unit) = parse_time_input("0.5s");
1982 assert_eq!(num, "0.5");
1983 assert_eq!(unit, Some(TimeUnit::Seconds));
1984 }
1985
1986 #[test]
1987 fn test_parse_time_input_femtoseconds() {
1988 let (num, unit) = parse_time_input("1000000fs");
1989 assert_eq!(num, "1000000");
1990 assert_eq!(unit, Some(TimeUnit::FemtoSeconds));
1991 }
1992
1993 #[test]
1994 fn test_parse_time_input_with_whitespace() {
1995 let (num, unit) = parse_time_input(" 100ns ");
1996 assert_eq!(num, "100");
1997 assert_eq!(unit, Some(TimeUnit::NanoSeconds));
1998 }
1999
2000 #[test]
2001 fn test_split_numeric_parts() {
2002 assert_eq!(
2003 split_numeric_parts("100").ok(),
2004 Some(("100".to_string(), "".to_string()))
2005 );
2006 assert_eq!(
2007 split_numeric_parts("100.").ok(),
2008 Some(("100".to_string(), "".to_string()))
2009 );
2010 assert_eq!(
2011 split_numeric_parts(".5").ok(),
2012 Some(("0".to_string(), "5".to_string()))
2013 );
2014 assert_eq!(
2015 split_numeric_parts("1.5").ok(),
2016 Some(("1".to_string(), "5".to_string()))
2017 );
2018 }
2019
2020 #[test]
2021 fn test_split_numeric_parts_invalid() {
2022 assert!(split_numeric_parts("").is_err());
2023 assert!(split_numeric_parts("abc").is_err());
2024 assert!(split_numeric_parts("12.34.56").is_err());
2025 assert!(split_numeric_parts("-1").is_err());
2026 }
2027
2028 #[test]
2029 fn test_normalize_numeric_with_unit_integer() {
2030 let (val, unit) = normalize_numeric_with_unit("100", TimeUnit::NanoSeconds).unwrap();
2031 assert_eq!(val, BigInt::from(100));
2032 assert_eq!(unit, TimeUnit::NanoSeconds);
2033 }
2034
2035 #[test]
2036 fn test_normalize_numeric_with_unit_decimal_single_step() {
2037 let (val, unit) = normalize_numeric_with_unit("1.5", TimeUnit::NanoSeconds).unwrap();
2038 assert_eq!(val, BigInt::from(1500));
2039 assert_eq!(unit, TimeUnit::PicoSeconds);
2040 }
2041
2042 #[test]
2043 fn test_normalize_numeric_with_unit_decimal_multi_step() {
2044 let (val, unit) = normalize_numeric_with_unit("1.2345", TimeUnit::NanoSeconds).unwrap();
2045 assert_eq!(val, BigInt::from(1_234_500));
2046 assert_eq!(unit, TimeUnit::FemtoSeconds);
2047 }
2048
2049 #[test]
2050 fn test_time_input_state_default() {
2051 let state = TimeInputState::default();
2052 assert_eq!(state.input_text, "");
2053 assert_eq!(state.parsed_value, None);
2054 assert_eq!(state.input_unit, None);
2055 assert_eq!(state.normalized_unit, None);
2056 assert_eq!(state.selected_unit, TimeUnit::NanoSeconds);
2057 assert_eq!(state.error, None);
2058 }
2059
2060 #[test]
2061 fn test_time_input_state_update_valid() {
2062 let mut state = TimeInputState::new();
2063 state.update_input("100ns".to_string());
2064
2065 assert_eq!(state.input_text, "100ns");
2066 assert_eq!(state.parsed_value, Some(BigInt::from(100)));
2067 assert_eq!(state.input_unit, Some(TimeUnit::NanoSeconds));
2068 assert_eq!(state.normalized_unit, Some(TimeUnit::NanoSeconds));
2069 assert_eq!(state.error, None);
2070 }
2071
2072 #[test]
2073 fn test_time_input_state_update_invalid() {
2074 let mut state = TimeInputState::new();
2075 state.update_input("abc".to_string());
2076
2077 assert_eq!(state.parsed_value, None);
2078 assert!(state.error.is_some());
2079 }
2080
2081 #[test]
2082 fn test_time_input_state_update_decimal_unit_normalization() {
2083 let mut state = TimeInputState::new();
2084 state.update_input("1.5ns".to_string());
2085
2086 assert_eq!(state.parsed_value, Some(BigInt::from(1500)));
2087 assert_eq!(state.input_unit, Some(TimeUnit::NanoSeconds));
2088 assert_eq!(state.normalized_unit, Some(TimeUnit::PicoSeconds));
2089 assert_eq!(state.effective_unit(), TimeUnit::PicoSeconds);
2090 assert_eq!(state.error, None);
2091 }
2092
2093 #[test]
2094 fn test_time_input_state_to_timescale_ticks_invalid() {
2095 let state = TimeInputState::new();
2096 let timescale = TimeScale {
2097 unit: TimeUnit::NanoSeconds,
2098 multiplier: None,
2099 };
2100
2101 assert_eq!(state.to_timescale_ticks(×cale), None);
2102 }
2103
2104 #[test]
2105 fn test_time_input_comprehensive_example() {
2106 let mut state = TimeInputState::new();
2108 state.update_input("2.5 ms".to_string());
2109
2110 assert_eq!(state.parsed_value, Some(BigInt::from(2500)));
2112 assert_eq!(state.input_unit, Some(TimeUnit::MilliSeconds));
2113 assert_eq!(state.effective_unit(), TimeUnit::MicroSeconds);
2114 assert_eq!(state.error, None);
2115
2116 let timescale = TimeScale {
2118 unit: TimeUnit::MicroSeconds,
2119 multiplier: None,
2120 };
2121 assert_eq!(
2122 state.to_timescale_ticks(×cale),
2123 Some(BigInt::from(2500))
2124 );
2125 }
2126
2127 #[test]
2128 fn test_parse_time_longest_match_first() {
2129 let (num, unit) = parse_time_input("100ms");
2131 assert_eq!(num, "100");
2132 assert_eq!(unit, Some(TimeUnit::MilliSeconds));
2133
2134 let (_, unit) = parse_time_input("100s");
2136 assert_ne!(unit, Some(TimeUnit::MilliSeconds));
2137 }
2138
2139 #[test]
2140 fn test_parse_time_no_false_positives() {
2141 let (num, unit) = parse_time_input("mass");
2143 assert_eq!(num, "mass");
2144 assert_eq!(unit, None);
2145
2146 let (num, unit) = parse_time_input("uses");
2147 assert_eq!(num, "uses");
2148 assert_eq!(unit, None);
2149 }
2150
2151 #[test]
2152 fn test_time_input_state_clear() {
2153 let mut state = TimeInputState::new();
2154 state.update_input("100ns".to_string());
2155 assert!(state.parsed_value.is_some());
2156
2157 state.update_input(String::new());
2159 assert!(state.error.is_some());
2160 }
2161
2162 fn ns_timescale() -> TimeScale {
2165 TimeScale {
2166 unit: TimeUnit::NanoSeconds,
2167 multiplier: None,
2168 }
2169 }
2170
2171 fn ps_timescale() -> TimeScale {
2172 TimeScale {
2173 unit: TimeUnit::PicoSeconds,
2174 multiplier: None,
2175 }
2176 }
2177
2178 #[test]
2179 fn test_parse_time_string_to_ticks_plain_integer() {
2180 let ts = ns_timescale();
2182 assert_eq!(
2183 parse_time_string_to_ticks("100", &ts),
2184 Some(BigInt::from(100))
2185 );
2186 assert_eq!(parse_time_string_to_ticks("0", &ts), Some(BigInt::from(0)));
2187 }
2188
2189 #[test]
2190 fn test_parse_time_string_to_ticks_same_unit() {
2191 let ts = ns_timescale();
2193 assert_eq!(
2194 parse_time_string_to_ticks("100ns", &ts),
2195 Some(BigInt::from(100))
2196 );
2197 assert_eq!(
2198 parse_time_string_to_ticks("100 ns", &ts),
2199 Some(BigInt::from(100))
2200 );
2201 }
2202
2203 #[test]
2204 fn test_parse_time_string_to_ticks_coarser_unit() {
2205 let ts = ns_timescale();
2207 assert_eq!(
2208 parse_time_string_to_ticks("1us", &ts),
2209 Some(BigInt::from(1000))
2210 );
2211 assert_eq!(
2212 parse_time_string_to_ticks("1μs", &ts),
2213 Some(BigInt::from(1000))
2214 );
2215 assert_eq!(
2217 parse_time_string_to_ticks("1ms", &ts),
2218 Some(BigInt::from(1_000_000))
2219 );
2220 assert_eq!(
2222 parse_time_string_to_ticks("1s", &ts),
2223 Some(BigInt::from(1_000_000_000))
2224 );
2225 }
2226
2227 #[test]
2228 fn test_parse_time_string_to_ticks_finer_unit() {
2229 let ts = ns_timescale();
2231 assert_eq!(
2232 parse_time_string_to_ticks("1ps", &ts),
2233 Some(BigInt::from(0))
2234 );
2235 assert_eq!(
2237 parse_time_string_to_ticks("1000ps", &ts),
2238 Some(BigInt::from(1))
2239 );
2240 }
2241
2242 #[test]
2243 fn test_parse_time_string_to_ticks_decimal() {
2244 let ts = ns_timescale();
2246 assert_eq!(
2247 parse_time_string_to_ticks("1.5us", &ts),
2248 Some(BigInt::from(1500))
2249 );
2250 let ts_ps = ps_timescale();
2252 assert_eq!(
2253 parse_time_string_to_ticks("0.5ns", &ts_ps),
2254 Some(BigInt::from(500))
2255 );
2256 }
2257
2258 #[test]
2259 fn test_parse_time_string_to_ticks_with_whitespace() {
2260 let ts = ns_timescale();
2261 assert_eq!(
2262 parse_time_string_to_ticks(" 100 ns ", &ts),
2263 Some(BigInt::from(100))
2264 );
2265 }
2266
2267 #[test]
2268 fn test_parse_time_string_to_ticks_invalid() {
2269 let ts = ns_timescale();
2270 assert_eq!(parse_time_string_to_ticks("abc", &ts), None);
2271 assert_eq!(parse_time_string_to_ticks("", &ts), None);
2272 assert_eq!(parse_time_string_to_ticks("-5ns", &ts), None);
2273 }
2274
2275 #[test]
2276 fn test_parse_time_string_to_ticks_with_multiplier() {
2277 let ts = TimeScale {
2279 unit: TimeUnit::NanoSeconds,
2280 multiplier: Some(10),
2281 };
2282 assert_eq!(
2283 parse_time_string_to_ticks("100ns", &ts),
2284 Some(BigInt::from(10))
2285 );
2286 }
2287
2288 #[test]
2289 fn test_parse_time_string_to_ticks_ps_timescale() {
2290 let ts = ps_timescale();
2292 assert_eq!(
2293 parse_time_string_to_ticks("1ns", &ts),
2294 Some(BigInt::from(1000))
2295 );
2296 assert_eq!(
2298 parse_time_string_to_ticks("42", &ts),
2299 Some(BigInt::from(42))
2300 );
2301 }
2302}