@@ -394,6 +394,100 @@ pub fn to_title_case(s: String) -> String {
394394 } )
395395 string . join ( capitalized , " " )
396396}
397+
398+ // ----------------------------------------------------------------------------
399+ // Dedicated, Non-Destructive Case Converters
400+ // ----------------------------------------------------------------------------
401+
402+ fn is_upper_char ( g : String ) -> Bool {
403+ case string . to_utf_codepoints ( g ) {
404+ [ cp ] -> {
405+ let code = string . utf_codepoint_to_int ( cp )
406+ code >= 0x41 && code <= 0x5A
407+ }
408+ _ -> False
409+ }
410+ }
411+
412+ fn is_lower_char ( g : String ) -> Bool {
413+ case string . to_utf_codepoints ( g ) {
414+ [ cp ] -> {
415+ let code = string . utf_codepoint_to_int ( cp )
416+ code >= 0x61 && code <= 0x7A
417+ }
418+ _ -> False
419+ }
420+ }
421+
422+ fn camel_to_snake_loop ( chars : List ( String ) , acc : String , prev_char : String ) -> String {
423+ case chars {
424+ [ ] -> acc
425+ [ c , .. rest ] -> {
426+ let c_is_upper = is_upper_char ( c )
427+ let prev_is_lower = is_lower_char ( prev_char )
428+ let next_is_lower = case rest {
429+ [ n , .. ] -> is_lower_char ( n )
430+ [ ] -> False
431+ }
432+
433+ let insert_underscore = { prev_is_lower && c_is_upper } || { is_upper_char ( prev_char ) && c_is_upper && next_is_lower }
434+
435+ let new_acc = case insert_underscore && acc != "" && ! string . ends_with ( acc , "_" ) {
436+ True -> acc <> "_" <> string . lowercase ( c )
437+ False -> acc <> string . lowercase ( c )
438+ }
439+ camel_to_snake_loop ( rest , new_acc , c )
440+ }
441+ }
442+ }
443+
444+ /// Converts camelCase or PascalCase to snake_case without aggressively stripping characters.
445+ ///
446+ /// camel_to_snake("camelCase") -> "camel_case"
447+ /// camel_to_snake("XMLHttpRequest") -> "xml_http_request"
448+ ///
449+ pub fn camel_to_snake ( s : String ) -> String {
450+ camel_to_snake_loop ( string . to_graphemes ( s ) , "" , "" )
451+ }
452+
453+ /// Alias for camel_to_snake.
454+ pub fn pascal_to_snake ( s : String ) -> String {
455+ camel_to_snake ( s )
456+ }
457+
458+ /// Converts snake_case to camelCase without aggressively stripping characters.
459+ ///
460+ /// snake_to_camel("snake_case_name") -> "snakeCaseName"
461+ ///
462+ pub fn snake_to_camel ( s : String ) -> String {
463+ let parts = string . split ( s , "_" )
464+ case parts {
465+ [ ] -> ""
466+ [ first , .. rest ] -> {
467+ let camel_rest = list . fold ( rest , "" , fn ( acc , part ) {
468+ case string . is_empty ( part ) {
469+ True -> acc
470+ False -> acc <> string . uppercase ( string . slice ( part , 0 , 1 ) ) <> string . slice ( part , 1 , string . length ( part ) - 1 )
471+ }
472+ } )
473+ string . lowercase ( first ) <> camel_rest
474+ }
475+ }
476+ }
477+
478+ /// Converts snake_case to PascalCase without aggressively stripping characters.
479+ ///
480+ /// snake_to_pascal("snake_case_name") -> "SnakeCaseName"
481+ ///
482+ pub fn snake_to_pascal ( s : String ) -> String {
483+ let parts = string . split ( s , "_" )
484+ list . fold ( parts , "" , fn ( acc , part ) {
485+ case string . is_empty ( part ) {
486+ True -> acc
487+ False -> acc <> string . uppercase ( string . slice ( part , 0 , 1 ) ) <> string . slice ( part , 1 , string . length ( part ) - 1 )
488+ }
489+ } )
490+ }
397491// Note: normalizer helpers (NFC/NFD/NFKC/NFKD) are intentionally not
398492// exported by the `str` library to avoid introducing an OTP dependency.
399493// If you need to use OTP normalization, define a small helper in your
0 commit comments