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    Train,
132    Snake,
133    Kebab,
134    ShoutySnake,
135    ShoutyKebab,
136}
137
138impl AsciiCase {
139    const fn get_seperator(&self) -> Option<u8> {
140        match self {
141            Self::Title => Some(b' '),
142            Self::Snake | Self::ShoutySnake => Some(b'_'),
143            Self::Train | Self::Kebab | Self::ShoutyKebab => Some(b'-'),
144            _ => None,
145        }
146    }
147}
148
149pub struct ConvAsciiCase<T>(pub T, pub AsciiCase);
150
151impl ConvAsciiCase<&str> {
152    pub const fn output_len<const M: usize>(&self) -> usize {
153        assert!(self.0.len() + 1 == M);
154
155        use AsciiCase::*;
156        match self.1 {
157            Lower | Upper => self.0.len(),
158            LowerCamel | UpperCamel | Title | Train | Snake | Kebab | ShoutySnake | ShoutyKebab => {
159                let mut ans = 0;
160
161                let has_sep = self.1.get_seperator().is_some();
162
163                let boundaries = Boundaries::<M>::new(self.0);
164                let words_count = boundaries.words_count();
165
166                let mut i = 0;
167                let mut is_starting_boundary: bool = true;
168
169                while i < words_count {
170                    let rng = boundaries.word_range(i);
171                    let word = subslice(self.0.as_bytes(), rng);
172
173                    if !TokenKind::is_boundary_word(word) {
174                        if has_sep && !is_starting_boundary {
175                            ans += 1;
176                        }
177                        ans += word.len();
178                        is_starting_boundary = false;
179                    }
180
181                    i += 1;
182                }
183                ans
184            }
185        }
186    }
187
188    pub const fn const_eval<const M: usize, const N: usize>(&self) -> StrBuf<N> {
189        assert!(self.0.len() + 1 == M);
190
191        let mut buf = [0; N];
192        let mut pos = 0;
193        let s = self.0.as_bytes();
194
195        macro_rules! push {
196            ($x: expr) => {{
197                buf[pos] = $x;
198                pos += 1;
199            }};
200        }
201
202        use AsciiCase::*;
203        match self.1 {
204            Lower => {
205                while pos < s.len() {
206                    push!(s[pos].to_ascii_lowercase());
207                }
208            }
209            Upper => {
210                while pos < s.len() {
211                    push!(s[pos].to_ascii_uppercase());
212                }
213            }
214            LowerCamel | UpperCamel | Title | Train | Snake | Kebab | ShoutySnake | ShoutyKebab => {
215                let sep = self.1.get_seperator();
216
217                let boundaries = Boundaries::<M>::new(self.0);
218                let words_count = boundaries.words_count();
219
220                let mut i = 0;
221                let mut is_starting_boundary = true;
222
223                while i < words_count {
224                    let rng = boundaries.word_range(i);
225                    let word = subslice(self.0.as_bytes(), rng);
226
227                    if !TokenKind::is_boundary_word(word) {
228                        if let (Some(sep), false) = (sep, is_starting_boundary) {
229                            push!(sep)
230                        }
231                        let mut j = 0;
232                        while j < word.len() {
233                            let b = match self.1 {
234                                Snake | Kebab => word[j].to_ascii_lowercase(),
235                                ShoutySnake | ShoutyKebab => word[j].to_ascii_uppercase(),
236                                LowerCamel | UpperCamel | Title | Train => {
237                                    let is_upper = match self.1 {
238                                        LowerCamel => !is_starting_boundary && j == 0,
239                                        UpperCamel | Title | Train => j == 0,
240                                        _ => unreachable!(),
241                                    };
242                                    if is_upper {
243                                        word[j].to_ascii_uppercase()
244                                    } else {
245                                        word[j].to_ascii_lowercase()
246                                    }
247                                }
248                                _ => unreachable!(),
249                            };
250                            push!(b);
251                            j += 1;
252                        }
253                        is_starting_boundary = false;
254                    }
255
256                    i += 1;
257                }
258            }
259        }
260
261        assert!(pos == N);
262
263        unsafe { StrBuf::new_unchecked(buf) }
264    }
265}
266
267#[doc(hidden)]
268#[macro_export]
269macro_rules! __conv_ascii_case {
270    ($s: expr, $case: expr) => {{
271        const INPUT: &str = $s;
272        const M: usize = INPUT.len() + 1;
273        const N: usize = $crate::__ctfe::ConvAsciiCase(INPUT, $case).output_len::<M>();
274        const OUTPUT_BUF: $crate::__ctfe::StrBuf<N> =
275            $crate::__ctfe::ConvAsciiCase(INPUT, $case).const_eval::<M, N>();
276        OUTPUT_BUF.as_str()
277    }};
278}
279
280/// Converts a string slice to a specified case. Non-ascii characters are not affected.
281///
282/// This macro is [const-context only](./index.html#const-context-only).
283///
284/// # Examples
285///
286/// ```
287/// use const_str::convert_ascii_case;
288///
289/// const S1: &str = convert_ascii_case!(lower, "Lower Case");
290/// const S2: &str = convert_ascii_case!(upper, "Upper Case");
291/// const S3: &str = convert_ascii_case!(lower_camel, "lower camel case");
292/// const S4: &str = convert_ascii_case!(upper_camel, "upper camel case");
293/// const S5: &str = convert_ascii_case!(title, "title case");
294/// const S6: &str = convert_ascii_case!(train, "train case");
295/// const S7: &str = convert_ascii_case!(snake, "snake case");
296/// const S8: &str = convert_ascii_case!(kebab, "kebab case");
297/// const S9: &str = convert_ascii_case!(shouty_snake, "shouty snake case");
298/// const S10: &str = convert_ascii_case!(shouty_kebab, "shouty kebab case");
299///
300/// assert_eq!(S1, "lower case");
301/// assert_eq!(S2, "UPPER CASE");
302/// assert_eq!(S3, "lowerCamelCase");
303/// assert_eq!(S4, "UpperCamelCase");
304/// assert_eq!(S5, "Title Case");
305/// assert_eq!(S6, "Train-Case");
306/// assert_eq!(S7, "snake_case");
307/// assert_eq!(S8, "kebab-case");
308/// assert_eq!(S9, "SHOUTY_SNAKE_CASE");
309/// assert_eq!(S10, "SHOUTY-KEBAB-CASE");
310/// ```
311#[macro_export]
312macro_rules! convert_ascii_case {
313    (lower, $s: expr) => {
314        $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::Lower)
315    };
316    (upper, $s: expr) => {
317        $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::Upper)
318    };
319    (lower_camel, $s: expr) => {
320        $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::LowerCamel)
321    };
322    (upper_camel, $s: expr) => {
323        $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::UpperCamel)
324    };
325    (title, $s: expr) => {
326        $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::Title)
327    };
328    (train, $s: expr) => {
329        $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::Train)
330    };
331    (snake, $s: expr) => {
332        $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::Snake)
333    };
334    (kebab, $s: expr) => {
335        $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::Kebab)
336    };
337    (shouty_snake, $s: expr) => {
338        $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::ShoutySnake)
339    };
340    (shouty_kebab, $s: expr) => {
341        $crate::__conv_ascii_case!($s, $crate::__ctfe::AsciiCase::ShoutyKebab)
342    };
343}
344
345#[cfg(test)]
346mod tests {
347    #[test]
348    fn test_conv_ascii_case() {
349        macro_rules! test_conv_ascii_case {
350            ($v: tt, $a: expr, $b: expr $(,)?) => {{
351                const A: &str = $a;
352                const B: &str = convert_ascii_case!($v, A);
353                assert_eq!(B, $b);
354                test_conv_ascii_case!(heck, $v, $a, $b);
355            }};
356            (heck, assert_eq, $c: expr, $b: expr) => {{
357                if $c != $b {
358                    println!("heck mismatch:\nheck:     {:?}\nexpected: {:?}\n", $c, $b);
359                }
360            }};
361            (heck, lower_camel, $a: expr, $b: expr) => {{
362                use heck::ToLowerCamelCase;
363                let c: String = $a.to_lower_camel_case();
364                test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
365            }};
366            (heck, upper_camel, $a: expr, $b: expr) => {{
367                use heck::ToUpperCamelCase;
368                let c: String = $a.to_upper_camel_case();
369                test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
370            }};
371            (heck, title, $a: expr, $b: expr) => {{
372                use heck::ToTitleCase;
373                let c: String = $a.to_title_case();
374                test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
375            }};
376            (heck, train, $a: expr, $b: expr) => {{
377                use heck::ToTrainCase;
378                let c: String = $a.to_train_case();
379                test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
380            }};
381            (heck, snake, $a: expr, $b: expr) => {{
382                use heck::ToSnakeCase;
383                let c: String = $a.to_snake_case();
384                test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
385            }};
386            (heck, kebab, $a: expr, $b: expr) => {{
387                use heck::ToKebabCase;
388                let c: String = $a.to_kebab_case();
389                test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
390            }};
391            (heck, shouty_snake, $a: expr, $b: expr) => {{
392                use heck::ToShoutySnakeCase;
393                let c: String = $a.to_shouty_snake_case();
394                test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
395            }};
396            (heck, shouty_kebab, $a: expr, $b: expr) => {{
397                use heck::ToShoutyKebabCase;
398                let c: String = $a.to_shouty_kebab_case();
399                test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
400            }};
401        }
402
403        {
404            const S: &str = "b.8";
405            test_conv_ascii_case!(lower_camel, S, "b8");
406            test_conv_ascii_case!(upper_camel, S, "B8");
407            test_conv_ascii_case!(title, S, "B 8");
408            test_conv_ascii_case!(train, S, "B-8");
409            test_conv_ascii_case!(snake, S, "b_8");
410            test_conv_ascii_case!(kebab, S, "b-8");
411            test_conv_ascii_case!(shouty_snake, S, "B_8");
412            test_conv_ascii_case!(shouty_kebab, S, "B-8");
413        }
414
415        {
416            const S: &str = "Hello World123!XMLHttp我4t5.c6.7b.8";
417            test_conv_ascii_case!(lower_camel, S, "helloWorld123XmlHttp我4t5C67b8");
418            test_conv_ascii_case!(upper_camel, S, "HelloWorld123XmlHttp我4t5C67b8");
419            test_conv_ascii_case!(title, S, "Hello World123 Xml Http 我 4t5 C6 7b 8");
420            test_conv_ascii_case!(train, S, "Hello-World123-Xml-Http-我-4t5-C6-7b-8");
421            test_conv_ascii_case!(snake, S, "hello_world123_xml_http_我_4t5_c6_7b_8");
422            test_conv_ascii_case!(kebab, S, "hello-world123-xml-http-我-4t5-c6-7b-8");
423            test_conv_ascii_case!(shouty_snake, S, "HELLO_WORLD123_XML_HTTP_我_4T5_C6_7B_8");
424            test_conv_ascii_case!(shouty_kebab, S, "HELLO-WORLD123-XML-HTTP-我-4T5-C6-7B-8");
425        }
426        {
427            const S: &str = "XMLHttpRequest";
428            test_conv_ascii_case!(lower_camel, S, "xmlHttpRequest");
429            test_conv_ascii_case!(upper_camel, S, "XmlHttpRequest");
430            test_conv_ascii_case!(title, S, "Xml Http Request");
431            test_conv_ascii_case!(train, S, "Xml-Http-Request");
432            test_conv_ascii_case!(snake, S, "xml_http_request");
433            test_conv_ascii_case!(kebab, S, "xml-http-request");
434            test_conv_ascii_case!(shouty_snake, S, "XML_HTTP_REQUEST");
435            test_conv_ascii_case!(shouty_kebab, S, "XML-HTTP-REQUEST");
436        }
437        {
438            const S: &str = "  hello world  ";
439            test_conv_ascii_case!(lower_camel, S, "helloWorld");
440            test_conv_ascii_case!(upper_camel, S, "HelloWorld");
441            test_conv_ascii_case!(title, S, "Hello World");
442            test_conv_ascii_case!(train, S, "Hello-World");
443            test_conv_ascii_case!(snake, S, "hello_world");
444            test_conv_ascii_case!(kebab, S, "hello-world");
445            test_conv_ascii_case!(shouty_snake, S, "HELLO_WORLD");
446            test_conv_ascii_case!(shouty_kebab, S, "HELLO-WORLD");
447        }
448        {
449            const S: &str = "";
450            test_conv_ascii_case!(lower_camel, S, "");
451            test_conv_ascii_case!(upper_camel, S, "");
452            test_conv_ascii_case!(title, S, "");
453            test_conv_ascii_case!(train, S, "");
454            test_conv_ascii_case!(snake, S, "");
455            test_conv_ascii_case!(kebab, S, "");
456            test_conv_ascii_case!(shouty_snake, S, "");
457            test_conv_ascii_case!(shouty_kebab, S, "");
458        }
459        {
460            const S: &str = "_";
461            test_conv_ascii_case!(lower_camel, S, "");
462            test_conv_ascii_case!(upper_camel, S, "");
463            test_conv_ascii_case!(title, S, "");
464            test_conv_ascii_case!(train, S, "");
465            test_conv_ascii_case!(snake, S, "");
466            test_conv_ascii_case!(kebab, S, "");
467            test_conv_ascii_case!(shouty_snake, S, "");
468            test_conv_ascii_case!(shouty_kebab, S, "");
469        }
470        {
471            const S: &str = "1.2E3";
472            test_conv_ascii_case!(lower_camel, S, "12e3");
473            test_conv_ascii_case!(upper_camel, S, "12e3");
474            test_conv_ascii_case!(title, S, "1 2e3");
475            test_conv_ascii_case!(train, S, "1-2e3");
476            test_conv_ascii_case!(snake, S, "1_2e3");
477            test_conv_ascii_case!(kebab, S, "1-2e3");
478            test_conv_ascii_case!(shouty_snake, S, "1_2E3");
479            test_conv_ascii_case!(shouty_kebab, S, "1-2E3");
480        }
481        {
482            const S: &str = "__a__b-c__d__";
483            test_conv_ascii_case!(lower_camel, S, "aBCD");
484            test_conv_ascii_case!(upper_camel, S, "ABCD");
485            test_conv_ascii_case!(title, S, "A B C D");
486            test_conv_ascii_case!(train, S, "A-B-C-D");
487            test_conv_ascii_case!(snake, S, "a_b_c_d");
488            test_conv_ascii_case!(kebab, S, "a-b-c-d");
489            test_conv_ascii_case!(shouty_snake, S, "A_B_C_D");
490            test_conv_ascii_case!(shouty_kebab, S, "A-B-C-D");
491        }
492        {
493            const S: &str = "futures-core123";
494            test_conv_ascii_case!(lower_camel, S, "futuresCore123");
495            test_conv_ascii_case!(upper_camel, S, "FuturesCore123");
496            test_conv_ascii_case!(title, S, "Futures Core123");
497            test_conv_ascii_case!(train, S, "Futures-Core123");
498            test_conv_ascii_case!(snake, S, "futures_core123");
499            test_conv_ascii_case!(kebab, S, "futures-core123");
500            test_conv_ascii_case!(shouty_snake, S, "FUTURES_CORE123");
501            test_conv_ascii_case!(shouty_kebab, S, "FUTURES-CORE123");
502        }
503    }
504}