libsurfer/
time.rs

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