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::TinyStrError;
8use core::fmt;
9use core::ops::Deref;
10use core::str::{self, FromStr};
11
12#[repr(transparent)]
13#[derive(PartialEq, Eq, Ord, PartialOrd, Copy, Clone, Hash)]
14pub struct TinyAsciiStr<const N: usize> {
15    bytes: [AsciiByte; N],
16}
17
18impl<const N: usize> TinyAsciiStr<N> {
19    /// Creates a `TinyAsciiStr<N>` from the given byte slice.
20    /// `bytes` may contain at most `N` non-null ASCII bytes.
21    pub const fn from_bytes(bytes: &[u8]) -> Result<Self, TinyStrError> {
22        Self::from_bytes_inner(bytes, 0, bytes.len(), false)
23    }
24
25    /// Creates a `TinyAsciiStr<N>` from a byte slice, replacing invalid bytes.
26    ///
27    /// Null and non-ASCII bytes (i.e. those outside the range `0x01..=0x7F`)
28    /// will be replaced with the '?' character.
29    ///
30    /// The input slice will be truncated if its length exceeds `N`.
31    pub const fn from_bytes_lossy(bytes: &[u8]) -> Self {
32        const QUESTION: u8 = b'?';
33        let mut out = [0; N];
34        let mut i = 0;
35        // Ord is not available in const, so no `.min(N)`
36        let len = if bytes.len() > N { N } else { bytes.len() };
37
38        // Indexing is protected by the len check above
39        #[allow(clippy::indexing_slicing)]
40        while i < len {
41            let b = bytes[i];
42            if b > 0 && b < 0x80 {
43                out[i] = b;
44            } else {
45                out[i] = QUESTION;
46            }
47            i += 1;
48        }
49
50        Self {
51            // SAFETY: `out` only contains ASCII bytes and has same size as `self.bytes`
52            bytes: unsafe { AsciiByte::to_ascii_byte_array(&out) },
53        }
54    }
55
56    /// Attempts to parse a fixed-length byte array to a `TinyAsciiStr`.
57    ///
58    /// The byte array may contain trailing NUL bytes.
59    ///
60    /// # Example
61    ///
62    /// ```
63    /// use tinystr::tinystr;
64    /// use tinystr::TinyAsciiStr;
65    ///
66    /// assert_eq!(
67    ///     TinyAsciiStr::<3>::try_from_raw(*b"GB\0"),
68    ///     Ok(tinystr!(3, "GB"))
69    /// );
70    /// assert_eq!(
71    ///     TinyAsciiStr::<3>::try_from_raw(*b"USD"),
72    ///     Ok(tinystr!(3, "USD"))
73    /// );
74    /// assert!(matches!(TinyAsciiStr::<3>::try_from_raw(*b"\0A\0"), Err(_)));
75    /// ```
76    pub const fn try_from_raw(raw: [u8; N]) -> Result<Self, TinyStrError> {
77        Self::from_bytes_inner(&raw, 0, N, true)
78    }
79
80    /// Equivalent to [`from_bytes(bytes[start..end])`](Self::from_bytes),
81    /// but callable in a `const` context (which range indexing is not).
82    pub const fn from_bytes_manual_slice(
83        bytes: &[u8],
84        start: usize,
85        end: usize,
86    ) -> Result<Self, TinyStrError> {
87        Self::from_bytes_inner(bytes, start, end, false)
88    }
89
90    #[inline]
91    pub(crate) const fn from_bytes_inner(
92        bytes: &[u8],
93        start: usize,
94        end: usize,
95        allow_trailing_null: bool,
96    ) -> Result<Self, TinyStrError> {
97        let len = end - start;
98        if len > N {
99            return Err(TinyStrError::TooLarge { max: N, len });
100        }
101
102        let mut out = [0; N];
103        let mut i = 0;
104        let mut found_null = false;
105        // Indexing is protected by TinyStrError::TooLarge
106        #[allow(clippy::indexing_slicing)]
107        while i < len {
108            let b = bytes[start + i];
109
110            if b == 0 {
111                found_null = true;
112            } else if b >= 0x80 {
113                return Err(TinyStrError::NonAscii);
114            } else if found_null {
115                // Error if there are contentful bytes after null
116                return Err(TinyStrError::ContainsNull);
117            }
118            out[i] = b;
119
120            i += 1;
121        }
122
123        if !allow_trailing_null && found_null {
124            // We found some trailing nulls, error
125            return Err(TinyStrError::ContainsNull);
126        }
127
128        Ok(Self {
129            // SAFETY: `out` only contains ASCII bytes and has same size as `self.bytes`
130            bytes: unsafe { AsciiByte::to_ascii_byte_array(&out) },
131        })
132    }
133
134    // TODO: This function shadows the FromStr trait. Rename?
135    #[inline]
136    pub const fn from_str(s: &str) -> Result<Self, TinyStrError> {
137        Self::from_bytes_inner(s.as_bytes(), 0, s.len(), false)
138    }
139
140    #[inline]
141    pub const fn as_str(&self) -> &str {
142        // as_bytes is valid utf8
143        unsafe { str::from_utf8_unchecked(self.as_bytes()) }
144    }
145
146    #[inline]
147    #[must_use]
148    pub const fn len(&self) -> usize {
149        if N <= 4 {
150            Aligned4::from_ascii_bytes(&self.bytes).len()
151        } else if N <= 8 {
152            Aligned8::from_ascii_bytes(&self.bytes).len()
153        } else {
154            let mut i = 0;
155            #[allow(clippy::indexing_slicing)] // < N is safe
156            while i < N && self.bytes[i] as u8 != AsciiByte::B0 as u8 {
157                i += 1
158            }
159            i
160        }
161    }
162
163    #[inline]
164    #[must_use]
165    pub const fn is_empty(&self) -> bool {
166        self.bytes[0] as u8 == AsciiByte::B0 as u8
167    }
168
169    #[inline]
170    #[must_use]
171    pub const fn as_bytes(&self) -> &[u8] {
172        // Safe because `self.bytes.as_slice()` pointer-casts to `&[u8]`,
173        // and changing the length of that slice to self.len() < N is safe.
174        unsafe {
175            core::slice::from_raw_parts(self.bytes.as_slice().as_ptr() as *const u8, self.len())
176        }
177    }
178
179    #[inline]
180    #[must_use]
181    pub const fn all_bytes(&self) -> &[u8; N] {
182        // SAFETY: `self.bytes` has same size as [u8; N]
183        unsafe { &*(self.bytes.as_ptr() as *const [u8; N]) }
184    }
185
186    #[inline]
187    #[must_use]
188    /// Resizes a `TinyAsciiStr<N>` to a `TinyAsciiStr<M>`.
189    ///
190    /// If `M < len()` the string gets truncated, otherwise only the
191    /// memory representation changes.
192    pub const fn resize<const M: usize>(self) -> TinyAsciiStr<M> {
193        let mut bytes = [0; M];
194        let mut i = 0;
195        // Indexing is protected by the loop guard
196        #[allow(clippy::indexing_slicing)]
197        while i < M && i < N {
198            bytes[i] = self.bytes[i] as u8;
199            i += 1;
200        }
201        // `self.bytes` only contains ASCII bytes, with no null bytes between
202        // ASCII characters, so this also holds for `bytes`.
203        unsafe { TinyAsciiStr::from_bytes_unchecked(bytes) }
204    }
205
206    /// # Safety
207    /// Must be called with a bytes array made of valid ASCII bytes, with no null bytes
208    /// between ASCII characters
209    #[must_use]
210    pub const unsafe fn from_bytes_unchecked(bytes: [u8; N]) -> Self {
211        Self {
212            bytes: AsciiByte::to_ascii_byte_array(&bytes),
213        }
214    }
215}
216
217macro_rules! check_is {
218    ($self:ident, $check_int:ident, $check_u8:ident) => {
219        if N <= 4 {
220            Aligned4::from_ascii_bytes(&$self.bytes).$check_int()
221        } else if N <= 8 {
222            Aligned8::from_ascii_bytes(&$self.bytes).$check_int()
223        } else {
224            let mut i = 0;
225            // Won't panic because self.bytes has length N
226            #[allow(clippy::indexing_slicing)]
227            while i < N && $self.bytes[i] as u8 != AsciiByte::B0 as u8 {
228                if !($self.bytes[i] as u8).$check_u8() {
229                    return false;
230                }
231                i += 1;
232            }
233            true
234        }
235    };
236    ($self:ident, $check_int:ident, !$check_u8_0_inv:ident, !$check_u8_1_inv:ident) => {
237        if N <= 4 {
238            Aligned4::from_ascii_bytes(&$self.bytes).$check_int()
239        } else if N <= 8 {
240            Aligned8::from_ascii_bytes(&$self.bytes).$check_int()
241        } else {
242            // Won't panic because N is > 8
243            if ($self.bytes[0] as u8).$check_u8_0_inv() {
244                return false;
245            }
246            let mut i = 1;
247            // Won't panic because self.bytes has length N
248            #[allow(clippy::indexing_slicing)]
249            while i < N && $self.bytes[i] as u8 != AsciiByte::B0 as u8 {
250                if ($self.bytes[i] as u8).$check_u8_1_inv() {
251                    return false;
252                }
253                i += 1;
254            }
255            true
256        }
257    };
258    ($self:ident, $check_int:ident, $check_u8_0_inv:ident, $check_u8_1_inv:ident) => {
259        if N <= 4 {
260            Aligned4::from_ascii_bytes(&$self.bytes).$check_int()
261        } else if N <= 8 {
262            Aligned8::from_ascii_bytes(&$self.bytes).$check_int()
263        } else {
264            // Won't panic because N is > 8
265            if !($self.bytes[0] as u8).$check_u8_0_inv() {
266                return false;
267            }
268            let mut i = 1;
269            // Won't panic because self.bytes has length N
270            #[allow(clippy::indexing_slicing)]
271            while i < N && $self.bytes[i] as u8 != AsciiByte::B0 as u8 {
272                if !($self.bytes[i] as u8).$check_u8_1_inv() {
273                    return false;
274                }
275                i += 1;
276            }
277            true
278        }
279    };
280}
281
282impl<const N: usize> TinyAsciiStr<N> {
283    /// Checks if the value is composed of ASCII alphabetic characters:
284    ///
285    ///  * U+0041 'A' ..= U+005A 'Z', or
286    ///  * U+0061 'a' ..= U+007A 'z'.
287    ///
288    /// # Examples
289    ///
290    /// ```
291    /// use tinystr::TinyAsciiStr;
292    ///
293    /// let s1: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse.");
294    /// let s2: TinyAsciiStr<4> = "Te3t".parse().expect("Failed to parse.");
295    ///
296    /// assert!(s1.is_ascii_alphabetic());
297    /// assert!(!s2.is_ascii_alphabetic());
298    /// ```
299    #[inline]
300    #[must_use]
301    pub const fn is_ascii_alphabetic(&self) -> bool {
302        check_is!(self, is_ascii_alphabetic, is_ascii_alphabetic)
303    }
304
305    /// Checks if the value is composed of ASCII alphanumeric characters:
306    ///
307    ///  * U+0041 'A' ..= U+005A 'Z', or
308    ///  * U+0061 'a' ..= U+007A 'z', or
309    ///  * U+0030 '0' ..= U+0039 '9'.
310    ///
311    /// # Examples
312    ///
313    /// ```
314    /// use tinystr::TinyAsciiStr;
315    ///
316    /// let s1: TinyAsciiStr<4> = "A15b".parse().expect("Failed to parse.");
317    /// let s2: TinyAsciiStr<4> = "[3@w".parse().expect("Failed to parse.");
318    ///
319    /// assert!(s1.is_ascii_alphanumeric());
320    /// assert!(!s2.is_ascii_alphanumeric());
321    /// ```
322    #[inline]
323    #[must_use]
324    pub const fn is_ascii_alphanumeric(&self) -> bool {
325        check_is!(self, is_ascii_alphanumeric, is_ascii_alphanumeric)
326    }
327
328    /// Checks if the value is composed of ASCII decimal digits:
329    ///
330    ///  * U+0030 '0' ..= U+0039 '9'.
331    ///
332    /// # Examples
333    ///
334    /// ```
335    /// use tinystr::TinyAsciiStr;
336    ///
337    /// let s1: TinyAsciiStr<4> = "312".parse().expect("Failed to parse.");
338    /// let s2: TinyAsciiStr<4> = "3d".parse().expect("Failed to parse.");
339    ///
340    /// assert!(s1.is_ascii_numeric());
341    /// assert!(!s2.is_ascii_numeric());
342    /// ```
343    #[inline]
344    #[must_use]
345    pub const fn is_ascii_numeric(&self) -> bool {
346        check_is!(self, is_ascii_numeric, is_ascii_digit)
347    }
348
349    /// Checks if the value is in ASCII lower case.
350    ///
351    /// All letter characters are checked for case. Non-letter characters are ignored.
352    ///
353    /// # Examples
354    ///
355    /// ```
356    /// use tinystr::TinyAsciiStr;
357    ///
358    /// let s1: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
359    /// let s2: TinyAsciiStr<4> = "test".parse().expect("Failed to parse.");
360    /// let s3: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
361    ///
362    /// assert!(!s1.is_ascii_lowercase());
363    /// assert!(s2.is_ascii_lowercase());
364    /// assert!(s3.is_ascii_lowercase());
365    /// ```
366    #[inline]
367    #[must_use]
368    pub const fn is_ascii_lowercase(&self) -> bool {
369        check_is!(
370            self,
371            is_ascii_lowercase,
372            !is_ascii_uppercase,
373            !is_ascii_uppercase
374        )
375    }
376
377    /// Checks if the value is in ASCII title case.
378    ///
379    /// This verifies that the first character is ASCII uppercase and all others ASCII lowercase.
380    /// Non-letter characters are ignored.
381    ///
382    /// # Examples
383    ///
384    /// ```
385    /// use tinystr::TinyAsciiStr;
386    ///
387    /// let s1: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
388    /// let s2: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse.");
389    /// let s3: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
390    ///
391    /// assert!(!s1.is_ascii_titlecase());
392    /// assert!(s2.is_ascii_titlecase());
393    /// assert!(s3.is_ascii_titlecase());
394    /// ```
395    #[inline]
396    #[must_use]
397    pub const fn is_ascii_titlecase(&self) -> bool {
398        check_is!(
399            self,
400            is_ascii_titlecase,
401            !is_ascii_lowercase,
402            !is_ascii_uppercase
403        )
404    }
405
406    /// Checks if the value is in ASCII upper case.
407    ///
408    /// All letter characters are checked for case. Non-letter characters are ignored.
409    ///
410    /// # Examples
411    ///
412    /// ```
413    /// use tinystr::TinyAsciiStr;
414    ///
415    /// let s1: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
416    /// let s2: TinyAsciiStr<4> = "TEST".parse().expect("Failed to parse.");
417    /// let s3: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
418    ///
419    /// assert!(!s1.is_ascii_uppercase());
420    /// assert!(s2.is_ascii_uppercase());
421    /// assert!(!s3.is_ascii_uppercase());
422    /// ```
423    #[inline]
424    #[must_use]
425    pub const fn is_ascii_uppercase(&self) -> bool {
426        check_is!(
427            self,
428            is_ascii_uppercase,
429            !is_ascii_lowercase,
430            !is_ascii_lowercase
431        )
432    }
433
434    /// Checks if the value is composed of ASCII alphabetic lower case characters:
435    ///
436    ///  * U+0061 'a' ..= U+007A 'z',
437    ///
438    /// # Examples
439    ///
440    /// ```
441    /// use tinystr::TinyAsciiStr;
442    ///
443    /// let s1: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse.");
444    /// let s2: TinyAsciiStr<4> = "Te3t".parse().expect("Failed to parse.");
445    /// let s3: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
446    /// let s4: TinyAsciiStr<4> = "test".parse().expect("Failed to parse.");
447    /// let s5: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
448    ///
449    /// assert!(!s1.is_ascii_alphabetic_lowercase());
450    /// assert!(!s2.is_ascii_alphabetic_lowercase());
451    /// assert!(!s3.is_ascii_alphabetic_lowercase());
452    /// assert!(s4.is_ascii_alphabetic_lowercase());
453    /// assert!(!s5.is_ascii_alphabetic_lowercase());
454    /// ```
455    #[inline]
456    #[must_use]
457    pub const fn is_ascii_alphabetic_lowercase(&self) -> bool {
458        check_is!(
459            self,
460            is_ascii_alphabetic_lowercase,
461            is_ascii_lowercase,
462            is_ascii_lowercase
463        )
464    }
465
466    /// Checks if the value is composed of ASCII alphabetic, with the first character being ASCII uppercase, and all others ASCII lowercase.
467    ///
468    /// # Examples
469    ///
470    /// ```
471    /// use tinystr::TinyAsciiStr;
472    ///
473    /// let s1: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse.");
474    /// let s2: TinyAsciiStr<4> = "Te3t".parse().expect("Failed to parse.");
475    /// let s3: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
476    /// let s4: TinyAsciiStr<4> = "test".parse().expect("Failed to parse.");
477    /// let s5: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
478    ///
479    /// assert!(s1.is_ascii_alphabetic_titlecase());
480    /// assert!(!s2.is_ascii_alphabetic_titlecase());
481    /// assert!(!s3.is_ascii_alphabetic_titlecase());
482    /// assert!(!s4.is_ascii_alphabetic_titlecase());
483    /// assert!(!s5.is_ascii_alphabetic_titlecase());
484    /// ```
485    #[inline]
486    #[must_use]
487    pub const fn is_ascii_alphabetic_titlecase(&self) -> bool {
488        check_is!(
489            self,
490            is_ascii_alphabetic_titlecase,
491            is_ascii_uppercase,
492            is_ascii_lowercase
493        )
494    }
495
496    /// Checks if the value is composed of ASCII alphabetic upper case characters:
497    ///
498    ///  * U+0041 'A' ..= U+005A 'Z',
499    ///
500    /// # Examples
501    ///
502    /// ```
503    /// use tinystr::TinyAsciiStr;
504    ///
505    /// let s1: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse.");
506    /// let s2: TinyAsciiStr<4> = "Te3t".parse().expect("Failed to parse.");
507    /// let s3: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
508    /// let s4: TinyAsciiStr<4> = "TEST".parse().expect("Failed to parse.");
509    /// let s5: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse.");
510    ///
511    /// assert!(!s1.is_ascii_alphabetic_uppercase());
512    /// assert!(!s2.is_ascii_alphabetic_uppercase());
513    /// assert!(!s3.is_ascii_alphabetic_uppercase());
514    /// assert!(s4.is_ascii_alphabetic_uppercase());
515    /// assert!(!s5.is_ascii_alphabetic_uppercase());
516    /// ```
517    #[inline]
518    #[must_use]
519    pub const fn is_ascii_alphabetic_uppercase(&self) -> bool {
520        check_is!(
521            self,
522            is_ascii_alphabetic_uppercase,
523            is_ascii_uppercase,
524            is_ascii_uppercase
525        )
526    }
527}
528
529macro_rules! to {
530    ($self:ident, $to:ident, $later_char_to:ident $(,$first_char_to:ident)?) => {{
531        let mut i = 0;
532        if N <= 4 {
533            let aligned = Aligned4::from_ascii_bytes(&$self.bytes).$to().to_ascii_bytes();
534            // Won't panic because self.bytes has length N and aligned has length >= N
535            #[allow(clippy::indexing_slicing)]
536            while i < N {
537                $self.bytes[i] = aligned[i];
538                i += 1;
539            }
540        } else if N <= 8 {
541            let aligned = Aligned8::from_ascii_bytes(&$self.bytes).$to().to_ascii_bytes();
542            // Won't panic because self.bytes has length N and aligned has length >= N
543            #[allow(clippy::indexing_slicing)]
544            while i < N {
545                $self.bytes[i] = aligned[i];
546                i += 1;
547            }
548        } else {
549            // Won't panic because self.bytes has length N
550            #[allow(clippy::indexing_slicing)]
551            while i < N && $self.bytes[i] as u8 != AsciiByte::B0 as u8 {
552                // SAFETY: AsciiByte is repr(u8) and has same size as u8
553                unsafe {
554                    $self.bytes[i] = core::mem::transmute::<u8, AsciiByte>(
555                        ($self.bytes[i] as u8).$later_char_to()
556                    );
557                }
558                i += 1;
559            }
560            // SAFETY: AsciiByte is repr(u8) and has same size as u8
561            $(
562                $self.bytes[0] = unsafe {
563                    core::mem::transmute::<u8, AsciiByte>(($self.bytes[0] as u8).$first_char_to())
564                };
565            )?
566        }
567        $self
568    }};
569}
570
571impl<const N: usize> TinyAsciiStr<N> {
572    /// Converts this type to its ASCII lower case equivalent in-place.
573    ///
574    /// ASCII letters 'A' to 'Z' are mapped to 'a' to 'z', other characters are unchanged.
575    ///
576    /// # Examples
577    ///
578    /// ```
579    /// use tinystr::TinyAsciiStr;
580    ///
581    /// let s1: TinyAsciiStr<4> = "TeS3".parse().expect("Failed to parse.");
582    ///
583    /// assert_eq!(&*s1.to_ascii_lowercase(), "tes3");
584    /// ```
585    #[inline]
586    #[must_use]
587    pub const fn to_ascii_lowercase(mut self) -> Self {
588        to!(self, to_ascii_lowercase, to_ascii_lowercase)
589    }
590
591    /// Converts this type to its ASCII title case equivalent in-place.
592    ///
593    /// The first character is converted to ASCII uppercase; the remaining characters
594    /// are converted to ASCII lowercase.
595    ///
596    /// # Examples
597    ///
598    /// ```
599    /// use tinystr::TinyAsciiStr;
600    ///
601    /// let s1: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse.");
602    ///
603    /// assert_eq!(&*s1.to_ascii_titlecase(), "Test");
604    /// ```
605    #[inline]
606    #[must_use]
607    pub const fn to_ascii_titlecase(mut self) -> Self {
608        to!(
609            self,
610            to_ascii_titlecase,
611            to_ascii_lowercase,
612            to_ascii_uppercase
613        )
614    }
615
616    /// Converts this type to its ASCII upper case equivalent in-place.
617    ///
618    /// ASCII letters 'a' to 'z' are mapped to 'A' to 'Z', other characters are unchanged.
619    ///
620    /// # Examples
621    ///
622    /// ```
623    /// use tinystr::TinyAsciiStr;
624    ///
625    /// let s1: TinyAsciiStr<4> = "Tes3".parse().expect("Failed to parse.");
626    ///
627    /// assert_eq!(&*s1.to_ascii_uppercase(), "TES3");
628    /// ```
629    #[inline]
630    #[must_use]
631    pub const fn to_ascii_uppercase(mut self) -> Self {
632        to!(self, to_ascii_uppercase, to_ascii_uppercase)
633    }
634}
635
636impl<const N: usize> fmt::Debug for TinyAsciiStr<N> {
637    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
638        fmt::Debug::fmt(self.as_str(), f)
639    }
640}
641
642impl<const N: usize> fmt::Display for TinyAsciiStr<N> {
643    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
644        fmt::Display::fmt(self.as_str(), f)
645    }
646}
647
648impl<const N: usize> Deref for TinyAsciiStr<N> {
649    type Target = str;
650    #[inline]
651    fn deref(&self) -> &str {
652        self.as_str()
653    }
654}
655
656impl<const N: usize> FromStr for TinyAsciiStr<N> {
657    type Err = TinyStrError;
658    #[inline]
659    fn from_str(s: &str) -> Result<Self, Self::Err> {
660        Self::from_str(s)
661    }
662}
663
664impl<const N: usize> PartialEq<str> for TinyAsciiStr<N> {
665    fn eq(&self, other: &str) -> bool {
666        self.deref() == other
667    }
668}
669
670impl<const N: usize> PartialEq<&str> for TinyAsciiStr<N> {
671    fn eq(&self, other: &&str) -> bool {
672        self.deref() == *other
673    }
674}
675
676#[cfg(feature = "alloc")]
677impl<const N: usize> PartialEq<alloc::string::String> for TinyAsciiStr<N> {
678    fn eq(&self, other: &alloc::string::String) -> bool {
679        self.deref() == other.deref()
680    }
681}
682
683#[cfg(feature = "alloc")]
684impl<const N: usize> PartialEq<TinyAsciiStr<N>> for alloc::string::String {
685    fn eq(&self, other: &TinyAsciiStr<N>) -> bool {
686        self.deref() == other.deref()
687    }
688}
689
690#[cfg(test)]
691mod test {
692    use super::*;
693    use rand::distributions::Distribution;
694    use rand::distributions::Standard;
695    use rand::rngs::SmallRng;
696    use rand::seq::SliceRandom;
697    use rand::SeedableRng;
698
699    const STRINGS: [&str; 26] = [
700        "Latn",
701        "laTn",
702        "windows",
703        "AR",
704        "Hans",
705        "macos",
706        "AT",
707        "infiniband",
708        "FR",
709        "en",
710        "Cyrl",
711        "FromIntegral",
712        "NO",
713        "419",
714        "MacintoshOSX2019",
715        "a3z",
716        "A3z",
717        "A3Z",
718        "a3Z",
719        "3A",
720        "3Z",
721        "3a",
722        "3z",
723        "@@[`{",
724        "UK",
725        "E12",
726    ];
727
728    fn gen_strings(num_strings: usize, allowed_lengths: &[usize]) -> Vec<String> {
729        let mut rng = SmallRng::seed_from_u64(2022);
730        // Need to do this in 2 steps since the RNG is needed twice
731        let string_lengths = core::iter::repeat_with(|| *allowed_lengths.choose(&mut rng).unwrap())
732            .take(num_strings)
733            .collect::<Vec<usize>>();
734        string_lengths
735            .iter()
736            .map(|len| {
737                Standard
738                    .sample_iter(&mut rng)
739                    .filter(|b: &u8| *b > 0 && *b < 0x80)
740                    .take(*len)
741                    .collect::<Vec<u8>>()
742            })
743            .map(|byte_vec| String::from_utf8(byte_vec).expect("All ASCII"))
744            .collect()
745    }
746
747    fn check_operation<T, F1, F2, const N: usize>(reference_f: F1, tinystr_f: F2)
748    where
749        F1: Fn(&str) -> T,
750        F2: Fn(TinyAsciiStr<N>) -> T,
751        T: core::fmt::Debug + core::cmp::PartialEq,
752    {
753        for s in STRINGS
754            .into_iter()
755            .map(str::to_owned)
756            .chain(gen_strings(100, &[3, 4, 5, 8, 12]))
757        {
758            let t = match TinyAsciiStr::<N>::from_str(&s) {
759                Ok(t) => t,
760                Err(TinyStrError::TooLarge { .. }) => continue,
761                Err(e) => panic!("{}", e),
762            };
763            let expected = reference_f(&s);
764            let actual = tinystr_f(t);
765            assert_eq!(expected, actual, "TinyAsciiStr<{N}>: {s:?}");
766        }
767    }
768
769    #[test]
770    fn test_is_ascii_alphabetic() {
771        fn check<const N: usize>() {
772            check_operation(
773                |s| s.chars().all(|c| c.is_ascii_alphabetic()),
774                |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphabetic(&t),
775            )
776        }
777        check::<2>();
778        check::<3>();
779        check::<4>();
780        check::<5>();
781        check::<8>();
782        check::<16>();
783    }
784
785    #[test]
786    fn test_is_ascii_alphanumeric() {
787        fn check<const N: usize>() {
788            check_operation(
789                |s| s.chars().all(|c| c.is_ascii_alphanumeric()),
790                |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphanumeric(&t),
791            )
792        }
793        check::<2>();
794        check::<3>();
795        check::<4>();
796        check::<5>();
797        check::<8>();
798        check::<16>();
799    }
800
801    #[test]
802    fn test_is_ascii_numeric() {
803        fn check<const N: usize>() {
804            check_operation(
805                |s| s.chars().all(|c| c.is_ascii_digit()),
806                |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_numeric(&t),
807            )
808        }
809        check::<2>();
810        check::<3>();
811        check::<4>();
812        check::<5>();
813        check::<8>();
814        check::<16>();
815    }
816
817    #[test]
818    fn test_is_ascii_lowercase() {
819        fn check<const N: usize>() {
820            check_operation(
821                |s| {
822                    s == TinyAsciiStr::<16>::from_str(s)
823                        .unwrap()
824                        .to_ascii_lowercase()
825                        .as_str()
826                },
827                |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_lowercase(&t),
828            )
829        }
830        check::<2>();
831        check::<3>();
832        check::<4>();
833        check::<5>();
834        check::<8>();
835        check::<16>();
836    }
837
838    #[test]
839    fn test_is_ascii_titlecase() {
840        fn check<const N: usize>() {
841            check_operation(
842                |s| {
843                    s == TinyAsciiStr::<16>::from_str(s)
844                        .unwrap()
845                        .to_ascii_titlecase()
846                        .as_str()
847                },
848                |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_titlecase(&t),
849            )
850        }
851        check::<2>();
852        check::<3>();
853        check::<4>();
854        check::<5>();
855        check::<8>();
856        check::<16>();
857    }
858
859    #[test]
860    fn test_is_ascii_uppercase() {
861        fn check<const N: usize>() {
862            check_operation(
863                |s| {
864                    s == TinyAsciiStr::<16>::from_str(s)
865                        .unwrap()
866                        .to_ascii_uppercase()
867                        .as_str()
868                },
869                |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_uppercase(&t),
870            )
871        }
872        check::<2>();
873        check::<3>();
874        check::<4>();
875        check::<5>();
876        check::<8>();
877        check::<16>();
878    }
879
880    #[test]
881    fn test_is_ascii_alphabetic_lowercase() {
882        fn check<const N: usize>() {
883            check_operation(
884                |s| {
885                    // Check alphabetic
886                    s.chars().all(|c| c.is_ascii_alphabetic()) &&
887                    // Check lowercase
888                    s == TinyAsciiStr::<16>::from_str(s)
889                        .unwrap()
890                        .to_ascii_lowercase()
891                        .as_str()
892                },
893                |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphabetic_lowercase(&t),
894            )
895        }
896        check::<2>();
897        check::<3>();
898        check::<4>();
899        check::<5>();
900        check::<8>();
901        check::<16>();
902    }
903
904    #[test]
905    fn test_is_ascii_alphabetic_titlecase() {
906        fn check<const N: usize>() {
907            check_operation(
908                |s| {
909                    // Check alphabetic
910                    s.chars().all(|c| c.is_ascii_alphabetic()) &&
911                    // Check titlecase
912                    s == TinyAsciiStr::<16>::from_str(s)
913                        .unwrap()
914                        .to_ascii_titlecase()
915                        .as_str()
916                },
917                |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphabetic_titlecase(&t),
918            )
919        }
920        check::<2>();
921        check::<3>();
922        check::<4>();
923        check::<5>();
924        check::<8>();
925        check::<16>();
926    }
927
928    #[test]
929    fn test_is_ascii_alphabetic_uppercase() {
930        fn check<const N: usize>() {
931            check_operation(
932                |s| {
933                    // Check alphabetic
934                    s.chars().all(|c| c.is_ascii_alphabetic()) &&
935                    // Check uppercase
936                    s == TinyAsciiStr::<16>::from_str(s)
937                        .unwrap()
938                        .to_ascii_uppercase()
939                        .as_str()
940                },
941                |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphabetic_uppercase(&t),
942            )
943        }
944        check::<2>();
945        check::<3>();
946        check::<4>();
947        check::<5>();
948        check::<8>();
949        check::<16>();
950    }
951
952    #[test]
953    fn test_to_ascii_lowercase() {
954        fn check<const N: usize>() {
955            check_operation(
956                |s| {
957                    s.chars()
958                        .map(|c| c.to_ascii_lowercase())
959                        .collect::<String>()
960                },
961                |t: TinyAsciiStr<N>| TinyAsciiStr::to_ascii_lowercase(t).as_str().to_owned(),
962            )
963        }
964        check::<2>();
965        check::<3>();
966        check::<4>();
967        check::<5>();
968        check::<8>();
969        check::<16>();
970    }
971
972    #[test]
973    fn test_to_ascii_titlecase() {
974        fn check<const N: usize>() {
975            check_operation(
976                |s| {
977                    let mut r = s
978                        .chars()
979                        .map(|c| c.to_ascii_lowercase())
980                        .collect::<String>();
981                    // Safe because the string is nonempty and an ASCII string
982                    unsafe { r.as_bytes_mut()[0].make_ascii_uppercase() };
983                    r
984                },
985                |t: TinyAsciiStr<N>| TinyAsciiStr::to_ascii_titlecase(t).as_str().to_owned(),
986            )
987        }
988        check::<2>();
989        check::<3>();
990        check::<4>();
991        check::<5>();
992        check::<8>();
993        check::<16>();
994    }
995
996    #[test]
997    fn test_to_ascii_uppercase() {
998        fn check<const N: usize>() {
999            check_operation(
1000                |s| {
1001                    s.chars()
1002                        .map(|c| c.to_ascii_uppercase())
1003                        .collect::<String>()
1004                },
1005                |t: TinyAsciiStr<N>| TinyAsciiStr::to_ascii_uppercase(t).as_str().to_owned(),
1006            )
1007        }
1008        check::<2>();
1009        check::<3>();
1010        check::<4>();
1011        check::<5>();
1012        check::<8>();
1013        check::<16>();
1014    }
1015
1016    #[test]
1017    fn lossy_constructor() {
1018        assert_eq!(TinyAsciiStr::<4>::from_bytes_lossy(b"").as_str(), "");
1019        assert_eq!(
1020            TinyAsciiStr::<4>::from_bytes_lossy(b"oh\0o").as_str(),
1021            "oh?o"
1022        );
1023        assert_eq!(TinyAsciiStr::<4>::from_bytes_lossy(b"\0").as_str(), "?");
1024        assert_eq!(
1025            TinyAsciiStr::<4>::from_bytes_lossy(b"toolong").as_str(),
1026            "tool"
1027        );
1028        assert_eq!(
1029            TinyAsciiStr::<4>::from_bytes_lossy(&[b'a', 0x80, 0xFF, b'1']).as_str(),
1030            "a??1"
1031        );
1032    }
1033}