26

I have a kebabize function which converts camelCase to kebab-case. I am sharing my code. Can it be more optimized? I know this problem can be solved using regex. But, I want to do it without using regex.

const kebabize = str => {

    let subs = []
    let char = ''
    let j = 0

    for( let i = 0; i < str.length; i++ ) {

        char = str[i]

        if(str[i] === char.toUpperCase()) {
            subs.push(str.slice(j, i))
            j = i
        }

        if(i == str.length - 1) {
            subs.push(str.slice(j, str.length))
        }
    }

    return subs.map(el => (el.charAt(0).toLowerCase() + el.substr(1, el.length))).join('-')
}

kebabize('myNameIsStack')
Zoe
  • 27,060
  • 21
  • 118
  • 148
Stack
  • 429
  • 1
  • 4
  • 12
  • 1
    @YevgenGorbunkov Just improving my logical skills. That's why not using regex. I know this approach is cleaner and optimized. – Stack Jul 27 '20 at 13:34
  • 1
    Instead of optimizing your code, I really would advice you to use a library for this kind of util functions. For instance, use the [_.kebabCase](https://lodash.com/docs/4.17.15#kebabCase) function from Lodash! – Jacob van Lingen Jul 27 '20 at 13:15

7 Answers7

44

I have a one-liner similar to Marc's but with a simpler Regular Expression and ~20% faster according my benchmark (Chrome 89).

const kebabize = (str) => str.replace(/[A-Z]+(?![a-z])|[A-Z]/g, ($, ofs) => (ofs ? "-" : "") + $.toLowerCase())

const words = ['StackOverflow', 'camelCase', 'alllowercase', 'ALLCAPITALLETTERS', 'CustomXMLParser', 'APIFinder', 'JSONResponseData', 'Person20Address', 'UserAPI20Endpoint'];

console.log(words.map(kebabize));

[A-Z]+(?![a-z]) matches any consecutive capital letters, excluding any capitals followed by a lowercase (signifying the next word). Adding |[A-Z] then includes any single capital letters. It must be after the consecutive capital expression, otherwise the expression will match all capital letters individually and never match consecutives.

String.prototype.replace can take a replacer function. Here, it returns the lowercased matched capital(s) for each word, after prefixing a hyphen when the match offset is truthy (not zero - not the first character of the string).

I suspect Marc's solution is less performant than mine because by using replace to insert hyphens and lowercasing the whole string afterwards, it must iterate over the string more than once, and its expression also has more complex look aheads/behind constructs.

Benchmark

ABabin
  • 2,724
  • 24
  • 30
30

const kebabize = str => {
   return str.split('').map((letter, idx) => {
     return letter.toUpperCase() === letter
      ? `${idx !== 0 ? '-' : ''}${letter.toLowerCase()}`
      : letter;
   }).join('');
}

console.log(kebabize('myNameIsStack'));
console.log(kebabize('MyNameIsStack'));

You can just check every letter is if upperCase or not and replace it.

Nicolae Maties
  • 2,476
  • 1
  • 16
  • 26
14

RegEx is faster!

Unlike what you might think, the RegEx way of doing this is actually significantly faster! See benchmark.

The function below supports converting both camelCase and PascalCase into kebab-case:

function toKebabCase(str) {
    return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
}
Arad Alvand
  • 8,607
  • 10
  • 51
  • 71
  • 5
    This fails with fully capitalized words. `JSONResponseData` would return `jsonresponse-data` instead of `json-response-data`. I suppose it could be a stylistic choice, but it's not what I think most people would expect and it will cause issues if they're not aware. – ABabin Feb 02 '22 at 20:05
  • @ABabin, but wouldn't be correct `jsonRespondeData` instead of `JSONResponseData`? – NNL993 Aug 30 '22 at 06:12
  • 1
    @NNL993 There is no "correct" standard. Some people capitalize acronyms in `camelCase` - sometimes up to a certain length or selectively or all the time. The other solutions here handle those cases without significantly more overhead. – ABabin Aug 31 '22 at 03:45
8

Here is my solution:

Works with camelCase and PascalCase:

let words = ['StackOverflow', 'camelCase', 'alllowercase', 'ALLCAPITALLETTERS', 'CustomXMLParser', 'APIFinder', 'JSONResponseData', 'Person20Address', 'UserAPI20Endpoint'];

let result = words.map(w => w.replace(/((?<=[a-z\d])[A-Z]|(?<=[A-Z\d])[A-Z](?=[a-z]))/g, '-$1').toLowerCase());

console.log(result);

/*

Returns:
[
  "stack-overflow",
  "camel-case",
  "alllowercase",
  "allcapitalletters",
  "custom-xml-parser",
  "api-finder",
  "json-response-data",
  "person20-address",
  "user-api20-endpoint"
]

*/

Explanation:

  1. Match any of the following regular expressions:
  • Find any capital letter, that is immediately preceeded by a small letter or a number, or
  • Find any capital letter, that is immediately preceeded by a capital letter or a number, that is immediately followed by a small letter
  1. Replace the captured position with a dash ('-') followed by the captured capital letter
  2. Finally, convert the whole string to lowercase.
3

I would use something like this.

function kebabize(string) {
  // uppercase after a non-uppercase or uppercase before non-uppercase
  const upper = /(?<!\p{Uppercase_Letter})\p{Uppercase_Letter}|\p{Uppercase_Letter}(?!\p{Uppercase_Letter})/gu;
  return string.replace(upper, "-$&").replace(/^-/, "").toLowerCase();
}


const strings = ["myNameIsStack", "HTTPRequestData", "DataX", "Foo6HelloWorld9Bar", "Áb"];
const result  = strings.map(kebabize);

console.log(result);

This snippet replaces all uppercase characters before or after a non-uppercase character with - followed by the uppercase. It then removes the - at the start of the string (if there is any) and downcases the whole string.

3limin4t0r
  • 19,353
  • 2
  • 31
  • 52
  • 1
    be careful about this solution, Safari will fail because, Safari does not support the negative lookbehind yet. – lowzhao Sep 08 '21 at 16:17
0

Simple solution for older browsers:

var str = 'someExampleString'
var i

function camelToKebab() {
  var __str = arguments[0]
  var __result = ''

  for (i = 0; i < __str.length; i++) {
    var x = __str[i]

    if(x === x.toUpperCase()) {
      __result += '-' + x.toLowerCase()
    } else {
      __result += x
    }
  }

  return __result
}

console.log(str, '->', camelToKebab(str))
NNL993
  • 372
  • 4
  • 11
0

Here is the solution I came up with:

let resultDiv = document.querySelector(".result");

let camelCase = "thisIsCamelCase";
let kebabCase;
kebabCase = camelCase.split('').map(el=> {
  const charCode = el.charCodeAt(0);
  if(charCode>=65 && charCode<=90){
    return "-" + el.toLowerCase() 
  }else{
    return el;
  }
})

return(kebabCase.join(''))
  • How about using the index provided by the map() and removing charCode? ``` kebabCase = camelCase.split('').map((el,index)=> { if(camelCase.charCodeAt(index)>=65 && camelCase.charCodeAt(index)<=90){ return "-" + el.toLowerCase() }else{ return el; } }) ``` – Rohan Kumar Thakur Sep 03 '22 at 08:10