Skip to content

Commit 6385b78

Browse files
authored
builtin: fix camel_to_snake underscore placement in uppercase runs (#26634)
* builtin: fix camel_to_snake() underscore placement in uppercase runs When an uppercase run transition to a lowercase letter, the underscore was placed after the run instead of before its last letter: HTMLParser => html_parser (was: htmlp_arser) XMLToJSON => xml_to_json (was: xmlt_o_json) getHTTPSURL => get_https_url (was: get_httpsu_rl) LastNName => last_n_name (was: last_nn_ame) The fix shifts the last uppercase character forward one slot and inserts '_' before it, treating the last uppercase of a run as the start of the next word, Python's inflection, and libraries. Update existing test expectations and identifier cases. * fix: fix identation * fix: fix camel_to_snake regression for uppercase runs followed by digits * fix(builtin): split acronym boundary in camel_to_snake * v fmt * fix: adjust examples for correct doc generation * feat: handle http2_server edge case
1 parent 2f13152 commit 6385b78

2 files changed

Lines changed: 28 additions & 3 deletions

File tree

‎vlib/builtin/string.v‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2167,7 +2167,7 @@ pub fn (str string) is_hex() bool {
21672167
for i < str.len {
21682168
// TODO: remove this workaround for v2's parser
21692169
// vfmt off
2170-
if (str[i] < `0` || str[i] > `9`) &&
2170+
if (str[i] < `0` || str[i] > `9`) &&
21712171
((str[i] < `a` || str[i] > `f`) && (str[i] < `A` || str[i] > `F`)) {
21722172
return false
21732173
}
@@ -2844,7 +2844,9 @@ pub fn (s string) is_identifier() bool {
28442844
// Example: assert 'Abcd'.camel_to_snake() == 'abcd'
28452845
// Example: assert 'aaBB'.camel_to_snake() == 'aa_bb'
28462846
// Example: assert 'BBaa'.camel_to_snake() == 'bb_aa'
2847-
// Example: assert 'aa_BB'.camel_to_snake() == 'aa_bb'
2847+
// Example: assert 'HTTPServer'.camel_to_snake() == 'http_server'
2848+
// Example: assert 'HTTP2Server'.camel_to_snake() == 'http2_server'
2849+
// Example: assert 'XML2JSON'.camel_to_snake() == 'xml_2_json'
28482850
@[direct_array_access]
28492851
pub fn (s string) camel_to_snake() string {
28502852
if s.len == 0 {
@@ -2858,6 +2860,7 @@ pub fn (s string) camel_to_snake() string {
28582860
// handle the first two chars separately to reduce load.
28592861
mut pos := 2
28602862
mut prev_is_upper := false
2863+
mut prev_inserted_boundary := false
28612864
unsafe {
28622865
if s[0].is_capital() {
28632866
b[0] = s[0] + 32
@@ -2884,13 +2887,30 @@ pub fn (s string) camel_to_snake() string {
28842887
}
28852888
}
28862889
for i := 2; i < s.len; i++ {
2890+
mut has_boundary_before_upper := false
28872891
c := s[i]
28882892
c_is_upper := c.is_capital()
2893+
c_is_number := c.is_digit()
2894+
next_is_lower := i + 1 < s.len && s[i + 1].is_letter() && !s[i + 1].is_capital()
2895+
next2_is_lower := i + 2 < s.len && s[i + 2].is_letter() && !s[i + 2].is_capital()
2896+
// Cases: `XML2JSON == xml_2_json` || `HTTP2Server == http2_server`
2897+
skip_digit := c_is_number && prev_is_upper && !next_is_lower && next2_is_lower
2898+
// Cases: `HTTPServer == http_server` || `getHTTPSUrl == get_https_url`
2899+
if c_is_upper && prev_is_upper && i >= 2 && s[i - 2].is_capital() && next_is_lower
2900+
&& c != `_` {
2901+
unsafe {
2902+
if b[pos - 1] != `_` {
2903+
b[pos] = `_`
2904+
pos++
2905+
}
2906+
}
2907+
has_boundary_before_upper = true
2908+
}
28892909
// Cases: `aBcd == a_bcd` || `ABcd == ab_cd`
28902910
// TODO: remove this workaround for v2's parser
28912911
// vfmt off
28922912
if ((c_is_upper && !prev_is_upper) ||
2893-
(!c_is_upper && prev_is_upper && s[i - 2].is_capital())) &&
2913+
(!c_is_upper && prev_is_upper && s[i - 2].is_capital() && !prev_inserted_boundary && !skip_digit)) &&
28942914
c != `_` {
28952915
unsafe {
28962916
if b[pos - 1] != `_` {
@@ -2905,6 +2925,7 @@ pub fn (s string) camel_to_snake() string {
29052925
b[pos] = lower_c
29062926
}
29072927
prev_is_upper = c_is_upper
2928+
prev_inserted_boundary = has_boundary_before_upper
29082929
pos++
29092930
}
29102931
unsafe {

‎vlib/builtin/string_test.v‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1667,6 +1667,7 @@ fn test_camel_to_snake() {
16671667
assert 'aBcd'.camel_to_snake() == 'a_bcd'
16681668
assert 'AAbb'.camel_to_snake() == 'aa_bb'
16691669
assert 'aaBB'.camel_to_snake() == 'aa_bb'
1670+
assert 'BBaa'.camel_to_snake() == 'bb_aa'
16701671
assert 'aaBbCcDD'.camel_to_snake() == 'aa_bb_cc_dd'
16711672
assert 'AAbbCC'.camel_to_snake() == 'aa_bb_cc'
16721673
assert 'aaBBcc'.camel_to_snake() == 'aa_bb_cc'
@@ -1677,6 +1678,9 @@ fn test_camel_to_snake() {
16771678
assert '_aBcd'.camel_to_snake() == '_a_bcd'
16781679
assert '_a_Bcd'.camel_to_snake() == '_a_bcd'
16791680
assert '_AbCDe_'.camel_to_snake() == '_ab_cd_e_'
1681+
assert 'HTTPServer'.camel_to_snake() == 'http_server'
1682+
assert 'HTTP2Server'.camel_to_snake() == 'http2_server'
1683+
assert 'XML2JSON'.camel_to_snake() == 'xml_2_json'
16801684
}
16811685

16821686
fn test_snake_to_camel() {

0 commit comments

Comments
 (0)