1use 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 #[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 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 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
116pub 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#[derive(Debug, Deserialize, Serialize)]
130pub struct TimeFormat {
131 format: TimeStringFormatting,
133 show_space: bool,
135 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 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
165pub 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#[derive(Debug, Clone, Copy, Display, Eq, PartialEq, Serialize, Deserialize, Sequence)]
189pub enum TimeStringFormatting {
190 #[display("No")]
192 No,
193
194 #[display("Locale")]
196 Locale,
197
198 #[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
219fn 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
229fn 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 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
287fn find_auto_scale(time: &BigInt, timescale: &TimeScale) -> TimeUnit {
289 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
304pub 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 #[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(&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 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}