1use derive_more::Display;
3use ecolor::Color32;
4use egui::{Button, Key, RichText, Ui};
5use egui_remixicon::icons;
6use emath::{Align2, Pos2};
7use enum_iterator::Sequence;
8use epaint::{FontId, Stroke};
9use ftr_parser::types::Timescale;
10use itertools::Itertools;
11use num::{BigInt, BigRational, ToPrimitive, Zero};
12use pure_rust_locales::{Locale, locale_match};
13use serde::{Deserialize, Serialize};
14use std::sync::OnceLock;
15use sys_locale::get_locale;
16
17use crate::viewport::Viewport;
18use crate::wave_data::WaveData;
19use crate::{
20 Message, SystemState,
21 translation::group_n_chars,
22 view::{DrawConfig, DrawingContext},
23};
24
25#[derive(Serialize, Deserialize, Clone)]
26pub struct TimeScale {
27 pub unit: TimeUnit,
28 pub multiplier: Option<u32>,
29}
30
31#[derive(Debug, Clone, Copy, Display, Eq, PartialEq, Serialize, Deserialize, Sequence)]
32pub enum TimeUnit {
33 #[display("zs")]
34 ZeptoSeconds,
35
36 #[display("as")]
37 AttoSeconds,
38
39 #[display("fs")]
40 FemtoSeconds,
41
42 #[display("ps")]
43 PicoSeconds,
44
45 #[display("ns")]
46 NanoSeconds,
47
48 #[display("μs")]
49 MicroSeconds,
50
51 #[display("ms")]
52 MilliSeconds,
53
54 #[display("s")]
55 Seconds,
56
57 #[display("No unit")]
58 None,
59
60 #[display("Auto")]
62 Auto,
63}
64
65pub const DEFAULT_TIMELINE_NAME: &str = "Time";
66const THIN_SPACE: &str = "\u{2009}";
67
68pub const TICK_STEPS: [f64; 8] = [1., 2., 2.5, 5., 10., 20., 25., 50.];
70
71struct LocaleFormatCache {
73 grouping: &'static [i64],
74 thousands_sep: String,
75 decimal_point: String,
76}
77
78static LOCALE_FORMAT_CACHE: OnceLock<LocaleFormatCache> = OnceLock::new();
79
80fn get_locale_format_cache() -> &'static LocaleFormatCache {
82 LOCALE_FORMAT_CACHE.get_or_init(|| {
83 let locale = get_locale()
84 .unwrap_or_else(|| "en-US".to_string())
85 .as_str()
86 .try_into()
87 .unwrap_or(Locale::en_US);
88 create_cache(locale)
89 })
90}
91
92fn create_cache(locale: Locale) -> LocaleFormatCache {
93 let grouping = locale_match!(locale => LC_NUMERIC::GROUPING);
94 let thousands_sep =
95 locale_match!(locale => LC_NUMERIC::THOUSANDS_SEP).replace('\u{202f}', THIN_SPACE);
96 let decimal_point = locale_match!(locale => LC_NUMERIC::DECIMAL_POINT).to_string();
97
98 LocaleFormatCache {
99 grouping,
100 thousands_sep,
101 decimal_point,
102 }
103}
104
105impl From<wellen::TimescaleUnit> for TimeUnit {
106 fn from(timescale: wellen::TimescaleUnit) -> Self {
107 match timescale {
108 wellen::TimescaleUnit::ZeptoSeconds => TimeUnit::ZeptoSeconds,
109 wellen::TimescaleUnit::AttoSeconds => TimeUnit::AttoSeconds,
110 wellen::TimescaleUnit::FemtoSeconds => TimeUnit::FemtoSeconds,
111 wellen::TimescaleUnit::PicoSeconds => TimeUnit::PicoSeconds,
112 wellen::TimescaleUnit::NanoSeconds => TimeUnit::NanoSeconds,
113 wellen::TimescaleUnit::MicroSeconds => TimeUnit::MicroSeconds,
114 wellen::TimescaleUnit::MilliSeconds => TimeUnit::MilliSeconds,
115 wellen::TimescaleUnit::Seconds => TimeUnit::Seconds,
116 wellen::TimescaleUnit::Unknown => TimeUnit::None,
117 }
118 }
119}
120
121impl From<ftr_parser::types::Timescale> for TimeUnit {
122 fn from(timescale: Timescale) -> Self {
123 match timescale {
124 Timescale::Fs => TimeUnit::FemtoSeconds,
125 Timescale::Ps => TimeUnit::PicoSeconds,
126 Timescale::Ns => TimeUnit::NanoSeconds,
127 Timescale::Us => TimeUnit::MicroSeconds,
128 Timescale::Ms => TimeUnit::MilliSeconds,
129 Timescale::S => TimeUnit::Seconds,
130 Timescale::Unit => TimeUnit::None,
131 Timescale::None => TimeUnit::None,
132 }
133 }
134}
135
136impl TimeUnit {
137 fn exponent(self) -> i8 {
139 match self {
140 TimeUnit::ZeptoSeconds => -21,
141 TimeUnit::AttoSeconds => -18,
142 TimeUnit::FemtoSeconds => -15,
143 TimeUnit::PicoSeconds => -12,
144 TimeUnit::NanoSeconds => -9,
145 TimeUnit::MicroSeconds => -6,
146 TimeUnit::MilliSeconds => -3,
147 TimeUnit::Seconds => 0,
148 TimeUnit::None => 0,
149 TimeUnit::Auto => 0,
150 }
151 }
152 fn from_exponent(exponent: i8) -> Option<Self> {
154 match exponent {
155 -21 => Some(TimeUnit::ZeptoSeconds),
156 -18 => Some(TimeUnit::AttoSeconds),
157 -15 => Some(TimeUnit::FemtoSeconds),
158 -12 => Some(TimeUnit::PicoSeconds),
159 -9 => Some(TimeUnit::NanoSeconds),
160 -6 => Some(TimeUnit::MicroSeconds),
161 -3 => Some(TimeUnit::MilliSeconds),
162 0 => Some(TimeUnit::Seconds),
163 _ => None,
164 }
165 }
166}
167
168pub fn timeunit_menu(ui: &mut Ui, msgs: &mut Vec<Message>, wanted_timeunit: &TimeUnit) {
170 for timeunit in enum_iterator::all::<TimeUnit>() {
171 if ui
172 .radio(*wanted_timeunit == timeunit, timeunit.to_string())
173 .clicked()
174 {
175 msgs.push(Message::SetTimeUnit(timeunit));
176 }
177 }
178}
179
180#[derive(Debug, Deserialize, Serialize, Clone)]
182pub struct TimeFormat {
183 format: TimeStringFormatting,
185 show_space: bool,
187 show_unit: bool,
189}
190
191impl Default for TimeFormat {
192 fn default() -> Self {
193 TimeFormat {
194 format: TimeStringFormatting::No,
195 show_space: true,
196 show_unit: true,
197 }
198 }
199}
200
201impl TimeFormat {
202 #[must_use]
204 pub fn new(format: TimeStringFormatting, show_space: bool, show_unit: bool) -> Self {
205 TimeFormat {
206 format,
207 show_space,
208 show_unit,
209 }
210 }
211
212 #[must_use]
214 pub fn with_format(mut self, format: TimeStringFormatting) -> Self {
215 self.format = format;
216 self
217 }
218
219 #[must_use]
221 pub fn with_space(mut self, show_space: bool) -> Self {
222 self.show_space = show_space;
223 self
224 }
225
226 #[must_use]
228 pub fn with_unit(mut self, show_unit: bool) -> Self {
229 self.show_unit = show_unit;
230 self
231 }
232}
233
234pub fn timeformat_menu(ui: &mut Ui, msgs: &mut Vec<Message>, current_timeformat: &TimeFormat) {
236 for time_string_format in enum_iterator::all::<TimeStringFormatting>() {
237 if ui
238 .radio(
239 current_timeformat.format == time_string_format,
240 if time_string_format == TimeStringFormatting::Locale {
241 format!(
242 "{time_string_format} ({locale})",
243 locale = get_locale().unwrap_or_else(|| "unknown".to_string())
244 )
245 } else {
246 time_string_format.to_string()
247 },
248 )
249 .clicked()
250 {
251 msgs.push(Message::SetTimeStringFormatting(Some(time_string_format)));
252 }
253 }
254}
255
256#[derive(Debug, Clone, Copy, Display, Eq, PartialEq, Serialize, Deserialize, Sequence)]
258pub enum TimeStringFormatting {
259 No,
261
262 Locale,
264
265 SI,
268}
269
270fn strip_trailing_zeros_and_period(time: String) -> String {
273 if !time.contains('.') {
274 return time;
275 }
276 time.trim_end_matches('0').trim_end_matches('.').to_string()
277}
278
279fn split_and_format_number(time: &str, format: TimeStringFormatting) -> String {
282 match format {
283 TimeStringFormatting::No => time.to_string(),
284 TimeStringFormatting::Locale => format_locale(time, get_locale_format_cache()),
285 TimeStringFormatting::SI => format_si(time),
286 }
287}
288
289fn format_si(time: &str) -> String {
290 if let Some((integer_part, fractional_part)) = time.split_once('.') {
291 let integer_result = if integer_part.len() > 4 {
292 group_n_chars(integer_part, 3).join(THIN_SPACE)
293 } else {
294 integer_part.to_string()
295 };
296 if fractional_part.len() > 4 {
297 let reversed = fractional_part.chars().rev().collect::<String>();
298 let reversed_fractional_parts = group_n_chars(&reversed, 3).join(THIN_SPACE);
299 let fractional_result = reversed_fractional_parts.chars().rev().collect::<String>();
300 format!("{integer_result}.{fractional_result}")
301 } else {
302 format!("{integer_result}.{fractional_part}")
303 }
304 } else if time.len() > 4 {
305 group_n_chars(time, 3).join(THIN_SPACE)
306 } else {
307 time.to_string()
308 }
309}
310
311fn format_locale(time: &str, cache: &LocaleFormatCache) -> String {
312 if cache.grouping[0] > 0 {
313 if let Some((integer_part, fractional_part)) = time.split_once('.') {
314 let integer_result = group_n_chars(integer_part, cache.grouping[0] as usize)
315 .join(cache.thousands_sep.as_str());
316 format!(
317 "{integer_result}{decimal_point}{fractional_part}",
318 decimal_point = &cache.decimal_point
319 )
320 } else {
321 group_n_chars(time, cache.grouping[0] as usize).join(cache.thousands_sep.as_str())
322 }
323 } else {
324 time.to_string()
325 }
326}
327
328fn find_auto_scale(time: &BigInt, timescale: &TimeScale) -> TimeUnit {
330 if matches!(timescale.unit, TimeUnit::Seconds) {
333 return TimeUnit::Seconds;
334 }
335 let multiplier_digits = timescale.multiplier.unwrap_or(1).ilog10();
336 let start_digits = -timescale.unit.exponent();
337 for e in (3..=start_digits).step_by(3).rev() {
338 if (time % pow10(e as u32 - multiplier_digits)).is_zero()
339 && let Some(unit) = TimeUnit::from_exponent(e - start_digits)
340 {
341 return unit;
342 }
343 }
344 timescale.unit
345}
346
347pub struct TimeFormatter {
350 timescale: TimeScale,
351 wanted_unit: TimeUnit,
352 time_format: TimeFormat,
353 exponent_diff: i8,
355 unit_string: String,
357 space_string: String,
359}
360
361impl TimeFormatter {
362 #[must_use]
364 pub fn new(timescale: &TimeScale, wanted_unit: &TimeUnit, time_format: &TimeFormat) -> Self {
365 let (exponent_diff, unit_string) = if *wanted_unit == TimeUnit::Auto {
367 (0i8, String::new())
369 } else {
370 let wanted_exponent = wanted_unit.exponent();
371 let data_exponent = timescale.unit.exponent();
372 let exponent_diff = wanted_exponent - data_exponent;
373
374 let unit_string = if time_format.show_unit {
375 wanted_unit.to_string()
376 } else {
377 String::new()
378 };
379
380 (exponent_diff, unit_string)
381 };
382
383 TimeFormatter {
384 timescale: timescale.clone(),
385 wanted_unit: *wanted_unit,
386 time_format: time_format.clone(),
387 exponent_diff,
388 unit_string,
389 space_string: if time_format.show_space {
390 " ".to_string()
391 } else {
392 String::new()
393 },
394 }
395 }
396
397 #[must_use]
399 pub fn format(&self, time: &BigInt) -> String {
400 if self.wanted_unit == TimeUnit::None {
401 return split_and_format_number(&time.to_string(), self.time_format.format);
402 }
403
404 let (exponent_diff, unit_string) = if self.wanted_unit == TimeUnit::Auto {
406 let auto_unit = find_auto_scale(time, &self.timescale);
407 let wanted_exponent = auto_unit.exponent();
408 let data_exponent = self.timescale.unit.exponent();
409 let exp_diff = wanted_exponent - data_exponent;
410
411 let unit_str = if self.time_format.show_unit {
412 auto_unit.to_string()
413 } else {
414 String::new()
415 };
416
417 (exp_diff, unit_str)
418 } else {
419 (self.exponent_diff, self.unit_string.clone())
420 };
421
422 let timestring = if exponent_diff >= 0 {
423 let precision = exponent_diff as usize;
424 strip_trailing_zeros_and_period(format!(
425 "{scaledtime:.precision$}",
426 scaledtime = BigRational::new(
427 time * self.timescale.multiplier.unwrap_or(1),
428 pow10(exponent_diff as u32)
429 )
430 .to_f64()
431 .unwrap_or(f64::NAN)
432 ))
433 } else {
434 (time * self.timescale.multiplier.unwrap_or(1) * pow10(-exponent_diff as u32))
435 .to_string()
436 };
437
438 format!(
439 "{scaledtime}{space}{unit}",
440 scaledtime = split_and_format_number(×tring, self.time_format.format),
441 space = if unit_string.is_empty() {
442 ""
443 } else {
444 &self.space_string
445 },
446 unit = &unit_string
447 )
448 }
449}
450
451fn pow10(exp: u32) -> BigInt {
454 match exp {
455 0 => BigInt::from(1),
456 1 => BigInt::from(10),
457 2 => BigInt::from(100),
458 3 => BigInt::from(1000),
459 6 => BigInt::from(1_000_000),
460 9 => BigInt::from(1_000_000_000),
461 12 => BigInt::from(1_000_000_000_000i64),
462 15 => BigInt::from(1_000_000_000_000_000i64),
463 18 => BigInt::from(1_000_000_000_000_000_000i64),
464 21 => BigInt::from(1_000_000_000_000_000_000_000i128),
465 _ => BigInt::from(10).pow(exp),
466 }
467}
468
469#[must_use]
472pub fn time_string(
473 time: &BigInt,
474 timescale: &TimeScale,
475 wanted_timeunit: &TimeUnit,
476 wanted_time_format: &TimeFormat,
477) -> String {
478 let formatter = TimeFormatter::new(timescale, wanted_timeunit, wanted_time_format);
479 formatter.format(time)
480}
481
482fn parse_time_input(input: &str) -> (String, Option<TimeUnit>) {
487 let sorted_units =
488 [
490 ("zs", TimeUnit::ZeptoSeconds),
491 ("as", TimeUnit::AttoSeconds),
492 ("fs", TimeUnit::FemtoSeconds),
493 ("ps", TimeUnit::PicoSeconds),
494 ("ns", TimeUnit::NanoSeconds),
495 ("μs", TimeUnit::MicroSeconds),
496 ("us", TimeUnit::MicroSeconds), ("ms", TimeUnit::MilliSeconds),
498 ("s", TimeUnit::Seconds),
499 ];
500
501 let trimmed = input.trim();
502
503 for (unit_str, unit) in sorted_units {
504 if trimmed.ends_with(unit_str) {
506 let after_number = trimmed.len() - unit_str.len();
507
508 if after_number > 0 {
510 let numeric = trimmed[..after_number].trim_end();
511 if let Some(last_char) = numeric.chars().next_back()
512 && (last_char.is_ascii_digit() || last_char == '.')
513 {
514 return (numeric.to_string(), Some(unit));
515 }
516 }
517 }
518 }
519
520 (trimmed.to_string(), None)
521}
522
523fn split_numeric_parts(numeric_str: &str) -> Result<(String, String), String> {
527 let trimmed = numeric_str.trim();
528 if trimmed.is_empty() {
529 return Err("Empty input".to_string());
530 }
531 if trimmed.starts_with('-') {
532 return Err("Negative numbers not supported".to_string());
533 }
534 let normalized = trimmed.strip_prefix('+').unwrap_or(trimmed);
535 let mut parts = normalized.split('.');
536 let integer_part = parts.next().unwrap_or("");
537 let fractional_part = parts.next().unwrap_or("");
538 if parts.next().is_some() {
539 return Err("Invalid number: multiple decimal points".to_string());
540 }
541
542 let all_valid =
544 (integer_part.chars().chain(fractional_part.chars())).all(|c| c.is_ascii_digit());
545 if !all_valid {
546 return Err(format!("Failed to parse '{numeric_str}' as number"));
547 }
548
549 let integer = if integer_part.is_empty() {
550 "0".to_string()
551 } else {
552 integer_part.to_string()
553 };
554 Ok((integer, fractional_part.to_string()))
555}
556
557fn normalize_numeric_with_unit(
562 numeric_str: &str,
563 unit: TimeUnit,
564) -> Result<(BigInt, TimeUnit), String> {
565 let (integer_part, mut fractional_part) = split_numeric_parts(numeric_str)?;
566
567 while fractional_part.ends_with('0') {
569 fractional_part.pop();
570 }
571
572 if fractional_part.is_empty() {
573 let value =
574 BigInt::parse_bytes(integer_part.as_bytes(), 10).unwrap_or_else(|| BigInt::from(0));
575 return Ok((value, unit));
576 }
577
578 let fractional_len = fractional_part.len();
579 if fractional_len > 21 {
581 return Err("Too many decimal places (max 21 supported)".to_string());
582 }
583
584 let steps = fractional_len.div_ceil(3) as i8; let new_exponent = unit.exponent() - (steps * 3);
586 let new_unit = TimeUnit::from_exponent(new_exponent)
587 .ok_or_else(|| "Too much precision for available time units".to_string())?;
588
589 let mut combined = integer_part;
590 combined.push_str(&fractional_part);
591 let mut value = BigInt::parse_bytes(combined.as_bytes(), 10).unwrap_or_else(|| BigInt::from(0));
592
593 let extra_zeros = (steps as usize * 3).saturating_sub(fractional_len);
594 if extra_zeros > 0 {
595 let scale = pow10(extra_zeros as u32);
596 value *= scale;
597 }
598
599 Ok((value, new_unit))
600}
601
602#[derive(Clone, Debug)]
606pub struct TimeInputState {
607 input_text: String,
609 parsed_value: Option<BigInt>,
611 input_unit: Option<TimeUnit>,
613 normalized_unit: Option<TimeUnit>,
615 selected_unit: TimeUnit,
617 error: Option<String>,
619}
620
621impl Default for TimeInputState {
622 fn default() -> Self {
623 Self {
624 input_text: String::new(),
625 parsed_value: None,
626 input_unit: None,
627 normalized_unit: None,
628 selected_unit: TimeUnit::NanoSeconds,
629 error: None,
630 }
631 }
632}
633
634impl TimeInputState {
635 pub fn new() -> Self {
637 Self::default()
638 }
639
640 pub fn update_input(&mut self, input: String) {
642 self.input_text = input;
643 let (numeric_str, unit) = parse_time_input(&self.input_text);
644 self.input_unit = unit;
645
646 let base_unit = self.input_unit.unwrap_or(self.selected_unit);
647 match normalize_numeric_with_unit(&numeric_str, base_unit) {
648 Ok((val, normalized_unit)) => {
649 self.parsed_value = Some(val);
650 self.normalized_unit = Some(normalized_unit);
651 self.error = None;
652 }
653 Err(e) => {
654 self.parsed_value = None;
655 self.normalized_unit = None;
656 self.error = Some(e);
657 }
658 }
659 }
660
661 pub fn effective_unit(&self) -> TimeUnit {
663 self.normalized_unit
664 .or(self.input_unit)
665 .unwrap_or(self.selected_unit)
666 }
667
668 pub fn to_timescale_ticks(&self, timescale: &TimeScale) -> Option<BigInt> {
672 let value = self.parsed_value.clone()?;
673 let unit = self.effective_unit();
674 let base_unit = if unit == TimeUnit::None {
675 timescale.unit
676 } else {
677 unit
678 };
679 let unit_exp = base_unit.exponent();
680 let data_exp = timescale.unit.exponent();
681 let diff = unit_exp - data_exp;
682
683 let mut result = value;
684 if diff > 0 {
685 let scale = pow10(diff as u32);
686 result *= scale;
687 } else if diff < 0 {
688 let scale = pow10((-diff) as u32);
689 result /= scale;
690 }
691
692 let multiplier = timescale.multiplier.unwrap_or(1);
693 if multiplier != 1 {
694 let mult = BigInt::from(multiplier);
695 result /= mult;
696 }
697
698 Some(result)
699 }
700}
701
702pub fn time_input_widget(
717 ui: &mut Ui,
718 waves: &WaveData,
719 msgs: &mut Vec<Message>,
720 state: &mut TimeInputState,
721 request_focus: bool,
722) {
723 ui.horizontal(|ui| {
724 let mut input = state.input_text.clone();
726 let text_response = ui.add(
727 egui::TextEdit::singleline(&mut input)
728 .desired_width(100.0)
729 .hint_text("e.g., 1.5ms"),
730 );
731
732 if request_focus && !text_response.has_focus() {
733 text_response.request_focus();
734 msgs.push(Message::SetRequestTimeEditFocus(false));
735 }
736
737 if text_response.changed() {
738 state.update_input(input);
739 }
740
741 let dropdown_enabled = state.input_unit.is_none();
743
744 if dropdown_enabled {
745 egui::ComboBox::new("Unit", "")
746 .width(32.0)
747 .selected_text(state.selected_unit.to_string())
748 .show_ui(ui, |ui| {
749 for unit in enum_iterator::all::<TimeUnit>() {
750 if !matches!(unit, TimeUnit::Auto | TimeUnit::None) {
752 ui.selectable_value(&mut state.selected_unit, unit, unit.to_string());
753 }
754 }
755 });
756 }
757
758 if text_response.gained_focus() {
760 msgs.push(Message::SetTimeEditFocused(true));
761 }
762 if text_response.lost_focus() {
763 if text_response.ctx.input(|i| i.key_pressed(Key::Enter)) {
764 if let Some(time_stamp) =
766 state.to_timescale_ticks(&waves.inner.metadata().timescale)
767 {
768 msgs.push(Message::GoToTime(Some(time_stamp), 0));
769 }
770 }
771 msgs.push(Message::SetTimeEditFocused(false));
772 }
773
774 let button_enabled = state.parsed_value.is_some();
776 let goto_button = Button::new(RichText::new(icons::TARGET_FILL).heading()).frame(false);
777 if ui
778 .add_enabled(button_enabled, goto_button)
779 .on_hover_text("Go to time")
780 .clicked()
781 && let Some(time_stamp) = state.to_timescale_ticks(&waves.inner.metadata().timescale)
782 {
783 msgs.push(Message::GoToTime(Some(time_stamp), 0));
784 }
785 let cursor_button = Button::new(RichText::new(icons::CURSOR_FILL).heading()).frame(false);
786 if ui
787 .add_enabled(button_enabled, cursor_button)
788 .on_hover_text("Set cursor at time")
789 .clicked()
790 && let Some(time_stamp) = state.to_timescale_ticks(&waves.inner.metadata().timescale)
791 {
792 msgs.push(Message::CursorSet(time_stamp));
793 }
794 });
795}
796
797impl WaveData {
798 pub fn draw_tick_line(&self, x: f32, ctx: &mut DrawingContext, stroke: &Stroke) {
799 let Pos2 {
800 x: x_pos,
801 y: y_start,
802 } = (ctx.to_screen)(x, 0.);
803 ctx.painter.vline(
804 x_pos,
805 (y_start)..=(y_start + ctx.cfg.canvas_height),
806 *stroke,
807 );
808 }
809 pub fn draw_ticks(
811 &self,
812 color: Color32,
813 ticks: &[(String, f32)],
814 ctx: &DrawingContext<'_>,
815 y_offset: f32,
816 align: Align2,
817 ) {
818 for (tick_text, x) in ticks {
819 ctx.painter.text(
820 (ctx.to_screen)(*x, y_offset),
821 align,
822 tick_text,
823 FontId::proportional(ctx.cfg.text_size),
824 color,
825 );
826 }
827 }
828}
829
830impl SystemState {
831 pub fn get_time_format(&self) -> TimeFormat {
832 let time_format = self.user.config.default_time_format.clone();
833 if let Some(time_string_format) = self.user.time_string_format {
834 time_format.with_format(time_string_format)
835 } else {
836 time_format
837 }
838 }
839
840 pub fn get_ticks_for_viewport_idx(
841 &self,
842 waves: &WaveData,
843 viewport_idx: usize,
844 cfg: &DrawConfig,
845 ) -> Vec<(String, f32)> {
846 self.get_ticks_for_viewport(waves, &waves.viewports[viewport_idx], cfg)
847 }
848
849 pub fn get_ticks_for_viewport(
850 &self,
851 waves: &WaveData,
852 viewport: &Viewport,
853 cfg: &DrawConfig,
854 ) -> Vec<(String, f32)> {
855 get_ticks_internal(
856 viewport,
857 &waves.inner.metadata().timescale,
858 cfg.canvas_width,
859 cfg.text_size,
860 &self.user.wanted_timeunit,
861 &self.get_time_format(),
862 self.user.config.theme.ticks.density,
863 &waves.safe_num_timestamps(),
864 )
865 }
866}
867
868#[allow(clippy::too_many_arguments)]
872#[must_use]
873fn get_ticks_internal(
874 viewport: &Viewport,
875 timescale: &TimeScale,
876 frame_width: f32,
877 text_size: f32,
878 wanted_timeunit: &TimeUnit,
879 time_format: &TimeFormat,
880 density: f32,
881 num_timestamps: &BigInt,
882) -> Vec<(String, f32)> {
883 let char_width = text_size * (20. / 31.);
884 let rightexp = viewport
885 .curr_right
886 .absolute(num_timestamps)
887 .inner()
888 .abs()
889 .log10()
890 .round() as i16;
891 let leftexp = viewport
892 .curr_left
893 .absolute(num_timestamps)
894 .inner()
895 .abs()
896 .log10()
897 .round() as i16;
898 let max_labelwidth = f32::from(rightexp.max(leftexp) + 3) * char_width;
899 let max_labels = ((frame_width * density) / max_labelwidth).floor() + 2.;
900 let scale = 10.0f64.powf(
901 ((viewport.curr_right - viewport.curr_left)
902 .absolute(num_timestamps)
903 .inner()
904 / f64::from(max_labels))
905 .log10()
906 .floor(),
907 );
908
909 let mut ticks: Vec<(String, f32)> = [].to_vec();
910 for step in &TICK_STEPS {
911 let scaled_step = scale * step;
912 let rounded_min_label_time =
913 (viewport.curr_left.absolute(num_timestamps).inner() / scaled_step).floor()
914 * scaled_step;
915 let high = ((viewport.curr_right.absolute(num_timestamps).inner() - rounded_min_label_time)
916 / scaled_step)
917 .ceil() as f32
918 + 1.;
919 if high <= max_labels {
920 let time_formatter = TimeFormatter::new(timescale, wanted_timeunit, time_format);
921 ticks = (0..high as i16)
922 .map(|v| {
923 BigInt::from((f64::from(v) * scaled_step + rounded_min_label_time) as i128)
924 })
925 .unique()
926 .map(|tick| {
927 (
928 time_formatter.format(&tick),
930 viewport.pixel_from_time(&tick, frame_width, num_timestamps),
932 )
933 })
934 .collect::<Vec<(String, f32)>>();
935 break;
936 }
937 }
938 ticks
939}
940
941#[cfg(test)]
942mod test {
943 use num::BigInt;
944
945 use crate::time::{TimeFormat, TimeScale, TimeStringFormatting, TimeUnit, time_string};
946
947 #[test]
948 fn print_time_standard() {
949 assert_eq!(
950 time_string(
951 &BigInt::from(103),
952 &TimeScale {
953 multiplier: Some(1),
954 unit: TimeUnit::FemtoSeconds
955 },
956 &TimeUnit::FemtoSeconds,
957 &TimeFormat::default()
958 ),
959 "103 fs"
960 );
961 assert_eq!(
962 time_string(
963 &BigInt::from(2200),
964 &TimeScale {
965 multiplier: Some(1),
966 unit: TimeUnit::MicroSeconds
967 },
968 &TimeUnit::MicroSeconds,
969 &TimeFormat::default()
970 ),
971 "2200 μs"
972 );
973 assert_eq!(
974 time_string(
975 &BigInt::from(2200),
976 &TimeScale {
977 multiplier: Some(1),
978 unit: TimeUnit::MicroSeconds
979 },
980 &TimeUnit::MilliSeconds,
981 &TimeFormat::default()
982 ),
983 "2.2 ms"
984 );
985 assert_eq!(
986 time_string(
987 &BigInt::from(2200),
988 &TimeScale {
989 multiplier: Some(1),
990 unit: TimeUnit::MicroSeconds
991 },
992 &TimeUnit::NanoSeconds,
993 &TimeFormat::default()
994 ),
995 "2200000 ns"
996 );
997 assert_eq!(
998 time_string(
999 &BigInt::from(2200),
1000 &TimeScale {
1001 multiplier: Some(1),
1002 unit: TimeUnit::NanoSeconds
1003 },
1004 &TimeUnit::PicoSeconds,
1005 &TimeFormat {
1006 format: TimeStringFormatting::No,
1007 show_space: false,
1008 show_unit: true
1009 }
1010 ),
1011 "2200000ps"
1012 );
1013 assert_eq!(
1014 time_string(
1015 &BigInt::from(2200),
1016 &TimeScale {
1017 multiplier: Some(10),
1018 unit: TimeUnit::MicroSeconds
1019 },
1020 &TimeUnit::MicroSeconds,
1021 &TimeFormat {
1022 format: TimeStringFormatting::No,
1023 show_space: false,
1024 show_unit: false
1025 }
1026 ),
1027 "22000"
1028 );
1029 }
1030 #[test]
1031 fn print_time_si() {
1032 assert_eq!(
1033 time_string(
1034 &BigInt::from(123456789010i128),
1035 &TimeScale {
1036 multiplier: Some(1),
1037 unit: TimeUnit::MicroSeconds
1038 },
1039 &TimeUnit::Seconds,
1040 &TimeFormat {
1041 format: TimeStringFormatting::SI,
1042 show_space: true,
1043 show_unit: true
1044 }
1045 ),
1046 "123\u{2009}456.789\u{2009}01 s"
1047 );
1048 assert_eq!(
1049 time_string(
1050 &BigInt::from(1456789100i128),
1051 &TimeScale {
1052 multiplier: Some(1),
1053 unit: TimeUnit::MicroSeconds
1054 },
1055 &TimeUnit::Seconds,
1056 &TimeFormat {
1057 format: TimeStringFormatting::SI,
1058 show_space: true,
1059 show_unit: true
1060 }
1061 ),
1062 "1456.7891 s"
1063 );
1064 assert_eq!(
1065 time_string(
1066 &BigInt::from(2200),
1067 &TimeScale {
1068 multiplier: Some(1),
1069 unit: TimeUnit::MicroSeconds
1070 },
1071 &TimeUnit::MicroSeconds,
1072 &TimeFormat {
1073 format: TimeStringFormatting::SI,
1074 show_space: true,
1075 show_unit: true
1076 }
1077 ),
1078 "2200 μs"
1079 );
1080 assert_eq!(
1081 time_string(
1082 &BigInt::from(22200),
1083 &TimeScale {
1084 multiplier: Some(1),
1085 unit: TimeUnit::MicroSeconds
1086 },
1087 &TimeUnit::MicroSeconds,
1088 &TimeFormat {
1089 format: TimeStringFormatting::SI,
1090 show_space: true,
1091 show_unit: true
1092 }
1093 ),
1094 "22\u{2009}200 μs"
1095 );
1096 }
1097 #[test]
1098 fn print_time_auto() {
1099 assert_eq!(
1100 time_string(
1101 &BigInt::from(2200),
1102 &TimeScale {
1103 multiplier: Some(1),
1104 unit: TimeUnit::MicroSeconds
1105 },
1106 &TimeUnit::Auto,
1107 &TimeFormat {
1108 format: TimeStringFormatting::SI,
1109 show_space: true,
1110 show_unit: true
1111 }
1112 ),
1113 "2200 μs"
1114 );
1115 assert_eq!(
1116 time_string(
1117 &BigInt::from(22000),
1118 &TimeScale {
1119 multiplier: Some(1),
1120 unit: TimeUnit::MicroSeconds
1121 },
1122 &TimeUnit::Auto,
1123 &TimeFormat {
1124 format: TimeStringFormatting::SI,
1125 show_space: true,
1126 show_unit: true
1127 }
1128 ),
1129 "22 ms"
1130 );
1131 assert_eq!(
1132 time_string(
1133 &BigInt::from(1500000000),
1134 &TimeScale {
1135 multiplier: Some(1),
1136 unit: TimeUnit::PicoSeconds
1137 },
1138 &TimeUnit::Auto,
1139 &TimeFormat {
1140 format: TimeStringFormatting::SI,
1141 show_space: true,
1142 show_unit: true
1143 }
1144 ),
1145 "1500 μs"
1146 );
1147 assert_eq!(
1148 time_string(
1149 &BigInt::from(22000),
1150 &TimeScale {
1151 multiplier: Some(10),
1152 unit: TimeUnit::MicroSeconds
1153 },
1154 &TimeUnit::Auto,
1155 &TimeFormat {
1156 format: TimeStringFormatting::SI,
1157 show_space: true,
1158 show_unit: true
1159 }
1160 ),
1161 "220 ms"
1162 );
1163 assert_eq!(
1164 time_string(
1165 &BigInt::from(220000),
1166 &TimeScale {
1167 multiplier: Some(100),
1168 unit: TimeUnit::MicroSeconds
1169 },
1170 &TimeUnit::Auto,
1171 &TimeFormat {
1172 format: TimeStringFormatting::SI,
1173 show_space: true,
1174 show_unit: true
1175 }
1176 ),
1177 "22 s"
1178 );
1179 assert_eq!(
1180 time_string(
1181 &BigInt::from(22000),
1182 &TimeScale {
1183 multiplier: Some(10),
1184 unit: TimeUnit::Seconds
1185 },
1186 &TimeUnit::Auto,
1187 &TimeFormat {
1188 format: TimeStringFormatting::No,
1189 show_space: true,
1190 show_unit: true
1191 }
1192 ),
1193 "220000 s"
1194 );
1195 }
1196 #[test]
1197 fn print_time_none() {
1198 assert_eq!(
1199 time_string(
1200 &BigInt::from(2200),
1201 &TimeScale {
1202 multiplier: Some(1),
1203 unit: TimeUnit::MicroSeconds
1204 },
1205 &TimeUnit::None,
1206 &TimeFormat {
1207 format: TimeStringFormatting::No,
1208 show_space: true,
1209 show_unit: true
1210 }
1211 ),
1212 "2200"
1213 );
1214 assert_eq!(
1215 time_string(
1216 &BigInt::from(220),
1217 &TimeScale {
1218 multiplier: Some(10),
1219 unit: TimeUnit::MicroSeconds
1220 },
1221 &TimeUnit::None,
1222 &TimeFormat {
1223 format: TimeStringFormatting::No,
1224 show_space: true,
1225 show_unit: true
1226 }
1227 ),
1228 "220"
1229 );
1230 }
1231
1232 #[test]
1233 fn test_strip_trailing_zeros_and_period() {
1234 use crate::time::strip_trailing_zeros_and_period;
1235
1236 assert_eq!(strip_trailing_zeros_and_period("123.000".into()), "123");
1237 assert_eq!(strip_trailing_zeros_and_period("123.450".into()), "123.45");
1238 assert_eq!(strip_trailing_zeros_and_period("123.456".into()), "123.456");
1239 assert_eq!(strip_trailing_zeros_and_period("123.".into()), "123");
1240 assert_eq!(strip_trailing_zeros_and_period("123".into()), "123");
1241 assert_eq!(strip_trailing_zeros_and_period("0.000".into()), "0");
1242 assert_eq!(strip_trailing_zeros_and_period("0.100".into()), "0.1");
1243 assert_eq!(strip_trailing_zeros_and_period(String::new()), "");
1244 }
1245
1246 #[test]
1247 fn test_format_si() {
1248 use crate::time::format_si;
1249
1250 assert_eq!(format_si("1234.56"), "1234.56");
1252 assert_eq!(format_si("123.4"), "123.4");
1253
1254 assert_eq!(format_si("12345.67"), "12\u{2009}345.67");
1256 assert_eq!(format_si("1234567.89"), "1\u{2009}234\u{2009}567.89");
1257 assert_eq!(format_si("12345"), "12\u{2009}345");
1259 assert_eq!(format_si("123"), "123");
1260
1261 assert_eq!(format_si("0.123"), "0.123");
1263 assert_eq!(format_si(""), "");
1264
1265 assert_eq!(format_si("123.4567890"), "123.456\u{2009}789\u{2009}0");
1267 }
1268
1269 #[test]
1270 fn test_time_unit_exponent() {
1271 assert_eq!(TimeUnit::Seconds.exponent(), 0);
1273 assert_eq!(TimeUnit::MilliSeconds.exponent(), -3);
1274 assert_eq!(TimeUnit::MicroSeconds.exponent(), -6);
1275 assert_eq!(TimeUnit::NanoSeconds.exponent(), -9);
1276 assert_eq!(TimeUnit::PicoSeconds.exponent(), -12);
1277 assert_eq!(TimeUnit::FemtoSeconds.exponent(), -15);
1278 assert_eq!(TimeUnit::AttoSeconds.exponent(), -18);
1279 assert_eq!(TimeUnit::ZeptoSeconds.exponent(), -21);
1280
1281 for unit in [
1283 TimeUnit::Seconds,
1284 TimeUnit::MilliSeconds,
1285 TimeUnit::MicroSeconds,
1286 TimeUnit::NanoSeconds,
1287 TimeUnit::PicoSeconds,
1288 TimeUnit::FemtoSeconds,
1289 TimeUnit::AttoSeconds,
1290 TimeUnit::ZeptoSeconds,
1291 ] {
1292 assert_eq!(TimeUnit::from_exponent(unit.exponent()), Some(unit));
1293 }
1294
1295 assert_eq!(TimeUnit::from_exponent(-5), None);
1297 assert_eq!(TimeUnit::from_exponent(1), None);
1298 }
1299
1300 #[test]
1301 fn test_time_string_zero() {
1302 assert_eq!(
1304 time_string(
1305 &BigInt::from(0),
1306 &TimeScale {
1307 multiplier: Some(1),
1308 unit: TimeUnit::MicroSeconds
1309 },
1310 &TimeUnit::MicroSeconds,
1311 &TimeFormat::default()
1312 ),
1313 "0 μs"
1314 );
1315
1316 assert_eq!(
1317 time_string(
1318 &BigInt::from(0),
1319 &TimeScale {
1320 multiplier: Some(1),
1321 unit: TimeUnit::Seconds
1322 },
1323 &TimeUnit::Auto,
1324 &TimeFormat::default()
1325 ),
1326 "0 s"
1327 );
1328 }
1329
1330 #[test]
1331 fn test_time_string_large_numbers() {
1332 assert_eq!(
1334 time_string(
1335 &BigInt::from(999_999_999_999i64),
1336 &TimeScale {
1337 multiplier: Some(1),
1338 unit: TimeUnit::NanoSeconds
1339 },
1340 &TimeUnit::Seconds,
1341 &TimeFormat {
1342 format: TimeStringFormatting::SI,
1343 show_space: true,
1344 show_unit: true
1345 }
1346 ),
1347 "999.999\u{2009}999\u{2009}999 s"
1348 );
1349 }
1350
1351 #[test]
1352 fn test_time_string_no_multiplier() {
1353 assert_eq!(
1355 time_string(
1356 &BigInt::from(1234),
1357 &TimeScale {
1358 multiplier: None,
1359 unit: TimeUnit::NanoSeconds
1360 },
1361 &TimeUnit::NanoSeconds,
1362 &TimeFormat::default()
1363 ),
1364 "1234 ns"
1365 );
1366 }
1367
1368 #[test]
1369 fn test_time_format_variations() {
1370 let value = BigInt::from(123456);
1371 let scale = TimeScale {
1372 multiplier: Some(1),
1373 unit: TimeUnit::NanoSeconds,
1374 };
1375
1376 assert_eq!(
1378 time_string(
1379 &value,
1380 &scale,
1381 &TimeUnit::NanoSeconds,
1382 &TimeFormat {
1383 format: TimeStringFormatting::No,
1384 show_space: true,
1385 show_unit: true
1386 }
1387 ),
1388 "123456 ns"
1389 );
1390
1391 assert_eq!(
1392 time_string(
1393 &value,
1394 &scale,
1395 &TimeUnit::NanoSeconds,
1396 &TimeFormat {
1397 format: TimeStringFormatting::No,
1398 show_space: false,
1399 show_unit: true
1400 }
1401 ),
1402 "123456ns"
1403 );
1404
1405 assert_eq!(
1406 time_string(
1407 &value,
1408 &scale,
1409 &TimeUnit::NanoSeconds,
1410 &TimeFormat {
1411 format: TimeStringFormatting::No,
1412 show_space: true,
1413 show_unit: false
1414 }
1415 ),
1416 "123456"
1417 );
1418
1419 assert_eq!(
1420 time_string(
1421 &value,
1422 &scale,
1423 &TimeUnit::NanoSeconds,
1424 &TimeFormat {
1425 format: TimeStringFormatting::SI,
1426 show_space: true,
1427 show_unit: true
1428 }
1429 ),
1430 "123\u{2009}456 ns"
1431 );
1432 }
1433
1434 #[test]
1435 fn test_find_auto_scale_seconds_passthrough() {
1436 use crate::time::find_auto_scale;
1437
1438 let ts = TimeScale {
1439 unit: TimeUnit::Seconds,
1440 multiplier: Some(1),
1441 };
1442 assert_eq!(find_auto_scale(&BigInt::from(1), &ts), TimeUnit::Seconds);
1443 assert_eq!(
1444 find_auto_scale(&BigInt::from(1_234_567), &ts),
1445 TimeUnit::Seconds
1446 );
1447 }
1448
1449 #[test]
1450 fn test_find_auto_scale_nanoseconds() {
1451 use crate::time::find_auto_scale;
1452
1453 let ts = TimeScale {
1454 unit: TimeUnit::NanoSeconds,
1455 multiplier: Some(1),
1456 };
1457
1458 assert_eq!(
1460 find_auto_scale(&BigInt::from(1_000_000_000i64), &ts),
1461 TimeUnit::Seconds
1462 );
1463 assert_eq!(
1465 find_auto_scale(&BigInt::from(1_000_000), &ts),
1466 TimeUnit::MilliSeconds
1467 );
1468 assert_eq!(
1470 find_auto_scale(&BigInt::from(1_000), &ts),
1471 TimeUnit::MicroSeconds
1472 );
1473 assert_eq!(
1475 find_auto_scale(&BigInt::from(1234), &ts),
1476 TimeUnit::NanoSeconds
1477 );
1478 }
1479
1480 #[test]
1481 fn test_find_auto_scale_microseconds_with_multiplier() {
1482 use crate::time::find_auto_scale;
1483
1484 let ts_none = TimeScale {
1486 unit: TimeUnit::MicroSeconds,
1487 multiplier: None,
1488 };
1489 assert_eq!(
1490 find_auto_scale(&BigInt::from(1_000_000), &ts_none),
1491 TimeUnit::Seconds
1492 );
1493 assert_eq!(
1494 find_auto_scale(&BigInt::from(1_000), &ts_none),
1495 TimeUnit::MilliSeconds
1496 );
1497 assert_eq!(
1498 find_auto_scale(&BigInt::from(123), &ts_none),
1499 TimeUnit::MicroSeconds
1500 );
1501
1502 let ts_mul10 = TimeScale {
1504 unit: TimeUnit::MicroSeconds,
1505 multiplier: Some(10),
1506 };
1507 assert_eq!(
1508 find_auto_scale(&BigInt::from(100_000), &ts_mul10),
1509 TimeUnit::Seconds
1510 );
1511 assert_eq!(
1512 find_auto_scale(&BigInt::from(100), &ts_mul10),
1513 TimeUnit::MilliSeconds
1514 );
1515 assert_eq!(
1516 find_auto_scale(&BigInt::from(123), &ts_mul10),
1517 TimeUnit::MicroSeconds
1518 );
1519 }
1520
1521 #[test]
1522 fn test_find_auto_scale_femtoseconds() {
1523 use crate::time::find_auto_scale;
1524
1525 let ts = TimeScale {
1526 unit: TimeUnit::FemtoSeconds,
1527 multiplier: Some(1),
1528 };
1529 assert_eq!(
1531 find_auto_scale(&BigInt::from(10_i128.pow(15)), &ts),
1532 TimeUnit::Seconds
1533 );
1534 assert_eq!(
1536 find_auto_scale(&BigInt::from(10_i128.pow(12)), &ts),
1537 TimeUnit::MilliSeconds
1538 );
1539 assert_eq!(
1541 find_auto_scale(&BigInt::from(10_i128.pow(9)), &ts),
1542 TimeUnit::MicroSeconds
1543 );
1544 assert_eq!(
1546 find_auto_scale(&BigInt::from(10_i128.pow(6)), &ts),
1547 TimeUnit::NanoSeconds
1548 );
1549 assert_eq!(
1551 find_auto_scale(&BigInt::from(10_i128.pow(3)), &ts),
1552 TimeUnit::PicoSeconds
1553 );
1554 assert_eq!(
1556 find_auto_scale(&BigInt::from(1), &ts),
1557 TimeUnit::FemtoSeconds
1558 );
1559 }
1560
1561 #[test]
1562 fn test_locale_cache_en_us() {
1563 use crate::time::{create_cache, format_locale};
1564 use pure_rust_locales::Locale;
1565
1566 let locale = Locale::en_US;
1567 let cache = create_cache(locale);
1568
1569 let result = format_locale("1234567.89", &cache);
1571 assert_eq!(result, "1,234,567.89");
1572 }
1573
1574 #[test]
1575 fn test_locale_cache_de_de() {
1576 use crate::time::{create_cache, format_locale};
1577 use pure_rust_locales::Locale;
1578
1579 let locale = Locale::de_DE;
1580 let cache = create_cache(locale);
1581
1582 let result = format_locale("1234567.89", &cache);
1583 assert_eq!(result, "1.234.567,89");
1584 }
1585
1586 #[test]
1587 fn test_locale_cache_fr_fr() {
1588 use crate::time::{create_cache, format_locale};
1589 use pure_rust_locales::Locale;
1590
1591 let locale = Locale::fr_FR;
1592 let cache = create_cache(locale);
1593
1594 let result = format_locale("1234567.89", &cache);
1596 assert_eq!(result, "1\u{2009}234\u{2009}567,89");
1598 }
1599
1600 #[test]
1601 fn test_locale_cache_small_numbers() {
1602 use crate::time::{create_cache, format_locale};
1603 use pure_rust_locales::Locale;
1604
1605 let locale = Locale::en_US;
1606 let cache = create_cache(locale);
1607
1608 assert_eq!(format_locale("123", &cache), "123");
1610 assert_eq!(format_locale("12.34", &cache), "12.34");
1611 assert_eq!(format_locale("0", &cache), "0");
1612 }
1613
1614 #[test]
1615 fn test_locale_cache_consistency_across_locales() {
1616 use crate::time::create_cache;
1617 use pure_rust_locales::Locale;
1618
1619 let cache1 = create_cache(Locale::en_US);
1621 let cache2 = create_cache(Locale::en_US);
1622
1623 assert_eq!(cache1.thousands_sep, cache2.thousands_sep);
1624 assert_eq!(cache1.decimal_point, cache2.decimal_point);
1625 assert_eq!(cache1.grouping, cache2.grouping);
1626 }
1627
1628 #[test]
1629 fn test_create_cache_from_various_locales() {
1630 use crate::time::{create_cache, format_locale};
1631 use pure_rust_locales::Locale;
1632
1633 let locales = vec![
1635 Locale::en_US,
1636 Locale::de_DE,
1637 Locale::fr_FR,
1638 Locale::es_ES,
1639 Locale::it_IT,
1640 Locale::pt_BR,
1641 Locale::pt_PT,
1642 Locale::ja_JP,
1643 Locale::zh_CN,
1644 Locale::zh_TW,
1645 Locale::ru_RU,
1646 Locale::ko_KR,
1647 Locale::pl_PL,
1648 Locale::tr_TR,
1649 Locale::nl_NL,
1650 Locale::sv_SE,
1651 Locale::da_DK,
1652 Locale::fi_FI,
1653 Locale::el_GR,
1654 Locale::hu_HU,
1655 Locale::cs_CZ,
1656 Locale::ro_RO,
1657 Locale::th_TH,
1658 Locale::vi_VN,
1659 Locale::ar_SA,
1660 Locale::he_IL,
1661 Locale::id_ID,
1662 Locale::uk_UA,
1663 Locale::en_GB,
1664 Locale::en_AU,
1665 Locale::en_CA,
1666 Locale::en_NZ,
1667 Locale::en_IN,
1668 Locale::fr_CA,
1669 Locale::de_AT,
1670 Locale::de_CH,
1671 Locale::fr_CH,
1672 Locale::it_CH,
1673 Locale::es_MX,
1674 Locale::es_AR,
1675 ];
1676
1677 for locale in locales {
1678 let cache = create_cache(locale);
1679 assert!(
1681 !format_locale("1234567.89", &cache).is_empty(),
1682 "Failed for {locale:?}"
1683 );
1684 }
1685 }
1686}
1687
1688#[cfg(test)]
1689mod get_ticks_tests {
1690 use super::*;
1691 use itertools::Itertools;
1692 use num::BigInt;
1693
1694 #[test]
1697 fn get_ticks_basic() {
1698 let vp = crate::viewport::Viewport::default();
1699 let timescale = TimeScale {
1700 unit: TimeUnit::MicroSeconds,
1701 multiplier: Some(1),
1702 };
1703 let frame_width = 800.0_f32;
1704 let text_size = 12.0_f32;
1705 let wanted = TimeUnit::MicroSeconds;
1706 let time_format = TimeFormat::default();
1707 let config = crate::config::SurferConfig::default();
1708 let num_timestamps = BigInt::from(1_000_000i64);
1709
1710 let ticks = get_ticks_internal(
1711 &vp,
1712 ×cale,
1713 frame_width,
1714 text_size,
1715 &wanted,
1716 &time_format,
1717 config.theme.ticks.density,
1718 &num_timestamps,
1719 );
1720
1721 assert!(!ticks.is_empty(), "expected at least one tick");
1722
1723 let mut last_x = -1.0_f32;
1725 let mut labels: Vec<String> = Vec::with_capacity(ticks.len());
1726 for (label, x) in &ticks {
1727 assert!(
1728 *x >= last_x,
1729 "tick x not monotonic: {x} < {last_x} for label {label}"
1730 );
1731 last_x = *x;
1732 assert!(*x >= 0.0, "tick x < 0: {x}");
1733 assert!(
1734 *x <= frame_width,
1735 "tick x > frame_width: {x} > {frame_width}"
1736 );
1737 labels.push(label.clone());
1738 }
1739 let unique_labels = labels.iter().unique().count();
1741 assert_eq!(labels.len(), unique_labels, "duplicate tick labels found");
1742 }
1743
1744 #[test]
1747 fn get_ticks_respects_frame_width_and_density() {
1748 let mut vp = crate::viewport::Viewport::default();
1749 vp.curr_left = crate::viewport::Relative(0.0);
1751 vp.curr_right = crate::viewport::Relative(0.1);
1752
1753 let timescale = TimeScale {
1754 unit: TimeUnit::NanoSeconds,
1755 multiplier: Some(1),
1756 };
1757 let frame_width = 200.0_f32;
1758 let text_size = 10.0_f32;
1759 let wanted = TimeUnit::Auto;
1760 let time_format = TimeFormat {
1761 format: TimeStringFormatting::SI,
1762 show_space: true,
1763 show_unit: true,
1764 };
1765
1766 let mut config = crate::config::SurferConfig::default();
1767 config.theme.ticks.density = 1.0;
1769
1770 let num_timestamps = BigInt::from(1_000_000i64);
1771
1772 let ticks = get_ticks_internal(
1773 &vp,
1774 ×cale,
1775 frame_width,
1776 text_size,
1777 &wanted,
1778 &time_format,
1779 config.theme.ticks.density,
1780 &num_timestamps,
1781 );
1782
1783 assert!(!ticks.is_empty(), "expected ticks even for narrow view");
1784 assert!(ticks.len() < 200, "too many ticks: {}", ticks.len());
1786
1787 let mut last_x = -1.0_f32;
1789 let mut labels: Vec<String> = Vec::with_capacity(ticks.len());
1790 for (label, x) in &ticks {
1791 assert!(
1792 *x >= last_x,
1793 "tick x not monotonic: {x} < {last_x} for label {label}"
1794 );
1795 last_x = *x;
1796 assert!(*x >= 0.0, "tick x < 0: {x}");
1797 assert!(
1798 *x <= frame_width,
1799 "tick x > frame_width: {x} > {frame_width}"
1800 );
1801 labels.push(label.clone());
1802 }
1803 let unique_labels = labels.iter().unique().count();
1804 assert_eq!(labels.len(), unique_labels, "duplicate tick labels found");
1805 }
1806}
1807
1808#[cfg(test)]
1809mod time_input_tests {
1810 use super::*;
1811
1812 #[test]
1813 fn test_parse_time_input_simple() {
1814 let (num, unit) = parse_time_input("100");
1815 assert_eq!(num, "100");
1816 assert_eq!(unit, None);
1817 }
1818
1819 #[test]
1820 fn test_parse_time_input_unit_only_no_panic() {
1821 let (num, unit) = parse_time_input("ns");
1823 assert_eq!(num, "ns");
1824 assert_eq!(unit, None);
1825 }
1826
1827 #[test]
1828 fn test_parse_time_input_with_unit_no_space() {
1829 let (num, unit) = parse_time_input("100ns");
1830 assert_eq!(num, "100");
1831 assert_eq!(unit, Some(TimeUnit::NanoSeconds));
1832
1833 let (num, unit) = parse_time_input("50ps");
1834 assert_eq!(num, "50");
1835 assert_eq!(unit, Some(TimeUnit::PicoSeconds));
1836
1837 let (num, unit) = parse_time_input("1.5ms");
1838 assert_eq!(num, "1.5");
1839 assert_eq!(unit, Some(TimeUnit::MilliSeconds));
1840 }
1841
1842 #[test]
1843 fn test_parse_time_input_with_unit_space() {
1844 let (num, unit) = parse_time_input("100 ns");
1845 assert_eq!(num, "100");
1846 assert_eq!(unit, Some(TimeUnit::NanoSeconds));
1847
1848 let (num, unit) = parse_time_input("1.5 ms");
1849 assert_eq!(num, "1.5");
1850 assert_eq!(unit, Some(TimeUnit::MilliSeconds));
1851
1852 let (num, unit) = parse_time_input("100\tms");
1853 assert_eq!(num, "100");
1854 assert_eq!(unit, Some(TimeUnit::MilliSeconds));
1855
1856 let (num, unit) = parse_time_input("100 ns");
1857 assert_eq!(num, "100");
1858 assert_eq!(unit, Some(TimeUnit::NanoSeconds));
1859 }
1860
1861 #[test]
1862 fn test_parse_time_input_microseconds_unicode() {
1863 let (num, unit) = parse_time_input("100μs");
1864 assert_eq!(num, "100");
1865 assert_eq!(unit, Some(TimeUnit::MicroSeconds));
1866
1867 let (num, unit) = parse_time_input("50 μs");
1868 assert_eq!(num, "50");
1869 assert_eq!(unit, Some(TimeUnit::MicroSeconds));
1870 }
1871
1872 #[test]
1873 fn test_parse_time_input_microseconds_ascii() {
1874 let (num, unit) = parse_time_input("100us");
1876 assert_eq!(num, "100");
1877 assert_eq!(unit, Some(TimeUnit::MicroSeconds));
1878 }
1879
1880 #[test]
1881 fn test_parse_time_input_seconds() {
1882 let (num, unit) = parse_time_input("10s");
1883 assert_eq!(num, "10");
1884 assert_eq!(unit, Some(TimeUnit::Seconds));
1885
1886 let (num, unit) = parse_time_input("0.5s");
1887 assert_eq!(num, "0.5");
1888 assert_eq!(unit, Some(TimeUnit::Seconds));
1889 }
1890
1891 #[test]
1892 fn test_parse_time_input_femtoseconds() {
1893 let (num, unit) = parse_time_input("1000000fs");
1894 assert_eq!(num, "1000000");
1895 assert_eq!(unit, Some(TimeUnit::FemtoSeconds));
1896 }
1897
1898 #[test]
1899 fn test_parse_time_input_with_whitespace() {
1900 let (num, unit) = parse_time_input(" 100ns ");
1901 assert_eq!(num, "100");
1902 assert_eq!(unit, Some(TimeUnit::NanoSeconds));
1903 }
1904
1905 #[test]
1906 fn test_split_numeric_parts() {
1907 assert_eq!(
1908 split_numeric_parts("100").ok(),
1909 Some(("100".to_string(), "".to_string()))
1910 );
1911 assert_eq!(
1912 split_numeric_parts("100.").ok(),
1913 Some(("100".to_string(), "".to_string()))
1914 );
1915 assert_eq!(
1916 split_numeric_parts(".5").ok(),
1917 Some(("0".to_string(), "5".to_string()))
1918 );
1919 assert_eq!(
1920 split_numeric_parts("1.5").ok(),
1921 Some(("1".to_string(), "5".to_string()))
1922 );
1923 }
1924
1925 #[test]
1926 fn test_split_numeric_parts_invalid() {
1927 assert!(split_numeric_parts("").is_err());
1928 assert!(split_numeric_parts("abc").is_err());
1929 assert!(split_numeric_parts("12.34.56").is_err());
1930 assert!(split_numeric_parts("-1").is_err());
1931 }
1932
1933 #[test]
1934 fn test_normalize_numeric_with_unit_integer() {
1935 let (val, unit) = normalize_numeric_with_unit("100", TimeUnit::NanoSeconds).unwrap();
1936 assert_eq!(val, BigInt::from(100));
1937 assert_eq!(unit, TimeUnit::NanoSeconds);
1938 }
1939
1940 #[test]
1941 fn test_normalize_numeric_with_unit_decimal_single_step() {
1942 let (val, unit) = normalize_numeric_with_unit("1.5", TimeUnit::NanoSeconds).unwrap();
1943 assert_eq!(val, BigInt::from(1500));
1944 assert_eq!(unit, TimeUnit::PicoSeconds);
1945 }
1946
1947 #[test]
1948 fn test_normalize_numeric_with_unit_decimal_multi_step() {
1949 let (val, unit) = normalize_numeric_with_unit("1.2345", TimeUnit::NanoSeconds).unwrap();
1950 assert_eq!(val, BigInt::from(1_234_500));
1951 assert_eq!(unit, TimeUnit::FemtoSeconds);
1952 }
1953
1954 #[test]
1955 fn test_time_input_state_default() {
1956 let state = TimeInputState::default();
1957 assert_eq!(state.input_text, "");
1958 assert_eq!(state.parsed_value, None);
1959 assert_eq!(state.input_unit, None);
1960 assert_eq!(state.normalized_unit, None);
1961 assert_eq!(state.selected_unit, TimeUnit::NanoSeconds);
1962 assert_eq!(state.error, None);
1963 }
1964
1965 #[test]
1966 fn test_time_input_state_update_valid() {
1967 let mut state = TimeInputState::new();
1968 state.update_input("100ns".to_string());
1969
1970 assert_eq!(state.input_text, "100ns");
1971 assert_eq!(state.parsed_value, Some(BigInt::from(100)));
1972 assert_eq!(state.input_unit, Some(TimeUnit::NanoSeconds));
1973 assert_eq!(state.normalized_unit, Some(TimeUnit::NanoSeconds));
1974 assert_eq!(state.error, None);
1975 }
1976
1977 #[test]
1978 fn test_time_input_state_update_invalid() {
1979 let mut state = TimeInputState::new();
1980 state.update_input("abc".to_string());
1981
1982 assert_eq!(state.parsed_value, None);
1983 assert!(state.error.is_some());
1984 }
1985
1986 #[test]
1987 fn test_time_input_state_update_decimal_unit_normalization() {
1988 let mut state = TimeInputState::new();
1989 state.update_input("1.5ns".to_string());
1990
1991 assert_eq!(state.parsed_value, Some(BigInt::from(1500)));
1992 assert_eq!(state.input_unit, Some(TimeUnit::NanoSeconds));
1993 assert_eq!(state.normalized_unit, Some(TimeUnit::PicoSeconds));
1994 assert_eq!(state.effective_unit(), TimeUnit::PicoSeconds);
1995 assert_eq!(state.error, None);
1996 }
1997
1998 #[test]
1999 fn test_time_input_state_to_timescale_ticks_invalid() {
2000 let state = TimeInputState::new();
2001 let timescale = TimeScale {
2002 unit: TimeUnit::NanoSeconds,
2003 multiplier: None,
2004 };
2005
2006 assert_eq!(state.to_timescale_ticks(×cale), None);
2007 }
2008
2009 #[test]
2010 fn test_time_input_comprehensive_example() {
2011 let mut state = TimeInputState::new();
2013 state.update_input("2.5 ms".to_string());
2014
2015 assert_eq!(state.parsed_value, Some(BigInt::from(2500)));
2017 assert_eq!(state.input_unit, Some(TimeUnit::MilliSeconds));
2018 assert_eq!(state.effective_unit(), TimeUnit::MicroSeconds);
2019 assert_eq!(state.error, None);
2020
2021 let timescale = TimeScale {
2023 unit: TimeUnit::MicroSeconds,
2024 multiplier: None,
2025 };
2026 assert_eq!(
2027 state.to_timescale_ticks(×cale),
2028 Some(BigInt::from(2500))
2029 );
2030 }
2031
2032 #[test]
2033 fn test_parse_time_longest_match_first() {
2034 let (num, unit) = parse_time_input("100ms");
2036 assert_eq!(num, "100");
2037 assert_eq!(unit, Some(TimeUnit::MilliSeconds));
2038
2039 let (_, unit) = parse_time_input("100s");
2041 assert_ne!(unit, Some(TimeUnit::MilliSeconds));
2042 }
2043
2044 #[test]
2045 fn test_parse_time_no_false_positives() {
2046 let (num, unit) = parse_time_input("mass");
2048 assert_eq!(num, "mass");
2049 assert_eq!(unit, None);
2050
2051 let (num, unit) = parse_time_input("uses");
2052 assert_eq!(num, "uses");
2053 assert_eq!(unit, None);
2054 }
2055
2056 #[test]
2057 fn test_time_input_state_clear() {
2058 let mut state = TimeInputState::new();
2059 state.update_input("100ns".to_string());
2060 assert!(state.parsed_value.is_some());
2061
2062 state.update_input(String::new());
2064 assert!(state.error.is_some());
2065 }
2066}