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::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(exp as u32),
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 time_offset = waves.time_offset();
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            &waves.safe_max_timestamp(),
971            time_offset,
972        )
973    }
974}
975
976/// Get suitable tick locations for the current view port.
977/// The method is based on guessing the length of the time string and
978/// is inspired by the corresponding code in Matplotlib.
979#[allow(clippy::too_many_arguments)]
980#[must_use]
981fn get_ticks_internal(
982    viewport: &Viewport,
983    timescale: &TimeScale,
984    frame_width: f32,
985    text_size: f32,
986    wanted_timeunit: &TimeUnit,
987    time_format: &TimeFormat,
988    density: f32,
989    max_timestamp: &BigInt,
990    time_offset: &BigInt,
991) -> Vec<(String, f32, i64)> {
992    let char_width = text_size * (20. / 31.);
993    let rightexp = viewport
994        .curr_right
995        .absolute(max_timestamp, time_offset)
996        .inner()
997        .abs()
998        .log10()
999        .round() as i16;
1000    let leftexp = viewport
1001        .curr_left
1002        .absolute(max_timestamp, time_offset)
1003        .inner()
1004        .abs()
1005        .log10()
1006        .round() as i16;
1007    let max_labelwidth = f32::from(rightexp.max(leftexp) + 3) * char_width;
1008    let max_labels = ((frame_width * density) / max_labelwidth).floor() + 2.;
1009    let viewport_width = viewport.width_absolute(max_timestamp, time_offset);
1010    let scale = 10.0f64.powf(
1011        (viewport_width.inner() / f64::from(max_labels))
1012            .log10()
1013            .floor(),
1014    );
1015
1016    let mut ticks: Vec<(String, f32, i64)> = [].to_vec();
1017    for step in &TICK_STEPS {
1018        let scaled_step = scale * step;
1019        let left_abs = viewport
1020            .curr_left
1021            .absolute(max_timestamp, time_offset)
1022            .inner();
1023        let right_abs = viewport
1024            .curr_right
1025            .absolute(max_timestamp, time_offset)
1026            .inner();
1027        let rounded_min_label_time = (left_abs / scaled_step).floor() * scaled_step;
1028        let high = ((right_abs - rounded_min_label_time) / scaled_step).ceil() as f32 + 1.;
1029
1030        if high <= max_labels {
1031            let time_formatter = TimeFormatter::new(timescale, wanted_timeunit, time_format);
1032            ticks = (0..high as i16)
1033                .map(|v| {
1034                    BigInt::from((f64::from(v) * scaled_step + rounded_min_label_time) as i128)
1035                })
1036                .unique()
1037                .map(|tick| {
1038                    (
1039                        // Time string
1040                        time_formatter.format(&tick),
1041                        // X position
1042                        viewport.pixel_from_time(&tick, frame_width, max_timestamp, time_offset),
1043                        // Absolute time
1044                        tick.to_i64().unwrap_or_default(),
1045                    )
1046                })
1047                .collect::<Vec<(String, f32, i64)>>();
1048            break;
1049        }
1050    }
1051    ticks
1052}
1053
1054/// Parse a time string (optionally with units) and convert to timescale ticks.
1055///
1056/// Accepts plain integers (`"100"`) and unit-suffixed values (`"100ns"`, `"1.5 ms"`).
1057/// When no unit is present the value is treated as raw timescale ticks (same as the
1058/// previous behaviour of `cursor_set`).
1059/// Returns `None` if the input cannot be parsed.
1060pub(crate) fn parse_time_string_to_ticks(input: &str, timescale: &TimeScale) -> Option<BigInt> {
1061    let (numeric_str, unit_opt) = parse_time_input(input);
1062
1063    let base_unit = unit_opt.unwrap_or(TimeUnit::None);
1064    let (value, normalized_unit) = normalize_numeric_with_unit(&numeric_str, base_unit).ok()?;
1065
1066    if normalized_unit == TimeUnit::None {
1067        // No unit: treat value as raw ticks (plain integer path)
1068        return Some(value);
1069    }
1070
1071    let unit_exp = normalized_unit.exponent();
1072    let data_exp = timescale.unit.exponent();
1073    let diff = unit_exp - data_exp;
1074
1075    scale_time(&value, diff, timescale)
1076}
1077
1078fn scale_time(value: &BigInt, exponent_diff: i8, timescale: &TimeScale) -> Option<BigInt> {
1079    let exponent_diff = exponent_diff - timescale.multiplier_digits() as i8;
1080    let result = if exponent_diff > 0 {
1081        let scale = pow10(exponent_diff as u8);
1082        value * scale
1083    } else if exponent_diff < 0 {
1084        let scale = pow10((-exponent_diff) as u8);
1085        value / scale
1086    } else {
1087        value.clone()
1088    };
1089
1090    Some(result)
1091}
1092
1093#[cfg(test)]
1094mod test {
1095    use num::BigInt;
1096
1097    use crate::time::{TimeFormat, TimeScale, TimeStringFormatting, TimeUnit, time_string};
1098
1099    #[test]
1100    fn print_time_standard() {
1101        assert_eq!(
1102            time_string(
1103                &BigInt::from(103),
1104                &TimeScale {
1105                    multiplier: Some(1),
1106                    unit: TimeUnit::FemtoSeconds
1107                },
1108                &TimeUnit::FemtoSeconds,
1109                &TimeFormat::default()
1110            ),
1111            "103 fs"
1112        );
1113        assert_eq!(
1114            time_string(
1115                &BigInt::from(2200),
1116                &TimeScale {
1117                    multiplier: Some(1),
1118                    unit: TimeUnit::MicroSeconds
1119                },
1120                &TimeUnit::MicroSeconds,
1121                &TimeFormat::default()
1122            ),
1123            "2200 μs"
1124        );
1125        assert_eq!(
1126            time_string(
1127                &BigInt::from(2200),
1128                &TimeScale {
1129                    multiplier: Some(1),
1130                    unit: TimeUnit::MicroSeconds
1131                },
1132                &TimeUnit::MilliSeconds,
1133                &TimeFormat::default()
1134            ),
1135            "2.2 ms"
1136        );
1137        assert_eq!(
1138            time_string(
1139                &BigInt::from(2200),
1140                &TimeScale {
1141                    multiplier: Some(1),
1142                    unit: TimeUnit::MicroSeconds
1143                },
1144                &TimeUnit::NanoSeconds,
1145                &TimeFormat::default()
1146            ),
1147            "2200000 ns"
1148        );
1149        assert_eq!(
1150            time_string(
1151                &BigInt::from(2200),
1152                &TimeScale {
1153                    multiplier: Some(1),
1154                    unit: TimeUnit::NanoSeconds
1155                },
1156                &TimeUnit::PicoSeconds,
1157                &TimeFormat {
1158                    format: TimeStringFormatting::No,
1159                    show_space: false,
1160                    show_unit: true
1161                }
1162            ),
1163            "2200000ps"
1164        );
1165        assert_eq!(
1166            time_string(
1167                &BigInt::from(2200),
1168                &TimeScale {
1169                    multiplier: Some(10),
1170                    unit: TimeUnit::MicroSeconds
1171                },
1172                &TimeUnit::MicroSeconds,
1173                &TimeFormat {
1174                    format: TimeStringFormatting::No,
1175                    show_space: false,
1176                    show_unit: false
1177                }
1178            ),
1179            "22000"
1180        );
1181    }
1182    #[test]
1183    fn print_time_si() {
1184        assert_eq!(
1185            time_string(
1186                &BigInt::from(123456789010i128),
1187                &TimeScale {
1188                    multiplier: Some(1),
1189                    unit: TimeUnit::MicroSeconds
1190                },
1191                &TimeUnit::Seconds,
1192                &TimeFormat {
1193                    format: TimeStringFormatting::SI,
1194                    show_space: true,
1195                    show_unit: true
1196                }
1197            ),
1198            "123\u{2009}456.789\u{2009}01 s"
1199        );
1200        assert_eq!(
1201            time_string(
1202                &BigInt::from(1456789100i128),
1203                &TimeScale {
1204                    multiplier: Some(1),
1205                    unit: TimeUnit::MicroSeconds
1206                },
1207                &TimeUnit::Seconds,
1208                &TimeFormat {
1209                    format: TimeStringFormatting::SI,
1210                    show_space: true,
1211                    show_unit: true
1212                }
1213            ),
1214            "1456.7891 s"
1215        );
1216        assert_eq!(
1217            time_string(
1218                &BigInt::from(2200),
1219                &TimeScale {
1220                    multiplier: Some(1),
1221                    unit: TimeUnit::MicroSeconds
1222                },
1223                &TimeUnit::MicroSeconds,
1224                &TimeFormat {
1225                    format: TimeStringFormatting::SI,
1226                    show_space: true,
1227                    show_unit: true
1228                }
1229            ),
1230            "2200 μs"
1231        );
1232        assert_eq!(
1233            time_string(
1234                &BigInt::from(22200),
1235                &TimeScale {
1236                    multiplier: Some(1),
1237                    unit: TimeUnit::MicroSeconds
1238                },
1239                &TimeUnit::MicroSeconds,
1240                &TimeFormat {
1241                    format: TimeStringFormatting::SI,
1242                    show_space: true,
1243                    show_unit: true
1244                }
1245            ),
1246            "22\u{2009}200 μs"
1247        );
1248    }
1249    #[test]
1250    fn print_time_auto() {
1251        assert_eq!(
1252            time_string(
1253                &BigInt::from(2200),
1254                &TimeScale {
1255                    multiplier: Some(1),
1256                    unit: TimeUnit::MicroSeconds
1257                },
1258                &TimeUnit::Auto,
1259                &TimeFormat {
1260                    format: TimeStringFormatting::SI,
1261                    show_space: true,
1262                    show_unit: true
1263                }
1264            ),
1265            "2200 μs"
1266        );
1267        assert_eq!(
1268            time_string(
1269                &BigInt::from(22000),
1270                &TimeScale {
1271                    multiplier: Some(1),
1272                    unit: TimeUnit::MicroSeconds
1273                },
1274                &TimeUnit::Auto,
1275                &TimeFormat {
1276                    format: TimeStringFormatting::SI,
1277                    show_space: true,
1278                    show_unit: true
1279                }
1280            ),
1281            "22 ms"
1282        );
1283        assert_eq!(
1284            time_string(
1285                &BigInt::from(1500000000),
1286                &TimeScale {
1287                    multiplier: Some(1),
1288                    unit: TimeUnit::PicoSeconds
1289                },
1290                &TimeUnit::Auto,
1291                &TimeFormat {
1292                    format: TimeStringFormatting::SI,
1293                    show_space: true,
1294                    show_unit: true
1295                }
1296            ),
1297            "1500 μs"
1298        );
1299        assert_eq!(
1300            time_string(
1301                &BigInt::from(22000),
1302                &TimeScale {
1303                    multiplier: Some(10),
1304                    unit: TimeUnit::MicroSeconds
1305                },
1306                &TimeUnit::Auto,
1307                &TimeFormat {
1308                    format: TimeStringFormatting::SI,
1309                    show_space: true,
1310                    show_unit: true
1311                }
1312            ),
1313            "220 ms"
1314        );
1315        assert_eq!(
1316            time_string(
1317                &BigInt::from(220000),
1318                &TimeScale {
1319                    multiplier: Some(100),
1320                    unit: TimeUnit::MicroSeconds
1321                },
1322                &TimeUnit::Auto,
1323                &TimeFormat {
1324                    format: TimeStringFormatting::SI,
1325                    show_space: true,
1326                    show_unit: true
1327                }
1328            ),
1329            "22 s"
1330        );
1331        assert_eq!(
1332            time_string(
1333                &BigInt::from(22000),
1334                &TimeScale {
1335                    multiplier: Some(10),
1336                    unit: TimeUnit::Seconds
1337                },
1338                &TimeUnit::Auto,
1339                &TimeFormat {
1340                    format: TimeStringFormatting::No,
1341                    show_space: true,
1342                    show_unit: true
1343                }
1344            ),
1345            "220000 s"
1346        );
1347    }
1348    #[test]
1349    fn print_time_none() {
1350        assert_eq!(
1351            time_string(
1352                &BigInt::from(2200),
1353                &TimeScale {
1354                    multiplier: Some(1),
1355                    unit: TimeUnit::MicroSeconds
1356                },
1357                &TimeUnit::None,
1358                &TimeFormat {
1359                    format: TimeStringFormatting::No,
1360                    show_space: true,
1361                    show_unit: true
1362                }
1363            ),
1364            "2200"
1365        );
1366        assert_eq!(
1367            time_string(
1368                &BigInt::from(220),
1369                &TimeScale {
1370                    multiplier: Some(10),
1371                    unit: TimeUnit::MicroSeconds
1372                },
1373                &TimeUnit::None,
1374                &TimeFormat {
1375                    format: TimeStringFormatting::No,
1376                    show_space: true,
1377                    show_unit: true
1378                }
1379            ),
1380            "220"
1381        );
1382    }
1383
1384    #[test]
1385    fn test_strip_trailing_zeros_and_period() {
1386        use crate::time::strip_trailing_zeros_and_period;
1387
1388        assert_eq!(strip_trailing_zeros_and_period("123.000"), "123");
1389        assert_eq!(strip_trailing_zeros_and_period("123.450"), "123.45");
1390        assert_eq!(strip_trailing_zeros_and_period("123.456"), "123.456");
1391        assert_eq!(strip_trailing_zeros_and_period("123."), "123");
1392        assert_eq!(strip_trailing_zeros_and_period("123"), "123");
1393        assert_eq!(strip_trailing_zeros_and_period("0.000"), "0");
1394        assert_eq!(strip_trailing_zeros_and_period("0.100"), "0.1");
1395        assert_eq!(strip_trailing_zeros_and_period(""), "");
1396    }
1397
1398    #[test]
1399    fn test_format_si() {
1400        use crate::time::format_si;
1401
1402        // 4-digit rule: no grouping for 4 digits or less
1403        assert_eq!(format_si("1234.56"), "1234.56");
1404        assert_eq!(format_si("123.4"), "123.4");
1405
1406        // Grouping for 5+ digits
1407        assert_eq!(format_si("12345.67"), "12\u{2009}345.67");
1408        assert_eq!(format_si("1234567.89"), "1\u{2009}234\u{2009}567.89");
1409        // No decimal part
1410        assert_eq!(format_si("12345"), "12\u{2009}345");
1411        assert_eq!(format_si("123"), "123");
1412
1413        // Empty inputs
1414        assert_eq!(format_si("0.123"), "0.123");
1415        assert_eq!(format_si(""), "");
1416
1417        // Decimal grouping
1418        assert_eq!(format_si("123.4567890"), "123.456\u{2009}789\u{2009}0");
1419    }
1420
1421    #[test]
1422    fn test_time_unit_exponent() {
1423        // Test exponent method
1424        assert_eq!(TimeUnit::Seconds.exponent(), 0);
1425        assert_eq!(TimeUnit::MilliSeconds.exponent(), -3);
1426        assert_eq!(TimeUnit::MicroSeconds.exponent(), -6);
1427        assert_eq!(TimeUnit::NanoSeconds.exponent(), -9);
1428        assert_eq!(TimeUnit::PicoSeconds.exponent(), -12);
1429        assert_eq!(TimeUnit::FemtoSeconds.exponent(), -15);
1430        assert_eq!(TimeUnit::AttoSeconds.exponent(), -18);
1431        assert_eq!(TimeUnit::ZeptoSeconds.exponent(), -21);
1432
1433        // Test from_exponent roundtrip
1434        for unit in [
1435            TimeUnit::Seconds,
1436            TimeUnit::MilliSeconds,
1437            TimeUnit::MicroSeconds,
1438            TimeUnit::NanoSeconds,
1439            TimeUnit::PicoSeconds,
1440            TimeUnit::FemtoSeconds,
1441            TimeUnit::AttoSeconds,
1442            TimeUnit::ZeptoSeconds,
1443        ] {
1444            assert_eq!(TimeUnit::from_exponent(unit.exponent()), Some(unit));
1445        }
1446
1447        // Invalid exponents
1448        assert_eq!(TimeUnit::from_exponent(-5), None);
1449        assert_eq!(TimeUnit::from_exponent(1), None);
1450    }
1451
1452    #[test]
1453    fn test_time_string_zero() {
1454        // Test zero values
1455        assert_eq!(
1456            time_string(
1457                &BigInt::from(0),
1458                &TimeScale {
1459                    multiplier: Some(1),
1460                    unit: TimeUnit::MicroSeconds
1461                },
1462                &TimeUnit::MicroSeconds,
1463                &TimeFormat::default()
1464            ),
1465            "0 μs"
1466        );
1467
1468        assert_eq!(
1469            time_string(
1470                &BigInt::from(0),
1471                &TimeScale {
1472                    multiplier: Some(1),
1473                    unit: TimeUnit::Seconds
1474                },
1475                &TimeUnit::Auto,
1476                &TimeFormat::default()
1477            ),
1478            "0 s"
1479        );
1480    }
1481
1482    #[test]
1483    fn test_time_string_large_numbers() {
1484        // Test very large numbers with SI formatting
1485        assert_eq!(
1486            time_string(
1487                &BigInt::from(999_999_999_999i64),
1488                &TimeScale {
1489                    multiplier: Some(1),
1490                    unit: TimeUnit::NanoSeconds
1491                },
1492                &TimeUnit::Seconds,
1493                &TimeFormat {
1494                    format: TimeStringFormatting::SI,
1495                    show_space: true,
1496                    show_unit: true
1497                }
1498            ),
1499            "999.999\u{2009}999\u{2009}999 s"
1500        );
1501    }
1502
1503    #[test]
1504    fn test_time_string_no_multiplier() {
1505        // Test with None multiplier (raw ticks)
1506        assert_eq!(
1507            time_string(
1508                &BigInt::from(1234),
1509                &TimeScale {
1510                    multiplier: None,
1511                    unit: TimeUnit::NanoSeconds
1512                },
1513                &TimeUnit::NanoSeconds,
1514                &TimeFormat::default()
1515            ),
1516            "1234 ns"
1517        );
1518    }
1519
1520    #[test]
1521    fn test_time_format_variations() {
1522        let value = BigInt::from(123456);
1523        let scale = TimeScale {
1524            multiplier: Some(1),
1525            unit: TimeUnit::NanoSeconds,
1526        };
1527
1528        // Test all format variations
1529        assert_eq!(
1530            time_string(
1531                &value,
1532                &scale,
1533                &TimeUnit::NanoSeconds,
1534                &TimeFormat {
1535                    format: TimeStringFormatting::No,
1536                    show_space: true,
1537                    show_unit: true
1538                }
1539            ),
1540            "123456 ns"
1541        );
1542
1543        assert_eq!(
1544            time_string(
1545                &value,
1546                &scale,
1547                &TimeUnit::NanoSeconds,
1548                &TimeFormat {
1549                    format: TimeStringFormatting::No,
1550                    show_space: false,
1551                    show_unit: true
1552                }
1553            ),
1554            "123456ns"
1555        );
1556
1557        assert_eq!(
1558            time_string(
1559                &value,
1560                &scale,
1561                &TimeUnit::NanoSeconds,
1562                &TimeFormat {
1563                    format: TimeStringFormatting::No,
1564                    show_space: true,
1565                    show_unit: false
1566                }
1567            ),
1568            "123456"
1569        );
1570
1571        assert_eq!(
1572            time_string(
1573                &value,
1574                &scale,
1575                &TimeUnit::NanoSeconds,
1576                &TimeFormat {
1577                    format: TimeStringFormatting::SI,
1578                    show_space: true,
1579                    show_unit: true
1580                }
1581            ),
1582            "123\u{2009}456 ns"
1583        );
1584    }
1585
1586    #[test]
1587    fn test_find_auto_scale_seconds_passthrough() {
1588        use crate::time::find_auto_scale;
1589
1590        let ts = TimeScale {
1591            unit: TimeUnit::Seconds,
1592            multiplier: Some(1),
1593        };
1594        assert_eq!(find_auto_scale(&BigInt::from(1), &ts), TimeUnit::Seconds);
1595        assert_eq!(
1596            find_auto_scale(&BigInt::from(1_234_567), &ts),
1597            TimeUnit::Seconds
1598        );
1599    }
1600
1601    #[test]
1602    fn test_find_auto_scale_nanoseconds() {
1603        use crate::time::find_auto_scale;
1604
1605        let ts = TimeScale {
1606            unit: TimeUnit::NanoSeconds,
1607            multiplier: Some(1),
1608        };
1609
1610        // Divisible by 10^9 -> seconds
1611        assert_eq!(
1612            find_auto_scale(&BigInt::from(1_000_000_000i64), &ts),
1613            TimeUnit::Seconds
1614        );
1615        // Divisible by 10^6 -> milliseconds
1616        assert_eq!(
1617            find_auto_scale(&BigInt::from(1_000_000), &ts),
1618            TimeUnit::MilliSeconds
1619        );
1620        // Divisible by 10^3 -> microseconds
1621        assert_eq!(
1622            find_auto_scale(&BigInt::from(1_000), &ts),
1623            TimeUnit::MicroSeconds
1624        );
1625        // Not divisible by 10^3 -> stay at nanos
1626        assert_eq!(
1627            find_auto_scale(&BigInt::from(1234), &ts),
1628            TimeUnit::NanoSeconds
1629        );
1630    }
1631
1632    #[test]
1633    fn test_find_auto_scale_microseconds_with_multiplier() {
1634        use crate::time::find_auto_scale;
1635
1636        // multiplier: None (treated as 1)
1637        let ts_none = TimeScale {
1638            unit: TimeUnit::MicroSeconds,
1639            multiplier: None,
1640        };
1641        assert_eq!(
1642            find_auto_scale(&BigInt::from(1_000_000), &ts_none),
1643            TimeUnit::Seconds
1644        );
1645        assert_eq!(
1646            find_auto_scale(&BigInt::from(1_000), &ts_none),
1647            TimeUnit::MilliSeconds
1648        );
1649        assert_eq!(
1650            find_auto_scale(&BigInt::from(123), &ts_none),
1651            TimeUnit::MicroSeconds
1652        );
1653
1654        // multiplier: Some(10) -> reduces required divisibility by 10^1
1655        let ts_mul10 = TimeScale {
1656            unit: TimeUnit::MicroSeconds,
1657            multiplier: Some(10),
1658        };
1659        assert_eq!(
1660            find_auto_scale(&BigInt::from(100_000), &ts_mul10),
1661            TimeUnit::Seconds
1662        );
1663        assert_eq!(
1664            find_auto_scale(&BigInt::from(100), &ts_mul10),
1665            TimeUnit::MilliSeconds
1666        );
1667        assert_eq!(
1668            find_auto_scale(&BigInt::from(123), &ts_mul10),
1669            TimeUnit::MicroSeconds
1670        );
1671    }
1672
1673    #[test]
1674    fn test_find_auto_scale_femtoseconds() {
1675        use crate::time::find_auto_scale;
1676
1677        let ts = TimeScale {
1678            unit: TimeUnit::FemtoSeconds,
1679            multiplier: Some(1),
1680        };
1681        // 10^15 fs = 1 s
1682        assert_eq!(
1683            find_auto_scale(&BigInt::from(10_i128.pow(15)), &ts),
1684            TimeUnit::Seconds
1685        );
1686        // 10^12 fs = 1 ms
1687        assert_eq!(
1688            find_auto_scale(&BigInt::from(10_i128.pow(12)), &ts),
1689            TimeUnit::MilliSeconds
1690        );
1691        // 10^9 fs = 1 μs
1692        assert_eq!(
1693            find_auto_scale(&BigInt::from(10_i128.pow(9)), &ts),
1694            TimeUnit::MicroSeconds
1695        );
1696        // 10^6 fs = 1 ns
1697        assert_eq!(
1698            find_auto_scale(&BigInt::from(10_i128.pow(6)), &ts),
1699            TimeUnit::NanoSeconds
1700        );
1701        // 10^3 fs = 1 ps
1702        assert_eq!(
1703            find_auto_scale(&BigInt::from(10_i128.pow(3)), &ts),
1704            TimeUnit::PicoSeconds
1705        );
1706        // Not divisible by 10^3 -> stay at fs
1707        assert_eq!(
1708            find_auto_scale(&BigInt::from(1), &ts),
1709            TimeUnit::FemtoSeconds
1710        );
1711    }
1712
1713    #[test]
1714    fn test_locale_cache_en_us() {
1715        use crate::time::{create_cache, format_locale};
1716        use pure_rust_locales::Locale;
1717
1718        let locale = Locale::en_US;
1719        let cache = create_cache(locale);
1720
1721        // en_US uses period as decimal point and comma as thousands separator
1722        let result = format_locale("1234567.89", &cache);
1723        assert_eq!(result, "1,234,567.89");
1724    }
1725
1726    #[test]
1727    fn test_locale_cache_de_de() {
1728        use crate::time::{create_cache, format_locale};
1729        use pure_rust_locales::Locale;
1730
1731        let locale = Locale::de_DE;
1732        let cache = create_cache(locale);
1733
1734        let result = format_locale("1234567.89", &cache);
1735        assert_eq!(result, "1.234.567,89");
1736    }
1737
1738    #[test]
1739    fn test_locale_cache_fr_fr() {
1740        use crate::time::{create_cache, format_locale};
1741        use pure_rust_locales::Locale;
1742
1743        let locale = Locale::fr_FR;
1744        let cache = create_cache(locale);
1745
1746        // fr_FR typically uses space/thin_space and comma
1747        let result = format_locale("1234567.89", &cache);
1748        // Verify it produces valid output
1749        assert_eq!(result, "1\u{2009}234\u{2009}567,89");
1750    }
1751
1752    #[test]
1753    fn test_locale_cache_small_numbers() {
1754        use crate::time::{create_cache, format_locale};
1755        use pure_rust_locales::Locale;
1756
1757        let locale = Locale::en_US;
1758        let cache = create_cache(locale);
1759
1760        // Numbers smaller than grouping threshold should remain unchanged
1761        assert_eq!(format_locale("123", &cache), "123");
1762        assert_eq!(format_locale("12.34", &cache), "12.34");
1763        assert_eq!(format_locale("0", &cache), "0");
1764    }
1765
1766    #[test]
1767    fn test_locale_cache_consistency_across_locales() {
1768        use crate::time::create_cache;
1769        use pure_rust_locales::Locale;
1770
1771        // Verify that creating cache for the same locale twice produces consistent results
1772        let cache1 = create_cache(Locale::en_US);
1773        let cache2 = create_cache(Locale::en_US);
1774
1775        assert_eq!(cache1.thousands_sep, cache2.thousands_sep);
1776        assert_eq!(cache1.decimal_point, cache2.decimal_point);
1777        assert_eq!(cache1.grouping, cache2.grouping);
1778    }
1779
1780    #[test]
1781    fn test_create_cache_from_various_locales() {
1782        use crate::time::{create_cache, format_locale};
1783        use pure_rust_locales::Locale;
1784
1785        // Test that create_cache works for many Locale variants without panicking
1786        let locales = vec![
1787            Locale::en_US,
1788            Locale::de_DE,
1789            Locale::fr_FR,
1790            Locale::es_ES,
1791            Locale::it_IT,
1792            Locale::pt_BR,
1793            Locale::pt_PT,
1794            Locale::ja_JP,
1795            Locale::zh_CN,
1796            Locale::zh_TW,
1797            Locale::ru_RU,
1798            Locale::ko_KR,
1799            Locale::pl_PL,
1800            Locale::tr_TR,
1801            Locale::nl_NL,
1802            Locale::sv_SE,
1803            Locale::da_DK,
1804            Locale::fi_FI,
1805            Locale::el_GR,
1806            Locale::hu_HU,
1807            Locale::cs_CZ,
1808            Locale::ro_RO,
1809            Locale::th_TH,
1810            Locale::vi_VN,
1811            Locale::ar_SA,
1812            Locale::he_IL,
1813            Locale::id_ID,
1814            Locale::uk_UA,
1815            Locale::en_GB,
1816            Locale::en_AU,
1817            Locale::en_CA,
1818            Locale::en_NZ,
1819            Locale::en_IN,
1820            Locale::fr_CA,
1821            Locale::de_AT,
1822            Locale::de_CH,
1823            Locale::fr_CH,
1824            Locale::it_CH,
1825            Locale::es_MX,
1826            Locale::es_AR,
1827        ];
1828
1829        for locale in locales {
1830            let cache = create_cache(locale);
1831            // Check so that it is not empty for a sample number
1832            assert!(
1833                !format_locale("1234567.89", &cache).is_empty(),
1834                "Failed for {locale:?}"
1835            );
1836        }
1837    }
1838}
1839
1840#[cfg(test)]
1841mod get_ticks_tests {
1842    use super::*;
1843    use itertools::Itertools;
1844    use num::BigInt;
1845
1846    // Basic smoke test: ensure we get at least one tick and that returned
1847    // pixel coordinates lie within the frame width.
1848    #[test]
1849    fn get_ticks_basic() {
1850        let vp = crate::viewport::Viewport::default();
1851        let timescale = TimeScale {
1852            unit: TimeUnit::MicroSeconds,
1853            multiplier: Some(1),
1854        };
1855        let frame_width = 800.0_f32;
1856        let text_size = 12.0_f32;
1857        let wanted = TimeUnit::MicroSeconds;
1858        let time_format = TimeFormat::default();
1859        let config = crate::config::SurferConfig::default();
1860        let max_timestamp = BigInt::from(1_000_000i64);
1861
1862        let ticks = get_ticks_internal(
1863            &vp,
1864            &timescale,
1865            frame_width,
1866            text_size,
1867            &wanted,
1868            &time_format,
1869            config.theme.ticks.density,
1870            &max_timestamp,
1871            &BigInt::from(0),
1872        );
1873
1874        assert!(!ticks.is_empty(), "expected at least one tick");
1875
1876        // Check monotonic x positions and collect labels for uniqueness check
1877        let mut last_x = -1.0_f32;
1878        let mut labels: Vec<String> = Vec::with_capacity(ticks.len());
1879        for (label, x, _) in &ticks {
1880            assert!(
1881                *x >= last_x,
1882                "tick x not monotonic: {x} < {last_x} for label {label}"
1883            );
1884            last_x = *x;
1885            assert!(*x >= 0.0, "tick x < 0: {x}");
1886            assert!(
1887                *x <= frame_width,
1888                "tick x > frame_width: {x} > {frame_width}"
1889            );
1890            labels.push(label.clone());
1891        }
1892        // Labels should be unique
1893        let unique_labels = labels.iter().unique().count();
1894        assert_eq!(labels.len(), unique_labels, "duplicate tick labels found");
1895    }
1896
1897    // Ensure tick generation produces a reasonable number of ticks when
1898    // viewport is zoomed and density is high.
1899    #[test]
1900    fn get_ticks_respects_frame_width_and_density() {
1901        let mut vp = crate::viewport::Viewport::default();
1902        // zoom to a narrower view
1903        vp.curr_left = crate::viewport::Relative(0.0);
1904        vp.curr_right = crate::viewport::Relative(0.1);
1905
1906        let timescale = TimeScale {
1907            unit: TimeUnit::NanoSeconds,
1908            multiplier: Some(1),
1909        };
1910        let frame_width = 200.0_f32;
1911        let text_size = 10.0_f32;
1912        let wanted = TimeUnit::Auto;
1913        let time_format = TimeFormat {
1914            format: TimeStringFormatting::SI,
1915            show_space: true,
1916            show_unit: true,
1917        };
1918
1919        let mut config = crate::config::SurferConfig::default();
1920        // make ticks dense
1921        config.theme.ticks.density = 1.0;
1922
1923        let max_timestamp = BigInt::from(1_000_000i64);
1924
1925        let ticks = get_ticks_internal(
1926            &vp,
1927            &timescale,
1928            frame_width,
1929            text_size,
1930            &wanted,
1931            &time_format,
1932            config.theme.ticks.density,
1933            &max_timestamp,
1934            &BigInt::from(0),
1935        );
1936
1937        assert!(!ticks.is_empty(), "expected ticks even for narrow view");
1938        // expect a sane upper bound (protects against accidental infinite loops)
1939        assert!(ticks.len() < 200, "too many ticks: {}", ticks.len());
1940
1941        // monotonic x positions and unique labels
1942        let mut last_x = -1.0_f32;
1943        let mut labels: Vec<String> = Vec::with_capacity(ticks.len());
1944        for (label, x, _) in &ticks {
1945            assert!(
1946                *x >= last_x,
1947                "tick x not monotonic: {x} < {last_x} for label {label}"
1948            );
1949            last_x = *x;
1950            assert!(*x >= 0.0, "tick x < 0: {x}");
1951            assert!(
1952                *x <= frame_width,
1953                "tick x > frame_width: {x} > {frame_width}"
1954            );
1955            labels.push(label.clone());
1956        }
1957        let unique_labels = labels.iter().unique().count();
1958        assert_eq!(labels.len(), unique_labels, "duplicate tick labels found");
1959    }
1960}
1961
1962#[cfg(test)]
1963mod time_input_tests {
1964    use super::*;
1965
1966    #[test]
1967    fn test_parse_time_input_simple() {
1968        let (num, unit) = parse_time_input("100");
1969        assert_eq!(num, "100");
1970        assert_eq!(unit, None);
1971    }
1972
1973    #[test]
1974    fn test_parse_time_input_unit_only_no_panic() {
1975        // Unit-only input should not panic and should be treated as plain text.
1976        let (num, unit) = parse_time_input("ns");
1977        assert_eq!(num, "ns");
1978        assert_eq!(unit, None);
1979    }
1980
1981    #[test]
1982    fn test_parse_time_input_with_unit_no_space() {
1983        let (num, unit) = parse_time_input("100ns");
1984        assert_eq!(num, "100");
1985        assert_eq!(unit, Some(TimeUnit::NanoSeconds));
1986
1987        let (num, unit) = parse_time_input("50ps");
1988        assert_eq!(num, "50");
1989        assert_eq!(unit, Some(TimeUnit::PicoSeconds));
1990
1991        let (num, unit) = parse_time_input("1.5ms");
1992        assert_eq!(num, "1.5");
1993        assert_eq!(unit, Some(TimeUnit::MilliSeconds));
1994    }
1995
1996    #[test]
1997    fn test_parse_time_input_with_unit_space() {
1998        let (num, unit) = parse_time_input("100 ns");
1999        assert_eq!(num, "100");
2000        assert_eq!(unit, Some(TimeUnit::NanoSeconds));
2001
2002        let (num, unit) = parse_time_input("1.5 ms");
2003        assert_eq!(num, "1.5");
2004        assert_eq!(unit, Some(TimeUnit::MilliSeconds));
2005
2006        let (num, unit) = parse_time_input("100\tms");
2007        assert_eq!(num, "100");
2008        assert_eq!(unit, Some(TimeUnit::MilliSeconds));
2009
2010        let (num, unit) = parse_time_input("100    ns");
2011        assert_eq!(num, "100");
2012        assert_eq!(unit, Some(TimeUnit::NanoSeconds));
2013    }
2014
2015    #[test]
2016    fn test_parse_time_input_microseconds_unicode() {
2017        let (num, unit) = parse_time_input("100μs");
2018        assert_eq!(num, "100");
2019        assert_eq!(unit, Some(TimeUnit::MicroSeconds));
2020
2021        let (num, unit) = parse_time_input("50 μs");
2022        assert_eq!(num, "50");
2023        assert_eq!(unit, Some(TimeUnit::MicroSeconds));
2024    }
2025
2026    #[test]
2027    fn test_parse_time_input_microseconds_ascii() {
2028        // Support "us" as alternative to "μs"
2029        let (num, unit) = parse_time_input("100us");
2030        assert_eq!(num, "100");
2031        assert_eq!(unit, Some(TimeUnit::MicroSeconds));
2032    }
2033
2034    #[test]
2035    fn test_parse_time_input_seconds() {
2036        let (num, unit) = parse_time_input("10s");
2037        assert_eq!(num, "10");
2038        assert_eq!(unit, Some(TimeUnit::Seconds));
2039
2040        let (num, unit) = parse_time_input("0.5s");
2041        assert_eq!(num, "0.5");
2042        assert_eq!(unit, Some(TimeUnit::Seconds));
2043    }
2044
2045    #[test]
2046    fn test_parse_time_input_femtoseconds() {
2047        let (num, unit) = parse_time_input("1000000fs");
2048        assert_eq!(num, "1000000");
2049        assert_eq!(unit, Some(TimeUnit::FemtoSeconds));
2050    }
2051
2052    #[test]
2053    fn test_parse_time_input_with_whitespace() {
2054        let (num, unit) = parse_time_input("  100ns  ");
2055        assert_eq!(num, "100");
2056        assert_eq!(unit, Some(TimeUnit::NanoSeconds));
2057    }
2058
2059    #[test]
2060    fn test_split_numeric_parts() {
2061        assert_eq!(
2062            split_numeric_parts("100").ok(),
2063            Some(("100".to_string(), String::new()))
2064        );
2065        assert_eq!(
2066            split_numeric_parts("100.").ok(),
2067            Some(("100".to_string(), String::new()))
2068        );
2069        assert_eq!(
2070            split_numeric_parts(".5").ok(),
2071            Some(("0".to_string(), "5".to_string()))
2072        );
2073        assert_eq!(
2074            split_numeric_parts("1.5").ok(),
2075            Some(("1".to_string(), "5".to_string()))
2076        );
2077    }
2078
2079    #[test]
2080    fn test_split_numeric_parts_invalid() {
2081        assert!(split_numeric_parts("").is_err());
2082        assert!(split_numeric_parts("abc").is_err());
2083        assert!(split_numeric_parts("12.34.56").is_err());
2084        assert!(split_numeric_parts("-1").is_err());
2085    }
2086
2087    #[test]
2088    fn test_normalize_numeric_with_unit_integer() {
2089        let (val, unit) = normalize_numeric_with_unit("100", TimeUnit::NanoSeconds).unwrap();
2090        assert_eq!(val, BigInt::from(100));
2091        assert_eq!(unit, TimeUnit::NanoSeconds);
2092    }
2093
2094    #[test]
2095    fn test_normalize_numeric_with_unit_decimal_single_step() {
2096        let (val, unit) = normalize_numeric_with_unit("1.5", TimeUnit::NanoSeconds).unwrap();
2097        assert_eq!(val, BigInt::from(1500));
2098        assert_eq!(unit, TimeUnit::PicoSeconds);
2099    }
2100
2101    #[test]
2102    fn test_normalize_numeric_with_unit_decimal_multi_step() {
2103        let (val, unit) = normalize_numeric_with_unit("1.2345", TimeUnit::NanoSeconds).unwrap();
2104        assert_eq!(val, BigInt::from(1_234_500));
2105        assert_eq!(unit, TimeUnit::FemtoSeconds);
2106    }
2107
2108    #[test]
2109    fn test_time_input_state_default() {
2110        let state = TimeInputState::default();
2111        assert_eq!(state.input_text, "");
2112        assert_eq!(state.parsed_value, None);
2113        assert_eq!(state.input_unit, None);
2114        assert_eq!(state.normalized_unit, None);
2115        assert_eq!(state.selected_unit, TimeUnit::NanoSeconds);
2116        assert_eq!(state.error, None);
2117    }
2118
2119    #[test]
2120    fn test_time_input_state_update_valid() {
2121        let mut state = TimeInputState::new();
2122        state.update_input("100ns".to_string());
2123
2124        assert_eq!(state.input_text, "100ns");
2125        assert_eq!(state.parsed_value, Some(BigInt::from(100)));
2126        assert_eq!(state.input_unit, Some(TimeUnit::NanoSeconds));
2127        assert_eq!(state.normalized_unit, Some(TimeUnit::NanoSeconds));
2128        assert_eq!(state.error, None);
2129    }
2130
2131    #[test]
2132    fn test_time_input_state_update_invalid() {
2133        let mut state = TimeInputState::new();
2134        state.update_input("abc".to_string());
2135
2136        assert_eq!(state.parsed_value, None);
2137        assert!(state.error.is_some());
2138    }
2139
2140    #[test]
2141    fn test_time_input_state_update_decimal_unit_normalization() {
2142        let mut state = TimeInputState::new();
2143        state.update_input("1.5ns".to_string());
2144
2145        assert_eq!(state.parsed_value, Some(BigInt::from(1500)));
2146        assert_eq!(state.input_unit, Some(TimeUnit::NanoSeconds));
2147        assert_eq!(state.normalized_unit, Some(TimeUnit::PicoSeconds));
2148        assert_eq!(state.effective_unit(), TimeUnit::PicoSeconds);
2149        assert_eq!(state.error, None);
2150    }
2151
2152    #[test]
2153    fn test_time_input_state_to_timescale_ticks_invalid() {
2154        let state = TimeInputState::new();
2155        let timescale = TimeScale {
2156            unit: TimeUnit::NanoSeconds,
2157            multiplier: None,
2158        };
2159
2160        assert_eq!(state.to_timescale_ticks(&timescale), None);
2161    }
2162
2163    #[test]
2164    fn test_time_input_comprehensive_example() {
2165        // User types "2.5 ms"
2166        let mut state = TimeInputState::new();
2167        state.update_input("2.5 ms".to_string());
2168
2169        // Parse should succeed
2170        assert_eq!(state.parsed_value, Some(BigInt::from(2500)));
2171        assert_eq!(state.input_unit, Some(TimeUnit::MilliSeconds));
2172        assert_eq!(state.effective_unit(), TimeUnit::MicroSeconds);
2173        assert_eq!(state.error, None);
2174
2175        // Should be able to convert to timescale ticks
2176        let timescale = TimeScale {
2177            unit: TimeUnit::MicroSeconds,
2178            multiplier: None,
2179        };
2180        assert_eq!(
2181            state.to_timescale_ticks(&timescale),
2182            Some(BigInt::from(2500))
2183        );
2184    }
2185
2186    #[test]
2187    fn test_parse_time_longest_match_first() {
2188        // "ms" should match before "s"
2189        let (num, unit) = parse_time_input("100ms");
2190        assert_eq!(num, "100");
2191        assert_eq!(unit, Some(TimeUnit::MilliSeconds));
2192
2193        // Not just the "s"
2194        let (_, unit) = parse_time_input("100s");
2195        assert_ne!(unit, Some(TimeUnit::MilliSeconds));
2196    }
2197
2198    #[test]
2199    fn test_parse_time_no_false_positives() {
2200        // These should not match units
2201        let (num, unit) = parse_time_input("mass");
2202        assert_eq!(num, "mass");
2203        assert_eq!(unit, None);
2204
2205        let (num, unit) = parse_time_input("uses");
2206        assert_eq!(num, "uses");
2207        assert_eq!(unit, None);
2208    }
2209
2210    #[test]
2211    fn test_time_input_state_clear() {
2212        let mut state = TimeInputState::new();
2213        state.update_input("100ns".to_string());
2214        assert!(state.parsed_value.is_some());
2215
2216        // Clear by updating with empty string
2217        state.update_input(String::new());
2218        assert!(state.error.is_some());
2219    }
2220
2221    // --- parse_time_string_to_ticks tests ---
2222
2223    fn ns_timescale() -> TimeScale {
2224        TimeScale {
2225            unit: TimeUnit::NanoSeconds,
2226            multiplier: None,
2227        }
2228    }
2229
2230    fn ps_timescale() -> TimeScale {
2231        TimeScale {
2232            unit: TimeUnit::PicoSeconds,
2233            multiplier: None,
2234        }
2235    }
2236
2237    #[test]
2238    fn test_parse_time_string_to_ticks_plain_integer() {
2239        // No unit: value is raw ticks
2240        let ts = ns_timescale();
2241        assert_eq!(
2242            parse_time_string_to_ticks("100", &ts),
2243            Some(BigInt::from(100))
2244        );
2245        assert_eq!(parse_time_string_to_ticks("0", &ts), Some(BigInt::from(0)));
2246    }
2247
2248    #[test]
2249    fn test_parse_time_string_to_ticks_same_unit() {
2250        // "100ns" with ns timescale -> 100 ticks
2251        let ts = ns_timescale();
2252        assert_eq!(
2253            parse_time_string_to_ticks("100ns", &ts),
2254            Some(BigInt::from(100))
2255        );
2256        assert_eq!(
2257            parse_time_string_to_ticks("100 ns", &ts),
2258            Some(BigInt::from(100))
2259        );
2260    }
2261
2262    #[test]
2263    fn test_parse_time_string_to_ticks_coarser_unit() {
2264        // "1μs" with ns timescale -> 1000 ticks
2265        let ts = ns_timescale();
2266        assert_eq!(
2267            parse_time_string_to_ticks("1us", &ts),
2268            Some(BigInt::from(1000))
2269        );
2270        assert_eq!(
2271            parse_time_string_to_ticks("1μs", &ts),
2272            Some(BigInt::from(1000))
2273        );
2274        // "1ms" -> 1_000_000 ticks
2275        assert_eq!(
2276            parse_time_string_to_ticks("1ms", &ts),
2277            Some(BigInt::from(1_000_000))
2278        );
2279        // "1s" -> 1_000_000_000 ticks
2280        assert_eq!(
2281            parse_time_string_to_ticks("1s", &ts),
2282            Some(BigInt::from(1_000_000_000))
2283        );
2284    }
2285
2286    #[test]
2287    fn test_parse_time_string_to_ticks_finer_unit() {
2288        // "1ps" with ns timescale -> truncates to 0 (1 ps < 1 ns)
2289        let ts = ns_timescale();
2290        assert_eq!(
2291            parse_time_string_to_ticks("1ps", &ts),
2292            Some(BigInt::from(0))
2293        );
2294        // "1000ps" -> 1 ns tick
2295        assert_eq!(
2296            parse_time_string_to_ticks("1000ps", &ts),
2297            Some(BigInt::from(1))
2298        );
2299    }
2300
2301    #[test]
2302    fn test_parse_time_string_to_ticks_decimal() {
2303        // "1.5μs" with ns timescale: 1.5 μs = 1500 ns -> 1500 ticks
2304        let ts = ns_timescale();
2305        assert_eq!(
2306            parse_time_string_to_ticks("1.5us", &ts),
2307            Some(BigInt::from(1500))
2308        );
2309        // "0.5ns" with ps timescale: 0.5 ns = 500 ps -> 500 ticks
2310        let ts_ps = ps_timescale();
2311        assert_eq!(
2312            parse_time_string_to_ticks("0.5ns", &ts_ps),
2313            Some(BigInt::from(500))
2314        );
2315    }
2316
2317    #[test]
2318    fn test_parse_time_string_to_ticks_with_whitespace() {
2319        let ts = ns_timescale();
2320        assert_eq!(
2321            parse_time_string_to_ticks("  100 ns  ", &ts),
2322            Some(BigInt::from(100))
2323        );
2324    }
2325
2326    #[test]
2327    fn test_parse_time_string_to_ticks_invalid() {
2328        let ts = ns_timescale();
2329        assert_eq!(parse_time_string_to_ticks("abc", &ts), None);
2330        assert_eq!(parse_time_string_to_ticks("", &ts), None);
2331        assert_eq!(parse_time_string_to_ticks("-5ns", &ts), None);
2332    }
2333
2334    #[test]
2335    fn test_parse_time_string_to_ticks_with_multiplier() {
2336        // timescale multiplier=10ns: "100ns" -> 100/10 = 10 ticks
2337        let ts = TimeScale {
2338            unit: TimeUnit::NanoSeconds,
2339            multiplier: Some(10),
2340        };
2341        assert_eq!(
2342            parse_time_string_to_ticks("100ns", &ts),
2343            Some(BigInt::from(10))
2344        );
2345    }
2346
2347    #[test]
2348    fn test_parse_time_string_to_ticks_ps_timescale() {
2349        // 1ns with ps timescale -> 1000 ticks
2350        let ts = ps_timescale();
2351        assert_eq!(
2352            parse_time_string_to_ticks("1ns", &ts),
2353            Some(BigInt::from(1000))
2354        );
2355        // plain integer stays as-is
2356        assert_eq!(
2357            parse_time_string_to_ticks("42", &ts),
2358            Some(BigInt::from(42))
2359        );
2360    }
2361}