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