use std::{str::FromStr, sync::OnceLock};
use chrono::{NaiveTime, Timelike};
use regex::Regex;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LightningTime {
pub bolts: u8,
pub bolt_colors: LightningBaseColors,
pub zaps: u8,
pub zap_colors: LightningBaseColors,
pub sparks: u8,
pub spark_colors: LightningBaseColors,
pub charges: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LightningBaseColors(pub u8, pub u8);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LightningTimeColors {
pub bolt: prisma::Rgb<u8>,
pub zap: prisma::Rgb<u8>,
pub spark: prisma::Rgb<u8>,
}
impl Default for LightningTime {
fn default() -> Self {
Self { bolts: 0, bolt_colors: LightningBaseColors(161, 0), zaps: 0, zap_colors: LightningBaseColors(50, 214), sparks: 0, spark_colors: LightningBaseColors(246, 133), charges: 0 }
}
}
impl LightningTime {
pub fn new(bolts: u8, zaps: u8, sparks: u8, charges: u8) -> Self {
Self { bolts, zaps, sparks, charges, ..Default::default() }
}
pub fn colors(&self) -> LightningTimeColors {
LightningTimeColors {
bolt: prisma::Rgb::new(self.bolts * 16 + self.zaps, self.bolt_colors.0, self.bolt_colors.1),
zap: prisma::Rgb::new(self.zap_colors.0, self.zaps * 16 + self.sparks, self.zap_colors.1),
spark: prisma::Rgb::new(self.spark_colors.0, self.spark_colors.1, self.sparks * 16 + self.charges),
}
}
pub fn to_stripped_string(&self) -> String {
format!(
"{:x}~{:x}~{:x}",
self.bolts, self.zaps, self.sparks
)
}
}
const MILLIS_PER_CHARGE: f64 = 86_400_000.0 / 65_536.0; impl From<NaiveTime> for LightningTime {
fn from(value: NaiveTime) -> Self {
let millis = 1_000. * 60. * 60. * value.hour() as f64
+ 1_000. * 60. * value.minute() as f64
+ 1_000. * value.second() as f64
+ value.nanosecond() as f64 / 1.0e6;
let total_charges = millis / MILLIS_PER_CHARGE;
let total_sparks = total_charges / 16.;
let total_zaps = total_sparks / 16.;
let total_bolts = total_zaps / 16.;
LightningTime {
bolts: (total_bolts.floor() % 16.) as u8,
sparks: (total_sparks.floor() % 16.) as u8,
zaps: (total_zaps.floor() % 16.) as u8,
charges: (total_charges.floor() % 16.) as u8,
..Default::default()
}
}
}
static RE: OnceLock<Regex> = OnceLock::new();
impl FromStr for LightningTime {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let re = RE.get_or_init(|| {
Regex::new(r"(?P<bolt>[[:xdigit:]])~(?P<spark>[[:xdigit:]])~(?P<zap>[[:xdigit:]])(?:\|(?P<charge>[[:xdigit:]]))?").unwrap()
});
let caps = re.captures(s);
match caps {
Some(caps) => {
if caps.len() < 4 {
return Err(Error::InvalidConversion);
}
Ok(LightningTime {
bolts: u8::from_str_radix(caps.name("bolt").unwrap().as_str(), 16).unwrap(),
zaps: u8::from_str_radix(caps.name("zap").unwrap().as_str(), 16).unwrap(),
sparks: u8::from_str_radix(caps.name("spark").unwrap().as_str(), 16).unwrap(),
charges: caps.name("charge").map(|c| u8::from_str_radix(c.as_str(), 16).unwrap()).unwrap_or(0),
..Default::default()
})
},
None => Err(Error::InvalidConversion),
}
}
}
impl ToString for LightningTime {
fn to_string(&self) -> String {
format!(
"{:x}~{:x}~{:x}|{:x}",
self.bolts, self.zaps, self.sparks, self.charges
)
}
}
#[derive(Debug, Clone, Copy, Error)]
pub enum Error {
#[error("Invalid conversion")]
InvalidConversion,
}
impl TryInto<NaiveTime> for LightningTime {
type Error = Error;
fn try_into(self) -> Result<NaiveTime, Self::Error> {
let elapsed: usize = ((self.bolts as usize * 16 + self.zaps as usize) * 16 + self.sparks as usize) * 16 + self.charges as usize;
let millis = elapsed as f64 * MILLIS_PER_CHARGE;
let seconds = millis / 1000.;
let leftover_millis = millis % 1000.;
NaiveTime::from_num_seconds_from_midnight_opt(
seconds as u32,
(leftover_millis * 1.0e6) as u32,
)
.ok_or(Error::InvalidConversion)
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use chrono::{NaiveTime, Timelike};
use prisma::Rgb;
use crate::{Error, LightningTime, LightningTimeColors};
#[test]
fn convert_to_lightning() {
let real = NaiveTime::from_hms_opt(12, 0, 0).unwrap();
let lightning = LightningTime::from(real);
assert_eq!(lightning, LightningTime {
bolts: 0x8,
..Default::default()
});
assert_eq!(lightning.to_string(), "8~0~0|0");
assert_eq!(lightning.to_stripped_string(), "8~0~0");
assert_eq!(lightning.colors(), LightningTimeColors {
bolt: Rgb::new(0x80, 0xa1, 0x00),
zap: Rgb::new(0x32, 0x00, 0xd6),
spark: Rgb::new(0xf6, 0x85, 0x00),
});
}
#[test]
fn parse() {
assert!(LightningTime::from_str("f~3~a|8").is_ok());
assert!(LightningTime::from_str("f~3~a").is_ok());
assert!(LightningTime::from_str("f~~|").is_err());
}
#[test]
fn convert_to_real() {
let lightning = LightningTime {
bolts: 0x8,
..Default::default()
};
let naive: Result<NaiveTime, Error> = lightning.try_into();
assert!(naive.is_ok());
assert_eq!(naive.unwrap(), NaiveTime::from_hms_opt(12, 0, 0).unwrap());
let lightning = LightningTime {
bolts: 0x8,
charges: 0xa,
..Default::default()
};
let naive: Result<NaiveTime, Error> = lightning.try_into();
assert!(naive.is_ok());
assert_eq!(naive.unwrap().second(), NaiveTime::from_hms_opt(12, 0, 13).unwrap().second());
}
}