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