DEV Community

Viren B
Viren B

Posted on • Originally published at virenb.cc

Solving "Spinal Tap Case" / freeCodeCamp Algorithm Challenges

'Spinal Tap Case'

Let's solve freeCodeCamp's intermediate algorithm scripting challenge, 'Spinal Tap Case'.

Starter Code

function spinalCase(str) {
  return str;
}

spinalCase('This Is Spinal Tap');

Instructions

Convert a string to spinal case. Spinal case is all-lowercase-words-joined-by-dashes.

Test Cases

spinalCase("This Is Spinal Tap") should return "this-is-spinal-tap".
spinalCase("thisIsSpinalTap") should return "this-is-spinal-tap".
spinalCase("The_Andy_Griffith_Show") should return "the-andy-griffith-show".
spinalCase("Teletubbies say Eh-oh") should return "teletubbies-say-eh-oh".
spinalCase("AllThe-small Things") should return "all-the-small-things".

Our Approach

The instructions for this challenge are short and to the point.

  • Our one input is str, a string. Looking at the test cases, there can be spaces or no spaces.

  • We must return a string.

  • We need to convert str to all lowercase and have each word separated by a '-' (I hear RegEx calling...).

I'm sure there could be non RegEx solutions, I did try one intially but it wouldn't work as it was only working if the words in str were separated by white spaces.

# Failed Attempt No. 1
"This Is Spinal Tap".split(' ').join('-').toLowerCase();
"this-is-spinal-tap"
// This worked

"The_Andy_Griffith_Show".split(' ').join('-').toLowerCase()
"the_andy_griffith_show"
// Nope

I figured RegEx would be the optimal solution for this challenge. I am not that familiar or comfortable with using it but let's give it a try.

The cases we had to consider were: white spaces, underscores, and uppercased letters.

There are a lot of resources and tools for learning about Regular Expressions if you'd like to read more:

Regular Expressions (MDN)

https://regexr.com/

Regular expressions on javascript.info

In this challenge, I plan to use the .replace() method. It looks for the pattern we provide and will replace it with what we use in the second argument. More can be read about how to use it on MDN: String.replace()

Here is a small example of how to use it:

// String.replace(RegEx here, replacement)
console.log('Hello World'.replace(/[A-Z]/, '$'))
'$ello World'
// The above replaces the first capital letter it finds with a '$'

// Adding the /g modifier will go through every capital letter, not just stop after the first capital letter found
console.log('Hello World'.replace(/[A-Z]/g, '$'))
'$ello $orld'

So now, knowing the above (kind of), and looking at the test cases, we should try to create a whitespace in the cases were there isn't one:

spinalCase("thisIsSpinalTap")
spinalCase("AllThe-small Things")

We want to create a space between lower case and upper case words. We want a RegEx that will replace 'thisIs' to 'this Is'.

([a-z]) is for all lower cased letters and ([A-Z]) is for upper cased letters so we can begin with that.

After a lot of reading, I found this to be helpful on how to set up this replace() function.

MDN: RegExp.$1-$9

console.log('helloWorld'.replace(/([a-z])([A-Z])/g, '$1 $2'));
// "hello World"

The above adds a space between a lower case letter and an upper case letter.

console.log("thisIsSpinalTap".replace(/([a-z])([A-Z])/g, '$1 $2'));
// "this Is Spinal Tap"

So now we just have to figure out how to replace white spaces (or underscores) with dashes and then lower case the whole string.

From reading some documentation, \s is what we want to use for white spaces. For underscores, we can use _. The OR operator is |.

The other thing to add is +. From MDN, "Matches the preceding item "x" 1 or more times. Equivalent to {1,}. For example, /a+/ matches the "a" in "candy" and all the "a"'s in "caaaaaaandy"."

So our function should now look something like this,

replace(/([a-z])([A-Z])/g, '$1 $2').replace(/\s+|_+/g, '-')

To test it out (with the hardest test case),

"AllThe-small Things".replace(/([a-z])([A-Z])/g, '$1 $2').replace(/\s+|_+/g, '-')
// "All-The-small-Things"

