1#![cfg_attr(not(feature = "std"), no_std)]
2
3#[cfg(feature = "std")]
4use std::{str::FromStr, sync::OnceLock};
5
6use chrono::{NaiveTime, Timelike};
7#[cfg(feature = "std")]
8use regex::Regex;
9use thiserror_no_std::Error;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct LightningTimeColorConfig {
13 pub bolt: LightningBaseColors,
14 pub zap: LightningBaseColors,
15 pub spark: LightningBaseColors,
16}
17
18impl Default for LightningTimeColorConfig {
19 fn default() -> Self {
20 Self {
21 bolt: LightningBaseColors(161, 0),
22 zap: LightningBaseColors(50, 214),
23 spark: LightningBaseColors(246, 133),
24 }
25 }
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29pub struct LightningTime {
30 pub bolts: u8,
31 pub zaps: u8,
32 pub sparks: u8,
33 pub charges: u8,
34 pub subcharges: u8,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct LightningBaseColors(pub u8, pub u8);
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub struct LightningTimeColors {
42 pub bolt: palette::Srgb<u8>,
43 pub zap: palette::Srgb<u8>,
44 pub spark: palette::Srgb<u8>,
45}
46
47impl LightningTime {
48 pub fn new(bolts: u8, zaps: u8, sparks: u8, charges: u8) -> Self {
49 Self {
50 bolts,
51 zaps,
52 sparks,
53 charges,
54 ..Default::default()
55 }
56 }
57
58 pub fn colors(&self, config: &LightningTimeColorConfig) -> LightningTimeColors {
59 LightningTimeColors {
60 bolt: palette::Srgb::new(self.bolts * 16 + self.zaps, config.bolt.0, config.bolt.1),
61 zap: palette::Srgb::new(config.zap.0, self.zaps * 16 + self.sparks, config.zap.1),
62 spark: palette::Srgb::new(
63 config.spark.0,
64 config.spark.1,
65 self.sparks * 16 + self.charges,
66 ),
67 }
68 }
69
70 #[cfg(feature = "std")]
71 pub fn to_stripped_string(&self) -> String {
72 format!("{:x}~{:x}~{:x}", self.bolts, self.zaps, self.sparks)
73 }
74
75 pub fn now() -> Self {
76 Self::from(chrono::offset::Local::now().naive_local().time())
77 }
78}
79
80const MILLIS_PER_SUBCHARGE: f64 = 86_400_000.0 / 1048576.0; impl From<NaiveTime> for LightningTime {
83 fn from(value: NaiveTime) -> Self {
84 let millis = 1_000. * 60. * 60. * value.hour() as f64
85 + 1_000. * 60. * value.minute() as f64
86 + 1_000. * value.second() as f64
87 + value.nanosecond() as f64 / 1.0e6;
88
89 let total_subcharges = millis / MILLIS_PER_SUBCHARGE;
90 let total_charges = total_subcharges / 16.;
91 let total_sparks = total_charges / 16.;
92 let total_zaps = total_sparks / 16.;
93 let total_bolts = total_zaps / 16.;
94
95 #[cfg(feature = "std")]
96 {
97 LightningTime {
98 bolts: (total_bolts.floor() % 16.) as u8,
99 sparks: (total_sparks.floor() % 16.) as u8,
100 zaps: (total_zaps.floor() % 16.) as u8,
101 charges: (total_charges.floor() % 16.) as u8,
102 subcharges: (total_subcharges.floor() % 16.) as u8,
103 }
104 }
105
106 #[cfg(not(feature = "std"))]
107 {
108 use libm::floor;
109 LightningTime {
110 bolts: (floor(total_bolts) % 16.) as u8,
111 sparks: (floor(total_sparks) % 16.) as u8,
112 zaps: (floor(total_zaps) % 16.) as u8,
113 charges: (floor(total_charges) % 16.) as u8,
114 subcharges: (floor(total_subcharges) % 16.) as u8,
115 }
116 }
117 }
118}
119
120#[cfg(feature = "std")]
121static RE: OnceLock<Regex> = OnceLock::new();
122
123#[cfg(feature = "std")]
124impl FromStr for LightningTime {
125 type Err = Error;
126
127 fn from_str(s: &str) -> Result<Self, Self::Err> {
128 let re = RE.get_or_init(|| {
129 Regex::new(r"(?P<bolt>[[:xdigit:]])~(?P<spark>[[:xdigit:]])~(?P<zap>[[:xdigit:]])(?:\|(?P<charge>[[:xdigit:]])(?P<subcharge>[[:xdigit:]])?)?").unwrap()
130 });
131
132 let caps = re.captures(s);
133 match caps {
134 Some(caps) => {
135 if caps.len() < 3 {
136 return Err(Error::InvalidConversion);
137 }
138 Ok(LightningTime {
139 bolts: u8::from_str_radix(caps.name("bolt").unwrap().as_str(), 16).unwrap(),
140 zaps: u8::from_str_radix(caps.name("zap").unwrap().as_str(), 16).unwrap(),
141 sparks: u8::from_str_radix(caps.name("spark").unwrap().as_str(), 16).unwrap(),
142 charges: caps
143 .name("charge")
144 .map(|c| u8::from_str_radix(c.as_str(), 16).unwrap())
145 .unwrap_or(0),
146 subcharges: caps
147 .name("subcharge")
148 .map(|c| u8::from_str_radix(c.as_str(), 16).unwrap())
149 .unwrap_or(0),
150 })
151 }
152 None => Err(Error::InvalidConversion),
153 }
154 }
155}
156
157impl core::fmt::Display for LightningTime {
158 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
159 f.write_fmt(format_args!(
160 "{:x}~{:x}~{:x}|{:x}{:x}",
161 self.bolts, self.zaps, self.sparks, self.charges, self.subcharges
162 ))
163 }
164}
165
166#[derive(Debug, Clone, Copy, Error)]
167pub enum Error {
168 #[error("Invalid conversion")]
169 InvalidConversion,
170}
171
172impl From<LightningTime> for NaiveTime {
173 fn from(value: LightningTime) -> Self {
174 let elapsed: usize =
175 (((value.bolts as usize * 16 + value.zaps as usize) * 16 + value.sparks as usize) * 16
176 + value.charges as usize)
177 * 16
178 + value.subcharges as usize;
179
180 let millis = elapsed as f64 * MILLIS_PER_SUBCHARGE;
181
182 let seconds = millis / 1000.;
183 let leftover_millis = millis % 1000.;
184
185 NaiveTime::from_num_seconds_from_midnight_opt(
186 seconds as u32,
187 (leftover_millis * 1.0e6) as u32,
188 )
189 .expect("Lightning Time to never overflow")
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use chrono::{NaiveTime, Timelike};
196 use palette::Srgb;
197
198 use crate::{LightningTime, LightningTimeColors};
199
200 #[test]
201 fn convert_to_lightning() {
202 let real = NaiveTime::from_hms_opt(12, 0, 0).unwrap();
203 let lightning = LightningTime::from(real);
204 assert_eq!(
205 lightning,
206 LightningTime {
207 bolts: 0x8,
208 ..Default::default()
209 }
210 );
211
212 #[cfg(feature = "std")]
213 {
214 assert_eq!(lightning.to_string(), "8~0~0|00");
215 assert_eq!(lightning.to_stripped_string(), "8~0~0");
216 }
217 assert_eq!(
218 lightning.colors(&Default::default()),
219 LightningTimeColors {
220 bolt: Srgb::new(0x80, 0xa1, 0x00),
221 zap: Srgb::new(0x32, 0x00, 0xd6),
222 spark: Srgb::new(0xf6, 0x85, 0x00),
223 }
224 );
225 }
226
227 #[test]
228 #[cfg(feature = "std")]
229 fn parse() {
230 use std::str::FromStr;
231 assert!(LightningTime::from_str("f~3~a|8c").is_ok());
232 assert!(LightningTime::from_str("f~3~a|8").is_ok());
233 assert!(LightningTime::from_str("f~3~a").is_ok());
234 assert!(LightningTime::from_str("f~~|").is_err());
235 }
236
237 #[test]
238 fn convert_to_real() {
239 let lightning = LightningTime {
240 bolts: 0x8,
241 ..Default::default()
242 };
243
244 let naive: NaiveTime = lightning.into();
245
246 assert_eq!(naive, NaiveTime::from_hms_opt(12, 0, 0).unwrap());
247
248 let lightning = LightningTime {
249 bolts: 0x8,
250 charges: 0xa,
251 ..Default::default()
252 };
253
254 let naive: NaiveTime = lightning.into();
255
256 assert_eq!(
258 naive.second(),
259 NaiveTime::from_hms_opt(12, 0, 13).unwrap().second()
260 );
261 }
262}