use crate::{DataError, DataErrorKind};
use core::cmp::Ordering;
use core::default::Default;
use core::fmt;
use core::fmt::Debug;
use core::hash::Hash;
use core::str::FromStr;
use icu_locid::extensions::unicode as unicode_ext;
use icu_locid::subtags::{Language, Region, Script, Variants};
use icu_locid::{LanguageIdentifier, Locale, SubtagOrderingResult};
use writeable::{LengthHint, Writeable};
#[cfg(feature = "experimental")]
use alloc::string::String;
#[cfg(feature = "experimental")]
use core::ops::Deref;
#[cfg(feature = "experimental")]
use tinystr::TinyAsciiStr;
#[cfg(doc)]
use icu_locid::subtags::Variant;
const AUXILIARY_KEY_SEPARATOR: u8 = b'+';
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
#[allow(clippy::exhaustive_structs)] pub struct DataRequest<'a> {
pub locale: &'a DataLocale,
pub metadata: DataRequestMetadata,
}
impl fmt::Display for DataRequest<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self.locale, f)
}
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[non_exhaustive]
pub struct DataRequestMetadata {
pub silent: bool,
}
#[derive(PartialEq, Clone, Default, Eq, Hash)]
pub struct DataLocale {
langid: LanguageIdentifier,
keywords: unicode_ext::Keywords,
#[cfg(feature = "experimental")]
aux: Option<AuxiliaryKeys>,
}
impl<'a> Default for &'a DataLocale {
fn default() -> Self {
static DEFAULT: DataLocale = DataLocale {
langid: LanguageIdentifier::UND,
keywords: unicode_ext::Keywords::new(),
#[cfg(feature = "experimental")]
aux: None,
};
&DEFAULT
}
}
impl fmt::Debug for DataLocale {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "DataLocale{{{self}}}")
}
}
impl Writeable for DataLocale {
fn write_to<W: core::fmt::Write + ?Sized>(&self, sink: &mut W) -> core::fmt::Result {
self.langid.write_to(sink)?;
if !self.keywords.is_empty() {
sink.write_str("-u-")?;
self.keywords.write_to(sink)?;
}
#[cfg(feature = "experimental")]
if let Some(aux) = self.aux.as_ref() {
sink.write_char(AuxiliaryKeys::separator() as char)?;
aux.write_to(sink)?;
}
Ok(())
}
fn writeable_length_hint(&self) -> LengthHint {
let mut length_hint = self.langid.writeable_length_hint();
if !self.keywords.is_empty() {
length_hint += self.keywords.writeable_length_hint() + 3;
}
#[cfg(feature = "experimental")]
if let Some(aux) = self.aux.as_ref() {
length_hint += aux.writeable_length_hint() + 1;
}
length_hint
}
fn write_to_string(&self) -> alloc::borrow::Cow<str> {
#[cfg_attr(not(feature = "experimental"), allow(unused_mut))]
let mut is_only_langid = self.keywords.is_empty();
#[cfg(feature = "experimental")]
{
is_only_langid = is_only_langid && self.aux.is_none();
}
if is_only_langid {
return self.langid.write_to_string();
}
let mut string =
alloc::string::String::with_capacity(self.writeable_length_hint().capacity());
let _ = self.write_to(&mut string);
alloc::borrow::Cow::Owned(string)
}
}
writeable::impl_display_with_writeable!(DataLocale);
impl From<LanguageIdentifier> for DataLocale {
fn from(langid: LanguageIdentifier) -> Self {
Self {
langid,
keywords: unicode_ext::Keywords::new(),
#[cfg(feature = "experimental")]
aux: None,
}
}
}
impl From<Locale> for DataLocale {
fn from(locale: Locale) -> Self {
Self {
langid: locale.id,
keywords: locale.extensions.unicode.keywords,
#[cfg(feature = "experimental")]
aux: None,
}
}
}
impl From<&LanguageIdentifier> for DataLocale {
fn from(langid: &LanguageIdentifier) -> Self {
Self {
langid: langid.clone(),
keywords: unicode_ext::Keywords::new(),
#[cfg(feature = "experimental")]
aux: None,
}
}
}
impl From<&Locale> for DataLocale {
fn from(locale: &Locale) -> Self {
Self {
langid: locale.id.clone(),
keywords: locale.extensions.unicode.keywords.clone(),
#[cfg(feature = "experimental")]
aux: None,
}
}
}
impl FromStr for DataLocale {
type Err = DataError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut aux_iter = s.splitn(2, AUXILIARY_KEY_SEPARATOR as char);
let Some(locale_str) = aux_iter.next() else {
return Err(DataErrorKind::KeyLocaleSyntax
.into_error()
.with_display_context(s));
};
let locale = Locale::from_str(locale_str).map_err(|e| {
DataErrorKind::KeyLocaleSyntax
.into_error()
.with_display_context(s)
.with_display_context(&e)
})?;
#[cfg_attr(not(feature = "experimental"), allow(unused_mut))]
let mut data_locale = DataLocale::from(locale);
#[cfg(feature = "experimental")]
if let Some(aux_str) = aux_iter.next() {
let aux = AuxiliaryKeys::from_str(aux_str)?;
data_locale.set_aux(aux);
}
if aux_iter.next().is_some() {
return Err(DataErrorKind::KeyLocaleSyntax
.into_error()
.with_display_context(s));
}
Ok(data_locale)
}
}
impl DataLocale {
pub fn strict_cmp(&self, other: &[u8]) -> Ordering {
let mut aux_iter = other.splitn(2, |b| *b == AUXILIARY_KEY_SEPARATOR);
let Some(locale_str) = aux_iter.next() else {
debug_assert!(other.is_empty());
return Ordering::Greater;
};
let aux_str = aux_iter.next();
let subtags = locale_str.split(|b| *b == b'-');
let mut subtag_result = self.langid.strict_cmp_iter(subtags);
if self.has_unicode_ext() {
let mut subtags = match subtag_result {
SubtagOrderingResult::Subtags(s) => s,
SubtagOrderingResult::Ordering(o) => return o,
};
match subtags.next() {
Some(b"u") => (),
Some(s) => return s.cmp(b"u").reverse(),
None => return Ordering::Greater,
}
subtag_result = self.keywords.strict_cmp_iter(subtags);
}
let has_more_subtags = match subtag_result {
SubtagOrderingResult::Subtags(mut s) => s.next().is_some(),
SubtagOrderingResult::Ordering(o) => return o,
};
match (has_more_subtags, self.get_aux(), aux_str) {
(false, None, None) => {
Ordering::Equal
}
(false, Some(self_aux), Some(other_aux)) => {
let aux_ordering = self_aux.as_bytes().cmp(other_aux);
if aux_ordering != Ordering::Equal {
return aux_ordering;
}
Ordering::Equal
}
(false, Some(_), None) => {
Ordering::Greater
}
(_, _, _) => {
Ordering::Less
}
}
}
}
impl DataLocale {
pub fn is_empty(&self) -> bool {
self == <&DataLocale>::default()
}
pub fn is_und(&self) -> bool {
self.langid == LanguageIdentifier::UND && self.keywords.is_empty()
}
pub fn is_langid_und(&self) -> bool {
self.langid == LanguageIdentifier::UND
}
pub fn get_langid(&self) -> LanguageIdentifier {
self.langid.clone()
}
#[inline]
pub fn set_langid(&mut self, lid: LanguageIdentifier) {
self.langid = lid;
}
pub fn into_locale(self) -> Locale {
let mut loc = Locale {
id: self.langid,
..Default::default()
};
loc.extensions.unicode.keywords = self.keywords;
loc
}
#[inline]
pub fn language(&self) -> Language {
self.langid.language
}
#[inline]
pub fn set_language(&mut self, language: Language) {
self.langid.language = language;
}
#[inline]
pub fn script(&self) -> Option<Script> {
self.langid.script
}
#[inline]
pub fn set_script(&mut self, script: Option<Script>) {
self.langid.script = script;
}
#[inline]
pub fn region(&self) -> Option<Region> {
self.langid.region
}
#[inline]
pub fn set_region(&mut self, region: Option<Region>) {
self.langid.region = region;
}
#[inline]
pub fn has_variants(&self) -> bool {
!self.langid.variants.is_empty()
}
#[inline]
pub fn set_variants(&mut self, variants: Variants) {
self.langid.variants = variants;
}
#[inline]
pub fn clear_variants(&mut self) -> Variants {
self.langid.variants.clear()
}
#[inline]
pub fn get_unicode_ext(&self, key: &unicode_ext::Key) -> Option<unicode_ext::Value> {
self.keywords.get(key).cloned()
}
#[inline]
pub fn has_unicode_ext(&self) -> bool {
!self.keywords.is_empty()
}
#[inline]
pub fn contains_unicode_ext(&self, key: &unicode_ext::Key) -> bool {
self.keywords.contains_key(key)
}
#[inline]
pub fn matches_unicode_ext(&self, key: &unicode_ext::Key, value: &unicode_ext::Value) -> bool {
self.keywords.get(key) == Some(value)
}
#[inline]
pub fn set_unicode_ext(
&mut self,
key: unicode_ext::Key,
value: unicode_ext::Value,
) -> Option<unicode_ext::Value> {
self.keywords.set(key, value)
}
#[inline]
pub fn remove_unicode_ext(&mut self, key: &unicode_ext::Key) -> Option<unicode_ext::Value> {
self.keywords.remove(key)
}
#[inline]
pub fn retain_unicode_ext<F>(&mut self, predicate: F)
where
F: FnMut(&unicode_ext::Key) -> bool,
{
self.keywords.retain_by_key(predicate)
}
#[cfg(feature = "experimental")]
pub fn get_aux(&self) -> Option<&AuxiliaryKeys> {
self.aux.as_ref()
}
#[cfg(not(feature = "experimental"))]
pub(crate) fn get_aux(&self) -> Option<&str> {
None
}
#[cfg(feature = "experimental")]
pub fn has_aux(&self) -> bool {
self.aux.is_some()
}
#[cfg(feature = "experimental")]
pub fn set_aux(&mut self, value: AuxiliaryKeys) -> Option<AuxiliaryKeys> {
self.aux.replace(value)
}
#[cfg(feature = "experimental")]
pub fn remove_aux(&mut self) -> Option<AuxiliaryKeys> {
self.aux.take()
}
}
#[derive(Debug, PartialEq, Clone, Eq, Hash)]
#[cfg(feature = "experimental")]
pub struct AuxiliaryKeys {
value: AuxiliaryKeysInner,
}
#[cfg(feature = "experimental")]
#[derive(Clone)]
enum AuxiliaryKeysInner {
Boxed(alloc::boxed::Box<str>),
Stack(TinyAsciiStr<23>),
}
#[cfg(feature = "experimental")]
impl Deref for AuxiliaryKeysInner {
type Target = str;
#[inline]
fn deref(&self) -> &Self::Target {
match self {
Self::Boxed(s) => s.deref(),
Self::Stack(s) => s.as_str(),
}
}
}
#[cfg(feature = "experimental")]
impl PartialEq for AuxiliaryKeysInner {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.deref() == other.deref()
}
}
#[cfg(feature = "experimental")]
impl Eq for AuxiliaryKeysInner {}
#[cfg(feature = "experimental")]
impl Debug for AuxiliaryKeysInner {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.deref().fmt(f)
}
}
#[cfg(feature = "experimental")]
impl Hash for AuxiliaryKeysInner {
#[inline]
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.deref().hash(state)
}
}
#[cfg(feature = "experimental")]
writeable::impl_display_with_writeable!(AuxiliaryKeys);
#[cfg(feature = "experimental")]
impl Writeable for AuxiliaryKeys {
fn write_to<W: fmt::Write + ?Sized>(&self, sink: &mut W) -> fmt::Result {
self.value.write_to(sink)
}
fn writeable_length_hint(&self) -> LengthHint {
self.value.writeable_length_hint()
}
fn write_to_string(&self) -> alloc::borrow::Cow<str> {
self.value.write_to_string()
}
}
#[cfg(feature = "experimental")]
impl FromStr for AuxiliaryKeys {
type Err = DataError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::try_from_str(s)
}
}
#[cfg(feature = "experimental")]
impl AuxiliaryKeys {
#[inline]
pub(crate) fn as_bytes(&self) -> &[u8] {
self.value.as_bytes()
}
pub fn try_from_iter<'a>(iter: impl IntoIterator<Item = &'a str>) -> Result<Self, DataError> {
let mut builder = String::new();
for item in iter {
if !item.is_empty()
&& item
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-'))
{
if !builder.is_empty() {
builder.push(AuxiliaryKeys::separator() as char);
}
builder.push_str(item)
} else {
return Err(DataErrorKind::KeyLocaleSyntax
.into_error()
.with_display_context(item));
}
}
if builder.len() <= 23 {
#[allow(clippy::unwrap_used)] Ok(Self {
value: AuxiliaryKeysInner::Stack(builder.parse().unwrap()),
})
} else {
Ok(Self {
value: AuxiliaryKeysInner::Boxed(builder.into()),
})
}
}
pub(crate) fn try_from_str(s: &str) -> Result<Self, DataError> {
if !s.is_empty()
&& s.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'+'))
{
if s.len() <= 23 {
#[allow(clippy::unwrap_used)] Ok(Self {
value: AuxiliaryKeysInner::Stack(s.parse().unwrap()),
})
} else {
Ok(Self {
value: AuxiliaryKeysInner::Boxed(s.into()),
})
}
} else {
Err(DataErrorKind::KeyLocaleSyntax
.into_error()
.with_display_context(s))
}
}
pub fn iter(&self) -> impl Iterator<Item = &str> + '_ {
self.value.split(Self::separator() as char)
}
#[inline]
pub const fn separator() -> u8 {
AUXILIARY_KEY_SEPARATOR
}
}
#[test]
fn test_data_locale_to_string() {
use icu_locid::locale;
struct TestCase {
pub locale: Locale,
pub aux: Option<&'static str>,
pub expected: &'static str,
}
for cas in [
TestCase {
locale: Locale::UND,
aux: None,
expected: "und",
},
TestCase {
locale: locale!("und-u-cu-gbp"),
aux: None,
expected: "und-u-cu-gbp",
},
TestCase {
locale: locale!("en-ZA-u-cu-gbp"),
aux: None,
expected: "en-ZA-u-cu-gbp",
},
#[cfg(feature = "experimental")]
TestCase {
locale: locale!("en-ZA-u-nu-arab"),
aux: Some("GBP"),
expected: "en-ZA-u-nu-arab+GBP",
},
] {
let mut data_locale = DataLocale::from(cas.locale);
#[cfg(feature = "experimental")]
if let Some(aux) = cas.aux {
data_locale.set_aux(aux.parse().unwrap());
}
writeable::assert_writeable_eq!(data_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: true,
},
TestCase {
input: "en-ZA-u-cu-gbp",
success: true,
},
TestCase {
input: "en...",
success: false,
},
#[cfg(feature = "experimental")]
TestCase {
input: "en-ZA-u-nu-arab+GBP",
success: true,
},
#[cfg(not(feature = "experimental"))]
TestCase {
input: "en-ZA-u-nu-arab+GBP",
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);
}
}