Skip to main content

libsurfer/
time.rs

1//! Time handling and formatting.
2use 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::viewport::Viewport;
18use crate::wave_data::WaveData;
19use crate::{
20    Message, SystemState,
21    translation::group_n_chars,
22    view::{DrawConfig, DrawingContext},
23};
24
25#[derive(Serialize, Deserialize, Clone)]
26pub struct TimeScale {
27    pub unit: TimeUnit,
28    pub multiplier: Option<u32>,
29}
30
31#[derive(Debug, Clone, Copy, Display, Eq, PartialEq, Serialize, Deserialize, Sequence)]
32pub enum TimeUnit {
33    #[display("zs")]
34    ZeptoSeconds,
35
36    #[display("as")]
37    AttoSeconds,
38
39    #[display("fs")]
40    FemtoSeconds,
41
42    #[display("ps")]
43    PicoSeconds,
44
45    #[display("ns")]
46    NanoSeconds,
47
48    #[display("μs")]
49    MicroSeconds,
50
51    #[display("ms")]
52    MilliSeconds,
53
54    #[display("s")]
55    Seconds,
56
57    #[display("No unit")]
58    None,
59
60    /// Use the largest time unit feasible for each time.
61    #[display("Auto")]
62    Auto,
63}
64
65pub const DEFAULT_TIMELINE_NAME: &str = "Time";
66const THIN_SPACE: &str = "\u{2009}";
67
68/// Candidate multipliers used to choose tick spacing.
69pub const TICK_STEPS: [f64; 8] = [1., 2., 2.5, 5., 10., 20., 25., 50.];
70
71/// Cached locale-specific formatting properties.
72struct LocaleFormatCache {
73    grouping: &'static [i64],
74    thousands_sep: String,
75    decimal_point: String,
76}
77
78static LOCALE_FORMAT_CACHE: OnceLock<LocaleFormatCache> = OnceLock::new();
79
80/// Get the cached locale formatting properties.
81fn get_locale_format_cache() -> &'static LocaleFormatCache {
82    LOCALE_FORMAT_CACHE.get_or_init(|| {
83        let locale = get_locale()
84            .unwrap_or_else(|| "en-US".to_string())
85            .as_str()
86            .try_into()
87            .unwrap_or(Locale::en_US);
88        create_cache(locale)
89    })
90}
91
92fn create_cache(locale: Locale) -> LocaleFormatCache {
93    let grouping = locale_match!(locale => LC_NUMERIC::GROUPING);
94    let thousands_sep =
95        locale_match!(locale => LC_NUMERIC::THOUSANDS_SEP).replace('\u{202f}', THIN_SPACE);
96    let decimal_point = locale_match!(locale => LC_NUMERIC::DECIMAL_POINT).to_string();
97
98    LocaleFormatCache {
99        grouping,
100        thousands_sep,
101        decimal_point,
102    }
103}
104
105impl From<wellen::TimescaleUnit> for TimeUnit {
106    fn from(timescale: wellen::TimescaleUnit) -> Self {
107        match timescale {
108            wellen::TimescaleUnit::ZeptoSeconds => TimeUnit::ZeptoSeconds,
109            wellen::TimescaleUnit::AttoSeconds => TimeUnit::AttoSeconds,
110            wellen::TimescaleUnit::FemtoSeconds => TimeUnit::FemtoSeconds,
111            wellen::TimescaleUnit::PicoSeconds => TimeUnit::PicoSeconds,
112            wellen::TimescaleUnit::NanoSeconds => TimeUnit::NanoSeconds,
113            wellen::TimescaleUnit::MicroSeconds => TimeUnit::MicroSeconds,
114            wellen::TimescaleUnit::MilliSeconds => TimeUnit::MilliSeconds,
115            wellen::TimescaleUnit::Seconds => TimeUnit::Seconds,
116            wellen::TimescaleUnit::Unknown => TimeUnit::None,
117        }
118    }
119}
120
121impl From<ftr_parser::types::Timescale> for TimeUnit {
122    fn from(timescale: Timescale) -> Self {
123        match timescale {
124            Timescale::Fs => TimeUnit::FemtoSeconds,
125            Timescale::Ps => TimeUnit::PicoSeconds,
126            Timescale::Ns => TimeUnit::NanoSeconds,
127            Timescale::Us => TimeUnit::MicroSeconds,
128            Timescale::Ms => TimeUnit::MilliSeconds,
129            Timescale::S => TimeUnit::Seconds,
130            Timescale::Unit => TimeUnit::None,
131            Timescale::None => TimeUnit::None,
132        }
133    }
134}
135
136impl TimeUnit {
137    /// Get the power-of-ten exponent for a time unit.
138    fn exponent(self) -> i8 {
139        match self {
140            TimeUnit::ZeptoSeconds => -21,
141            TimeUnit::AttoSeconds => -18,
142            TimeUnit::FemtoSeconds => -15,
143            TimeUnit::PicoSeconds => -12,
144            TimeUnit::NanoSeconds => -9,
145            TimeUnit::MicroSeconds => -6,
146            TimeUnit::MilliSeconds => -3,
147            TimeUnit::Seconds => 0,
148            TimeUnit::None => 0,
149            TimeUnit::Auto => 0,
150        }
151    }
152    /// Convert a power-of-ten exponent to a time unit.
153    fn from_exponent(exponent: i8) -> Option<Self> {
154        match exponent {
155            -21 => Some(TimeUnit::ZeptoSeconds),
156            -18 => Some(TimeUnit::AttoSeconds),
157            -15 => Some(TimeUnit::FemtoSeconds),
158            -12 => Some(TimeUnit::PicoSeconds),
159            -9 => Some(TimeUnit::NanoSeconds),
160            -6 => Some(TimeUnit::MicroSeconds),
161            -3 => Some(TimeUnit::MilliSeconds),
162            0 => Some(TimeUnit::Seconds),
163            _ => None,
164        }
165    }
166}
167
168/// Create menu for selecting preferred time unit.
169pub fn timeunit_menu(ui: &mut Ui, msgs: &mut Vec<Message>, wanted_timeunit: &TimeUnit) {
170    for timeunit in enum_iterator::all::<TimeUnit>() {
171        if ui
172            .radio(*wanted_timeunit == timeunit, timeunit.to_string())
173            .clicked()
174        {
175            msgs.push(Message::SetTimeUnit(timeunit));
176        }
177    }
178}
179
180/// How to format the time stamps.
181#[derive(Debug, Deserialize, Serialize, Clone)]
182pub struct TimeFormat {
183    /// How to format the numeric part of the time string.
184    format: TimeStringFormatting,
185    /// Insert a space between number and unit.
186    show_space: bool,
187    /// Display time unit.
188    show_unit: bool,
189}
190
191impl Default for TimeFormat {
192    fn default() -> Self {
193        TimeFormat {
194            format: TimeStringFormatting::No,
195            show_space: true,
196            show_unit: true,
197        }
198    }
199}
200
201impl TimeFormat {
202    /// Create a new `TimeFormat` with custom settings.
203    #[must_use]
204    pub fn new(format: TimeStringFormatting, show_space: bool, show_unit: bool) -> Self {
205        TimeFormat {
206            format,
207            show_space,
208            show_unit,
209        }
210    }
211
212    /// Set the format type.
213    #[must_use]
214    pub fn with_format(mut self, format: TimeStringFormatting) -> Self {
215        self.format = format;
216        self
217    }
218
219    /// Set whether to show space between number and unit.
220    #[must_use]
221    pub fn with_space(mut self, show_space: bool) -> Self {
222        self.show_space = show_space;
223        self
224    }
225
226    /// Set whether to show the time unit.
227    #[must_use]
228    pub fn with_unit(mut self, show_unit: bool) -> Self {
229        self.show_unit = show_unit;
230        self
231    }
232}
233
234/// Draw the menu for selecting the time format.
235pub fn timeformat_menu(ui: &mut Ui, msgs: &mut Vec<Message>, current_timeformat: &TimeFormat) {
236    for time_string_format in enum_iterator::all::<TimeStringFormatting>() {
237        if ui
238            .radio(
239                current_timeformat.format == time_string_format,
240                if time_string_format == TimeStringFormatting::Locale {
241                    format!(
242                        "{time_string_format} ({locale})",
243                        locale = get_locale().unwrap_or_else(|| "unknown".to_string())
244                    )
245                } else {
246                    time_string_format.to_string()
247                },
248            )
249            .clicked()
250        {
251            msgs.push(Message::SetTimeStringFormatting(Some(time_string_format)));
252        }
253    }
254}
255
256/// How to format the numeric part of the time string.
257#[derive(Debug, Clone, Copy, Display, Eq, PartialEq, Serialize, Deserialize, Sequence)]
258pub enum TimeStringFormatting {
259    /// No additional formatting.
260    No,
261
262    /// Use the current locale to determine decimal separator, thousands separator, and grouping
263    Locale,
264
265    /// Use the SI standard: split into groups of three digits, unless there are exactly four
266    /// for both integer and fractional part. Use space as group separator.
267    SI,
268}
269
270/// Get rid of trailing zeros if the string contains a ., i.e., being fractional
271/// If the resulting string ends with ., remove that as well.
272fn strip_trailing_zeros_and_period(time: String) -> String {
273    if !time.contains('.') {
274        return time;
275    }
276    time.trim_end_matches('0').trim_end_matches('.').to_string()
277}
278
279/// Format number based on [`TimeStringFormatting`], i.e., possibly group digits together
280/// and use correct separator for each group.
281fn split_and_format_number(time: &str, format: TimeStringFormatting) -> String {
282    match format {
283        TimeStringFormatting::No => time.to_string(),
284        TimeStringFormatting::Locale => format_locale(time, get_locale_format_cache()),
285        TimeStringFormatting::SI => format_si(time),
286    }
287}
288
289fn format_si(time: &str) -> String {
290    if let Some((integer_part, fractional_part)) = time.split_once('.') {
291        let integer_result = if integer_part.len() > 4 {
292            group_n_chars(integer_part, 3).join(THIN_SPACE)
293        } else {
294            integer_part.to_string()
295        };
296        if fractional_part.len() > 4 {
297            let reversed = fractional_part.chars().rev().collect::<String>();
298            let reversed_fractional_parts = group_n_chars(&reversed, 3).join(THIN_SPACE);
299            let fractional_result = reversed_fractional_parts.chars().rev().collect::<String>();
300            format!("{integer_result}.{fractional_result}")
301        } else {
302            format!("{integer_result}.{fractional_part}")
303        }
304    } else if time.len() > 4 {
305        group_n_chars(time, 3).join(THIN_SPACE)
306    } else {
307        time.to_string()
308    }
309}
310
311fn format_locale(time: &str, cache: &LocaleFormatCache) -> String {
312    if cache.grouping[0] > 0 {
313        if let Some((integer_part, fractional_part)) = time.split_once('.') {
314            let integer_result = group_n_chars(integer_part, cache.grouping[0] as usize)
315                .join(cache.thousands_sep.as_str());
316            format!(
317                "{integer_result}{decimal_point}{fractional_part}",
318                decimal_point = &cache.decimal_point
319            )
320        } else {
321            group_n_chars(time, cache.grouping[0] as usize).join(cache.thousands_sep.as_str())
322        }
323    } else {
324        time.to_string()
325    }
326}
327
328/// Heuristically find a suitable time unit for the given time.
329fn find_auto_scale(time: &BigInt, timescale: &TimeScale) -> TimeUnit {
330    // In case of seconds, nothing to do as it is the largest supported unit
331    // (unless we want to support minutes etc...)
332    if matches!(timescale.unit, TimeUnit::Seconds) {
333        return TimeUnit::Seconds;
334    }
335    let multiplier_digits = timescale.multiplier.unwrap_or(1).ilog10();
336    let start_digits = -timescale.unit.exponent();
337    for e in (3..=start_digits).step_by(3).rev() {
338        if (time % pow10(e as u32 - multiplier_digits)).is_zero()
339            && let Some(unit) = TimeUnit::from_exponent(e - start_digits)
340        {
341            return unit;
342        }
343    }
344    timescale.unit
345}
346
347/// Formatter for time strings with caching of computed values.
348/// Enables efficient formatting of multiple time values with the same timescale and format settings.
349pub struct TimeFormatter {
350    timescale: TimeScale,
351    wanted_unit: TimeUnit,
352    time_format: TimeFormat,
353    /// Cached exponent difference (wanted - data)
354    exponent_diff: i8,
355    /// Cached unit string (empty if `show_unit` is false)
356    unit_string: String,
357    /// Cached space string (empty if `show_space` is false)
358    space_string: String,
359}
360
361impl TimeFormatter {
362    /// Create a new `TimeFormatter` with the given settings.
363    #[must_use]
364    pub fn new(timescale: &TimeScale, wanted_unit: &TimeUnit, time_format: &TimeFormat) -> Self {
365        // Note: For Auto unit, we defer resolution to format() time since it depends on the value
366        let (exponent_diff, unit_string) = if *wanted_unit == TimeUnit::Auto {
367            // Use placeholder values for Auto - will be computed per-format call
368            (0i8, String::new())
369        } else {
370            let wanted_exponent = wanted_unit.exponent();
371            let data_exponent = timescale.unit.exponent();
372            let exponent_diff = wanted_exponent - data_exponent;
373
374            let unit_string = if time_format.show_unit {
375                wanted_unit.to_string()
376            } else {
377                String::new()
378            };
379
380            (exponent_diff, unit_string)
381        };
382
383        TimeFormatter {
384            timescale: timescale.clone(),
385            wanted_unit: *wanted_unit,
386            time_format: time_format.clone(),
387            exponent_diff,
388            unit_string,
389            space_string: if time_format.show_space {
390                " ".to_string()
391            } else {
392                String::new()
393            },
394        }
395    }
396
397    /// Format a single time value.
398    #[must_use]
399    pub fn format(&self, time: &BigInt) -> String {
400        if self.wanted_unit == TimeUnit::None {
401            return split_and_format_number(&time.to_string(), self.time_format.format);
402        }
403
404        // Handle Auto unit by resolving it for this specific time value
405        let (exponent_diff, unit_string) = if self.wanted_unit == TimeUnit::Auto {
406            let auto_unit = find_auto_scale(time, &self.timescale);
407            let wanted_exponent = auto_unit.exponent();
408            let data_exponent = self.timescale.unit.exponent();
409            let exp_diff = wanted_exponent - data_exponent;
410
411            let unit_str = if self.time_format.show_unit {
412                auto_unit.to_string()
413            } else {
414                String::new()
415            };
416
417            (exp_diff, unit_str)
418        } else {
419            (self.exponent_diff, self.unit_string.clone())
420        };
421
422        let timestring = if exponent_diff >= 0 {
423            let precision = exponent_diff as usize;
424            strip_trailing_zeros_and_period(format!(
425                "{scaledtime:.precision$}",
426                scaledtime = BigRational::new(
427                    time * self.timescale.multiplier.unwrap_or(1),
428                    pow10(exponent_diff as u32)
429                )
430                .to_f64()
431                .unwrap_or(f64::NAN)
432            ))
433        } else {
434            (time * self.timescale.multiplier.unwrap_or(1) * pow10(-exponent_diff as u32))
435                .to_string()
436        };
437
438        format!(
439            "{scaledtime}{space}{unit}",
440            scaledtime = split_and_format_number(&timestring, self.time_format.format),
441            space = if unit_string.is_empty() {
442                ""
443            } else {
444                &self.space_string
445            },
446            unit = &unit_string
447        )
448    }
449}
450
451/// Helper to compute powers of 10 efficiently.
452/// Returns precomputed values for exponents 0-21, or computes on-demand for others.
453fn pow10(exp: u32) -> BigInt {
454    match exp {
455        0 => BigInt::from(1),
456        1 => BigInt::from(10),
457        2 => BigInt::from(100),
458        3 => BigInt::from(1000),
459        6 => BigInt::from(1_000_000),
460        9 => BigInt::from(1_000_000_000),
461        12 => BigInt::from(1_000_000_000_000i64),
462        15 => BigInt::from(1_000_000_000_000_000i64),
463        18 => BigInt::from(1_000_000_000_000_000_000i64),
464        21 => BigInt::from(1_000_000_000_000_000_000_000i128),
465        _ => BigInt::from(10).pow(exp),
466    }
467}
468
469/// Format the time string taking all settings into account.
470/// This function delegates to `TimeFormatter` which handles the Auto timeunit.
471#[must_use]
472pub fn time_string(
473    time: &BigInt,
474    timescale: &TimeScale,
475    wanted_timeunit: &TimeUnit,
476    wanted_time_format: &TimeFormat,
477) -> String {
478    let formatter = TimeFormatter::new(timescale, wanted_timeunit, wanted_time_format);
479    formatter.format(time)
480}
481
482/// Parse a time string and extract numeric value and unit (if present).
483///
484/// Parses strings like "100", "100ps", "100 ps", "1.5ms", etc.
485/// Returns (numeric_value_str, optional_unit)
486fn parse_time_input(input: &str) -> (String, Option<TimeUnit>) {
487    let sorted_units =
488        // Must be sorted by descending length to ensure correct matching (e.g., "ms" before "s")
489        [
490            ("zs", TimeUnit::ZeptoSeconds),
491            ("as", TimeUnit::AttoSeconds),
492            ("fs", TimeUnit::FemtoSeconds),
493            ("ps", TimeUnit::PicoSeconds),
494            ("ns", TimeUnit::NanoSeconds),
495            ("μs", TimeUnit::MicroSeconds),
496            ("us", TimeUnit::MicroSeconds), // Alternative spelling
497            ("ms", TimeUnit::MilliSeconds),
498            ("s", TimeUnit::Seconds),
499        ];
500
501    let trimmed = input.trim();
502
503    for (unit_str, unit) in sorted_units {
504        // Check if trimmed ends with this unit
505        if trimmed.ends_with(unit_str) {
506            let after_number = trimmed.len() - unit_str.len();
507
508            // Support both adjacent and whitespace-separated units: "100ns", "100 ns", "100\tms".
509            if after_number > 0 {
510                let numeric = trimmed[..after_number].trim_end();
511                if let Some(last_char) = numeric.chars().next_back()
512                    && (last_char.is_ascii_digit() || last_char == '.')
513                {
514                    return (numeric.to_string(), Some(unit));
515                }
516            }
517        }
518    }
519
520    (trimmed.to_string(), None)
521}
522
523/// Split a numeric string into integer and fractional parts.
524///
525/// Accepts "123", "123.", ".5", "123.456". Rejects negatives and non-digits.
526fn split_numeric_parts(numeric_str: &str) -> Result<(String, String), String> {
527    let trimmed = numeric_str.trim();
528    if trimmed.is_empty() {
529        return Err("Empty input".to_string());
530    }
531    if trimmed.starts_with('-') {
532        return Err("Negative numbers not supported".to_string());
533    }
534    let normalized = trimmed.strip_prefix('+').unwrap_or(trimmed);
535    let mut parts = normalized.split('.');
536    let integer_part = parts.next().unwrap_or("");
537    let fractional_part = parts.next().unwrap_or("");
538    if parts.next().is_some() {
539        return Err("Invalid number: multiple decimal points".to_string());
540    }
541
542    // Validate all characters in one pass by chaining iterators
543    let all_valid =
544        (integer_part.chars().chain(fractional_part.chars())).all(|c| c.is_ascii_digit());
545    if !all_valid {
546        return Err(format!("Failed to parse '{numeric_str}' as number"));
547    }
548
549    let integer = if integer_part.is_empty() {
550        "0".to_string()
551    } else {
552        integer_part.to_string()
553    };
554    Ok((integer, fractional_part.to_string()))
555}
556
557/// Convert a numeric string into an integer BigInt and normalized TimeUnit.
558///
559/// Decimal inputs are scaled by selecting a smaller unit so the numeric part is an integer.
560/// Example: "1.5" with unit ns -> 1500 ps.
561fn normalize_numeric_with_unit(
562    numeric_str: &str,
563    unit: TimeUnit,
564) -> Result<(BigInt, TimeUnit), String> {
565    let (integer_part, mut fractional_part) = split_numeric_parts(numeric_str)?;
566
567    // Trim trailing zeros in fractional part to minimize scaling
568    while fractional_part.ends_with('0') {
569        fractional_part.pop();
570    }
571
572    if fractional_part.is_empty() {
573        let value =
574            BigInt::parse_bytes(integer_part.as_bytes(), 10).unwrap_or_else(|| BigInt::from(0));
575        return Ok((value, unit));
576    }
577
578    let fractional_len = fractional_part.len();
579    // Prevent excessively long fractional parts that exceed available time units
580    if fractional_len > 21 {
581        return Err("Too many decimal places (max 21 supported)".to_string());
582    }
583
584    let steps = fractional_len.div_ceil(3) as i8; // ceil(fractional_len / 3)
585    let new_exponent = unit.exponent() - (steps * 3);
586    let new_unit = TimeUnit::from_exponent(new_exponent)
587        .ok_or_else(|| "Too much precision for available time units".to_string())?;
588
589    let mut combined = integer_part;
590    combined.push_str(&fractional_part);
591    let mut value = BigInt::parse_bytes(combined.as_bytes(), 10).unwrap_or_else(|| BigInt::from(0));
592
593    let extra_zeros = (steps as usize * 3).saturating_sub(fractional_len);
594    if extra_zeros > 0 {
595        let scale = pow10(extra_zeros as u32);
596        value *= scale;
597    }
598
599    Ok((value, new_unit))
600}
601
602// Conversion helper lives on TimeInputState to avoid leaking conversion detail.
603
604/// State for the time input widget.
605#[derive(Clone, Debug)]
606pub struct TimeInputState {
607    /// User's text input
608    input_text: String,
609    /// Parsed time value (if valid)
610    parsed_value: Option<BigInt>,
611    /// Unit extracted from input (if present)
612    input_unit: Option<TimeUnit>,
613    /// Normalized unit after handling decimals
614    normalized_unit: Option<TimeUnit>,
615    /// Selected unit from dropdown (used when no unit in input)
616    selected_unit: TimeUnit,
617    /// Error message (if any)
618    error: Option<String>,
619}
620
621impl Default for TimeInputState {
622    fn default() -> Self {
623        Self {
624            input_text: String::new(),
625            parsed_value: None,
626            input_unit: None,
627            normalized_unit: None,
628            selected_unit: TimeUnit::NanoSeconds,
629            error: None,
630        }
631    }
632}
633
634impl TimeInputState {
635    /// Create a new time input state with default values.
636    pub fn new() -> Self {
637        Self::default()
638    }
639
640    /// Update the input text and parse it.
641    pub fn update_input(&mut self, input: String) {
642        self.input_text = input;
643        let (numeric_str, unit) = parse_time_input(&self.input_text);
644        self.input_unit = unit;
645
646        let base_unit = self.input_unit.unwrap_or(self.selected_unit);
647        match normalize_numeric_with_unit(&numeric_str, base_unit) {
648            Ok((val, normalized_unit)) => {
649                self.parsed_value = Some(val);
650                self.normalized_unit = Some(normalized_unit);
651                self.error = None;
652            }
653            Err(e) => {
654                self.parsed_value = None;
655                self.normalized_unit = None;
656                self.error = Some(e);
657            }
658        }
659    }
660
661    /// Get the effective unit (from input or dropdown).
662    pub fn effective_unit(&self) -> TimeUnit {
663        self.normalized_unit
664            .or(self.input_unit)
665            .unwrap_or(self.selected_unit)
666    }
667
668    /// Convert the parsed value into timescale ticks.
669    ///
670    /// When conversion is not exact, this truncates toward zero.
671    pub fn to_timescale_ticks(&self, timescale: &TimeScale) -> Option<BigInt> {
672        let value = self.parsed_value.clone()?;
673        let unit = self.effective_unit();
674        let base_unit = if unit == TimeUnit::None {
675            timescale.unit
676        } else {
677            unit
678        };
679        let unit_exp = base_unit.exponent();
680        let data_exp = timescale.unit.exponent();
681        let diff = unit_exp - data_exp;
682
683        let mut result = value;
684        if diff > 0 {
685            let scale = pow10(diff as u32);
686            result *= scale;
687        } else if diff < 0 {
688            let scale = pow10((-diff) as u32);
689            result /= scale;
690        }
691
692        let multiplier = timescale.multiplier.unwrap_or(1);
693        if multiplier != 1 {
694            let mult = BigInt::from(multiplier);
695            result /= mult;
696        }
697
698        Some(result)
699    }
700}
701
702/// Render a time input widget in egui.
703///
704/// Shows a text input field and a unit selector dropdown (only enabled when no unit in input).
705///
706/// # Example
707/// ```ignore
708/// let mut time_state = TimeInputState::default();
709///
710/// time_input_widget(ui, "Enter time:", &mut time_state);
711///
712/// if let Some((value, timescale)) = time_state.to_timescale() {
713///     println!("Time: {value} {:?}", timescale);
714/// }
715/// ```
716pub fn time_input_widget(
717    ui: &mut Ui,
718    waves: &WaveData,
719    msgs: &mut Vec<Message>,
720    state: &mut TimeInputState,
721    request_focus: bool,
722) {
723    ui.horizontal(|ui| {
724        // Text input field
725        let mut input = state.input_text.clone();
726        let text_response = ui.add(
727            egui::TextEdit::singleline(&mut input)
728                .desired_width(100.0)
729                .hint_text("e.g., 1.5ms"),
730        );
731
732        if request_focus && !text_response.has_focus() {
733            text_response.request_focus();
734            msgs.push(Message::SetRequestTimeEditFocus(false));
735        }
736
737        if text_response.changed() {
738            state.update_input(input);
739        }
740
741        // Unit dropdown (only enabled if no unit in input)
742        let dropdown_enabled = state.input_unit.is_none();
743
744        if dropdown_enabled {
745            egui::ComboBox::new("Unit", "")
746                .width(32.0)
747                .selected_text(state.selected_unit.to_string())
748                .show_ui(ui, |ui| {
749                    for unit in enum_iterator::all::<TimeUnit>() {
750                        // Filter out Auto and None units from the dropdown
751                        if !matches!(unit, TimeUnit::Auto | TimeUnit::None) {
752                            ui.selectable_value(&mut state.selected_unit, unit, unit.to_string());
753                        }
754                    }
755                });
756        }
757
758        // Handle focus
759        if text_response.gained_focus() {
760            msgs.push(Message::SetTimeEditFocused(true));
761        }
762        if text_response.lost_focus() {
763            if text_response.ctx.input(|i| i.key_pressed(Key::Enter)) {
764                // Enter pressed - trigger action if input is valid
765                if let Some(time_stamp) =
766                    state.to_timescale_ticks(&waves.inner.metadata().timescale)
767                {
768                    msgs.push(Message::GoToTime(Some(time_stamp), 0));
769                }
770            }
771            msgs.push(Message::SetTimeEditFocused(false));
772        }
773
774        // Buttons
775        let button_enabled = state.parsed_value.is_some();
776        let goto_button = Button::new(RichText::new(icons::TARGET_FILL).heading()).frame(false);
777        if ui
778            .add_enabled(button_enabled, goto_button)
779            .on_hover_text("Go to time")
780            .clicked()
781            && let Some(time_stamp) = state.to_timescale_ticks(&waves.inner.metadata().timescale)
782        {
783            msgs.push(Message::GoToTime(Some(time_stamp), 0));
784        }
785        let cursor_button = Button::new(RichText::new(icons::CURSOR_FILL).heading()).frame(false);
786        if ui
787            .add_enabled(button_enabled, cursor_button)
788            .on_hover_text("Set cursor at time")
789            .clicked()
790            && let Some(time_stamp) = state.to_timescale_ticks(&waves.inner.metadata().timescale)
791        {
792            msgs.push(Message::CursorSet(time_stamp));
793        }
794    });
795}
796
797impl WaveData {
798    pub fn draw_tick_line(&self, x: f32, ctx: &mut DrawingContext, stroke: &Stroke) {
799        let Pos2 {
800            x: x_pos,
801            y: y_start,
802        } = (ctx.to_screen)(x, 0.);
803        ctx.painter.vline(
804            x_pos,
805            (y_start)..=(y_start + ctx.cfg.canvas_height),
806            *stroke,
807        );
808    }
809    /// Draw the text for each tick location.
810    pub fn draw_ticks(
811        &self,
812        color: Color32,
813        ticks: &[(String, f32)],
814        ctx: &DrawingContext<'_>,
815        y_offset: f32,
816        align: Align2,
817    ) {
818        for (tick_text, x) in ticks {
819            ctx.painter.text(
820                (ctx.to_screen)(*x, y_offset),
821                align,
822                tick_text,
823                FontId::proportional(ctx.cfg.text_size),
824                color,
825            );
826        }
827    }
828}
829
830impl SystemState {
831    pub fn get_time_format(&self) -> TimeFormat {
832        let time_format = self.user.config.default_time_format.clone();
833        if let Some(time_string_format) = self.user.time_string_format {
834            time_format.with_format(time_string_format)
835        } else {
836            time_format
837        }
838    }
839
840    pub fn get_ticks_for_viewport_idx(
841        &self,
842        waves: &WaveData,
843        viewport_idx: usize,
844        cfg: &DrawConfig,
845    ) -> Vec<(String, f32)> {
846        self.get_ticks_for_viewport(waves, &waves.viewports[viewport_idx], cfg)
847    }
848
849    pub fn get_ticks_for_viewport(
850        &self,
851        waves: &WaveData,
852        viewport: &Viewport,
853        cfg: &DrawConfig,
854    ) -> Vec<(String, f32)> {
855        get_ticks_internal(
856            viewport,
857            &waves.inner.metadata().timescale,
858            cfg.canvas_width,
859            cfg.text_size,
860            &self.user.wanted_timeunit,
861            &self.get_time_format(),
862            self.user.config.theme.ticks.density,
863            &waves.safe_num_timestamps(),
864        )
865    }
866}
867
868/// Get suitable tick locations for the current view port.
869/// The method is based on guessing the length of the time string and
870/// is inspired by the corresponding code in Matplotlib.
871#[allow(clippy::too_many_arguments)]
872#[must_use]
873fn get_ticks_internal(
874    viewport: &Viewport,
875    timescale: &TimeScale,
876    frame_width: f32,
877    text_size: f32,
878    wanted_timeunit: &TimeUnit,
879    time_format: &TimeFormat,
880    density: f32,
881    num_timestamps: &BigInt,
882) -> Vec<(String, f32)> {
883    let char_width = text_size * (20. / 31.);
884    let rightexp = viewport
885        .curr_right
886        .absolute(num_timestamps)
887        .inner()
888        .abs()
889        .log10()
890        .round() as i16;
891    let leftexp = viewport
892        .curr_left
893        .absolute(num_timestamps)
894        .inner()
895        .abs()
896        .log10()
897        .round() as i16;
898    let max_labelwidth = f32::from(rightexp.max(leftexp) + 3) * char_width;
899    let max_labels = ((frame_width * density) / max_labelwidth).floor() + 2.;
900    let scale = 10.0f64.powf(
901        ((viewport.curr_right - viewport.curr_left)
902            .absolute(num_timestamps)
903            .inner()
904            / f64::from(max_labels))
905        .log10()
906        .floor(),
907    );
908
909    let mut ticks: Vec<(String, f32)> = [].to_vec();
910    for step in &TICK_STEPS {
911        let scaled_step = scale * step;
912        let rounded_min_label_time =
913            (viewport.curr_left.absolute(num_timestamps).inner() / scaled_step).floor()
914                * scaled_step;
915        let high = ((viewport.curr_right.absolute(num_timestamps).inner() - rounded_min_label_time)
916            / scaled_step)
917            .ceil() as f32
918            + 1.;
919        if high <= max_labels {
920            let time_formatter = TimeFormatter::new(timescale, wanted_timeunit, time_format);
921            ticks = (0..high as i16)
922                .map(|v| {
923                    BigInt::from((f64::from(v) * scaled_step + rounded_min_label_time) as i128)
924                })
925                .unique()
926                .map(|tick| {
927                    (
928                        // Time string
929                        time_formatter.format(&tick),
930                        // X position
931                        viewport.pixel_from_time(&tick, frame_width, num_timestamps),
932                    )
933                })
934                .collect::<Vec<(String, f32)>>();
935            break;
936        }
937    }
938    ticks
939}
940
941#[cfg(test)]
942mod test {
943    use num::BigInt;
944
945    use crate::time::{TimeFormat, TimeScale, TimeStringFormatting, TimeUnit, time_string};
946
947    #[test]
948    fn print_time_standard() {
949        assert_eq!(
950            time_string(
951                &BigInt::from(103),
952                &TimeScale {
953                    multiplier: Some(1),
954                    unit: TimeUnit::FemtoSeconds
955                },
956                &TimeUnit::FemtoSeconds,
957                &TimeFormat::default()
958            ),
959            "103 fs"
960        );
961        assert_eq!(
962            time_string(
963                &BigInt::from(2200),
964                &TimeScale {
965                    multiplier: Some(1),
966                    unit: TimeUnit::MicroSeconds
967                },
968                &TimeUnit::MicroSeconds,
969                &TimeFormat::default()
970            ),
971            "2200 μs"
972        );
973        assert_eq!(
974            time_string(
975                &BigInt::from(2200),
976                &TimeScale {
977                    multiplier: Some(1),
978                    unit: TimeUnit::MicroSeconds
979                },
980                &TimeUnit::MilliSeconds,
981                &TimeFormat::default()
982            ),
983            "2.2 ms"
984        );
985        assert_eq!(
986            time_string(
987                &BigInt::from(2200),
988                &TimeScale {
989                    multiplier: Some(1),
990                    unit: TimeUnit::MicroSeconds
991                },
992                &TimeUnit::NanoSeconds,
993                &TimeFormat::default()
994            ),
995            "2200000 ns"
996        );
997        assert_eq!(
998            time_string(
999                &BigInt::from(2200),
1000                &TimeScale {
1001                    multiplier: Some(1),
1002                    unit: TimeUnit::NanoSeconds
1003                },
1004                &TimeUnit::PicoSeconds,
1005                &TimeFormat {
1006                    format: TimeStringFormatting::No,
1007                    show_space: false,
1008                    show_unit: true
1009                }
1010            ),
1011            "2200000ps"
1012        );
1013        assert_eq!(
1014            time_string(
1015                &BigInt::from(2200),
1016                &TimeScale {
1017                    multiplier: Some(10),
1018                    unit: TimeUnit::MicroSeconds
1019                },
1020                &TimeUnit::MicroSeconds,
1021                &TimeFormat {
1022                    format: TimeStringFormatting::No,
1023                    show_space: false,
1024                    show_unit: false
1025                }
1026            ),
1027            "22000"
1028        );
1029    }
1030    #[test]
1031    fn print_time_si() {
1032        assert_eq!(
1033            time_string(
1034                &BigInt::from(123456789010i128),
1035                &TimeScale {
1036                    multiplier: Some(1),
1037                    unit: TimeUnit::MicroSeconds
1038                },
1039                &TimeUnit::Seconds,
1040                &TimeFormat {
1041                    format: TimeStringFormatting::SI,
1042                    show_space: true,
1043                    show_unit: true
1044                }
1045            ),
1046            "123\u{2009}456.789\u{2009}01 s"
1047        );
1048        assert_eq!(
1049            time_string(
1050                &BigInt::from(1456789100i128),
1051                &TimeScale {
1052                    multiplier: Some(1),
1053                    unit: TimeUnit::MicroSeconds
1054                },
1055                &TimeUnit::Seconds,
1056                &TimeFormat {
1057                    format: TimeStringFormatting::SI,
1058                    show_space: true,
1059                    show_unit: true
1060                }
1061            ),
1062            "1456.7891 s"
1063        );
1064        assert_eq!(
1065            time_string(
1066                &BigInt::from(2200),
1067                &TimeScale {
1068                    multiplier: Some(1),
1069                    unit: TimeUnit::MicroSeconds
1070                },
1071                &TimeUnit::MicroSeconds,
1072                &TimeFormat {
1073                    format: TimeStringFormatting::SI,
1074                    show_space: true,
1075                    show_unit: true
1076                }
1077            ),
1078            "2200 μs"
1079        );
1080        assert_eq!(
1081            time_string(
1082                &BigInt::from(22200),
1083                &TimeScale {
1084                    multiplier: Some(1),
1085                    unit: TimeUnit::MicroSeconds
1086                },
1087                &TimeUnit::MicroSeconds,
1088                &TimeFormat {
1089                    format: TimeStringFormatting::SI,
1090                    show_space: true,
1091                    show_unit: true
1092                }
1093            ),
1094            "22\u{2009}200 μs"
1095        );
1096    }
1097    #[test]
1098    fn print_time_auto() {
1099        assert_eq!(
1100            time_string(
1101                &BigInt::from(2200),
1102                &TimeScale {
1103                    multiplier: Some(1),
1104                    unit: TimeUnit::MicroSeconds
1105                },
1106                &TimeUnit::Auto,
1107                &TimeFormat {
1108                    format: TimeStringFormatting::SI,
1109                    show_space: true,
1110                    show_unit: true
1111                }
1112            ),
1113            "2200 μs"
1114        );
1115        assert_eq!(
1116            time_string(
1117                &BigInt::from(22000),
1118                &TimeScale {
1119                    multiplier: Some(1),
1120                    unit: TimeUnit::MicroSeconds
1121                },
1122                &TimeUnit::Auto,
1123                &TimeFormat {
1124                    format: TimeStringFormatting::SI,
1125                    show_space: true,
1126                    show_unit: true
1127                }
1128            ),
1129            "22 ms"
1130        );
1131        assert_eq!(
1132            time_string(
1133                &BigInt::from(1500000000),
1134                &TimeScale {
1135                    multiplier: Some(1),
1136                    unit: TimeUnit::PicoSeconds
1137                },
1138                &TimeUnit::Auto,
1139                &TimeFormat {
1140                    format: TimeStringFormatting::SI,
1141                    show_space: true,
1142                    show_unit: true
1143                }
1144            ),
1145            "1500 μs"
1146        );
1147        assert_eq!(
1148            time_string(
1149                &BigInt::from(22000),
1150                &TimeScale {
1151                    multiplier: Some(10),
1152                    unit: TimeUnit::MicroSeconds
1153                },
1154                &TimeUnit::Auto,
1155                &TimeFormat {
1156                    format: TimeStringFormatting::SI,
1157                    show_space: true,
1158                    show_unit: true
1159                }
1160            ),
1161            "220 ms"
1162        );
1163        assert_eq!(
1164            time_string(
1165                &BigInt::from(220000),
1166                &TimeScale {
1167                    multiplier: Some(100),
1168                    unit: TimeUnit::MicroSeconds
1169                },
1170                &TimeUnit::Auto,
1171                &TimeFormat {
1172                    format: TimeStringFormatting::SI,
1173                    show_space: true,
1174                    show_unit: true
1175                }
1176            ),
1177            "22 s"
1178        );
1179        assert_eq!(
1180            time_string(
1181                &BigInt::from(22000),
1182                &TimeScale {
1183                    multiplier: Some(10),
1184                    unit: TimeUnit::Seconds
1185                },
1186                &TimeUnit::Auto,
1187                &TimeFormat {
1188                    format: TimeStringFormatting::No,
1189                    show_space: true,
1190                    show_unit: true
1191                }
1192            ),
1193            "220000 s"
1194        );
1195    }
1196    #[test]
1197    fn print_time_none() {
1198        assert_eq!(
1199            time_string(
1200                &BigInt::from(2200),
1201                &TimeScale {
1202                    multiplier: Some(1),
1203                    unit: TimeUnit::MicroSeconds
1204                },
1205                &TimeUnit::None,
1206                &TimeFormat {
1207                    format: TimeStringFormatting::No,
1208                    show_space: true,
1209                    show_unit: true
1210                }
1211            ),
1212            "2200"
1213        );
1214        assert_eq!(
1215            time_string(
1216                &BigInt::from(220),
1217                &TimeScale {
1218                    multiplier: Some(10),
1219                    unit: TimeUnit::MicroSeconds
1220                },
1221                &TimeUnit::None,
1222                &TimeFormat {
1223                    format: TimeStringFormatting::No,
1224                    show_space: true,
1225                    show_unit: true
1226                }
1227            ),
1228            "220"
1229        );
1230    }
1231
1232    #[test]
1233    fn test_strip_trailing_zeros_and_period() {
1234        use crate::time::strip_trailing_zeros_and_period;
1235
1236        assert_eq!(strip_trailing_zeros_and_period("123.000".into()), "123");
1237        assert_eq!(strip_trailing_zeros_and_period("123.450".into()), "123.45");
1238        assert_eq!(strip_trailing_zeros_and_period("123.456".into()), "123.456");
1239        assert_eq!(strip_trailing_zeros_and_period("123.".into()), "123");
1240        assert_eq!(strip_trailing_zeros_and_period("123".into()), "123");
1241        assert_eq!(strip_trailing_zeros_and_period("0.000".into()), "0");
1242        assert_eq!(strip_trailing_zeros_and_period("0.100".into()), "0.1");
1243        assert_eq!(strip_trailing_zeros_and_period(String::new()), "");
1244    }
1245
1246    #[test]
1247    fn test_format_si() {
1248        use crate::time::format_si;
1249
1250        // 4-digit rule: no grouping for 4 digits or less
1251        assert_eq!(format_si("1234.56"), "1234.56");
1252        assert_eq!(format_si("123.4"), "123.4");
1253
1254        // Grouping for 5+ digits
1255        assert_eq!(format_si("12345.67"), "12\u{2009}345.67");
1256        assert_eq!(format_si("1234567.89"), "1\u{2009}234\u{2009}567.89");
1257        // No decimal part
1258        assert_eq!(format_si("12345"), "12\u{2009}345");
1259        assert_eq!(format_si("123"), "123");
1260
1261        // Empty inputs
1262        assert_eq!(format_si("0.123"), "0.123");
1263        assert_eq!(format_si(""), "");
1264
1265        // Decimal grouping
1266        assert_eq!(format_si("123.4567890"), "123.456\u{2009}789\u{2009}0");
1267    }
1268
1269    #[test]
1270    fn test_time_unit_exponent() {
1271        // Test exponent method
1272        assert_eq!(TimeUnit::Seconds.exponent(), 0);
1273        assert_eq!(TimeUnit::MilliSeconds.exponent(), -3);
1274        assert_eq!(TimeUnit::MicroSeconds.exponent(), -6);
1275        assert_eq!(TimeUnit::NanoSeconds.exponent(), -9);
1276        assert_eq!(TimeUnit::PicoSeconds.exponent(), -12);
1277        assert_eq!(TimeUnit::FemtoSeconds.exponent(), -15);
1278        assert_eq!(TimeUnit::AttoSeconds.exponent(), -18);
1279        assert_eq!(TimeUnit::ZeptoSeconds.exponent(), -21);
1280
1281        // Test from_exponent roundtrip
1282        for unit in [
1283            TimeUnit::Seconds,
1284            TimeUnit::MilliSeconds,
1285            TimeUnit::MicroSeconds,
1286            TimeUnit::NanoSeconds,
1287            TimeUnit::PicoSeconds,
1288            TimeUnit::FemtoSeconds,
1289            TimeUnit::AttoSeconds,
1290            TimeUnit::ZeptoSeconds,
1291        ] {
1292            assert_eq!(TimeUnit::from_exponent(unit.exponent()), Some(unit));
1293        }
1294
1295        // Invalid exponents
1296        assert_eq!(TimeUnit::from_exponent(-5), None);
1297        assert_eq!(TimeUnit::from_exponent(1), None);
1298    }
1299
1300    #[test]
1301    fn test_time_string_zero() {
1302        // Test zero values
1303        assert_eq!(
1304            time_string(
1305                &BigInt::from(0),
1306                &TimeScale {
1307                    multiplier: Some(1),
1308                    unit: TimeUnit::MicroSeconds
1309                },
1310                &TimeUnit::MicroSeconds,
1311                &TimeFormat::default()
1312            ),
1313            "0 μs"
1314        );
1315
1316        assert_eq!(
1317            time_string(
1318                &BigInt::from(0),
1319                &TimeScale {
1320                    multiplier: Some(1),
1321                    unit: TimeUnit::Seconds
1322                },
1323                &TimeUnit::Auto,
1324                &TimeFormat::default()
1325            ),
1326            "0 s"
1327        );
1328    }
1329
1330    #[test]
1331    fn test_time_string_large_numbers() {
1332        // Test very large numbers with SI formatting
1333        assert_eq!(
1334            time_string(
1335                &BigInt::from(999_999_999_999i64),
1336                &TimeScale {
1337                    multiplier: Some(1),
1338                    unit: TimeUnit::NanoSeconds
1339                },
1340                &TimeUnit::Seconds,
1341                &TimeFormat {
1342                    format: TimeStringFormatting::SI,
1343                    show_space: true,
1344                    show_unit: true
1345                }
1346            ),
1347            "999.999\u{2009}999\u{2009}999 s"
1348        );
1349    }
1350
1351    #[test]
1352    fn test_time_string_no_multiplier() {
1353        // Test with None multiplier (raw ticks)
1354        assert_eq!(
1355            time_string(
1356                &BigInt::from(1234),
1357                &TimeScale {
1358                    multiplier: None,
1359                    unit: TimeUnit::NanoSeconds
1360                },
1361                &TimeUnit::NanoSeconds,
1362                &TimeFormat::default()
1363            ),
1364            "1234 ns"
1365        );
1366    }
1367
1368    #[test]
1369    fn test_time_format_variations() {
1370        let value = BigInt::from(123456);
1371        let scale = TimeScale {
1372            multiplier: Some(1),
1373            unit: TimeUnit::NanoSeconds,
1374        };
1375
1376        // Test all format variations
1377        assert_eq!(
1378            time_string(
1379                &value,
1380                &scale,
1381                &TimeUnit::NanoSeconds,
1382                &TimeFormat {
1383                    format: TimeStringFormatting::No,
1384                    show_space: true,
1385                    show_unit: true
1386                }
1387            ),
1388            "123456 ns"
1389        );
1390
1391        assert_eq!(
1392            time_string(
1393                &value,
1394                &scale,
1395                &TimeUnit::NanoSeconds,
1396                &TimeFormat {
1397                    format: TimeStringFormatting::No,
1398                    show_space: false,
1399                    show_unit: true
1400                }
1401            ),
1402            "123456ns"
1403        );
1404
1405        assert_eq!(
1406            time_string(
1407                &value,
1408                &scale,
1409                &TimeUnit::NanoSeconds,
1410                &TimeFormat {
1411                    format: TimeStringFormatting::No,
1412                    show_space: true,
1413                    show_unit: false
1414                }
1415            ),
1416            "123456"
1417        );
1418
1419        assert_eq!(
1420            time_string(
1421                &value,
1422                &scale,
1423                &TimeUnit::NanoSeconds,
1424                &TimeFormat {
1425                    format: TimeStringFormatting::SI,
1426                    show_space: true,
1427                    show_unit: true
1428                }
1429            ),
1430            "123\u{2009}456 ns"
1431        );
1432    }
1433
1434    #[test]
1435    fn test_find_auto_scale_seconds_passthrough() {
1436        use crate::time::find_auto_scale;
1437
1438        let ts = TimeScale {
1439            unit: TimeUnit::Seconds,
1440            multiplier: Some(1),
1441        };
1442        assert_eq!(find_auto_scale(&BigInt::from(1), &ts), TimeUnit::Seconds);
1443        assert_eq!(
1444            find_auto_scale(&BigInt::from(1_234_567), &ts),
1445            TimeUnit::Seconds
1446        );
1447    }
1448
1449    #[test]
1450    fn test_find_auto_scale_nanoseconds() {
1451        use crate::time::find_auto_scale;
1452
1453        let ts = TimeScale {
1454            unit: TimeUnit::NanoSeconds,
1455            multiplier: Some(1),
1456        };
1457
1458        // Divisible by 10^9 -> seconds
1459        assert_eq!(
1460            find_auto_scale(&BigInt::from(1_000_000_000i64), &ts),
1461            TimeUnit::Seconds
1462        );
1463        // Divisible by 10^6 -> milliseconds
1464        assert_eq!(
1465            find_auto_scale(&BigInt::from(1_000_000), &ts),
1466            TimeUnit::MilliSeconds
1467        );
1468        // Divisible by 10^3 -> microseconds
1469        assert_eq!(
1470            find_auto_scale(&BigInt::from(1_000), &ts),
1471            TimeUnit::MicroSeconds
1472        );
1473        // Not divisible by 10^3 -> stay at nanos
1474        assert_eq!(
1475            find_auto_scale(&BigInt::from(1234), &ts),
1476            TimeUnit::NanoSeconds
1477        );
1478    }
1479
1480    #[test]
1481    fn test_find_auto_scale_microseconds_with_multiplier() {
1482        use crate::time::find_auto_scale;
1483
1484        // multiplier: None (treated as 1)
1485        let ts_none = TimeScale {
1486            unit: TimeUnit::MicroSeconds,
1487            multiplier: None,
1488        };
1489        assert_eq!(
1490            find_auto_scale(&BigInt::from(1_000_000), &ts_none),
1491            TimeUnit::Seconds
1492        );
1493        assert_eq!(
1494            find_auto_scale(&BigInt::from(1_000), &ts_none),
1495            TimeUnit::MilliSeconds
1496        );
1497        assert_eq!(
1498            find_auto_scale(&BigInt::from(123), &ts_none),
1499            TimeUnit::MicroSeconds
1500        );
1501
1502        // multiplier: Some(10) -> reduces required divisibility by 10^1
1503        let ts_mul10 = TimeScale {
1504            unit: TimeUnit::MicroSeconds,
1505            multiplier: Some(10),
1506        };
1507        assert_eq!(
1508            find_auto_scale(&BigInt::from(100_000), &ts_mul10),
1509            TimeUnit::Seconds
1510        );
1511        assert_eq!(
1512            find_auto_scale(&BigInt::from(100), &ts_mul10),
1513            TimeUnit::MilliSeconds
1514        );
1515        assert_eq!(
1516            find_auto_scale(&BigInt::from(123), &ts_mul10),
1517            TimeUnit::MicroSeconds
1518        );
1519    }
1520
1521    #[test]
1522    fn test_find_auto_scale_femtoseconds() {
1523        use crate::time::find_auto_scale;
1524
1525        let ts = TimeScale {
1526            unit: TimeUnit::FemtoSeconds,
1527            multiplier: Some(1),
1528        };
1529        // 10^15 fs = 1 s
1530        assert_eq!(
1531            find_auto_scale(&BigInt::from(10_i128.pow(15)), &ts),
1532            TimeUnit::Seconds
1533        );
1534        // 10^12 fs = 1 ms
1535        assert_eq!(
1536            find_auto_scale(&BigInt::from(10_i128.pow(12)), &ts),
1537            TimeUnit::MilliSeconds
1538        );
1539        // 10^9 fs = 1 μs
1540        assert_eq!(
1541            find_auto_scale(&BigInt::from(10_i128.pow(9)), &ts),
1542            TimeUnit::MicroSeconds
1543        );
1544        // 10^6 fs = 1 ns
1545        assert_eq!(
1546            find_auto_scale(&BigInt::from(10_i128.pow(6)), &ts),
1547            TimeUnit::NanoSeconds
1548        );
1549        // 10^3 fs = 1 ps
1550        assert_eq!(
1551            find_auto_scale(&BigInt::from(10_i128.pow(3)), &ts),
1552            TimeUnit::PicoSeconds
1553        );
1554        // Not divisible by 10^3 -> stay at fs
1555        assert_eq!(
1556            find_auto_scale(&BigInt::from(1), &ts),
1557            TimeUnit::FemtoSeconds
1558        );
1559    }
1560
1561    #[test]
1562    fn test_locale_cache_en_us() {
1563        use crate::time::{create_cache, format_locale};
1564        use pure_rust_locales::Locale;
1565
1566        let locale = Locale::en_US;
1567        let cache = create_cache(locale);
1568
1569        // en_US uses period as decimal point and comma as thousands separator
1570        let result = format_locale("1234567.89", &cache);
1571        assert_eq!(result, "1,234,567.89");
1572    }
1573
1574    #[test]
1575    fn test_locale_cache_de_de() {
1576        use crate::time::{create_cache, format_locale};
1577        use pure_rust_locales::Locale;
1578
1579        let locale = Locale::de_DE;
1580        let cache = create_cache(locale);
1581
1582        let result = format_locale("1234567.89", &cache);
1583        assert_eq!(result, "1.234.567,89");
1584    }
1585
1586    #[test]
1587    fn test_locale_cache_fr_fr() {
1588        use crate::time::{create_cache, format_locale};
1589        use pure_rust_locales::Locale;
1590
1591        let locale = Locale::fr_FR;
1592        let cache = create_cache(locale);
1593
1594        // fr_FR typically uses space/thin_space and comma
1595        let result = format_locale("1234567.89", &cache);
1596        // Verify it produces valid output
1597        assert_eq!(result, "1\u{2009}234\u{2009}567,89");
1598    }
1599
1600    #[test]
1601    fn test_locale_cache_small_numbers() {
1602        use crate::time::{create_cache, format_locale};
1603        use pure_rust_locales::Locale;
1604
1605        let locale = Locale::en_US;
1606        let cache = create_cache(locale);
1607
1608        // Numbers smaller than grouping threshold should remain unchanged
1609        assert_eq!(format_locale("123", &cache), "123");
1610        assert_eq!(format_locale("12.34", &cache), "12.34");
1611        assert_eq!(format_locale("0", &cache), "0");
1612    }
1613
1614    #[test]
1615    fn test_locale_cache_consistency_across_locales() {
1616        use crate::time::create_cache;
1617        use pure_rust_locales::Locale;
1618
1619        // Verify that creating cache for the same locale twice produces consistent results
1620        let cache1 = create_cache(Locale::en_US);
1621        let cache2 = create_cache(Locale::en_US);
1622
1623        assert_eq!(cache1.thousands_sep, cache2.thousands_sep);
1624        assert_eq!(cache1.decimal_point, cache2.decimal_point);
1625        assert_eq!(cache1.grouping, cache2.grouping);
1626    }
1627
1628    #[test]
1629    fn test_create_cache_from_various_locales() {
1630        use crate::time::{create_cache, format_locale};
1631        use pure_rust_locales::Locale;
1632
1633        // Test that create_cache works for many Locale variants without panicking
1634        let locales = vec![
1635            Locale::en_US,
1636            Locale::de_DE,
1637            Locale::fr_FR,
1638            Locale::es_ES,
1639            Locale::it_IT,
1640            Locale::pt_BR,
1641            Locale::pt_PT,
1642            Locale::ja_JP,
1643            Locale::zh_CN,
1644            Locale::zh_TW,
1645            Locale::ru_RU,
1646            Locale::ko_KR,
1647            Locale::pl_PL,
1648            Locale::tr_TR,
1649            Locale::nl_NL,
1650            Locale::sv_SE,
1651            Locale::da_DK,
1652            Locale::fi_FI,
1653            Locale::el_GR,
1654            Locale::hu_HU,
1655            Locale::cs_CZ,
1656            Locale::ro_RO,
1657            Locale::th_TH,
1658            Locale::vi_VN,
1659            Locale::ar_SA,
1660            Locale::he_IL,
1661            Locale::id_ID,
1662            Locale::uk_UA,
1663            Locale::en_GB,
1664            Locale::en_AU,
1665            Locale::en_CA,
1666            Locale::en_NZ,
1667            Locale::en_IN,
1668            Locale::fr_CA,
1669            Locale::de_AT,
1670            Locale::de_CH,
1671            Locale::fr_CH,
1672            Locale::it_CH,
1673            Locale::es_MX,
1674            Locale::es_AR,
1675        ];
1676
1677        for locale in locales {
1678            let cache = create_cache(locale);
1679            // Check so that it is not empty for a sample number
1680            assert!(
1681                !format_locale("1234567.89", &cache).is_empty(),
1682                "Failed for {locale:?}"
1683            );
1684        }
1685    }
1686}
1687
1688#[cfg(test)]
1689mod get_ticks_tests {
1690    use super::*;
1691    use itertools::Itertools;
1692    use num::BigInt;
1693
1694    // Basic smoke test: ensure we get at least one tick and that returned
1695    // pixel coordinates lie within the frame width.
1696    #[test]
1697    fn get_ticks_basic() {
1698        let vp = crate::viewport::Viewport::default();
1699        let timescale = TimeScale {
1700            unit: TimeUnit::MicroSeconds,
1701            multiplier: Some(1),
1702        };
1703        let frame_width = 800.0_f32;
1704        let text_size = 12.0_f32;
1705        let wanted = TimeUnit::MicroSeconds;
1706        let time_format = TimeFormat::default();
1707        let config = crate::config::SurferConfig::default();
1708        let num_timestamps = BigInt::from(1_000_000i64);
1709
1710        let ticks = get_ticks_internal(
1711            &vp,
1712            &timescale,
1713            frame_width,
1714            text_size,
1715            &wanted,
1716            &time_format,
1717            config.theme.ticks.density,
1718            &num_timestamps,
1719        );
1720
1721        assert!(!ticks.is_empty(), "expected at least one tick");
1722
1723        // Check monotonic x positions and collect labels for uniqueness check
1724        let mut last_x = -1.0_f32;
1725        let mut labels: Vec<String> = Vec::with_capacity(ticks.len());
1726        for (label, x) in &ticks {
1727            assert!(
1728                *x >= last_x,
1729                "tick x not monotonic: {x} < {last_x} for label {label}"
1730            );
1731            last_x = *x;
1732            assert!(*x >= 0.0, "tick x < 0: {x}");
1733            assert!(
1734                *x <= frame_width,
1735                "tick x > frame_width: {x} > {frame_width}"
1736            );
1737            labels.push(label.clone());
1738        }
1739        // Labels should be unique
1740        let unique_labels = labels.iter().unique().count();
1741        assert_eq!(labels.len(), unique_labels, "duplicate tick labels found");
1742    }
1743
1744    // Ensure tick generation produces a reasonable number of ticks when
1745    // viewport is zoomed and density is high.
1746    #[test]
1747    fn get_ticks_respects_frame_width_and_density() {
1748        let mut vp = crate::viewport::Viewport::default();
1749        // zoom to a narrower view
1750        vp.curr_left = crate::viewport::Relative(0.0);
1751        vp.curr_right = crate::viewport::Relative(0.1);
1752
1753        let timescale = TimeScale {
1754            unit: TimeUnit::NanoSeconds,
1755            multiplier: Some(1),
1756        };
1757        let frame_width = 200.0_f32;
1758        let text_size = 10.0_f32;
1759        let wanted = TimeUnit::Auto;
1760        let time_format = TimeFormat {
1761            format: TimeStringFormatting::SI,
1762            show_space: true,
1763            show_unit: true,
1764        };
1765
1766        let mut config = crate::config::SurferConfig::default();
1767        // make ticks dense
1768        config.theme.ticks.density = 1.0;
1769
1770        let num_timestamps = BigInt::from(1_000_000i64);
1771
1772        let ticks = get_ticks_internal(
1773            &vp,
1774            &timescale,
1775            frame_width,
1776            text_size,
1777            &wanted,
1778            &time_format,
1779            config.theme.ticks.density,
1780            &num_timestamps,
1781        );
1782
1783        assert!(!ticks.is_empty(), "expected ticks even for narrow view");
1784        // expect a sane upper bound (protects against accidental infinite loops)
1785        assert!(ticks.len() < 200, "too many ticks: {}", ticks.len());
1786
1787        // monotonic x positions and unique labels
1788        let mut last_x = -1.0_f32;
1789        let mut labels: Vec<String> = Vec::with_capacity(ticks.len());
1790        for (label, x) in &ticks {
1791            assert!(
1792                *x >= last_x,
1793                "tick x not monotonic: {x} < {last_x} for label {label}"
1794            );
1795            last_x = *x;
1796            assert!(*x >= 0.0, "tick x < 0: {x}");
1797            assert!(
1798                *x <= frame_width,
1799                "tick x > frame_width: {x} > {frame_width}"
1800            );
1801            labels.push(label.clone());
1802        }
1803        let unique_labels = labels.iter().unique().count();
1804        assert_eq!(labels.len(), unique_labels, "duplicate tick labels found");
1805    }
1806}
1807
1808#[cfg(test)]
1809mod time_input_tests {
1810    use super::*;
1811
1812    #[test]
1813    fn test_parse_time_input_simple() {
1814        let (num, unit) = parse_time_input("100");
1815        assert_eq!(num, "100");
1816        assert_eq!(unit, None);
1817    }
1818
1819    #[test]
1820    fn test_parse_time_input_unit_only_no_panic() {
1821        // Unit-only input should not panic and should be treated as plain text.
1822        let (num, unit) = parse_time_input("ns");
1823        assert_eq!(num, "ns");
1824        assert_eq!(unit, None);
1825    }
1826
1827    #[test]
1828    fn test_parse_time_input_with_unit_no_space() {
1829        let (num, unit) = parse_time_input("100ns");
1830        assert_eq!(num, "100");
1831        assert_eq!(unit, Some(TimeUnit::NanoSeconds));
1832
1833        let (num, unit) = parse_time_input("50ps");
1834        assert_eq!(num, "50");
1835        assert_eq!(unit, Some(TimeUnit::PicoSeconds));
1836
1837        let (num, unit) = parse_time_input("1.5ms");
1838        assert_eq!(num, "1.5");
1839        assert_eq!(unit, Some(TimeUnit::MilliSeconds));
1840    }
1841
1842    #[test]
1843    fn test_parse_time_input_with_unit_space() {
1844        let (num, unit) = parse_time_input("100 ns");
1845        assert_eq!(num, "100");
1846        assert_eq!(unit, Some(TimeUnit::NanoSeconds));
1847
1848        let (num, unit) = parse_time_input("1.5 ms");
1849        assert_eq!(num, "1.5");
1850        assert_eq!(unit, Some(TimeUnit::MilliSeconds));
1851
1852        let (num, unit) = parse_time_input("100\tms");
1853        assert_eq!(num, "100");
1854        assert_eq!(unit, Some(TimeUnit::MilliSeconds));
1855
1856        let (num, unit) = parse_time_input("100    ns");
1857        assert_eq!(num, "100");
1858        assert_eq!(unit, Some(TimeUnit::NanoSeconds));
1859    }
1860
1861    #[test]
1862    fn test_parse_time_input_microseconds_unicode() {
1863        let (num, unit) = parse_time_input("100μs");
1864        assert_eq!(num, "100");
1865        assert_eq!(unit, Some(TimeUnit::MicroSeconds));
1866
1867        let (num, unit) = parse_time_input("50 μs");
1868        assert_eq!(num, "50");
1869        assert_eq!(unit, Some(TimeUnit::MicroSeconds));
1870    }
1871
1872    #[test]
1873    fn test_parse_time_input_microseconds_ascii() {
1874        // Support "us" as alternative to "μs"
1875        let (num, unit) = parse_time_input("100us");
1876        assert_eq!(num, "100");
1877        assert_eq!(unit, Some(TimeUnit::MicroSeconds));
1878    }
1879
1880    #[test]
1881    fn test_parse_time_input_seconds() {
1882        let (num, unit) = parse_time_input("10s");
1883        assert_eq!(num, "10");
1884        assert_eq!(unit, Some(TimeUnit::Seconds));
1885
1886        let (num, unit) = parse_time_input("0.5s");
1887        assert_eq!(num, "0.5");
1888        assert_eq!(unit, Some(TimeUnit::Seconds));
1889    }
1890
1891    #[test]
1892    fn test_parse_time_input_femtoseconds() {
1893        let (num, unit) = parse_time_input("1000000fs");
1894        assert_eq!(num, "1000000");
1895        assert_eq!(unit, Some(TimeUnit::FemtoSeconds));
1896    }
1897
1898    #[test]
1899    fn test_parse_time_input_with_whitespace() {
1900        let (num, unit) = parse_time_input("  100ns  ");
1901        assert_eq!(num, "100");
1902        assert_eq!(unit, Some(TimeUnit::NanoSeconds));
1903    }
1904
1905    #[test]
1906    fn test_split_numeric_parts() {
1907        assert_eq!(
1908            split_numeric_parts("100").ok(),
1909            Some(("100".to_string(), "".to_string()))
1910        );
1911        assert_eq!(
1912            split_numeric_parts("100.").ok(),
1913            Some(("100".to_string(), "".to_string()))
1914        );
1915        assert_eq!(
1916            split_numeric_parts(".5").ok(),
1917            Some(("0".to_string(), "5".to_string()))
1918        );
1919        assert_eq!(
1920            split_numeric_parts("1.5").ok(),
1921            Some(("1".to_string(), "5".to_string()))
1922        );
1923    }
1924
1925    #[test]
1926    fn test_split_numeric_parts_invalid() {
1927        assert!(split_numeric_parts("").is_err());
1928        assert!(split_numeric_parts("abc").is_err());
1929        assert!(split_numeric_parts("12.34.56").is_err());
1930        assert!(split_numeric_parts("-1").is_err());
1931    }
1932
1933    #[test]
1934    fn test_normalize_numeric_with_unit_integer() {
1935        let (val, unit) = normalize_numeric_with_unit("100", TimeUnit::NanoSeconds).unwrap();
1936        assert_eq!(val, BigInt::from(100));
1937        assert_eq!(unit, TimeUnit::NanoSeconds);
1938    }
1939
1940    #[test]
1941    fn test_normalize_numeric_with_unit_decimal_single_step() {
1942        let (val, unit) = normalize_numeric_with_unit("1.5", TimeUnit::NanoSeconds).unwrap();
1943        assert_eq!(val, BigInt::from(1500));
1944        assert_eq!(unit, TimeUnit::PicoSeconds);
1945    }
1946
1947    #[test]
1948    fn test_normalize_numeric_with_unit_decimal_multi_step() {
1949        let (val, unit) = normalize_numeric_with_unit("1.2345", TimeUnit::NanoSeconds).unwrap();
1950        assert_eq!(val, BigInt::from(1_234_500));
1951        assert_eq!(unit, TimeUnit::FemtoSeconds);
1952    }
1953
1954    #[test]
1955    fn test_time_input_state_default() {
1956        let state = TimeInputState::default();
1957        assert_eq!(state.input_text, "");
1958        assert_eq!(state.parsed_value, None);
1959        assert_eq!(state.input_unit, None);
1960        assert_eq!(state.normalized_unit, None);
1961        assert_eq!(state.selected_unit, TimeUnit::NanoSeconds);
1962        assert_eq!(state.error, None);
1963    }
1964
1965    #[test]
1966    fn test_time_input_state_update_valid() {
1967        let mut state = TimeInputState::new();
1968        state.update_input("100ns".to_string());
1969
1970        assert_eq!(state.input_text, "100ns");
1971        assert_eq!(state.parsed_value, Some(BigInt::from(100)));
1972        assert_eq!(state.input_unit, Some(TimeUnit::NanoSeconds));
1973        assert_eq!(state.normalized_unit, Some(TimeUnit::NanoSeconds));
1974        assert_eq!(state.error, None);
1975    }
1976
1977    #[test]
1978    fn test_time_input_state_update_invalid() {
1979        let mut state = TimeInputState::new();
1980        state.update_input("abc".to_string());
1981
1982        assert_eq!(state.parsed_value, None);
1983        assert!(state.error.is_some());
1984    }
1985
1986    #[test]
1987    fn test_time_input_state_update_decimal_unit_normalization() {
1988        let mut state = TimeInputState::new();
1989        state.update_input("1.5ns".to_string());
1990
1991        assert_eq!(state.parsed_value, Some(BigInt::from(1500)));
1992        assert_eq!(state.input_unit, Some(TimeUnit::NanoSeconds));
1993        assert_eq!(state.normalized_unit, Some(TimeUnit::PicoSeconds));
1994        assert_eq!(state.effective_unit(), TimeUnit::PicoSeconds);
1995        assert_eq!(state.error, None);
1996    }
1997
1998    #[test]
1999    fn test_time_input_state_to_timescale_ticks_invalid() {
2000        let state = TimeInputState::new();
2001        let timescale = TimeScale {
2002            unit: TimeUnit::NanoSeconds,
2003            multiplier: None,
2004        };
2005
2006        assert_eq!(state.to_timescale_ticks(&timescale), None);
2007    }
2008
2009    #[test]
2010    fn test_time_input_comprehensive_example() {
2011        // User types "2.5 ms"
2012        let mut state = TimeInputState::new();
2013        state.update_input("2.5 ms".to_string());
2014
2015        // Parse should succeed
2016        assert_eq!(state.parsed_value, Some(BigInt::from(2500)));
2017        assert_eq!(state.input_unit, Some(TimeUnit::MilliSeconds));
2018        assert_eq!(state.effective_unit(), TimeUnit::MicroSeconds);
2019        assert_eq!(state.error, None);
2020
2021        // Should be able to convert to timescale ticks
2022        let timescale = TimeScale {
2023            unit: TimeUnit::MicroSeconds,
2024            multiplier: None,
2025        };
2026        assert_eq!(
2027            state.to_timescale_ticks(&timescale),
2028            Some(BigInt::from(2500))
2029        );
2030    }
2031
2032    #[test]
2033    fn test_parse_time_longest_match_first() {
2034        // "ms" should match before "s"
2035        let (num, unit) = parse_time_input("100ms");
2036        assert_eq!(num, "100");
2037        assert_eq!(unit, Some(TimeUnit::MilliSeconds));
2038
2039        // Not just the "s"
2040        let (_, unit) = parse_time_input("100s");
2041        assert_ne!(unit, Some(TimeUnit::MilliSeconds));
2042    }
2043
2044    #[test]
2045    fn test_parse_time_no_false_positives() {
2046        // These should not match units
2047        let (num, unit) = parse_time_input("mass");
2048        assert_eq!(num, "mass");
2049        assert_eq!(unit, None);
2050
2051        let (num, unit) = parse_time_input("uses");
2052        assert_eq!(num, "uses");
2053        assert_eq!(unit, None);
2054    }
2055
2056    #[test]
2057    fn test_time_input_state_clear() {
2058        let mut state = TimeInputState::new();
2059        state.update_input("100ns".to_string());
2060        assert!(state.parsed_value.is_some());
2061
2062        // Clear by updating with empty string
2063        state.update_input(String::new());
2064        assert!(state.error.is_some());
2065    }
2066}