diff --git a/src/display/cli.rs b/src/display/cli.rs index 3c90567..eaf762f 100644 --- a/src/display/cli.rs +++ b/src/display/cli.rs @@ -5,6 +5,7 @@ use alloc::format; use alloc::string::String; use owo_colors::{OwoColorize, Style}; +/// Formatter used to display a complete jaw in a CLI. pub struct ToothCliFormatter; impl ToothCliFormatter { @@ -77,12 +78,10 @@ impl ToothCliFormatter { }; (item_width + 1) * (item_count * 2 + 1) } +} - pub fn format_dentition( - notation: &NotationKind, - permanent: bool, - selected_value: &Option, - ) -> String { +impl ToothDisplay for ToothCliFormatter { + fn format(notation: &NotationKind, permanent: bool, selected_value: &Option) -> String { let jaw_len = ToothCliFormatter::calculate_separator_len(notation, permanent); let mut result = String::with_capacity((jaw_len * 4).into()); result.push_str(&ToothCliFormatter::format_jaw( @@ -104,9 +103,3 @@ impl ToothCliFormatter { result } } - -impl ToothDisplay for ToothCliFormatter { - fn format(notation: &NotationKind, permanent: bool, selected_value: &Option) -> String { - ToothCliFormatter::format_dentition(notation, permanent, selected_value) - } -} diff --git a/src/display/mod.rs b/src/display/mod.rs index b248f58..f88217d 100644 --- a/src/display/mod.rs +++ b/src/display/mod.rs @@ -1,14 +1,17 @@ +//! Formatters to display a complete jaw. + pub use crate::display::cli::*; use crate::NotationKind; use crate::Tooth; use alloc::string::String; mod cli; -pub enum JawKind { +enum JawKind { Top, Bottom, } +/// Trait needed to format a complete jaw pub trait ToothDisplay { fn format(notation: &NotationKind, permanent: bool, selected_value: &Option) -> String; } diff --git a/src/lib.rs b/src/lib.rs index 8901bb3..66cacab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,812 +1,29 @@ +/*! +This crate provides seralizing and deserializing from different text representation of dental notations. You can find the different available notation in the enum [NotationKind]. + +It primarily exposes one struct, [Tooth], which operates the different conversions. It also contains another module, [`display`], used to format tooth for different +# Usage + +This crate is [on crates.io](https://crates.io/crates/dental_notation) and can be +used by adding `dental_notation` to your dependencies in your project's `Cargo.toml`. + +```toml +[dependencies] +dental_notation = "1" +``` + +If you're using Rust 2015, then you'll also need to add it to your crate root: + +```rust +extern crate dental_notation; +``` + +*/ + #![no_std] extern crate alloc; -use crate::alloc::borrow::ToOwned; -use alloc::format; -use alloc::string::{String, ToString}; pub mod display; - -#[derive(Copy, Clone)] -pub enum NotationKind { - Iso, - Uns, - Alphanumeric, -} - -#[derive(PartialEq, Clone, Copy, Debug)] -pub enum QuadrantKind { - TopLeft, - TopRight, - BottomLeft, - BottomRight, -} - -pub enum ToothType { - Incisor, - Canine, - Premolar, - Molar, -} - -#[derive(PartialEq)] -pub struct Tooth { - quadrant: QuadrantKind, - number: u8, - permanent: bool, -} - -impl Tooth { - pub fn new(number: u8, quadrant: QuadrantKind, permanent: bool) -> Result { - if let Err(err) = Tooth::check_tooth_number(number, permanent) { - return Err(err); - } - - Ok(Tooth { - number, - quadrant, - permanent, - }) - } - - fn check_tooth_number(number: u8, permanent: bool) -> Result<(), String> { - if permanent && (number < 1 || number > 8) { - Err(format!( - "Permanent tooth {} should be in range [1; 8]", - number - )) - } else if !permanent && (number < 1 || number > 5) { - Err(format!( - "Primary tooth {} should be in range [1; 5]", - number - )) - } else { - Ok(()) - } - } - - pub fn quadrant_max(permanent: bool) -> u8 { - if permanent { - 8 - } else { - 5 - } - } - - pub fn from_iso(value: &str) -> Result { - if value.len() != 2 { - return Err(format!("{} is not a valid ISO dental notation", value)); - } - let mut char_iter = value.chars(); - let (quadrant, permanent) = match char_iter.next().unwrap() { - '1' => (QuadrantKind::TopLeft, true), - '2' => (QuadrantKind::TopRight, true), - '3' => (QuadrantKind::BottomRight, true), - '4' => (QuadrantKind::BottomLeft, true), - '5' => (QuadrantKind::TopLeft, false), - '6' => (QuadrantKind::TopRight, false), - '7' => (QuadrantKind::BottomRight, false), - '8' => (QuadrantKind::BottomLeft, false), - x => return Err(format!("Quadrant value {} not included in range [1; 8]", x)), - }; - - let tooth_string = char_iter.next().unwrap(); - let tooth_number_option = tooth_string.to_digit(10); - - if tooth_number_option.is_none() { - return Err(format!("{} is not a number", tooth_string)); - } - let number = tooth_number_option.unwrap() as u8; - if let Err(err) = Tooth::check_tooth_number(number, permanent) { - return Err(err); - } - Ok(Tooth { - quadrant, - permanent, - number, - }) - } - - pub fn to_iso(&self) -> String { - let quadrant_number: u8 = match (&self.quadrant, self.permanent) { - (QuadrantKind::TopLeft, true) => 1, - (QuadrantKind::TopRight, true) => 2, - (QuadrantKind::BottomLeft, true) => 4, - (QuadrantKind::BottomRight, true) => 3, - (QuadrantKind::TopLeft, false) => 5, - (QuadrantKind::TopRight, false) => 6, - (QuadrantKind::BottomLeft, false) => 8, - (QuadrantKind::BottomRight, false) => 7, - }; - quadrant_number.to_string() + &self.number.to_string() - } - - pub fn from_uns(value: &str) -> Result { - let number_value_option = value.parse::(); - let mut permanent = false; - let uns = if number_value_option.is_ok() { - permanent = true; - number_value_option.unwrap() - } else { - (value.chars().next().expect("No value available") as i32 - 64) as u8 - }; - - if permanent && (uns < 1 || uns > 32) { - return Err(format!( - "UNS permanent tooth has to be in range [1; 32] (currently {})", - uns - )); - } else if !permanent && (uns < 1 || uns > 20) { - return Err(format!("UNS primary tooth has to be in range [A; T]")); - } - let max = Tooth::quadrant_max(permanent); - let quadrant = match ((uns - 1) / max) + 1 { - 1 => QuadrantKind::TopLeft, - 2 => QuadrantKind::TopRight, - 3 => QuadrantKind::BottomRight, - 4 => QuadrantKind::BottomLeft, - _ => return Err(format!("UNS tooth value not in range")), - }; - let number = if quadrant == QuadrantKind::TopRight || quadrant == QuadrantKind::BottomLeft { - ((uns - 1) % max) + 1 - } else { - max - ((uns - 1) % max) - }; - if let Err(err) = Tooth::check_tooth_number(number, permanent) { - return Err(err); - } - Ok(Tooth { - quadrant, - number, - permanent, - }) - } - - pub fn to_uns(&self) -> String { - let max = Tooth::quadrant_max(self.permanent); - let value: u8 = match (&self.quadrant, self.number) { - (QuadrantKind::TopLeft, x) => max - x + 1, - (QuadrantKind::TopRight, x) => x + max, - (QuadrantKind::BottomRight, x) => (max - x + 1) + max * 2, - (QuadrantKind::BottomLeft, x) => x + max * 3, - }; - - if self.permanent { - format!("{:2}", value) - } else { - ((value + 64) as char).to_string() - } - } - - pub fn from_alphanumeric(value: &str) -> Result { - if value.len() != 3 { - return Err(format!( - "{} is not a valid alphanumeric dental notation", - value - )); - } - let quadrant = match &value[0..2] { - "UL" => QuadrantKind::TopLeft, - "UR" => QuadrantKind::TopRight, - "LR" => QuadrantKind::BottomRight, - "LL" => QuadrantKind::BottomLeft, - x => { - return Err(format!( - "Quadrant value {} not a valid value (accepted: ['UL', 'UR', 'LR', 'LL'])", - x - )) - } - }; - - let tooth_string = &value[2..3]; - - let (number, permanent) = match tooth_string { - "1" => (1, true), - "2" => (2, true), - "3" => (3, true), - "4" => (4, true), - "5" => (5, true), - "6" => (6, true), - "7" => (7, true), - "8" => (8, true), - "A" => (1, false), - "B" => (2, false), - "C" => (3, false), - "D" => (4, false), - "E" => (5, false), - x => { - return Err(format!( - "Number value {} not a valid value (accepted: [1; 8] and ['A'; 'E'])", - x - )) - } - }; - if let Err(err) = Tooth::check_tooth_number(number, permanent) { - return Err(err); - } - Ok(Tooth { - quadrant, - permanent, - number, - }) - } - - pub fn to_alphanumeric(&self) -> String { - let quadrant = match &self.quadrant { - QuadrantKind::TopLeft => "UL", - QuadrantKind::TopRight => "UR", - QuadrantKind::BottomRight => "LR", - QuadrantKind::BottomLeft => "LL", - }; - - let number = if self.permanent { - self.number.to_string() - } else { - ((self.number + 64) as char).to_string() - }; - quadrant.to_owned() + &number - } - - pub fn from(value: &str, from: &NotationKind) -> Result { - match from { - NotationKind::Iso => Tooth::from_iso(value), - NotationKind::Uns => Tooth::from_uns(value), - NotationKind::Alphanumeric => Tooth::from_alphanumeric(value), - } - } - pub fn to(&self, to: &NotationKind) -> String { - match to { - NotationKind::Iso => self.to_iso(), - NotationKind::Uns => self.to_uns(), - NotationKind::Alphanumeric => self.to_alphanumeric(), - } - } - - #[deprecated(since = "1.0.0", note = "Use 'to' and 'from' directly")] - pub fn convert(from: &NotationKind, to: &NotationKind, value: &str) -> Result { - let tooth_result = Tooth::from(value, from); - - match tooth_result { - Ok(tooth) => Ok(tooth.to(to)), - Err(err) => Err(err), - } - } - - pub fn get_type(&self) -> ToothType { - match (&self.number, &self.permanent) { - (1..=2, _) => ToothType::Incisor, - (3, _) => ToothType::Canine, - (4..=5, true) => ToothType::Premolar, - _ => ToothType::Molar, - } - } -} - -#[cfg(test)] -mod test { - use super::*; - - macro_rules! to { - ($name:ident, $func:ident, $quadrant:expr,$number:expr, $permanent:expr, $value:expr) => { - #[test] - fn $name() { - let tooth = Tooth { - quadrant: $quadrant, - number: $number, - permanent: $permanent, - }; - assert_eq!(tooth.$func(), $value); - } - }; - } - - macro_rules! from { - ($name:ident, $func:ident, $value:expr, $quadrant:expr,$number:expr, $permanent:expr) => { - #[test] - fn $name() { - let tooth = Tooth::$func($value).unwrap(); - - assert_eq!(tooth.quadrant, $quadrant); - assert_eq!(tooth.number, $number); - assert_eq!(tooth.permanent, $permanent); - } - }; - } - - macro_rules! from_fail { - ($name:ident, $func:ident, $value:expr) => { - #[test] - fn $name() { - assert!(Tooth::$func($value).is_err()); - } - }; - } - - to!(to_iso_11, to_iso, QuadrantKind::TopLeft, 1, true, "11"); - to!(to_iso_18, to_iso, QuadrantKind::TopLeft, 8, true, "18"); - to!(to_iso_21, to_iso, QuadrantKind::TopRight, 1, true, "21"); - to!(to_iso_28, to_iso, QuadrantKind::TopRight, 8, true, "28"); - to!(to_iso_31, to_iso, QuadrantKind::BottomRight, 1, true, "31"); - to!(to_iso_38, to_iso, QuadrantKind::BottomRight, 8, true, "38"); - to!(to_iso_41, to_iso, QuadrantKind::BottomLeft, 1, true, "41"); - to!(to_iso_48, to_iso, QuadrantKind::BottomLeft, 8, true, "48"); - to!(to_iso_51, to_iso, QuadrantKind::TopLeft, 1, false, "51"); - to!(to_iso_55, to_iso, QuadrantKind::TopLeft, 5, false, "55"); - to!(to_iso_61, to_iso, QuadrantKind::TopRight, 1, false, "61"); - to!(to_iso_65, to_iso, QuadrantKind::TopRight, 5, false, "65"); - to!(to_iso_71, to_iso, QuadrantKind::BottomRight, 1, false, "71"); - to!(to_iso_75, to_iso, QuadrantKind::BottomRight, 5, false, "75"); - to!(to_iso_81, to_iso, QuadrantKind::BottomLeft, 1, false, "81"); - to!(to_iso_85, to_iso, QuadrantKind::BottomLeft, 5, false, "85"); - - from!(from_iso_11, from_iso, "11", QuadrantKind::TopLeft, 1, true); - from!(from_iso_18, from_iso, "18", QuadrantKind::TopLeft, 8, true); - from!(from_iso_21, from_iso, "21", QuadrantKind::TopRight, 1, true); - from!(from_iso_28, from_iso, "28", QuadrantKind::TopRight, 8, true); - from!( - from_iso_31, - from_iso, - "31", - QuadrantKind::BottomRight, - 1, - true - ); - from!( - from_iso_38, - from_iso, - "38", - QuadrantKind::BottomRight, - 8, - true - ); - from!( - from_iso_41, - from_iso, - "41", - QuadrantKind::BottomLeft, - 1, - true - ); - from!( - from_iso_48, - from_iso, - "48", - QuadrantKind::BottomLeft, - 8, - true - ); - from!(from_iso_51, from_iso, "51", QuadrantKind::TopLeft, 1, false); - from!(from_iso_55, from_iso, "55", QuadrantKind::TopLeft, 5, false); - from!( - from_iso_61, - from_iso, - "61", - QuadrantKind::TopRight, - 1, - false - ); - from!( - from_iso_65, - from_iso, - "65", - QuadrantKind::TopRight, - 5, - false - ); - from!( - from_iso_71, - from_iso, - "71", - QuadrantKind::BottomRight, - 1, - false - ); - from!( - from_iso_75, - from_iso, - "75", - QuadrantKind::BottomRight, - 5, - false - ); - from!( - from_iso_81, - from_iso, - "81", - QuadrantKind::BottomLeft, - 1, - false - ); - from!( - from_iso_85, - from_iso, - "85", - QuadrantKind::BottomLeft, - 5, - false - ); - - from_fail!(from_iso_fail_10, from_iso, "10"); - from_fail!(from_iso_fail_19, from_iso, "19"); - from_fail!(from_iso_fail_20, from_iso, "20"); - from_fail!(from_iso_fail_29, from_iso, "29"); - from_fail!(from_iso_fail_30, from_iso, "30"); - from_fail!(from_iso_fail_39, from_iso, "39"); - from_fail!(from_iso_fail_40, from_iso, "40"); - from_fail!(from_iso_fail_49, from_iso, "49"); - from_fail!(from_iso_fail_50, from_iso, "50"); - from_fail!(from_iso_fail_56, from_iso, "56"); - from_fail!(from_iso_fail_60, from_iso, "60"); - from_fail!(from_iso_fail_66, from_iso, "66"); - from_fail!(from_iso_fail_70, from_iso, "70"); - from_fail!(from_iso_fail_76, from_iso, "76"); - from_fail!(from_iso_fail_80, from_iso, "80"); - from_fail!(from_iso_fail_86, from_iso, "86"); - - to!(to_uns_1, to_uns, QuadrantKind::TopLeft, 1, true, "8"); - to!(to_uns_8, to_uns, QuadrantKind::TopLeft, 8, true, "1"); - to!(to_uns_9, to_uns, QuadrantKind::TopRight, 1, true, "9"); - to!(to_uns_16, to_uns, QuadrantKind::TopRight, 8, true, "16"); - to!(to_uns_17, to_uns, QuadrantKind::BottomRight, 1, true, "24"); - to!(to_uns_24, to_uns, QuadrantKind::BottomRight, 8, true, "17"); - to!(to_uns_25, to_uns, QuadrantKind::BottomLeft, 1, true, "25"); - to!(to_uns_32, to_uns, QuadrantKind::BottomLeft, 8, true, "32"); - to!(to_uns_a, to_uns, QuadrantKind::TopLeft, 1, false, "E"); - to!(to_uns_e, to_uns, QuadrantKind::TopLeft, 5, false, "A"); - to!(to_uns_f, to_uns, QuadrantKind::TopRight, 1, false, "F"); - to!(to_uns_j, to_uns, QuadrantKind::TopRight, 5, false, "J"); - to!(to_uns_k, to_uns, QuadrantKind::BottomRight, 1, false, "O"); - to!(to_uns_o, to_uns, QuadrantKind::BottomRight, 5, false, "K"); - to!(to_uns_p, to_uns, QuadrantKind::BottomLeft, 1, false, "P"); - to!(to_uns_t, to_uns, QuadrantKind::BottomLeft, 5, false, "T"); - - from!(from_uns_8, from_uns, "8", QuadrantKind::TopLeft, 1, true); - from!(from_uns_1, from_uns, "1", QuadrantKind::TopLeft, 8, true); - from!(from_uns_9, from_uns, "9", QuadrantKind::TopRight, 1, true); - from!(from_uns_16, from_uns, "16", QuadrantKind::TopRight, 8, true); - from!( - from_uns_24, - from_uns, - "24", - QuadrantKind::BottomRight, - 1, - true - ); - from!( - from_uns_17, - from_uns, - "17", - QuadrantKind::BottomRight, - 8, - true - ); - from!( - from_uns_25, - from_uns, - "25", - QuadrantKind::BottomLeft, - 1, - true - ); - from!( - from_uns_32, - from_uns, - "32", - QuadrantKind::BottomLeft, - 8, - true - ); - from!(from_uns_e, from_uns, "E", QuadrantKind::TopLeft, 1, false); - from!(from_uns_a, from_uns, "A", QuadrantKind::TopLeft, 5, false); - from!(from_uns_f, from_uns, "F", QuadrantKind::TopRight, 1, false); - from!(from_uns_j, from_uns, "J", QuadrantKind::TopRight, 5, false); - from!( - from_uns_o, - from_uns, - "O", - QuadrantKind::BottomRight, - 1, - false - ); - from!( - from_uns_k, - from_uns, - "K", - QuadrantKind::BottomRight, - 5, - false - ); - from!( - from_uns_p, - from_uns, - "P", - QuadrantKind::BottomLeft, - 1, - false - ); - from!( - from_uns_t, - from_uns, - "T", - QuadrantKind::BottomLeft, - 5, - false - ); - - from_fail!(from_uns_fail_0, from_uns, "0"); - from_fail!(from_uns_fail_33, from_uns, "33"); - from_fail!(from_uns_fail_at, from_uns, "@"); - from_fail!(from_uns_fail_u, from_uns, "U"); - - to!( - to_alphanumeric_ul1, - to_alphanumeric, - QuadrantKind::TopLeft, - 1, - true, - "UL1" - ); - to!( - to_alphanumeric_ul8, - to_alphanumeric, - QuadrantKind::TopLeft, - 8, - true, - "UL8" - ); - to!( - to_alphanumeric_ur1, - to_alphanumeric, - QuadrantKind::TopRight, - 1, - true, - "UR1" - ); - to!( - to_alphanumeric_ur8, - to_alphanumeric, - QuadrantKind::TopRight, - 8, - true, - "UR8" - ); - to!( - to_alphanumeric_lr1, - to_alphanumeric, - QuadrantKind::BottomRight, - 1, - true, - "LR1" - ); - to!( - to_alphanumeric_lr8, - to_alphanumeric, - QuadrantKind::BottomRight, - 8, - true, - "LR8" - ); - to!( - to_alphanumeric_ll1, - to_alphanumeric, - QuadrantKind::BottomLeft, - 1, - true, - "LL1" - ); - to!( - to_alphanumeric_ll8, - to_alphanumeric, - QuadrantKind::BottomLeft, - 8, - true, - "LL8" - ); - to!( - to_alphanumeric_ula, - to_alphanumeric, - QuadrantKind::TopLeft, - 1, - false, - "ULA" - ); - to!( - to_alphanumeric_ule, - to_alphanumeric, - QuadrantKind::TopLeft, - 5, - false, - "ULE" - ); - to!( - to_alphanumeric_ura, - to_alphanumeric, - QuadrantKind::TopRight, - 1, - false, - "URA" - ); - to!( - to_alphanumeric_ure, - to_alphanumeric, - QuadrantKind::TopRight, - 5, - false, - "URE" - ); - to!( - to_alphanumeric_lra, - to_alphanumeric, - QuadrantKind::BottomRight, - 1, - false, - "LRA" - ); - to!( - to_alphanumeric_lre, - to_alphanumeric, - QuadrantKind::BottomRight, - 5, - false, - "LRE" - ); - to!( - to_alphanumeric_lla, - to_alphanumeric, - QuadrantKind::BottomLeft, - 1, - false, - "LLA" - ); - to!( - to_alphanumeric_lle, - to_alphanumeric, - QuadrantKind::BottomLeft, - 5, - false, - "LLE" - ); - from!( - from_alphanumeric_ul1, - from_alphanumeric, - "UL1", - QuadrantKind::TopLeft, - 1, - true - ); - from!( - from_alphanumeric_ul8, - from_alphanumeric, - "UL8", - QuadrantKind::TopLeft, - 8, - true - ); - from!( - from_alphanumeric_ur1, - from_alphanumeric, - "UR1", - QuadrantKind::TopRight, - 1, - true - ); - from!( - from_alphanumeric_ur8, - from_alphanumeric, - "UR8", - QuadrantKind::TopRight, - 8, - true - ); - from!( - from_alphanumeric_lr1, - from_alphanumeric, - "LR1", - QuadrantKind::BottomRight, - 1, - true - ); - from!( - from_alphanumeric_lr8, - from_alphanumeric, - "LR8", - QuadrantKind::BottomRight, - 8, - true - ); - from!( - from_alphanumeric_ll1, - from_alphanumeric, - "LL1", - QuadrantKind::BottomLeft, - 1, - true - ); - from!( - from_alphanumeric_ll8, - from_alphanumeric, - "LL8", - QuadrantKind::BottomLeft, - 8, - true - ); - from!( - from_alphanumeric_ula, - from_alphanumeric, - "ULA", - QuadrantKind::TopLeft, - 1, - false - ); - from!( - from_alphanumeric_ule, - from_alphanumeric, - "ULE", - QuadrantKind::TopLeft, - 5, - false - ); - from!( - from_alphanumeric_ura, - from_alphanumeric, - "URA", - QuadrantKind::TopRight, - 1, - false - ); - from!( - from_alphanumeric_ure, - from_alphanumeric, - "URE", - QuadrantKind::TopRight, - 5, - false - ); - from!( - from_alphanumeric_lra, - from_alphanumeric, - "LRA", - QuadrantKind::BottomRight, - 1, - false - ); - from!( - from_alphanumeric_lre, - from_alphanumeric, - "LRE", - QuadrantKind::BottomRight, - 5, - false - ); - from!( - from_alphanumeric_lla, - from_alphanumeric, - "LLA", - QuadrantKind::BottomLeft, - 1, - false - ); - from!( - from_alphanumeric_lle, - from_alphanumeric, - "LLE", - QuadrantKind::BottomLeft, - 5, - false - ); - from_fail!(from_alphanumeric_fail_ul0, from_alphanumeric, "UL0"); - from_fail!(from_alphanumeric_fail_ul9, from_alphanumeric, "UL9"); - from_fail!(from_alphanumeric_fail_ur0, from_alphanumeric, "UR0"); - from_fail!(from_alphanumeric_fail_ur9, from_alphanumeric, "UR9"); - from_fail!(from_alphanumeric_fail_lr0, from_alphanumeric, "LR0"); - from_fail!(from_alphanumeric_fail_lr9, from_alphanumeric, "LR9"); - from_fail!(from_alphanumeric_fail_ll0, from_alphanumeric, "LL0"); - from_fail!(from_alphanumeric_fail_ll9, from_alphanumeric, "LL9"); - from_fail!(from_alphanumeric_fail_ulat, from_alphanumeric, "UL@"); - from_fail!(from_alphanumeric_fail_ulf, from_alphanumeric, "ULF"); - from_fail!(from_alphanumeric_fail_urat, from_alphanumeric, "UR@"); - from_fail!(from_alphanumeric_fail_urf, from_alphanumeric, "URF"); - from_fail!(from_alphanumeric_fail_lrat, from_alphanumeric, "LR@"); - from_fail!(from_alphanumeric_fail_lrf, from_alphanumeric, "LRF"); - from_fail!(from_alphanumeric_fail_llat, from_alphanumeric, "LL@"); - from_fail!(from_alphanumeric_fail_llf, from_alphanumeric, "LLF"); -} +pub mod tooth; +#[doc(inline)] +pub use tooth::*; diff --git a/src/tooth.rs b/src/tooth.rs new file mode 100644 index 0000000..d4968c0 --- /dev/null +++ b/src/tooth.rs @@ -0,0 +1,815 @@ +use crate::alloc::borrow::ToOwned; +use alloc::format; +use alloc::string::{String, ToString}; + +#[derive(Copy, Clone)] +pub enum NotationKind { + /// [FDI / ISO 3950 notation](https://en.wikipedia.org/wiki/FDI_World_Dental_Federation_notation) + Iso, + /// [UNS (Universal Numbering System) notation](https://en.wikipedia.org/wiki/Universal_Numbering_System) + Uns, + /// [Alphanumeric notation](https://en.wikipedia.org/wiki/Dental_notation#Alphanumeric_notation) + Alphanumeric, +} + +#[derive(PartialEq, Clone, Copy, Debug)] +pub enum QuadrantKind { + TopLeft, + TopRight, + BottomLeft, + BottomRight, +} + +pub enum ToothType { + Incisor, + Canine, + Premolar, + Molar, +} + +/// Represent a tooth notation. It provides the different methods to convert from and to different notations in two different ways: +/// - [`from`](Tooth::from) and [`to`](Tooth::to), and providing the [NotationKind] as parameter, +/// - `from_xx` and `to_xx`, `xx` being one of the following: `iso`, `uns` and `alphanumeric. +#[derive(PartialEq)] +pub struct Tooth { + quadrant: QuadrantKind, + number: u8, + permanent: bool, +} + +impl Tooth { + pub fn new(number: u8, quadrant: QuadrantKind, permanent: bool) -> Result { + if let Err(err) = Tooth::check_tooth_number(number, permanent) { + return Err(err); + } + + Ok(Tooth { + number, + quadrant, + permanent, + }) + } + + fn check_tooth_number(number: u8, permanent: bool) -> Result<(), String> { + if permanent && (number < 1 || number > 8) { + Err(format!( + "Permanent tooth {} should be in range [1; 8]", + number + )) + } else if !permanent && (number < 1 || number > 5) { + Err(format!( + "Primary tooth {} should be in range [1; 5]", + number + )) + } else { + Ok(()) + } + } + + /// Get the maximum value for a tooth number (depending if it is a primary dentition or not) + pub fn quadrant_max(permanent: bool) -> u8 { + if permanent { + 8 + } else { + 5 + } + } + + pub fn from_iso(value: &str) -> Result { + if value.len() != 2 { + return Err(format!("{} is not a valid ISO dental notation", value)); + } + let mut char_iter = value.chars(); + let (quadrant, permanent) = match char_iter.next().unwrap() { + '1' => (QuadrantKind::TopLeft, true), + '2' => (QuadrantKind::TopRight, true), + '3' => (QuadrantKind::BottomRight, true), + '4' => (QuadrantKind::BottomLeft, true), + '5' => (QuadrantKind::TopLeft, false), + '6' => (QuadrantKind::TopRight, false), + '7' => (QuadrantKind::BottomRight, false), + '8' => (QuadrantKind::BottomLeft, false), + x => return Err(format!("Quadrant value {} not included in range [1; 8]", x)), + }; + + let tooth_string = char_iter.next().unwrap(); + let tooth_number_option = tooth_string.to_digit(10); + + if tooth_number_option.is_none() { + return Err(format!("{} is not a number", tooth_string)); + } + let number = tooth_number_option.unwrap() as u8; + if let Err(err) = Tooth::check_tooth_number(number, permanent) { + return Err(err); + } + Ok(Tooth { + quadrant, + permanent, + number, + }) + } + + pub fn to_iso(&self) -> String { + let quadrant_number: u8 = match (&self.quadrant, self.permanent) { + (QuadrantKind::TopLeft, true) => 1, + (QuadrantKind::TopRight, true) => 2, + (QuadrantKind::BottomLeft, true) => 4, + (QuadrantKind::BottomRight, true) => 3, + (QuadrantKind::TopLeft, false) => 5, + (QuadrantKind::TopRight, false) => 6, + (QuadrantKind::BottomLeft, false) => 8, + (QuadrantKind::BottomRight, false) => 7, + }; + quadrant_number.to_string() + &self.number.to_string() + } + + pub fn from_uns(value: &str) -> Result { + let number_value_option = value.parse::(); + let mut permanent = false; + let uns = if number_value_option.is_ok() { + permanent = true; + number_value_option.unwrap() + } else { + (value.chars().next().expect("No value available") as i32 - 64) as u8 + }; + + if permanent && (uns < 1 || uns > 32) { + return Err(format!( + "UNS permanent tooth has to be in range [1; 32] (currently {})", + uns + )); + } else if !permanent && (uns < 1 || uns > 20) { + return Err(format!("UNS primary tooth has to be in range [A; T]")); + } + let max = Tooth::quadrant_max(permanent); + let quadrant = match ((uns - 1) / max) + 1 { + 1 => QuadrantKind::TopLeft, + 2 => QuadrantKind::TopRight, + 3 => QuadrantKind::BottomRight, + 4 => QuadrantKind::BottomLeft, + _ => return Err(format!("UNS tooth value not in range")), + }; + let number = if quadrant == QuadrantKind::TopRight || quadrant == QuadrantKind::BottomLeft { + ((uns - 1) % max) + 1 + } else { + max - ((uns - 1) % max) + }; + if let Err(err) = Tooth::check_tooth_number(number, permanent) { + return Err(err); + } + Ok(Tooth { + quadrant, + number, + permanent, + }) + } + + pub fn to_uns(&self) -> String { + let max = Tooth::quadrant_max(self.permanent); + let value: u8 = match (&self.quadrant, self.number) { + (QuadrantKind::TopLeft, x) => max - x + 1, + (QuadrantKind::TopRight, x) => x + max, + (QuadrantKind::BottomRight, x) => (max - x + 1) + max * 2, + (QuadrantKind::BottomLeft, x) => x + max * 3, + }; + + if self.permanent { + format!("{:2}", value) + } else { + ((value + 64) as char).to_string() + } + } + + pub fn from_alphanumeric(value: &str) -> Result { + if value.len() != 3 { + return Err(format!( + "{} is not a valid alphanumeric dental notation", + value + )); + } + let quadrant = match &value[0..2] { + "UL" => QuadrantKind::TopLeft, + "UR" => QuadrantKind::TopRight, + "LR" => QuadrantKind::BottomRight, + "LL" => QuadrantKind::BottomLeft, + x => { + return Err(format!( + "Quadrant value {} not a valid value (accepted: ['UL', 'UR', 'LR', 'LL'])", + x + )) + } + }; + + let tooth_string = &value[2..3]; + + let (number, permanent) = match tooth_string { + "1" => (1, true), + "2" => (2, true), + "3" => (3, true), + "4" => (4, true), + "5" => (5, true), + "6" => (6, true), + "7" => (7, true), + "8" => (8, true), + "A" => (1, false), + "B" => (2, false), + "C" => (3, false), + "D" => (4, false), + "E" => (5, false), + x => { + return Err(format!( + "Number value {} not a valid value (accepted: [1; 8] and ['A'; 'E'])", + x + )) + } + }; + if let Err(err) = Tooth::check_tooth_number(number, permanent) { + return Err(err); + } + Ok(Tooth { + quadrant, + permanent, + number, + }) + } + + pub fn to_alphanumeric(&self) -> String { + let quadrant = match &self.quadrant { + QuadrantKind::TopLeft => "UL", + QuadrantKind::TopRight => "UR", + QuadrantKind::BottomRight => "LR", + QuadrantKind::BottomLeft => "LL", + }; + + let number = if self.permanent { + self.number.to_string() + } else { + ((self.number + 64) as char).to_string() + }; + quadrant.to_owned() + &number + } + + pub fn from(value: &str, from: &NotationKind) -> Result { + match from { + NotationKind::Iso => Tooth::from_iso(value), + NotationKind::Uns => Tooth::from_uns(value), + NotationKind::Alphanumeric => Tooth::from_alphanumeric(value), + } + } + pub fn to(&self, to: &NotationKind) -> String { + match to { + NotationKind::Iso => self.to_iso(), + NotationKind::Uns => self.to_uns(), + NotationKind::Alphanumeric => self.to_alphanumeric(), + } + } + + #[deprecated(since = "1.0.0", note = "Use 'to' and 'from' directly")] + pub fn convert(from: &NotationKind, to: &NotationKind, value: &str) -> Result { + let tooth_result = Tooth::from(value, from); + + match tooth_result { + Ok(tooth) => Ok(tooth.to(to)), + Err(err) => Err(err), + } + } + + pub fn get_type(&self) -> ToothType { + match (&self.number, &self.permanent) { + (1..=2, _) => ToothType::Incisor, + (3, _) => ToothType::Canine, + (4..=5, true) => ToothType::Premolar, + _ => ToothType::Molar, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + macro_rules! to { + ($name:ident, $func:ident, $quadrant:expr,$number:expr, $permanent:expr, $value:expr) => { + #[test] + fn $name() { + let tooth = Tooth { + quadrant: $quadrant, + number: $number, + permanent: $permanent, + }; + assert_eq!(tooth.$func(), $value); + } + }; + } + + macro_rules! from { + ($name:ident, $func:ident, $value:expr, $quadrant:expr,$number:expr, $permanent:expr) => { + #[test] + fn $name() { + let tooth = Tooth::$func($value).unwrap(); + + assert_eq!(tooth.quadrant, $quadrant); + assert_eq!(tooth.number, $number); + assert_eq!(tooth.permanent, $permanent); + } + }; + } + + macro_rules! from_fail { + ($name:ident, $func:ident, $value:expr) => { + #[test] + fn $name() { + assert!(Tooth::$func($value).is_err()); + } + }; + } + + to!(to_iso_11, to_iso, QuadrantKind::TopLeft, 1, true, "11"); + to!(to_iso_18, to_iso, QuadrantKind::TopLeft, 8, true, "18"); + to!(to_iso_21, to_iso, QuadrantKind::TopRight, 1, true, "21"); + to!(to_iso_28, to_iso, QuadrantKind::TopRight, 8, true, "28"); + to!(to_iso_31, to_iso, QuadrantKind::BottomRight, 1, true, "31"); + to!(to_iso_38, to_iso, QuadrantKind::BottomRight, 8, true, "38"); + to!(to_iso_41, to_iso, QuadrantKind::BottomLeft, 1, true, "41"); + to!(to_iso_48, to_iso, QuadrantKind::BottomLeft, 8, true, "48"); + to!(to_iso_51, to_iso, QuadrantKind::TopLeft, 1, false, "51"); + to!(to_iso_55, to_iso, QuadrantKind::TopLeft, 5, false, "55"); + to!(to_iso_61, to_iso, QuadrantKind::TopRight, 1, false, "61"); + to!(to_iso_65, to_iso, QuadrantKind::TopRight, 5, false, "65"); + to!(to_iso_71, to_iso, QuadrantKind::BottomRight, 1, false, "71"); + to!(to_iso_75, to_iso, QuadrantKind::BottomRight, 5, false, "75"); + to!(to_iso_81, to_iso, QuadrantKind::BottomLeft, 1, false, "81"); + to!(to_iso_85, to_iso, QuadrantKind::BottomLeft, 5, false, "85"); + + from!(from_iso_11, from_iso, "11", QuadrantKind::TopLeft, 1, true); + from!(from_iso_18, from_iso, "18", QuadrantKind::TopLeft, 8, true); + from!(from_iso_21, from_iso, "21", QuadrantKind::TopRight, 1, true); + from!(from_iso_28, from_iso, "28", QuadrantKind::TopRight, 8, true); + from!( + from_iso_31, + from_iso, + "31", + QuadrantKind::BottomRight, + 1, + true + ); + from!( + from_iso_38, + from_iso, + "38", + QuadrantKind::BottomRight, + 8, + true + ); + from!( + from_iso_41, + from_iso, + "41", + QuadrantKind::BottomLeft, + 1, + true + ); + from!( + from_iso_48, + from_iso, + "48", + QuadrantKind::BottomLeft, + 8, + true + ); + from!(from_iso_51, from_iso, "51", QuadrantKind::TopLeft, 1, false); + from!(from_iso_55, from_iso, "55", QuadrantKind::TopLeft, 5, false); + from!( + from_iso_61, + from_iso, + "61", + QuadrantKind::TopRight, + 1, + false + ); + from!( + from_iso_65, + from_iso, + "65", + QuadrantKind::TopRight, + 5, + false + ); + from!( + from_iso_71, + from_iso, + "71", + QuadrantKind::BottomRight, + 1, + false + ); + from!( + from_iso_75, + from_iso, + "75", + QuadrantKind::BottomRight, + 5, + false + ); + from!( + from_iso_81, + from_iso, + "81", + QuadrantKind::BottomLeft, + 1, + false + ); + from!( + from_iso_85, + from_iso, + "85", + QuadrantKind::BottomLeft, + 5, + false + ); + + from_fail!(from_iso_fail_10, from_iso, "10"); + from_fail!(from_iso_fail_19, from_iso, "19"); + from_fail!(from_iso_fail_20, from_iso, "20"); + from_fail!(from_iso_fail_29, from_iso, "29"); + from_fail!(from_iso_fail_30, from_iso, "30"); + from_fail!(from_iso_fail_39, from_iso, "39"); + from_fail!(from_iso_fail_40, from_iso, "40"); + from_fail!(from_iso_fail_49, from_iso, "49"); + from_fail!(from_iso_fail_50, from_iso, "50"); + from_fail!(from_iso_fail_56, from_iso, "56"); + from_fail!(from_iso_fail_60, from_iso, "60"); + from_fail!(from_iso_fail_66, from_iso, "66"); + from_fail!(from_iso_fail_70, from_iso, "70"); + from_fail!(from_iso_fail_76, from_iso, "76"); + from_fail!(from_iso_fail_80, from_iso, "80"); + from_fail!(from_iso_fail_86, from_iso, "86"); + + to!(to_uns_1, to_uns, QuadrantKind::TopLeft, 1, true, "8"); + to!(to_uns_8, to_uns, QuadrantKind::TopLeft, 8, true, "1"); + to!(to_uns_9, to_uns, QuadrantKind::TopRight, 1, true, "9"); + to!(to_uns_16, to_uns, QuadrantKind::TopRight, 8, true, "16"); + to!(to_uns_17, to_uns, QuadrantKind::BottomRight, 1, true, "24"); + to!(to_uns_24, to_uns, QuadrantKind::BottomRight, 8, true, "17"); + to!(to_uns_25, to_uns, QuadrantKind::BottomLeft, 1, true, "25"); + to!(to_uns_32, to_uns, QuadrantKind::BottomLeft, 8, true, "32"); + to!(to_uns_a, to_uns, QuadrantKind::TopLeft, 1, false, "E"); + to!(to_uns_e, to_uns, QuadrantKind::TopLeft, 5, false, "A"); + to!(to_uns_f, to_uns, QuadrantKind::TopRight, 1, false, "F"); + to!(to_uns_j, to_uns, QuadrantKind::TopRight, 5, false, "J"); + to!(to_uns_k, to_uns, QuadrantKind::BottomRight, 1, false, "O"); + to!(to_uns_o, to_uns, QuadrantKind::BottomRight, 5, false, "K"); + to!(to_uns_p, to_uns, QuadrantKind::BottomLeft, 1, false, "P"); + to!(to_uns_t, to_uns, QuadrantKind::BottomLeft, 5, false, "T"); + + from!(from_uns_8, from_uns, "8", QuadrantKind::TopLeft, 1, true); + from!(from_uns_1, from_uns, "1", QuadrantKind::TopLeft, 8, true); + from!(from_uns_9, from_uns, "9", QuadrantKind::TopRight, 1, true); + from!(from_uns_16, from_uns, "16", QuadrantKind::TopRight, 8, true); + from!( + from_uns_24, + from_uns, + "24", + QuadrantKind::BottomRight, + 1, + true + ); + from!( + from_uns_17, + from_uns, + "17", + QuadrantKind::BottomRight, + 8, + true + ); + from!( + from_uns_25, + from_uns, + "25", + QuadrantKind::BottomLeft, + 1, + true + ); + from!( + from_uns_32, + from_uns, + "32", + QuadrantKind::BottomLeft, + 8, + true + ); + from!(from_uns_e, from_uns, "E", QuadrantKind::TopLeft, 1, false); + from!(from_uns_a, from_uns, "A", QuadrantKind::TopLeft, 5, false); + from!(from_uns_f, from_uns, "F", QuadrantKind::TopRight, 1, false); + from!(from_uns_j, from_uns, "J", QuadrantKind::TopRight, 5, false); + from!( + from_uns_o, + from_uns, + "O", + QuadrantKind::BottomRight, + 1, + false + ); + from!( + from_uns_k, + from_uns, + "K", + QuadrantKind::BottomRight, + 5, + false + ); + from!( + from_uns_p, + from_uns, + "P", + QuadrantKind::BottomLeft, + 1, + false + ); + from!( + from_uns_t, + from_uns, + "T", + QuadrantKind::BottomLeft, + 5, + false + ); + + from_fail!(from_uns_fail_0, from_uns, "0"); + from_fail!(from_uns_fail_33, from_uns, "33"); + from_fail!(from_uns_fail_at, from_uns, "@"); + from_fail!(from_uns_fail_u, from_uns, "U"); + + to!( + to_alphanumeric_ul1, + to_alphanumeric, + QuadrantKind::TopLeft, + 1, + true, + "UL1" + ); + to!( + to_alphanumeric_ul8, + to_alphanumeric, + QuadrantKind::TopLeft, + 8, + true, + "UL8" + ); + to!( + to_alphanumeric_ur1, + to_alphanumeric, + QuadrantKind::TopRight, + 1, + true, + "UR1" + ); + to!( + to_alphanumeric_ur8, + to_alphanumeric, + QuadrantKind::TopRight, + 8, + true, + "UR8" + ); + to!( + to_alphanumeric_lr1, + to_alphanumeric, + QuadrantKind::BottomRight, + 1, + true, + "LR1" + ); + to!( + to_alphanumeric_lr8, + to_alphanumeric, + QuadrantKind::BottomRight, + 8, + true, + "LR8" + ); + to!( + to_alphanumeric_ll1, + to_alphanumeric, + QuadrantKind::BottomLeft, + 1, + true, + "LL1" + ); + to!( + to_alphanumeric_ll8, + to_alphanumeric, + QuadrantKind::BottomLeft, + 8, + true, + "LL8" + ); + to!( + to_alphanumeric_ula, + to_alphanumeric, + QuadrantKind::TopLeft, + 1, + false, + "ULA" + ); + to!( + to_alphanumeric_ule, + to_alphanumeric, + QuadrantKind::TopLeft, + 5, + false, + "ULE" + ); + to!( + to_alphanumeric_ura, + to_alphanumeric, + QuadrantKind::TopRight, + 1, + false, + "URA" + ); + to!( + to_alphanumeric_ure, + to_alphanumeric, + QuadrantKind::TopRight, + 5, + false, + "URE" + ); + to!( + to_alphanumeric_lra, + to_alphanumeric, + QuadrantKind::BottomRight, + 1, + false, + "LRA" + ); + to!( + to_alphanumeric_lre, + to_alphanumeric, + QuadrantKind::BottomRight, + 5, + false, + "LRE" + ); + to!( + to_alphanumeric_lla, + to_alphanumeric, + QuadrantKind::BottomLeft, + 1, + false, + "LLA" + ); + to!( + to_alphanumeric_lle, + to_alphanumeric, + QuadrantKind::BottomLeft, + 5, + false, + "LLE" + ); + from!( + from_alphanumeric_ul1, + from_alphanumeric, + "UL1", + QuadrantKind::TopLeft, + 1, + true + ); + from!( + from_alphanumeric_ul8, + from_alphanumeric, + "UL8", + QuadrantKind::TopLeft, + 8, + true + ); + from!( + from_alphanumeric_ur1, + from_alphanumeric, + "UR1", + QuadrantKind::TopRight, + 1, + true + ); + from!( + from_alphanumeric_ur8, + from_alphanumeric, + "UR8", + QuadrantKind::TopRight, + 8, + true + ); + from!( + from_alphanumeric_lr1, + from_alphanumeric, + "LR1", + QuadrantKind::BottomRight, + 1, + true + ); + from!( + from_alphanumeric_lr8, + from_alphanumeric, + "LR8", + QuadrantKind::BottomRight, + 8, + true + ); + from!( + from_alphanumeric_ll1, + from_alphanumeric, + "LL1", + QuadrantKind::BottomLeft, + 1, + true + ); + from!( + from_alphanumeric_ll8, + from_alphanumeric, + "LL8", + QuadrantKind::BottomLeft, + 8, + true + ); + from!( + from_alphanumeric_ula, + from_alphanumeric, + "ULA", + QuadrantKind::TopLeft, + 1, + false + ); + from!( + from_alphanumeric_ule, + from_alphanumeric, + "ULE", + QuadrantKind::TopLeft, + 5, + false + ); + from!( + from_alphanumeric_ura, + from_alphanumeric, + "URA", + QuadrantKind::TopRight, + 1, + false + ); + from!( + from_alphanumeric_ure, + from_alphanumeric, + "URE", + QuadrantKind::TopRight, + 5, + false + ); + from!( + from_alphanumeric_lra, + from_alphanumeric, + "LRA", + QuadrantKind::BottomRight, + 1, + false + ); + from!( + from_alphanumeric_lre, + from_alphanumeric, + "LRE", + QuadrantKind::BottomRight, + 5, + false + ); + from!( + from_alphanumeric_lla, + from_alphanumeric, + "LLA", + QuadrantKind::BottomLeft, + 1, + false + ); + from!( + from_alphanumeric_lle, + from_alphanumeric, + "LLE", + QuadrantKind::BottomLeft, + 5, + false + ); + from_fail!(from_alphanumeric_fail_ul0, from_alphanumeric, "UL0"); + from_fail!(from_alphanumeric_fail_ul9, from_alphanumeric, "UL9"); + from_fail!(from_alphanumeric_fail_ur0, from_alphanumeric, "UR0"); + from_fail!(from_alphanumeric_fail_ur9, from_alphanumeric, "UR9"); + from_fail!(from_alphanumeric_fail_lr0, from_alphanumeric, "LR0"); + from_fail!(from_alphanumeric_fail_lr9, from_alphanumeric, "LR9"); + from_fail!(from_alphanumeric_fail_ll0, from_alphanumeric, "LL0"); + from_fail!(from_alphanumeric_fail_ll9, from_alphanumeric, "LL9"); + from_fail!(from_alphanumeric_fail_ulat, from_alphanumeric, "UL@"); + from_fail!(from_alphanumeric_fail_ulf, from_alphanumeric, "ULF"); + from_fail!(from_alphanumeric_fail_urat, from_alphanumeric, "UR@"); + from_fail!(from_alphanumeric_fail_urf, from_alphanumeric, "URF"); + from_fail!(from_alphanumeric_fail_lrat, from_alphanumeric, "LR@"); + from_fail!(from_alphanumeric_fail_lrf, from_alphanumeric, "LRF"); + from_fail!(from_alphanumeric_fail_llat, from_alphanumeric, "LL@"); + from_fail!(from_alphanumeric_fail_llf, from_alphanumeric, "LLF"); +}