Coverage Report

Created: 2026-07-25 06:08

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/tinystr-0.8.3/src/ascii.rs
Line
Count
Source
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
5
use crate::asciibyte::AsciiByte;
6
use crate::int_ops::{Aligned4, Aligned8};
7
use crate::ParseError;
8
#[cfg(feature = "alloc")]
9
use alloc::string::String;
10
use core::borrow::Borrow;
11
use core::fmt;
12
use core::ops::Deref;
13
use core::str::{self, FromStr};
14
15
#[repr(transparent)]
16
#[derive(PartialEq, Eq, Ord, PartialOrd, Copy, Clone, Hash)]
17
pub struct TinyAsciiStr<const N: usize> {
18
    bytes: [AsciiByte; N],
19
}
20
21
impl<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
0
    pub const fn try_from_str(s: &str) -> Result<Self, ParseError> {
29
0
        Self::try_from_utf8(s.as_bytes())
30
0
    }
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
0
    pub const fn try_from_utf8(code_units: &[u8]) -> Result<Self, ParseError> {
36
0
        Self::try_from_utf8_inner(code_units, false)
37
0
    }
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<2>>::try_from_utf8
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::try_from_utf8
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::try_from_utf8
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8>>::try_from_utf8
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::try_from_utf8
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
0
    pub const fn try_from_utf16(code_units: &[u16]) -> Result<Self, ParseError> {
43
0
        Self::try_from_utf16_inner(code_units, 0, code_units.len(), false)
44
0
    }
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
0
    pub const fn from_utf8_lossy(code_units: &[u8], replacement: u8) -> Self {
58
0
        assert!(
59
0
            replacement > 0 && replacement < 0x80,
60
0
            "replacement must be a non-null ASCII byte (1..=127)"
61
        );
62
0
        let mut out = [0; N];
63
0
        let mut i = 0;
64
        // Ord is not available in const, so no `.min(N)`
65
0
        let len = if code_units.len() > N {
66
0
            N
67
        } else {
68
0
            code_units.len()
69
        };
70
71
        // Indexing is protected by the len check above
72
        #[expect(clippy::indexing_slicing)]
73
0
        while i < len {
74
0
            let b = code_units[i];
75
0
            if b > 0 && b < 0x80 {
76
0
                out[i] = b;
77
0
            } else {
78
0
                out[i] = replacement;
79
0
            }
80
0
            i += 1;
81
        }
82
83
0
        Self {
84
0
            // SAFETY: `out` only contains ASCII bytes and has same size as `self.bytes`
85
0
            bytes: unsafe { AsciiByte::to_ascii_byte_array(&out) },
86
0
        }
87
0
    }
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
0
    pub const fn from_utf16_lossy(code_units: &[u16], replacement: u8) -> Self {
101
0
        assert!(
102
0
            replacement > 0 && replacement < 0x80,
103
0
            "replacement must be a non-null ASCII byte (1..=127)"
104
        );
105
0
        let mut out = [0; N];
106
0
        let mut i = 0;
107
        // Ord is not available in const, so no `.min(N)`
108
0
        let len = if code_units.len() > N {
109
0
            N
110
        } else {
111
0
            code_units.len()
112
        };
113
114
        // Indexing is protected by the len check above
115
        #[expect(clippy::indexing_slicing)]
116
0
        while i < len {
117
0
            let b = code_units[i];
118
0
            if b > 0 && b < 0x80 {
119
0
                out[i] = b as u8;
120
0
            } else {
121
0
                out[i] = replacement;
122
0
            }
123
0
            i += 1;
124
        }
125
126
0
        Self {
127
0
            // SAFETY: `out` only contains ASCII bytes and has same size as `self.bytes`
128
0
            bytes: unsafe { AsciiByte::to_ascii_byte_array(&out) },
129
0
        }
130
0
    }
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
0
    pub const fn try_from_raw(raw: [u8; N]) -> Result<Self, ParseError> {
153
0
        Self::try_from_utf8_inner(&raw, true)
154
0
    }
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<2>>::try_from_raw
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::try_from_raw
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::try_from_raw
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8>>::try_from_raw
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::try_from_raw
155
156
0
    pub(crate) const fn try_from_utf8_inner(
157
0
        code_units: &[u8],
158
0
        allow_trailing_null: bool,
159
0
    ) -> Result<Self, ParseError> {
160
0
        if code_units.len() > N {
161
0
            return Err(ParseError::TooLong {
162
0
                max: N,
163
0
                len: code_units.len(),
164
0
            });
165
0
        }
166
167
0
        let mut out = [0; N];
168
0
        let mut i = 0;
169
0
        let mut found_null = false;
170
        // Indexing is protected by TinyStrError::TooLarge
171
        #[expect(clippy::indexing_slicing)]
172
0
        while i < code_units.len() {
173
0
            let b = code_units[i];
174
175
0
            if b == 0 {
176
0
                found_null = true;
177
0
            } else if b >= 0x80 {
178
0
                return Err(ParseError::NonAscii);
179
0
            } else if found_null {
180
                // Error if there are contentful bytes after null
181
0
                return Err(ParseError::ContainsNull);
182
0
            }
183
0
            out[i] = b;
184
185
0
            i += 1;
186
        }
187
188
0
        if !allow_trailing_null && found_null {
189
            // We found some trailing nulls, error
190
0
            return Err(ParseError::ContainsNull);
191
0
        }
192
193
0
        Ok(Self {
194
0
            // SAFETY: `out` only contains ASCII bytes and has same size as `self.bytes`
195
0
            bytes: unsafe { AsciiByte::to_ascii_byte_array(&out) },
196
0
        })
197
0
    }
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<2>>::try_from_utf8_inner
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::try_from_utf8_inner
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::try_from_utf8_inner
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8>>::try_from_utf8_inner
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::try_from_utf8_inner
198
199
0
    pub(crate) const fn try_from_utf16_inner(
200
0
        code_units: &[u16],
201
0
        start: usize,
202
0
        end: usize,
203
0
        allow_trailing_null: bool,
204
0
    ) -> Result<Self, ParseError> {
205
0
        let len = end - start;
206
0
        if len > N {
207
0
            return Err(ParseError::TooLong { max: N, len });
208
0
        }
209
210
0
        let mut out = [0; N];
211
0
        let mut i = 0;
212
0
        let mut found_null = false;
213
        // Indexing is protected by TinyStrError::TooLarge
214
        #[expect(clippy::indexing_slicing)]
215
0
        while i < len {
216
0
            let b = code_units[start + i];
217
218
0
            if b == 0 {
219
0
                found_null = true;
220
0
            } else if b >= 0x80 {
221
0
                return Err(ParseError::NonAscii);
222
0
            } else if found_null {
223
                // Error if there are contentful bytes after null
224
0
                return Err(ParseError::ContainsNull);
225
0
            }
226
0
            out[i] = b as u8;
227
228
0
            i += 1;
229
        }
230
231
0
        if !allow_trailing_null && found_null {
232
            // We found some trailing nulls, error
233
0
            return Err(ParseError::ContainsNull);
234
0
        }
235
236
0
        Ok(Self {
237
0
            // SAFETY: `out` only contains ASCII bytes and has same size as `self.bytes`
238
0
            bytes: unsafe { AsciiByte::to_ascii_byte_array(&out) },
239
0
        })
240
0
    }
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
0
    pub fn new_unsigned_decimal(number: u32) -> Result<Self, Self> {
284
0
        let mut bytes = [AsciiByte::B0; N];
285
0
        let mut x = number;
286
0
        let mut i = 0usize;
287
        #[expect(clippy::indexing_slicing)] // in-range: i < N
288
0
        while i < N && (x != 0 || i == 0) {
289
0
            bytes[N - i - 1] = AsciiByte::from_decimal_digit((x % 10) as u8);
290
0
            x /= 10;
291
0
            i += 1;
292
0
        }
293
0
        if i < N {
294
0
            bytes.copy_within((N - i)..N, 0);
295
0
            bytes[i..N].fill(AsciiByte::B0);
296
0
        }
297
0
        let s = Self { bytes };
298
0
        if x != 0 {
299
0
            Err(s)
300
        } else {
301
0
            Ok(s)
302
        }
303
0
    }
