# 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? – Qwertiy Apr 22 '16 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.... – CJ Dennis Apr 22 '16 at 8:23
• Very closely related – Digital Trauma Apr 22 '16 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. – CJ Dennis Apr 23 '16 at 4:59
• @ggorlen the ... indicates it's in the middle of a string. – CJ Dennis Dec 22 '18 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!

# 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. – mazzy Dec 23 '18 at 6:40
• @mazzy fixed, thanks. – Gabriel Mills Jan 26 '19 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 ;


# 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.

# Python 2, 82 bytes

lambda x:re.sub('(\d+|.([A-Z]*|[a-z]*)(?![a-z]))(?!\$)','\\1_',x).lower()
import re


Try it online!