harper_core/
case.rs

1use std::borrow::Borrow;
2
3use smallvec::SmallVec;
4
5use crate::{CharString, char_string::CHAR_STRING_INLINE_SIZE};
6
7/// Apply the casing of `template` to `target`.
8///
9/// If `template` is shorter than `target`, the casing of the last character of `template` will be reused for
10/// the rest of the string.
11///
12/// If `template` is empty, all characters will be lowercased.
13#[must_use]
14pub fn copy_casing(
15    template: impl IntoIterator<Item = impl Borrow<char>>,
16    target: impl IntoIterator<Item = impl Borrow<char>>,
17) -> CharString {
18    target
19        .into_iter()
20        .scan(
21            (template.into_iter().get_casing(), Case::Lower),
22            |(template, prev_case), c| {
23                // Skip non-alphabetic characters in `target` without advancing `template`.
24                if c.borrow().is_alphabetic()
25                    && let Some(template_case) = template.next()
26                {
27                    *prev_case = template_case;
28                };
29                Some(prev_case.apply_to(*c.borrow()))
30            },
31        )
32        .flatten()
33        .collect()
34}
35
36/// Represents the casing of a character.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum Case {
39    Upper,
40    Lower,
41}
42
43impl Case {
44    /// Apply the casing to a provided character.
45    ///
46    /// This essentially calls [`char::to_uppercase()`] or [`char::to_lowercase()`] depending on
47    /// the state of `self`. Similarly to those functions, it returns an iterator of the resulting
48    /// character(s).
49    pub fn apply_to(&self, char: char) -> impl Iterator<Item = char> + use<> {
50        match self {
51            Self::Upper => char.to_uppercase().collect::<SmallVec<[char; 2]>>(),
52            Self::Lower => char.to_lowercase().collect::<SmallVec<[char; 2]>>(),
53        }
54        .into_iter()
55    }
56}
57
58impl TryFrom<char> for Case {
59    type Error = ();
60
61    /// Try to get the casing from the given character.
62    ///
63    /// This fails if the character is neither uppercase nor lowercase.
64    fn try_from(value: char) -> Result<Self, Self::Error> {
65        if value.is_uppercase() {
66            Ok(Self::Upper)
67        } else if value.is_lowercase() {
68            Ok(Self::Lower)
69        } else {
70            Err(())
71        }
72    }
73}
74
75// TODO: maybe move this functionality to CharStringExt if and when CharStringExt can be
76// generalized to work with char iterators.
77pub trait CaseIterExt {
78    fn get_casing(self) -> impl Iterator<Item = Case>;
79    fn get_casing_unfiltered(self) -> SmallVec<[Option<Case>; CHAR_STRING_INLINE_SIZE]>;
80}
81impl<I: IntoIterator<Item = T>, T: Borrow<char>> CaseIterExt for I {
82    /// Get an iterator of [`Case`] from a collection of characters. Note that this will not
83    /// include cases for characters that are neither uppercase nor lowercase.
84    fn get_casing(self) -> impl Iterator<Item = Case> {
85        self.into_iter()
86            .filter_map(|char| (*char.borrow()).try_into().ok())
87    }
88
89    /// Get casing for the provided string. Unlike [`Self::get_casing`], the output will always
90    /// be the same length as the input string. If a character is neither uppercase nor lowercase,
91    /// its corresponding case will be `None`.
92    fn get_casing_unfiltered(self) -> SmallVec<[Option<Case>; CHAR_STRING_INLINE_SIZE]> {
93        self.into_iter()
94            .map(|c| Case::try_from(*c.borrow()).ok())
95            .collect()
96    }
97}