304
305
    #[inline]
306
0
    pub const fn as_str(&self) -> &str {
307
        // as_utf8 is valid utf8
308
0
        unsafe { str::from_utf8_unchecked(self.as_utf8()) }
309
0
    }
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<2>>::as_str
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::as_str
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::as_str
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8>>::as_str
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::as_str
310
311
    #[inline]
312
    #[must_use]
313
0
    pub const fn len(&self) -> usize {
314
0
        if N <= 4 {
315
0
            Aligned4::from_ascii_bytes(&self.bytes).len()
316
0
        } else if N <= 8 {
317
0
            Aligned8::from_ascii_bytes(&self.bytes).len()
318
        } else {
319
0
            let mut i = 0;
320
            #[expect(clippy::indexing_slicing)] // < N is safe
321
0
            while i < N && self.bytes[i] as u8 != AsciiByte::B0 as u8 {
322
0
                i += 1
323
            }
324
0
            i
325
        }
326
0
    }
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<2>>::len
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::len
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::len
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8>>::len
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::len
327
328
    #[inline]
329
    #[must_use]
330
0
    pub const fn is_empty(&self) -> bool {
331
0
        self.bytes[0] as u8 == AsciiByte::B0 as u8
332
0
    }
333
334
    #[inline]
335
    #[must_use]
