use crate::error::{Error, Result};
use crate::max::Max;
use crate::number::Number;
use crate::options::OptionSpec;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FormKind {
Cardinal,
Ordinal,
Currency,
}
impl FormKind {
pub const ALL: [FormKind; 3] = [FormKind::Cardinal, FormKind::Ordinal, FormKind::Currency];
#[must_use]
pub const fn name(self) -> &'static str {
match self {
FormKind::Cardinal => "cardinal",
FormKind::Ordinal => "ordinal",
FormKind::Currency => "currency",
}
}
}
impl fmt::Display for FormKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
impl std::str::FromStr for FormKind {
type Err = crate::options::ParseValueError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"cardinal" => Ok(FormKind::Cardinal),
"ordinal" => Ok(FormKind::Ordinal),
"currency" => Ok(FormKind::Currency),
_ => Err(crate::options::ParseValueError),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Kind {
Full,
Profile {
base: &'static str,
},
Alias {
target: &'static str,
},
}
pub type ConvertFn = fn(Number<'_>, &[(&str, &str)]) -> Result<String>;
#[derive(Debug, Clone, Copy)]
pub struct Form {
pub max: Max,
pub options: &'static [OptionSpec],
pub convert: ConvertFn,
}
impl Form {
pub fn convert<'a>(
&self,
value: impl Into<Number<'a>>,
options: &[(&str, &str)],
) -> Result<String> {
(self.convert)(value.into(), options)
}
#[must_use]
pub fn option(&self, name: &str) -> Option<&'static OptionSpec> {
self.options.iter().find(|o| o.name == name)
}
}
#[derive(Debug)]
pub struct Language {
pub code: &'static str,
pub name: &'static str,
pub kind: Kind,
pub cardinal: Option<Form>,
pub ordinal: Option<Form>,
pub currency: Option<Form>,
}
impl Language {
#[must_use]
pub fn form(&self, kind: FormKind) -> Option<&Form> {
match kind {
FormKind::Cardinal => self.cardinal.as_ref(),
FormKind::Ordinal => self.ordinal.as_ref(),
FormKind::Currency => self.currency.as_ref(),
}
}
pub fn forms(&self) -> impl Iterator<Item = (FormKind, &Form)> {
FormKind::ALL
.into_iter()
.filter_map(|k| self.form(k).map(|f| (k, f)))
}
pub fn convert<'a>(
&self,
kind: FormKind,
value: impl Into<Number<'a>>,
options: &[(&str, &str)],
) -> Result<String> {
let form = self.form(kind).ok_or(Error::UnsupportedForm {
language: self.code,
form: kind,
})?;
form.convert(value, options)
}
#[must_use]
pub fn primary_subtag(&self) -> &'static str {
self.code.split('-').next().unwrap_or(self.code)
}
#[must_use]
pub const fn is_alias(&self) -> bool {
matches!(self.kind, Kind::Alias { .. })
}
#[must_use]
pub fn resolve(&self) -> &'static Language {
match self.kind {
Kind::Full => language(self.code).expect("every language is registered"),
Kind::Profile { base } => language(base)
.expect("profile base is registered")
.resolve(),
Kind::Alias { target } => language(target)
.expect("alias target is registered")
.resolve(),
}
}
}
#[must_use]
pub fn languages() -> &'static [&'static Language] {
crate::lang::LANGUAGES
}
#[must_use]
pub fn language(code: &str) -> Option<&'static Language> {
let wanted: String = code
.trim()
.chars()
.map(|c| {
if c == '_' {
'-'
} else {
c.to_ascii_lowercase()
}
})
.collect();
languages()
.iter()
.copied()
.find(|l| l.code.eq_ignore_ascii_case(&wanted))
}
macro_rules! form {
(plain $f:path, $max:expr) => {{
fn convert(value: $crate::Number<'_>, options: &[(&str, &str)]) -> $crate::Result<String> {
$crate::options::ensure_no_options(options)?;
$f(value)
}
$crate::language::Form {
max: $max,
options: &[],
convert,
}
}};
(with_options $f:path, $opts:ty, $max:expr) => {{
fn convert(value: $crate::Number<'_>, options: &[(&str, &str)]) -> $crate::Result<String> {
let opts = <$opts>::from_pairs(options)?;
$f(value, &opts)
}
$crate::language::Form {
max: $max,
options: <$opts>::SPEC,
convert,
}
}};
}
pub(crate) use form;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn registry_is_sorted_and_unique() {
let codes: Vec<&str> = languages().iter().map(|l| l.code).collect();
let mut sorted = codes.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(
codes, sorted,
"registry must be sorted by code with no duplicates"
);
assert_eq!(codes.len(), 118);
}
#[test]
fn lookup_is_case_insensitive() {
assert_eq!(language("en-US").map(|l| l.code), Some("en-US"));
assert_eq!(language("EN-us").map(|l| l.code), Some("en-US"));
assert_eq!(language("zh_hans_cn").map(|l| l.code), Some("zh-Hans-CN"));
assert_eq!(language(" de ").map(|l| l.code), Some("de"));
assert!(language("xx").is_none());
}
#[test]
fn kinds_resolve_to_full_implementations() {
for lang in languages() {
let base = lang.resolve();
assert_eq!(
base.kind,
Kind::Full,
"{} resolves to {}, which is not a full implementation",
lang.code,
base.code
);
match lang.kind {
Kind::Full => {}
Kind::Profile { base } | Kind::Alias { target: base } => {
assert!(
language(base).is_some(),
"{} points at unknown {base}",
lang.code
);
}
}
}
}
}