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::{self, FromStr};
14
15#[repr(transparent)]
16#[derive(#[automatically_derived]
impl<const N : usize> ::core::cmp::PartialEq for TinyAsciiStr<N> {
    #[inline]
    fn eq(&self, other: &TinyAsciiStr<N>) -> 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: &TinyAsciiStr<N>) -> ::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: &TinyAsciiStr<N>)
        -> ::core::option::Option<::core::cmp::Ordering> {
        ::core::cmp::PartialOrd::partial_cmp(&self.bytes, &other.bytes)
    }
}PartialOrd, #[automatically_derived]
impl<const N : usize> ::core::marker::Copy for TinyAsciiStr<N> { }Copy, #[automatically_derived]
impl<const N : usize> ::core::clone::Clone for TinyAsciiStr<N> {
    #[inline]
    fn clone(&self) -> TinyAsciiStr<N> {
        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    #[inline]
28    pub const fn try_from_str(s: &str) -> Result<Self, ParseError> {
29        Self::try_from_utf8(s.as_bytes())
30    }
31
32    /// Creates a `TinyAsciiStr<N>` from the given UTF-8 slice.
33    /// `code_units` may contain at most `N` non-null ASCII code points.
34    #[inline]
35    pub const fn try_from_utf8(code_units: &[u8]) -> Result<Self, ParseError> {
36        Self::try_from_utf8_inner(code_units, false)
37    }
38
39    /// Creates a `TinyAsciiStr<N>` from the given UTF-16 slice.
40    /// `code_units` may contain at most `N` non-null ASCII code points.
41    #[inline]
42    pub const fn try_from_utf16(code_units: &[u16]) -> Result<Self, ParseError> {
43        Self::try_from_utf16_inner(code_units, 0, code_units.len(), false)
44    }
45
46    /// Creates a `TinyAsciiStr<N>` from a UTF-8 slice, replacing invalid code units.
47    ///
48    /// Invalid code units, as well as null or non-ASCII code points
49    /// (i.e. those outside the range `U+0001..=U+007F`)
50    /// will be replaced with the replacement byte.
51    ///
52    /// The input slice will be truncated if its length exceeds `N`.
53    ///
54    /// # Panics
55    ///
56    /// Panics if `replacement` is not a non-null ASCII byte (i.e. not in `1..=127`).
57    pub const fn from_utf8_lossy(code_units: &[u8], replacement: u8) -> Self {
58        if !(replacement > 0 && replacement < 0x80) {
    {
        ::core::panicking::panic_fmt(format_args!("replacement must be a non-null ASCII byte (1..=127)"));
    }
};assert!(
59            replacement > 0 && replacement < 0x80,
60            "replacement must be a non-null ASCII byte (1..=127)"
61        );
62        let mut out = [0; N];
63        let mut i = 0;
64        // Ord is not available in const, so no `.min(N)`
65        let len = if code_units.len() > N {
66            N
67        } else {
68            code_units.len()
69        };
70
71        // Indexing is protected by the len check above
72        #[expect(clippy::indexing_slicing)]
73        while i < len {
74            let b = code_units[i];
75            if b > 0 && b < 0x80 {
76                out[i] = b;
77            } else {
78                out[i] = replacement;
79            }
80            i += 1;
81        }
82
83        Self {
84            // SAFETY: `out` only contains ASCII bytes and has same size as `self.bytes`
85            bytes: unsafe { AsciiByte::to_ascii_byte_array(&out) },
86        }
87    }
88
89    /// Creates a `TinyAsciiStr<N>` from a UTF-16 slice, replacing invalid code units.
90    ///
91    /// Invalid code units, as well as null or non-ASCII code points
92    /// (i.e. those outside the range `U+0001..=U+007F`)
93    /// will be replaced with the replacement byte.
94    ///
95    /// The input slice will be truncated if its length exceeds `N`.
96    ///
97    /// # Panics
98    ///
99    /// Panics if `replacement` is not a non-null ASCII byte (i.e. not in `1..=127`).
100    pub const fn from_utf16_lossy(code_units: &[u16], replacement: u8) -> Self {
101        if !(replacement > 0 && replacement < 0x80) {
    {
        ::core::panicking::panic_fmt(format_args!("replacement must be a non-null ASCII byte (1..=127)"));
    }
};assert!(
102            replacement > 0 && replacement < 0x80,
103            "replacement must be a non-null ASCII byte (1..=127)"
104        );
105        let mut out = [0; N];
106        let mut i = 0;
107        // Ord is not available in const, so no `.min(N)`
108        let len = if code_units.len() > N {
109            N
110        } else {
111            code_units.len()
112        };
113
114        // Indexing is protected by the len check above
115        #[expect(clippy::indexing_slicing)]
116        while i < len {
117            let b = code_units[i];
118            if b > 0 && b < 0x80 {
119                out[i] = b as u8;
120            } else {
121                out[i] = replacement;
122            }
123            i += 1;
124        }
125
126        Self {
127            // SAFETY: `out` only contains ASCII bytes and has same size as `self.bytes`
128            bytes: unsafe { AsciiByte::to_ascii_byte_array(&out) },
129        }
130    }
131
132    /// Attempts to parse a fixed-length byte array to a `TinyAsciiStr`.
133    ///
134    /// The byte array may contain trailing NUL bytes.
135    ///
136    /// # Example
137    ///
138    /// ```
139    /// use tinystr::tinystr;
140    /// use tinystr::TinyAsciiStr;
141    ///
142    /// assert_eq!(
143    ///     TinyAsciiStr::<3>::try_from_raw(*b"GB\0"),
144    ///     Ok(tinystr!(3, "GB"))
145    /// );
146    /// assert_eq!(
147    ///     TinyAsciiStr::<3>::try_from_raw(*b"USD"),
148    ///     Ok(tinystr!(3, "USD"))
149    /// );
150    /// assert!(TinyAsciiStr::<3>::try_from_raw(*b"\0A\0").is_err());
151    /// ```
152    pub const fn try_from_raw(raw: [u8; N]) -> Result<Self, ParseError> {
153        Self::try_from_utf8_inner(&raw, true)
154    }
155
156    pub(crate) const fn try_from_utf8_inner(
157        code_units: &[u8],
158        allow_trailing_null: bool,
159    ) -> Result<Self, ParseError> {
160        if code_units.len() > N {
161            return Err(ParseError::TooLong {
162                max: N,
163                len: code_units.len(),
164            });
165        }
166
167        let mut out = [0; N];
168        let mut i = 0;
169        let mut found_null = false;
170        // Indexing is protected by TinyStrError::TooLarge
171        #[expect(clippy::indexing_slicing)]
172        while i < code_units.len() {
173            let b = code_units[i];
174
175            if b == 0 {
176                found_null = true;
177            } else if b >= 0x80 {
178                return Err(ParseError::NonAscii);
179            } else if found_null {
180                // Error if there are contentful bytes after null
181                return Err(ParseError::ContainsNull);
182            }
183            out[i] = b;
184
185            i += 1;
186        }
187
188        if !allow_trailing_null && found_null {
189            // We found some trailing nulls, error
190            return Err(ParseError::ContainsNull);
191        }
192
193        Ok(Self {
194            // SAFETY: `out` only contains ASCII bytes and has same size as `self.bytes`
195            bytes: unsafe { AsciiByte::to_ascii_byte_array(&out) },
196        })
197    }
198
199    pub(crate) const fn try_from_utf16_inner(
200        code_units: &[u16],
201        start: usize,
202        end: usize,
203        allow_trailing_null: bool,
204    ) -> Result<Self, ParseError> {
205        let len = end - start;
206        if len > N {
207            return Err(ParseError::TooLong { max: N, len });
208        }
209
210        let mut out = [0; N];
211        let mut i = 0;
212        let mut found_null = false;
213        // Indexing is protected by TinyStrError::TooLarge
214        #[expect(clippy::indexing_slicing)]
215        while i < len {
216            let b = code_units[start + i];
217
218            if b == 0 {
219                found_null = true;
220            } else if b >= 0x80 {
221                return Err(ParseError::NonAscii);
222            } else if found_null {
223                // Error if there are contentful bytes after null
224                return Err(ParseError::ContainsNull);
225            }
226            out[i] = b as u8;
227
228            i += 1;
229        }
230
231        if !allow_trailing_null && found_null {
232            // We found some trailing nulls, error
233            return Err(ParseError::ContainsNull);
234        }
235
236        Ok(Self {
237            // SAFETY: `out` only contains ASCII bytes and has same size as `self.bytes`
238            bytes: unsafe { AsciiByte::to_ascii_byte_array(&out) },
239        })
240    }
241
242    /// Creates a `TinyAsciiStr<N>` containing the decimal representation of
243    /// the given unsigned integer.
244    ///
245    /// If the number of decimal digits exceeds `N`, the highest-magnitude
246    /// digits are truncated, and the lowest-magnitude digits are returned
247    /// as the error.
248    ///
249    /// Note: this function takes a u32. Larger integer types should probably
250    /// not be stored in a `TinyAsciiStr`.
251    ///
252    /// # Examples
253    ///
254    /// ```
255    /// use tinystr::tinystr;
256    /// use tinystr::TinyAsciiStr;
257    ///
258    /// let s0_4 = TinyAsciiStr::<4>::new_unsigned_decimal(0).unwrap();
259    /// let s456_4 = TinyAsciiStr::<4>::new_unsigned_decimal(456).unwrap();
260    /// let s456_3 = TinyAsciiStr::<3>::new_unsigned_decimal(456).unwrap();
261    /// let s456_2 = TinyAsciiStr::<2>::new_unsigned_decimal(456).unwrap_err();
262    ///
263    /// assert_eq!(s0_4, tinystr!(4, "0"));
264    /// assert_eq!(s456_4, tinystr!(4, "456"));
265    /// assert_eq!(s456_3, tinystr!(3, "456"));
266    /// assert_eq!(s456_2, tinystr!(2, "56"));
267    /// ```
268    ///
269    /// Example with saturating the value:
270    ///
271    /// ```
272    /// use tinystr::tinystr;
273    /// use tinystr::TinyAsciiStr;
274    ///
275    /// let str_truncated =
276    ///     TinyAsciiStr::<2>::new_unsigned_decimal(456).unwrap_or_else(|s| s);
277    /// let str_saturated = TinyAsciiStr::<2>::new_unsigned_decimal(456)
278    ///     .unwrap_or(tinystr!(2, "99"));
279    ///
280    /// assert_eq!(str_truncated, tinystr!(2, "56"));
281    /// assert_eq!(str_saturated, tinystr!(2, "99"));
282    /// ```
283    pub fn new_unsigned_decimal(number: u32) -> Result<Self, Self> {
284        let mut bytes = [AsciiByte::B0; N];
285        let mut x = number;
286        let mut i = 0usize;
287        #[expect(clippy::indexing_slicing)] // in-range: i < N
288        while i < N && (x != 0 || i == 0) {
289            bytes[N - i - 1] = AsciiByte::from_decimal_digit((x % 10) as u8);
290            x /= 10;
291            i += 1;
292        }
293        if i < N {
294            bytes.copy_within((N - i)..N, 0);
295            bytes[i..N].fill(AsciiByte::B0);
296        }
297        let s = Self { bytes };
298        if x != 0 {
299            Err(s)
300        } else {
301            Ok(s)
302        }
303    }
304
305    #[inline]
306    pub const fn as_str(&self) -> &str {
307        // as_utf8 is valid utf8
308        unsafe { str::from_utf8_unchecked(self.as_utf8()) }
309    }
310
311    #[inline]
312    #[must_use]
313    pub const fn len(&self) -> usize {
314        if N <= 4 {
315            Aligned4::from_ascii_bytes(&self.bytes).len()
316        } else if N <= 8 {
317            Aligned8::from_ascii_bytes(&self.bytes).len()
318        } else {
319            let mut i = 0;
320            #[expect(clippy::indexing_slicing)] // < N is safe
321            while i < N && self.bytes[i] as u8 != AsciiByte::B0 as u8 {
322                i += 1
323            }
324            i
325        }
326    }
327
328    #[inline]
329    #[must_use]
330    pub const fn is_empty(&self) -> bool {
331        self.bytes[0] as u8 == AsciiByte::B0 as u8
332    }
333
334    #[inline]
335    #[must_use]
336    pub const fn as_utf8(&self) -> &[u8] {
337        // Safe because `self.bytes.as_slice()` pointer-casts to `&[u8]`,
338        // and changing the length of that slice to self.len() < N is safe.
339        unsafe {
340            core::slice::from_raw_parts(self.bytes.as_slice().as_ptr() as *const u8, self.len())
341        }
342    }
343
344    #[inline]
345    #[must_use]
346    pub const fn all_bytes(&self) -> &[u8; N] {
347        // SAFETY: `self.bytes` has same size as [u8; N]
348        unsafe { &*(self.bytes.as_ptr() as *const [u8; N]) }
349    }
350
351    #[inline]
352    #[must_use]
353    /// Resizes a `TinyAsciiStr<N>` to a `TinyAsciiStr<M>`.
354    ///
355    /// If `M < len()` the string gets truncated, otherwise only the
356    /// memory representation changes.
357    pub const fn resize<const M: usize>(self) -> TinyAsciiStr<M> {
358        let mut bytes = [0; M];
359        let mut i = 0;
360        // Indexing is protected by the loop guard
361        #[expect(clippy::indexing_slicing)]
362        while i < M && i < N {
363            bytes[i] = self.bytes[i] as u8;
364            i += 1;
365        }
366        // `self.bytes` only contains ASCII bytes, with no null bytes between
367        // ASCII characters, so this also holds for `bytes`.
368        unsafe { TinyAsciiStr::from_utf8_unchecked(bytes) }
369    }
370
371    #[inline]
372    #[must_use]
373    /// Returns a `TinyAsciiStr<Q>` with the concatenation of this string,
374    /// `TinyAsciiStr<N>`, and another string, `TinyAsciiStr<M>`.
375    ///
376    /// If `Q < N + M`, the string gets truncated.
377    ///
378    /// # Examples
379    ///
380    /// ```
381    /// use tinystr::tinystr;
382    /// use tinystr::TinyAsciiStr;
383    ///
384    /// let abc = tinystr!(6, "abc");
385    /// let defg = tinystr!(6, "defg");
386    ///
387    /// // The concatenation is successful if Q is large enough...
388    /// assert_eq!(abc.concat(defg), tinystr!(16, "abcdefg"));
389    /// assert_eq!(abc.concat(defg), tinystr!(12, "abcdefg"));
390    /// assert_eq!(abc.concat(defg), tinystr!(8, "abcdefg"));
391    /// assert_eq!(abc.concat(defg), tinystr!(7, "abcdefg"));
392    ///
393    /// /// ...but it truncates of Q is too small.
394    /// assert_eq!(abc.concat(defg), tinystr!(6, "abcdef"));
395    /// assert_eq!(abc.concat(defg), tinystr!(2, "ab"));
396    /// ```
397    pub const fn concat<const M: usize, const Q: usize>(
398        self,
399        other: TinyAsciiStr<M>,
400    ) -> TinyAsciiStr<Q> {
401        let mut result = self.resize::<Q>();
402        let mut i = self.len();
403        let mut j = 0;
404        // Indexing is protected by the loop guard
405        #[expect(clippy::indexing_slicing)]
406        while i < Q && j < M {
407            result.bytes[i] = other.bytes[j];
408            i += 1;
409            j += 1;
410        }
411        result
412    }
413
414    /// # Safety
415    /// Must be called with a bytes array made of valid ASCII bytes, with no null bytes
416    /// between ASCII characters
417    #[must_use]
418    pub const unsafe fn from_utf8_unchecked(code_units: [u8; N]) -> Self {
419        Self {
420            bytes: AsciiByte::to_ascii_byte_array(&code_units),
421        }
422    }
423}
424
425macro_rules! check_is {
426    ($self:ident, $check_int:ident, $check_u8:ident) => {
427        if N <= 4 {
428            Aligned4::from_ascii_bytes(&$self.bytes).$check_int()
429        } else if N <= 8 {
430            Aligned8::from_ascii_bytes(&$self.bytes).$check_int()
431        } else {
432            let mut i = 0;
433            while i < N && $self.bytes[i] as u8 != AsciiByte::B0 as u8 {
434                if !($self.bytes[i] as u8).$check_u8() {
435                    return false;
436                }
437                i += 1;
438            }
439            true
440        }
441    };
442    ($self:ident, $check_int:ident, !$check_u8_0_inv:ident, !$check_u8_1_inv:ident) => {
443        if N <= 4 {
444            Aligned4::from_ascii_bytes(&$self.bytes).$check_int()
445        } else if N <= 8 {
446            Aligned8::from_ascii_bytes(&$self.bytes).$check_int()
447        } else {
448            // Won't panic because N is > 8
449            if ($self.bytes[0] as u8).$check_u8_0_inv() {
450                return false;
451            }
452            let mut i = 1;
453            while i < N && $self.bytes[i] as u8 != AsciiByte::B0 as u8 {
454                if ($self.bytes[i] as u8).$check_u8_1_inv() {
455                    return false;
456                }
457                i += 1;
458            }
459            true
460        }
461    };
462    ($self:ident, $check_int:ident, $check_u8_0_inv:ident, $check_u8_1_inv:ident) => {
463        if N <= 4 {
464            Aligned4::from_ascii_bytes(&$self.bytes).$check_int()
465        } else if N <= 8 {
466            Aligned8::from_ascii_bytes(&$self.bytes).$check_int()
467        } else {
468            // Won't panic because N is > 8
469            if !($self.bytes[0] as u8).$check_u8_0_inv() {
470                return false;
471            }
472            let mut i = 1;
473            while i < N && $self.bytes[i] as u8 != AsciiByte::B0 as u8 {
474                if !($self.bytes[i] as u8).$check_u8_1_inv() {
475                    return false;
476                }
477                i += 1;
478            }
479            true
480        }
481    };
482}
483
484impl<const N: usize> TinyAsciiStr<N> {
485    /// Checks if the value is composed of ASCII alphabetic characters:
486    ///
487    ///  * U+0041 'A' ..= U+005A 'Z', or
488    ///  * U+0061 'a' ..= U+007A 'z'.
489    ///
490    /// # Examples
491    ///
492    /// ```
493    /// use tinystr::TinyAsciiStr;
494    ///
495    /// let s1: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse.");
496    /// let s2: TinyAsciiStr<4> = "Te3t".parse().expect("Failed to parse.");
497    ///
498    /// assert!(s1.is_ascii_alphabetic());
499    /// assert!(!s2.is_ascii_alphabetic());
500    /// ```
501    #[inline]
502    #[must_use]
503    pub const fn is_ascii_alphabetic(&self) -> bool {
504        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)
505    }
506
507    /// Checks if the value is composed of ASCII alphanumeric characters:
508    ///
509    ///  * U+0041 'A' ..= U+005A 'Z', or
510    ///  * U+0061 'a' ..= U+007A 'z', or
511    ///  * U+0030 '0' ..= U+0039 '9'.
512    ///
513    /// # Examples
514    ///
515    /// ```
516    /// use tinystr::TinyAsciiStr;
517    ///
518    /// let s1: TinyAsciiStr<4> = "A15b".parse().expect("Failed to parse.");
519    /// let s2: TinyAsciiStr<4> = "[3@w".parse().expect("Failed to parse.");
520    ///
521    /// assert!(s1.is_ascii_alphanumeric());
522    /// assert!(!s2.is_ascii_alphanumeric());
523    /// ```
524    #[inline]
525    #[must_use]
526    pub const fn is_ascii_alphanumeric(&self) -> bool {
527        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)
528    }
529
530    /// Checks if the value is composed of ASCII decimal digits:
531    ///
532    ///  * U+0030 '0' ..= U+0039 '9'.
533    ///
534    /// # Examples
535    ///
536    /// ```
537    /// use tinystr::TinyAsciiStr;
538    ///
539    /// let s1: TinyAsciiStr<4> = "312".parse().expect("Failed to parse.");
540    /// let s2: TinyAsciiStr<4> = "3d".parse().expect("Failed to parse.");
541    ///
542    /// assert!(s1.is_ascii_numeric());
543    /// assert!(!s2.is_ascii_numeric());
544    /// ```
545    #[inline]
546    #[must_use]
547    pub const fn is_ascii_numeric(&self) -> bool {
548        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)
549    }
550
551    /// Checks if the value is in ASCII lower case.
552    ///
553    /// All letter characters are checked for case. Non-letter characters are ignored.
554    ///
555    /// # Examples
556    ///
557    /// ```
558    /// use tinystr::TinyAsciiStr;
559    ///
560    /// let s1: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
561    /// let s2: TinyAsciiStr<4> = "test".parse().expect("Failed to parse.");
562    /// let s3: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
563    ///
564    /// assert!(!s1.is_ascii_lowercase());
565    /// assert!(s2.is_ascii_lowercase());
566    /// assert!(s3.is_ascii_lowercase());
567    /// ```
568    #[inline]
569    #[must_use]
570    pub const fn is_ascii_lowercase(&self) -> bool {
571        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!(
572            self,
573            is_ascii_lowercase,
574            !is_ascii_uppercase,
575            !is_ascii_uppercase
576        )
577    }
578
579    /// Checks if the value is in ASCII title case.
580    ///
581    /// This verifies that the first character is ASCII uppercase and all others ASCII lowercase.
582    /// Non-letter characters are ignored.
583    ///
584    /// # Examples
585    ///
586    /// ```
587    /// use tinystr::TinyAsciiStr;
588    ///
589    /// let s1: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
590    /// let s2: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse.");
591    /// let s3: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
592    ///
593    /// assert!(!s1.is_ascii_titlecase());
594    /// assert!(s2.is_ascii_titlecase());
595    /// assert!(s3.is_ascii_titlecase());
596    /// ```
597    #[inline]
598    #[must_use]
599    pub const fn is_ascii_titlecase(&self) -> bool {
600        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!(
601            self,
602            is_ascii_titlecase,
603            !is_ascii_lowercase,
604            !is_ascii_uppercase
605        )
606    }
607
608    /// Checks if the value is in ASCII upper case.
609    ///
610    /// All letter characters are checked for case. Non-letter characters are ignored.
611    ///
612    /// # Examples
613    ///
614    /// ```
615    /// use tinystr::TinyAsciiStr;
616    ///
617    /// let s1: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
618    /// let s2: TinyAsciiStr<4> = "TEST".parse().expect("Failed to parse.");
619    /// let s3: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
620    ///
621    /// assert!(!s1.is_ascii_uppercase());
622    /// assert!(s2.is_ascii_uppercase());
623    /// assert!(!s3.is_ascii_uppercase());
624    /// ```
625    #[inline]
626    #[must_use]
627    pub const fn is_ascii_uppercase(&self) -> bool {
628        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!(
629            self,
630            is_ascii_uppercase,
631            !is_ascii_lowercase,
632            !is_ascii_lowercase
633        )
634    }
635
636    /// Checks if the value is composed of ASCII alphabetic lower case characters:
637    ///
638    ///  * U+0061 'a' ..= U+007A 'z',
639    ///
640    /// # Examples
641    ///
642    /// ```
643    /// use tinystr::TinyAsciiStr;
644    ///
645    /// let s1: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse.");
646    /// let s2: TinyAsciiStr<4> = "Te3t".parse().expect("Failed to parse.");
647    /// let s3: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
648    /// let s4: TinyAsciiStr<4> = "test".parse().expect("Failed to parse.");
649    /// let s5: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
650    ///
651    /// assert!(!s1.is_ascii_alphabetic_lowercase());
652    /// assert!(!s2.is_ascii_alphabetic_lowercase());
653    /// assert!(!s3.is_ascii_alphabetic_lowercase());
654    /// assert!(s4.is_ascii_alphabetic_lowercase());
655    /// assert!(!s5.is_ascii_alphabetic_lowercase());
656    /// ```
657    #[inline]
658    #[must_use]
659    pub const fn is_ascii_alphabetic_lowercase(&self) -> bool {
660        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!(
661            self,
662            is_ascii_alphabetic_lowercase,
663            is_ascii_lowercase,
664            is_ascii_lowercase
665        )
666    }
667
668    /// Checks if the value is composed of ASCII alphabetic, with the first character being ASCII uppercase, and all others ASCII lowercase.
669    ///
670    /// # Examples
671    ///
672    /// ```
673    /// use tinystr::TinyAsciiStr;
674    ///
675    /// let s1: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse.");
676    /// let s2: TinyAsciiStr<4> = "Te3t".parse().expect("Failed to parse.");
677    /// let s3: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
678    /// let s4: TinyAsciiStr<4> = "test".parse().expect("Failed to parse.");
679    /// let s5: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
680    ///
681    /// assert!(s1.is_ascii_alphabetic_titlecase());
682    /// assert!(!s2.is_ascii_alphabetic_titlecase());
683    /// assert!(!s3.is_ascii_alphabetic_titlecase());
684    /// assert!(!s4.is_ascii_alphabetic_titlecase());
685    /// assert!(!s5.is_ascii_alphabetic_titlecase());
686    /// ```
687    #[inline]
688    #[must_use]
689    pub const fn is_ascii_alphabetic_titlecase(&self) -> bool {
690        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!(
691            self,
692            is_ascii_alphabetic_titlecase,
693            is_ascii_uppercase,
694            is_ascii_lowercase
695        )
696    }
697
698    /// Checks if the value is composed of ASCII alphabetic upper case characters:
699    ///
700    ///  * U+0041 'A' ..= U+005A 'Z',
701    ///
702    /// # Examples
703    ///
704    /// ```
705    /// use tinystr::TinyAsciiStr;
706    ///
707    /// let s1: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse.");
708    /// let s2: TinyAsciiStr<4> = "Te3t".parse().expect("Failed to parse.");
709    /// let s3: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
710    /// let s4: TinyAsciiStr<4> = "TEST".parse().expect("Failed to parse.");
711    /// let s5: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
712    ///
713    /// assert!(!s1.is_ascii_alphabetic_uppercase());
714    /// assert!(!s2.is_ascii_alphabetic_uppercase());
715    /// assert!(!s3.is_ascii_alphabetic_uppercase());
716    /// assert!(s4.is_ascii_alphabetic_uppercase());
717    /// assert!(!s5.is_ascii_alphabetic_uppercase());
718    /// ```
719    #[inline]
720    #[must_use]
721    pub const fn is_ascii_alphabetic_uppercase(&self) -> bool {
722        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!(
723            self,
724            is_ascii_alphabetic_uppercase,
725            is_ascii_uppercase,
726            is_ascii_uppercase
727        )
728    }
729}
730
731macro_rules! to {
732    ($self:ident, $to:ident, $later_char_to:ident $(,$first_char_to:ident)?) => {{
733        let mut i = 0;
734        if N <= 4 {
735            let aligned = Aligned4::from_ascii_bytes(&$self.bytes).$to();
736            // Won't panic because self.bytes has length N and aligned has length >= N
737            #[expect(clippy::indexing_slicing)]
738            while i < N {
739                $self.bytes[i] = aligned[i];
740                i += 1;
741            }
742        } else if N <= 8 {
743            let aligned = Aligned8::from_ascii_bytes(&$self.bytes).$to();
744            // Won't panic because self.bytes has length N and aligned has length >= N
745            #[expect(clippy::indexing_slicing)]
746            while i < N {
747                $self.bytes[i] = aligned[i];
748                i += 1;
749            }
750        } else {
751            while i < N && $self.bytes[i] as u8 != AsciiByte::B0 as u8 {
752                $self.bytes[i] = $self.bytes[i].$later_char_to();
753                i += 1;
754            }
755            $(
756                $self.bytes[0] = $self.bytes[0].$first_char_to();
757            )?
758        }
759        $self
760    }};
761}
762
763impl<const N: usize> TinyAsciiStr<N> {
764    /// Converts this type to its ASCII lower case equivalent in-place.
765    ///
766    /// ASCII letters 'A' to 'Z' are mapped to 'a' to 'z', other characters are unchanged.
767    ///
768    /// # Examples
769    ///
770    /// ```
771    /// use tinystr::TinyAsciiStr;
772    ///
773    /// let s1: TinyAsciiStr<4> = "TeS3".parse().expect("Failed to parse.");
774    ///
775    /// assert_eq!(&*s1.to_ascii_lowercase(), "tes3");
776    /// ```
777    #[inline]
778    #[must_use]
779    pub const fn to_ascii_lowercase(mut self) -> Self {
780        {
    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)
781    }
782
783    /// Converts this type to its ASCII title case equivalent in-place.
784    ///
785    /// The first character is converted to ASCII uppercase; the remaining characters
786    /// are converted to ASCII lowercase.
787    ///
788    /// # Examples
789    ///
790    /// ```
791    /// use tinystr::TinyAsciiStr;
792    ///
793    /// let s1: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
794    ///
795    /// assert_eq!(&*s1.to_ascii_titlecase(), "Test");
796    /// ```
797    #[inline]
798    #[must_use]
799    pub const fn to_ascii_titlecase(mut self) -> Self {
800        {
    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!(
801            self,
802            to_ascii_titlecase,
803            to_ascii_lowercase,
804            to_ascii_uppercase
805        )
806    }
807
808    /// Converts this type to its ASCII upper case equivalent in-place.
809    ///
810    /// ASCII letters 'a' to 'z' are mapped to 'A' to 'Z', other characters are unchanged.
811    ///
812    /// # Examples
813    ///
814    /// ```
815    /// use tinystr::TinyAsciiStr;
816    ///
817    /// let s1: TinyAsciiStr<4> = "Tes3".parse().expect("Failed to parse.");
818    ///
819    /// assert_eq!(&*s1.to_ascii_uppercase(), "TES3");
820    /// ```
821    #[inline]
822    #[must_use]
823    pub const fn to_ascii_uppercase(mut self) -> Self {
824        {
    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)
825    }
826}
827
828impl<const N: usize> fmt::Debug for TinyAsciiStr<N> {
829    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
830        fmt::Debug::fmt(self.as_str(), f)
831    }
832}
833
834impl<const N: usize> fmt::Display for TinyAsciiStr<N> {
835    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
836        fmt::Display::fmt(self.as_str(), f)
837    }
838}
839
840impl<const N: usize> Deref for TinyAsciiStr<N> {
841    type Target = str;
842    #[inline]
843    fn deref(&self) -> &str {
844        self.as_str()
845    }
846}
847
848impl<const N: usize> Borrow<str> for TinyAsciiStr<N> {
849    #[inline]
850    fn borrow(&self) -> &str {
851        self.as_str()
852    }
853}
854
855impl<const N: usize> FromStr for TinyAsciiStr<N> {
856    type Err = ParseError;
857    #[inline]
858    fn from_str(s: &str) -> Result<Self, Self::Err> {
859        Self::try_from_str(s)
860    }
861}
862
863impl<const N: usize> PartialEq<str> for TinyAsciiStr<N> {
864    fn eq(&self, other: &str) -> bool {
865        self.deref() == other
866    }
867}
868
869impl<const N: usize> PartialEq<&str> for TinyAsciiStr<N> {
870    fn eq(&self, other: &&str) -> bool {
871        self.deref() == *other
872    }
873}
874
875#[cfg(feature = "alloc")]
876impl<const N: usize> PartialEq<String> for TinyAsciiStr<N> {
877    fn eq(&self, other: &String) -> bool {
878        self.deref() == other.deref()
879    }
880}
881
882#[cfg(feature = "alloc")]
883impl<const N: usize> PartialEq<TinyAsciiStr<N>> for String {
884    fn eq(&self, other: &TinyAsciiStr<N>) -> bool {
885        self.deref() == other.deref()
886    }
887}
888
889#[cfg(test)]
890mod test {
891    use super::*;
892    use rand::distr::Distribution;
893    use rand::distr::StandardUniform;
894    use rand::rngs::SmallRng;
895    use rand::SeedableRng;
896
897    const STRINGS: [&str; 26] = [
898        "Latn",
899        "laTn",
900        "windows",
901        "AR",
902        "Hans",
903        "macos",
904        "AT",
905        "infiniband",
906        "FR",
907        "en",
908        "Cyrl",
909        "FromIntegral",
910        "NO",
911        "419",
912        "MacintoshOSX2019",
913        "a3z",
914        "A3z",
915        "A3Z",
916        "a3Z",
917        "3A",
918        "3Z",
919        "3a",
920        "3z",
921        "@@[`{",
922        "UK",
923        "E12",
924    ];
925
926    fn gen_strings(num_strings: usize, allowed_lengths: &[usize]) -> Vec<String> {
927        use rand::seq::IndexedRandom;
928        let mut rng = SmallRng::seed_from_u64(2022);
929        // Need to do this in 2 steps since the RNG is needed twice
930        let string_lengths = core::iter::repeat_with(|| *allowed_lengths.choose(&mut rng).unwrap())
931            .take(num_strings)
932            .collect::<Vec<usize>>();
933        string_lengths
934            .iter()
935            .map(|len| {
936                StandardUniform
937                    .sample_iter(&mut rng)
938                    .filter(|b: &u8| *b > 0 && *b < 0x80)
939                    .take(*len)
940                    .collect::<Vec<u8>>()
941            })
942            .map(|byte_vec| String::from_utf8(byte_vec).expect("All ASCII"))
943            .collect()
944    }
945
946    fn check_operation<T, F1, F2, const N: usize>(reference_f: F1, tinystr_f: F2)
947    where
948        F1: Fn(&str) -> T,
949        F2: Fn(TinyAsciiStr<N>) -> T,
950        T: fmt::Debug + PartialEq,
951    {
952        for s in STRINGS
953            .into_iter()
954            .map(str::to_owned)
955            .chain(gen_strings(100, &[3, 4, 5, 8, 12]))
956        {
957            let t = match TinyAsciiStr::<N>::from_str(&s) {
958                Ok(t) => t,
959                Err(ParseError::TooLong { .. }) => continue,
960                Err(e) => panic!("{}", e),
961            };
962            let expected = reference_f(&s);
963            let actual = tinystr_f(t);
964            assert_eq!(expected, actual, "TinyAsciiStr<{N}>: {s:?}");
965
966            let s_utf16: Vec<u16> = s.encode_utf16().collect();
967            let t = match TinyAsciiStr::<N>::try_from_utf16(&s_utf16) {
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    }
977
978    #[test]
979    fn test_is_ascii_alphabetic() {
980        fn check<const N: usize>() {
981            check_operation(
982                |s| s.chars().all(|c| c.is_ascii_alphabetic()),
983                |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphabetic(&t),
984            )
985        }
986        check::<2>();
987        check::<3>();
988        check::<4>();
989        check::<5>();
990        check::<8>();
991        check::<16>();
992    }
993
994    #[test]
995    fn test_is_ascii_alphanumeric() {
996        fn check<const N: usize>() {
997            check_operation(
998                |s| s.chars().all(|c| c.is_ascii_alphanumeric()),
999                |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphanumeric(&t),
1000            )
1001        }
1002        check::<2>();
1003        check::<3>();
1004        check::<4>();
1005        check::<5>();
1006        check::<8>();
1007        check::<16>();
1008    }
1009
1010    #[test]
1011    fn test_is_ascii_numeric() {
1012        fn check<const N: usize>() {
1013            check_operation(
1014                |s| s.chars().all(|c| c.is_ascii_digit()),
1015                |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_numeric(&t),
1016            )
1017        }
1018        check::<2>();
1019        check::<3>();
1020        check::<4>();
1021        check::<5>();
1022        check::<8>();
1023        check::<16>();
1024    }
1025
1026    #[test]
1027    fn test_is_ascii_lowercase() {
1028        fn check<const N: usize>() {
1029            check_operation(
1030                |s| {
1031                    s == TinyAsciiStr::<16>::try_from_str(s)
1032                        .unwrap()
1033                        .to_ascii_lowercase()
1034                        .as_str()
1035                },
1036                |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_lowercase(&t),
1037            )
1038        }
1039        check::<2>();
1040        check::<3>();
1041        check::<4>();
1042        check::<5>();
1043        check::<8>();
1044        check::<16>();
1045    }
1046
1047    #[test]
1048    fn test_is_ascii_titlecase() {
1049        fn check<const N: usize>() {
1050            check_operation(
1051                |s| {
1052                    s == TinyAsciiStr::<16>::try_from_str(s)
1053                        .unwrap()
1054                        .to_ascii_titlecase()
1055                        .as_str()
1056                },
1057                |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_titlecase(&t),
1058            )
1059        }
1060        check::<2>();
1061        check::<3>();
1062        check::<4>();
1063        check::<5>();
1064        check::<8>();
1065        check::<16>();
1066    }
1067
1068    #[test]
1069    fn test_is_ascii_uppercase() {
1070        fn check<const N: usize>() {
1071            check_operation(
1072                |s| {
1073                    s == TinyAsciiStr::<16>::try_from_str(s)
1074                        .unwrap()
1075                        .to_ascii_uppercase()
1076                        .as_str()
1077                },
1078                |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_uppercase(&t),
1079            )
1080        }
1081        check::<2>();
1082        check::<3>();
1083        check::<4>();
1084        check::<5>();
1085        check::<8>();
1086        check::<16>();
1087    }
1088
1089    #[test]
1090    fn test_is_ascii_alphabetic_lowercase() {
1091        fn check<const N: usize>() {
1092            check_operation(
1093                |s| {
1094                    // Check alphabetic
1095                    s.chars().all(|c| c.is_ascii_alphabetic()) &&
1096                    // Check lowercase
1097                    s == TinyAsciiStr::<16>::try_from_str(s)
1098                        .unwrap()
1099                        .to_ascii_lowercase()
1100                        .as_str()
1101                },
1102                |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphabetic_lowercase(&t),
1103            )
1104        }
1105        check::<2>();
1106        check::<3>();
1107        check::<4>();
1108        check::<5>();
1109        check::<8>();
1110        check::<16>();
1111    }
1112
1113    #[test]
1114    fn test_is_ascii_alphabetic_titlecase() {
1115        fn check<const N: usize>() {
1116            check_operation(
1117                |s| {
1118                    // Check alphabetic
1119                    s.chars().all(|c| c.is_ascii_alphabetic()) &&
1120                    // Check titlecase
1121                    s == TinyAsciiStr::<16>::try_from_str(s)
1122                        .unwrap()
1123                        .to_ascii_titlecase()
1124                        .as_str()
1125                },
1126                |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphabetic_titlecase(&t),
1127            )
1128        }
1129        check::<2>();
1130        check::<3>();
1131        check::<4>();
1132        check::<5>();
1133        check::<8>();
1134        check::<16>();
1135    }
1136
1137    #[test]
1138    fn test_is_ascii_alphabetic_uppercase() {
1139        fn check<const N: usize>() {
1140            check_operation(
1141                |s| {
1142                    // Check alphabetic
1143                    s.chars().all(|c| c.is_ascii_alphabetic()) &&
1144                    // Check uppercase
1145                    s == TinyAsciiStr::<16>::try_from_str(s)
1146                        .unwrap()
1147                        .to_ascii_uppercase()
1148                        .as_str()
1149                },
1150                |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphabetic_uppercase(&t),
1151            )
1152        }
1153        check::<2>();
1154        check::<3>();
1155        check::<4>();
1156        check::<5>();
1157        check::<8>();
1158        check::<16>();
1159    }
1160
1161    #[test]
1162    fn test_to_ascii_lowercase() {
1163        fn check<const N: usize>() {
1164            check_operation(
1165                |s| {
1166                    s.chars()
1167                        .map(|c| c.to_ascii_lowercase())
1168                        .collect::<String>()
1169                },
1170                |t: TinyAsciiStr<N>| TinyAsciiStr::to_ascii_lowercase(t).as_str().to_owned(),
1171            )
1172        }
1173        check::<2>();
1174        check::<3>();
1175        check::<4>();
1176        check::<5>();
1177        check::<8>();
1178        check::<16>();
1179    }
1180
1181    #[test]
1182    fn test_to_ascii_titlecase() {
1183        fn check<const N: usize>() {
1184            check_operation(
1185                |s| {
1186                    let mut r = s
1187                        .chars()
1188                        .map(|c| c.to_ascii_lowercase())
1189                        .collect::<String>();
1190                    // Safe because the string is nonempty and an ASCII string
1191                    unsafe { r.as_bytes_mut()[0].make_ascii_uppercase() };
1192                    r
1193                },
1194                |t: TinyAsciiStr<N>| TinyAsciiStr::to_ascii_titlecase(t).as_str().to_owned(),
1195            )
1196        }
1197        check::<2>();
1198        check::<3>();
1199        check::<4>();
1200        check::<5>();
1201        check::<8>();
1202        check::<16>();
1203    }
1204
1205    #[test]
1206    fn test_to_ascii_uppercase() {
1207        fn check<const N: usize>() {
1208            check_operation(
1209                |s| {
1210                    s.chars()
1211                        .map(|c| c.to_ascii_uppercase())
1212                        .collect::<String>()
1213                },
1214                |t: TinyAsciiStr<N>| TinyAsciiStr::to_ascii_uppercase(t).as_str().to_owned(),
1215            )
1216        }
1217        check::<2>();
1218        check::<3>();
1219        check::<4>();
1220        check::<5>();
1221        check::<8>();
1222        check::<16>();
1223    }
1224
1225    #[test]
1226    fn lossy_constructor() {
1227        assert_eq!(TinyAsciiStr::<4>::from_utf8_lossy(b"", b'?').as_str(), "");
1228        assert_eq!(
1229            TinyAsciiStr::<4>::from_utf8_lossy(b"oh\0o", b'?').as_str(),
1230            "oh?o"
1231        );
1232        assert_eq!(
1233            TinyAsciiStr::<4>::from_utf8_lossy(b"\0", b'?').as_str(),
1234            "?"
1235        );
1236        assert_eq!(
1237            TinyAsciiStr::<4>::from_utf8_lossy(b"toolong", b'?').as_str(),
1238            "tool"
1239        );
1240        assert_eq!(
1241            TinyAsciiStr::<4>::from_utf8_lossy(&[b'a', 0x80, 0xFF, b'1'], b'?').as_str(),
1242            "a??1"
1243        );
1244    }
1245
1246    #[test]
1247    #[should_panic(expected = "replacement must be a non-null ASCII byte")]
1248    fn from_utf8_lossy_rejects_non_ascii_replacement() {
1249        // A non-ASCII replacement byte (>= 0x80) would create an invalid AsciiByte
1250        // discriminant and then from_utf8_unchecked would be called on non-UTF-8
1251        // data (UB). The fix validates the replacement byte on entry.
1252        let _ = TinyAsciiStr::<4>::from_utf8_lossy(b"\xFF", 0xFF);
1253    }
1254
1255    #[test]
1256    #[should_panic(expected = "replacement must be a non-null ASCII byte")]
1257    fn from_utf16_lossy_rejects_non_ascii_replacement() {
1258        let _ = TinyAsciiStr::<4>::from_utf16_lossy(&[0xFFFF], 0xFF);
1259    }
1260
1261    #[test]
1262    #[should_panic(expected = "replacement must be a non-null ASCII byte")]
1263    fn from_utf8_lossy_rejects_null_replacement() {
1264        // Null (0x00) is also invalid — it's the TinyAsciiStr terminator.
1265        let _ = TinyAsciiStr::<4>::from_utf8_lossy(b"\xFF", 0x00);
1266    }
1267}