1

I need to process files that are structured like

Title
/foo/bar/foo/bar 1
/foo/bar/bar 2
/bar/foo 3
/bar/foo/bar 4

It's easy enough to parse this into an array of arrays, by splitting at every / and \n. However, once I get an array of arrays, I can't figure out a good way to turn that into nested objects. Desired format:

{
  Title,
  {
    foo: {
      bar: {
        foo: {
          bar: 1
        },
        bar: 2
      }
    },
    bar: {
      foo: {
        3,
        bar: 4
      }
    }
  }
}

This seems like a super common thing to do, so I'm totally stumped as to why I can't find any pre-existing solutions. I sort of expected javascript to even have a native function for this, but merging objects apparently overrides values instead of making a nested object, by default. I've tried the following, making use of jQuery.extend(), but it doesn't actually combine like-terms (i.e. parents and grand-parents).

let array = fileContents.split('\n');
let object = array.map(function(string) {
  const result = string.split('/').reduceRight((all, item) => ({
    [item]: all
  }), {});
  return result;
});

output = $.extend(true, object);
console.log(JSON.stringify(output));

This turned the array of arrays into nested objects, but didn't merge like-terms... I could brute-force this, but my real-world problem has over 2000 lines, goes 5 layers deep (/foo/foo/foo/foo value value value), and actually has an array of space-separated values rather than a single value per line. I'm willing to treat the array of values like a string and just pretend its not an array, but it would be really nice to at least get the objects nested properly without writing a hefty/primitive algorithm.

Since this is essentially how subdirectories are organized, it seems like this should be an easy one. Is there a relatively simple solution I'm not seeing?

1 Answers1

1

You could reduce the array of keys/value and set the value by using all keys.

If no key is supplied, it take the keys instead.

const
    setValue = (target, keys, value) => {
        const last = keys.pop();
        keys.reduce((o, k) => o[k] ??= {}, target)[last] = value;
        return target;
    },
    data = 'Title\n/foo/bar/foo/bar 1\n/foo/bar/bar 2\n/bar/foo 3\n/bar/foo/bar 4',
    result = data
        .split(/\n/)
        .reduce((r, line) => {
            const [keys, value] = line.split(' ');
            return setValue(r, keys.split('/').filter(Boolean), value || keys);
        }, {});

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • `bar: 4` is missing; apart from that, I for one am shocked and awed by your code, kudos – GrafiCode Feb 16 '22 at 18:09
  • 1
    right, foo has already a value and is not an object. the op's wanted resutl is not possible. – Nina Scholz Feb 16 '22 at 18:20
  • This is great! I'm not sure if the missing bar:4 case will actually be an issue in my real-world application. I think there's a good chance this will do what I need. Tysm. However, when I tried implementing this, the code failed to compile, due to this line here: keys.reduce((o, k) => o[k] ?? = {}, target)[last] = value; My linter is showing these errors: E030 - Expected an identifier and instead saw '?'. E033 - Expected an operator and instead saw '='. E021 - Expected ':' and instead saw '{'. E030 - Expected an identifier and instead saw '}'. – Jeffrey Hall Feb 16 '22 at 18:47
  • 2
    to overcome this, you could replace `o[k] ??= {}` with `o[k] = o[k] || {}`. – Nina Scholz Feb 16 '22 at 19:04
  • You're a god-send. :) Thanks so much. I'll try this after work today. – Jeffrey Hall Feb 16 '22 at 19:06