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};
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 #[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 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 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
126pub 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#[derive(Debug, Deserialize, Serialize)]
139pub struct TimeFormat {
140 format: TimeStringFormatting,
142 show_space: bool,
144 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 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
174pub 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#[derive(Debug, Clone, Copy, Display, Eq, PartialEq, Serialize, Deserialize, Sequence)]
197pub enum TimeStringFormatting {
198 No,
200
201 Locale,
203
204 SI,
207}
208
209fn 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
219fn 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 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
277fn find_auto_scale(time: &BigInt, timescale: &TimeScale) -> TimeUnit {
279 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
294pub 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 #[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(&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 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}