use crate::extensions::unicode as unicode_ext;
use crate::preferences::{extensions::unicode::keywords::RegionalSubdivision, LocalePreferences};
use crate::subtags::{Language, Region, Script, Subtag, Variant};
#[cfg(feature = "alloc")]
use crate::ParseError;
use crate::{LanguageIdentifier, Locale};
use core::cmp::Ordering;
use core::default::Default;
use core::fmt;
use core::hash::Hash;
#[cfg(feature = "alloc")]
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()
}
}
#[cfg(feature = "alloc")]
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]
#[cfg(feature = "alloc")]
pub fn try_from_str(s: &str) -> Result<Self, ParseError> {
Self::try_from_utf8(s.as_bytes())
}
#[cfg(feature = "alloc")]
pub fn try_from_utf8(code_units: &[u8]) -> Result<Self, ParseError> {
let locale = Locale::try_from_utf8(code_units)?;
if locale.id.variants.len() > 1
|| !locale.extensions.transform.is_empty()
|| !locale.extensions.private.is_empty()
|| !locale.extensions.other.is_empty()
|| !locale.extensions.unicode.attributes.is_empty()
{
return Err(ParseError::InvalidExtension);
}
let unicode_extensions_count = locale.extensions.unicode.keywords.iter().count();
if unicode_extensions_count != 0
&& (unicode_extensions_count != 1
|| !locale
.extensions
.unicode
.keywords
.contains_key(&RegionalSubdivision::UNICODE_EXTENSION_KEY))
{
return Err(ParseError::InvalidExtension);
}
Ok(locale.into())
}
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);
}
}