336
0
    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
0
            core::slice::from_raw_parts(self.bytes.as_slice().as_ptr() as *const u8, self.len())
341
        }
342
0
    }
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<2>>::as_utf8
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::as_utf8
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::as_utf8
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8>>::as_utf8
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::as_utf8
343
344
    #[inline]
345
    #[must_use]
346
0
    pub const fn all_bytes(&self) -> &[u8; N] {
347
        // SAFETY: `self.bytes` has same size as [u8; N]
348
0
        unsafe { &*(self.bytes.as_ptr() as *const [u8; N]) }
349
0
    }
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<2>>::all_bytes
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::all_bytes
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::all_bytes
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8>>::all_bytes
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::all_bytes
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
0
    pub const fn resize<const M: usize>(self) -> TinyAsciiStr<M> {
358
0
        let mut bytes = [0; M];
359
0
        let mut i = 0;
360
        // Indexing is protected by the loop guard
361
        #[expect(clippy::indexing_slicing)]
362
0
        while i < M && i < N {
363
0
            bytes[i] = self.bytes[i] as u8;
364
0
            i += 1;
365
0
        }
366
        // `self.bytes` only contains ASCII bytes, with no null bytes between
367
        // ASCII characters, so this also holds for `bytes`.
368
0
        unsafe { TinyAsciiStr::from_utf8_unchecked(bytes) }
369
0
    }
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::resize::<8>
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::resize::<8>
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8>>::resize::<3>
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::resize::<_>
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
0
    pub const fn concat<const M: usize, const Q: usize>(
398
0
        self,
399
0
        other: TinyAsciiStr<M>,
400
0
    ) -> TinyAsciiStr<Q> {
401
0
        let mut result = self.resize::<Q>();
402
0
        let mut i = self.len();
403
0
        let mut j = 0;
404
        // Indexing is protected by the loop guard
405
        #[expect(clippy::indexing_slicing)]
406
0
        while i < Q && j < M {
407
0
            result.bytes[i] = other.bytes[j];
408
0
            i += 1;
409
0
            j += 1;
410
0
        }
411
0
        result
412
0
    }
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::concat::<4, 8>
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::concat::<_, _>
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
0
    pub const unsafe fn from_utf8_unchecked(code_units: [u8; N]) -> Self {
419
0
        Self {
420
0
            bytes: AsciiByte::to_ascii_byte_array(&code_units),
421
0
        }
422
0
    }
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<2>>::from_utf8_unchecked
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::from_utf8_unchecked
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::from_utf8_unchecked
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8>>::from_utf8_unchecked
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::from_utf8_unchecked
423
}
424
425
macro_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
484
impl<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
0
    pub const fn is_ascii_alphabetic(&self) -> bool {
504
0
        check_is!(self, is_ascii_alphabetic, is_ascii_alphabetic)
505
0
    }
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::is_ascii_alphabetic
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::is_ascii_alphabetic
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8>>::is_ascii_alphabetic
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::is_ascii_alphabetic
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
0
    pub const fn is_ascii_alphanumeric(&self) -> bool {
527
0
        check_is!(self, is_ascii_alphanumeric, is_ascii_alphanumeric)
528
0
    }
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::is_ascii_alphanumeric
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8>>::is_ascii_alphanumeric
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::is_ascii_alphanumeric
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
0
    pub const fn is_ascii_numeric(&self) -> bool {
548
0
        check_is!(self, is_ascii_numeric, is_ascii_digit)
549
0
    }
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::is_ascii_numeric
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::is_ascii_numeric
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
0
    pub const fn is_ascii_lowercase(&self) -> bool {
571
0
        check_is!(
572
            self,
573
            is_ascii_lowercase,
574
            !is_ascii_uppercase,
575
            !is_ascii_uppercase
576
        )
577
0
    }
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::is_ascii_lowercase
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8>>::is_ascii_lowercase
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::is_ascii_lowercase
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
0
    pub const fn is_ascii_titlecase(&self) -> bool {
600
0
        check_is!(
601
            self,
602
            is_ascii_titlecase,
603
            !is_ascii_lowercase,
604
            !is_ascii_uppercase
605
        )
606
0
    }
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
0
    pub const fn is_ascii_uppercase(&self) -> bool {
628
0
        check_is!(
629
            self,
630
            is_ascii_uppercase,
631
            !is_ascii_lowercase,
632
            !is_ascii_lowercase
633
        )
634
0
    }
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
0
    pub const fn is_ascii_alphabetic_lowercase(&self) -> bool {
660
0
        check_is!(
661
            self,
662
            is_ascii_alphabetic_lowercase,
663
            is_ascii_lowercase,
664
            is_ascii_lowercase
665
        )
666
0
    }
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::is_ascii_alphabetic_lowercase
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::is_ascii_alphabetic_lowercase
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
0
    pub const fn is_ascii_alphabetic_titlecase(&self) -> bool {
690
0
        check_is!(
691
            self,
692
            is_ascii_alphabetic_titlecase,
693
            is_ascii_uppercase,
694
            is_ascii_lowercase
695
        )
696
0
    }
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::is_ascii_alphabetic_titlecase
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::is_ascii_alphabetic_titlecase
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
0
    pub const fn is_ascii_alphabetic_uppercase(&self) -> bool {
722
0
        check_is!(
723
            self,
724
            is_ascii_alphabetic_uppercase,
725
            is_ascii_uppercase,
726
            is_ascii_uppercase
727
        )
728
0
    }
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::is_ascii_alphabetic_uppercase
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::is_ascii_alphabetic_uppercase
729
}
730
731
macro_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
763
impl<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
0
    pub const fn to_ascii_lowercase(mut self) -> Self {
780
0
        to!(self, to_ascii_lowercase, to_ascii_lowercase)
781
0
    }
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<2>>::to_ascii_lowercase
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::to_ascii_lowercase
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::to_ascii_lowercase
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8>>::to_ascii_lowercase
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::to_ascii_lowercase
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
0
    pub const fn to_ascii_titlecase(mut self) -> Self {
800
0
        to!(
801
            self,
802
            to_ascii_titlecase,
803
            to_ascii_lowercase,
804
            to_ascii_uppercase
805
        )
806
0
    }
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::to_ascii_titlecase
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::to_ascii_titlecase
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
0
    pub const fn to_ascii_uppercase(mut self) -> Self {
824
0
        to!(self, to_ascii_uppercase, to_ascii_uppercase)
825
0
    }
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::to_ascii_uppercase
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::to_ascii_uppercase
826
}
827
828
impl<const N: usize> fmt::Debug for TinyAsciiStr<N> {
829
0
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
830
0
        fmt::Debug::fmt(self.as_str(), f)
831
0
    }
