6
\$\begingroup\$

The basic idea is that by defining three functions--recognize, dissect, and combine--conversion between any case type is possible.

  • recognize is meant to recognize a string of a given case type
  • dissect is meant to break down a string into an array of lower case substrings according to the given case type
  • combine is meant to take those substrings and join them back up into a string of the given case type

For example, let's say someone wanted to add a new case type say, hyphenated case "my-name-is-jonas". With my code, all they would have to do is define the three aforementioned methods using the addType method, and every other case type would be able to be converted to and from hyphenated case.

While this utility is pretty cool, I wonder if the complexity this solution adds to the code is worth it. Also, please let me know of any other thoughts that you might have.

Here is my code, but I also made a jsFiddle

var CaseConverter = (function() {
  // Utility functions
  /**
   *  Can be used by map function to lowercase array of strings
   */
  function toLowerCase(string) {
    return string.toLowerCase();
  }

  // private
  var caseMap = {}; // used to map methods for a case conversion
  var listOfCases = []; // a list of the added case types

  /**
   *  Creates a function to convert a cased to string to the given type.
   *  @param {string} methodName the name of the conversion method to add
   *  @param {function} recognize Takes a string and returns true
   *    if the string is of the given type.
   *  @param {function} dissect Takes a string in the given case and
   *    returns an array of lowercase substrings of the case blocks.
   *    For example: [my, name, is, jonas] for "myNameIsJonas"
   *                                           "MyNameIsJonas"
   *                                           "my_name_is_jonas"
   *  @param {function} combine Takes an array of case blocks and
   *    returns them combined into a string of the desired case.
   *    substrings. 
   */
  function addType(methodName, recognize, dissect, combine) {
    if (caseMap[methodName] !== undefined) {
      throw "A method " + methodName + " already exists";
    } else {
      caseMap[methodName] = {
        methodName: methodName,
        recognize: recognize,
        dissect: dissect,
        combine: combine
      };
      listOfCases.push(methodName);
    }
  }

  // add all the types of cases
  addType("toCamelCase", function(string) {
    return /^([a-z]+)([A-Z][a-z]*)*$/.test(string);
  }, function(string) {
    return string.split(/(?=[A-Z])/).map(toLowerCase);
  }, function(array) {
    return array.reduce(function(combined, caseBlock) {
      return combined + caseBlock.charAt(caseBlock).toUpperCase() + caseBlock.substring(1);
    });
  });

  addType("toPascalCase", function(string) {
    return /^([A-Z][a-z]*)+$/.test(string);
  }, function(string) {
    return string.split(/(?=[A-Z])/).map(toLowerCase);
  }, function(array) {
    return array.reduce(function(combined, caseBlock) {
      return combined + caseBlock.charAt(caseBlock).toUpperCase() + caseBlock.substring(1);
    }, "");
  });

  addType("toUnderscoreCase", function(string) {
    return /^[a-z]+(_[a-z]+)*$/.test(string);
  }, function(string) {
    return string.toLowerCase().split("_");
  }, function(array) {
    return array.join("_");
  });

  // someone could add hyphenated case here


  // build the module
  function createConverterFunction(typeTo) {
    return function(string) {
      if (typeof string !== "string") throw string + " is not a string";
      var typeFrom = listOfCases.find(function(type) {
        return caseMap[type].recognize(string);
      });

      if (typeFrom === undefined) throw string + " is not a recognized case type"

      if (typeFrom === typeTo) {
        return string;
      } else {
        var to = caseMap[typeTo];
        var from = caseMap[typeFrom];
        return to.combine(from.dissect(string));
      }
    };
  }

  var CaseConverter = {};

  listOfCases.forEach(function(methodName) {
    CaseConverter[methodName] = createConverterFunction(methodName);
  })

  return CaseConverter
})();

function test(method, string, expected) {
  if (CaseConverter[method](string) !== expected)
    throw "Failded for: " + method + ", string=" + string + ", expected=" + expected + ", result=" + CaseConverter[method](string);
}

