Skip to main content

libsurfer/
time.rs

1//! Time handling and formatting.
2use derive_more::Display;
3use ecolor::Color32;
4use egui::Ui;
5use emath::{Align2, Pos2};
6use enum_iterator::Sequence;
7use epaint::{FontId, Stroke};
8use ftr_parser::types::Timescale;
9use itertools::Itertools;
10use num::{BigInt, BigRational, ToPrimitive, Zero};
11use pure_rust_locales::{Locale, locale_match};
12use serde::{Deserialize, Serialize};
13use std::sync::OnceLock;
14use sys_locale::get_locale;
15
16use crate::viewport::Viewport;
17use crate::wave_data::WaveData;
18use crate::{Message, SystemState, translation::group_n_chars, view::DrawingContext};
19
20#[derive(Serialize, Deserialize, Clone)]
21pub struct TimeScale {
22    pub unit: TimeUnit,
23    pub multiplier: Option<u32>,
24}
25
26#[derive(Debug, Clone, Copy, Display, Eq, PartialEq, Serialize, Deserialize, Sequence)]
27pub enum TimeUnit {
28    #[display("zs")]
29    ZeptoSeconds,
30
31    #[display("as")]
32    AttoSeconds,
33
34    #[display("fs")]
35    FemtoSeconds,
36
37    #[display("ps")]
38    PicoSeconds,
39
40    #[display("ns")]
41    NanoSeconds,
42
43    #[display("μs")]
44    MicroSeconds,
45
46    #[display("ms")]
47    MilliSeconds,
48
49    #[display("s")]
50    Seconds,
51
52    #[display("No unit")]
53    None,
54
55    /// Use the largest time unit feasible for each time.
56    #[display("Auto")]
57    Auto,
58}
59
60pub const DEFAULT_TIMELINE_NAME: &str = "Time";
61const THIN_SPACE: &str = "\u{2009}";
62
63/// Candidate multipliers used to choose tick spacing.
64pub const TICK_STEPS: [f64; 8] = [1., 2., 2.5, 5., 10., 20., 25., 50.];
65
66/// Cached locale-specific formatting properties.
67struct LocaleFormatCache {
68    grouping: &'static [i64],
69    thousands_sep: String,
70    decimal_point: String,
71}
72
73static LOCALE_FORMAT_CACHE: OnceLock<LocaleFormatCache> = OnceLock::new();
74
75/// Get the cached locale formatting properties.
76fn get_locale_format_cache() -> &'static LocaleFormatCache {
77    LOCALE_FORMAT_CACHE.get_or_init(|| {
78        let locale = get_locale()
79            .unwrap_or_else(|| "en-US".to_string())
80            .as_str()
81            .try_into()
82            .unwrap_or(Locale::en_US);
83        create_cache(locale)
84    })
85}
86
87fn create_cache(locale: Locale) -> LocaleFormatCache {
88    let grouping = locale_match!(locale => LC_NUMERIC::GROUPING);
89    let thousands_sep =
90        locale_match!(locale => LC_NUMERIC::THOUSANDS_SEP).replace('\u{202f}', THIN_SPACE);
91    let decimal_point = locale_match!(locale => LC_NUMERIC::DECIMAL_POINT).to_string();
92
93    LocaleFormatCache {
94        grouping,
95        thousands_sep,
96        decimal_point,
97    }
98}
99
100impl From<wellen::TimescaleUnit> for TimeUnit {
101    fn from(timescale: wellen::TimescaleUnit) -> Self {
102        match timescale {
103            wellen::TimescaleUnit::ZeptoSeconds => TimeUnit::ZeptoSeconds,
104            wellen::TimescaleUnit::AttoSeconds => TimeUnit::AttoSeconds,
105            wellen::TimescaleUnit::FemtoSeconds => TimeUnit::FemtoSeconds,
106            wellen::TimescaleUnit::PicoSeconds => TimeUnit::PicoSeconds,
107            wellen::TimescaleUnit::NanoSeconds => TimeUnit::NanoSeconds,
108            wellen::TimescaleUnit::MicroSeconds => TimeUnit::MicroSeconds,
109            wellen::TimescaleUnit::MilliSeconds => TimeUnit::MilliSeconds,
110            wellen::TimescaleUnit::Seconds => TimeUnit::Seconds,
111            wellen::TimescaleUnit::Unknown => TimeUnit::None,
112        }
113    }
114}
115
116impl From<ftr_parser::types::Timescale> for TimeUnit {
117    fn from(timescale: Timescale) -> Self {
118        match timescale {
119            Timescale::Fs => TimeUnit::FemtoSeconds,
120            Timescale::Ps => TimeUnit::PicoSeconds,
121            Timescale::Ns => TimeUnit::NanoSeconds,
122            Timescale::Us => TimeUnit::MicroSeconds,
123            Timescale::Ms => TimeUnit::MilliSeconds,
124            Timescale::S => TimeUnit::Seconds,
125            Timescale::Unit => TimeUnit::None,
126            Timescale::None => TimeUnit::None,
127        }
128    }
129}
130
131impl TimeUnit {
132    /// Get the power-of-ten exponent for a time unit.
133    fn exponent(self) -> i8 {
134        match self {
135            TimeUnit::ZeptoSeconds => -21,
136            TimeUnit::AttoSeconds => -18,
137            TimeUnit::FemtoSeconds => -15,
138            TimeUnit::PicoSeconds => -12,
139            TimeUnit::NanoSeconds => -9,
140            TimeUnit::MicroSeconds => -6,
141            TimeUnit::MilliSeconds => -3,
142            TimeUnit::Seconds => 0,
143            TimeUnit::None => 0,
144            TimeUnit::Auto => 0,
145        }
146    }
147    /// Convert a power-of-ten exponent to a time unit.
148    fn from_exponent(exponent: i8) -> Option<Self> {
149        match exponent {
150            -21 => Some(TimeUnit::ZeptoSeconds),
151            -18 => Some(TimeUnit::AttoSeconds),
152            -15 => Some(TimeUnit::FemtoSeconds),
153            -12 => Some(TimeUnit::PicoSeconds),
154            -9 => Some(TimeUnit::NanoSeconds),
155            -6 => Some(TimeUnit::MicroSeconds),
156            -3 => Some(TimeUnit::MilliSeconds),
157            0 => Some(TimeUnit::Seconds),
158            _ => None,
159        }
160    }
161}
162
163/// Create menu for selecting preferred time unit.
164pub fn timeunit_menu(ui: &mut Ui, msgs: &mut Vec<Message>, wanted_timeunit: &TimeUnit) {
165    for timeunit in enum_iterator::all::<TimeUnit>() {
166        if ui
167            .radio(*wanted_timeunit == timeunit, timeunit.to_string())
168            .clicked()
169        {
170            msgs.push(Message::SetTimeUnit(timeunit));
171        }
172    }
173}
174
175/// How to format the time stamps.
176#[derive(Debug, Deserialize, Serialize, Clone)]
177pub struct TimeFormat {
178    /// How to format the numeric part of the time string.
179    format: TimeStringFormatting,
180    /// Insert a space between number and unit.
181    show_space: bool,
182    /// Display time unit.
183    show_unit: bool,
184}
185
186impl Default for TimeFormat {
187    fn default() -> Self {
188        TimeFormat {
189            format: TimeStringFormatting::No,
190            show_space: true,
191            show_unit: true,
192        }
193    }
194}
195
196impl TimeFormat {
197    /// Create a new `TimeFormat` with custom settings.
198    #[must_use]
199    pub fn new(format: TimeStringFormatting, show_space: bool, show_unit: bool) -> Self {
200        TimeFormat {
201            format,
202            show_space,
203            show_unit,
204        }
205    }
206
207    /// Set the format type.
208    #[must_use]
209    pub fn with_format(mut self, format: TimeStringFormatting) -> Self {
210        self.format = format;
211        self
212    }
213
214    /// Set whether to show space between number and unit.
215    #[must_use]
216    pub fn with_space(mut self, show_space: bool) -> Self {
217        self.show_space = show_space;
218        self
219    }
220
221    /// Set whether to show the time unit.
222    #[must_use]
223    pub fn with_unit(mut self, show_unit: bool) -> Self {
224        self.show_unit = show_unit;
225        self
226    }
227}
228
229/// Draw the menu for selecting the time format.
230pub fn timeformat_menu(ui: &mut Ui, msgs: &mut Vec<Message>, current_timeformat: &TimeFormat) {
231    for time_string_format in enum_iterator::all::<TimeStringFormatting>() {
232        if ui
233            .radio(
234                current_timeformat.format == time_string_format,
235                if time_string_format == TimeStringFormatting::Locale {
236                    format!(
237                        "{time_string_format} ({locale})",
238                        locale = get_locale().unwrap_or_else(|| "unknown".to_string())
239                    )
240                } else {
241                    time_string_format.to_string()
242                },
243            )
244            .clicked()
245        {
246            msgs.push(Message::SetTimeStringFormatting(Some(time_string_format)));
247        }
248    }
249}
250
251/// How to format the numeric part of the time string.
252#[derive(Debug, Clone, Copy, Display, Eq, PartialEq, Serialize, Deserialize, Sequence)]
253pub enum TimeStringFormatting {
254    /// No additional formatting.
255    No,
256
257    /// Use the current locale to determine decimal separator, thousands separator, and grouping
258    Locale,
259
260    /// Use the SI standard: split into groups of three digits, unless there are exactly four
261    /// for both integer and fractional part. Use space as group separator.
262    SI,
263}
264
265/// Get rid of trailing zeros if the string contains a ., i.e., being fractional
266/// If the resulting string ends with ., remove that as well.
267fn strip_trailing_zeros_and_period(time: String) -> String {
268    if time.contains('.') {
269        time.trim_end_matches('0').trim_end_matches('.').to_string()
270    } else {
271        time
272    }
273}
274
275/// Format number based on [`TimeStringFormatting`], i.e., possibly group digits together
276/// and use correct separator for each group.
277fn split_and_format_number(time: &str, format: TimeStringFormatting) -> String {
278    match format {
279        TimeStringFormatting::No => time.to_string(),
280        TimeStringFormatting::Locale => format_locale(time, get_locale_format_cache()),
281        TimeStringFormatting::SI => format_si(time),
282    }
283}
284
285fn format_si(time: &str) -> String {
286    if let Some((integer_part, fractional_part)) = time.split_once('.') {
287        let integer_result = if integer_part.len() > 4 {
288            group_n_chars(integer_part, 3).join(THIN_SPACE)
289        } else {
290            integer_part.to_string()
291        };
292        if fractional_part.len() > 4 {
293            let reversed = fractional_part.chars().rev().collect::<String>();
294            let reversed_fractional_parts = group_n_chars(&reversed, 3).join(THIN_SPACE);
295            let fractional_result = reversed_fractional_parts.chars().rev().collect::<String>();
296            format!("{integer_result}.{fractional_result}")
297        } else {
298            format!("{integer_result}.{fractional_part}")
299        }
300    } else if time.len() > 4 {
301        group_n_chars(time, 3).join(THIN_SPACE)
302    } else {
303        time.to_string()
304    }
305}
306
307fn format_locale(time: &str, cache: &LocaleFormatCache) -> String {
308    if cache.grouping[0] > 0 {
309        if let Some((integer_part, fractional_part)) = time.split_once('.') {
310            let integer_result = group_n_chars(integer_part, cache.grouping[0] as usize)
311                .join(cache.thousands_sep.as_str());
312            format!(
313                "{integer_result}{decimal_point}{fractional_part}",
314                decimal_point = &cache.decimal_point
315            )
316        } else {
317            group_n_chars(time, cache.grouping[0] as usize).join(cache.thousands_sep.as_str())
318        }
319    } else {
320        time.to_string()
321    }
322}
323
324/// Heuristically find a suitable time unit for the given time.
325fn find_auto_scale(time: &BigInt, timescale: &TimeScale) -> TimeUnit {
326    // In case of seconds, nothing to do as it is the largest supported unit
327    // (unless we want to support minutes etc...)
328    if matches!(timescale.unit, TimeUnit::Seconds) {
329        return TimeUnit::Seconds;
330    }
331    let multiplier_digits = timescale.multiplier.unwrap_or(1).ilog10();
332    let start_digits = -timescale.unit.exponent();
333    for e in (3..=start_digits).step_by(3).rev() {
334        if (time % (BigInt::from(10).pow(e as u32 - multiplier_digits))).is_zero()
335            && let Some(unit) = TimeUnit::from_exponent(e - start_digits)
336        {
337            return unit;
338        }
339    }
340    timescale.unit
341}
342
343/// Formatter for time strings with caching of computed values.
344/// Enables efficient formatting of multiple time values with the same timescale and format settings.
345pub struct TimeFormatter {
346    timescale: TimeScale,
347    wanted_unit: TimeUnit,
348    time_format: TimeFormat,
349    /// Cached exponent difference (wanted - data)
350    exponent_diff: i8,
351    /// Cached unit string (empty if `show_unit` is false)
352    unit_string: String,
353    /// Cached space string (empty if `show_space` is false)
354    space_string: String,
355}
356
357impl TimeFormatter {
358    /// Create a new `TimeFormatter` with the given settings.
359    #[must_use]
360    pub fn new(timescale: &TimeScale, wanted_unit: &TimeUnit, time_format: &TimeFormat) -> Self {
361        // Note: For Auto unit, we defer resolution to format() time since it depends on the value
362        let (exponent_diff, unit_string) = if *wanted_unit == TimeUnit::Auto {
363            // Use placeholder values for Auto - will be computed per-format call
364            (0i8, String::new())
365        } else {
366            let wanted_exponent = wanted_unit.exponent();
367            let data_exponent = timescale.unit.exponent();
368            let exponent_diff = wanted_exponent - data_exponent;
369
370            let unit_string = if time_format.show_unit {
371                wanted_unit.to_string()
372            } else {
373                String::new()
374            };
375
376            (exponent_diff, unit_string)
377        };
378
379        TimeFormatter {
380            timescale: timescale.clone(),
381            wanted_unit: *wanted_unit,
382            time_format: time_format.clone(),
383            exponent_diff,
384            unit_string,
385            space_string: if time_format.show_space {
386                " ".to_string()
387            } else {
388                String::new()
389            },
390        }
391    }
392
393    /// Format a single time value.
394    #[must_use]
395    pub fn format(&self, time: &BigInt) -> String {
396        if self.wanted_unit == TimeUnit::None {
397            return split_and_format_number(&time.to_string(), self.time_format.format);
398        }
399
400        // Handle Auto unit by resolving it for this specific time value
401        let (exponent_diff, unit_string) = if self.wanted_unit == TimeUnit::Auto {
402            let auto_unit = find_auto_scale(time, &self.timescale);
403            let wanted_exponent = auto_unit.exponent();
404            let data_exponent = self.timescale.unit.exponent();
405            let exp_diff = wanted_exponent - data_exponent;
406
407            let unit_str = if self.time_format.show_unit {
408                auto_unit.to_string()
409            } else {
410                String::new()
411            };
412
413            (exp_diff, unit_str)
414        } else {
415            (self.exponent_diff, self.unit_string.clone())
416        };
417
418        let timestring = if exponent_diff >= 0 {
419            let precision = exponent_diff as usize;
420            strip_trailing_zeros_and_period(format!(
421                "{scaledtime:.precision$}",
422                scaledtime = BigRational::new(
423                    time * self.timescale.multiplier.unwrap_or(1),
424                    (BigInt::from(10)).pow(exponent_diff as u32)
425                )
426                .to_f64()
427                .unwrap_or(f64::NAN)
428            ))
429        } else {
430            (time
431                * self.timescale.multiplier.unwrap_or(1)
432                * (BigInt::from(10)).pow(-exponent_diff as u32))
433            .to_string()
434        };
435
436        format!(
437            "{scaledtime}{space}{unit}",
438            scaledtime = split_and_format_number(&timestring, self.time_format.format),
439            space = &self.space_string,
440            unit = &unit_string
441        )
442    }
443}
444
445/// Format the time string taking all settings into account.
446/// This function delegates to `TimeFormatter` which handles the Auto timeunit.
447#[must_use]
448pub fn time_string(
449    time: &BigInt,
450    timescale: &TimeScale,
451    wanted_timeunit: &TimeUnit,
452    wanted_time_format: &TimeFormat,
453) -> String {
454    let formatter = TimeFormatter::new(timescale, wanted_timeunit, wanted_time_format);
455    formatter.format(time)
456}
457
458impl WaveData {
459    pub fn draw_tick_line(&self, x: f32, ctx: &mut DrawingContext, stroke: &Stroke) {
460        let Pos2 {
461            x: x_pos,
462            y: y_start,
463        } = (ctx.to_screen)(x, 0.);
464        ctx.painter.vline(
465            x_pos,
466            (y_start)..=(y_start + ctx.cfg.canvas_height),
467            *stroke,
468        );
469    }
470
471    /// Draw the text for each tick location.
472    pub fn draw_ticks(
473        &self,
474        color: Color32,
475        ticks: &[(String, f32)],
476        ctx: &DrawingContext<'_>,
477        y_offset: f32,
478        align: Align2,
479    ) {
480        for (tick_text, x) in ticks {
481            ctx.painter.text(
482                (ctx.to_screen)(*x, y_offset),
483                align,
484                tick_text,
485                FontId::proportional(ctx.cfg.text_size),
486                color,
487            );
488        }
489    }
490}
491
492impl SystemState {
493    pub fn get_time_format(&self) -> TimeFormat {
494        let time_format = self.user.config.default_time_format.clone();
495        if let Some(time_string_format) = self.user.time_string_format {
496            time_format.with_format(time_string_format)
497        } else {
498            time_format
499        }
500    }
501
502    pub fn get_ticks_for_viewport_idx(
503        &self,
504        waves: &WaveData,
505        viewport_idx: usize,
506        frame_width: f32,
507        text_size: f32,
508    ) -> Vec<(String, f32)> {
509        self.get_ticks_for_viewport(
510            waves,
511            &waves.viewports[viewport_idx],
512            frame_width,
513            text_size,
514        )
515    }
516
517    pub fn get_ticks_for_viewport(
518        &self,
519        waves: &WaveData,
520        viewport: &Viewport,
521        frame_width: f32,
522        text_size: f32,
523    ) -> Vec<(String, f32)> {
524        get_ticks_internal(
525            viewport,
526            &waves.inner.metadata().timescale,
527            frame_width,
528            text_size,
529            &self.user.wanted_timeunit,
530            &self.get_time_format(),
531            self.user.config.theme.ticks.density,
532            &waves.safe_num_timestamps(),
533        )
534    }
535}
536
537/// Get suitable tick locations for the current view port.
538/// The method is based on guessing the length of the time string and
539/// is inspired by the corresponding code in Matplotlib.
540#[allow(clippy::too_many_arguments)]
541#[must_use]
542fn get_ticks_internal(
543    viewport: &Viewport,
544    timescale: &TimeScale,
545    frame_width: f32,
546    text_size: f32,
547    wanted_timeunit: &TimeUnit,
548    time_format: &TimeFormat,
549    density: f32,
550    num_timestamps: &BigInt,
551) -> Vec<(String, f32)> {
552    let char_width = text_size * (20. / 31.);
553    let rightexp = viewport
554        .curr_right
555        .absolute(num_timestamps)
556        .inner()
557        .abs()
558        .log10()
559        .round() as i16;
560    let leftexp = viewport
561        .curr_left
562        .absolute(num_timestamps)
563        .inner()
564        .abs()
565        .log10()
566        .round() as i16;
567    let max_labelwidth = f32::from(rightexp.max(leftexp) + 3) * char_width;
568    let max_labels = ((frame_width * density) / max_labelwidth).floor() + 2.;
569    let scale = 10.0f64.powf(
570        ((viewport.curr_right - viewport.curr_left)
571            .absolute(num_timestamps)
572            .inner()
573            / f64::from(max_labels))
574        .log10()
575        .floor(),
576    );
577
578    let mut ticks: Vec<(String, f32)> = [].to_vec();
579    for step in &TICK_STEPS {
580        let scaled_step = scale * step;
581        let rounded_min_label_time =
582            (viewport.curr_left.absolute(num_timestamps).inner() / scaled_step).floor()
583                * scaled_step;
584        let high = ((viewport.curr_right.absolute(num_timestamps).inner() - rounded_min_label_time)
585            / scaled_step)
586            .ceil() as f32
587            + 1.;
588        if high <= max_labels {
589            let time_formatter = TimeFormatter::new(timescale, wanted_timeunit, time_format);
590            ticks = (0..high as i16)
591                .map(|v| {
592                    BigInt::from((f64::from(v) * scaled_step + rounded_min_label_time) as i128)
593                })
594                .unique()
595                .map(|tick| {
596                    (
597                        // Time string
598                        time_formatter.format(&tick),
599                        // X position
600                        viewport.pixel_from_time(&tick, frame_width, num_timestamps),
601                    )
602                })
603                .collect::<Vec<(String, f32)>>();
604            break;
605        }
606    }
607    ticks
608}
609
610#[cfg(test)]
611mod test {
612    use num::BigInt;
613
614    use crate::time::{TimeFormat, TimeScale, TimeStringFormatting, TimeUnit, time_string};
615
616    #[test]
617    fn print_time_standard() {
618        assert_eq!(
619            time_string(
620                &BigInt::from(103),
621                &TimeScale {
622                    multiplier: Some(1),
623                    unit: TimeUnit::FemtoSeconds
624                },
625                &TimeUnit::FemtoSeconds,
626                &TimeFormat::default()
627            ),
628            "103 fs"
629        );
630        assert_eq!(
631            time_string(
632                &BigInt::from(2200),
633                &TimeScale {
634                    multiplier: Some(1),
635                    unit: TimeUnit::MicroSeconds
636                },
637                &TimeUnit::MicroSeconds,
638                &TimeFormat::default()
639            ),
640            "2200 μs"
641        );
642        assert_eq!(
643            time_string(
644                &BigInt::from(2200),
645                &TimeScale {
646                    multiplier: Some(1),
647                    unit: TimeUnit::MicroSeconds
648                },
649                &TimeUnit::MilliSeconds,
650                &TimeFormat::default()
651            ),
652            "2.2 ms"
653        );
654        assert_eq!(
655            time_string(
656                &BigInt::from(2200),
657                &TimeScale {
658                    multiplier: Some(1),
659                    unit: TimeUnit::MicroSeconds
660                },
661                &TimeUnit::NanoSeconds,
662                &TimeFormat::default()
663            ),
664            "2200000 ns"
665        );
666        assert_eq!(
667            time_string(
668                &BigInt::from(2200),
669                &TimeScale {
670                    multiplier: Some(1),
671                    unit: TimeUnit::NanoSeconds
672                },
673                &TimeUnit::PicoSeconds,
674                &TimeFormat {
675                    format: TimeStringFormatting::No,
676                    show_space: false,
677                    show_unit: true
678                }
679            ),
680            "2200000ps"
681        );
682        assert_eq!(
683            time_string(
684                &BigInt::from(2200),
685                &TimeScale {
686                    multiplier: Some(10),
687                    unit: TimeUnit::MicroSeconds
688                },
689                &TimeUnit::MicroSeconds,
690                &TimeFormat {
691                    format: TimeStringFormatting::No,
692                    show_space: false,
693                    show_unit: false
694                }
695            ),
696            "22000"
697        );
698    }
699    #[test]
700    fn print_time_si() {
701        assert_eq!(
702            time_string(
703                &BigInt::from(123456789010i128),
704                &TimeScale {
705                    multiplier: Some(1),
706                    unit: TimeUnit::MicroSeconds
707                },
708                &TimeUnit::Seconds,
709                &TimeFormat {
710                    format: TimeStringFormatting::SI,
711                    show_space: true,
712                    show_unit: true
713                }
714            ),
715            "123\u{2009}456.789\u{2009}01 s"
716        );
717        assert_eq!(
718            time_string(
719                &BigInt::from(1456789100i128),
720                &TimeScale {
721                    multiplier: Some(1),
722                    unit: TimeUnit::MicroSeconds
723                },
724                &TimeUnit::Seconds,
725                &TimeFormat {
726                    format: TimeStringFormatting::SI,
727                    show_space: true,
728                    show_unit: true
729                }
730            ),
731            "1456.7891 s"
732        );
733        assert_eq!(
734            time_string(
735                &BigInt::from(2200),
736                &TimeScale {
737                    multiplier: Some(1),
738                    unit: TimeUnit::MicroSeconds
739                },
740                &TimeUnit::MicroSeconds,
741                &TimeFormat {
742                    format: TimeStringFormatting::SI,
743                    show_space: true,
744                    show_unit: true
745                }
746            ),
747            "2200 μs"
748        );
749        assert_eq!(
750            time_string(
751                &BigInt::from(22200),
752                &TimeScale {
753                    multiplier: Some(1),
754                    unit: TimeUnit::MicroSeconds
755                },
756                &TimeUnit::MicroSeconds,
757                &TimeFormat {
758                    format: TimeStringFormatting::SI,
759                    show_space: true,
760                    show_unit: true
761                }
762            ),
763            "22\u{2009}200 μs"
764        );
765    }
766    #[test]
767    fn print_time_auto() {
768        assert_eq!(
769            time_string(
770                &BigInt::from(2200),
771                &TimeScale {
772                    multiplier: Some(1),
773                    unit: TimeUnit::MicroSeconds
774                },
775                &TimeUnit::Auto,
776                &TimeFormat {
777                    format: TimeStringFormatting::SI,
778                    show_space: true,
779                    show_unit: true
780                }
781            ),
782            "2200 μs"
783        );
784        assert_eq!(
785            time_string(
786                &BigInt::from(22000),
787                &TimeScale {
788                    multiplier: Some(1),
789                    unit: TimeUnit::MicroSeconds
790                },
791                &TimeUnit::Auto,
792                &TimeFormat {
793                    format: TimeStringFormatting::SI,
794                    show_space: true,
795                    show_unit: true
796                }
797            ),
798            "22 ms"
799        );
800        assert_eq!(
801            time_string(
802                &BigInt::from(1500000000),
803                &TimeScale {
804                    multiplier: Some(1),
805                    unit: TimeUnit::PicoSeconds
806                },
807                &TimeUnit::Auto,
808                &TimeFormat {
809                    format: TimeStringFormatting::SI,
810                    show_space: true,
811                    show_unit: true
812                }
813            ),
814            "1500 μs"
815        );
816        assert_eq!(
817            time_string(
818                &BigInt::from(22000),
819                &TimeScale {
820                    multiplier: Some(10),
821                    unit: TimeUnit::MicroSeconds
822                },
823                &TimeUnit::Auto,
824                &TimeFormat {
825                    format: TimeStringFormatting::SI,
826                    show_space: true,
827                    show_unit: true
828                }
829            ),
830            "220 ms"
831        );
832        assert_eq!(
833            time_string(
834                &BigInt::from(220000),
835                &TimeScale {
836                    multiplier: Some(100),
837                    unit: TimeUnit::MicroSeconds
838                },
839                &TimeUnit::Auto,
840                &TimeFormat {
841                    format: TimeStringFormatting::SI,
842                    show_space: true,
843                    show_unit: true
844                }
845            ),
846            "22 s"
847        );
848        assert_eq!(
849            time_string(
850                &BigInt::from(22000),
851                &TimeScale {
852                    multiplier: Some(10),
853                    unit: TimeUnit::Seconds
854                },
855                &TimeUnit::Auto,
856                &TimeFormat {
857                    format: TimeStringFormatting::No,
858                    show_space: true,
859                    show_unit: true
860                }
861            ),
862            "220000 s"
863        );
864    }
865    #[test]
866    fn print_time_none() {
867        assert_eq!(
868            time_string(
869                &BigInt::from(2200),
870                &TimeScale {
871                    multiplier: Some(1),
872                    unit: TimeUnit::MicroSeconds
873                },
874                &TimeUnit::None,
875                &TimeFormat {
876                    format: TimeStringFormatting::No,
877                    show_space: true,
878                    show_unit: true
879                }
880            ),
881            "2200"
882        );
883        assert_eq!(
884            time_string(
885                &BigInt::from(220),
886                &TimeScale {
887                    multiplier: Some(10),
888                    unit: TimeUnit::MicroSeconds
889                },
890                &TimeUnit::None,
891                &TimeFormat {
892                    format: TimeStringFormatting::No,
893                    show_space: true,
894                    show_unit: true
895                }
896            ),
897            "220"
898        );
899    }
900
901    #[test]
902    fn test_strip_trailing_zeros_and_period() {
903        use crate::time::strip_trailing_zeros_and_period;
904
905        assert_eq!(strip_trailing_zeros_and_period("123.000".into()), "123");
906        assert_eq!(strip_trailing_zeros_and_period("123.450".into()), "123.45");
907        assert_eq!(strip_trailing_zeros_and_period("123.456".into()), "123.456");
908        assert_eq!(strip_trailing_zeros_and_period("123.".into()), "123");
909        assert_eq!(strip_trailing_zeros_and_period("123".into()), "123");
910        assert_eq!(strip_trailing_zeros_and_period("0.000".into()), "0");
911        assert_eq!(strip_trailing_zeros_and_period("0.100".into()), "0.1");
912        assert_eq!(strip_trailing_zeros_and_period(String::new()), "");
913    }
914
915    #[test]
916    fn test_format_si() {
917        use crate::time::format_si;
918
919        // 4-digit rule: no grouping for 4 digits or less
920        assert_eq!(format_si("1234.56"), "1234.56");
921        assert_eq!(format_si("123.4"), "123.4");
922
923        // Grouping for 5+ digits
924        assert_eq!(format_si("12345.67"), "12\u{2009}345.67");
925        assert_eq!(format_si("1234567.89"), "1\u{2009}234\u{2009}567.89");
926        // No decimal part
927        assert_eq!(format_si("12345"), "12\u{2009}345");
928        assert_eq!(format_si("123"), "123");
929
930        // Empty inputs
931        assert_eq!(format_si("0.123"), "0.123");
932        assert_eq!(format_si(""), "");
933
934        // Decimal grouping
935        assert_eq!(format_si("123.4567890"), "123.456\u{2009}789\u{2009}0");
936    }
937
938    #[test]
939    fn test_time_unit_exponent() {
940        // Test exponent method
941        assert_eq!(TimeUnit::Seconds.exponent(), 0);
942        assert_eq!(TimeUnit::MilliSeconds.exponent(), -3);
943        assert_eq!(TimeUnit::MicroSeconds.exponent(), -6);
944        assert_eq!(TimeUnit::NanoSeconds.exponent(), -9);
945        assert_eq!(TimeUnit::PicoSeconds.exponent(), -12);
946        assert_eq!(TimeUnit::FemtoSeconds.exponent(), -15);
947        assert_eq!(TimeUnit::AttoSeconds.exponent(), -18);
948        assert_eq!(TimeUnit::ZeptoSeconds.exponent(), -21);
949
950        // Test from_exponent roundtrip
951        for unit in [
952            TimeUnit::Seconds,
953            TimeUnit::MilliSeconds,
954            TimeUnit::MicroSeconds,
955            TimeUnit::NanoSeconds,
956            TimeUnit::PicoSeconds,
957            TimeUnit::FemtoSeconds,
958            TimeUnit::AttoSeconds,
959            TimeUnit::ZeptoSeconds,
960        ] {
961            assert_eq!(TimeUnit::from_exponent(unit.exponent()), Some(unit));
962        }
963
964        // Invalid exponents
965        assert_eq!(TimeUnit::from_exponent(-5), None);
966        assert_eq!(TimeUnit::from_exponent(1), None);
967    }
968
969    #[test]
970    fn test_time_string_zero() {
971        // Test zero values
972        assert_eq!(
973            time_string(
974                &BigInt::from(0),
975                &TimeScale {
976                    multiplier: Some(1),
977                    unit: TimeUnit::MicroSeconds
978                },
979                &TimeUnit::MicroSeconds,
980                &TimeFormat::default()
981            ),
982            "0 μs"
983        );
984
985        assert_eq!(
986            time_string(
987                &BigInt::from(0),
988                &TimeScale {
989                    multiplier: Some(1),
990                    unit: TimeUnit::Seconds
991                },
992                &TimeUnit::Auto,
993                &TimeFormat::default()
994            ),
995            "0 s"
996        );
997    }
998
999    #[test]
1000    fn test_time_string_large_numbers() {
1001        // Test very large numbers with SI formatting
1002        assert_eq!(
1003            time_string(
1004                &BigInt::from(999_999_999_999i64),
1005                &TimeScale {
1006                    multiplier: Some(1),
1007                    unit: TimeUnit::NanoSeconds
1008                },
1009                &TimeUnit::Seconds,
1010                &TimeFormat {
1011                    format: TimeStringFormatting::SI,
1012                    show_space: true,
1013                    show_unit: true
1014                }
1015            ),
1016            "999.999\u{2009}999\u{2009}999 s"
1017        );
1018    }
1019
1020    #[test]
1021    fn test_time_string_no_multiplier() {
1022        // Test with None multiplier (raw ticks)
1023        assert_eq!(
1024            time_string(
1025                &BigInt::from(1234),
1026                &TimeScale {
1027                    multiplier: None,
1028                    unit: TimeUnit::NanoSeconds
1029                },
1030                &TimeUnit::NanoSeconds,
1031                &TimeFormat::default()
1032            ),
1033            "1234 ns"
1034        );
1035    }
1036
1037    #[test]
1038    fn test_time_format_variations() {
1039        let value = BigInt::from(123456);
1040        let scale = TimeScale {
1041            multiplier: Some(1),
1042            unit: TimeUnit::NanoSeconds,
1043        };
1044
1045        // Test all format variations
1046        assert_eq!(
1047            time_string(
1048                &value,
1049                &scale,
1050                &TimeUnit::NanoSeconds,
1051                &TimeFormat {
1052                    format: TimeStringFormatting::No,
1053                    show_space: true,
1054                    show_unit: true
1055                }
1056            ),
1057            "123456 ns"
1058        );
1059
1060        assert_eq!(
1061            time_string(
1062                &value,
1063                &scale,
1064                &TimeUnit::NanoSeconds,
1065                &TimeFormat {
1066                    format: TimeStringFormatting::No,
1067                    show_space: false,
1068                    show_unit: true
1069                }
1070            ),
1071            "123456ns"
1072        );
1073
1074        assert_eq!(
1075            time_string(
1076                &value,
1077                &scale,
1078                &TimeUnit::NanoSeconds,
1079                &TimeFormat {
1080                    format: TimeStringFormatting::No,
1081                    show_space: true,
1082                    show_unit: false
1083                }
1084            ),
1085            "123456 "
1086        );
1087
1088        assert_eq!(
1089            time_string(
1090                &value,
1091                &scale,
1092                &TimeUnit::NanoSeconds,
1093                &TimeFormat {
1094                    format: TimeStringFormatting::SI,
1095                    show_space: true,
1096                    show_unit: true
1097                }
1098            ),
1099            "123\u{2009}456 ns"
1100        );
1101    }
1102
1103    #[test]
1104    fn test_find_auto_scale_seconds_passthrough() {
1105        use crate::time::find_auto_scale;
1106
1107        let ts = TimeScale {
1108            unit: TimeUnit::Seconds,
1109            multiplier: Some(1),
1110        };
1111        assert_eq!(find_auto_scale(&BigInt::from(1), &ts), TimeUnit::Seconds);
1112        assert_eq!(
1113            find_auto_scale(&BigInt::from(1_234_567), &ts),
1114            TimeUnit::Seconds
1115        );
1116    }
1117
1118    #[test]
1119    fn test_find_auto_scale_nanoseconds() {
1120        use crate::time::find_auto_scale;
1121
1122        let ts = TimeScale {
1123            unit: TimeUnit::NanoSeconds,
1124            multiplier: Some(1),
1125        };
1126
1127        // Divisible by 10^9 -> seconds
1128        assert_eq!(
1129            find_auto_scale(&BigInt::from(1_000_000_000i64), &ts),
1130            TimeUnit::Seconds
1131        );
1132        // Divisible by 10^6 -> milliseconds
1133        assert_eq!(
1134            find_auto_scale(&BigInt::from(1_000_000), &ts),
1135            TimeUnit::MilliSeconds
1136        );
1137        // Divisible by 10^3 -> microseconds
1138        assert_eq!(
1139            find_auto_scale(&BigInt::from(1_000), &ts),
1140            TimeUnit::MicroSeconds
1141        );
1142        // Not divisible by 10^3 -> stay at nanos
1143        assert_eq!(
1144            find_auto_scale(&BigInt::from(1234), &ts),
1145            TimeUnit::NanoSeconds
1146        );
1147    }
1148
1149    #[test]
1150    fn test_find_auto_scale_microseconds_with_multiplier() {
1151        use crate::time::find_auto_scale;
1152
1153        // multiplier: None (treated as 1)
1154        let ts_none = TimeScale {
1155            unit: TimeUnit::MicroSeconds,
1156            multiplier: None,
1157        };
1158        assert_eq!(
1159            find_auto_scale(&BigInt::from(1_000_000), &ts_none),
1160            TimeUnit::Seconds
1161        );
1162        assert_eq!(
1163            find_auto_scale(&BigInt::from(1_000), &ts_none),
1164            TimeUnit::MilliSeconds
1165        );
1166        assert_eq!(
1167            find_auto_scale(&BigInt::from(123), &ts_none),
1168            TimeUnit::MicroSeconds
1169        );
1170
1171        // multiplier: Some(10) -> reduces required divisibility by 10^1
1172        let ts_mul10 = TimeScale {
1173            unit: TimeUnit::MicroSeconds,
1174            multiplier: Some(10),
1175        };
1176        assert_eq!(
1177            find_auto_scale(&BigInt::from(100_000), &ts_mul10),
1178            TimeUnit::Seconds
1179        );
1180        assert_eq!(
1181            find_auto_scale(&BigInt::from(100), &ts_mul10),
1182            TimeUnit::MilliSeconds
1183        );
1184        assert_eq!(
1185            find_auto_scale(&BigInt::from(123), &ts_mul10),
1186            TimeUnit::MicroSeconds
1187        );
1188    }
1189
1190    #[test]
1191    fn test_find_auto_scale_femtoseconds() {
1192        use crate::time::find_auto_scale;
1193
1194        let ts = TimeScale {
1195            unit: TimeUnit::FemtoSeconds,
1196            multiplier: Some(1),
1197        };
1198        // 10^15 fs = 1 s
1199        assert_eq!(
1200            find_auto_scale(&BigInt::from(10_i128.pow(15)), &ts),
1201            TimeUnit::Seconds
1202        );
1203        // 10^12 fs = 1 ms
1204        assert_eq!(
1205            find_auto_scale(&BigInt::from(10_i128.pow(12)), &ts),
1206            TimeUnit::MilliSeconds
1207        );
1208        // 10^9 fs = 1 μs
1209        assert_eq!(
1210            find_auto_scale(&BigInt::from(10_i128.pow(9)), &ts),
1211            TimeUnit::MicroSeconds
1212        );
1213        // 10^6 fs = 1 ns
1214        assert_eq!(
1215            find_auto_scale(&BigInt::from(10_i128.pow(6)), &ts),
1216            TimeUnit::NanoSeconds
1217        );
1218        // 10^3 fs = 1 ps
1219        assert_eq!(
1220            find_auto_scale(&BigInt::from(10_i128.pow(3)), &ts),
1221            TimeUnit::PicoSeconds
1222        );
1223        // Not divisible by 10^3 -> stay at fs
1224        assert_eq!(
1225            find_auto_scale(&BigInt::from(1), &ts),
1226            TimeUnit::FemtoSeconds
1227        );
1228    }
1229
1230    #[test]
1231    fn test_locale_cache_en_us() {
1232        use crate::time::{create_cache, format_locale};
1233        use pure_rust_locales::Locale;
1234
1235        let locale = Locale::en_US;
1236        let cache = create_cache(locale);
1237
1238        // en_US uses period as decimal point and comma as thousands separator
1239        let result = format_locale("1234567.89", &cache);
1240        assert_eq!(result, "1,234,567.89");
1241    }
1242
1243    #[test]
1244    fn test_locale_cache_de_de() {
1245        use crate::time::{create_cache, format_locale};
1246        use pure_rust_locales::Locale;
1247
1248        let locale = Locale::de_DE;
1249        let cache = create_cache(locale);
1250
1251        let result = format_locale("1234567.89", &cache);
1252        assert_eq!(result, "1.234.567,89");
1253    }
1254
1255    #[test]
1256    fn test_locale_cache_fr_fr() {
1257        use crate::time::{create_cache, format_locale};
1258        use pure_rust_locales::Locale;
1259
1260        let locale = Locale::fr_FR;
1261        let cache = create_cache(locale);
1262
1263        // fr_FR typically uses space/thin_space and comma
1264        let result = format_locale("1234567.89", &cache);
1265        // Verify it produces valid output
1266        assert_eq!(result, "1\u{2009}234\u{2009}567,89");
1267    }
1268
1269    #[test]
1270    fn test_locale_cache_small_numbers() {
1271        use crate::time::{create_cache, format_locale};
1272        use pure_rust_locales::Locale;
1273
1274        let locale = Locale::en_US;
1275        let cache = create_cache(locale);
1276
1277        // Numbers smaller than grouping threshold should remain unchanged
1278        assert_eq!(format_locale("123", &cache), "123");
1279        assert_eq!(format_locale("12.34", &cache), "12.34");
1280        assert_eq!(format_locale("0", &cache), "0");
1281    }
1282
1283    #[test]
1284    fn test_locale_cache_consistency_across_locales() {
1285        use crate::time::create_cache;
1286        use pure_rust_locales::Locale;
1287
1288        // Verify that creating cache for the same locale twice produces consistent results
1289        let cache1 = create_cache(Locale::en_US);
1290        let cache2 = create_cache(Locale::en_US);
1291
1292        assert_eq!(cache1.thousands_sep, cache2.thousands_sep);
1293        assert_eq!(cache1.decimal_point, cache2.decimal_point);
1294        assert_eq!(cache1.grouping, cache2.grouping);
1295    }
1296
1297    #[test]
1298    fn test_create_cache_from_various_locales() {
1299        use crate::time::{create_cache, format_locale};
1300        use pure_rust_locales::Locale;
1301
1302        // Test that create_cache works for many Locale variants without panicking
1303        let locales = vec![
1304            Locale::en_US,
1305            Locale::de_DE,
1306            Locale::fr_FR,
1307            Locale::es_ES,
1308            Locale::it_IT,
1309            Locale::pt_BR,
1310            Locale::pt_PT,
1311            Locale::ja_JP,
1312            Locale::zh_CN,
1313            Locale::zh_TW,
1314            Locale::ru_RU,
1315            Locale::ko_KR,
1316            Locale::pl_PL,
1317            Locale::tr_TR,
1318            Locale::nl_NL,
1319            Locale::sv_SE,
1320            Locale::da_DK,
1321            Locale::fi_FI,
1322            Locale::el_GR,
1323            Locale::hu_HU,
1324            Locale::cs_CZ,
1325            Locale::ro_RO,
1326            Locale::th_TH,
1327            Locale::vi_VN,
1328            Locale::ar_SA,
1329            Locale::he_IL,
1330            Locale::id_ID,
1331            Locale::uk_UA,
1332            Locale::en_GB,
1333            Locale::en_AU,
1334            Locale::en_CA,
1335            Locale::en_NZ,
1336            Locale::en_IN,
1337            Locale::fr_CA,
1338            Locale::de_AT,
1339            Locale::de_CH,
1340            Locale::fr_CH,
1341            Locale::it_CH,
1342            Locale::es_MX,
1343            Locale::es_AR,
1344        ];
1345
1346        for locale in locales {
1347            let cache = create_cache(locale);
1348            // Check so that it is not empty for a sample number
1349            assert!(
1350                !format_locale("1234567.89", &cache).is_empty(),
1351                "Failed for {locale:?}"
1352            );
1353        }
1354    }
1355}
1356
1357#[cfg(test)]
1358mod get_ticks_tests {
1359    use super::*;
1360    use itertools::Itertools;
1361    use num::BigInt;
1362
1363    // Basic smoke test: ensure we get at least one tick and that returned
1364    // pixel coordinates lie within the frame width.
1365    #[test]
1366    fn get_ticks_basic() {
1367        let vp = crate::viewport::Viewport::default();
1368        let timescale = TimeScale {
1369            unit: TimeUnit::MicroSeconds,
1370            multiplier: Some(1),
1371        };
1372        let frame_width = 800.0_f32;
1373        let text_size = 12.0_f32;
1374        let wanted = TimeUnit::MicroSeconds;
1375        let time_format = TimeFormat::default();
1376        let config = crate::config::SurferConfig::default();
1377        let num_timestamps = BigInt::from(1_000_000i64);
1378
1379        let ticks = get_ticks_internal(
1380            &vp,
1381            &timescale,
1382            frame_width,
1383            text_size,
1384            &wanted,
1385            &time_format,
1386            config.theme.ticks.density,
1387            &num_timestamps,
1388        );
1389
1390        assert!(!ticks.is_empty(), "expected at least one tick");
1391
1392        // Check monotonic x positions and collect labels for uniqueness check
1393        let mut last_x = -1.0_f32;
1394        let mut labels: Vec<String> = Vec::with_capacity(ticks.len());
1395        for (label, x) in &ticks {
1396            assert!(
1397                *x >= last_x,
1398                "tick x not monotonic: {x} < {last_x} for label {label}"
1399            );
1400            last_x = *x;
1401            assert!(*x >= 0.0, "tick x < 0: {x}");
1402            assert!(
1403                *x <= frame_width,
1404                "tick x > frame_width: {x} > {frame_width}"
1405            );
1406            labels.push(label.clone());
1407        }
1408        // Labels should be unique
1409        let unique_labels = labels.iter().unique().count();
1410        assert_eq!(labels.len(), unique_labels, "duplicate tick labels found");
1411    }
1412
1413    // Ensure tick generation produces a reasonable number of ticks when
1414    // viewport is zoomed and density is high.
1415    #[test]
1416    fn get_ticks_respects_frame_width_and_density() {
1417        let mut vp = crate::viewport::Viewport::default();
1418        // zoom to a narrower view
1419        vp.curr_left = crate::viewport::Relative(0.0);
1420        vp.curr_right = crate::viewport::Relative(0.1);
1421
1422        let timescale = TimeScale {
1423            unit: TimeUnit::NanoSeconds,
1424            multiplier: Some(1),
1425        };
1426        let frame_width = 200.0_f32;
1427        let text_size = 10.0_f32;
1428        let wanted = TimeUnit::Auto;
1429        let time_format = TimeFormat {
1430            format: TimeStringFormatting::SI,
1431            show_space: true,
1432            show_unit: true,
1433        };
1434
1435        let mut config = crate::config::SurferConfig::default();
1436        // make ticks dense
1437        config.theme.ticks.density = 1.0;
1438
1439        let num_timestamps = BigInt::from(1_000_000i64);
1440
1441        let ticks = get_ticks_internal(
1442            &vp,
1443            &timescale,
1444            frame_width,
1445            text_size,
1446            &wanted,
1447            &time_format,
1448            config.theme.ticks.density,
1449            &num_timestamps,
1450        );
1451
1452        assert!(!ticks.is_empty(), "expected ticks even for narrow view");
1453        // expect a sane upper bound (protects against accidental infinite loops)
1454        assert!(ticks.len() < 200, "too many ticks: {}", ticks.len());
1455
1456        // monotonic x positions and unique labels
1457        let mut last_x = -1.0_f32;
1458        let mut labels: Vec<String> = Vec::with_capacity(ticks.len());
1459        for (label, x) in &ticks {
1460            assert!(
1461                *x >= last_x,
1462                "tick x not monotonic: {x} < {last_x} for label {label}"
1463            );
1464            last_x = *x;
1465            assert!(*x >= 0.0, "tick x < 0: {x}");
1466            assert!(
1467                *x <= frame_width,
1468                "tick x > frame_width: {x} > {frame_width}"
1469            );
1470            labels.push(label.clone());
1471        }
1472        let unique_labels = labels.iter().unique().count();
1473        assert_eq!(labels.len(), unique_labels, "duplicate tick labels found");
1474    }
1475}