1use super::timescale_marks::TickMarkType;
11use crate::model::TimezoneId;
12use chrono::{DateTime, Utc};
13use std::sync::Arc;
14
15#[cfg(test)]
16use chrono::Datelike;
17
18#[derive(Debug, Clone, Copy)]
20pub struct TickMarkContext<'a> {
21 pub time: DateTime<Utc>,
24 pub mark_type: TickMarkType,
26 pub locale: Option<&'a str>,
29}
30
31pub trait TimeFormatter: Send + Sync {
34 fn format(&self, time: DateTime<Utc>, mark_type: TickMarkType) -> String;
43
44 fn format_tick(&self, context: TickMarkContext<'_>) -> Option<String> {
52 Some(self.format(context.time, context.mark_type))
53 }
54
55 fn wall_time(&self, time: DateTime<Utc>) -> DateTime<Utc> {
58 time
59 }
60
61 fn utc_time(&self, wall_time: DateTime<Utc>) -> Option<DateTime<Utc>> {
64 Some(wall_time)
65 }
66
67 fn time_visible(&self) -> bool {
69 true
70 }
71
72 fn format_crosshair(&self, time: DateTime<Utc>, bar_seconds: i64) -> String {
74 let mut label = self.format(time, TickMarkType::DayOfMonth);
75 let year = self.format(time, TickMarkType::Year);
76 if year != self.format(Utc::now(), TickMarkType::Year) {
77 label = format!("{label}, {year}");
78 }
79 match bar_seconds {
80 86_400.. => label,
81 60.. => format!("{label} {}", self.format(time, TickMarkType::Time)),
82 1.. => format!(
83 "{label} {}",
84 self.format(time, TickMarkType::TimeWithSeconds)
85 ),
86 _ => format!(
87 "{label} {}",
88 self.format(time, TickMarkType::TimeWithMilliseconds)
89 ),
90 }
91 }
92
93 fn clone_box(&self) -> Box<dyn TimeFormatter>;
95}
96
97#[derive(Debug, Clone)]
99pub struct DefaultTimeFormatter {
100 pub use_24_hour: bool,
102
103 pub show_seconds: bool,
105
106 pub date_format: Option<String>,
110}
111
112impl Default for DefaultTimeFormatter {
113 fn default() -> Self {
114 Self {
115 use_24_hour: true,
116 show_seconds: true,
117 date_format: None,
118 }
119 }
120}
121
122impl DefaultTimeFormatter {
123 fn style(&self) -> Style<'_> {
124 Style {
125 use_24_hour: self.use_24_hour,
126 show_seconds: self.show_seconds,
127 date_format: self.date_format.as_deref(),
128 #[cfg(feature = "locales")]
129 locale: None,
130 }
131 }
132
133 fn into_box(self) -> Box<dyn TimeFormatter> {
134 if self.date_format.is_none() {
135 Box::new(ClockTimeFormatter {
136 use_24_hour: self.use_24_hour,
137 show_seconds: self.show_seconds,
138 })
139 } else {
140 Box::new(self)
141 }
142 }
143}
144
145impl TimeFormatter for DefaultTimeFormatter {
146 fn format(&self, time: DateTime<Utc>, mark_type: TickMarkType) -> String {
147 self.style().format(time, mark_type)
148 }
149
150 fn format_crosshair(&self, time: DateTime<Utc>, bar_seconds: i64) -> String {
151 self.style().format_crosshair(time, bar_seconds)
152 }
153
154 fn clone_box(&self) -> Box<dyn TimeFormatter> {
155 self.clone().into_box()
156 }
157}
158
159#[derive(Clone)]
161struct ClockTimeFormatter {
162 use_24_hour: bool,
163 show_seconds: bool,
164}
165
166impl TimeFormatter for ClockTimeFormatter {
167 fn format(&self, time: DateTime<Utc>, mark_type: TickMarkType) -> String {
168 DefaultTimeFormatter {
169 use_24_hour: self.use_24_hour,
170 show_seconds: self.show_seconds,
171 date_format: None,
172 }
173 .format(time, mark_type)
174 }
175
176 fn format_crosshair(&self, time: DateTime<Utc>, bar_seconds: i64) -> String {
177 DefaultTimeFormatter {
178 use_24_hour: self.use_24_hour,
179 show_seconds: self.show_seconds,
180 date_format: None,
181 }
182 .format_crosshair(time, bar_seconds)
183 }
184
185 fn clone_box(&self) -> Box<dyn TimeFormatter> {
186 Box::new(self.clone())
187 }
188}
189
190struct Style<'a> {
192 use_24_hour: bool,
193 show_seconds: bool,
194 date_format: Option<&'a str>,
195 #[cfg(feature = "locales")]
197 locale: Option<chrono::Locale>,
198}
199
200impl Style<'_> {
201 fn write(&self, time: DateTime<Utc>, pattern: &str) -> Option<String> {
203 use std::fmt::Write;
204 let mut text = String::new();
205 #[cfg(feature = "locales")]
206 if let Some(locale) = self.locale {
207 write!(text, "{}", time.format_localized(pattern, locale)).ok()?;
208 return Some(text.split_whitespace().collect::<Vec<_>>().join(" "));
210 }
211 write!(text, "{}", time.format(pattern)).ok()?;
212 Some(text)
213 }
214
215 fn format(&self, time: DateTime<Utc>, mark_type: TickMarkType) -> String {
216 let pattern = match (mark_type, self.use_24_hour) {
217 (TickMarkType::Year, _) => "%Y",
218 (TickMarkType::Month, _) => "%b",
219 (TickMarkType::DayOfMonth, _) => "%d",
220 (TickMarkType::TimeWithMilliseconds, true) if self.show_seconds => "%H:%M:%S%.3f",
221 (TickMarkType::TimeWithMilliseconds, false) if self.show_seconds => "%I:%M:%S%.3f %p",
222 (TickMarkType::TimeWithSeconds, true) if self.show_seconds => "%H:%M:%S",
223 (TickMarkType::TimeWithSeconds, false) if self.show_seconds => "%I:%M:%S %p",
224 (_, true) => "%H:%M",
226 (_, false) => "%I:%M %p",
227 };
228 self.write(time, pattern).unwrap_or_default()
229 }
230
231 fn format_crosshair(&self, time: DateTime<Utc>, bar_seconds: i64) -> String {
232 let date = self
233 .date_format
234 .and_then(|pattern| self.write(time, pattern));
235 let custom_date = date.is_some();
236 let daily = bar_seconds >= 86_400;
237 let pattern = if daily { "%b %d" } else { "%a %d %b '%y" };
238 #[cfg(feature = "locales")]
239 let date = date.or_else(|| self.locale.and_then(|_| self.write(time, pattern)));
240 let default_date = time.format(pattern);
242 let date: &dyn std::fmt::Display = match &date {
243 Some(date) => date,
244 None => &default_date,
245 };
246 if daily {
247 return if custom_date
248 || time.format("%Y").to_string() == Utc::now().format("%Y").to_string()
249 {
250 date.to_string()
251 } else {
252 format!("{date}, {}", time.format("%Y"))
253 };
254 }
255 let kind = if bar_seconds >= 60 {
256 TickMarkType::Time
257 } else if bar_seconds >= 1 {
258 TickMarkType::TimeWithSeconds
259 } else {
260 TickMarkType::TimeWithMilliseconds
261 };
262 let clock = self.format(time, kind);
263 format!("{date} {clock}")
264 }
265}
266
267#[derive(Debug, Clone)]
273pub struct LocaleTimeFormatter {
274 pub locale: String,
277
278 pub use_24_hour: bool,
280
281 pub show_seconds: bool,
283
284 pub date_format: Option<String>,
287}
288
289impl LocaleTimeFormatter {
290 pub fn new(locale: impl Into<String>) -> Self {
292 let locale_str = locale.into();
293 let use_24_hour = Self::default_24hour_for_locale(&locale_str);
294 Self {
295 locale: locale_str,
296 use_24_hour,
297 show_seconds: true,
298 date_format: None,
299 }
300 }
301
302 fn default_24hour_for_locale(locale: &str) -> bool {
304 !matches!(locale.replace('_', "-").as_str(), "en-US" | "en-CA")
305 }
306
307 fn style(&self) -> Style<'_> {
308 Style {
309 use_24_hour: self.use_24_hour,
310 show_seconds: self.show_seconds,
311 date_format: self.date_format.as_deref(),
312 #[cfg(feature = "locales")]
313 locale: Some(
314 chrono::Locale::try_from(self.locale.replace('-', "_").as_str())
315 .unwrap_or(chrono::Locale::POSIX),
316 ),
317 }
318 }
319}
320
321impl TimeFormatter for LocaleTimeFormatter {
322 fn format(&self, time: DateTime<Utc>, mark_type: TickMarkType) -> String {
323 self.style().format(time, mark_type)
324 }
325
326 fn format_crosshair(&self, time: DateTime<Utc>, bar_seconds: i64) -> String {
327 self.style().format_crosshair(time, bar_seconds)
328 }
329
330 fn clone_box(&self) -> Box<dyn TimeFormatter> {
331 Box::new(self.clone())
332 }
333}
334
335#[derive(Clone)]
337pub struct CustomTimeFormatter {
338 formatter: Arc<dyn Fn(DateTime<Utc>, TickMarkType) -> String + Send + Sync>,
339}
340
341impl CustomTimeFormatter {
342 pub fn new<F>(formatter: F) -> Self
344 where
345 F: Fn(DateTime<Utc>, TickMarkType) -> String + Send + Sync + 'static,
346 {
347 Self {
348 formatter: Arc::new(formatter),
349 }
350 }
351}
352
353impl TimeFormatter for CustomTimeFormatter {
354 fn format(&self, time: DateTime<Utc>, mark_type: TickMarkType) -> String {
355 (self.formatter)(time, mark_type)
356 }
357
358 fn clone_box(&self) -> Box<dyn TimeFormatter> {
359 Box::new(self.clone())
360 }
361}
362
363struct TickContextFormatter {
365 formatter: Box<dyn TimeFormatter>,
366 fallback: Box<dyn TimeFormatter>,
367 locale: Option<String>,
368}
369
370impl TimeFormatter for TickContextFormatter {
371 fn format(&self, time: DateTime<Utc>, mark_type: TickMarkType) -> String {
372 self.formatter
373 .format_tick(TickMarkContext {
374 time,
375 mark_type,
376 locale: self.locale.as_deref(),
377 })
378 .unwrap_or_else(|| self.fallback.format(time, mark_type))
379 }
380
381 fn format_crosshair(&self, time: DateTime<Utc>, bar_seconds: i64) -> String {
382 self.formatter.format_crosshair(time, bar_seconds)
383 }
384
385 fn clone_box(&self) -> Box<dyn TimeFormatter> {
386 Box::new(Self {
387 formatter: self.formatter.clone_box(),
388 fallback: self.fallback.clone_box(),
389 locale: self.locale.clone(),
390 })
391 }
392}
393
394#[derive(Debug, Clone)]
396pub struct RelativeTimeFormatter {
397 pub reference_time: DateTime<Utc>,
399}
400
401impl RelativeTimeFormatter {
402 pub fn new(reference_time: DateTime<Utc>) -> Self {
404 Self { reference_time }
405 }
406
407 pub fn now() -> Self {
409 Self {
410 reference_time: Utc::now(),
411 }
412 }
413}
414
415impl TimeFormatter for RelativeTimeFormatter {
416 fn format(&self, time: DateTime<Utc>, _mark_type: TickMarkType) -> String {
417 let diff = self.reference_time.signed_duration_since(time);
418
419 if diff.num_seconds() < 0 {
420 return "future".to_string();
421 }
422
423 let seconds = diff.num_seconds();
424
425 if seconds < 60 {
426 format!("{seconds}s ago")
427 } else if seconds < 3600 {
428 format!("{}m ago", seconds / 60)
429 } else if seconds < 86400 {
430 format!("{}h ago", seconds / 3600)
431 } else if seconds < 604800 {
432 format!("{}d ago", seconds / 86400)
433 } else if seconds < 2592000 {
434 format!("{}w ago", seconds / 604800)
435 } else if seconds < 31536000 {
436 format!("{}mo ago", seconds / 2592000)
437 } else {
438 format!("{}y ago", seconds / 31536000)
439 }
440 }
441
442 fn clone_box(&self) -> Box<dyn TimeFormatter> {
443 Box::new(self.clone())
444 }
445}
446
447#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
449pub enum TimezoneMode {
450 #[default]
452 Utc,
453 Local,
455 Timezone(TimezoneId),
457 Exchange(TimezoneId),
459}
460
461impl TimezoneMode {
462 #[cfg(feature = "timezones")]
464 pub fn nyse() -> Self {
465 TimezoneMode::Exchange("America/New_York".parse().unwrap())
466 }
467
468 #[cfg(feature = "timezones")]
470 pub fn lse() -> Self {
471 TimezoneMode::Exchange("Europe/London".parse().unwrap())
472 }
473
474 #[cfg(feature = "timezones")]
476 pub fn jse() -> Self {
477 TimezoneMode::Exchange("Asia/Tokyo".parse().unwrap())
478 }
479
480 #[cfg(feature = "timezones")]
482 pub fn hkex() -> Self {
483 TimezoneMode::Exchange("Asia/Hong_Kong".parse().unwrap())
484 }
485
486 pub fn to_tz(&self) -> Option<TimezoneId> {
488 match self {
489 TimezoneMode::Utc => None, TimezoneMode::Local => None, TimezoneMode::Timezone(tz) | TimezoneMode::Exchange(tz) => Some(tz.clone()),
492 }
493 }
494}
495
496pub(crate) struct TimezoneAwareFormatter<T> {
499 inner: Box<dyn TimeFormatter>,
500 timezone: T,
501 time_visible: bool,
502}
503
504impl<T: Clone> Clone for TimezoneAwareFormatter<T> {
505 fn clone(&self) -> Self {
506 Self {
507 inner: self.inner.clone_box(),
508 timezone: self.timezone.clone(),
509 time_visible: self.time_visible,
510 }
511 }
512}
513
514impl<T: chrono::TimeZone> TimezoneAwareFormatter<T> {
515 pub(crate) fn new(inner: Box<dyn TimeFormatter>, timezone: T, time_visible: bool) -> Self {
517 Self {
518 inner,
519 timezone,
520 time_visible,
521 }
522 }
523
524 fn convert_timezone(&self, utc_time: DateTime<Utc>) -> DateTime<Utc> {
526 use crate::model::local_time;
527 local_time(self.timezone.clone(), utc_time).and_utc()
528 }
529}
530
531impl<T: chrono::TimeZone + Send + Sync + 'static> TimeFormatter for TimezoneAwareFormatter<T> {
532 fn format(&self, time: DateTime<Utc>, mark_type: TickMarkType) -> String {
533 let converted = self.convert_timezone(time);
534 if !self.time_visible
535 && matches!(
536 mark_type,
537 TickMarkType::Time
538 | TickMarkType::TimeWithSeconds
539 | TickMarkType::TimeWithMilliseconds
540 )
541 {
542 self.inner.format_crosshair(converted, 86_400)
543 } else {
544 self.inner.format(converted, mark_type)
545 }
546 }
547
548 fn time_visible(&self) -> bool {
549 self.time_visible
550 }
551
552 fn wall_time(&self, time: DateTime<Utc>) -> DateTime<Utc> {
553 self.convert_timezone(time)
554 }
555
556 fn utc_time(&self, time: DateTime<Utc>) -> Option<DateTime<Utc>> {
557 use crate::model::local_instant;
558 local_instant(self.timezone.clone(), time.naive_utc())
559 }
560
561 fn format_crosshair(&self, time: DateTime<Utc>, bar_seconds: i64) -> String {
562 self.inner.format_crosshair(
563 self.convert_timezone(time),
564 if self.time_visible {
565 bar_seconds
566 } else {
567 86_400
568 },
569 )
570 }
571
572 fn clone_box(&self) -> Box<dyn TimeFormatter> {
573 Box::new(self.clone())
574 }
575}
576
577pub struct TimeFormatterBuilder {
579 use_24_hour: bool,
580 show_seconds: bool,
581 locale: Option<String>,
582 date_format: Option<String>,
583 formatter: Option<Box<dyn TimeFormatter>>,
584 timezone: Option<TimezoneMode>,
585 time_visible: bool,
586}
587
588impl TimeFormatterBuilder {
589 pub fn new() -> Self {
591 Self {
592 use_24_hour: true,
593 show_seconds: true,
594 locale: None,
595 date_format: None,
596 formatter: None,
597 timezone: None,
598 time_visible: true,
599 }
600 }
601
602 pub fn with_24_hour(mut self, use_24_hour: bool) -> Self {
604 self.use_24_hour = use_24_hour;
605 self
606 }
607
608 pub fn with_seconds(mut self, show_seconds: bool) -> Self {
610 self.show_seconds = show_seconds;
611 self
612 }
613
614 pub fn with_time_visible(mut self, time_visible: bool) -> Self {
616 self.time_visible = time_visible;
617 self
618 }
619
620 pub fn with_locale(mut self, locale: impl Into<String>) -> Self {
622 self.locale = Some(locale.into());
623 self
624 }
625
626 pub fn with_date_format(mut self, date_format: impl Into<String>) -> Self {
629 self.date_format = Some(date_format.into());
630 self
631 }
632
633 pub fn with_formatter(mut self, formatter: Box<dyn TimeFormatter>) -> Self {
641 self.formatter = Some(formatter);
642 self
643 }
644
645 pub fn with_timezone(mut self, timezone: TimezoneMode) -> Self {
647 self.timezone = Some(timezone);
648 self
649 }
650
651 pub fn build(self) -> Box<dyn TimeFormatter> {
653 let base: Box<dyn TimeFormatter> = if let Some(locale) = &self.locale {
654 Box::new(LocaleTimeFormatter {
655 locale: locale.clone(),
656 use_24_hour: self.use_24_hour,
657 show_seconds: self.show_seconds,
658 date_format: self.date_format,
659 })
660 } else {
661 DefaultTimeFormatter {
662 use_24_hour: self.use_24_hour,
663 show_seconds: self.show_seconds,
664 date_format: self.date_format,
665 }
666 .into_box()
667 };
668 let base: Box<dyn TimeFormatter> = match self.formatter {
669 Some(formatter) => Box::new(TickContextFormatter {
670 formatter,
671 fallback: base,
672 locale: self.locale,
673 }),
674 None => base,
675 };
676
677 match self.timezone.unwrap_or_default() {
679 TimezoneMode::Utc => {
680 Box::new(TimezoneAwareFormatter::new(base, Utc, self.time_visible))
681 }
682 TimezoneMode::Local => Box::new(TimezoneAwareFormatter::new(
683 base,
684 chrono::Local,
685 self.time_visible,
686 )),
687 TimezoneMode::Timezone(zone) | TimezoneMode::Exchange(zone) => {
688 Box::new(TimezoneAwareFormatter::new(base, zone, self.time_visible))
689 }
690 }
691 }
692}
693
694impl Default for TimeFormatterBuilder {
695 fn default() -> Self {
696 Self::new()
697 }
698}
699
700#[cfg(test)]
701mod tests {
702 use super::*;
703 use chrono::TimeZone;
704
705 #[test]
706 fn test_default_formatter() {
707 let formatter = DefaultTimeFormatter::default().clone_box();
708 let time = Utc.with_ymd_and_hms(2024, 6, 15, 14, 30, 45).unwrap();
709
710 assert_eq!(formatter.format(time, TickMarkType::Year), "2024");
711 assert_eq!(formatter.format(time, TickMarkType::Month), "Jun");
712 assert_eq!(formatter.format(time, TickMarkType::Time), "14:30");
713 assert_eq!(
714 formatter.format(time, TickMarkType::TimeWithSeconds),
715 "14:30:45"
716 );
717
718 let twelve_hour = TimeFormatterBuilder::new()
719 .with_24_hour(false)
720 .with_seconds(false)
721 .build();
722 assert_eq!(twelve_hour.format(time, TickMarkType::Time), "02:30 PM");
723 }
724
725 #[test]
726 fn fractional_crosshair_seconds_precede_the_meridiem() {
727 let time = Utc.with_ymd_and_hms(2024, 6, 15, 14, 30, 45).unwrap()
728 + chrono::Duration::milliseconds(123);
729 for (use_24_hour, seconds, fractional) in [
730 (true, "14:30:45", "14:30:45.123"),
731 (false, "02:30:45 PM", "02:30:45.123 PM"),
732 ] {
733 let formatter = DefaultTimeFormatter {
734 use_24_hour,
735 ..Default::default()
736 };
737 let custom = CustomTimeFormatter::new(move |time, kind| {
738 DefaultTimeFormatter {
739 use_24_hour,
740 ..Default::default()
741 }
742 .format(time, kind)
743 });
744 for formatter in [&formatter as &dyn TimeFormatter, &custom] {
746 for (bar_seconds, clock) in [(0, fractional), (1, seconds)] {
747 let label = formatter.format_crosshair(time, bar_seconds);
748 assert!(label.ends_with(clock), "{label}");
749 }
750 }
751 }
752 let hidden_seconds = DefaultTimeFormatter {
753 use_24_hour: false,
754 show_seconds: false,
755 ..Default::default()
756 };
757 assert!(
758 hidden_seconds
759 .format_crosshair(time, 0)
760 .ends_with("02:30 PM")
761 );
762 }
763
764 #[test]
765 fn locale_and_date_format_shape_labels() {
766 let time = Utc.with_ymd_and_hms(2024, 3, 13, 14, 30, 45).unwrap();
768 let localized = |name, english| {
770 if cfg!(feature = "locales") {
771 name
772 } else {
773 english
774 }
775 };
776 for (locale, expected) in [
777 ("en-US", "02:30 PM"),
778 ("en_US", "02:30 PM"),
779 ("en-CA", "02:30 PM"),
780 ("en_CA", "02:30 PM"),
781 ("en-GB", "14:30"),
782 ("en_GB", "14:30"),
783 ("de-DE", "14:30"),
784 ("de_DE", "14:30"),
785 ] {
786 assert_eq!(
787 LocaleTimeFormatter::new(locale).format(time, TickMarkType::Time),
788 expected,
789 "{locale}"
790 );
791 }
792 let german = LocaleTimeFormatter::new("de-DE");
793 assert_eq!(
794 german.format(time, TickMarkType::Month),
795 localized("Mär", "Mar")
796 );
797 assert_eq!(
798 german.format_crosshair(time, 60),
799 localized("Mi 13 Mär '24 14:30", "Wed 13 Mar '24 14:30")
800 );
801 let japanese = LocaleTimeFormatter::new("ja_JP");
802 assert_eq!(
803 japanese.format(time, TickMarkType::Month),
804 localized("3月", "Mar")
805 );
806 assert_eq!(
807 LocaleTimeFormatter::new("xx-YY").format(time, TickMarkType::Month),
808 "Mar"
809 );
810 let dated = |date_format: &str| {
811 DefaultTimeFormatter {
812 date_format: Some(date_format.into()),
813 ..Default::default()
814 }
815 .clone_box()
816 };
817 assert_eq!(
818 dated("%d.%m.%Y").format_crosshair(time, 60),
819 "13.03.2024 14:30"
820 );
821 assert_eq!(
822 dated("%d.%m.%Y").format_crosshair(time, 86_400),
823 "13.03.2024"
824 );
825 assert_eq!(
827 dated("%Q").format_crosshair(time, 60),
828 "Wed 13 Mar '24 14:30"
829 );
830 let current = time.with_year(Utc::now().year()).unwrap();
831 let previous = current.with_year(current.year() - 1).unwrap();
832 let default = DefaultTimeFormatter::default().clone_box();
833 assert_eq!(default.format_crosshair(current, 86_400), "Mar 13");
834 assert_eq!(
835 default.format_crosshair(previous, 86_400),
836 format!("Mar 13, {}", previous.year())
837 );
838 }
839
840 #[test]
841 fn test_relative_formatter() {
842 let reference = Utc.with_ymd_and_hms(2024, 6, 15, 14, 30, 0).unwrap();
843 let formatter = RelativeTimeFormatter::new(reference);
844
845 let time = Utc.with_ymd_and_hms(2024, 6, 15, 14, 28, 0).unwrap();
846 assert_eq!(formatter.format(time, TickMarkType::Time), "2m ago");
847
848 let time = Utc.with_ymd_and_hms(2024, 6, 15, 12, 30, 0).unwrap();
849 assert_eq!(formatter.format(time, TickMarkType::Time), "2h ago");
850 }
851
852 #[test]
853 fn builder_overrides_host_calendar_and_clock_visibility() {
854 #[derive(Clone)]
855 struct Host;
856 impl TimeFormatter for Host {
857 fn format(&self, time: DateTime<Utc>, _: TickMarkType) -> String {
858 time.to_rfc3339()
859 }
860 fn wall_time(&self, time: DateTime<Utc>) -> DateTime<Utc> {
861 time + chrono::Duration::hours(1)
862 }
863 fn utc_time(&self, _: DateTime<Utc>) -> Option<DateTime<Utc>> {
864 None
865 }
866 fn time_visible(&self) -> bool {
867 false
868 }
869 fn clone_box(&self) -> Box<dyn TimeFormatter> {
870 Box::new(self.clone())
871 }
872 }
873 let time = Utc.with_ymd_and_hms(2024, 6, 15, 14, 30, 0).unwrap();
874 let formatter = TimeFormatterBuilder::new()
875 .with_formatter(Box::new(Host))
876 .with_timezone(TimezoneMode::Utc)
877 .build();
878 for formatter in [formatter.clone_box(), formatter] {
879 assert!(formatter.time_visible());
880 assert_eq!(formatter.wall_time(time), time);
881 assert_eq!(formatter.utc_time(time), Some(time));
882 assert_eq!(
883 formatter.format(time, TickMarkType::Time),
884 time.to_rfc3339()
885 );
886 }
887 }
888
889 #[test]
890 fn test_timezone_conversion_utc_and_fixed_offsets() {
891 let time = Utc.with_ymd_and_hms(2024, 6, 15, 14, 30, 0).unwrap();
892 for (name, label, wall) in [
893 ("UTC", "14:30", "2024-06-15T14:30:00Z"),
894 ("+05:45", "20:15", "2024-06-15T20:15:00Z"),
895 ("-03:30", "11:00", "2024-06-15T11:00:00Z"),
896 ("+14:00", "04:30", "2024-06-16T04:30:00Z"),
897 ("-12:00", "02:30", "2024-06-15T02:30:00Z"),
898 ("+00:19:32", "14:49", "2024-06-15T14:49:32Z"),
899 ("+23:59:59", "14:29", "2024-06-16T14:29:59Z"),
900 ("-23:59:59", "14:30", "2024-06-14T14:30:01Z"),
901 ] {
902 let mode = TimezoneMode::Timezone(name.parse().unwrap());
903 let json = serde_json::to_string(&mode).unwrap();
904 assert_eq!(json, format!(r#"{{"Timezone":"{name}"}}"#));
905 assert_eq!(serde_json::from_str::<TimezoneMode>(&json).unwrap(), mode);
906 let formatter = TimeFormatterBuilder::new()
907 .with_24_hour(true)
908 .with_seconds(false)
909 .with_timezone(mode)
910 .build();
911 assert_eq!(formatter.format(time, TickMarkType::Time), label, "{name}");
912 assert_eq!(
913 formatter.wall_time(time),
914 wall.parse::<DateTime<Utc>>().unwrap(),
915 "{name}"
916 );
917 assert_eq!(formatter.utc_time(formatter.wall_time(time)), Some(time));
918 }
919 for invalid in [
920 "",
921 "+24:00",
922 "-24:00",
923 "+01:60",
924 "+01:00:60",
925 "+01:00junk",
926 "+1:00",
927 "unknown",
928 ] {
929 assert!(invalid.parse::<TimezoneId>().is_err(), "{invalid}");
930 }
931 for seconds in [i32::MIN, -86_400, 86_400, i32::MAX] {
932 assert!(TimezoneId::fixed_offset(seconds).is_none());
933 }
934 assert_eq!(
935 TimezoneId::named("America/New_York").is_some(),
936 cfg!(feature = "timezones")
937 );
938 }
939
940 #[cfg(feature = "timezones")]
941 #[test]
942 fn named_zones_keep_their_clock_through_dst_and_cloning() {
943 use TickMarkType::{Time, TimeWithSeconds};
944 for (zone, instant, kind, expected) in [
946 (TimezoneMode::nyse(), "2024-06-15T14:30:00Z", Time, "10:30"),
947 (
948 TimezoneMode::Exchange("America/New_York".parse().unwrap()),
949 "2024-03-10T06:00:00Z",
950 TimeWithSeconds,
951 "01:00:00",
952 ),
953 (
954 TimezoneMode::Exchange("America/New_York".parse().unwrap()),
955 "2024-03-10T08:00:00Z",
956 TimeWithSeconds,
957 "04:00:00",
958 ),
959 (
960 TimezoneMode::Exchange("America/New_York".parse().unwrap()),
961 "2024-11-03T05:00:00Z",
962 TimeWithSeconds,
963 "01:00:00",
964 ),
965 (
966 TimezoneMode::Exchange("America/New_York".parse().unwrap()),
967 "2024-11-03T07:00:00Z",
968 TimeWithSeconds,
969 "02:00:00",
970 ),
971 (TimezoneMode::lse(), "2024-06-15T14:30:00Z", Time, "15:30"),
972 (TimezoneMode::lse(), "2024-01-15T14:30:00Z", Time, "14:30"),
973 (TimezoneMode::jse(), "2024-06-15T14:30:00Z", Time, "23:30"),
974 ] {
975 let time = instant.parse::<DateTime<Utc>>().unwrap();
976 let formatter = TimeFormatterBuilder::new()
977 .with_24_hour(true)
978 .with_timezone(zone.clone())
979 .build();
980 for formatter in [formatter.clone_box(), formatter] {
981 assert_eq!(formatter.format(time, kind), expected, "{zone:?} {instant}");
982 }
983 }
984 let time = Utc.with_ymd_and_hms(2024, 6, 15, 14, 30, 0).unwrap();
985 let formatter = TimeFormatterBuilder::new()
986 .with_timezone(TimezoneMode::nyse())
987 .build();
988 assert_eq!(
989 formatter.format_crosshair(time, 3600),
990 "Sat 15 Jun '24 10:30"
991 );
992 assert_eq!(
993 formatter.format_crosshair(time + chrono::Duration::milliseconds(123), 0),
994 "Sat 15 Jun '24 10:30:00.123"
995 );
996 }
997}