1use 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 #[display("Auto")]
57 Auto,
58}
59
60pub const DEFAULT_TIMELINE_NAME: &str = "Time";
61const THIN_SPACE: &str = "\u{2009}";
62
63pub const TICK_STEPS: [f64; 8] = [1., 2., 2.5, 5., 10., 20., 25., 50.];
65
66struct LocaleFormatCache {
68 grouping: &'static [i64],
69 thousands_sep: String,
70 decimal_point: String,
71}
72
73static LOCALE_FORMAT_CACHE: OnceLock<LocaleFormatCache> = OnceLock::new();
74
75fn 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 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 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
163pub 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#[derive(Debug, Deserialize, Serialize, Clone)]
177pub struct TimeFormat {
178 format: TimeStringFormatting,
180 show_space: bool,
182 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 #[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 #[must_use]
209 pub fn with_format(mut self, format: TimeStringFormatting) -> Self {
210 self.format = format;
211 self
212 }
213
214 #[must_use]
216 pub fn with_space(mut self, show_space: bool) -> Self {
217 self.show_space = show_space;
218 self
219 }
220
221 #[must_use]
223 pub fn with_unit(mut self, show_unit: bool) -> Self {
224 self.show_unit = show_unit;
225 self
226 }
227}
228
229pub 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#[derive(Debug, Clone, Copy, Display, Eq, PartialEq, Serialize, Deserialize, Sequence)]
253pub enum TimeStringFormatting {
254 No,
256
257 Locale,
259
260 SI,
263}
264
265fn 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
275fn 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
324fn find_auto_scale(time: &BigInt, timescale: &TimeScale) -> TimeUnit {
326 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
343pub struct TimeFormatter {
346 timescale: TimeScale,
347 wanted_unit: TimeUnit,
348 time_format: TimeFormat,
349 exponent_diff: i8,
351 unit_string: String,
353 space_string: String,
355}
356
357impl TimeFormatter {
358 #[must_use]
360 pub fn new(timescale: &TimeScale, wanted_unit: &TimeUnit, time_format: &TimeFormat) -> Self {
361 let (exponent_diff, unit_string) = if *wanted_unit == TimeUnit::Auto {
363 (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 #[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 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(×tring, self.time_format.format),
439 space = &self.space_string,
440 unit = &unit_string
441 )
442 }
443}
444
445#[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 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#[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_formatter.format(&tick),
599 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 assert_eq!(format_si("1234.56"), "1234.56");
921 assert_eq!(format_si("123.4"), "123.4");
922
923 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 assert_eq!(format_si("12345"), "12\u{2009}345");
928 assert_eq!(format_si("123"), "123");
929
930 assert_eq!(format_si("0.123"), "0.123");
932 assert_eq!(format_si(""), "");
933
934 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 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 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 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 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 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 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 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 assert_eq!(
1129 find_auto_scale(&BigInt::from(1_000_000_000i64), &ts),
1130 TimeUnit::Seconds
1131 );
1132 assert_eq!(
1134 find_auto_scale(&BigInt::from(1_000_000), &ts),
1135 TimeUnit::MilliSeconds
1136 );
1137 assert_eq!(
1139 find_auto_scale(&BigInt::from(1_000), &ts),
1140 TimeUnit::MicroSeconds
1141 );
1142 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 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 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 assert_eq!(
1200 find_auto_scale(&BigInt::from(10_i128.pow(15)), &ts),
1201 TimeUnit::Seconds
1202 );
1203 assert_eq!(
1205 find_auto_scale(&BigInt::from(10_i128.pow(12)), &ts),
1206 TimeUnit::MilliSeconds
1207 );
1208 assert_eq!(
1210 find_auto_scale(&BigInt::from(10_i128.pow(9)), &ts),
1211 TimeUnit::MicroSeconds
1212 );
1213 assert_eq!(
1215 find_auto_scale(&BigInt::from(10_i128.pow(6)), &ts),
1216 TimeUnit::NanoSeconds
1217 );
1218 assert_eq!(
1220 find_auto_scale(&BigInt::from(10_i128.pow(3)), &ts),
1221 TimeUnit::PicoSeconds
1222 );
1223 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 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 let result = format_locale("1234567.89", &cache);
1265 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 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 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 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 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 #[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 ×cale,
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 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 let unique_labels = labels.iter().unique().count();
1410 assert_eq!(labels.len(), unique_labels, "duplicate tick labels found");
1411 }
1412
1413 #[test]
1416 fn get_ticks_respects_frame_width_and_density() {
1417 let mut vp = crate::viewport::Viewport::default();
1418 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 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 ×cale,
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 assert!(ticks.len() < 200, "too many ticks: {}", ticks.len());
1455
1456 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}