dsmr_parse/
tst.rs

1use core::str;
2
3/// A point in time as reported by the meter
4///
5/// `year` is normalized from 2 digits by mapping it to 1969..=2068 range
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
7pub struct Tst {
8	/// Year, normalized from 2 digits by mapping it to 1969..=2068 range
9	pub year: u16,
10	pub month: u8,
11	pub day: u8,
12	pub hour: u8,
13	pub minute: u8,
14	pub second: u8,
15	/// True if Daylight Saving Time is active
16	pub dst: bool,
17}
18
19impl Tst {
20	pub fn try_from_bytes(bytes: &[u8]) -> Option<Self> {
21		let mut parts = bytes.chunks(2);
22		let year = normalize_two_digit_year(str::from_utf8(parts.next()?).ok()?.parse().ok()?);
23		let month = str::from_utf8(parts.next()?).ok()?.parse().ok()?;
24		let day = str::from_utf8(parts.next()?).ok()?.parse().ok()?;
25		let hour = str::from_utf8(parts.next()?).ok()?.parse().ok()?;
26		let minute = str::from_utf8(parts.next()?).ok()?.parse().ok()?;
27		let second = str::from_utf8(parts.next()?).ok()?.parse().ok()?;
28		let dst = parts.next()? == b"S";
29		Some(Self {
30			year,
31			month,
32			day,
33			hour,
34			minute,
35			second,
36			dst,
37		})
38	}
39
40	/// Convert current [Tst] to [jiff::Zoned] in the indicated timezone
41	#[cfg(feature = "jiff")]
42	pub fn to_jiff(&self, timezone: &jiff::tz::TimeZone) -> Option<jiff::Zoned> {
43		jiff::civil::DateTime::new(
44			i16::try_from(self.year).unwrap_or(i16::MAX),
45			i8::try_from(self.month).unwrap_or(i8::MAX),
46			i8::try_from(self.day).unwrap_or(i8::MAX),
47			i8::try_from(self.hour).unwrap_or(i8::MAX),
48			i8::try_from(self.minute).unwrap_or(i8::MAX),
49			i8::try_from(self.second).unwrap_or(i8::MAX),
50			0,
51		)
52		.map(|d| timezone.to_ambiguous_zoned(d))
53		.ok()
54		.and_then(|d| {
55			let strategy = if self.dst {
56				jiff::tz::Disambiguation::Earlier
57			} else {
58				jiff::tz::Disambiguation::Later
59			};
60			d.disambiguate(strategy).ok()
61		})
62	}
63
64	/// Convert current [Tst] to [chrono::DateTime] in the indicated timezone
65	#[cfg(feature = "chrono")]
66	pub fn to_chrono<Tz: chrono::TimeZone>(&self, timezone: &Tz) -> Option<chrono::DateTime<Tz>> {
67		let d = timezone.from_local_datetime(&chrono::NaiveDateTime::new(
68			chrono::NaiveDate::from_ymd_opt(i32::from(self.year), u32::from(self.month), u32::from(self.day))?,
69			chrono::NaiveTime::from_hms_opt(u32::from(self.hour), u32::from(self.minute), u32::from(self.second))?,
70		));
71		match d {
72			chrono::LocalResult::None => None,
73			chrono::LocalResult::Single(d) => Some(d),
74			chrono::LocalResult::Ambiguous(min, max) => Some(if self.dst {
75				min
76			} else {
77				max
78			}),
79		}
80	}
81}
82
83fn normalize_two_digit_year(year: u16) -> u16 {
84	if (69..=99).contains(&year) {
85		year + 1900
86	} else if (0..=68).contains(&year) {
87		year + 2000
88	} else {
89		year
90	}
91}