qubit_case/case_style.rs
1// =============================================================================
2// Copyright (c) 2025 - 2026 Haixing Hu.
3//
4// SPDX-License-Identifier: Apache-2.0
5//
6// Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! # Case Style
9//!
10//! Defines ASCII identifier naming styles and conversion helpers.
11
12use crate::CaseStyleError;
13
14use std::str::FromStr;
15
16/// XML hyphenated variable naming style, such as `lower-hyphen`.
17pub const LOWER_HYPHEN: CaseStyle = CaseStyle::LowerHyphen;
18
19/// C++/Python variable naming style, such as `lower_underscore`.
20pub const LOWER_UNDERSCORE: CaseStyle = CaseStyle::LowerUnderscore;
21
22/// Java variable naming style, such as `lowerCamel`.
23pub const LOWER_CAMEL: CaseStyle = CaseStyle::LowerCamel;
24
25/// Java and C++ class naming style, such as `UpperCamel`.
26pub const UPPER_CAMEL: CaseStyle = CaseStyle::UpperCamel;
27
28/// Java and C++ constant naming style, such as `UPPER_UNDERSCORE`.
29pub const UPPER_UNDERSCORE: CaseStyle = CaseStyle::UpperUnderscore;
30
31const VALUES: [CaseStyle; 5] = [
32 LOWER_HYPHEN,
33 LOWER_UNDERSCORE,
34 LOWER_CAMEL,
35 UPPER_CAMEL,
36 UPPER_UNDERSCORE,
37];
38
39/// Naming styles supported by this crate.
40///
41/// The conversion rules are intended for ASCII identifiers. Non-ASCII input is
42/// accepted on a best-effort basis, but its exact conversion behavior is not a
43/// stable contract.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45pub enum CaseStyle {
46 /// Hyphen separated lowercase words, such as `lower-hyphen`.
47 LowerHyphen,
48
49 /// Underscore separated lowercase words, such as `lower_underscore`.
50 LowerUnderscore,
51
52 /// Lower camel case words, such as `lowerCamel`.
53 LowerCamel,
54
55 /// Upper camel case words, such as `UpperCamel`.
56 UpperCamel,
57
58 /// Underscore separated uppercase words, such as `UPPER_UNDERSCORE`.
59 UpperUnderscore,
60}
61
62impl CaseStyle {
63 /// XML hyphenated variable naming style, such as `lower-hyphen`.
64 pub const LOWER_HYPHEN: Self = Self::LowerHyphen;
65
66 /// C++/Python variable naming style, such as `lower_underscore`.
67 pub const LOWER_UNDERSCORE: Self = Self::LowerUnderscore;
68
69 /// Java variable naming style, such as `lowerCamel`.
70 pub const LOWER_CAMEL: Self = Self::LowerCamel;
71
72 /// Java and C++ class naming style, such as `UpperCamel`.
73 pub const UPPER_CAMEL: Self = Self::UpperCamel;
74
75 /// Java and C++ constant naming style, such as `UPPER_UNDERSCORE`.
76 pub const UPPER_UNDERSCORE: Self = Self::UpperUnderscore;
77
78 /// Returns all supported naming styles in the reference implementation
79 /// order.
80 ///
81 /// # Returns
82 ///
83 /// Returns a static slice containing all naming styles.
84 #[inline]
85 pub const fn values() -> &'static [Self; 5] {
86 &VALUES
87 }
88
89 /// Parses a naming style from its canonical name.
90 ///
91 /// The comparison is case-insensitive, and hyphen and underscore are
92 /// treated as equivalent separators.
93 ///
94 /// # Parameters
95 ///
96 /// * `name` - Naming style name to parse.
97 ///
98 /// # Returns
99 ///
100 /// Returns the parsed naming style, or `CaseStyleError` carrying the
101 /// original name when no style matches.
102 pub fn of(name: &str) -> Result<Self, CaseStyleError> {
103 let normalized_name = name.replace('_', "-").to_ascii_lowercase();
104 for style in Self::values() {
105 if style.name() == normalized_name {
106 return Ok(*style);
107 }
108 }
109 Err(CaseStyleError::new(name))
110 }
111
112 /// Returns the canonical name of this naming style.
113 ///
114 /// # Returns
115 ///
116 /// Returns the lower-hyphen style name used by the JavaScript reference
117 /// implementation.
118 #[inline]
119 pub const fn name(self) -> &'static str {
120 match self {
121 Self::LowerHyphen => "lower-hyphen",
122 Self::LowerUnderscore => "lower-underscore",
123 Self::LowerCamel => "lower-camel",
124 Self::UpperCamel => "upper-camel",
125 Self::UpperUnderscore => "upper-underscore",
126 }
127 }
128
129 /// Returns the word separator inserted by this naming style.
130 ///
131 /// # Returns
132 ///
133 /// Returns `"-"` for lower hyphen, `"_"` for underscore styles, and `""`
134 /// for camel case styles.
135 #[inline]
136 pub const fn word_separator(self) -> &'static str {
137 match self {
138 Self::LowerHyphen => "-",
139 Self::LowerUnderscore | Self::UpperUnderscore => "_",
140 Self::LowerCamel | Self::UpperCamel => "",
141 }
142 }
143
144 /// Converts a string from this style to the target style.
145 ///
146 /// # Parameters
147 ///
148 /// * `target` - Target naming style.
149 /// * `value` - Source string. It should be an ASCII identifier in this
150 /// style; if it is not, conversion is best-effort.
151 ///
152 /// # Returns
153 ///
154 /// Returns the converted string. Empty input is returned as an empty
155 /// string.
156 pub fn to(self, target: Self, value: &str) -> String {
157 if value.is_empty() || target == self {
158 return value.to_string();
159 }
160 if let Some(converted) = self.quick_convert(target, value) {
161 return converted;
162 }
163 self.convert_by_words(target, value)
164 }
165
166 /// Tests whether a string strictly matches this naming style.
167 ///
168 /// # Parameters
169 ///
170 /// * `value` - String to test.
171 ///
172 /// # Returns
173 ///
174 /// Returns `true` if `value` is non-empty and follows this style's ASCII
175 /// identifier rules; otherwise returns `false`.
176 pub fn matches(self, value: &str) -> bool {
177 if value.is_empty() {
178 return false;
179 }
180 match self {
181 Self::LowerHyphen => {
182 matches_separated(value, b'-', is_ascii_lower_or_digit)
183 }
184 Self::LowerUnderscore => {
185 matches_separated(value, b'_', is_ascii_lower_or_digit)
186 }
187 Self::LowerCamel => matches_camel(value, is_ascii_lower),
188 Self::UpperCamel => matches_camel(value, is_ascii_upper),
189 Self::UpperUnderscore => {
190 matches_separated(value, b'_', is_ascii_upper_or_digit)
191 }
192 }
193 }
194
195 /// Performs optimized direct conversions for separator-only style pairs.
196 ///
197 /// # Parameters
198 ///
199 /// * `target` - Target naming style.
200 /// * `value` - Source string to convert.
201 ///
202 /// # Returns
203 ///
204 /// Returns `Some` converted string for optimized pairs, or `None` when the
205 /// generic word conversion should be used.
206 fn quick_convert(self, target: Self, value: &str) -> Option<String> {
207 match (self, target) {
208 (Self::LowerHyphen, Self::LowerUnderscore) => {
209 Some(value.replace('-', "_"))
210 }
211 (Self::LowerHyphen, Self::UpperUnderscore) => {
212 Some(value.replace('-', "_").to_ascii_uppercase())
213 }
214 (Self::LowerUnderscore, Self::LowerHyphen) => {
215 Some(value.replace('_', "-"))
216 }
217 (Self::LowerUnderscore, Self::UpperUnderscore) => {
218 Some(value.to_ascii_uppercase())
219 }
220 (Self::UpperUnderscore, Self::LowerHyphen) => {
221 Some(value.replace('_', "-").to_ascii_lowercase())
222 }
223 (Self::UpperUnderscore, Self::LowerUnderscore) => {
224 Some(value.to_ascii_lowercase())
225 }
226 _ => None,
227 }
228 }
229
230 /// Converts a string by splitting it into source words and normalizing
231 /// them.
232 ///
233 /// # Parameters
234 ///
235 /// * `target` - Target naming style.
236 /// * `value` - Source string to convert.
237 ///
238 /// # Returns
239 ///
240 /// Returns a best-effort converted string.
241 fn convert_by_words(self, target: Self, value: &str) -> String {
242 let mut out = String::with_capacity(
243 value.len() + 4 * target.word_separator().len(),
244 );
245 let mut word_start = 0;
246 let mut search_start = 0;
247 let mut has_boundary = false;
248 while let Some(boundary) = self.find_boundary(value, search_start) {
249 if word_start == boundary {
250 search_start = boundary + self.word_separator().len().max(1);
251 word_start = search_start.min(value.len());
252 continue;
253 }
254 let word = &value[word_start..boundary];
255 if word_start == 0 {
256 out.push_str(&target.normalize_first_word(word));
257 } else {
258 out.push_str(&target.normalize_word(word));
259 }
260 out.push_str(target.word_separator());
261 has_boundary = true;
262 word_start = boundary + self.word_separator().len();
263 search_start = boundary + self.word_separator().len().max(1);
264 }
265 if !has_boundary {
266 target.normalize_first_word(value)
267 } else {
268 let word = &value[word_start..];
269 out.push_str(&target.normalize_word(word));
270 out
271 }
272 }
273
274 /// Finds the next source word boundary in `value`.
275 ///
276 /// # Parameters
277 ///
278 /// * `value` - Source string to search.
279 /// * `start` - Byte index where the search starts.
280 ///
281 /// # Returns
282 ///
283 /// Returns the byte index of the next boundary, or `None` when no boundary
284 /// exists after `start`.
285 fn find_boundary(self, value: &str, start: usize) -> Option<usize> {
286 match self {
287 Self::LowerHyphen => find_first_byte(value, start, b'-'),
288 Self::LowerUnderscore | Self::UpperUnderscore => {
289 find_first_byte(value, start, b'_')
290 }
291 Self::LowerCamel | Self::UpperCamel => {
292 find_first_camel_case_boundary(value, start)
293 }
294 }
295 }
296
297 /// Normalizes a non-first word for this naming style.
298 ///
299 /// # Parameters
300 ///
301 /// * `word` - Source word to normalize.
302 ///
303 /// # Returns
304 ///
305 /// Returns the word normalized for this style.
306 fn normalize_word(self, word: &str) -> String {
307 match self {
308 Self::LowerHyphen | Self::LowerUnderscore => {
309 word.to_ascii_lowercase()
310 }
311 Self::LowerCamel | Self::UpperCamel => {
312 first_char_only_to_upper(word)
313 }
314 Self::UpperUnderscore => word.to_ascii_uppercase(),
315 }
316 }
317
318 /// Normalizes the first word for this naming style.
319 ///
320 /// # Parameters
321 ///
322 /// * `word` - First source word to normalize.
323 ///
324 /// # Returns
325 ///
326 /// Returns the first word normalized for this style.
327 fn normalize_first_word(self, word: &str) -> String {
328 match self {
329 Self::LowerCamel => word.to_ascii_lowercase(),
330 _ => self.normalize_word(word),
331 }
332 }
333}
334
335impl FromStr for CaseStyle {
336 type Err = CaseStyleError;
337
338 /// Parses a naming style from a string.
339 ///
340 /// # Parameters
341 ///
342 /// * `s` - Naming style name to parse.
343 ///
344 /// # Returns
345 ///
346 /// Returns the parsed style, or `CaseStyleError` when `s` is unknown.
347 #[inline]
348 fn from_str(s: &str) -> Result<Self, Self::Err> {
349 Self::of(s)
350 }
351}
352
353/// Finds the first occurrence of a byte from a start index.
354///
355/// # Parameters
356///
357/// * `value` - String to search.
358/// * `start` - Byte index where the search starts.
359/// * `needle` - ASCII byte to find.
360///
361/// # Returns
362///
363/// Returns the byte index of `needle`, or `None` if it is not found.
364fn find_first_byte(value: &str, start: usize, needle: u8) -> Option<usize> {
365 value
366 .as_bytes()
367 .iter()
368 .enumerate()
369 .skip(start)
370 .find_map(|(index, byte)| (*byte == needle).then_some(index))
371}
372
373/// Finds the next CamelCase word boundary.
374///
375/// # Parameters
376///
377/// * `value` - String to search.
378/// * `start` - Byte index where the search starts.
379///
380/// # Returns
381///
382/// Returns the byte index of the next CamelCase boundary, or `None` if no
383/// boundary exists.
384fn find_first_camel_case_boundary(value: &str, start: usize) -> Option<usize> {
385 let start = start.max(1);
386 value
387 .as_bytes()
388 .iter()
389 .enumerate()
390 .skip(start)
391 .find_map(|(index, _)| {
392 is_camel_case_word_boundary(value, index).then_some(index)
393 })
394}
395
396/// Tests whether an index is a CamelCase word boundary.
397///
398/// # Parameters
399///
400/// * `value` - String to test.
401/// * `index` - Byte index to examine.
402///
403/// # Returns
404///
405/// Returns `true` if `index` is a boundary according to the JavaScript
406/// reference finite-state rules; otherwise returns `false`.
407fn is_camel_case_word_boundary(value: &str, index: usize) -> bool {
408 let bytes = value.as_bytes();
409 if index == 0 || index >= bytes.len() {
410 return false;
411 }
412 let current_type = char_type(bytes[index]);
413 if current_type == CharType::Other {
414 return false;
415 }
416 let previous_type = char_type(bytes[index - 1]);
417 match previous_type {
418 CharType::Lower => {
419 current_type == CharType::Upper || current_type == CharType::Digit
420 }
421 CharType::Upper => {
422 if current_type == CharType::Lower {
423 false
424 } else if current_type == CharType::Digit {
425 true
426 } else if current_type == CharType::Upper {
427 let next = bytes
428 .get(index + 1)
429 .copied()
430 .map(char_type)
431 .unwrap_or(CharType::Other);
432 next == CharType::Lower
433 } else {
434 false
435 }
436 }
437 CharType::Digit => current_type == CharType::Upper,
438 CharType::Other => false,
439 }
440}
441
442/// ASCII character classes used by CamelCase boundary detection.
443#[derive(Debug, Clone, Copy, PartialEq, Eq)]
444enum CharType {
445 /// ASCII uppercase letter.
446 Upper,
447
448 /// ASCII lowercase letter.
449 Lower,
450
451 /// ASCII decimal digit.
452 Digit,
453
454 /// Any other byte.
455 Other,
456}
457
458/// Classifies an ASCII byte.
459///
460/// # Parameters
461///
462/// * `byte` - Byte to classify.
463///
464/// # Returns
465///
466/// Returns the character class used by the boundary detector.
467fn char_type(byte: u8) -> CharType {
468 if is_ascii_upper(byte) {
469 CharType::Upper
470 } else if is_ascii_lower(byte) {
471 CharType::Lower
472 } else if is_ascii_digit(byte) {
473 CharType::Digit
474 } else {
475 CharType::Other
476 }
477}
478
479/// Converts a word to upper camel capitalization.
480///
481/// # Parameters
482///
483/// * `word` - Source word to normalize.
484///
485/// # Returns
486///
487/// Returns the word with its first ASCII character uppercased and remaining
488/// ASCII characters lowercased.
489fn first_char_only_to_upper(word: &str) -> String {
490 let mut chars = word.chars();
491 let Some(first) = chars.next() else {
492 return String::new();
493 };
494 let mut out = String::with_capacity(word.len());
495 out.push(first.to_ascii_uppercase());
496 out.push_str(&chars.as_str().to_ascii_lowercase());
497 out
498}
499
500/// Tests whether a separated style string matches all separator rules.
501///
502/// # Parameters
503///
504/// * `value` - String to test.
505/// * `separator` - Required ASCII separator byte.
506/// * `is_word_byte` - Predicate for valid non-separator bytes.
507///
508/// # Returns
509///
510/// Returns `true` if `value` has no leading, trailing, or repeated separators
511/// and every word byte satisfies `is_word_byte`.
512fn matches_separated(
513 value: &str,
514 separator: u8,
515 is_word_byte: fn(u8) -> bool,
516) -> bool {
517 let bytes = value.as_bytes();
518 if bytes.first() == Some(&separator) || bytes.last() == Some(&separator) {
519 return false;
520 }
521 let mut last_is_separator = false;
522 for byte in bytes {
523 if *byte == separator {
524 if last_is_separator {
525 return false;
526 }
527 last_is_separator = true;
528 } else if is_word_byte(*byte) {
529 last_is_separator = false;
530 } else {
531 return false;
532 }
533 }
534 true
535}
536
537/// Tests whether a camel style string matches the first character rule.
538///
539/// # Parameters
540///
541/// * `value` - String to test.
542/// * `is_valid_first` - Predicate for the required first byte.
543///
544/// # Returns
545///
546/// Returns `true` if the first byte satisfies `is_valid_first` and all
547/// following bytes are ASCII letters or digits.
548fn matches_camel(value: &str, is_valid_first: fn(u8) -> bool) -> bool {
549 let bytes = value.as_bytes();
550 if !is_valid_first(bytes[0]) {
551 return false;
552 }
553 bytes.iter().skip(1).all(|byte| is_ascii_alnum(*byte))
554}
555
556/// Tests whether a byte is an ASCII lowercase letter.
557///
558/// # Parameters
559///
560/// * `byte` - Byte to test.
561///
562/// # Returns
563///
564/// Returns `true` for `a` through `z`.
565#[inline]
566fn is_ascii_lower(byte: u8) -> bool {
567 byte.is_ascii_lowercase()
568}
569
570/// Tests whether a byte is an ASCII uppercase letter.
571///
572/// # Parameters
573///
574/// * `byte` - Byte to test.
575///
576/// # Returns
577///
578/// Returns `true` for `A` through `Z`.
579#[inline]
580fn is_ascii_upper(byte: u8) -> bool {
581 byte.is_ascii_uppercase()
582}
583
584/// Tests whether a byte is an ASCII decimal digit.
585///
586/// # Parameters
587///
588/// * `byte` - Byte to test.
589///
590/// # Returns
591///
592/// Returns `true` for `0` through `9`.
593#[inline]
594fn is_ascii_digit(byte: u8) -> bool {
595 byte.is_ascii_digit()
596}
597
598/// Tests whether a byte is an ASCII letter or digit.
599///
600/// # Parameters
601///
602/// * `byte` - Byte to test.
603///
604/// # Returns
605///
606/// Returns `true` for ASCII letters or decimal digits.
607#[inline]
608fn is_ascii_alnum(byte: u8) -> bool {
609 byte.is_ascii_alphanumeric()
610}
611
612/// Tests whether a byte is an ASCII lowercase letter or digit.
613///
614/// # Parameters
615///
616/// * `byte` - Byte to test.
617///
618/// # Returns
619///
620/// Returns `true` for lowercase ASCII letters or decimal digits.
621#[inline]
622fn is_ascii_lower_or_digit(byte: u8) -> bool {
623 is_ascii_lower(byte) || is_ascii_digit(byte)
624}
625
626/// Tests whether a byte is an ASCII uppercase letter or digit.
627///
628/// # Parameters
629///
630/// * `byte` - Byte to test.
631///
632/// # Returns
633///
634/// Returns `true` for uppercase ASCII letters or decimal digits.
635#[inline]
636fn is_ascii_upper_or_digit(byte: u8) -> bool {
637 is_ascii_upper(byte) || is_ascii_digit(byte)
638}