const_str/__ctfe/
ascii_case.rs

1#![allow(unsafe_code)]
2
3use core::ops::Range;
4
5use crate::__ctfe::StrBuf;
6use crate::slice::subslice;
7
8#[derive(Clone, Copy)]
9#[repr(u8)]
10enum TokenKind {
11    NonAscii = 1,
12    Lower = 2,
13    Upper = 3,
14    Digit = 4,
15    Dot = 5,
16    Other = 6,
17}
18
19impl TokenKind {
20    const fn new(b: u8) -> Self {
21        if !b.is_ascii() {
22            return TokenKind::NonAscii;
23        }
24        if b.is_ascii_lowercase() {
25            return TokenKind::Lower;
26        }
27        if b.is_ascii_uppercase() {
28            return TokenKind::Upper;
29        }
30        if b.is_ascii_digit() {
31            return TokenKind::Digit;
32        }
33        if b == b'.' {
34            return TokenKind::Dot;
35        }
36        TokenKind::Other
37    }
38
39    const fn is_boundary_word(s: &[u8]) -> bool {
40        let mut i = 0;
41        while i < s.len() {
42            let kind = Self::new(s[i]);
43            match kind {
44                TokenKind::Other | TokenKind::Dot => {}
45                _ => return false,
46            }
47            i += 1;
48        }
49        true
50    }
51}
52
53#[derive(Debug)]
54struct Boundaries<const N: usize> {
55    buf: [usize; N],
56    len: usize,
57}
58
59impl<const N: usize> Boundaries<N> {
60    const fn new(src: &str) -> Self {
61        let s = src.as_bytes();
62        assert!(s.len() + 1 == N);
63
64        let mut buf = [0; N];
65        let mut pos = 0;
66
67        macro_rules! push {
68            ($x: expr) => {{
69                buf[pos] = $x;
70                pos += 1;
71            }};
72        }
73
74        let mut k2: Option<TokenKind> = None;
75        let mut k1: Option<TokenKind> = None;
76
77        let mut i = 0;
78        while i < s.len() {
79            let b = s[i];
80            let k0 = TokenKind::new(b);
81
82            use TokenKind::*;
83
84            match (k1, k0) {
85                (None, _) => push!(i),
86                (Some(k1), k0) => {
87                    if k1 as u8 != k0 as u8 {
88                        match (k1, k0) {
89                            (Upper, Lower) => push!(i - 1),
90                            (NonAscii, Digit) => push!(i),
91                            (Lower | Upper, Digit) => {} // or-pattens stable since 1.53
92                            (Digit, Lower | Upper | NonAscii) => {}
93                            (_, Dot) => {}
94                            (Dot, _) => match (k2, k0) {
95                                (None, _) => push!(i),
96                                (Some(_), _) => {
97                                    push!(i - 1);
98                                    push!(i);
99                                }
100                            },
101                            _ => push!(i),
102                        }
103                    }
104                }
105            }
106
107            k2 = k1;
108            k1 = Some(k0);
109            i += 1;
110        }
111        push!(i);
112
113        Self { buf, len: pos }
114    }
115
116    const fn words_count(&self) -> usize {
117        self.len - 1
118    }
119
120    const fn word_range(&self, idx: usize) -> Range<usize> {
121        self.buf[idx]..self.buf[idx + 1]
122    }
123}
124
125pub enum AsciiCase {
126    Lower,
127    Upper,
128    LowerCamel,
129    UpperCamel,
130    Title,
131    Snake,
132    Kebab,
133    ShoutySnake,
134    ShoutyKebab,
135}
136
137impl AsciiCase {
138    const fn get_seperator(&self) -> Option<u8> {
139        match self {
140            Self::Title => Some(b' '),
141            Self::Snake | Self::ShoutySnake => Some(b'_'),
142            Self::Kebab | Self::ShoutyKebab => Some(b'-'),
143            _ => None,
144        }
145    }
146}
147
148pub struct ConvAsciiCase<T>(pub T, pub AsciiCase);
149
150impl ConvAsciiCase<&str> {
151    pub const fn output_len<const M: usize>(&self) -> usize {
152        assert!(self.0.len() + 1 == M);
153
154        use AsciiCase::*;
155        match self.1 {
156            Lower | Upper => self.0.len(),
157            LowerCamel | UpperCamel | Title | Snake | Kebab | ShoutySnake | ShoutyKebab => {
158                let mut ans = 0;
159
160                let has_sep = self.1.get_seperator().is_some();
161
162                let boundaries = Boundaries::<M>::new(self.0);
163                let words_count = boundaries.words_count();
164
165                let mut i = 0;
166                let mut is_starting_boundary: bool = true;
167
168                while i < words_count {
169                    let rng = boundaries.word_range(i);
170                    let word = subslice(self.0.as_bytes(), rng);
171
172                    if !TokenKind::is_boundary_word(word) {
173                        if has_sep && !is_starting_boundary {
174                            ans += 1;
175                        }
176                        ans += word.len();
177                        is_starting_boundary = false;
178                    }
179
180                    i += 1;
181                }
182                ans
183            }
184        }
185    }
186
187    pub const fn const_eval<const M: usize, const N: usize>(&self) -> StrBuf<N> {
188        assert!(self.0.len() + 1 == M);
189
190        let mut buf = [0; N];
191        let mut pos = 0;
192        let s = self.0.as_bytes();
193
194        macro_rules! push {
195            ($x: expr) => {{
196                buf[pos] = $x;
197                pos += 1;
198            }};
199        }
200
201        use AsciiCase::*;
202        match self.1 {
203            Lower => {
204                while pos < s.len() {
205                    push!(s[pos].to_ascii_lowercase());
206                }
207            }
208            Upper => {
209                while pos < s.len() {
210                    push!(s[pos].to_ascii_uppercase());
211                }
212            }
213            LowerCamel | UpperCamel | Title | Snake | Kebab | ShoutySnake | ShoutyKebab => {
214                let sep = self.1.get_seperator();
215
216                let boundaries = Boundaries::<M>::new(self.0);
217                let words_count = boundaries.words_count();
218
219                let mut i = 0;
220                let mut is_starting_boundary = true;
221
222                while i < words_count {
223                    let rng = boundaries.word_range(i);
224                    let word = subslice(self.0.as_bytes(), rng);
225
226                    if !TokenKind::is_boundary_word(word) {
227                        if let (Some(sep), false) = (sep, is_starting_boundary) {
228                            push!(sep)
229                        }
230                        let mut j = 0;
231                        while j < word.len() {
232                            let b = match self.1 {
233                                Snake | Kebab => word[j].to_ascii_lowercase(),
234                                ShoutySnake | ShoutyKebab => word[j].to_ascii_uppercase(),
235                                LowerCamel | UpperCamel | Title => {
236                                    let is_upper = match self.1 {
237                                        LowerCamel => !is_starting_boundary && j == 0,
238                                        UpperCamel | Title => j == 0,
239                                        _ => unreachable!(),
240                                    };
241                                    if is_upper {
242                                        word[j].to_ascii_uppercase()
243                                    } else {
244                                        word[j].to_ascii_lowercase()
245                                    }
246                                }
247                                _ => unreachable!(),
248                            };
249                            push!(b);
250                            j += 1;
251                        }
252                        is_starting_boundary = false;
253                    }
254
255                    i += 1;
256                }
257            }
258        }
259
260        assert!(pos == N);
261
262        unsafe { StrBuf::new_unchecked(buf) }
263    }
264}
265
266#[doc(hidden)]
267#[macro_export]
268macro_rules! __conv_ascii_case {
269    ($s: expr, $case: expr) => {{
270        const INPUT: &str = $s;
271        const M: usize = INPUT.len() + 1;
272        const N: usize = $crate::__ctfe::ConvAsciiCase(INPUT, $case).output_len::<M>();
273        const OUTPUT_BUF: $crate::__ctfe::StrBuf<N> =
274            $crate::__ctfe::ConvAsciiCase(INPUT, $case).const_eval::<M, N>();
275        OUTPUT_BUF.as_str()
276    }};
277}
278
279/// Converts a string slice to a specified case. Non-ascii characters are not affected.
280///
281/// This macro is [const-context only](./index.html#const-context-only).
282///
283/// # Examples
284///
285/// ```
286/// use const_str::convert_ascii_case;
287///
288/// const S1: &str = convert_ascii_case!(lower, "Lower Case");
289/// const S2: &str = convert_ascii_case!(upper, "Upper Case");
290/// const S3: &str = convert_ascii_case!(lower_camel, "lower camel case");
291/// const S4: &str = convert_ascii_case!(upper_camel, "upper camel case");
292/// const S5: &str = convert_ascii_case!(title, "title case");
293/// const S6: &str = convert_ascii_case!(snake, "snake case");
294/// const S7: &str = convert_ascii_case!(kebab, "kebab case");
295/// const S8: &str = convert_ascii_case!(shouty_snake, "shouty snake case");
296/// const S9: &str = convert_ascii_case!(shouty_kebab, "shouty kebab case");
297///
298/// assert_eq!(S1, "lower case");
299/// assert_eq!(S2, "UPPER CASE");
300/// assert_eq!(S3, "lowerCamelCase");
301/// assert_eq!(S4, "UpperCamelCase");
302/// assert_eq!(S5, "Title Case");
303/// assert_eq!(S6, "snake_case");
304/// assert_eq!(S7, "kebab-case");
305/// assert_eq!(S8, "SHOUTY_SNAKE_CASE");
306/// assert_eq!(S9, "SHOUTY-KEBAB-CASE");
307/// ```
308#[macro_export]
309macro_rules! convert_ascii_case {
310    (lower, $s: expr) => {
311        $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::Lower)
312    };
313    (upper, $s: expr) => {
314        $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::Upper)
315    };
316    (lower_camel, $s: expr) => {
317        $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::LowerCamel)
318    };
319    (upper_camel, $s: expr) => {
320        $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::UpperCamel)
321    };
322    (title, $s: expr) => {
323        $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::Title)
324    };
325    (snake, $s: expr) => {
326        $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::Snake)
327    };
328    (kebab, $s: expr) => {
329        $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::Kebab)
330    };
331    (shouty_snake, $s: expr) => {
332        $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::ShoutySnake)
333    };
334    (shouty_kebab, $s: expr) => {
335        $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::ShoutyKebab)
336    };
337}
338
339#[cfg(test)]
340mod tests {
341    #[test]
342    fn test_conv_ascii_case() {
343        macro_rules! test_conv_ascii_case {
344            ($v: tt, $a: expr, $b: expr $(,)?) => {{
345                const A: &str = $a;
346                const B: &str = convert_ascii_case!($v, A);
347                assert_eq!(B, $b);
348                test_conv_ascii_case!(heck, $v, $a, $b);
349            }};
350            (heck, assert_eq, $c: expr, $b: expr) => {{
351                if $c != $b {
352                    println!("heck mismatch:\nheck:     {:?}\nexpected: {:?}\n", $c, $b);
353                }
354            }};
355            (heck, lower_camel, $a: expr, $b: expr) => {{
356                use heck::ToLowerCamelCase;
357                let c: String = $a.to_lower_camel_case();
358                test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
359            }};
360            (heck, upper_camel, $a: expr, $b: expr) => {{
361                use heck::ToUpperCamelCase;
362                let c: String = $a.to_upper_camel_case();
363                test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
364            }};
365            (heck, title, $a: expr, $b: expr) => {{
366                use heck::ToTitleCase;
367                let c: String = $a.to_title_case();
368                test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
369            }};
370            (heck, snake, $a: expr, $b: expr) => {{
371                use heck::ToSnakeCase;
372                let c: String = $a.to_snake_case();
373                test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
374            }};
375            (heck, kebab, $a: expr, $b: expr) => {{
376                use heck::ToKebabCase;
377                let c: String = $a.to_kebab_case();
378                test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
379            }};
380            (heck, shouty_snake, $a: expr, $b: expr) => {{
381                use heck::ToShoutySnakeCase;
382                let c: String = $a.to_shouty_snake_case();
383                test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
384            }};
385            (heck, shouty_kebab, $a: expr, $b: expr) => {{
386                use heck::ToShoutyKebabCase;
387                let c: String = $a.to_shouty_kebab_case();
388                test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
389            }};
390        }
391
392        {
393            const S: &str = "b.8";
394            test_conv_ascii_case!(lower_camel, S, "b8");
395            test_conv_ascii_case!(upper_camel, S, "B8");
396            test_conv_ascii_case!(title, S, "B 8");
397            test_conv_ascii_case!(snake, S, "b_8");
398            test_conv_ascii_case!(kebab, S, "b-8");
399            test_conv_ascii_case!(shouty_snake, S, "B_8");
400            test_conv_ascii_case!(shouty_kebab, S, "B-8");
401        }
402
403        {
404            const S: &str = "Hello World123!XMLHttp我4t5.c6.7b.8";
405            test_conv_ascii_case!(lower_camel, S, "helloWorld123XmlHttp我4t5C67b8");
406            test_conv_ascii_case!(upper_camel, S, "HelloWorld123XmlHttp我4t5C67b8");
407            test_conv_ascii_case!(title, S, "Hello World123 Xml Http 我 4t5 C6 7b 8");
408            test_conv_ascii_case!(snake, S, "hello_world123_xml_http_我_4t5_c6_7b_8");
409            test_conv_ascii_case!(kebab, S, "hello-world123-xml-http-我-4t5-c6-7b-8");
410            test_conv_ascii_case!(shouty_snake, S, "HELLO_WORLD123_XML_HTTP_我_4T5_C6_7B_8");
411            test_conv_ascii_case!(shouty_kebab, S, "HELLO-WORLD123-XML-HTTP-我-4T5-C6-7B-8");
412        }
413        {
414            const S: &str = "XMLHttpRequest";
415            test_conv_ascii_case!(lower_camel, S, "xmlHttpRequest");
416            test_conv_ascii_case!(upper_camel, S, "XmlHttpRequest");
417            test_conv_ascii_case!(title, S, "Xml Http Request");
418            test_conv_ascii_case!(snake, S, "xml_http_request");
419            test_conv_ascii_case!(kebab, S, "xml-http-request");
420            test_conv_ascii_case!(shouty_snake, S, "XML_HTTP_REQUEST");
421            test_conv_ascii_case!(shouty_kebab, S, "XML-HTTP-REQUEST");
422        }
423        {
424            const S: &str = "  hello world  ";
425            test_conv_ascii_case!(lower_camel, S, "helloWorld");
426            test_conv_ascii_case!(upper_camel, S, "HelloWorld");
427            test_conv_ascii_case!(title, S, "Hello World");
428            test_conv_ascii_case!(snake, S, "hello_world");
429            test_conv_ascii_case!(kebab, S, "hello-world");
430            test_conv_ascii_case!(shouty_snake, S, "HELLO_WORLD");
431            test_conv_ascii_case!(shouty_kebab, S, "HELLO-WORLD");
432        }
433        {
434            const S: &str = "";
435            test_conv_ascii_case!(lower_camel, S, "");
436            test_conv_ascii_case!(upper_camel, S, "");
437            test_conv_ascii_case!(title, S, "");
438            test_conv_ascii_case!(snake, S, "");
439            test_conv_ascii_case!(kebab, S, "");
440            test_conv_ascii_case!(shouty_snake, S, "");
441            test_conv_ascii_case!(shouty_kebab, S, "");
442        }
443        {
444            const S: &str = "_";
445            test_conv_ascii_case!(lower_camel, S, "");
446            test_conv_ascii_case!(upper_camel, S, "");
447            test_conv_ascii_case!(title, S, "");
448            test_conv_ascii_case!(snake, S, "");
449            test_conv_ascii_case!(kebab, S, "");
450            test_conv_ascii_case!(shouty_snake, S, "");
451            test_conv_ascii_case!(shouty_kebab, S, "");
452        }
453        {
454            const S: &str = "1.2E3";
455            test_conv_ascii_case!(lower_camel, S, "12e3");
456            test_conv_ascii_case!(upper_camel, S, "12e3");
457            test_conv_ascii_case!(title, S, "1 2e3");
458            test_conv_ascii_case!(snake, S, "1_2e3");
459            test_conv_ascii_case!(kebab, S, "1-2e3");
460            test_conv_ascii_case!(shouty_snake, S, "1_2E3");
461            test_conv_ascii_case!(shouty_kebab, S, "1-2E3");
462        }
463        {
464            const S: &str = "__a__b-c__d__";
465            test_conv_ascii_case!(lower_camel, S, "aBCD");
466            test_conv_ascii_case!(upper_camel, S, "ABCD");
467            test_conv_ascii_case!(snake, S, "a_b_c_d");
468            test_conv_ascii_case!(kebab, S, "a-b-c-d");
469            test_conv_ascii_case!(shouty_snake, S, "A_B_C_D");
470            test_conv_ascii_case!(shouty_kebab, S, "A-B-C-D");
471        }
472        {
473            const S: &str = "futures-core123";
474            test_conv_ascii_case!(lower_camel, S, "futuresCore123");
475            test_conv_ascii_case!(upper_camel, S, "FuturesCore123");
476            test_conv_ascii_case!(snake, S, "futures_core123");
477            test_conv_ascii_case!(kebab, S, "futures-core123");
478            test_conv_ascii_case!(shouty_snake, S, "FUTURES_CORE123");
479            test_conv_ascii_case!(shouty_kebab, S, "FUTURES-CORE123");
480        }
481    }
482}