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