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};
11use pure_rust_locales::{locale_match, Locale};
12use serde::{Deserialize, Serialize};
13use sys_locale::get_locale;
14
15use crate::config::SurferConfig;
16use crate::viewport::Viewport;
17use crate::wave_data::WaveData;
18use crate::{translation::group_n_chars, view::DrawingContext, Message, SystemState};
19
20#[derive(Serialize, Deserialize)]
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
63impl From<wellen::TimescaleUnit> for TimeUnit {
64    fn from(timescale: wellen::TimescaleUnit) -> Self {
65        match timescale {
66            wellen::TimescaleUnit::ZeptoSeconds => TimeUnit::ZeptoSeconds,
67            wellen::TimescaleUnit::AttoSeconds => TimeUnit::AttoSeconds,
68            wellen::TimescaleUnit::FemtoSeconds => TimeUnit::FemtoSeconds,
69            wellen::TimescaleUnit::PicoSeconds => TimeUnit::PicoSeconds,
70            wellen::TimescaleUnit::NanoSeconds => TimeUnit::NanoSeconds,
71            wellen::TimescaleUnit::MicroSeconds => TimeUnit::MicroSeconds,
72            wellen::TimescaleUnit::MilliSeconds => TimeUnit::MilliSeconds,
73            wellen::TimescaleUnit::Seconds => TimeUnit::Seconds,
74            wellen::TimescaleUnit::Unknown => TimeUnit::None,
75        }
76    }
77}
78
79impl From<ftr_parser::types::Timescale> for TimeUnit {
80    fn from(timescale: Timescale) -> Self {
81        match timescale {
82            Timescale::Fs => TimeUnit::FemtoSeconds,
83            Timescale::Ps => TimeUnit::PicoSeconds,
84            Timescale::Ns => TimeUnit::NanoSeconds,
85            Timescale::Us => TimeUnit::MicroSeconds,
86            Timescale::Ms => TimeUnit::MilliSeconds,
87            Timescale::S => TimeUnit::Seconds,
88            Timescale::Unit => TimeUnit::None,
89            Timescale::None => TimeUnit::None,
90        }
91    }
92}
93
94impl TimeUnit {
95    /// Get the power-of-ten exponent for a time unit.
96    fn exponent(&self) -> i8 {
97        match self {
98            TimeUnit::ZeptoSeconds => -21,
99            TimeUnit::AttoSeconds => -18,
100            TimeUnit::FemtoSeconds => -15,
101            TimeUnit::PicoSeconds => -12,
102            TimeUnit::NanoSeconds => -9,
103            TimeUnit::MicroSeconds => -6,
104            TimeUnit::MilliSeconds => -3,
105            TimeUnit::Seconds => 0,
106            TimeUnit::None => 0,
107            TimeUnit::Auto => 0,
108        }
109    }
110    /// Convert a power-of-ten exponent to a time unit.
111    fn from_exponent(exponent: i8) -> Self {
112        match exponent {
113            -21 => TimeUnit::ZeptoSeconds,
114            -18 => TimeUnit::AttoSeconds,
115            -15 => TimeUnit::FemtoSeconds,
116            -12 => TimeUnit::PicoSeconds,
117            -9 => TimeUnit::NanoSeconds,
118            -6 => TimeUnit::MicroSeconds,
119            -3 => TimeUnit::MilliSeconds,
120            0 => TimeUnit::Seconds,
121            _ => panic!("Invalid exponent"),
122        }
123    }
124}
125
126/// Create menu for selecting preferred time unit.
127pub fn timeunit_menu(ui: &mut Ui, msgs: &mut Vec<Message>, wanted_timeunit: &TimeUnit) {
128    for timeunit in enum_iterator::all::<TimeUnit>() {
129        ui.radio(*wanted_timeunit == timeunit, timeunit.to_string())
130            .clicked()
131            .then(|| {
132                msgs.push(Message::SetTimeUnit(timeunit));
133            });
134    }
135}
136
137/// How to format the time stamps.
138#[derive(Debug, Deserialize, Serialize)]
139pub struct TimeFormat {
140    /// How to format the numeric part of the time string.
141    format: TimeStringFormatting,
142    /// Insert a space between number and unit.
143    show_space: bool,
144    /// Display time unit.
145    show_unit: bool,
146}
147
148impl Default for TimeFormat {
149    fn default() -> Self {
150        TimeFormat {
151            format: TimeStringFormatting::No,
152            show_space: true,
153            show_unit: true,
154        }
155    }
156}
157
158impl TimeFormat {
159    /// Utility function to get a copy, but with some values changed.
160    pub fn get_with_changes(
161        &self,
162        format: Option<TimeStringFormatting>,
163        show_space: Option<bool>,
164        show_unit: Option<bool>,
165    ) -> Self {
166        TimeFormat {
167            format: format.unwrap_or(self.format),
168            show_space: show_space.unwrap_or(self.show_space),
169            show_unit: show_unit.unwrap_or(self.show_unit),
170        }
171    }
172}
173
174/// Draw the menu for selecting the time format.
175pub fn timeformat_menu(ui: &mut Ui, msgs: &mut Vec<Message>, current_timeformat: &TimeFormat) {
176    for time_string_format in enum_iterator::all::<TimeStringFormatting>() {
177        ui.radio(
178            current_timeformat.format == time_string_format,
179            if time_string_format == TimeStringFormatting::Locale {
180                format!(
181                    "{time_string_format} ({locale})",
182                    locale = get_locale().unwrap_or_else(|| "unknown".to_string())
183                )
184            } else {
185                time_string_format.to_string()
186            },
187        )
188        .clicked()
189        .then(|| {
190            msgs.push(Message::SetTimeStringFormatting(Some(time_string_format)));
191        });
192    }
193}
194
195/// How to format the numeric part of the time string.
196#[derive(Debug, Clone, Copy, Display, Eq, PartialEq, Serialize, Deserialize, Sequence)]
197pub enum TimeStringFormatting {
198    /// No additional formatting.
199    No,
200
201    /// Use the current locale to determine decimal separator, thousands separator, and grouping
202    Locale,
203
204    /// Use the SI standard: split into groups of three digits, unless there are exactly four
205    /// for both integer and fractional part. Use space as group separator.
206    SI,
207}
208
209/// Get rid of trailing zeros if the string contains a ., i.e., being fractional
210/// If the resulting string ends with ., remove that as well.
211fn strip_trailing_zeros_and_period(time: String) -> String {
212    if time.contains('.') {
213        time.trim_end_matches('0').trim_end_matches('.').to_string()
214    } else {
215        time
216    }
217}
218
219/// Format number based on [`TimeStringFormatting`], i.e., possibly group digits together
220/// and use correct separator for each group.
221fn split_and_format_number(time: String, format: &TimeStringFormatting) -> String {
222    match format {
223        TimeStringFormatting::No => time,
224        TimeStringFormatting::Locale => {
225            let locale: Locale = get_locale()
226                .unwrap_or_else(|| "en-US".to_string())
227                .as_str()
228                .try_into()
229                .unwrap_or(Locale::en_US);
230            let grouping = locale_match!(locale => LC_NUMERIC::GROUPING);
231            if grouping[0] > 0 {
232                // "\u{202f}" (non-breaking thin space) does not exist in used font, replace with "\u{2009}" (thin space)
233                let thousands_sep = locale_match!(locale => LC_NUMERIC::THOUSANDS_SEP)
234                    .replace('\u{202f}', THIN_SPACE);
235                if time.contains('.') {
236                    let decimal_point = locale_match!(locale => LC_NUMERIC::DECIMAL_POINT);
237                    let mut parts = time.split('.');
238                    let integer_result = group_n_chars(parts.next().unwrap(), grouping[0] as usize)
239                        .join(thousands_sep.as_str());
240                    let fractional_part = parts.next().unwrap();
241                    format!("{integer_result}{decimal_point}{fractional_part}")
242                } else {
243                    group_n_chars(&time, grouping[0] as usize).join(thousands_sep.as_str())
244                }
245            } else {
246                time
247            }
248        }
249        TimeStringFormatting::SI => {
250            if time.contains('.') {
251                let mut parts = time.split('.');
252                let integer_part = parts.next().unwrap();
253                let fractional_part = parts.next().unwrap();
254                let integer_result = if integer_part.len() > 4 {
255                    group_n_chars(integer_part, 3).join(THIN_SPACE)
256                } else {
257                    integer_part.to_string()
258                };
259                if fractional_part.len() > 4 {
260                    let reversed = fractional_part.chars().rev().collect::<String>();
261                    let reversed_fractional_parts = group_n_chars(&reversed, 3).join(THIN_SPACE);
262                    let fractional_result =
263                        reversed_fractional_parts.chars().rev().collect::<String>();
264                    format!("{integer_result}.{fractional_result}")
265                } else {
266                    format!("{integer_result}.{fractional_part}")
267                }
268            } else if time.len() > 4 {
269                group_n_chars(&time, 3).join(THIN_SPACE)
270            } else {
271                time
272            }
273        }
274    }
275}
276
277/// Heuristically find a suitable time unit for the given time.
278fn find_auto_scale(time: &BigInt, timescale: &TimeScale) -> TimeUnit {
279    // In case of seconds, nothing to do as it is the largest supported unit
280    // (unless we want to support minutes etc...)
281    if timescale.unit == TimeUnit::Seconds {
282        return TimeUnit::Seconds;
283    }
284    let multiplier_digits = timescale.multiplier.unwrap_or(1).ilog10();
285    let start_digits = -timescale.unit.exponent();
286    for e in (3..=start_digits).step_by(3).rev() {
287        if (time % (BigInt::from(10).pow(e as u32 - multiplier_digits))) == BigInt::from(0) {
288            return TimeUnit::from_exponent(e - start_digits);
289        }
290    }
291    timescale.unit
292}
293
294/// Format the time string taking all settings into account.
295pub fn time_string(
296    time: &BigInt,
297    timescale: &TimeScale,
298    wanted_timeunit: &TimeUnit,
299    wanted_time_format: &TimeFormat,
300) -> String {
301    if wanted_timeunit == &TimeUnit::Auto {
302        let auto_timeunit = find_auto_scale(time, timescale);
303        return time_string(time, timescale, &auto_timeunit, wanted_time_format);
304    }
305    if wanted_timeunit == &TimeUnit::None {
306        return split_and_format_number(time.to_string(), &wanted_time_format.format);
307    }
308    let wanted_exponent = wanted_timeunit.exponent();
309    let data_exponent = timescale.unit.exponent();
310    let exponent_diff = wanted_exponent - data_exponent;
311    let timeunit = if wanted_time_format.show_unit {
312        wanted_timeunit.to_string()
313    } else {
314        String::new()
315    };
316    let space = if wanted_time_format.show_space {
317        " ".to_string()
318    } else {
319        String::new()
320    };
321    let timestring = if exponent_diff >= 0 {
322        let precision = exponent_diff as usize;
323        strip_trailing_zeros_and_period(format!(
324            "{scaledtime:.precision$}",
325            scaledtime = BigRational::new(
326                time * timescale.multiplier.unwrap_or(1),
327                (BigInt::from(10)).pow(exponent_diff as u32)
328            )
329            .to_f64()
330            .unwrap_or(f64::NAN)
331        ))
332    } else {
333        (time * timescale.multiplier.unwrap_or(1) * (BigInt::from(10)).pow(-exponent_diff as u32))
334            .to_string()
335    };
336    format!(
337        "{scaledtime}{space}{timeunit}",
338        scaledtime = split_and_format_number(timestring, &wanted_time_format.format)
339    )
340}
341
342impl WaveData {
343    /// Get suitable tick locations for the current view port.
344    /// The method is based on guessing the length of the time string and
345    /// is inspired by the corresponding code in Matplotlib.
346    #[allow(clippy::too_many_arguments)]
347    pub fn get_ticks(
348        &self,
349        viewport: &Viewport,
350        timescale: &TimeScale,
351        frame_width: f32,
352        text_size: f32,
353        wanted_timeunit: &TimeUnit,
354        time_format: &TimeFormat,
355        config: &SurferConfig,
356    ) -> Vec<(String, f32)> {
357        let num_timestamps = self.num_timestamps().unwrap_or(1.into());
358        let char_width = text_size * (20. / 31.);
359        let rightexp = viewport
360            .curr_right
361            .absolute(&num_timestamps)
362            .0
363            .abs()
364            .log10()
365            .round() as i16;
366        let leftexp = viewport
367            .curr_left
368            .absolute(&num_timestamps)
369            .0
370            .abs()
371            .log10()
372            .round() as i16;
373        let max_labelwidth = (rightexp.max(leftexp) + 3) as f32 * char_width;
374        let max_labels = ((frame_width * config.theme.ticks.density) / max_labelwidth).floor() + 2.;
375        let scale = 10.0f64.powf(
376            ((viewport.curr_right - viewport.curr_left)
377                .absolute(&num_timestamps)
378                .0
379                / max_labels as f64)
380                .log10()
381                .floor(),
382        );
383
384        let steps = &[1., 2., 2.5, 5., 10., 20., 25., 50.];
385        let mut ticks: Vec<(String, f32)> = [].to_vec();
386        for step in steps {
387            let scaled_step = scale * step;
388            let rounded_min_label_time =
389                (viewport.curr_left.absolute(&num_timestamps).0 / scaled_step).floor()
390                    * scaled_step;
391            let high = ((viewport.curr_right.absolute(&num_timestamps).0 - rounded_min_label_time)
392                / scaled_step)
393                .ceil() as f32
394                + 1.;
395            if high <= max_labels {
396                ticks = (0..high as i16)
397                    .map(|v| {
398                        BigInt::from(((v as f64) * scaled_step + rounded_min_label_time) as i128)
399                    })
400                    .unique()
401                    .map(|tick| {
402                        (
403                            // Time string
404                            time_string(&tick, timescale, wanted_timeunit, time_format),
405                            viewport.pixel_from_time(&tick, frame_width, &num_timestamps),
406                        )
407                    })
408                    .collect::<Vec<(String, f32)>>();
409                break;
410            }
411        }
412        ticks
413    }
414
415    pub fn draw_tick_line(&self, x: f32, ctx: &mut DrawingContext, stroke: &Stroke) {
416        let Pos2 {
417            x: x_pos,
418            y: y_start,
419        } = (ctx.to_screen)(x, 0.);
420        ctx.painter.vline(
421            x_pos,
422            (y_start)..=(y_start + ctx.cfg.canvas_height),
423            *stroke,
424        );
425    }
426
427    /// Draw the text for each tick location.
428    pub fn draw_ticks(
429        &self,
430        color: Option<&Color32>,
431        ticks: &Vec<(String, f32)>,
432        ctx: &DrawingContext<'_>,
433        y_offset: f32,
434        align: Align2,
435        config: &SurferConfig,
436    ) {
437        let color = *color.unwrap_or(&config.theme.foreground);
438
439        for (tick_text, x) in ticks {
440            ctx.painter.text(
441                (ctx.to_screen)(*x, y_offset),
442                align,
443                tick_text,
444                FontId::proportional(ctx.cfg.text_size),
445                color,
446            );
447        }
448    }
449}
450
451impl SystemState {
452    pub fn get_time_format(&self) -> TimeFormat {
453        self.user.config.default_time_format.get_with_changes(
454            self.user.time_string_format,
455            None,
456            None,
457        )
458    }
459}
460
461#[cfg(test)]
462mod test {
463    use num::BigInt;
464
465    use crate::time::{time_string, TimeFormat, TimeScale, TimeStringFormatting, TimeUnit};
466
467    #[test]
468    fn print_time_standard() {
469        assert_eq!(
470            time_string(
471                &BigInt::from(103),
472                &TimeScale {
473                    multiplier: Some(1),
474                    unit: TimeUnit::FemtoSeconds
475                },
476                &TimeUnit::FemtoSeconds,
477                &TimeFormat::default()
478            ),
479            "103 fs"
480        );
481        assert_eq!(
482            time_string(
483                &BigInt::from(2200),
484                &TimeScale {
485                    multiplier: Some(1),
486                    unit: TimeUnit::MicroSeconds
487                },
488                &TimeUnit::MicroSeconds,
489                &TimeFormat::default()
490            ),
491            "2200 μs"
492        );
493        assert_eq!(
494            time_string(
495                &BigInt::from(2200),
496                &TimeScale {
497                    multiplier: Some(1),
498                    unit: TimeUnit::MicroSeconds
499                },
500                &TimeUnit::MilliSeconds,
501                &TimeFormat::default()
502            ),
503            "2.2 ms"
504        );
505        assert_eq!(
506            time_string(
507                &BigInt::from(2200),
508                &TimeScale {
509                    multiplier: Some(1),
510                    unit: TimeUnit::MicroSeconds
511                },
512                &TimeUnit::NanoSeconds,
513                &TimeFormat::default()
514            ),
515            "2200000 ns"
516        );
517        assert_eq!(
518            time_string(
519                &BigInt::from(2200),
520                &TimeScale {
521                    multiplier: Some(1),
522                    unit: TimeUnit::NanoSeconds
523                },
524                &TimeUnit::PicoSeconds,
525                &TimeFormat {
526                    format: TimeStringFormatting::No,
527                    show_space: false,
528                    show_unit: true
529                }
530            ),
531            "2200000ps"
532        );
533        assert_eq!(
534            time_string(
535                &BigInt::from(2200),
536                &TimeScale {
537                    multiplier: Some(10),
538                    unit: TimeUnit::MicroSeconds
539                },
540                &TimeUnit::MicroSeconds,
541                &TimeFormat {
542                    format: TimeStringFormatting::No,
543                    show_space: false,
544                    show_unit: false
545                }
546            ),
547            "22000"
548        );
549    }
550    #[test]
551    fn print_time_si() {
552        assert_eq!(
553            time_string(
554                &BigInt::from(123456789010i128),
555                &TimeScale {
556                    multiplier: Some(1),
557                    unit: TimeUnit::MicroSeconds
558                },
559                &TimeUnit::Seconds,
560                &TimeFormat {
561                    format: TimeStringFormatting::SI,
562                    show_space: true,
563                    show_unit: true
564                }
565            ),
566            "123\u{2009}456.789\u{2009}01 s"
567        );
568        assert_eq!(
569            time_string(
570                &BigInt::from(1456789100i128),
571                &TimeScale {
572                    multiplier: Some(1),
573                    unit: TimeUnit::MicroSeconds
574                },
575                &TimeUnit::Seconds,
576                &TimeFormat {
577                    format: TimeStringFormatting::SI,
578                    show_space: true,
579                    show_unit: true
580                }
581            ),
582            "1456.7891 s"
583        );
584        assert_eq!(
585            time_string(
586                &BigInt::from(2200),
587                &TimeScale {
588                    multiplier: Some(1),
589                    unit: TimeUnit::MicroSeconds
590                },
591                &TimeUnit::MicroSeconds,
592                &TimeFormat {
593                    format: TimeStringFormatting::SI,
594                    show_space: true,
595                    show_unit: true
596                }
597            ),
598            "2200 μs"
599        );
600        assert_eq!(
601            time_string(
602                &BigInt::from(22200),
603                &TimeScale {
604                    multiplier: Some(1),
605                    unit: TimeUnit::MicroSeconds
606                },
607                &TimeUnit::MicroSeconds,
608                &TimeFormat {
609                    format: TimeStringFormatting::SI,
610                    show_space: true,
611                    show_unit: true
612                }
613            ),
614            "22\u{2009}200 μs"
615        );
616    }
617    #[test]
618    fn print_time_auto() {
619        assert_eq!(
620            time_string(
621                &BigInt::from(2200),
622                &TimeScale {
623                    multiplier: Some(1),
624                    unit: TimeUnit::MicroSeconds
625                },
626                &TimeUnit::Auto,
627                &TimeFormat {
628                    format: TimeStringFormatting::SI,
629                    show_space: true,
630                    show_unit: true
631                }
632            ),
633            "2200 μs"
634        );
635        assert_eq!(
636            time_string(
637                &BigInt::from(22000),
638                &TimeScale {
639                    multiplier: Some(1),
640                    unit: TimeUnit::MicroSeconds
641                },
642                &TimeUnit::Auto,
643                &TimeFormat {
644                    format: TimeStringFormatting::SI,
645                    show_space: true,
646                    show_unit: true
647                }
648            ),
649            "22 ms"
650        );
651        assert_eq!(
652            time_string(
653                &BigInt::from(1500000000),
654                &TimeScale {
655                    multiplier: Some(1),
656                    unit: TimeUnit::PicoSeconds
657                },
658                &TimeUnit::Auto,
659                &TimeFormat {
660                    format: TimeStringFormatting::SI,
661                    show_space: true,
662                    show_unit: true
663                }
664            ),
665            "1500 μs"
666        );
667        assert_eq!(
668            time_string(
669                &BigInt::from(22000),
670                &TimeScale {
671                    multiplier: Some(10),
672                    unit: TimeUnit::MicroSeconds
673                },
674                &TimeUnit::Auto,
675                &TimeFormat {
676                    format: TimeStringFormatting::SI,
677                    show_space: true,
678                    show_unit: true
679                }
680            ),
681            "220 ms"
682        );
683        assert_eq!(
684            time_string(
685                &BigInt::from(220000),
686                &TimeScale {
687                    multiplier: Some(100),
688                    unit: TimeUnit::MicroSeconds
689                },
690                &TimeUnit::Auto,
691                &TimeFormat {
692                    format: TimeStringFormatting::SI,
693                    show_space: true,
694                    show_unit: true
695                }
696            ),
697            "22 s"
698        );
699        assert_eq!(
700            time_string(
701                &BigInt::from(22000),
702                &TimeScale {
703                    multiplier: Some(10),
704                    unit: TimeUnit::Seconds
705                },
706                &TimeUnit::Auto,
707                &TimeFormat {
708                    format: TimeStringFormatting::No,
709                    show_space: true,
710                    show_unit: true
711                }
712            ),
713            "220000 s"
714        );
715    }
716    #[test]
717    fn print_time_none() {
718        assert_eq!(
719            time_string(
720                &BigInt::from(2200),
721                &TimeScale {
722                    multiplier: Some(1),
723                    unit: TimeUnit::MicroSeconds
724                },
725                &TimeUnit::None,
726                &TimeFormat {
727                    format: TimeStringFormatting::No,
728                    show_space: true,
729                    show_unit: true
730                }
731            ),
732            "2200"
733        );
734        assert_eq!(
735            time_string(
736                &BigInt::from(220),
737                &TimeScale {
738                    multiplier: Some(10),
739                    unit: TimeUnit::MicroSeconds
740                },
741                &TimeUnit::None,
742                &TimeFormat {
743                    format: TimeStringFormatting::No,
744                    show_space: true,
745                    show_unit: true
746                }
747            ),
748            "220"
749        );
750    }
751}