lightning_time/
lib.rs

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; // Div by 16^5
81
82impl 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        // Floating point is not fun
257        assert_eq!(
258            naive.second(),
259            NaiveTime::from_hms_opt(12, 0, 13).unwrap().second()
260        );
261    }
262}