use crate::ParseError;
use crate::extensions::unicode as unicode_ext;
use crate::parser::{
ParserMode, SubtagIterator,
parse_locale_with_single_variant_single_keyword_unicode_extension_from_iter,
};
use crate::preferences::{LocalePreferences, extensions::unicode::keywords::RegionalSubdivision};
use crate::subtags::{Language, Region, Script, Subtag, Variant};
use crate::{LanguageIdentifier, Locale};
use core::cmp::Ordering;
use core::default::Default;
use core::fmt;
use core::hash::Hash;
use core::str::FromStr;
#[derive(Clone, Copy)]
#[non_exhaustive]
pub struct DataLocale {
pub language: Language,
pub script: Option<Script>,
pub region: Option<Region>,
pub variant: Option<Variant>,
pub subdivision: Option<Subtag>,
}
impl PartialEq for DataLocale {
fn eq(&self, other: &Self) -> bool {
self.as_tuple() == other.as_tuple()
}
}
impl Eq for DataLocale {}
impl Hash for DataLocale {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.as_tuple().hash(state);
}
}
impl Default for DataLocale {
fn default() -> Self {
Self {
language: Language::UNKNOWN,
script: None,
region: None,
variant: None,
subdivision: None,
}
}
}
impl DataLocale {
pub const fn default() -> Self {
DataLocale {
language: Language::UNKNOWN,
script: None,
region: None,
variant: None,
subdivision: None,
}
}
}
impl Default for &DataLocale {
fn default() -> Self {
static DEFAULT: DataLocale = DataLocale::default();
&DEFAULT
}
}
impl fmt::Debug for DataLocale {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "DataLocale{{{self}}}")
}
}
impl_writeable_for_each_subtag_str_no_test!(DataLocale, selff, selff.script.is_none() && selff.region.is_none() && selff.variant.is_none() && selff.subdivision.is_none() => Some(selff.language.as_str()));
impl From<LanguageIdentifier> for DataLocale {
fn from(langid: LanguageIdentifier) -> Self {
Self::from(&langid)
}
}
impl From<Locale> for DataLocale {
fn from(locale: Locale) -> Self {
Self::from(&locale)
}
}
impl From<&LanguageIdentifier> for DataLocale {
fn from(langid: &LanguageIdentifier) -> Self {
Self {
language: langid.language,
script: langid.script,
region: langid.region,
variant: langid.variants.iter().copied().next(),
subdivision: None,
}
}
}
impl From<&Locale> for DataLocale {
fn from(locale: &Locale) -> Self {
LocalePreferences::from(locale).to_data_locale_language_priority()
}
}
impl From<(Language, Option<Script>, Option<Region>)> for DataLocale {
fn from((l, s, r): (Language, Option<Script>, Option<Region>)) -> Self {
Self::from_parts(
l,
s,
r.map(|r| unicode_ext::SubdivisionId::new(r, unicode_ext::SubdivisionSuffix::UNKNOWN)),
None,
)
}
}
impl FromStr for DataLocale {
type Err = ParseError;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::try_from_str(s)
}
}
impl DataLocale {
#[inline]
pub const fn try_from_str(s: &str) -> Result<Self, ParseError> {
Self::try_from_utf8(s.as_bytes())
}
pub const fn try_from_utf8(code_units: &[u8]) -> Result<Self, ParseError> {
let (language, script, region, variant, keyword) =
match parse_locale_with_single_variant_single_keyword_unicode_extension_from_iter(
SubtagIterator::new(code_units),
ParserMode::Locale,
) {
Ok(o) => o,
Err(e) => return Err(e),
};
let subdivision = if let Some((key, value)) = keyword {
if let Some(value) = value
&& let RegionalSubdivision::UNICODE_EXTENSION_KEY = key
{
let Ok(subdivision) = unicode_ext::SubdivisionId::try_from_subtag(value) else {
return Err(ParseError::InvalidExtension);
};
if let Some(region) = region
&& (region.into_raw()[0] != subdivision.region.into_raw()[0]
|| region.into_raw()[1] != subdivision.region.into_raw()[1]
|| region.into_raw()[2] != subdivision.region.into_raw()[2])
{
Some(unicode_ext::SubdivisionId::new(
region,
unicode_ext::SubdivisionSuffix::UNKNOWN,
))
} else {
Some(subdivision)
}
} else {
return Err(ParseError::InvalidExtension);
}
} else if let Some(region) = region {
Some(unicode_ext::SubdivisionId::new(
region,
unicode_ext::SubdivisionSuffix::UNKNOWN,
))
} else {
None
};
Ok(Self::from_parts(language, script, subdivision, variant))
}
pub(crate) fn for_each_subtag_str<E, F>(&self, f: &mut F) -> Result<(), E>
where
F: FnMut(&str) -> Result<(), E>,
{
f(self.language.as_str())?;
if let Some(ref script) = self.script {
f(script.as_str())?;
}
if let Some(ref region) = self.region {
f(region.as_str())?;
}
if let Some(ref single_variant) = self.variant {
f(single_variant.as_str())?;
}
if let Some(extensions) = self.extensions() {
extensions.for_each_subtag_str(f)?;
}
Ok(())
}
fn region_and_subdivision(&self) -> Option<unicode_ext::SubdivisionId> {
self.subdivision
.and_then(|s| unicode_ext::SubdivisionId::try_from_str(s.as_str()).ok())
.or_else(|| {
self.region.map(|region| unicode_ext::SubdivisionId {
region,
suffix: unicode_ext::SubdivisionSuffix::UNKNOWN,
})
})
}
fn as_tuple(
&self,
) -> (
Language,
Option<Script>,
Option<unicode_ext::SubdivisionId>,
Option<Variant>,
) {
(
self.language,
self.script,
self.region_and_subdivision(),
self.variant,
)
}
pub(crate) const fn from_parts(
language: Language,
script: Option<Script>,
region: Option<unicode_ext::SubdivisionId>,
variant: Option<Variant>,
) -> Self {
Self {
language,
script,
region: if let Some(r) = region {
Some(r.region)
} else {
None
},
variant,
subdivision: if let Some(r) = region {
Some(r.into_subtag())
} else {
None
},
}
}
pub fn total_cmp(&self, other: &Self) -> Ordering {
self.as_tuple().cmp(&other.as_tuple())
}
pub fn strict_cmp(&self, other: &[u8]) -> Ordering {
writeable::cmp_utf8(self, other)
}
pub fn is_unknown(&self) -> bool {
self.language.is_unknown()
&& self.script.is_none()
&& self.region.is_none()
&& self.variant.is_none()
&& self.subdivision.is_none()
}
pub fn into_locale(self) -> Locale {
Locale {
id: LanguageIdentifier {
language: self.language,
script: self.script,
region: self.region,
variants: self
.variant
.map(crate::subtags::Variants::from_variant)
.unwrap_or_default(),
},
extensions: self.extensions().unwrap_or_default(),
}
}
fn extensions(&self) -> Option<crate::extensions::Extensions> {
Some(crate::extensions::Extensions {
unicode: unicode_ext::Unicode {
keywords: unicode_ext::Keywords::new_single(
RegionalSubdivision::UNICODE_EXTENSION_KEY,
RegionalSubdivision(
self.region_and_subdivision()
.filter(|sd| !sd.suffix.is_unknown())?,
)
.into(),
),
..Default::default()
},
..Default::default()
})
}
}
#[test]
fn test_data_locale_to_string() {
struct TestCase {
pub locale: &'static str,
pub expected: &'static str,
}
for cas in [
TestCase {
locale: "und",
expected: "und",
},
TestCase {
locale: "und-u-sd-sdd",
expected: "und-SD-u-sd-sdd",
},
TestCase {
locale: "en-ZA-u-sd-zaa",
expected: "en-ZA-u-sd-zaa",
},
TestCase {
locale: "en-ZA-u-sd-sdd",
expected: "en-ZA",
},
] {
let locale = cas.locale.parse::<DataLocale>().unwrap();
writeable::assert_writeable_eq!(locale, cas.expected);
}
}
#[test]
fn test_data_locale_from_string() {
#[derive(Debug)]
struct TestCase {
pub input: &'static str,
pub success: bool,
}
for cas in [
TestCase {
input: "und",
success: true,
},
TestCase {
input: "und-u-cu-gbp",
success: false,
},
TestCase {
input: "en-ZA-u-sd-zaa",
success: true,
},
TestCase {
input: "en...",
success: false,
},
] {
let data_locale = match (DataLocale::from_str(cas.input), cas.success) {
(Ok(l), true) => l,
(Err(_), false) => {
continue;
}
(Ok(_), false) => {
panic!("DataLocale parsed but it was supposed to fail: {cas:?}");
}
(Err(_), true) => {
panic!("DataLocale was supposed to parse but it failed: {cas:?}");
}
};
writeable::assert_writeable_eq!(data_locale, cas.input);
}
}