use core::fmt;
use core::str::FromStr;
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Lang {
language: [u8; 3],
region: [u8; 2],
}
impl Lang {
pub const EN_US: Lang = Lang::new("en-US");
pub const fn new(tag: &str) -> Lang {
let b = tag.as_bytes();
let mut language = [0u8; 3];
let mut i = 0;
while i < b.len() && b[i] != b'-' {
if i == 3 || !b[i].is_ascii_lowercase() {
panic!("a language tag is `ll`, `lll`, `ll-RR` or `lll-RR`");
}
language[i] = b[i];
i += 1;
}
if i < 2 {
panic!("a language tag is `ll`, `lll`, `ll-RR` or `lll-RR`");
}
let mut region = [0u8; 2];
if i < b.len() {
if b.len() - i != 3 || !b[i + 1].is_ascii_uppercase() || !b[i + 2].is_ascii_uppercase()
{
panic!("a language tag is `ll`, `lll`, `ll-RR` or `lll-RR`");
}
region[0] = b[i + 1];
region[1] = b[i + 2];
}
Lang { language, region }
}
pub fn parse(tag: &str) -> Option<Lang> {
let mut subtags = tag.split(['-', '_']);
let first = subtags.next()?.as_bytes();
if !(2..=3).contains(&first.len()) || !first.iter().all(u8::is_ascii_alphabetic) {
return None;
}
let mut language = [0u8; 3];
for (to, from) in language.iter_mut().zip(first) {
*to = from.to_ascii_lowercase();
}
let mut region = [0u8; 2];
for subtag in subtags {
let subtag = subtag.as_bytes();
match subtag.len() {
4 if subtag.iter().all(u8::is_ascii_alphabetic) => continue,
2 if subtag.iter().all(u8::is_ascii_alphabetic) => {
region[0] = subtag[0].to_ascii_uppercase();
region[1] = subtag[1].to_ascii_uppercase();
break;
}
_ => break,
}
}
Some(Lang { language, region })
}
#[cfg(feature = "koine03")]
pub(crate) fn from_parts(language: &str, region: Option<&str>) -> Option<Lang> {
let language_bytes = language.as_bytes();
if !(2..=3).contains(&language_bytes.len())
|| !language_bytes.iter().all(u8::is_ascii_alphabetic)
{
return None;
}
let mut lang = Lang {
language: [0; 3],
region: [0; 2],
};
for (to, from) in lang.language.iter_mut().zip(language_bytes) {
*to = from.to_ascii_lowercase();
}
if let Some(region) = region {
let &[a, b] = region.as_bytes() else {
return None;
};
if !a.is_ascii_alphabetic() || !b.is_ascii_alphabetic() {
return None;
}
lang.region = [a.to_ascii_uppercase(), b.to_ascii_uppercase()];
}
Some(lang)
}
pub fn language(&self) -> &str {
let len = self.language.iter().position(|&b| b == 0).unwrap_or(3);
ascii(&self.language[..len])
}
pub fn region(&self) -> Option<&str> {
if self.region[0] == 0 {
None
} else {
Some(ascii(&self.region))
}
}
pub fn primary(&self) -> Lang {
Lang {
language: self.language,
region: [0; 2],
}
}
}
fn ascii(bytes: &[u8]) -> &str {
core::str::from_utf8(bytes).unwrap_or_default()
}
impl fmt::Display for Lang {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.language())?;
if let Some(region) = self.region() {
write!(f, "-{region}")?;
}
Ok(())
}
}
impl fmt::Debug for Lang {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl FromStr for Lang {
type Err = ParseLangError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Lang::parse(s).ok_or(ParseLangError)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ParseLangError;
impl fmt::Display for ParseLangError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("not a language tag")
}
}
impl core::error::Error for ParseLangError {}
#[cfg(test)]
mod tests {
use std::format;
use std::string::ToString;
use super::Lang;
#[test]
fn new_and_display() {
assert_eq!(Lang::EN_US.to_string(), "en-US");
assert_eq!(Lang::new("fil").to_string(), "fil");
assert_eq!(Lang::new("fil-PH").language(), "fil");
assert_eq!(Lang::new("sv").region(), None);
assert_eq!(format!("{:?}", Lang::new("sv-SE")), "sv-SE");
}
#[test]
#[should_panic]
fn new_rejects_lowercase_region() {
let _ = Lang::new("sv-se");
}
#[test]
#[should_panic]
fn new_rejects_long_language() {
let _ = Lang::new("abcd");
}
#[test]
fn parse() {
assert_eq!(Lang::parse("en"), Some(Lang::new("en")));
assert_eq!(Lang::parse("EN-us"), Some(Lang::EN_US));
assert_eq!(Lang::parse("en_US"), Some(Lang::EN_US));
assert_eq!(Lang::parse("zh-Hant-TW"), Some(Lang::new("zh-TW")));
assert_eq!(Lang::parse("zh-Hant"), Some(Lang::new("zh")));
assert_eq!(Lang::parse("de-CH-1996"), Some(Lang::new("de-CH")));
assert_eq!(Lang::parse("en-u-ca-gregory"), Some(Lang::new("en")));
assert_eq!(Lang::parse("es-419"), Some(Lang::new("es")));
assert_eq!(Lang::parse(""), None);
assert_eq!(Lang::parse("x-private"), None);
assert_eq!(Lang::parse("e1"), None);
assert_eq!(Lang::parse("engl"), None);
}
#[test]
fn ordering_and_primary() {
assert!(Lang::new("en") < Lang::new("en-US"));
assert_eq!(Lang::new("sv-SE").primary(), Lang::new("sv"));
assert_eq!(Lang::new("sv").primary(), Lang::new("sv"));
}
}