Revision control
Copy as Markdown
Other Tools
// This file is part of ICU4X. For terms of use, please see the file
// called LICENSE at the top level of the ICU4X source tree
use crate::asciibyte::AsciiByte;
use crate::int_ops::{Aligned4, Aligned8};
use crate::TinyStrError;
use core::fmt;
use core::ops::Deref;
use core::str::{self, FromStr};
#[repr(transparent)]
#[derive(PartialEq, Eq, Ord, PartialOrd, Copy, Clone, Hash)]
pub struct TinyAsciiStr<const N: usize> {
bytes: [AsciiByte; N],
}
impl<const N: usize> TinyAsciiStr<N> {
/// Creates a `TinyAsciiStr<N>` from the given byte slice.
/// `bytes` may contain at most `N` non-null ASCII bytes.
pub const fn from_bytes(bytes: &[u8]) -> Result<Self, TinyStrError> {
Self::from_bytes_inner(bytes, 0, bytes.len(), false)
}
/// Creates a `TinyAsciiStr<N>` from a byte slice, replacing invalid bytes.
///
/// Null and non-ASCII bytes (i.e. those outside the range `0x01..=0x7F`)
/// will be replaced with the '?' character.
///
/// The input slice will be truncated if its length exceeds `N`.
pub const fn from_bytes_lossy(bytes: &[u8]) -> Self {
const QUESTION: u8 = b'?';
let mut out = [0; N];
let mut i = 0;
// Ord is not available in const, so no `.min(N)`
let len = if bytes.len() > N { N } else { bytes.len() };
// Indexing is protected by the len check above
#[allow(clippy::indexing_slicing)]
while i < len {
let b = bytes[i];
if b > 0 && b < 0x80 {
out[i] = b;
} else {
out[i] = QUESTION;
}
i += 1;
}
Self {
// SAFETY: `out` only contains ASCII bytes and has same size as `self.bytes`
bytes: unsafe { AsciiByte::to_ascii_byte_array(&out) },
}
}
/// Attempts to parse a fixed-length byte array to a `TinyAsciiStr`.
///
/// The byte array may contain trailing NUL bytes.
///
/// # Example
///
/// ```
/// use tinystr::tinystr;
/// use tinystr::TinyAsciiStr;
///
/// assert_eq!(
/// TinyAsciiStr::<3>::try_from_raw(*b"GB\0"),
/// Ok(tinystr!(3, "GB"))
/// );
/// assert_eq!(
/// TinyAsciiStr::<3>::try_from_raw(*b"USD"),
/// Ok(tinystr!(3, "USD"))
/// );
/// assert!(matches!(TinyAsciiStr::<3>::try_from_raw(*b"\0A\0"), Err(_)));
/// ```
pub const fn try_from_raw(raw: [u8; N]) -> Result<Self, TinyStrError> {
Self::from_bytes_inner(&raw, 0, N, true)
}
/// Equivalent to [`from_bytes(bytes[start..end])`](Self::from_bytes),
/// but callable in a `const` context (which range indexing is not).
pub const fn from_bytes_manual_slice(
bytes: &[u8],
start: usize,
end: usize,
) -> Result<Self, TinyStrError> {
Self::from_bytes_inner(bytes, start, end, false)
}
#[inline]
pub(crate) const fn from_bytes_inner(
bytes: &[u8],
start: usize,
end: usize,
allow_trailing_null: bool,
) -> Result<Self, TinyStrError> {
let len = end - start;
if len > N {
return Err(TinyStrError::TooLarge { max: N, len });
}
let mut out = [0; N];
let mut i = 0;
let mut found_null = false;
// Indexing is protected by TinyStrError::TooLarge
#[allow(clippy::indexing_slicing)]
while i < len {
let b = bytes[start + i];
if b == 0 {
found_null = true;
} else if b >= 0x80 {
return Err(TinyStrError::NonAscii);
} else if found_null {
// Error if there are contentful bytes after null
return Err(TinyStrError::ContainsNull);
}
out[i] = b;
i += 1;
}
if !allow_trailing_null && found_null {
// We found some trailing nulls, error
return Err(TinyStrError::ContainsNull);
}
Ok(Self {
// SAFETY: `out` only contains ASCII bytes and has same size as `self.bytes`
bytes: unsafe { AsciiByte::to_ascii_byte_array(&out) },
})
}
// TODO: This function shadows the FromStr trait. Rename?
#[inline]
pub const fn from_str(s: &str) -> Result<Self, TinyStrError> {
Self::from_bytes_inner(s.as_bytes(), 0, s.len(), false)
}
#[inline]
pub const fn as_str(&self) -> &str {
// as_bytes is valid utf8
unsafe { str::from_utf8_unchecked(self.as_bytes()) }
}
#[inline]
#[must_use]
pub const fn len(&self) -> usize {
if N <= 4 {
Aligned4::from_ascii_bytes(&self.bytes).len()
} else if N <= 8 {
Aligned8::from_ascii_bytes(&self.bytes).len()
} else {
let mut i = 0;
#[allow(clippy::indexing_slicing)] // < N is safe
while i < N && self.bytes[i] as u8 != AsciiByte::B0 as u8 {
i += 1
}
i
}
}
#[inline]
#[must_use]
pub const fn is_empty(&self) -> bool {
self.bytes[0] as u8 == AsciiByte::B0 as u8
}
#[inline]
#[must_use]
pub const fn as_bytes(&self) -> &[u8] {
// Safe because `self.bytes.as_slice()` pointer-casts to `&[u8]`,
// and changing the length of that slice to self.len() < N is safe.
unsafe {
core::slice::from_raw_parts(self.bytes.as_slice().as_ptr() as *const u8, self.len())
}
}
#[inline]
#[must_use]
pub const fn all_bytes(&self) -> &[u8; N] {
// SAFETY: `self.bytes` has same size as [u8; N]
unsafe { &*(self.bytes.as_ptr() as *const [u8; N]) }
}
#[inline]
#[must_use]
/// Resizes a `TinyAsciiStr<N>` to a `TinyAsciiStr<M>`.
///
/// If `M < len()` the string gets truncated, otherwise only the
/// memory representation changes.
pub const fn resize<const M: usize>(self) -> TinyAsciiStr<M> {
let mut bytes = [0; M];
let mut i = 0;
// Indexing is protected by the loop guard
#[allow(clippy::indexing_slicing)]
while i < M && i < N {
bytes[i] = self.bytes[i] as u8;
i += 1;
}
// `self.bytes` only contains ASCII bytes, with no null bytes between
// ASCII characters, so this also holds for `bytes`.
unsafe { TinyAsciiStr::from_bytes_unchecked(bytes) }
}
/// # Safety
/// Must be called with a bytes array made of valid ASCII bytes, with no null bytes
/// between ASCII characters
#[must_use]
pub const unsafe fn from_bytes_unchecked(bytes: [u8; N]) -> Self {
Self {
bytes: AsciiByte::to_ascii_byte_array(&bytes),
}
}
}
macro_rules! check_is {
($self:ident, $check_int:ident, $check_u8:ident) => {
if N <= 4 {
Aligned4::from_ascii_bytes(&$self.bytes).$check_int()
} else if N <= 8 {
Aligned8::from_ascii_bytes(&$self.bytes).$check_int()
} else {
let mut i = 0;
// Won't panic because self.bytes has length N
#[allow(clippy::indexing_slicing)]
while i < N && $self.bytes[i] as u8 != AsciiByte::B0 as u8 {
if !($self.bytes[i] as u8).$check_u8() {
return false;
}
i += 1;
}
true
}
};
($self:ident, $check_int:ident, !$check_u8_0_inv:ident, !$check_u8_1_inv:ident) => {
if N <= 4 {
Aligned4::from_ascii_bytes(&$self.bytes).$check_int()
} else if N <= 8 {
Aligned8::from_ascii_bytes(&$self.bytes).$check_int()
} else {
// Won't panic because N is > 8
if ($self.bytes[0] as u8).$check_u8_0_inv() {
return false;
}
let mut i = 1;
// Won't panic because self.bytes has length N
#[allow(clippy::indexing_slicing)]
while i < N && $self.bytes[i] as u8 != AsciiByte::B0 as u8 {
if ($self.bytes[i] as u8).$check_u8_1_inv() {
return false;
}
i += 1;
}
true
}
};
($self:ident, $check_int:ident, $check_u8_0_inv:ident, $check_u8_1_inv:ident) => {
if N <= 4 {
Aligned4::from_ascii_bytes(&$self.bytes).$check_int()
} else if N <= 8 {
Aligned8::from_ascii_bytes(&$self.bytes).$check_int()
} else {
// Won't panic because N is > 8
if !($self.bytes[0] as u8).$check_u8_0_inv() {
return false;
}
let mut i = 1;
// Won't panic because self.bytes has length N
#[allow(clippy::indexing_slicing)]
while i < N && $self.bytes[i] as u8 != AsciiByte::B0 as u8 {
if !($self.bytes[i] as u8).$check_u8_1_inv() {
return false;
}
i += 1;
}
true
}
};
}
impl<const N: usize> TinyAsciiStr<N> {
/// Checks if the value is composed of ASCII alphabetic characters:
///
/// * U+0041 'A' ..= U+005A 'Z', or
/// * U+0061 'a' ..= U+007A 'z'.
///
/// # Examples
///
/// ```
/// use tinystr::TinyAsciiStr;
///
/// let s1: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse.");
/// let s2: TinyAsciiStr<4> = "Te3t".parse().expect("Failed to parse.");
///
/// assert!(s1.is_ascii_alphabetic());
/// assert!(!s2.is_ascii_alphabetic());
/// ```
#[inline]
#[must_use]
pub const fn is_ascii_alphabetic(&self) -> bool {
check_is!(self, is_ascii_alphabetic, is_ascii_alphabetic)
}
/// Checks if the value is composed of ASCII alphanumeric characters:
///
/// * U+0041 'A' ..= U+005A 'Z', or
/// * U+0061 'a' ..= U+007A 'z', or
/// * U+0030 '0' ..= U+0039 '9'.
///
/// # Examples
///
/// ```
/// use tinystr::TinyAsciiStr;
///
/// let s1: TinyAsciiStr<4> = "A15b".parse().expect("Failed to parse.");
/// let s2: TinyAsciiStr<4> = "[3@w".parse().expect("Failed to parse.");
///
/// assert!(s1.is_ascii_alphanumeric());
/// assert!(!s2.is_ascii_alphanumeric());
/// ```
#[inline]
#[must_use]
pub const fn is_ascii_alphanumeric(&self) -> bool {
check_is!(self, is_ascii_alphanumeric, is_ascii_alphanumeric)
}
/// Checks if the value is composed of ASCII decimal digits:
///
/// * U+0030 '0' ..= U+0039 '9'.
///
/// # Examples
///
/// ```
/// use tinystr::TinyAsciiStr;
///
/// let s1: TinyAsciiStr<4> = "312".parse().expect("Failed to parse.");
/// let s2: TinyAsciiStr<4> = "3d".parse().expect("Failed to parse.");
///
/// assert!(s1.is_ascii_numeric());
/// assert!(!s2.is_ascii_numeric());
/// ```
#[inline]
#[must_use]
pub const fn is_ascii_numeric(&self) -> bool {
check_is!(self, is_ascii_numeric, is_ascii_digit)
}
/// Checks if the value is in ASCII lower case.
///
/// All letter characters are checked for case. Non-letter characters are ignored.
///
/// # Examples
///
/// ```
/// use tinystr::TinyAsciiStr;
///
/// let s1: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
/// let s2: TinyAsciiStr<4> = "test".parse().expect("Failed to parse.");
/// let s3: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
///
/// assert!(!s1.is_ascii_lowercase());
/// assert!(s2.is_ascii_lowercase());
/// assert!(s3.is_ascii_lowercase());
/// ```
#[inline]
#[must_use]
pub const fn is_ascii_lowercase(&self) -> bool {
check_is!(
self,
is_ascii_lowercase,
!is_ascii_uppercase,
!is_ascii_uppercase
)
}
/// Checks if the value is in ASCII title case.
///
/// This verifies that the first character is ASCII uppercase and all others ASCII lowercase.
/// Non-letter characters are ignored.
///
/// # Examples
///
/// ```
/// use tinystr::TinyAsciiStr;
///
/// let s1: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
/// let s2: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse.");
/// let s3: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
///
/// assert!(!s1.is_ascii_titlecase());
/// assert!(s2.is_ascii_titlecase());
/// assert!(s3.is_ascii_titlecase());
/// ```
#[inline]
#[must_use]
pub const fn is_ascii_titlecase(&self) -> bool {
check_is!(
self,
is_ascii_titlecase,
!is_ascii_lowercase,
!is_ascii_uppercase
)
}
/// Checks if the value is in ASCII upper case.
///
/// All letter characters are checked for case. Non-letter characters are ignored.
///
/// # Examples
///
/// ```
/// use tinystr::TinyAsciiStr;
///
/// let s1: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
/// let s2: TinyAsciiStr<4> = "TEST".parse().expect("Failed to parse.");
/// let s3: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
///
/// assert!(!s1.is_ascii_uppercase());
/// assert!(s2.is_ascii_uppercase());
/// assert!(!s3.is_ascii_uppercase());
/// ```
#[inline]
#[must_use]
pub const fn is_ascii_uppercase(&self) -> bool {
check_is!(
self,
is_ascii_uppercase,
!is_ascii_lowercase,
!is_ascii_lowercase
)
}
/// Checks if the value is composed of ASCII alphabetic lower case characters:
///
/// * U+0061 'a' ..= U+007A 'z',
///
/// # Examples
///
/// ```
/// use tinystr::TinyAsciiStr;
///
/// let s1: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse.");
/// let s2: TinyAsciiStr<4> = "Te3t".parse().expect("Failed to parse.");
/// let s3: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
/// let s4: TinyAsciiStr<4> = "test".parse().expect("Failed to parse.");
/// let s5: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
///
/// assert!(!s1.is_ascii_alphabetic_lowercase());
/// assert!(!s2.is_ascii_alphabetic_lowercase());
/// assert!(!s3.is_ascii_alphabetic_lowercase());
/// assert!(s4.is_ascii_alphabetic_lowercase());
/// assert!(!s5.is_ascii_alphabetic_lowercase());
/// ```
#[inline]
#[must_use]
pub const fn is_ascii_alphabetic_lowercase(&self) -> bool {
check_is!(
self,
is_ascii_alphabetic_lowercase,
is_ascii_lowercase,
is_ascii_lowercase
)
}
/// Checks if the value is composed of ASCII alphabetic, with the first character being ASCII uppercase, and all others ASCII lowercase.
///
/// # Examples
///
/// ```
/// use tinystr::TinyAsciiStr;
///
/// let s1: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse.");
/// let s2: TinyAsciiStr<4> = "Te3t".parse().expect("Failed to parse.");
/// let s3: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
/// let s4: TinyAsciiStr<4> = "test".parse().expect("Failed to parse.");
/// let s5: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
///
/// assert!(s1.is_ascii_alphabetic_titlecase());
/// assert!(!s2.is_ascii_alphabetic_titlecase());
/// assert!(!s3.is_ascii_alphabetic_titlecase());
/// assert!(!s4.is_ascii_alphabetic_titlecase());
/// assert!(!s5.is_ascii_alphabetic_titlecase());
/// ```
#[inline]
#[must_use]
pub const fn is_ascii_alphabetic_titlecase(&self) -> bool {
check_is!(
self,
is_ascii_alphabetic_titlecase,
is_ascii_uppercase,
is_ascii_lowercase
)
}
/// Checks if the value is composed of ASCII alphabetic upper case characters:
///
/// * U+0041 'A' ..= U+005A 'Z',
///
/// # Examples
///
/// ```
/// use tinystr::TinyAsciiStr;
///
/// let s1: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse.");
/// let s2: TinyAsciiStr<4> = "Te3t".parse().expect("Failed to parse.");
/// let s3: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
/// let s4: TinyAsciiStr<4> = "TEST".parse().expect("Failed to parse.");
/// let s5: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
///
/// assert!(!s1.is_ascii_alphabetic_uppercase());
/// assert!(!s2.is_ascii_alphabetic_uppercase());
/// assert!(!s3.is_ascii_alphabetic_uppercase());
/// assert!(s4.is_ascii_alphabetic_uppercase());
/// assert!(!s5.is_ascii_alphabetic_uppercase());
/// ```
#[inline]
#[must_use]
pub const fn is_ascii_alphabetic_uppercase(&self) -> bool {
check_is!(
self,
is_ascii_alphabetic_uppercase,
is_ascii_uppercase,
is_ascii_uppercase
)
}
}
macro_rules! to {
($self:ident, $to:ident, $later_char_to:ident $(,$first_char_to:ident)?) => {{
let mut i = 0;
if N <= 4 {
let aligned = Aligned4::from_ascii_bytes(&$self.bytes).$to().to_ascii_bytes();
// Won't panic because self.bytes has length N and aligned has length >= N
#[allow(clippy::indexing_slicing)]
while i < N {
$self.bytes[i] = aligned[i];
i += 1;
}
} else if N <= 8 {
let aligned = Aligned8::from_ascii_bytes(&$self.bytes).$to().to_ascii_bytes();
// Won't panic because self.bytes has length N and aligned has length >= N
#[allow(clippy::indexing_slicing)]
while i < N {
$self.bytes[i] = aligned[i];
i += 1;
}
} else {
// Won't panic because self.bytes has length N
#[allow(clippy::indexing_slicing)]
while i < N && $self.bytes[i] as u8 != AsciiByte::B0 as u8 {
// SAFETY: AsciiByte is repr(u8) and has same size as u8
unsafe {
$self.bytes[i] = core::mem::transmute::<u8, AsciiByte>(
($self.bytes[i] as u8).$later_char_to()
);
}
i += 1;
}
// SAFETY: AsciiByte is repr(u8) and has same size as u8
$(
$self.bytes[0] = unsafe {
core::mem::transmute::<u8, AsciiByte>(($self.bytes[0] as u8).$first_char_to())
};
)?
}
$self
}};
}
impl<const N: usize> TinyAsciiStr<N> {
/// Converts this type to its ASCII lower case equivalent in-place.
///
/// ASCII letters 'A' to 'Z' are mapped to 'a' to 'z', other characters are unchanged.
///
/// # Examples
///
/// ```
/// use tinystr::TinyAsciiStr;
///
/// let s1: TinyAsciiStr<4> = "TeS3".parse().expect("Failed to parse.");
///
/// assert_eq!(&*s1.to_ascii_lowercase(), "tes3");
/// ```
#[inline]
#[must_use]
pub const fn to_ascii_lowercase(mut self) -> Self {
to!(self, to_ascii_lowercase, to_ascii_lowercase)
}
/// Converts this type to its ASCII title case equivalent in-place.
///
/// The first character is converted to ASCII uppercase; the remaining characters
/// are converted to ASCII lowercase.
///
/// # Examples
///
/// ```
/// use tinystr::TinyAsciiStr;
///
/// let s1: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
///
/// assert_eq!(&*s1.to_ascii_titlecase(), "Test");
/// ```
#[inline]
#[must_use]
pub const fn to_ascii_titlecase(mut self) -> Self {
to!(
self,
to_ascii_titlecase,
to_ascii_lowercase,
to_ascii_uppercase
)
}
/// Converts this type to its ASCII upper case equivalent in-place.
///
/// ASCII letters 'a' to 'z' are mapped to 'A' to 'Z', other characters are unchanged.
///
/// # Examples
///
/// ```
/// use tinystr::TinyAsciiStr;
///
/// let s1: TinyAsciiStr<4> = "Tes3".parse().expect("Failed to parse.");
///
/// assert_eq!(&*s1.to_ascii_uppercase(), "TES3");
/// ```
#[inline]
#[must_use]
pub const fn to_ascii_uppercase(mut self) -> Self {
to!(self, to_ascii_uppercase, to_ascii_uppercase)
}
}
impl<const N: usize> fmt::Debug for TinyAsciiStr<N> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Debug::fmt(self.as_str(), f)
}
}
impl<const N: usize> fmt::Display for TinyAsciiStr<N> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(self.as_str(), f)
}
}
impl<const N: usize> Deref for TinyAsciiStr<N> {
type Target = str;
#[inline]
fn deref(&self) -> &str {
self.as_str()
}
}
impl<const N: usize> FromStr for TinyAsciiStr<N> {
type Err = TinyStrError;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_str(s)
}
}
impl<const N: usize> PartialEq<str> for TinyAsciiStr<N> {
fn eq(&self, other: &str) -> bool {
self.deref() == other
}
}
impl<const N: usize> PartialEq<&str> for TinyAsciiStr<N> {
fn eq(&self, other: &&str) -> bool {
self.deref() == *other
}
}
#[cfg(feature = "alloc")]
impl<const N: usize> PartialEq<alloc::string::String> for TinyAsciiStr<N> {
fn eq(&self, other: &alloc::string::String) -> bool {
self.deref() == other.deref()
}
}
#[cfg(feature = "alloc")]
impl<const N: usize> PartialEq<TinyAsciiStr<N>> for alloc::string::String {
fn eq(&self, other: &TinyAsciiStr<N>) -> bool {
self.deref() == other.deref()
}
}
#[cfg(test)]
mod test {
use super::*;
use rand::distributions::Distribution;
use rand::distributions::Standard;
use rand::rngs::SmallRng;
use rand::seq::SliceRandom;
use rand::SeedableRng;
const STRINGS: [&str; 26] = [
"Latn",
"laTn",
"windows",
"AR",
"Hans",
"macos",
"AT",
"infiniband",
"FR",
"en",
"Cyrl",
"FromIntegral",
"NO",
"419",
"MacintoshOSX2019",
"a3z",
"A3z",
"A3Z",
"a3Z",
"3A",
"3Z",
"3a",
"3z",
"@@[`{",
"UK",
"E12",
];
fn gen_strings(num_strings: usize, allowed_lengths: &[usize]) -> Vec<String> {
let mut rng = SmallRng::seed_from_u64(2022);
// Need to do this in 2 steps since the RNG is needed twice
let string_lengths = core::iter::repeat_with(|| *allowed_lengths.choose(&mut rng).unwrap())
.take(num_strings)
.collect::<Vec<usize>>();
string_lengths
.iter()
.map(|len| {
Standard
.sample_iter(&mut rng)
.filter(|b: &u8| *b > 0 && *b < 0x80)
.take(*len)
.collect::<Vec<u8>>()
})
.map(|byte_vec| String::from_utf8(byte_vec).expect("All ASCII"))
.collect()
}
fn check_operation<T, F1, F2, const N: usize>(reference_f: F1, tinystr_f: F2)
where
F1: Fn(&str) -> T,
F2: Fn(TinyAsciiStr<N>) -> T,
T: core::fmt::Debug + core::cmp::PartialEq,
{
for s in STRINGS
.into_iter()
.map(str::to_owned)
.chain(gen_strings(100, &[3, 4, 5, 8, 12]))
{
let t = match TinyAsciiStr::<N>::from_str(&s) {
Ok(t) => t,
Err(TinyStrError::TooLarge { .. }) => continue,
Err(e) => panic!("{}", e),
};
let expected = reference_f(&s);
let actual = tinystr_f(t);
assert_eq!(expected, actual, "TinyAsciiStr<{N}>: {s:?}");
}
}
#[test]
fn test_is_ascii_alphabetic() {
fn check<const N: usize>() {
check_operation(
|s| s.chars().all(|c| c.is_ascii_alphabetic()),
|t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphabetic(&t),
)
}
check::<2>();
check::<3>();
check::<4>();
check::<5>();
check::<8>();
check::<16>();
}
#[test]
fn test_is_ascii_alphanumeric() {
fn check<const N: usize>() {
check_operation(
|s| s.chars().all(|c| c.is_ascii_alphanumeric()),
|t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphanumeric(&t),
)
}
check::<2>();
check::<3>();
check::<4>();
check::<5>();
check::<8>();
check::<16>();
}
#[test]
fn test_is_ascii_numeric() {
fn check<const N: usize>() {
check_operation(
|s| s.chars().all(|c| c.is_ascii_digit()),
|t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_numeric(&t),
)
}
check::<2>();
check::<3>();
check::<4>();
check::<5>();
check::<8>();
check::<16>();
}
#[test]
fn test_is_ascii_lowercase() {
fn check<const N: usize>() {
check_operation(
|s| {
s == TinyAsciiStr::<16>::from_str(s)
.unwrap()
.to_ascii_lowercase()
.as_str()
},
|t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_lowercase(&t),
)
}
check::<2>();
check::<3>();
check::<4>();
check::<5>();
check::<8>();
check::<16>();
}
#[test]
fn test_is_ascii_titlecase() {
fn check<const N: usize>() {
check_operation(
|s| {
s == TinyAsciiStr::<16>::from_str(s)
.unwrap()
.to_ascii_titlecase()
.as_str()
},
|t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_titlecase(&t),
)
}
check::<2>();
check::<3>();
check::<4>();
check::<5>();
check::<8>();
check::<16>();
}
#[test]
fn test_is_ascii_uppercase() {
fn check<const N: usize>() {
check_operation(
|s| {
s == TinyAsciiStr::<16>::from_str(s)
.unwrap()
.to_ascii_uppercase()
.as_str()
},
|t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_uppercase(&t),
)
}
check::<2>();
check::<3>();
check::<4>();
check::<5>();
check::<8>();
check::<16>();
}
#[test]
fn test_is_ascii_alphabetic_lowercase() {
fn check<const N: usize>() {
check_operation(
|s| {
// Check alphabetic
s.chars().all(|c| c.is_ascii_alphabetic()) &&
// Check lowercase
s == TinyAsciiStr::<16>::from_str(s)
.unwrap()
.to_ascii_lowercase()
.as_str()
},
|t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphabetic_lowercase(&t),
)
}
check::<2>();
check::<3>();
check::<4>();
check::<5>();
check::<8>();
check::<16>();
}
#[test]
fn test_is_ascii_alphabetic_titlecase() {
fn check<const N: usize>() {
check_operation(
|s| {
// Check alphabetic
s.chars().all(|c| c.is_ascii_alphabetic()) &&
// Check titlecase
s == TinyAsciiStr::<16>::from_str(s)
.unwrap()
.to_ascii_titlecase()
.as_str()
},
|t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphabetic_titlecase(&t),
)
}
check::<2>();
check::<3>();
check::<4>();
check::<5>();
check::<8>();
check::<16>();
}
#[test]
fn test_is_ascii_alphabetic_uppercase() {
fn check<const N: usize>() {
check_operation(
|s| {
// Check alphabetic
s.chars().all(|c| c.is_ascii_alphabetic()) &&
// Check uppercase
s == TinyAsciiStr::<16>::from_str(s)
.unwrap()
.to_ascii_uppercase()
.as_str()
},
|t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphabetic_uppercase(&t),
)
}
check::<2>();
check::<3>();
check::<4>();
check::<5>();
check::<8>();
check::<16>();
}
#[test]
fn test_to_ascii_lowercase() {
fn check<const N: usize>() {
check_operation(
|s| {
s.chars()
.map(|c| c.to_ascii_lowercase())
.collect::<String>()
},
|t: TinyAsciiStr<N>| TinyAsciiStr::to_ascii_lowercase(t).as_str().to_owned(),
)
}
check::<2>();
check::<3>();
check::<4>();
check::<5>();
check::<8>();
check::<16>();
}
#[test]
fn test_to_ascii_titlecase() {
fn check<const N: usize>() {
check_operation(
|s| {
let mut r = s
.chars()
.map(|c| c.to_ascii_lowercase())
.collect::<String>();
// Safe because the string is nonempty and an ASCII string
unsafe { r.as_bytes_mut()[0].make_ascii_uppercase() };
r
},
|t: TinyAsciiStr<N>| TinyAsciiStr::to_ascii_titlecase(t).as_str().to_owned(),
)
}
check::<2>();
check::<3>();
check::<4>();
check::<5>();
check::<8>();
check::<16>();
}
#[test]
fn test_to_ascii_uppercase() {
fn check<const N: usize>() {
check_operation(
|s| {
s.chars()
.map(|c| c.to_ascii_uppercase())
.collect::<String>()
},
|t: TinyAsciiStr<N>| TinyAsciiStr::to_ascii_uppercase(t).as_str().to_owned(),
)
}
check::<2>();
check::<3>();
check::<4>();
check::<5>();
check::<8>();
check::<16>();
}
#[test]
fn lossy_constructor() {
assert_eq!(TinyAsciiStr::<4>::from_bytes_lossy(b"").as_str(), "");
assert_eq!(
TinyAsciiStr::<4>::from_bytes_lossy(b"oh\0o").as_str(),
"oh?o"
);
assert_eq!(TinyAsciiStr::<4>::from_bytes_lossy(b"\0").as_str(), "?");
assert_eq!(
TinyAsciiStr::<4>::from_bytes_lossy(b"toolong").as_str(),
"tool"
);
assert_eq!(
TinyAsciiStr::<4>::from_bytes_lossy(&[b'a', 0x80, 0xFF, b'1']).as_str(),
"a??1"
);
}
}