extern crate chrono;
extern crate thiserror;
use chrono::Datelike;
use std::convert::{TryFrom, TryInto};
#[derive(Debug, Eq, PartialEq)]
pub struct PersonaData {
pub model: ModelType,
pub date: chrono::NaiveDate,
pub personality: [char; 2],
pub manufacture_no: u16,
}
#[derive(Debug, thiserror::Error)]
pub enum ParseFailure {
#[error("String of invalid length or format passed")]
InvalidString,
#[error("Invalid model for value '{0}'")]
InvalidModel(char),
#[error("Invalid year '{0}'")]
InvalidYear(String),
#[error("Invalid month '{0}'")]
InvalidMonth(String),
#[error("Invalid manufacture number '{0}'")]
InvalidManufactureNo(String),
}
#[non_exhaustive]
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub enum ModelType {
Alpha,
Buisness,
Companion,
David,
Entertainment,
Fursona,
Guardian,
Healthcare,
Inherited,
KappaIotaTau,
Legal,
Matron,
Null,
Outsiders,
PersonaPlus,
QualityControl,
Retail,
Sensual,
Testing,
Utility,
Variable,
Experimental,
#[cfg_attr(not(feature = "unstable"), doc(hidden))]
ZSeries,
}
impl std::fmt::Display for ModelType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use ModelType as MT;
write!(
f,
"{}",
match self {
MT::Alpha => "Alpha",
MT::Buisness => "Buisness",
MT::Companion => "Companion",
MT::David => "David",
MT::Entertainment => "Entertainment",
MT::Fursona => "Fursona",
MT::Guardian => "Guardian",
MT::Healthcare => "Healthcare",
MT::Inherited => "Inherited",
MT::KappaIotaTau => "Kappa Iota Tau",
MT::Legal => "Legal",
MT::Matron => "Matron",
MT::Null => "Null",
MT::Outsiders => "Outsiders",
MT::PersonaPlus => "Persona+",
MT::QualityControl => "Quality Control",
MT::Retail => "Retail",
MT::Sensual => "Sensual",
MT::Testing => "Testing",
MT::Utility => "Utility",
MT::Variable => "Variable",
MT::Experimental => "Experimental",
MT::ZSeries => {
if cfg!(feature = "unstable") {
"Z Series"
} else {
""
}
}
}
)
}
}
impl TryFrom<char> for ModelType {
type Error = ParseFailure;
fn try_from(value: char) -> Result<Self, Self::Error> {
use ModelType as MT;
if !value.is_ascii_uppercase() {
return Err(ParseFailure::InvalidModel(value));
}
match value {
'A' => Ok(MT::Alpha),
'B' => Ok(MT::Buisness),
'C' => Ok(MT::Companion),
'D' => Ok(MT::David),
'E' => Ok(MT::Entertainment),
'F' => Ok(MT::Fursona),
'G' => Ok(MT::Guardian),
'H' => Ok(MT::Healthcare),
'I' => Ok(MT::Inherited),
'J' => Err(ParseFailure::InvalidModel(value)),
'K' => Ok(MT::KappaIotaTau),
'L' => Ok(MT::Legal),
'M' => Ok(MT::Matron),
'N' => Ok(MT::Null),
'O' => Ok(MT::Outsiders),
'P' => Ok(MT::PersonaPlus),
'Q' => Ok(MT::QualityControl),
'R' => Ok(MT::Retail),
'S' => Ok(MT::Sensual),
'T' => Ok(MT::Testing),
'U' => Ok(MT::Utility),
'V' => Ok(MT::Variable),
'W' => Err(ParseFailure::InvalidModel(value)),
'X' => Ok(MT::Experimental),
'Y' => Err(ParseFailure::InvalidModel(value)),
'Z' => {
if cfg!(feature = "unstable") {
Ok(MT::ZSeries)
} else {
Err(ParseFailure::InvalidModel(value))
}
}
_ => unreachable!(),
}
}
}
impl From<ModelType> for char {
fn from(v: ModelType) -> Self {
use ModelType as MT;
match v {
MT::Alpha => 'A',
MT::Buisness => 'B',
MT::Companion => 'C',
MT::David => 'D',
MT::Entertainment => 'E',
MT::Fursona => 'F',
MT::Guardian => 'G',
MT::Healthcare => 'H',
MT::Inherited => 'I',
MT::KappaIotaTau => 'K',
MT::Legal => 'L',
MT::Matron => 'M',
MT::Null => 'N',
MT::Outsiders => 'O',
MT::PersonaPlus => 'P',
MT::QualityControl => 'Q',
MT::Retail => 'R',
MT::Sensual => 'S',
MT::Testing => 'T',
MT::Utility => 'U',
MT::Variable => 'V',
MT::Experimental => 'X',
MT::ZSeries => {
if cfg!(feature = "unstable") {
'Z'
} else {
panic!()
}
}
}
}
}
impl std::str::FromStr for PersonaData {
type Err = ParseFailure;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.len() != 14 || !s.is_ascii() {
return Err(ParseFailure::InvalidString);
}
let mut iditer = s.split('-').map(|x| x.chars());
let model = if let Some(x) = iditer.next().map(|mut x| x.next()).flatten() {
x
} else {
return Err(ParseFailure::InvalidString);
};
let model: ModelType = model.try_into()?;
let year = if let Some(x) = iditer.next().map(|x| x.collect::<String>()) {
if let Ok(v) = x.parse::<u16>() {
if v > 999 {
return Err(ParseFailure::InvalidYear(v.to_string()));
} else {
v + 2000
}
} else {
return Err(ParseFailure::InvalidYear(x.into()));
}
} else {
return Err(ParseFailure::InvalidString);
};
let month = if let Some(x) = iditer.next().map(|x| x.collect::<String>()) {
if let Ok(v) = u16::from_str_radix(&x, 16) {
if v > 11 {
return Err(ParseFailure::InvalidMonth(v.to_string()));
} else {
v + 1
}
} else {
return Err(ParseFailure::InvalidMonth(x.into()));
}
} else {
return Err(ParseFailure::InvalidString);
};
let personality = if let Some(x) = iditer.next().map(|x| x.take(2)) {
if x.clone().count() == 2 {
let x = x.collect::<Vec<_>>();
[x[0], x[1]]
} else {
return Err(ParseFailure::InvalidString);
}
} else {
return Err(ParseFailure::InvalidString);
};
let manufacture_no = if let Some(x) = iditer.next().map(|x| x.collect::<String>()) {
if let Ok(v) = x.parse::<u16>() {
if v > 999 {
return Err(ParseFailure::InvalidManufactureNo(v.to_string()));
} else {
v
}
} else {
return Err(ParseFailure::InvalidManufactureNo(x.into()));
}
} else {
return Err(ParseFailure::InvalidString);
};
let date = chrono::NaiveDate::from_ymd(year as i32, month as u32, 1);
Ok(PersonaData {
model,
date,
personality,
manufacture_no,
})
}
}
impl std::fmt::Display for PersonaData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let model: char = self.model.into();
write!(
f,
"{}-{:03}-{:X}-{}{}-{:03}",
model,
self.date.year() - 2000,
self.date.month() - 1,
self.personality[0],
self.personality[1],
self.manufacture_no
)
}
}
#[cfg(test)]
mod tests {
use super::ModelType as MT;
use super::PersonaData;
#[test]
fn valid_parse_and_reassemble() {
let v = "A-223-0-QZ-029";
assert_eq!(
v.parse::<PersonaData>().unwrap(),
PersonaData {
model: MT::Alpha,
date: chrono::NaiveDate::from_ymd(2223, 1, 1),
personality: ['Q', 'Z'],
manufacture_no: 29,
}
);
assert_eq!(
format!(
"{}",
PersonaData {
model: MT::Alpha,
date: chrono::NaiveDate::from_ymd(2223, 1, 1),
personality: ['Q', 'Z'],
manufacture_no: 29,
}
),
v.to_string()
);
}
}