Skip to main content

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    #[cfg(feature = "case")]
350    #[test]
351    fn test_conv_ascii_case() {
352        macro_rules! test_conv_ascii_case {
353            ($v: tt, $a: expr, $b: expr $(,)?) => {{
354                const A: &str = $a;
355                const B: &str = convert_ascii_case!($v, A);
356                assert_eq!(B, $b);
357                test_conv_ascii_case!(heck, $v, $a, $b);
358            }};
359            (heck, assert_eq, $c: expr, $b: expr) => {{
360                if $c != $b {
361                    println!("heck mismatch:\nheck:     {:?}\nexpected: {:?}\n", $c, $b);
362                }
363            }};
364            (heck, lower_camel, $a: expr, $b: expr) => {{
365                use heck::ToLowerCamelCase;
366                let c: String = $a.to_lower_camel_case();
367                test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
368            }};
369            (heck, upper_camel, $a: expr, $b: expr) => {{
370                use heck::ToUpperCamelCase;
371                let c: String = $a.to_upper_camel_case();
372                test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
373            }};
374            (heck, title, $a: expr, $b: expr) => {{
375                use heck::ToTitleCase;
376                let c: String = $a.to_title_case();
377                test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
378            }};
379            (heck, train, $a: expr, $b: expr) => {{
380                use heck::ToTrainCase;
381                let c: String = $a.to_train_case();
382                test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
383            }};
384            (heck, snake, $a: expr, $b: expr) => {{
385                use heck::ToSnakeCase;
386                let c: String = $a.to_snake_case();
387                test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
388            }};
389            (heck, kebab, $a: expr, $b: expr) => {{
390                use heck::ToKebabCase;
391                let c: String = $a.to_kebab_case();
392                test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
393            }};
394            (heck, shouty_snake, $a: expr, $b: expr) => {{
395                use heck::ToShoutySnakeCase;
396                let c: String = $a.to_shouty_snake_case();
397                test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
398            }};
399            (heck, shouty_kebab, $a: expr, $b: expr) => {{
400                use heck::ToShoutyKebabCase;
401                let c: String = $a.to_shouty_kebab_case();
402                test_conv_ascii_case!(heck, assert_eq, c.as_str(), $b);
403            }};
404        }
405
406        {
407            const S: &str = "b.8";
408            test_conv_ascii_case!(lower_camel, S, "b8");
409            test_conv_ascii_case!(upper_camel, S, "B8");
410            test_conv_ascii_case!(title, S, "B 8");
411            test_conv_ascii_case!(train, S, "B-8");
412            test_conv_ascii_case!(snake, S, "b_8");
413            test_conv_ascii_case!(kebab, S, "b-8");
414            test_conv_ascii_case!(shouty_snake, S, "B_8");
415            test_conv_ascii_case!(shouty_kebab, S, "B-8");
416        }
417
418        {
419            const S: &str = "Hello World123!XMLHttp我4t5.c6.7b.8";
420            test_conv_ascii_case!(lower_camel, S, "helloWorld123XmlHttp我4t5C67b8");
421            test_conv_ascii_case!(upper_camel, S, "HelloWorld123XmlHttp我4t5C67b8");
422            test_conv_ascii_case!(title, S, "Hello World123 Xml Http我4t5 C6 7b 8");
423            test_conv_ascii_case!(train, S, "Hello-World123-Xml-Http我4t5-C6-7b-8");
424            test_conv_ascii_case!(snake, S, "hello_world123_xml_http我4t5_c6_7b_8");
425            test_conv_ascii_case!(kebab, S, "hello-world123-xml-http我4t5-c6-7b-8");
426            test_conv_ascii_case!(shouty_snake, S, "HELLO_WORLD123_XML_HTTP我4T5_C6_7B_8");
427            test_conv_ascii_case!(shouty_kebab, S, "HELLO-WORLD123-XML-HTTP我4T5-C6-7B-8");
428        }
429        {
430            const S: &str = "XMLHttpRequest";
431            test_conv_ascii_case!(lower_camel, S, "xmlHttpRequest");
432            test_conv_ascii_case!(upper_camel, S, "XmlHttpRequest");
433            test_conv_ascii_case!(title, S, "Xml Http Request");
434            test_conv_ascii_case!(train, S, "Xml-Http-Request");
435            test_conv_ascii_case!(snake, S, "xml_http_request");
436            test_conv_ascii_case!(kebab, S, "xml-http-request");
437            test_conv_ascii_case!(shouty_snake, S, "XML_HTTP_REQUEST");
438            test_conv_ascii_case!(shouty_kebab, S, "XML-HTTP-REQUEST");
439        }
440        {
441            const S: &str = "  hello world  ";
442            test_conv_ascii_case!(lower_camel, S, "helloWorld");
443            test_conv_ascii_case!(upper_camel, S, "HelloWorld");
444            test_conv_ascii_case!(title, S, "Hello World");
445            test_conv_ascii_case!(train, S, "Hello-World");
446            test_conv_ascii_case!(snake, S, "hello_world");
447            test_conv_ascii_case!(kebab, S, "hello-world");
448            test_conv_ascii_case!(shouty_snake, S, "HELLO_WORLD");
449            test_conv_ascii_case!(shouty_kebab, S, "HELLO-WORLD");
450        }
451        {
452            const S: &str = "";
453            test_conv_ascii_case!(lower_camel, S, "");
454            test_conv_ascii_case!(upper_camel, S, "");
455            test_conv_ascii_case!(title, S, "");
456            test_conv_ascii_case!(train, S, "");
457            test_conv_ascii_case!(snake, S, "");
458            test_conv_ascii_case!(kebab, S, "");
459            test_conv_ascii_case!(shouty_snake, S, "");
460            test_conv_ascii_case!(shouty_kebab, S, "");
461        }
462        {
463            const S: &str = "_";
464            test_conv_ascii_case!(lower_camel, S, "");
465            test_conv_ascii_case!(upper_camel, S, "");
466            test_conv_ascii_case!(title, S, "");
467            test_conv_ascii_case!(train, S, "");
468            test_conv_ascii_case!(snake, S, "");
469            test_conv_ascii_case!(kebab, S, "");
470            test_conv_ascii_case!(shouty_snake, S, "");
471            test_conv_ascii_case!(shouty_kebab, S, "");
472        }
473        {
474            const S: &str = "1.2E3";
475            test_conv_ascii_case!(lower_camel, S, "12e3");
476            test_conv_ascii_case!(upper_camel, S, "12e3");
477            test_conv_ascii_case!(title, S, "1 2e3");
478            test_conv_ascii_case!(train, S, "1-2e3");
479            test_conv_ascii_case!(snake, S, "1_2e3");
480            test_conv_ascii_case!(kebab, S, "1-2e3");
481            test_conv_ascii_case!(shouty_snake, S, "1_2E3");
482            test_conv_ascii_case!(shouty_kebab, S, "1-2E3");
483        }
484        {
485            const S: &str = "__a__b-c__d__";
486            test_conv_ascii_case!(lower_camel, S, "aBCD");
487            test_conv_ascii_case!(upper_camel, S, "ABCD");
488            test_conv_ascii_case!(title, S, "A B C D");
489            test_conv_ascii_case!(train, S, "A-B-C-D");
490            test_conv_ascii_case!(snake, S, "a_b_c_d");
491            test_conv_ascii_case!(kebab, S, "a-b-c-d");
492            test_conv_ascii_case!(shouty_snake, S, "A_B_C_D");
493            test_conv_ascii_case!(shouty_kebab, S, "A-B-C-D");
494        }
495        {
496            const S: &str = "futures-core123";
497            test_conv_ascii_case!(lower_camel, S, "futuresCore123");
498            test_conv_ascii_case!(upper_camel, S, "FuturesCore123");
499            test_conv_ascii_case!(title, S, "Futures Core123");
500            test_conv_ascii_case!(train, S, "Futures-Core123");
501            test_conv_ascii_case!(snake, S, "futures_core123");
502            test_conv_ascii_case!(kebab, S, "futures-core123");
503            test_conv_ascii_case!(shouty_snake, S, "FUTURES_CORE123");
504            test_conv_ascii_case!(shouty_kebab, S, "FUTURES-CORE123");
505        }
506    }
507
508    #[test]
509    fn test_conv_ascii_case_runtime() {
510        use super::*;
511
512        // Test Lower case
513        let conv_lower = ConvAsciiCase("HELLO", AsciiCase::Lower);
514        let len_lower = conv_lower.output_len::<6>();
515        assert_eq!(len_lower, 5);
516        let result_lower: StrBuf<5> = conv_lower.const_eval::<6, 5>();
517        assert_eq!(result_lower.as_str(), "hello");
518
519        // Test Upper case
520        let conv_upper = ConvAsciiCase("hello", AsciiCase::Upper);
521        let len_upper = conv_upper.output_len::<6>();
522        assert_eq!(len_upper, 5);
523        let result_upper: StrBuf<5> = conv_upper.const_eval::<6, 5>();
524        assert_eq!(result_upper.as_str(), "HELLO");
525
526        // Test LowerCamel case
527        let conv_camel = ConvAsciiCase("hello_world", AsciiCase::LowerCamel);
528        let _len_camel = conv_camel.output_len::<12>();
529        let result_camel: StrBuf<10> = conv_camel.const_eval::<12, 10>();
530        assert_eq!(result_camel.as_str(), "helloWorld");
531
532        // Test UpperCamel case
533        let conv_upper_camel = ConvAsciiCase("hello_world", AsciiCase::UpperCamel);
534        let _len_upper_camel = conv_upper_camel.output_len::<12>();
535        let result_upper_camel: StrBuf<10> = conv_upper_camel.const_eval::<12, 10>();
536        assert_eq!(result_upper_camel.as_str(), "HelloWorld");
537
538        // Test Title case
539        let conv_title = ConvAsciiCase("hello_world", AsciiCase::Title);
540        let _len_title = conv_title.output_len::<12>();
541        let result_title: StrBuf<11> = conv_title.const_eval::<12, 11>();
542        assert_eq!(result_title.as_str(), "Hello World");
543
544        // Test Train case
545        let conv_train = ConvAsciiCase("hello_world", AsciiCase::Train);
546        let _len_train = conv_train.output_len::<12>();
547        let result_train: StrBuf<11> = conv_train.const_eval::<12, 11>();
548        assert_eq!(result_train.as_str(), "Hello-World");
549
550        // Test Snake case
551        let conv_snake = ConvAsciiCase("HelloWorld", AsciiCase::Snake);
552        let _len_snake = conv_snake.output_len::<11>();
553        let result_snake: StrBuf<11> = conv_snake.const_eval::<11, 11>();
554        assert_eq!(result_snake.as_str(), "hello_world");
555
556        // Test Kebab case
557        let conv_kebab = ConvAsciiCase("HelloWorld", AsciiCase::Kebab);
558        let _len_kebab = conv_kebab.output_len::<11>();
559        let result_kebab: StrBuf<11> = conv_kebab.const_eval::<11, 11>();
560        assert_eq!(result_kebab.as_str(), "hello-world");
561
562        // Test ShoutySnake case
563        let conv_shouty_snake = ConvAsciiCase("helloWorld", AsciiCase::ShoutySnake);
564        let _len_shouty_snake = conv_shouty_snake.output_len::<11>();
565        let result_shouty_snake: StrBuf<11> = conv_shouty_snake.const_eval::<11, 11>();
566        assert_eq!(result_shouty_snake.as_str(), "HELLO_WORLD");
567
568        // Test ShoutyKebab case
569        let conv_shouty_kebab = ConvAsciiCase("helloWorld", AsciiCase::ShoutyKebab);
570        let _len_shouty_kebab = conv_shouty_kebab.output_len::<11>();
571        let result_shouty_kebab: StrBuf<11> = conv_shouty_kebab.const_eval::<11, 11>();
572        assert_eq!(result_shouty_kebab.as_str(), "HELLO-WORLD");
573
574        // Test edge cases with numbers and dots
575        let conv_edge = ConvAsciiCase("1.2E3", AsciiCase::LowerCamel);
576        let _len_edge = conv_edge.output_len::<6>();
577        let result_edge: StrBuf<4> = conv_edge.const_eval::<6, 4>();
578        assert_eq!(result_edge.as_str(), "12e3");
579
580        // Test empty-ish strings
581        let conv_empty = ConvAsciiCase("___", AsciiCase::LowerCamel);
582        let _len_empty = conv_empty.output_len::<4>();
583        let result_empty: StrBuf<0> = conv_empty.const_eval::<4, 0>();
584        assert_eq!(result_empty.as_str(), "");
585    }
586}