use crate::color::space;
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum ParseHexError {
#[error("{c:?} in the string {string:?} is not hexadecimal")]
NotHexadecimal { string: String, c: char },
#[error("No digits found")]
NoDigits,
#[error("Too many digits found in {string:?} (max: {max}, got: {got})")]
TooManyDigits { string: String, got: u32, max: u32 },
#[error("{string:?} is not a hex color: number of digits ({got}) not a multiple of 3")]
DigitsNotDivisibleBy3 { string: String, got: u32 },
}
pub fn parse(input: &str) -> Result<space::Rgb, ParseHexError> {
let color: String = input
.trim_start_matches('#')
.chars()
.filter(|&c| c != '_')
.map(|c| {
if c.is_ascii_hexdigit() {
Ok(c)
} else {
Err(ParseHexError::NotHexadecimal {
string: input.into(),
c,
})
}
})
.collect::<Result<_, ParseHexError>>()?;
if color.is_empty() {
return Err(ParseHexError::NoDigits);
}
if color.len() % 3 != 0 {
return Err(ParseHexError::DigitsNotDivisibleBy3 {
string: input.into(),
got: color.len() as u32,
});
}
if color.len() > 24 {
return Err(ParseHexError::TooManyDigits {
string: input.into(),
got: color.len() as u32,
max: 24,
});
}
let len = color.len() / 3;
let (r, gb) = color.split_at(len);
let (g, b) = gb.split_at(len);
let r = hex_to_f64(r);
let g = hex_to_f64(g);
let b = hex_to_f64(b);
Ok(scale_down(r, g, b, len))
}
fn hex_to_f64(src: &str) -> f64 {
u32::from_str_radix(src, 16).expect("Invalid hexadecimal number") as f64
}
fn scale_down(r: f64, g: f64, b: f64, len: usize) -> space::Rgb {
let up = match len {
1 => 0xF,
2 => 0xFF,
3 => 0xFFF,
4 => 0xFFFF,
5 => 0xFFFFF,
6 => 0xFFFFFF,
7 => 0xFFFFFFF,
8 => 0xFFFFFFFFu32,
_ => panic!("The number has more than 8 hex digits"),
} as f64;
let factor = up / 255.0;
space::Rgb::new(r / factor, g / factor, b / factor)
}
pub fn rgb_to_u32(rgb: space::Rgb) -> u32 {
let r = rgb.r.min(255.0).max(0.0).round() as u32;
let g = rgb.g.min(255.0).max(0.0).round() as u32;
let b = rgb.b.min(255.0).max(0.0).round() as u32;
(r << 16) + (g << 8) + b
}
#[cfg(test)]
mod tests {
use super::{parse, rgb_to_u32, space::Rgb};
fn rgb_to_string(rgb: Rgb) -> String {
format!("#{:06x}", rgb_to_u32(rgb))
}
#[test]
fn test_from_rgb() {
let rgb = Rgb {
r: 15.0,
g: 0.0,
b: 255.0,
};
assert_eq!(rgb_to_string(rgb), String::from("#0f00ff"));
}
#[test]
fn test_parse() {
assert_eq!(parse("000").unwrap(), Rgb::new(0.0, 0.0, 0.0));
assert_eq!(parse("FFF").unwrap(), Rgb::new(255.0, 255.0, 255.0));
assert_eq!(
parse("123456").unwrap(),
Rgb::new(0x12 as f64, 0x34 as f64, 0x56 as f64),
);
assert_eq!(
parse("22222222_44444444_66666666").unwrap(),
Rgb::new(0x22 as f64, 0x44 as f64, 0x66 as f64),
);
}
#[test]
fn test_parse_and_to_hex() {
assert_eq!(rgb_to_string(parse("224466").unwrap()), "#224466");
assert_eq!(rgb_to_string(parse("246").unwrap()), "#224466");
assert_eq!(rgb_to_string(parse("222_444_666").unwrap()), "#224466");
assert_eq!(rgb_to_string(parse("2222_4444_6666").unwrap()), "#224466");
assert_eq!(
rgb_to_string(parse("222222_444444_666666").unwrap()),
"#224466"
);
assert_eq!(
rgb_to_string(parse("12345678_3456789A_56789ABC").unwrap()),
"#123456"
);
}
}