CamelCase2snake_case()

Write a function to convert CamelCased text to snake_case: FunctionForHTMLManipulation becomes function_for_html_manipulation

The input text will be a single suitable identifier in many languages. It must start with an English letter, then be followed by any number of English letters or digits. No other characters (spaces, symbols, etc.) are allowed.

Each "word" within the CamelCased text will start with a capital letter unless at the beginning of the text or immediately after a digit, and be followed by zero or more letters, all of the same case. Groups of digits will be considered as separate words but pass through unchanged.

In other words, a lowercase letter followed by an uppercase letter indicates a word break. Any letter and digit next to each other indicates a word break. An uppercase letter followed by another uppercase letter and a lowercase letter indicates a word break.

...lU... => ...l_u...
...l9... => ...l_9...
...U9... => ...u_9...
...9l... => ...9_l...
...9U... => ...9_u...
...UUl... => ...u_ul...

Both Buy24Beers and buy24beers become buy_24_beers.
MacDonaldAndObrian becomes mac_donald_and_obrian.
MACDonaldAndOBrian becomes mac_donald_and_o_brian.

• "MACDonaldAndOBrian becomes mac_donald_and_o_brian" - why? Apr 22, 2016 at 8:20
• @Qwertiy Because I thought those names would be fun. Unless you're asking about the rule, which is covered by ...UUl... => ...u_ul.... Apr 22, 2016 at 8:23
• Very closely related Apr 22, 2016 at 20:04
• @DigitalTrauma Amazingly close to my original question but without the complaints about being two questions in one and no downvotes! The biggest difference is in the treatment of ALLCAPS strings. I searched to see if the question had been asked before but I didn't find it. Apr 23, 2016 at 4:59
• @ggorlen the ... indicates it's in the middle of a string. Dec 22, 2018 at 21:29

Retina, 61 37 bytes

r1>[A-Z]?[a-z]+|[A-Z]+|\d+
_$& TLl  Try it online! (Slightly modified to run a full test suite.) Explanation Instead of finding word boundaries to insert underscores, we simply match each word and prepend a _. Matching words from the left is a bit annoying because of the UUl rule, but using .NET's right-to-left matching we can easily match words greedily. To avoid a leading _, we make use of Retina's limits. r1>[A-Z]?[a-z]+|[A-Z]+|\d+ _$&


The r activates right-to-left mode, the 1> tells Retina to process everything except the first match (counting from left to right). Then there's four types of "words": Ulll, lll, UUU, ddd. These are easily matched with the given pattern. The substitution just writes a _ followed by the word itself.

TLl


This simply turns upper case into lower case to complete the transformation.

JavaScript (ES6), 79 bytes

s=>s.match(/[A-Z]+(?=[A-Z][a-z]|\d|$)|[A-Z]?[a-z]+|\d+/g).join_.toLowerCase()  JavaScript (ES6), 89 bytes s=>s.replace(/\d(?=\D)|\D(?=\d)|[a-z](?=[A-Z])|[A-Z](?=[A-Z][a-z])/g,"$&_").toL‌​owerCase()


Powershell, 77 bytes

$args-creplace'\d(?=\D)|\D(?=\d)|[a-z](?=[A-Z])|.(?=[A-Z][a-z])','$&_'|% *wer


Less golfed test script:

$f = {$args-creplace'\d(?=\D)|\D(?=\d)|[a-z](?=[A-Z])|.(?=[A-Z][a-z])','$&_'|% toLower } @( ,("Buy24Beers", "buy_24_beers") ,("buy24beers", "buy_24_beers") ,("MacDonaldAndObrian", "mac_donald_and_obrian") ,("MACDonaldAndOBrian", "mac_donald_and_o_brian") ,("BigD", "big_d") ) | % {$s,$expected =$_
$result = &$f $s "$($result-ceq$expected): $result" }  Output: True: buy_24_beers True: buy_24_beers True: mac_donald_and_obrian True: mac_donald_and_o_brian True: big_d  Retina, 62 bytes Shamelessly translated from the JavaScript solution. \d(?=\D)|\D(?=\d)|[a-z](?=[A-Z])|[A-Z](?=[A-Z][a-z])$&_
TLl


Try it online!

Lua, 135 bytes

function snake(s)return s:gsub('%f[^%l]%u','_%1'):gsub('%f[^%a]%d','_%1'):gsub('%f[^%d]%a','_%1'):gsub('(%u)(%u%l)','%1_%2'):lower()end


Try it online!

This solution benefits from Lua's shorthand notation for C's character classes (lowercase %l, uppercase %u, alphabetic %a, digit %d), frontier notation (%f[]), and from the whole match being added as the implicit first capture in the absence of any other captures.

lambda x:re.sub('(\d+|.([A-Z]*|[a-z]*)(?![a-z]))(?!$)','\\1_',x).lower() import re  Try it online! PowerShell, 68 92 bytes Briefly deleted, +24 bytes for using the wrong RegEx. ($args-creplace'\d(?=\D)|\D(?=\d)|[a-z](?=[A-Z])|.(?=[A-Z][a-z])','$&_').Trim('_').ToLower()  Try it online! Basically the same as the JavaScript solutions. • It does not work with buy24beers and MACDonaldAndOBrian. Sorry. Dec 23, 2018 at 6:40 • @mazzy fixed, thanks. Jan 26, 2019 at 14:58 Factor, 140 bytes [ R/ [a-z][A-Z][a-z]/ [ dup from>> swap dup to>> swap seq>> subseq R/ [A-Z][a-z]/ [ "_" prepend ] re-replace-with ] re-replace-with >lower ]  Ungolfed: : slice>subseq ( slice -- subseq ) dup from>> swap dup to>> swap seq>> subseq ; : camel-case>snake-case ( string -- string' ) R/ [a-z][A-Z][a-z]/ [ slice>subseq R/ [A-Z][a-z]/ [ "_" prepend ] re-replace-with ] re-replace-with >lower ;  Raku, 61 bytes {S:g:P5/\d+|[A-Z]+(?=[A-Z]|\d)|[A-Z][a-z]*/{$/.lc~"_"}/.chop}


Try it online!