12
\$\begingroup\$

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.

\$\endgroup\$
  • 6
    \$\begingroup\$ "MACDonaldAndOBrian becomes mac_donald_and_o_brian" - why? \$\endgroup\$ – Qwertiy Apr 22 '16 at 8:20
  • 2
    \$\begingroup\$ @Qwertiy Because I thought those names would be fun. Unless you're asking about the rule, which is covered by ...UUl... => ...u_ul.... \$\endgroup\$ – CJ Dennis Apr 22 '16 at 8:23
  • \$\begingroup\$ Very closely related \$\endgroup\$ – Digital Trauma Apr 22 '16 at 20:04
  • \$\begingroup\$ @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. \$\endgroup\$ – CJ Dennis Apr 23 '16 at 4:59
  • 1
    \$\begingroup\$ @ggorlen the ... indicates it's in the middle of a string. \$\endgroup\$ – CJ Dennis Dec 22 '18 at 21:29
7
\$\begingroup\$

Retina, 61 37 bytes

r1>`[A-Z]?[a-z]+|[A-Z]+|\d+
_$&
T`L`l

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.

T`L`l

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

\$\endgroup\$
6
\$\begingroup\$

JavaScript (ES6), 79 bytes

s=>s.match(/[A-Z]+(?=[A-Z][a-z]|\d|$)|[A-Z]?[a-z]+|\d+/g).join`_`.toLowerCase()
\$\endgroup\$
3
\$\begingroup\$

JavaScript (ES6), 89 bytes

s=>s.replace(/\d(?=\D)|\D(?=\d)|[a-z](?=[A-Z])|[A-Z](?=[A-Z][a-z])/g,"$&_").toL‌​owerCase()
\$\endgroup\$
2
\$\begingroup\$

Powershell, 77 bytes

Based on Neil's answer.

$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
\$\endgroup\$
1
\$\begingroup\$

Retina, 62 bytes

Shamelessly translated from the JavaScript solution.

\d(?=\D)|\D(?=\d)|[a-z](?=[A-Z])|[A-Z](?=[A-Z][a-z])
$&_
T`L`l

Try it online!

\$\endgroup\$
1
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ It does not work with buy24beers and MACDonaldAndOBrian. Sorry. \$\endgroup\$ – mazzy Dec 23 '18 at 6:40
  • 1
    \$\begingroup\$ @mazzy fixed, thanks. \$\endgroup\$ – Gabriel Mills Jan 26 at 14:58
0
\$\begingroup\$

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 ;
\$\endgroup\$
0
\$\begingroup\$

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.

\$\endgroup\$
0
\$\begingroup\$

Python 2, 82 bytes

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

Try it online!

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.