All that is left now is to lower case all the letters. We can use a built-in string method, no need for RegEx.

String.toLowerCase() on MDN

As always, make sure your function returns something.

Our Solution

function spinalCase(str) {
  return str.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/\s+|_+/g, '-').toLowerCase();
}

spinalCase('This Is Spinal Tap');

Links & Resources

'Spinal Tap Case' Challenge on fCC

freeCodeCamp

Donate to FCC!

Solution on my GitHub

Thank you for reading!

Discussion (5)

Collapse
willsmart profile image
willsmart • Edited

Nice one!
This can be made a little cleaner though by using zero-width assertions to divide up the camelCased words, that way you don't need to reinsert any captures and can do it all in one go...
I'd do something like:

kebabMe = s => s.replace(/(?<=[A-Za-z])(?=[A-Z])|[^A-Za-z]+/g, '-').toLowerCase()

the /(?<=[A-Za-z])(?=[A-Z])/ part matches the gap between words, i.e. between a letter and a following upper-case letter but not the letters themselves.

So, the whole regex matches any one of those gaps, and any string of non-letters.
Of course, this screws up numbers completely 🤷‍♂️

Collapse
virenb profile image
Viren B Author

thanks for the thorough explanation on that! 🤯🤯

Collapse
willsmart profile image
willsmart

No worries, hope it helps!

BTW I've updated the main line of code from /(?<=[a-z])(?=[A-Z])... to /(?<=[A-Za-z])(?=[A-Z])... matching the explanation below (sort of changed my mind on that halfway through writing).

Using [A-Za-z] just makes sure that SentencesContainingAWordWithOneLetter get translated correctly.
Of course it breaks support for CONSTANT_CASE, but what can you do?

Thread Thread
ttatsf profile image
tatsuo fukuchi • Edited

How do you like this?

const spinalCase = s => 
  s.replace(
    /(?<=[Aa-z])(?=[A-Z][a-z])|(?<=[a-z])(?=A)|[^A-Za-z]+/g
    , '-'
  )
  .toLowerCase()

spinalCase('SentencesContainingAWordWithOneLetter CONSTANT_CASE')  
// 'sentences-containing-a-word-with-one-letter-constant-case'
Thread Thread
willsmart profile image
willsmart

Well, it's a trade-off. On the one hand "AAAA" is the word "A" four times in UpperCamelCase, on the other hand it's the word "AAAA" in CONSTANT_CASE. In the end the problem is just ambiguous.

You've solved the tradeoff by only breaking camelcase words where the next word has more than one char
That may be a fair strategy since it means that constant case is well covered, but does mean some pretty reasonable camel case sentences won't work. ("ThisIsASpinalTap" -> "this-isa-spinal-tap" )

In the end, it's about figuring out which cases are important and what rules you want to cover.

I think a good medium might be to take blocks of uppercase letters surrounded by non-letters, and stop them from being clobbered as camel case words by lowercasing them.

f = s => s
  .replace(
    /(?<=[^A-Za-z]|^)[A-Z]+(?=[^A-Za-z]|$)/g, 
    w=>w.toLowerCase()
  )
  .replace( // edited to handle numbers a bit
    /(?<=[A-Za-z])(?=[A-Z0-9])|(?<=[0-9])(?=[A-Za-z])|[^A-Za-z0-9]+/g,
    '-'
  ).toLowerCase()


a=[
"ThisIsASpinalTapAAAA",
"THIS_IS_A_SPINAL_TAP_A_A_A_A",
"thisIsASpinalTapAAAA",
"this-is-a-spinal-tap-a-a-a-a",
"this is a spinal tap a a a a"
]
a.map(f) 
// -> [
"this-is-a-spinal-tap-a-a-a-a", 
"this-is-a-spinal-tap-a-a-a-a",
 "this-is-a-spinal-tap-a-a-a-a", 
"this-is-a-spinal-tap-a-a-a-a", 
"this-is-a-spinal-tap-a-a-a-a"
]