use crate::error::{MunsellError, Result};
use regex::Regex;
use std::collections::HashMap;
use lazy_static::lazy_static;
lazy_static! {
static ref MUNSELL_GRAY_PATTERN: Regex = Regex::new(
r"(?i)(?:[0-9]*\.?[0-9]+\s*)?N\s*(?P<value>[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)(/\s*0*\.?0*)?"
).unwrap();
}
lazy_static! {
static ref MUNSELL_COLOUR_PATTERN: Regex = Regex::new(
r"(?i)(?P<hue>[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)\s*(?P<letter>BG|GY|YR|RP|PB|B|G|Y|R|P)\s*(?P<value>[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)\s*/\s*(?P<chroma>[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)"
).unwrap();
}
lazy_static! {
static ref MUNSELL_HUE_LETTER_CODES: HashMap<&'static str, u8> = {
let mut m = HashMap::new();
m.insert("BG", 2);
m.insert("GY", 4);
m.insert("YR", 6);
m.insert("RP", 8);
m.insert("PB", 10);
m.insert("B", 1);
m.insert("G", 3);
m.insert("Y", 5);
m.insert("R", 7);
m.insert("P", 9);
m
};
}
lazy_static! {
static ref CODE_TO_HUE_LETTER: HashMap<u8, &'static str> = {
let mut m = HashMap::new();
m.insert(1, "B");
m.insert(2, "BG");
m.insert(3, "G");
m.insert(4, "GY");
m.insert(5, "Y");
m.insert(6, "YR");
m.insert(7, "R");
m.insert(8, "RP");
m.insert(9, "P");
m.insert(10, "PB");
m
};
}
pub fn parse_munsell_colour(munsell_colour: &str) -> Result<[f64; 4]> {
if let Some(captures) = MUNSELL_GRAY_PATTERN.captures(munsell_colour) {
let value = captures.name("value")
.ok_or_else(|| MunsellError::InvalidNotation { notation: munsell_colour.to_string(), reason: "Invalid format".to_string() })?
.as_str()
.parse::<f64>()
.map_err(|_| MunsellError::InvalidNotation { notation: munsell_colour.to_string(), reason: "Invalid format".to_string() })?;
return Ok([f64::NAN, value, f64::NAN, f64::NAN]);
}
if let Some(captures) = MUNSELL_COLOUR_PATTERN.captures(munsell_colour) {
let hue = captures.name("hue")
.ok_or_else(|| MunsellError::InvalidNotation { notation: munsell_colour.to_string(), reason: "Invalid format".to_string() })?
.as_str()
.parse::<f64>()
.map_err(|_| MunsellError::InvalidNotation { notation: munsell_colour.to_string(), reason: "Invalid format".to_string() })?;
let value = captures.name("value")
.ok_or_else(|| MunsellError::InvalidNotation { notation: munsell_colour.to_string(), reason: "Invalid format".to_string() })?
.as_str()
.parse::<f64>()
.map_err(|_| MunsellError::InvalidNotation { notation: munsell_colour.to_string(), reason: "Invalid format".to_string() })?;
let chroma = captures.name("chroma")
.ok_or_else(|| MunsellError::InvalidNotation { notation: munsell_colour.to_string(), reason: "Invalid format".to_string() })?
.as_str()
.parse::<f64>()
.map_err(|_| MunsellError::InvalidNotation { notation: munsell_colour.to_string(), reason: "Invalid format".to_string() })?;
let letter = captures.name("letter")
.ok_or_else(|| MunsellError::InvalidNotation { notation: munsell_colour.to_string(), reason: "Invalid format".to_string() })?
.as_str()
.to_uppercase();
let code = *MUNSELL_HUE_LETTER_CODES.get(letter.as_str())
.ok_or_else(|| MunsellError::InvalidNotation { notation: munsell_colour.to_string(), reason: "Invalid format".to_string() })?;
return Ok([hue, value, chroma, code as f64]);
}
Err(MunsellError::InvalidNotation {
notation: munsell_colour.to_string(),
reason: "Does not match Munsell notation pattern".to_string()
})
}
pub fn munsell_colour_to_munsell_specification(munsell_colour: &str) -> Result<[f64; 4]> {
let spec = parse_munsell_colour(munsell_colour)?;
Ok(crate::munsell_color_science::normalise_munsell_specification(&spec))
}
pub fn munsell_specification_to_munsell_colour(
specification: &[f64; 4],
hue_decimals: usize,
value_decimals: usize,
chroma_decimals: usize,
) -> Result<String> {
let spec = crate::munsell_color_science::normalise_munsell_specification(specification);
let hue = spec[0];
let value = spec[1];
let chroma = spec[2];
let code = spec[3] as u8;
if crate::munsell_color_science::is_grey_munsell_colour(&spec) {
return Ok(format!("N {:.prec$}", value, prec = value_decimals));
} else {
let hue = round_to_decimals(hue, hue_decimals);
if !(0.0 <= hue && hue <= 10.0) {
return Err(MunsellError::InvalidNotation {
notation: format!("{:?}", spec),
reason: format!("Specification hue must be normalised to domain [0, 10], got {}", hue)
});
}
let value = round_to_decimals(value, value_decimals);
if !(0.0 <= value && value <= 10.0) {
return Err(MunsellError::InvalidNotation {
notation: format!("{:?}", spec),
reason: format!("Specification value must be normalised to domain [0, 10], got {}", value)
});
}
let chroma = round_to_decimals(chroma, chroma_decimals);
if !(2.0 <= chroma && chroma <= 50.0) {
return Err(MunsellError::InvalidNotation {
notation: format!("{:?}", spec),
reason: format!("Specification chroma must be normalised to domain [2, 50], got {}", chroma)
});
}
let hue_letter = CODE_TO_HUE_LETTER.get(&code)
.ok_or_else(|| MunsellError::InvalidNotation {
notation: format!("{:?}", spec),
reason: format!("Invalid hue code: {}", code)
})?;
Ok(format!(
"{:.h_prec$}{} {:.v_prec$}/{:.c_prec$}",
hue, hue_letter, value, chroma,
h_prec = hue_decimals,
v_prec = value_decimals,
c_prec = chroma_decimals
))
}
}
pub fn xyy_to_munsell_colour(
xyy: [f64; 3],
hue_decimals: usize,
value_decimals: usize,
chroma_decimals: usize,
) -> Result<String> {
let specification = crate::munsell_color_science::xyy_to_munsell_specification(xyy)?;
munsell_specification_to_munsell_colour(
&specification,
hue_decimals,
value_decimals,
chroma_decimals
)
}
pub fn munsell_colour_to_xyy(munsell_colour: &str) -> Result<[f64; 3]> {
let specification = munsell_colour_to_munsell_specification(munsell_colour)?;
crate::munsell_color_science::munsell_specification_to_xyy(&specification)
}
fn round_to_decimals(value: f64, decimals: usize) -> f64 {
let multiplier = 10_f64.powi(decimals as i32);
(value * multiplier).round() / multiplier
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_munsell_colour() {
let grey = parse_munsell_colour("N5.5").unwrap();
assert!(grey[0].is_nan());
assert_eq!(grey[1], 5.5);
assert!(grey[2].is_nan());
assert!(grey[3].is_nan());
let red = parse_munsell_colour("5R 4/10").unwrap();
assert_eq!(red[0], 5.0);
assert_eq!(red[1], 4.0);
assert_eq!(red[2], 10.0);
assert_eq!(red[3], 7.0);
let yellow_red = parse_munsell_colour("2.5YR 6.5/8.2").unwrap();
assert_eq!(yellow_red[0], 2.5);
assert_eq!(yellow_red[1], 6.5);
assert_eq!(yellow_red[2], 8.2);
assert_eq!(yellow_red[3], 6.0); }
#[test]
fn test_munsell_specification_to_munsell_colour() {
let grey_spec = [f64::NAN, 5.2, f64::NAN, f64::NAN];
let grey_str = munsell_specification_to_munsell_colour(&grey_spec, 1, 1, 1).unwrap();
assert_eq!(grey_str, "N 5.2");
let red_spec = [10.0, 2.0, 4.0, 7.0];
let red_str = munsell_specification_to_munsell_colour(&red_spec, 1, 1, 1).unwrap();
assert_eq!(red_str, "10.0R 2.0/4.0");
}
}