convert_case/
boundary.rs

1use unicode_segmentation::UnicodeSegmentation;
2
3use alloc::vec::Vec;
4
5fn grapheme_is_digit(c: &&str) -> bool {
6    c.chars().all(|c| c.is_ascii_digit())
7}
8
9fn grapheme_is_uppercase(c: &&str) -> bool {
10    c.to_uppercase() != c.to_lowercase() && *c == c.to_uppercase()
11}
12
13fn grapheme_is_lowercase(c: &&str) -> bool {
14    c.to_uppercase() != c.to_lowercase() && *c == c.to_lowercase()
15}
16
17/// Conditions for splitting an identifier into words.
18///
19/// Some boundaries, [`Hyphen`](Boundary::Hyphen), [`Underscore`](Boundary::Underscore), and [`Space`](Boundary::Space),
20/// consume the character they split on, whereas the other boundaries do not.
21///
22/// `Boundary` includes methods that return useful groups of boundaries.  It also
23/// contains the [`defaults_from`](Boundary::defaults_from) method which will generate a subset
24/// of default boundaries based on the boundaries present in a string.
25///
26/// You can also create custom delimiter boundaries using the [`delim_boundary`](crate::delim_boundary)
27/// macro or directly instantiate `Boundary` for complex boundary conditions.
28/// ```
29/// use convert_case::{Boundary, Case, Casing, Converter};
30///
31/// assert_eq!(
32///     "TransformationsIn3D"
33///         .from_case(Case::Camel)
34///         .remove_boundaries(&Boundary::digit_letter())
35///         .to_case(Case::Snake),
36///     "transformations_in_3d",
37/// );
38///
39/// let conv = Converter::new()
40///     .set_boundaries(&Boundary::defaults_from("aA "))
41///     .to_case(Case::Title);
42/// assert_eq!("7empest By Tool", conv.convert("7empest byTool"));
43/// ```
44///
45/// ## Example
46///
47/// For more complex boundaries, such as splitting based on the first character being a certain
48/// symbol and the second is lowercase, you can instantiate a boundary directly.
49///
50/// ```
51/// # use convert_case::{Boundary, Case, Casing};
52/// let at_then_letter = Boundary::Custom {
53///     condition: |s| {
54///         s.get(0).map(|c| *c == "@") == Some(true)
55///             && s.get(1).map(|c| *c == c.to_lowercase()) == Some(true)
56///     },
57///     start: 1,
58///     len: 0,
59/// };
60/// assert_eq!(
61///     "name@domain"
62///         .set_boundaries(&[at_then_letter])
63///         .to_case(Case::Title),
64///     "Name@ Domain",
65/// )
66/// ```
67
68#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
69pub enum Boundary {
70    Custom {
71        /// A function that determines if this boundary is present at the start
72        /// of the string.  Second argument is the `arg` field.
73        condition: fn(&[&str]) -> bool,
74        /// Where the beginning of the boundary is.
75        start: usize,
76        /// The length of the boundary.  This is the number of graphemes that
77        /// are removed when splitting.
78        len: usize,
79    },
80
81    /// Splits on `-`, consuming the character on segmentation.
82    /// ```
83    /// # use convert_case::Boundary;
84    /// assert_eq!(
85    ///     Boundary::defaults_from("-"),
86    ///     vec![Boundary::Hyphen],
87    /// );
88    /// ```
89    Hyphen,
90
91    /// Splits on `_`, consuming the character on segmentation.
92    /// ```
93    /// # use convert_case::Boundary;
94    /// assert_eq!(
95    ///     Boundary::defaults_from("_"),
96    ///     vec![Boundary::Underscore],
97    /// );
98    /// ```
99    Underscore,
100
101    /// Splits on space, consuming the character on segmentation.
102    /// ```
103    /// # use convert_case::Boundary;
104    /// assert_eq!(
105    ///     Boundary::defaults_from(" "),
106    ///     vec![Boundary::Space],
107    /// );
108    /// ```
109    Space,
110
111    /// Splits where an uppercase letter is followed by a lowercase letter.  This is seldom used,
112    /// and is **not** included in the [defaults](Boundary::defaults).
113    /// ```
114    /// # use convert_case::Boundary;
115    /// assert!(
116    ///     Boundary::defaults_from("Aa").len() == 0
117    /// );
118    UpperLower,
119
120    /// Splits where a lowercase letter is followed by an uppercase letter.
121    /// ```
122    /// # use convert_case::Boundary;
123    /// assert_eq!(
124    ///     Boundary::defaults_from("aA"),
125    ///     vec![Boundary::LowerUpper],
126    /// );
127    /// ```
128    LowerUpper,
129
130    /// Splits where digit is followed by an uppercase letter.
131    /// ```
132    /// # use convert_case::Boundary;
133    /// assert_eq!(
134    ///     Boundary::defaults_from("1A"),
135    ///     vec![Boundary::DigitUpper],
136    /// );
137    /// ```
138    DigitUpper,
139
140    /// Splits where an uppercase letter is followed by a digit.
141    /// ```
142    /// # use convert_case::Boundary;
143    /// assert_eq!(
144    ///     Boundary::defaults_from("A1"),
145    ///     vec![Boundary::UpperDigit],
146    /// );
147    /// ```
148    UpperDigit,
149
150    /// Splits where digit is followed by a lowercase letter.
151    /// ```
152    /// # use convert_case::Boundary;
153    /// assert_eq!(
154    ///     Boundary::defaults_from("1a"),
155    ///     vec![Boundary::DigitLower],
156    /// );
157    /// ```
158    DigitLower,
159
160    /// Splits where a lowercase letter is followed by a digit.
161    /// ```
162    /// # use convert_case::Boundary;
163    /// assert_eq!(
164    ///     Boundary::defaults_from("a1"),
165    ///     vec![Boundary::LowerDigit],
166    /// );
167    /// ```
168    LowerDigit,
169
170    /// Acronyms are identified by two uppercase letters followed by a lowercase letter.
171    /// The word boundary is between the two uppercase letters.  For example, "HTTPRequest"
172    /// would have an acronym boundary identified at "PRe" and split into "HTTP" and "Request".
173    /// ```
174    /// # use convert_case::Boundary;
175    /// assert_eq!(
176    ///     Boundary::defaults_from("AAa"),
177    ///     vec![Boundary::Acronym],
178    /// );
179    /// ```
180    Acronym,
181}
182
183impl Boundary {
184    pub fn matches(self, s: &[&str]) -> bool {
185        use Boundary::*;
186        match self {
187            Underscore => s.first() == Some(&"_"),
188            Hyphen => s.first() == Some(&"-"),
189            Space => s.first() == Some(&" "),
190            Acronym => {
191                s.first().map(grapheme_is_uppercase) == Some(true)
192                    && s.get(1).map(grapheme_is_uppercase) == Some(true)
193                    && s.get(2).map(grapheme_is_lowercase) == Some(true)
194            }
195            LowerUpper => {
196                s.first().map(grapheme_is_lowercase) == Some(true)
197                    && s.get(1).map(grapheme_is_uppercase) == Some(true)
198            }
199            UpperLower => {
200                s.first().map(grapheme_is_uppercase) == Some(true)
201                    && s.get(1).map(grapheme_is_lowercase) == Some(true)
202            }
203            LowerDigit => {
204                s.first().map(grapheme_is_lowercase) == Some(true)
205                    && s.get(1).map(grapheme_is_digit) == Some(true)
206            }
207            UpperDigit => {
208                s.first().map(grapheme_is_uppercase) == Some(true)
209                    && s.get(1).map(grapheme_is_digit) == Some(true)
210            }
211            DigitLower => {
212                s.first().map(grapheme_is_digit) == Some(true)
213                    && s.get(1).map(grapheme_is_lowercase) == Some(true)
214            }
215            DigitUpper => {
216                s.first().map(grapheme_is_digit) == Some(true)
217                    && s.get(1).map(grapheme_is_uppercase) == Some(true)
218            }
219            Custom { condition, .. } => condition(s),
220        }
221    }
222
223    /// The number of graphemes consumed when splitting at the boundary.
224    pub fn len(self) -> usize {
225        use Boundary::*;
226        match self {
227            Underscore | Hyphen | Space => 1,
228            LowerUpper | UpperLower | LowerDigit | UpperDigit | DigitLower | DigitUpper
229            | Acronym => 0,
230            Custom { len, .. } => len,
231        }
232    }
233
234    /// The index of the character to split at.
235    pub fn start(self) -> usize {
236        use Boundary::*;
237        match self {
238            Underscore | Hyphen | Space => 0,
239            LowerUpper | UpperLower | LowerDigit | UpperDigit | DigitLower | DigitUpper
240            | Acronym => 1,
241            Custom { start, .. } => start,
242        }
243    }
244
245    /// The default list of boundaries used when `Casing::to_case` is called directly
246    /// and in a `Converter` generated from `Converter::new()`.
247    /// ```
248    /// # use convert_case::Boundary;
249    /// assert_eq!(
250    ///     Boundary::defaults(),
251    ///     [
252    ///         Boundary::Underscore,
253    ///         Boundary::Hyphen,
254    ///         Boundary::Space,
255    ///         Boundary::LowerUpper,
256    ///         Boundary::LowerDigit,
257    ///         Boundary::UpperDigit,
258    ///         Boundary::DigitLower,
259    ///         Boundary::DigitUpper,
260    ///         Boundary::Acronym,
261    ///     ],
262    /// );
263    /// ```
264    pub const fn defaults() -> [Boundary; 9] {
265        [
266            Boundary::Underscore,
267            Boundary::Hyphen,
268            Boundary::Space,
269            Boundary::LowerUpper,
270            Boundary::LowerDigit,
271            Boundary::UpperDigit,
272            Boundary::DigitLower,
273            Boundary::DigitUpper,
274            Boundary::Acronym,
275        ]
276    }
277
278    /// Returns the boundaries that involve digits.
279    /// ```
280    /// # use convert_case::Boundary;
281    /// assert_eq!(
282    ///     Boundary::digits(),
283    ///     [
284    ///         Boundary::LowerDigit,
285    ///         Boundary::UpperDigit,
286    ///         Boundary::DigitLower,
287    ///         Boundary::DigitUpper,
288    ///     ],
289    /// );
290    /// ```
291    pub const fn digits() -> [Boundary; 4] {
292        [
293            Boundary::LowerDigit,
294            Boundary::UpperDigit,
295            Boundary::DigitLower,
296            Boundary::DigitUpper,
297        ]
298    }
299
300    /// Returns the boundaries that are letters followed by digits.
301    /// ```
302    /// # use convert_case::Boundary;
303    /// assert_eq!(
304    ///     Boundary::letter_digit(),
305    ///     [
306    ///         Boundary::LowerDigit,
307    ///         Boundary::UpperDigit,
308    ///     ],
309    /// );
310    /// ```
311    pub const fn letter_digit() -> [Boundary; 2] {
312        [Boundary::LowerDigit, Boundary::UpperDigit]
313    }
314
315    /// Returns the boundaries that are digits followed by letters.
316    /// ```
317    /// # use convert_case::Boundary;
318    /// assert_eq!(
319    ///     Boundary::digit_letter(),
320    ///     [
321    ///         Boundary::DigitLower,
322    ///         Boundary::DigitUpper
323    ///     ],
324    /// );
325    /// ```
326    pub const fn digit_letter() -> [Boundary; 2] {
327        [Boundary::DigitLower, Boundary::DigitUpper]
328    }
329
330    /// Returns a list of all boundaries that are identified within the given string.
331    /// Could be a short of writing out all the boundaries in a list directly.  This will not
332    /// identify boundary `UpperLower` if it also used as part of `Acronym`.
333    ///
334    /// If you want to be very explicit and not overlap boundaries, it is recommended to use a colon
335    /// character.
336    /// ```
337    /// # use convert_case::Boundary;
338    /// assert_eq!(
339    ///     Boundary::defaults_from("aA8a -"),
340    ///     vec![
341    ///         Boundary::Hyphen,
342    ///         Boundary::Space,
343    ///         Boundary::LowerUpper,
344    ///         Boundary::UpperDigit,
345    ///         Boundary::DigitLower,
346    ///     ],
347    /// );
348    /// assert_eq!(
349    ///     Boundary::defaults_from("bD:0B:_:AAa"),
350    ///     vec![
351    ///         Boundary::Underscore,
352    ///         Boundary::LowerUpper,
353    ///         Boundary::DigitUpper,
354    ///         Boundary::Acronym,
355    ///     ],
356    /// );
357    /// ```
358    pub fn defaults_from(pattern: &str) -> Vec<Boundary> {
359        let mut boundaries = Vec::new();
360        for boundary in Boundary::defaults() {
361            let parts = split(&pattern, &[boundary]);
362            if parts.len() > 1 || parts.is_empty() || parts[0] != pattern {
363                boundaries.push(boundary);
364            }
365        }
366        boundaries
367    }
368}
369
370/// Split an identifier into a list of words using the list of boundaries.
371///
372/// This is used internally for splitting an identifier before mutating by
373/// a pattern and joining again with a delimiter.
374/// ```
375/// use convert_case::{Boundary, split};
376/// assert_eq!(
377///     split(&"one_two-three.four", &[Boundary::Underscore, Boundary::Hyphen]),
378///     vec!["one", "two", "three.four"],
379/// )
380/// ```
381pub fn split<'s, T>(s: &'s T, boundaries: &[Boundary]) -> Vec<&'s str>
382where
383    T: AsRef<str>,
384{
385    let s = s.as_ref();
386
387    if s.is_empty() {
388        return Vec::new();
389    }
390
391    let mut words = Vec::new();
392    let mut last_boundary_end = 0;
393
394    let (indices, graphemes): (Vec<_>, Vec<_>) = s.grapheme_indices(true).unzip();
395    let grapheme_length = indices[graphemes.len() - 1] + graphemes[graphemes.len() - 1].len();
396
397    // TODO:
398    // swapping the order of these would be faster
399    // end the loop sooner if any boundary condition is met
400    // could also hit a bitvector and do the splitting at the end?  May or may not be faster
401    for i in 0..graphemes.len() {
402        for boundary in boundaries {
403            //let byte_index = indices[i];
404
405            if boundary.matches(&graphemes[i..]) {
406                // What if we find a condition at the end of the array?
407                // Maybe we can stop early based on length
408                // To do this, need to switch the loops
409                // TODO
410                let boundary_byte_start: usize = *indices
411                    .get(i + boundary.start())
412                    .unwrap_or(&grapheme_length);
413                let boundary_byte_end: usize = *indices
414                    .get(i + boundary.start() + boundary.len())
415                    .unwrap_or(&grapheme_length);
416
417                // todo clean this up a bit
418                words.push(&s[last_boundary_end..boundary_byte_start]);
419                last_boundary_end = boundary_byte_end;
420                break;
421            }
422        }
423    }
424    words.push(&s[last_boundary_end..]);
425    //words.into_iter().filter(|s| !s.is_empty()).collect()
426    words.into_iter().collect()
427}
428
429/// Create a new boundary based on a delimiter.
430/// ```
431/// # use convert_case::{Case, Converter, delim_boundary};
432/// let conv = Converter::new()
433///     .set_boundaries(&[delim_boundary!("::")])
434///     .to_case(Case::Camel);
435///
436/// assert_eq!(
437///     conv.convert("my::var::name"),
438///     "myVarName",
439/// )
440/// ```
441#[macro_export]
442macro_rules! delim_boundary {
443    ($delim:expr) => {
444        convert_case::Boundary::Custom {
445            condition: |s| s.join("").starts_with($delim),
446            start: 0,
447            len: $delim.len(),
448        }
449    };
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455
456    #[test]
457    fn boundary_equality() {
458        let a = Boundary::Custom {
459            condition: |_| true,
460            start: 0,
461            len: 0,
462        };
463        let b = a;
464
465        assert_eq!(a, b)
466    }
467
468    #[test]
469    fn hyphen() {
470        let s = "a-b-c";
471        let v = split(&s, &[Boundary::Hyphen]);
472        assert_eq!(v, vec!["a", "b", "c"]);
473    }
474
475    #[test]
476    fn underscore() {
477        let s = "a_b_c";
478        let v = split(&s, &[Boundary::Underscore]);
479        assert_eq!(v, vec!["a", "b", "c"]);
480    }
481
482    #[test]
483    fn space() {
484        let s = "a b c";
485        let v = split(&s, &[Boundary::Space]);
486        assert_eq!(v, vec!["a", "b", "c"]);
487    }
488
489    #[test]
490    fn delimiters() {
491        let s = "aaa-bbb_ccc ddd ddd-eee";
492        let v = split(
493            &s,
494            &[Boundary::Space, Boundary::Underscore, Boundary::Hyphen],
495        );
496        assert_eq!(v, vec!["aaa", "bbb", "ccc", "ddd", "ddd", "eee"]);
497    }
498
499    #[test]
500    fn lower_upper() {
501        let s = "lowerUpperUpper";
502        let v = split(&s, &[Boundary::LowerUpper]);
503        assert_eq!(v, vec!["lower", "Upper", "Upper"]);
504    }
505
506    #[test]
507    fn acronym() {
508        let s = "XMLRequest";
509        let v = split(&s, &[Boundary::Acronym]);
510        assert_eq!(v, vec!["XML", "Request"]);
511    }
512
513    // TODO: add tests for other boundaries
514
515    #[test]
516    fn boundaries_found_in_string() {
517        // upper lower is not longer a default
518        assert_eq!(Vec::<Boundary>::new(), Boundary::defaults_from(".Aaaa"));
519        assert_eq!(
520            vec![Boundary::LowerUpper, Boundary::LowerDigit],
521            Boundary::defaults_from("a8.Aa.aA")
522        );
523        assert_eq!(
524            Boundary::digits().to_vec(),
525            Boundary::defaults_from("b1B1b")
526        );
527        assert_eq!(
528            vec![
529                Boundary::Underscore,
530                Boundary::Hyphen,
531                Boundary::Space,
532                Boundary::Acronym,
533            ],
534            Boundary::defaults_from("AAa -_")
535        );
536    }
537
538    #[test]
539    fn boundary_consts_same() {
540        assert_eq!(Boundary::Space, Boundary::Space);
541    }
542}