832
}
833
834
impl<const N: usize> fmt::Display for TinyAsciiStr<N> {
835
0
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
836
0
        fmt::Display::fmt(self.as_str(), f)
837
0
    }
838
}
839
840
impl<const N: usize> Deref for TinyAsciiStr<N> {
841
    type Target = str;
842
    #[inline]
843
0
    fn deref(&self) -> &str {
844
0
        self.as_str()
845
0
    }
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8> as core::ops::deref::Deref>::deref
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_> as core::ops::deref::Deref>::deref
846
}
847
848
impl<const N: usize> Borrow<str> for TinyAsciiStr<N> {
849
    #[inline]
850
0
    fn borrow(&self) -> &str {
851
0
        self.as_str()
852
0
    }
853
}
854
855
impl<const N: usize> FromStr for TinyAsciiStr<N> {
856
    type Err = ParseError;
857
    #[inline]
858
0
    fn from_str(s: &str) -> Result<Self, Self::Err> {
859
0
        Self::try_from_str(s)
860
0
    }
861
}
862
863
impl<const N: usize> PartialEq<str> for TinyAsciiStr<N> {
864
0
    fn eq(&self, other: &str) -> bool {
865
0
        self.deref() == other
866
0
    }
867
}
868
869
impl<const N: usize> PartialEq<&str> for TinyAsciiStr<N> {
870
0
    fn eq(&self, other: &&str) -> bool {
871
0
        self.deref() == *other
872
0
    }
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8> as core::cmp::PartialEq<&str>>::eq
Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_> as core::cmp::PartialEq<&str>>::eq
873
}
874
875
#[cfg(feature = "alloc")]
876
impl<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")]
883
impl<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)]
890
mod 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
}