Skip to main content

tinystr/
ascii.rs

1// This file is part of ICU4X. For terms of use, please see the file
2// called LICENSE at the top level of the ICU4X source tree
3// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4
5use crate::asciibyte::AsciiByte;
6use crate::int_ops::{Aligned4, Aligned8};
7use crate::ParseError;
8#[cfg(feature = "alloc")]
9use alloc::string::String;
10use core::borrow::Borrow;
11use core::fmt;
12use core::ops::Deref;
13use core::str::FromStr;
14
15#[repr(transparent)]
16#[derive(#[automatically_derived]impl<const N : usize>
    ::core::marker::StructuralPartialEq for TinyAsciiStr<N> {
}
#[automatically_derived]impl<const N : usize> ::core::cmp::PartialEq for
    TinyAsciiStr<N> {
    #[inline]
    fn eq(&self, other: &Self) -> bool { self.bytes == other.bytes }
}PartialEq, #[automatically_derived]impl<const N : usize> ::core::cmp::Eq for
    TinyAsciiStr<N> {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<[AsciiByte; N]>;
    }
}Eq, #[automatically_derived]impl<const N : usize> ::core::cmp::Ord for
    TinyAsciiStr<N> {
    #[inline]
    fn cmp(&self, other: &Self) -> ::core::cmp::Ordering {
        ::core::cmp::Ord::cmp(&self.bytes, &other.bytes)
    }
}Ord, #[automatically_derived]impl<const N : usize> ::core::cmp::PartialOrd for
    TinyAsciiStr<N> {
    #[inline]
    fn partial_cmp(&self, other: &Self)
        -> ::core::option::Option<::core::cmp::Ordering> {
        ::core::option::Option::Some(::core::cmp::Ord::cmp(self, other))
    }
}PartialOrd, #[automatically_derived]impl<const N : usize> ::core::marker::Copy for
    TinyAsciiStr<N> {
}Copy, #[automatically_derived]#[doc(hidden)]unsafe impl<const N : usize>
    ::core::clone::TrivialClone for TinyAsciiStr<N> {
}
#[automatically_derived]impl<const N : usize> ::core::clone::Clone for
    TinyAsciiStr<N> {
    #[inline]
    fn clone(&self) -> Self {
        let _: ::core::clone::AssertParamIsClone<[AsciiByte; N]>;
        *self
    }
}Clone, #[automatically_derived]impl<const N : usize> ::core::hash::Hash for
    TinyAsciiStr<N> {
    #[inline]
    fn hash<__H: ::core::hash::Hasher>(&self, state: &mut __H) {
        ::core::hash::Hash::hash(&self.bytes, state)
    }
}Hash)]
17pub struct TinyAsciiStr<const N: usize> {
18    bytes: [AsciiByte; N],
19}
20
21impl<const N: usize> TinyAsciiStr<N> {
22    /// The empty string.
23    pub const EMPTY: Self = Self {
24        bytes: [AsciiByte::B0; N],
25    };
26
27    pub const fn default() -> Self {
28        Self::EMPTY
29    }
30
31    #[inline]
32    pub const fn try_from_str(s: &str) -> Result<Self, ParseError> {
33        Self::try_from_utf8(s.as_bytes())
34    }
35
36    /// Creates a `TinyAsciiStr<N>` from the given UTF-8 slice.
37    /// `code_units` may contain at most `N` non-null ASCII code points.
38    #[inline]
39    pub const fn try_from_utf8(code_units: &[u8]) -> Result<Self, ParseError> {
40        Self::try_from_utf8_inner(code_units, false)
41    }
42
43    /// Creates a `TinyAsciiStr<N>` from the given UTF-16 slice.
44    /// `code_units` may contain at most `N` non-null ASCII code points.
45    #[inline]
46    pub const fn try_from_utf16(code_units: &[u16]) -> Result<Self, ParseError> {
47        Self::try_from_utf16_inner(code_units, 0, code_units.len(), false)
48    }
49
50    /// Creates a `TinyAsciiStr<N>` from a UTF-8 slice, replacing invalid code units.
51    ///
52    /// Invalid code units, as well as null or non-ASCII code points
53    /// (i.e. those outside the range `U+0001..=U+007F`)
54    /// will be replaced with the replacement byte.
55    ///
56    /// The input slice will be truncated if its length exceeds `N`.
57    ///
58    /// # Panics
59    ///
60    /// Panics if `replacement` is not a non-null ASCII byte (i.e. not in `1..=127`).
61    pub const fn from_utf8_lossy(code_units: &[u8], replacement: u8) -> Self {
62        if !(replacement > 0 && replacement < 0x80) {
    {
        ::core::panicking::panic_fmt(format_args!("replacement must be a non-null ASCII byte (1..=127)"));
    }
};assert!(
63            replacement > 0 && replacement < 0x80,
64            "replacement must be a non-null ASCII byte (1..=127)"
65        );
66        let mut out = [0; N];
67        let mut i = 0;
68        // Ord is not available in const, so no `.min(N)`
69        let len = if code_units.len() > N {
70            N
71        } else {
72            code_units.len()
73        };
74
75        // Indexing is protected by the len check above
76        #[expect(clippy::indexing_slicing)]
77        while i < len {
78            let b = code_units[i];
79            if b > 0 && b < 0x80 {
80                out[i] = b;
81            } else {
82                out[i] = replacement;
83            }
84            i += 1;
85        }
86
87        Self {
88            // SAFETY: `out` only contains ASCII bytes and has same size as `self.bytes`
89            bytes: unsafe { AsciiByte::to_ascii_byte_array(&out) },
90        }
91    }
92
93    /// Creates a `TinyAsciiStr<N>` from a UTF-16 slice, replacing invalid code units.
94    ///
95    /// Invalid code units, as well as null or non-ASCII code points
96    /// (i.e. those outside the range `U+0001..=U+007F`)
97    /// will be replaced with the replacement byte.
98    ///
99    /// The input slice will be truncated if its length exceeds `N`.
100    ///
101    /// # Panics
102    ///
103    /// Panics if `replacement` is not a non-null ASCII byte (i.e. not in `1..=127`).
104    pub const fn from_utf16_lossy(code_units: &[u16], replacement: u8) -> Self {
105        if !(replacement > 0 && replacement < 0x80) {
    {
        ::core::panicking::panic_fmt(format_args!("replacement must be a non-null ASCII byte (1..=127)"));
    }
};assert!(
106            replacement > 0 && replacement < 0x80,
107            "replacement must be a non-null ASCII byte (1..=127)"
108        );
109        let mut out = [0; N];
110        let mut i = 0;
111        // Ord is not available in const, so no `.min(N)`
112        let len = if code_units.len() > N {
113            N
114        } else {
115            code_units.len()
116        };
117
118        // Indexing is protected by the len check above
119        #[expect(clippy::indexing_slicing)]
120        while i < len {
121            let b = code_units[i];
122            if b > 0 && b < 0x80 {
123                out[i] = b as u8;
124            } else {
125                out[i] = replacement;
126            }
127            i += 1;
128        }
129
130        Self {
131            // SAFETY: `out` only contains ASCII bytes and has same size as `self.bytes`
132            bytes: unsafe { AsciiByte::to_ascii_byte_array(&out) },
133        }
134    }
135
136    /// Attempts to parse a fixed-length byte array to a `TinyAsciiStr`.
137    ///
138    /// The byte array may contain trailing NUL bytes.
139    ///
140    /// # Example
141    ///
142    /// ```
143    /// use tinystr::tinystr;
144    /// use tinystr::TinyAsciiStr;
145    ///
146    /// assert_eq!(
147    ///     TinyAsciiStr::<3>::try_from_raw(*b"GB\0"),
148    ///     Ok(tinystr!(3, "GB"))
149    /// );
150    /// assert_eq!(
151    ///     TinyAsciiStr::<3>::try_from_raw(*b"USD"),
152    ///     Ok(tinystr!(3, "USD"))
153    /// );
154    /// assert!(TinyAsciiStr::<3>::try_from_raw(*b"\0A\0").is_err());
155    /// ```
156    pub const fn try_from_raw(raw: [u8; N]) -> Result<Self, ParseError> {
157        Self::try_from_utf8_inner(&raw, true)
158    }
159
160    pub(crate) const fn try_from_utf8_inner(
161        code_units: &[u8],
162        allow_trailing_null: bool,
163    ) -> Result<Self, ParseError> {
164        if code_units.len() > N {
165            return Err(ParseError::TooLong {
166                max: N,
167                len: code_units.len(),
168            });
169        }
170
171        let mut out = [0; N];
172        let mut i = 0;
173        let mut found_null = false;
174        // Indexing is protected by TinyStrError::TooLarge
175        #[expect(clippy::indexing_slicing)]
176        while i < code_units.len() {
177            let b = code_units[i];
178
179            if b == 0 {
180                found_null = true;
181            } else if b >= 0x80 {
182                return Err(ParseError::NonAscii);
183            } else if found_null {
184                // Error if there are contentful bytes after null
185                return Err(ParseError::ContainsNull);
186            }
187            out[i] = b;
188
189            i += 1;
190        }
191
192        if !allow_trailing_null && found_null {
193            // We found some trailing nulls, error
194            return Err(ParseError::ContainsNull);
195        }
196
197        Ok(Self {
198            // SAFETY: `out` only contains ASCII bytes and has same size as `self.bytes`
199            bytes: unsafe { AsciiByte::to_ascii_byte_array(&out) },
200        })
201    }
202
203    pub(crate) const fn try_from_utf16_inner(
204        code_units: &[u16],
205        start: usize,
206        end: usize,
207        allow_trailing_null: bool,
208    ) -> Result<Self, ParseError> {
209        let len = end - start;
210        if len > N {
211            return Err(ParseError::TooLong { max: N, len });
212        }
213
214        let mut out = [0; N];
215        let mut i = 0;
216        let mut found_null = false;
217        // Indexing is protected by TinyStrError::TooLarge
218        #[expect(clippy::indexing_slicing)]
219        while i < len {
220            let b = code_units[start + i];
221
222            if b == 0 {
223                found_null = true;
224            } else if b >= 0x80 {
225                return Err(ParseError::NonAscii);
226            } else if found_null {
227                // Error if there are contentful bytes after null
228                return Err(ParseError::ContainsNull);
229            }
230            out[i] = b as u8;
231
232            i += 1;
233        }
234
235        if !allow_trailing_null && found_null {
236            // We found some trailing nulls, error
237            return Err(ParseError::ContainsNull);
238        }
239
240        Ok(Self {
241            // SAFETY: `out` only contains ASCII bytes and has same size as `self.bytes`
242            bytes: unsafe { AsciiByte::to_ascii_byte_array(&out) },
243        })
244    }
245
246    /// Creates a `TinyAsciiStr<N>` containing the decimal representation of
247    /// the given unsigned integer.
248    ///
249    /// If the number of decimal digits exceeds `N`, the highest-magnitude
250    /// digits are truncated, and the lowest-magnitude digits are returned
251    /// as the error.
252    ///
253    /// Note: this function takes a u32. Larger integer types should probably
254    /// not be stored in a `TinyAsciiStr`.
255    ///
256    /// # Examples
257    ///
258    /// ```
259    /// use tinystr::tinystr;
260    /// use tinystr::TinyAsciiStr;
261    ///
262    /// let s0_4 = TinyAsciiStr::<4>::new_unsigned_decimal(0).unwrap();
263    /// let s456_4 = TinyAsciiStr::<4>::new_unsigned_decimal(456).unwrap();
264    /// let s456_3 = TinyAsciiStr::<3>::new_unsigned_decimal(456).unwrap();
265    /// let s456_2 = TinyAsciiStr::<2>::new_unsigned_decimal(456).unwrap_err();
266    ///
267    /// assert_eq!(s0_4, tinystr!(4, "0"));
268    /// assert_eq!(s456_4, tinystr!(4, "456"));
269    /// assert_eq!(s456_3, tinystr!(3, "456"));
270    /// assert_eq!(s456_2, tinystr!(2, "56"));
271    /// ```
272    ///
273    /// Example with saturating the value:
274    ///
275    /// ```
276    /// use tinystr::tinystr;
277    /// use tinystr::TinyAsciiStr;
278    ///
279    /// let str_truncated =
280    ///     TinyAsciiStr::<2>::new_unsigned_decimal(456).unwrap_or_else(|s| s);
281    /// let str_saturated = TinyAsciiStr::<2>::new_unsigned_decimal(456)
282    ///     .unwrap_or(tinystr!(2, "99"));
283    ///
284    /// assert_eq!(str_truncated, tinystr!(2, "56"));
285    /// assert_eq!(str_saturated, tinystr!(2, "99"));
286    /// ```
287    pub fn new_unsigned_decimal(number: u32) -> Result<Self, Self> {
288        let mut bytes = [AsciiByte::B0; N];
289        let mut x = number;
290        let mut i = 0usize;
291        #[expect(clippy::indexing_slicing)] // in-range: i < N
292        while i < N && (x != 0 || i == 0) {
293            bytes[N - i - 1] = AsciiByte::from_decimal_digit((x % 10) as u8);
294            x /= 10;
295            i += 1;
296        }
297        if i < N {
298            bytes.copy_within((N - i)..N, 0);
299            bytes[i..N].fill(AsciiByte::B0);
300        }
301        let s = Self { bytes };
302        if x != 0 {
303            Err(s)
304        } else {
305            Ok(s)
306        }
307    }
308
309    #[inline]
310    pub const fn as_str(&self) -> &str {
311        // as_utf8 is valid utf8
312        unsafe { core::str::from_utf8_unchecked(self.as_utf8()) }
313    }
314
315    #[inline]
316    #[must_use]
317    pub const fn len(&self) -> usize {
318        if N <= 4 {
319            Aligned4::from_ascii_bytes(&self.bytes).len()
320        } else if N <= 8 {
321            Aligned8::from_ascii_bytes(&self.bytes).len()
322        } else {
323            let mut i = 0;
324            #[expect(clippy::indexing_slicing)] // < N is safe
325            while i < N && self.bytes[i] as u8 != AsciiByte::B0 as u8 {
326                i += 1
327            }
328            i
329        }
330    }
331
332    #[inline]
333    #[must_use]
334    pub const fn is_empty(&self) -> bool {
335        self.bytes[0] as u8 == AsciiByte::B0 as u8
336    }
337
338    #[inline]
339    #[must_use]
340    pub const fn as_utf8(&self) -> &[u8] {
341        // Safe because `self.bytes.as_slice()` pointer-casts to `&[u8]`,
342        // and changing the length of that slice to self.len() < N is safe.
343        unsafe {
344            core::slice::from_raw_parts(self.bytes.as_slice().as_ptr() as *const u8, self.len())
345        }
346    }
347
348    #[inline]
349    #[must_use]
350    pub const fn all_bytes(&self) -> &[u8; N] {
351        // SAFETY: `self.bytes` has same size as [u8; N]
352        unsafe { &*(self.bytes.as_ptr() as *const [u8; N]) }
353    }
354
355    #[inline]
356    #[must_use]
357    /// Resizes a `TinyAsciiStr<N>` to a `TinyAsciiStr<M>`.
358    ///
359    /// If `M < len()` the string gets truncated, otherwise only the
360    /// memory representation changes.
361    pub const fn resize<const M: usize>(self) -> TinyAsciiStr<M> {
362        let mut bytes = [0; M];
363        let mut i = 0;
364        // Indexing is protected by the loop guard
365        #[expect(clippy::indexing_slicing)]
366        while i < M && i < N {
367            bytes[i] = self.bytes[i] as u8;
368            i += 1;
369        }
370        // `self.bytes` only contains ASCII bytes, with no null bytes between
371        // ASCII characters, so this also holds for `bytes`.
372        unsafe { TinyAsciiStr::from_utf8_unchecked(bytes) }
373    }
374
375    #[inline]
376    #[must_use]
377    /// Returns a `TinyAsciiStr<Q>` with the concatenation of this string,
378    /// `TinyAsciiStr<N>`, and another string, `TinyAsciiStr<M>`.
379    ///
380    /// If `Q < N + M`, the string gets truncated.
381    ///
382    /// # Examples
383    ///
384    /// ```
385    /// use tinystr::tinystr;
386    /// use tinystr::TinyAsciiStr;
387    ///
388    /// let abc = tinystr!(6, "abc");
389    /// let defg = tinystr!(6, "defg");
390    ///
391    /// // The concatenation is successful if Q is large enough...
392    /// assert_eq!(abc.concat(defg), tinystr!(16, "abcdefg"));
393    /// assert_eq!(abc.concat(defg), tinystr!(12, "abcdefg"));
394    /// assert_eq!(abc.concat(defg), tinystr!(8, "abcdefg"));
395    /// assert_eq!(abc.concat(defg), tinystr!(7, "abcdefg"));
396    ///
397    /// /// ...but it truncates of Q is too small.
398    /// assert_eq!(abc.concat(defg), tinystr!(6, "abcdef"));
399    /// assert_eq!(abc.concat(defg), tinystr!(2, "ab"));
400    /// ```
401    pub const fn concat<const M: usize, const Q: usize>(
402        self,
403        other: TinyAsciiStr<M>,
404    ) -> TinyAsciiStr<Q> {
405        let mut result = self.resize::<Q>();
406        let mut i = self.len();
407        let mut j = 0;
408        // Indexing is protected by the loop guard
409        #[expect(clippy::indexing_slicing)]
410        while i < Q && j < M {
411            result.bytes[i] = other.bytes[j];
412            i += 1;
413            j += 1;
414        }
415        result
416    }
417
418    /// # Safety
419    /// Must be called with a bytes array made of valid ASCII bytes, with no null bytes
420    /// between ASCII characters
421    #[must_use]
422    pub const unsafe fn from_utf8_unchecked(code_units: [u8; N]) -> Self {
423        Self {
424            bytes: AsciiByte::to_ascii_byte_array(&code_units),
425        }
426    }
427}
428
429macro_rules! check_is {
430    ($self:ident, $check_int:ident, $check_u8:ident) => {
431        if N <= 4 {
432            Aligned4::from_ascii_bytes(&$self.bytes).$check_int()
433        } else if N <= 8 {
434            Aligned8::from_ascii_bytes(&$self.bytes).$check_int()
435        } else {
436            let mut i = 0;
437            while i < N && $self.bytes[i] as u8 != AsciiByte::B0 as u8 {
438                if !($self.bytes[i] as u8).$check_u8() {
439                    return false;
440                }
441                i += 1;
442            }
443            true
444        }
445    };
446    ($self:ident, $check_int:ident, !$check_u8_0_inv:ident, !$check_u8_1_inv:ident) => {
447        if N <= 4 {
448            Aligned4::from_ascii_bytes(&$self.bytes).$check_int()
449        } else if N <= 8 {
450            Aligned8::from_ascii_bytes(&$self.bytes).$check_int()
451        } else {
452            // Won't panic because N is > 8
453            if ($self.bytes[0] as u8).$check_u8_0_inv() {
454                return false;
455            }
456            let mut i = 1;
457            while i < N && $self.bytes[i] as u8 != AsciiByte::B0 as u8 {
458                if ($self.bytes[i] as u8).$check_u8_1_inv() {
459                    return false;
460                }
461                i += 1;
462            }
463            true
464        }
465    };
466    ($self:ident, $check_int:ident, $check_u8_0_inv:ident, $check_u8_1_inv:ident) => {
467        if N <= 4 {
468            Aligned4::from_ascii_bytes(&$self.bytes).$check_int()
469        } else if N <= 8 {
470            Aligned8::from_ascii_bytes(&$self.bytes).$check_int()
471        } else {
472            // Won't panic because N is > 8
473            if !($self.bytes[0] as u8).$check_u8_0_inv() {
474                return false;
475            }
476            let mut i = 1;
477            while i < N && $self.bytes[i] as u8 != AsciiByte::B0 as u8 {
478                if !($self.bytes[i] as u8).$check_u8_1_inv() {
479                    return false;
480                }
481                i += 1;
482            }
483            true
484        }
485    };
486}
487
488impl<const N: usize> TinyAsciiStr<N> {
489    /// Checks if the value is composed of ASCII alphabetic characters:
490    ///
491    ///  * U+0041 'A' ..= U+005A 'Z', or
492    ///  * U+0061 'a' ..= U+007A 'z'.
493    ///
494    /// # Examples
495    ///
496    /// ```
497    /// use tinystr::TinyAsciiStr;
498    ///
499    /// let s1: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse.");
500    /// let s2: TinyAsciiStr<4> = "Te3t".parse().expect("Failed to parse.");
501    ///
502    /// assert!(s1.is_ascii_alphabetic());
503    /// assert!(!s2.is_ascii_alphabetic());
504    /// ```
505    #[inline]
506    #[must_use]
507    pub const fn is_ascii_alphabetic(&self) -> bool {
508        if N <= 4 {
    Aligned4::from_ascii_bytes(&self.bytes).is_ascii_alphabetic()
} else if N <= 8 {
    Aligned8::from_ascii_bytes(&self.bytes).is_ascii_alphabetic()
} else {
    let mut i = 0;
    while i < N && self.bytes[i] as u8 != AsciiByte::B0 as u8 {
        if !(self.bytes[i] as u8).is_ascii_alphabetic() { return false; }
        i += 1;
    }
    true
}check_is!(self, is_ascii_alphabetic, is_ascii_alphabetic)
509    }
510
511    /// Checks if the value is composed of ASCII alphanumeric characters:
512    ///
513    ///  * U+0041 'A' ..= U+005A 'Z', or
514    ///  * U+0061 'a' ..= U+007A 'z', or
515    ///  * U+0030 '0' ..= U+0039 '9'.
516    ///
517    /// # Examples
518    ///
519    /// ```
520    /// use tinystr::TinyAsciiStr;
521    ///
522    /// let s1: TinyAsciiStr<4> = "A15b".parse().expect("Failed to parse.");
523    /// let s2: TinyAsciiStr<4> = "[3@w".parse().expect("Failed to parse.");
524    ///
525    /// assert!(s1.is_ascii_alphanumeric());
526    /// assert!(!s2.is_ascii_alphanumeric());
527    /// ```
528    #[inline]
529    #[must_use]
530    pub const fn is_ascii_alphanumeric(&self) -> bool {
531        if N <= 4 {
    Aligned4::from_ascii_bytes(&self.bytes).is_ascii_alphanumeric()
} else if N <= 8 {
    Aligned8::from_ascii_bytes(&self.bytes).is_ascii_alphanumeric()
} else {
    let mut i = 0;
    while i < N && self.bytes[i] as u8 != AsciiByte::B0 as u8 {
        if !(self.bytes[i] as u8).is_ascii_alphanumeric() { return false; }
        i += 1;
    }
    true
}check_is!(self, is_ascii_alphanumeric, is_ascii_alphanumeric)
532    }
533
534    /// Checks if the value is composed of ASCII decimal digits:
535    ///
536    ///  * U+0030 '0' ..= U+0039 '9'.
537    ///
538    /// # Examples
539    ///
540    /// ```
541    /// use tinystr::TinyAsciiStr;
542    ///
543    /// let s1: TinyAsciiStr<4> = "312".parse().expect("Failed to parse.");
544    /// let s2: TinyAsciiStr<4> = "3d".parse().expect("Failed to parse.");
545    ///
546    /// assert!(s1.is_ascii_numeric());
547    /// assert!(!s2.is_ascii_numeric());
548    /// ```
549    #[inline]
550    #[must_use]
551    pub const fn is_ascii_numeric(&self) -> bool {
552        if N <= 4 {
    Aligned4::from_ascii_bytes(&self.bytes).is_ascii_numeric()
} else if N <= 8 {
    Aligned8::from_ascii_bytes(&self.bytes).is_ascii_numeric()
} else {
    let mut i = 0;
    while i < N && self.bytes[i] as u8 != AsciiByte::B0 as u8 {
        if !(self.bytes[i] as u8).is_ascii_digit() { return false; }
        i += 1;
    }
    true
}check_is!(self, is_ascii_numeric, is_ascii_digit)
553    }
554
555    /// Checks if the value is in ASCII lower case.
556    ///
557    /// All letter characters are checked for case. Non-letter characters are ignored.
558    ///
559    /// # Examples
560    ///
561    /// ```
562    /// use tinystr::TinyAsciiStr;
563    ///
564    /// let s1: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
565    /// let s2: TinyAsciiStr<4> = "test".parse().expect("Failed to parse.");
566    /// let s3: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
567    ///
568    /// assert!(!s1.is_ascii_lowercase());
569    /// assert!(s2.is_ascii_lowercase());
570    /// assert!(s3.is_ascii_lowercase());
571    /// ```
572    #[inline]
573    #[must_use]
574    pub const fn is_ascii_lowercase(&self) -> bool {
575        if N <= 4 {
    Aligned4::from_ascii_bytes(&self.bytes).is_ascii_lowercase()
} else if N <= 8 {
    Aligned8::from_ascii_bytes(&self.bytes).is_ascii_lowercase()
} else {
    if (self.bytes[0] as u8).is_ascii_uppercase() { return false; }
    let mut i = 1;
    while i < N && self.bytes[i] as u8 != AsciiByte::B0 as u8 {
        if (self.bytes[i] as u8).is_ascii_uppercase() { return false; }
        i += 1;
    }
    true
}check_is!(
576            self,
577            is_ascii_lowercase,
578            !is_ascii_uppercase,
579            !is_ascii_uppercase
580        )
581    }
582
583    /// Checks if the value is in ASCII title case.
584    ///
585    /// This verifies that the first character is ASCII uppercase and all others ASCII lowercase.
586    /// Non-letter characters are ignored.
587    ///
588    /// # Examples
589    ///
590    /// ```
591    /// use tinystr::TinyAsciiStr;
592    ///
593    /// let s1: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
594    /// let s2: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse.");
595    /// let s3: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
596    ///
597    /// assert!(!s1.is_ascii_titlecase());
598    /// assert!(s2.is_ascii_titlecase());
599    /// assert!(s3.is_ascii_titlecase());
600    /// ```
601    #[inline]
602    #[must_use]
603    pub const fn is_ascii_titlecase(&self) -> bool {
604        if N <= 4 {
    Aligned4::from_ascii_bytes(&self.bytes).is_ascii_titlecase()
} else if N <= 8 {
    Aligned8::from_ascii_bytes(&self.bytes).is_ascii_titlecase()
} else {
    if (self.bytes[0] as u8).is_ascii_lowercase() { return false; }
    let mut i = 1;
    while i < N && self.bytes[i] as u8 != AsciiByte::B0 as u8 {
        if (self.bytes[i] as u8).is_ascii_uppercase() { return false; }
        i += 1;
    }
    true
}check_is!(
605            self,
606            is_ascii_titlecase,
607            !is_ascii_lowercase,
608            !is_ascii_uppercase
609        )
610    }
611
612    /// Checks if the value is in ASCII upper case.
613    ///
614    /// All letter characters are checked for case. Non-letter characters are ignored.
615    ///
616    /// # Examples
617    ///
618    /// ```
619    /// use tinystr::TinyAsciiStr;
620    ///
621    /// let s1: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
622    /// let s2: TinyAsciiStr<4> = "TEST".parse().expect("Failed to parse.");
623    /// let s3: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
624    ///
625    /// assert!(!s1.is_ascii_uppercase());
626    /// assert!(s2.is_ascii_uppercase());
627    /// assert!(!s3.is_ascii_uppercase());
628    /// ```
629    #[inline]
630    #[must_use]
631    pub const fn is_ascii_uppercase(&self) -> bool {
632        if N <= 4 {
    Aligned4::from_ascii_bytes(&self.bytes).is_ascii_uppercase()
} else if N <= 8 {
    Aligned8::from_ascii_bytes(&self.bytes).is_ascii_uppercase()
} else {
    if (self.bytes[0] as u8).is_ascii_lowercase() { return false; }
    let mut i = 1;
    while i < N && self.bytes[i] as u8 != AsciiByte::B0 as u8 {
        if (self.bytes[i] as u8).is_ascii_lowercase() { return false; }
        i += 1;
    }
    true
}check_is!(
633            self,
634            is_ascii_uppercase,
635            !is_ascii_lowercase,
636            !is_ascii_lowercase
637        )
638    }
639
640    /// Checks if the value is composed of ASCII alphabetic lower case characters:
641    ///
642    ///  * U+0061 'a' ..= U+007A 'z',
643    ///
644    /// # Examples
645    ///
646    /// ```
647    /// use tinystr::TinyAsciiStr;
648    ///
649    /// let s1: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse.");
650    /// let s2: TinyAsciiStr<4> = "Te3t".parse().expect("Failed to parse.");
651    /// let s3: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
652    /// let s4: TinyAsciiStr<4> = "test".parse().expect("Failed to parse.");
653    /// let s5: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
654    ///
655    /// assert!(!s1.is_ascii_alphabetic_lowercase());
656    /// assert!(!s2.is_ascii_alphabetic_lowercase());
657    /// assert!(!s3.is_ascii_alphabetic_lowercase());
658    /// assert!(s4.is_ascii_alphabetic_lowercase());
659    /// assert!(!s5.is_ascii_alphabetic_lowercase());
660    /// ```
661    #[inline]
662    #[must_use]
663    pub const fn is_ascii_alphabetic_lowercase(&self) -> bool {
664        if N <= 4 {
    Aligned4::from_ascii_bytes(&self.bytes).is_ascii_alphabetic_lowercase()
} else if N <= 8 {
    Aligned8::from_ascii_bytes(&self.bytes).is_ascii_alphabetic_lowercase()
} else {
    if !(self.bytes[0] as u8).is_ascii_lowercase() { return false; }
    let mut i = 1;
    while i < N && self.bytes[i] as u8 != AsciiByte::B0 as u8 {
        if !(self.bytes[i] as u8).is_ascii_lowercase() { return false; }
        i += 1;
    }
    true
}check_is!(
665            self,
666            is_ascii_alphabetic_lowercase,
667            is_ascii_lowercase,
668            is_ascii_lowercase
669        )
670    }
671
672    /// Checks if the value is composed of ASCII alphabetic, with the first character being ASCII uppercase, and all others ASCII lowercase.
673    ///
674    /// # Examples
675    ///
676    /// ```
677    /// use tinystr::TinyAsciiStr;
678    ///
679    /// let s1: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse.");
680    /// let s2: TinyAsciiStr<4> = "Te3t".parse().expect("Failed to parse.");
681    /// let s3: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
682    /// let s4: TinyAsciiStr<4> = "test".parse().expect("Failed to parse.");
683    /// let s5: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
684    ///
685    /// assert!(s1.is_ascii_alphabetic_titlecase());
686    /// assert!(!s2.is_ascii_alphabetic_titlecase());
687    /// assert!(!s3.is_ascii_alphabetic_titlecase());
688    /// assert!(!s4.is_ascii_alphabetic_titlecase());
689    /// assert!(!s5.is_ascii_alphabetic_titlecase());
690    /// ```
691    #[inline]
692    #[must_use]
693    pub const fn is_ascii_alphabetic_titlecase(&self) -> bool {
694        if N <= 4 {
    Aligned4::from_ascii_bytes(&self.bytes).is_ascii_alphabetic_titlecase()
} else if N <= 8 {
    Aligned8::from_ascii_bytes(&self.bytes).is_ascii_alphabetic_titlecase()
} else {
    if !(self.bytes[0] as u8).is_ascii_uppercase() { return false; }
    let mut i = 1;
    while i < N && self.bytes[i] as u8 != AsciiByte::B0 as u8 {
        if !(self.bytes[i] as u8).is_ascii_lowercase() { return false; }
        i += 1;
    }
    true
}check_is!(
695            self,
696            is_ascii_alphabetic_titlecase,
697            is_ascii_uppercase,
698            is_ascii_lowercase
699        )
700    }
701
702    /// Checks if the value is composed of ASCII alphabetic upper case characters:
703    ///
704    ///  * U+0041 'A' ..= U+005A 'Z',
705    ///
706    /// # Examples
707    ///
708    /// ```
709    /// use tinystr::TinyAsciiStr;
710    ///
711    /// let s1: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse.");
712    /// let s2: TinyAsciiStr<4> = "Te3t".parse().expect("Failed to parse.");
713    /// let s3: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
714    /// let s4: TinyAsciiStr<4> = "TEST".parse().expect("Failed to parse.");
715    /// let s5: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
716    ///
717    /// assert!(!s1.is_ascii_alphabetic_uppercase());
718    /// assert!(!s2.is_ascii_alphabetic_uppercase());
719    /// assert!(!s3.is_ascii_alphabetic_uppercase());
720    /// assert!(s4.is_ascii_alphabetic_uppercase());
721    /// assert!(!s5.is_ascii_alphabetic_uppercase());
722    /// ```
723    #[inline]
724    #[must_use]
725    pub const fn is_ascii_alphabetic_uppercase(&self) -> bool {
726        if N <= 4 {
    Aligned4::from_ascii_bytes(&self.bytes).is_ascii_alphabetic_uppercase()
} else if N <= 8 {
    Aligned8::from_ascii_bytes(&self.bytes).is_ascii_alphabetic_uppercase()
} else {
    if !(self.bytes[0] as u8).is_ascii_uppercase() { return false; }
    let mut i = 1;
    while i < N && self.bytes[i] as u8 != AsciiByte::B0 as u8 {
        if !(self.bytes[i] as u8).is_ascii_uppercase() { return false; }
        i += 1;
    }
    true
}check_is!(
727            self,
728            is_ascii_alphabetic_uppercase,
729            is_ascii_uppercase,
730            is_ascii_uppercase
731        )
732    }
733}
734
735macro_rules! to {
736    ($self:ident, $to:ident, $later_char_to:ident $(,$first_char_to:ident)?) => {{
737        let mut i = 0;
738        if N <= 4 {
739            let aligned = Aligned4::from_ascii_bytes(&$self.bytes).$to();
740            // Won't panic because self.bytes has length N and aligned has length >= N
741            #[expect(clippy::indexing_slicing)]
742            while i < N {
743                $self.bytes[i] = aligned[i];
744                i += 1;
745            }
746        } else if N <= 8 {
747            let aligned = Aligned8::from_ascii_bytes(&$self.bytes).$to();
748            // Won't panic because self.bytes has length N and aligned has length >= N
749            #[expect(clippy::indexing_slicing)]
750            while i < N {
751                $self.bytes[i] = aligned[i];
752                i += 1;
753            }
754        } else {
755            while i < N && $self.bytes[i] as u8 != AsciiByte::B0 as u8 {
756                $self.bytes[i] = $self.bytes[i].$later_char_to();
757                i += 1;
758            }
759            $(
760                $self.bytes[0] = $self.bytes[0].$first_char_to();
761            )?
762        }
763        $self
764    }};
765}
766
767impl<const N: usize> TinyAsciiStr<N> {
768    /// Converts this type to its ASCII lower case equivalent in-place.
769    ///
770    /// ASCII letters 'A' to 'Z' are mapped to 'a' to 'z', other characters are unchanged.
771    ///
772    /// # Examples
773    ///
774    /// ```
775    /// use tinystr::TinyAsciiStr;
776    ///
777    /// let s1: TinyAsciiStr<4> = "TeS3".parse().expect("Failed to parse.");
778    ///
779    /// assert_eq!(&*s1.to_ascii_lowercase(), "tes3");
780    /// ```
781    #[inline]
782    #[must_use]
783    pub const fn to_ascii_lowercase(mut self) -> Self {
784        {
    let mut i = 0;
    if N <= 4 {
        let aligned =
            Aligned4::from_ascii_bytes(&self.bytes).to_ascii_lowercase();

        #[expect(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_ascii_lowercase();

        #[expect(clippy :: indexing_slicing)]
        while i < N { self.bytes[i] = aligned[i]; i += 1; }
    } else {
        while i < N && self.bytes[i] as u8 != AsciiByte::B0 as u8 {
            self.bytes[i] = self.bytes[i].to_ascii_lowercase();
            i += 1;
        }
    }
    self
}to!(self, to_ascii_lowercase, to_ascii_lowercase)
785    }
786
787    /// Converts this type to its ASCII title case equivalent in-place.
788    ///
789    /// The first character is converted to ASCII uppercase; the remaining characters
790    /// are converted to ASCII lowercase.
791    ///
792    /// # Examples
793    ///
794    /// ```
795    /// use tinystr::TinyAsciiStr;
796    ///
797    /// let s1: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
798    ///
799    /// assert_eq!(&*s1.to_ascii_titlecase(), "Test");
800    /// ```
801    #[inline]
802    #[must_use]
803    pub const fn to_ascii_titlecase(mut self) -> Self {
804        {
    let mut i = 0;
    if N <= 4 {
        let aligned =
            Aligned4::from_ascii_bytes(&self.bytes).to_ascii_titlecase();

        #[expect(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_ascii_titlecase();

        #[expect(clippy :: indexing_slicing)]
        while i < N { self.bytes[i] = aligned[i]; i += 1; }
    } else {
        while i < N && self.bytes[i] as u8 != AsciiByte::B0 as u8 {
            self.bytes[i] = self.bytes[i].to_ascii_lowercase();
            i += 1;
        }
        self.bytes[0] = self.bytes[0].to_ascii_uppercase();
    }
    self
}to!(
805            self,
806            to_ascii_titlecase,
807            to_ascii_lowercase,
808            to_ascii_uppercase
809        )
810    }
811
812    /// Converts this type to its ASCII upper case equivalent in-place.
813    ///
814    /// ASCII letters 'a' to 'z' are mapped to 'A' to 'Z', other characters are unchanged.
815    ///
816    /// # Examples
817    ///
818    /// ```
819    /// use tinystr::TinyAsciiStr;
820    ///
821    /// let s1: TinyAsciiStr<4> = "Tes3".parse().expect("Failed to parse.");
822    ///
823    /// assert_eq!(&*s1.to_ascii_uppercase(), "TES3");
824    /// ```
825    #[inline]
826    #[must_use]
827    pub const fn to_ascii_uppercase(mut self) -> Self {
828        {
    let mut i = 0;
    if N <= 4 {
        let aligned =
            Aligned4::from_ascii_bytes(&self.bytes).to_ascii_uppercase();

        #[expect(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_ascii_uppercase();

        #[expect(clippy :: indexing_slicing)]
        while i < N { self.bytes[i] = aligned[i]; i += 1; }
    } else {
        while i < N && self.bytes[i] as u8 != AsciiByte::B0 as u8 {
            self.bytes[i] = self.bytes[i].to_ascii_uppercase();
            i += 1;
        }
    }
    self
}to!(self, to_ascii_uppercase, to_ascii_uppercase)
829    }
830}
831
832impl<const N: usize> fmt::Debug for TinyAsciiStr<N> {
833    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
834        fmt::Debug::fmt(self.as_str(), f)
835    }
836}
837
838impl<const N: usize> fmt::Display for TinyAsciiStr<N> {
839    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
840        fmt::Display::fmt(self.as_str(), f)
841    }
842}
843
844impl<const N: usize> Deref for TinyAsciiStr<N> {
845    type Target = str;
846    #[inline]
847    fn deref(&self) -> &str {
848        self.as_str()
849    }
850}
851
852impl<const N: usize> Default for TinyAsciiStr<N> {
853    fn default() -> Self {
854        Self::default()
855    }
856}
857
858impl<const N: usize> Borrow<str> for TinyAsciiStr<N> {
859    #[inline]
860    fn borrow(&self) -> &str {
861        self.as_str()
862    }
863}
864
865impl<const N: usize> FromStr for TinyAsciiStr<N> {
866    type Err = ParseError;
867    #[inline]
868    fn from_str(s: &str) -> Result<Self, Self::Err> {
869        Self::try_from_str(s)
870    }
871}
872
873impl<const N: usize> PartialEq<str> for TinyAsciiStr<N> {
874    fn eq(&self, other: &str) -> bool {
875        self.deref() == other
876    }
877}
878
879impl<const N: usize> PartialEq<&str> for TinyAsciiStr<N> {
880    fn eq(&self, other: &&str) -> bool {
881        self.deref() == *other
882    }
883}
884
885#[cfg(feature = "alloc")]
886impl<const N: usize> PartialEq<String> for TinyAsciiStr<N> {
887    fn eq(&self, other: &String) -> bool {
888        self.deref() == other.deref()
889    }
890}
891
892#[cfg(feature = "alloc")]
893impl<const N: usize> PartialEq<TinyAsciiStr<N>> for String {
894    fn eq(&self, other: &TinyAsciiStr<N>) -> bool {
895        self.deref() == other.deref()
896    }
897}
898
899#[cfg(test)]
900mod test {
901    use super::*;
902    use rand::distr::Distribution;
903    use rand::distr::StandardUniform;
904    use rand::rngs::SmallRng;
905    use rand::SeedableRng;
906
907    const STRINGS: [&str; 26] = [
908        "Latn",
909        "laTn",
910        "windows",
911        "AR",
912        "Hans",
913        "macos",
914        "AT",
915        "infiniband",
916        "FR",
917        "en",
918        "Cyrl",
919        "FromIntegral",
920        "NO",
921        "419",
922        "MacintoshOSX2019",
923        "a3z",
924        "A3z",
925        "A3Z",
926        "a3Z",
927        "3A",
928        "3Z",
929        "3a",
930        "3z",
931        "@@[`{",
932        "UK",
933        "E12",
934    ];
935
936    fn gen_strings(num_strings: usize, allowed_lengths: &[usize]) -> Vec<String> {
937        use rand::seq::IndexedRandom;
938        let mut rng = SmallRng::seed_from_u64(2022);
939        // Need to do this in 2 steps since the RNG is needed twice
940        let string_lengths = core::iter::repeat_with(|| *allowed_lengths.choose(&mut rng).unwrap())
941            .take(num_strings)
942            .collect::<Vec<usize>>();
943        string_lengths
944            .iter()
945            .map(|len| {
946                StandardUniform
947                    .sample_iter(&mut rng)
948                    .filter(|b: &u8| *b > 0 && *b < 0x80)
949                    .take(*len)
950                    .collect::<Vec<u8>>()
951            })
952            .map(|byte_vec| String::from_utf8(byte_vec).expect("All ASCII"))
953            .collect()
954    }
955
956    fn check_operation<T, F1, F2, const N: usize>(reference_f: F1, tinystr_f: F2)
957    where
958        F1: Fn(&str) -> T,
959        F2: Fn(TinyAsciiStr<N>) -> T,
960        T: fmt::Debug + PartialEq,
961    {
962        for s in STRINGS
963            .into_iter()
964            .map(str::to_owned)
965            .chain(gen_strings(100, &[3, 4, 5, 8, 12]))
966        {
967            let t = match TinyAsciiStr::<N>::from_str(&s) {
968                Ok(t) => t,
969                Err(ParseError::TooLong { .. }) => continue,
970                Err(e) => panic!("{}", e),
971            };
972            let expected = reference_f(&s);
973            let actual = tinystr_f(t);
974            assert_eq!(expected, actual, "TinyAsciiStr<{N}>: {s:?}");
975
976            let s_utf16: Vec<u16> = s.encode_utf16().collect();
977            let t = match TinyAsciiStr::<N>::try_from_utf16(&s_utf16) {
978                Ok(t) => t,
979                Err(ParseError::TooLong { .. }) => continue,
980                Err(e) => panic!("{}", e),
981            };
982            let expected = reference_f(&s);
983            let actual = tinystr_f(t);
984            assert_eq!(expected, actual, "TinyAsciiStr<{N}>: {s:?}");
985        }
986    }
987
988    #[test]
989    fn test_is_ascii_alphabetic() {
990        fn check<const N: usize>() {
991            check_operation(
992                |s| s.chars().all(|c| c.is_ascii_alphabetic()),
993                |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphabetic(&t),
994            )
995        }
996        check::<2>();
997        check::<3>();
998        check::<4>();
999        check::<5>();
1000        check::<8>();
1001        check::<16>();
1002    }
1003
1004    #[test]
1005    fn test_is_ascii_alphanumeric() {
1006        fn check<const N: usize>() {
1007            check_operation(
1008                |s| s.chars().all(|c| c.is_ascii_alphanumeric()),
1009                |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphanumeric(&t),
1010            )
1011        }
1012        check::<2>();
1013        check::<3>();
1014        check::<4>();
1015        check::<5>();
1016        check::<8>();
1017        check::<16>();
1018    }
1019
1020    #[test]
1021    fn test_is_ascii_numeric() {
1022        fn check<const N: usize>() {
1023            check_operation(
1024                |s| s.chars().all(|c| c.is_ascii_digit()),
1025                |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_numeric(&t),
1026            )
1027        }
1028        check::<2>();
1029        check::<3>();
1030        check::<4>();
1031        check::<5>();
1032        check::<8>();
1033        check::<16>();
1034    }
1035
1036    #[test]
1037    fn test_is_ascii_lowercase() {
1038        fn check<const N: usize>() {
1039            check_operation(
1040                |s| {
1041                    s == TinyAsciiStr::<16>::try_from_str(s)
1042                        .unwrap()
1043                        .to_ascii_lowercase()
1044                        .as_str()
1045                },
1046                |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_lowercase(&t),
1047            )
1048        }
1049        check::<2>();
1050        check::<3>();
1051        check::<4>();
1052        check::<5>();
1053        check::<8>();
1054        check::<16>();
1055    }
1056
1057    #[test]
1058    fn test_is_ascii_titlecase() {
1059        fn check<const N: usize>() {
1060            check_operation(
1061                |s| {
1062                    s == TinyAsciiStr::<16>::try_from_str(s)
1063                        .unwrap()
1064                        .to_ascii_titlecase()
1065                        .as_str()
1066                },
1067                |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_titlecase(&t),
1068            )
1069        }
1070        check::<2>();
1071        check::<3>();
1072        check::<4>();
1073        check::<5>();
1074        check::<8>();
1075        check::<16>();
1076    }
1077
1078    #[test]
1079    fn test_is_ascii_uppercase() {
1080        fn check<const N: usize>() {
1081            check_operation(
1082                |s| {
1083                    s == TinyAsciiStr::<16>::try_from_str(s)
1084                        .unwrap()
1085                        .to_ascii_uppercase()
1086                        .as_str()
1087                },
1088                |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_uppercase(&t),
1089            )
1090        }
1091        check::<2>();
1092        check::<3>();
1093        check::<4>();
1094        check::<5>();
1095        check::<8>();
1096        check::<16>();
1097    }
1098
1099    #[test]
1100    fn test_is_ascii_alphabetic_lowercase() {
1101        fn check<const N: usize>() {
1102            check_operation(
1103                |s| {
1104                    // Check alphabetic
1105                    s.chars().all(|c| c.is_ascii_alphabetic()) &&
1106                    // Check lowercase
1107                    s == TinyAsciiStr::<16>::try_from_str(s)
1108                        .unwrap()
1109                        .to_ascii_lowercase()
1110                        .as_str()
1111                },
1112                |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphabetic_lowercase(&t),
1113            )
1114        }
1115        check::<2>();
1116        check::<3>();
1117        check::<4>();
1118        check::<5>();
1119        check::<8>();
1120        check::<16>();
1121    }
1122
1123    #[test]
1124    fn test_is_ascii_alphabetic_titlecase() {
1125        fn check<const N: usize>() {
1126            check_operation(
1127                |s| {
1128                    // Check alphabetic
1129                    s.chars().all(|c| c.is_ascii_alphabetic()) &&
1130                    // Check titlecase
1131                    s == TinyAsciiStr::<16>::try_from_str(s)
1132                        .unwrap()
1133                        .to_ascii_titlecase()
1134                        .as_str()
1135                },
1136                |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphabetic_titlecase(&t),
1137            )
1138        }
1139        check::<2>();
1140        check::<3>();
1141        check::<4>();
1142        check::<5>();
1143        check::<8>();
1144        check::<16>();
1145    }
1146
1147    #[test]
1148    fn test_is_ascii_alphabetic_uppercase() {
1149        fn check<const N: usize>() {
1150            check_operation(
1151                |s| {
1152                    // Check alphabetic
1153                    s.chars().all(|c| c.is_ascii_alphabetic()) &&
1154                    // Check uppercase
1155                    s == TinyAsciiStr::<16>::try_from_str(s)
1156                        .unwrap()
1157                        .to_ascii_uppercase()
1158                        .as_str()
1159                },
1160                |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphabetic_uppercase(&t),
1161            )
1162        }
1163        check::<2>();
1164        check::<3>();
1165        check::<4>();
1166        check::<5>();
1167        check::<8>();
1168        check::<16>();
1169    }
1170
1171    #[test]
1172    fn test_to_ascii_lowercase() {
1173        fn check<const N: usize>() {
1174            check_operation(
1175                |s| {
1176                    s.chars()
1177                        .map(|c| c.to_ascii_lowercase())
1178                        .collect::<String>()
1179                },
1180                |t: TinyAsciiStr<N>| TinyAsciiStr::to_ascii_lowercase(t).as_str().to_owned(),
1181            )
1182        }
1183        check::<2>();
1184        check::<3>();
1185        check::<4>();
1186        check::<5>();
1187        check::<8>();
1188        check::<16>();
1189    }
1190
1191    #[test]
1192    fn test_to_ascii_titlecase() {
1193        fn check<const N: usize>() {
1194            check_operation(
1195                |s| {
1196                    let mut r = s
1197                        .chars()
1198                        .map(|c| c.to_ascii_lowercase())
1199                        .collect::<String>();
1200                    // Safe because the string is nonempty and an ASCII string
1201                    unsafe { r.as_bytes_mut()[0].make_ascii_uppercase() };
1202                    r
1203                },
1204                |t: TinyAsciiStr<N>| TinyAsciiStr::to_ascii_titlecase(t).as_str().to_owned(),
1205            )
1206        }
1207        check::<2>();
1208        check::<3>();
1209        check::<4>();
1210        check::<5>();
1211        check::<8>();
1212        check::<16>();
1213    }
1214
1215    #[test]
1216    fn test_to_ascii_uppercase() {
1217        fn check<const N: usize>() {
1218            check_operation(
1219                |s| {
1220                    s.chars()
1221                        .map(|c| c.to_ascii_uppercase())
1222                        .collect::<String>()
1223                },
1224                |t: TinyAsciiStr<N>| TinyAsciiStr::to_ascii_uppercase(t).as_str().to_owned(),
1225            )
1226        }
1227        check::<2>();
1228        check::<3>();
1229        check::<4>();
1230        check::<5>();
1231        check::<8>();
1232        check::<16>();
1233    }
1234
1235    #[test]
1236    fn lossy_constructor() {
1237        assert_eq!(TinyAsciiStr::<4>::from_utf8_lossy(b"", b'?').as_str(), "");
1238        assert_eq!(
1239            TinyAsciiStr::<4>::from_utf8_lossy(b"oh\0o", b'?').as_str(),
1240            "oh?o"
1241        );
1242        assert_eq!(
1243            TinyAsciiStr::<4>::from_utf8_lossy(b"\0", b'?').as_str(),
1244            "?"
1245        );
1246        assert_eq!(
1247            TinyAsciiStr::<4>::from_utf8_lossy(b"toolong", b'?').as_str(),
1248            "tool"
1249        );
1250        assert_eq!(
1251            TinyAsciiStr::<4>::from_utf8_lossy(&[b'a', 0x80, 0xFF, b'1'], b'?').as_str(),
1252            "a??1"
1253        );
1254    }
1255
1256    #[test]
1257    #[should_panic(expected = "replacement must be a non-null ASCII byte")]
1258    fn from_utf8_lossy_rejects_non_ascii_replacement() {
1259        // A non-ASCII replacement byte (>= 0x80) would create an invalid AsciiByte
1260        // discriminant and then from_utf8_unchecked would be called on non-UTF-8
1261        // data (UB). The fix validates the replacement byte on entry.
1262        let _ = TinyAsciiStr::<4>::from_utf8_lossy(b"\xFF", 0xFF);
1263    }
1264
1265    #[test]
1266    #[should_panic(expected = "replacement must be a non-null ASCII byte")]
1267    fn from_utf16_lossy_rejects_non_ascii_replacement() {
1268        let _ = TinyAsciiStr::<4>::from_utf16_lossy(&[0xFFFF], 0xFF);
1269    }
1270
1271    #[test]
1272    #[should_panic(expected = "replacement must be a non-null ASCII byte")]
1273    fn from_utf8_lossy_rejects_null_replacement() {
1274        // Null (0x00) is also invalid — it's the TinyAsciiStr terminator.
1275        let _ = TinyAsciiStr::<4>::from_utf8_lossy(b"\xFF", 0x00);
1276    }
1277}