test("toCamelCase", "myNameIsJonas", "myNameIsJonas");
test("toCamelCase", "MyNameIsJonas", "myNameIsJonas");
test("toCamelCase", "my_name_is_jonas", "myNameIsJonas");

test("toPascalCase", "myNameIsJonas", "MyNameIsJonas");
test("toPascalCase", "MyNameIsJonas", "MyNameIsJonas");
test("toPascalCase", "my_name_is_jonas", "MyNameIsJonas");

test("toUnderscoreCase", "myNameIsJonas", "my_name_is_jonas");
test("toUnderscoreCase", "MyNameIsJonas", "my_name_is_jonas");
test("toUnderscoreCase", "my_name_is_jonas", "my_name_is_jonas");

test("toCamelCase", "a", "a");
test("toPascalCase", "a", "A");
test("toUnderscoreCase", "a", "a");
console.log("all tests passed");

This question was inspired by this one

\$\endgroup\$
1
\$\begingroup\$
addType("toCamelCase", function(string) {
  return /^([a-z]+)([A-Z][a-z]*)*$/.test(string);
}, function(string) {
  return string.split(/(?=[A-Z])/).map(toLowerCase);
}, function(array) {
  return array.reduce(function(combined, caseBlock) {
    return combined + caseBlock.charAt(caseBlock).toUpperCase() + caseBlock.substring(1);
  });
});

This is neat, pluggable, but looks bulky. Consider using ES6. Arrow functions can give you breathing space while template strings allow you to construct strings without breaking up strings and using + to concatenate

Another is that you use a list of arguments. It's not very clear which function is which. It would probably be better if you passed in an object with config. This way, they're named and order doesn't matter:

addType({
  name: 'toCamelCase',
  recognizer: string => {...},
  dissector: string => {...},
  combiner: string => {...}
});

Also, should you want to omit some of the settings, you can set defaults which you can easily merge with Object.assign. In this case, we do a noop by returning the string as is:

const defaults = {
  name: 'toCamelCase',
  recognizer: string => string,
  dissector: string => string,
  combiner: string => string,
}

let settings = Object.assign({}, defaults, settings);

Also consider exposing addType to your namespace so that it can be utilized by other code. This way, you can plug in on the fly other converters, not just the ones you put in.

caseMap[methodName] = {
  methodName: methodName,
  recognize: recognize,
  dissect: dissect,
  combine: combine
};
listOfCases.push(methodName);

listOfCases appears to be just a list of methods that exist on caseMap. The problem with this is that there's this "two things representing one thing" issue. Forgetting to sync them would cause confusion in the code. To avoid this, you can simply use Object.keys(caseMap) to get an array of caseMap's keys, the method names.

var typeFrom = listOfCases.find(function(type) {
  return caseMap[type].recognize(string);
});

if (typeFrom === undefined) throw string + " is not a recognized case type"

if (typeFrom === typeTo) {
  return string;
} else {
  var to = caseMap[typeTo];
  var from = caseMap[typeFrom];
  return to.combine(from.dissect(string));
}

Instead of array.find, consider using array.filter. One advantage is that you are working with arrays through and through. The next set of operations is always assured that it gets an array even if it doesn't get the opportunity to call its callback (i.e.: array.filter won't find anything, array.slice and array.map will both get and return an empty array). After that, you can then check your results.

var string = listOfCases.filter(type => caseMap[type].recognize(string))
                        .slice(0, 1)
                        .map(string => {
                          const isSame = typeFrom === typeTo;
                          const to     = caseMap[typeTo];
                          const from   = caseMap[typeFrom];
                          return isSame ? string : to.combine(from.dissect(string))
                        })

// No result? No parser probably handled it.
if(!result[0]) throw new Error(`${string} is not a recognized case type`);

return result[0] 
\$\endgroup\$
  • \$\begingroup\$ Thanks! I really appreciate it. I made several of your advised changes. For those interested in how it turned out, here is a JSFiddle \$\endgroup\$ – Joshua Dawson Apr 21 '16 at 18:55

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.