5
\$\begingroup\$

I was attempting to do some contribution to the underscore-contrib library, and I ended up writing a function that will parse a JavaScript path to later on expose through an API if the path exists or to get the value.

However, a reasonable concern around it is the complexity. (Discussion in the repository, if you're interested.)

I did review other approaches to the problem, but most of them seem to leave valid complex property names behind, or some even are tied to actually retrieving the value at that point.

Can this be made simpler?

// Will take a path like 'element[0][1].subElement["Hey!.What?"]["[hey]"]'
// and return ["element", "0", "1", "subElement", "Hey!.What?", "[hey]"]
function parseJavaScriptPathIntoKeyNames(javascriptPath) {
  var parts = [];
  var terminatorExpected = null;
  var insideIndexer = false;
  var currentPart = "";

  function flushCurrentPart() {
    if (currentPart.length > 0) {
      parts.push(currentPart);
      currentPart = "";
    }
  }

  for (var i = 0; i < javascriptPath.length; i++) {
    var currentChar = javascriptPath[i];
    switch (currentChar) {
    case "[":
      if (!terminatorExpected) {
        flushCurrentPart();

        terminatorExpected = ']';
        insideIndexer = true;
      } else {
        currentPart += currentChar;
      }
      break;
    case "]":
      if (terminatorExpected === "]") {
        flushCurrentPart();

        terminatorExpected = null;
        insideIndexer = false;
      } else {
        currentPart += currentChar;
      }
      break;
    case ".":
      if (!terminatorExpected) {
        flushCurrentPart();
      } else {
        currentPart += currentChar;
      }
      break;
    case "\'":
      if (!terminatorExpected || terminatorExpected === "]") {
        terminatorExpected = "\'";
      } else if (terminatorExpected === "\'" && insideIndexer) {
        terminatorExpected = ']';
      } else if (terminatorExpected === "\'" && !insideIndexer) {
        flushCurrentPart();

        terminatorExpected = null;
      } else {
        currentPart += currentChar;
      }
      break;
    case "\"":
      if (!terminatorExpected || terminatorExpected === "]") {
        terminatorExpected = "\"";
      } else if (terminatorExpected === "\"" && insideIndexer) {
        terminatorExpected = ']';
      } else if (terminatorExpected === "\"" && !insideIndexer) {
        flushCurrentPart();

        terminatorExpected = null;
      } else {
        currentPart += currentChar;
      }
      break;
    default:
      currentPart += currentChar;
    } // switch (currentChar)
  } // for

  flushCurrentPart();

  return parts;
}
\$\endgroup\$
2
  • 1
    \$\begingroup\$ According to the comment, you're going to throw away nearly all of the information about the placement of brackets? \$\endgroup\$ Sep 16, 2014 at 3:15
  • \$\begingroup\$ @200_success Exactly: I don't need them, I just need the property names in order to access them later. \$\endgroup\$
    – Alpha
    Sep 16, 2014 at 3:50

1 Answer 1

5
\$\begingroup\$

It would be a good idea to reimplement the whole thing using a regular expression. Some uninitiated programmers despise or fear regular expressions, but in this case, avoiding them just means that you are giving up a language feature that is designed to solve just this kind of problem, forcing you to reinvent the wheel.

function parseJavaScriptPathIntoKeyNames(path) {
    /**
     * Repeatedly capture either:
     * - a bracketed expression, discarding optional matching quotes inside, or
     * - an unbracketed expression, delimited by a dot or a bracket.
     */
    var re = /\[("|'|)(.*?)\1\]|([^.\[\]]+)/g;

    var elements = [];
    var result;
    while ((result = re.exec(path)) !== null) {
        elements.push(result[2] || result[3]);
    }
    return elements;
}

I can't say for sure that this version behaves exactly the same as the original, especially in the face of illegal input. However, the reduction in code makes it a much more worthwhile approach, and you can work out the details.

\$\endgroup\$
4
  • \$\begingroup\$ The problem with regexes is that for actual valid input, they will become terribly complex. See the examples at the top of the code: "[hey]" is a valid property name, "['Hey']" is too. They don't even have to be balanced: "[Hey'". Coming up with a regex that solved for these cases would be terribly complex, wouldn't it? \$\endgroup\$
    – Alpha
    Sep 16, 2014 at 12:26
  • \$\begingroup\$ PS: Now that I've seen you used back-references, I may re-visit the approach with regexes. Perhaps it may not be as complex as I originally thought. Thanks! \$\endgroup\$
    – Alpha
    Sep 16, 2014 at 12:27
  • \$\begingroup\$ JavaScript (and other languages) call them "regular expressions", but they aren't actually limited to "regular" grammars in the computer-science sense of the word. \$\endgroup\$
    – user50399
    Sep 16, 2014 at 19:42
  • \$\begingroup\$ Heads up: I ended up trying to replicate your approach and came up with the same expression. Thanks a lot! \$\endgroup\$
    – Alpha
    Sep 21, 2014 at 15:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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