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