3

I have a query syntax which needs to be applied to a json object and return an array of valid paths in the json object.

For example, with a query as such:

People.[].Dependents.[]

And the following JSON object:

{
    "People": [
        {
            "FirstName": "John",
            "LastName": "Doe",
            "Dependents": [
                {
                    "Name": "John First Dep"
                },
                {
                    "Name": "John Second Dep"
                }
            ]
        },
        {
            "FirstName": "Jane",
            "LastName": "Smith",
            "Dependents": [
                {
                    "Name": "Jane First Dep"
                }
            ]
        }
    ]
}

The result would be:

[
    "People.0.Dependents.0",
    "People.0.Dependents.1",
    "People.1.Dependents.0",
]

I'm currently trying to do this as succinctly as possible. Any attempt I've made thus far has resulted in far too much code and is incredibly hard to follow. Am I missing something obvious?

Edit: Current Code:

function expandQuery(data, path) {
    const parts = path.split("[]").map(s => _.trim(s, "."));
    const [outer, ...right] = parts;

    const inner = _.join(right, ".[].");

    let groupData = _.get(data, outer, []);
    if (!_.isArray(groupData)) {
        groupData = [groupData];
    }
    const groupLength = groupData.length;

    let items = [];
    for (let ind = 0; ind < groupLength; ind++) {
        items.push(outer + "." + ind.toString() + "." + inner);
    }

    const result = [];

    for (let ind = 0; ind < items.length; ind++) {
        const item = items[ind];
        if (item.includes("[]")) {
            result.push(...expandQuery(data, item));
        } else {
            result.push(_.trim(item, "."));
        }
    }
    return result;
}

I'm looking specifically to make this shorter.

2
  • 2
    What was your attempt so far? Commented Nov 9, 2018 at 14:48
  • @lumio: Added a working sample Commented Nov 9, 2018 at 15:23

4 Answers 4

2

This does what you want but is not much simpler/shorter than your solution.

function getPaths (collection, query) {
    let tokens = query.split(".")

    function walkPath (collection, [currentToken, ...restTokens], paths) {
        if (!currentToken) { // no more tokens to follow
            return paths.join(".")
        }
        if (currentToken === "[]") { // iterate array
            const elemPaths = _.range(collection.length)
            return elemPaths.map(elemPath => walkPath(collection[elemPath], restTokens, [...paths, elemPath]))
        }
        else {
            return walkPath(collection[currentToken], restTokens, [...paths, currentToken])
        }
    }

    return _.flattenDeep(walkPath(collection, tokens, []))
}

It also lacks error handling.
Maybe this is of some use for you.

Sign up to request clarification or add additional context in comments.

4 Comments

Oh ... beside the _.range I love this solution! :)
This is an order of magnitude faster than the solution I provided, and is also slightly faster than the other answers. (also the shortest!)
@lumio: why don't you like the _.range ? It seems really sensible to me (as someone coming from a C#-with-linq background)
@caesay because it generates an array and uses .map over an empty array. It is better to just write a little for loop here (performance-wise)
0

To flatten a deeply nested array you can use _.flattenDeep. However, if you don't mind leaving lodash, you could do something like the following:

function getNextToken(path) {
  let token = path.trim();
  let rest = '';

  const separatorPos = path.indexOf('.');
  if (separatorPos > -1) {
    token = path.substr(0, separatorPos).trim();
    rest = path.substr(separatorPos + 1).trim();
  }

  return { token, rest };
}


const expandQuery = (data, path) => {
  const expand = (data, path, found = []) => {
    if (data === undefined) {
      return [];
    }

    const { token, rest } = getNextToken(path);
    switch (token) {
      // Got to the end of path
      case '':
        return [found.join('.')];

      // Got an array
      case '[]':
        if (data.constructor !== Array) {
          return [];
        }

        const foundPaths = [];
        let i;
        for (i = 0; i < data.length; ++i) {
          // Flatten out foundPaths
          foundPaths.push.apply(
            foundPaths,
            expand(data[i], rest, [...found, i])
          );
        }
        return foundPaths;

      // Got a property name
      default:
        return expand(data[token], rest, [...found, token]);
    }
  };
  
  return expand(data, path);
};

const query = 'People.[].Dependents.[]';
const input = {
  "People": [{
      "FirstName": "John",
      "LastName": "Doe",
      "Dependents": [{
          "Name": "John First Dep"
        },
        {
          "Name": "John Second Dep"
        }
      ]
    },
    {
      "FirstName": "Jane",
      "LastName": "Smith",
      "Dependents": [{
        "Name": "Jane First Dep"
      }]
    }
  ]
};
console.log(expandQuery(input, query));

Probably not the shortest, but it checks for data types. Also null values are considered to be a match. If you want to ignore them as well, you could check if data === null.

Comments

0

Another take :)

var _ = require('lodash');
var test = {
    "People": [
        {
            "FirstName": "John",
            "LastName": "Doe",
            "Dependents": [
                {
                    "Name": "John First Dep"
                },
                {
                    "Name": "John Second Dep"
                }
            ]
        },
        {
            "FirstName": "Jane",
            "LastName": "Smith",
            "Dependents": [
                {
                    "Name": "Jane First Dep"
                }
            ]
        }
    ]
}

function mapper(thing, prefix, paths) {
    if (_.isObject(thing)) {
        _.forEach(thing, function(value, key) {mapper(value, prefix+key+'.', paths);});
    } else if (_.isArray(thing)) {
        for (var i = 0; i < thing.length; i++) mapper(value, prefix+i+'.', paths);
    } else {
        paths.push(prefix.replace(/\.$/, ''));
    }
}

var query = 'People.[].Dependents.[]';
var paths = [];
var results;

query = new RegExp(query.replace(/\[\]/g,'\\d'));
mapper(test, '', paths); // Collect all paths
results = _.filter(paths, function(v) {return query.test(v);}); // Apply query
console.log(results);

Comments

0

I'll give it a shot too, note that when you use _.get(object,'path.with.point') and your object key names have dots in it your code will break, I prefer to use _.get(object,['path','with','point']) instead.

const data = {"People":[{"FirstName":"John","LastName":"Doe","Dependents":[{"Name":"John First Dep","a":[1,2]},{"Name":"John Second Dep","a":[3]}]},{"FirstName":"Jane","LastName":"Smith","Dependents":[{"Name":"Jane First Dep","a":[1]}]}]};

const flatten = arr =>
  arr.reduce((result, item) => result.concat(item))
const ARRAY = {}
const getPath = (obj, path) => {
  const recur = (result, path, item, index) => {
    if (path.length === index) {
      return result.concat(path)
    }
    if (item === undefined) {
      throw new Error('Wrong path')
    }
    if (path[index] === ARRAY) {
      const start = path.slice(0, index)
      const end = path.slice(index + 1, path.length)
      return item.map((_, i) =>
        recur(result, start.concat(i).concat(end), item, index)
      )
    }
    return recur(result, path, item[path[index]], index + 1)
  }
  const result = recur([], path, obj, 0)
  const levels = path.filter(item => item === ARRAY).length - 1
  return levels > 0
    ? [...new Array(levels)].reduce(flatten, result)
    : result
}

console.log(
  getPath(data, ['People', ARRAY, 'Dependents', ARRAY, 'a', ARRAY])
)

Comments

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.