Home > Software design >  How can I use an array of indexes to nagivate an array of objects
How can I use an array of indexes to nagivate an array of objects

Time:05-30

I'm making a checklist app and need to navigate through my data to edit the complete:false

My data is structured with children being an optional key, and objects with children won't have the completed key.

There are two ways I could solve this

  1. use each part of the path and navigate the array using items[path[0]] and so on
  2. Or use path as (or replace it with) a unique id and search for that id

both of which I do not know how to do

const TESTDATA = [
  {
    name: 'foo',
    path: [0],
    open: true,
    children: [
      { name: 'bar', completed: false, path: [0, 0] },
      { name: 'data', completed: false, path: [0, 1] },
    ],
  },
  {
    name: 'foo',
    path: [1],
    open: true,
    children: [
      { name: 'bar', completed: false, path: [1, 0] },
      {
        name: 'data',
        completed: false,
        path: [1, 1],
        children: [{ name: 'baz', completed: false, path: [1, 1, 0] }],
      },
    ],
  },
]

Is there a way to recursively (or not) search for a specific path/id in all the children keys and return the item so it could be modified?

CodePudding user response:

1. Follow path

This is the more direct way, as we can literally go and find the wanted element. As such, this is likely more performant.

Here is one way to do it:

  1. Navigate through all necessary .childrens.
  2. Take the element specified by the last index in path.

1. Navigate .childrens:

function findChild(path, data) {
  let array = data;
  for (let i = 0; i < path.length - 1;   i) {
    const pathIndex = path[i];
    array = array[pathIndex].children;
  }
  // ...
}

2. Take element by last index:

function findChild(path, data) {
  let array = data;
  for (let i = 0; i < path.length - 1;   i) {
    const pathIndex = path[i];
    array = array[pathIndex].children;
  }
  return array[path[path.length - 1]];
}

We actually haven't yet taken edge-cases into account, namely:

  1. What if data (or any array here) is an empty array?
  2. What if elements along the given path don't have .children?

Here's a fix to that:

function findChild(path, data) {
  let array = data;
  for (let i = 0; i < path.length - 1 && array;   i) {
    const pathIndex = path[i];
    array = array[pathIndex]?.children;
  }
  return array?.[path[path.length - 1]];
}

function findChild(path, data) {
  let array = data;
  for (let i = 0; i < path.length - 1 && array;   i) {
    const pathIndex = path[i];
    array = array[pathIndex]?.children;
  }
  return array?.[path[path.length - 1]];
}

const data = [
  {
    name: 'foo',
    path: [0],
    open: true,
    children: [
      { name: 'bar', completed: false, path: [0, 0] },
      { name: 'data', completed: false, path: [0, 1] },
    ],
  },
  {
    name: 'foo',
    path: [1],
    open: true,
    children: [
      { name: 'bar', completed: false, path: [1, 0] },
      {
        name: 'data',
        completed: false,
        path: [1, 1],
        children: [{ name: 'baz', completed: false, path: [1, 1, 0] }],
      },
    ],
  },
];

console.time();
const result = findChild([1, 0], data);
console.timeEnd();
console.log("Imperative:", result);

2. Find by path (as ID)

Ideally we have all elements in a single, flat array, because then we can simply do a Array.find() search.

Because we initially get the elements in a tree-like structure, we have to flatten them manually. Here is one way to do that:

const _flatten = (el) => ([el, ...(el.children ?? []).flatMap(_flatten)]);
const flatten = (array) => array.flatMap(_flatten);

Now that we have a flat array of all the elements, we can use Array.find():

function findChild(path, treeElements) {
  // When equal, serializes to the same string
  const isEqual = (p1, p2) => p1.join() === p2.join();

  const _flatten = (el) => ([el, ...(el.children ?? []).flatMap(_flatten)]);
  const flatten = (array) => array.flatMap(_flatten);

  return flatten(treeElements).find(el => isEqual(el.path, path));
}

function findChild(path, treeElements) {
  // When equal, serializes to the same string
  const isEqual = (p1, p2) => p1.join() === p2.join();

  const _flatten = (el) => ([el, ...(el.children ?? []).flatMap(_flatten)]);
  const flatten = (array) => array.flatMap(_flatten);

  return flatten(treeElements).find(el => isEqual(el.path, path));
}

const data = [
  {
    name: 'foo',
    path: [0],
    open: true,
    children: [
      { name: 'bar', completed: false, path: [0, 0] },
      { name: 'data', completed: false, path: [0, 1] },
    ],
  },
  {
    name: 'foo',
    path: [1],
    open: true,
    children: [
      { name: 'bar', completed: false, path: [1, 0] },
      {
        name: 'data',
        completed: false,
        path: [1, 1],
        children: [{ name: 'baz', completed: false, path: [1, 1, 0] }],
      },
    ],
  },
];

console.time();
const result = findChild([1, 0], data);
console.timeEnd();
console.log("Functional:", result);

Comparison between 1. and 2.

As per JSBen.ch, the first option compared to the second option is about 4-5 times faster, as expected.

CodePudding user response:

maybe something like this can get you started:

function gather(name, obj){
 if(name==obj.name) return obj;
 if(obj.children) return obj.children.map(x=>gather(name, x)).flat().filter(Boolean)
}

TESTDATA.map(x=>gather("baz", x)).flat()[0]
  • Related