Home > Software design >  Convert a list object into another structure
Convert a list object into another structure

Time:06-10

I have a list, converted into js array. Several rows has a Tab prefixes:

var data = [
    "2",
    "    2.1",
    "        2.1.1",
    "    2.2",
    "3",
    "4"
]

What I'm trying to do, is to get following structure:

var data = [
        "2",
        "2->2.1",
        "2->2.1->2.1.1",
        "2->2.2",
        "3",
        "4"
    ]

Tried (Produce wrong result):

for (var i = 0; i < data.length; i  ) {
            
            var current = data; 
            var length  = data[i].length - data[i].replaceAll("    ", "").length;
            
            if (!length) {
                console.log(current); 
            } else {
                console.log(data[i-1]   '->'   data[i].trim()); 
            }
}

Update (@MustSeeMelons) - your solution produce wrong results on test data attached below:

enter image description here

CodePudding user response:

This will almost do what you wish, not sure how you want to group them:

const mapped = data.reduce((acc, curr, idx) => {
  if (idx !== 0) {
    // Get leading digit of current & previous element
    const current = curr.trim()[0];
    const previous = acc[idx - 1].trim()[0];

    // If leading match, aggregate them
    if (current === previous) {
      acc.push(`${acc[idx - 1].trim()}->${curr.trim()}`);
    } else {
      acc.push(curr.trim());
    }
  } else {
    acc.push(curr.trim());
  }

  return acc;
}, []);

Don't use for loops unless you need to break out of the loop at some point. Transforming arrays usually should be done with the map function. I used reduce because this problem required me to access the new, already mapped element.

CodePudding user response:

Considering that a tab is composed of four spaces and the data has either zero or more complete tabs, then it can be solved by a combination of reduceRight() and map().

The idea is to split each string of the data by the tabs, i.e.,

<tab><tab>2.1.1

Into an array of length nº tabs 1

['', '', 2.1.1] 
  0   1    2

So that it can be built a structure that "replaces" each empty entry from the array by the level immediately above the last number from right to left.

[ '',    '', '2.1.1']
[ '', '2.1', '2.1.1']  
['2', '2.1', '2.1.1'] 
   "2->2.1->2.1.1"

As can be seen in the example:

let data = ["2","    2.1","        2.1.1","    2.2","3","4"]
let tab = " ".repeat(4)

let output = data.map(x => x.includes(tab) ?
    x.split(tab)
        .reduceRight((acc, curr, idx) =>
            (
                curr === "" ?
                    acc.split('.', idx   1).join('.') :
                    curr
            )   '->'   acc
        )
    : x)

console.log(output)

CodePudding user response:

flat to tree

I solved this problem in this Q&A. We can reuse the same functions on your data -

const data = `
2
    2.1
        2.1.1
    2.2
3
4
`
// using makeChildren and sanitize from the linked Q&A
console.log(makeChildren(sanitize(data)))
[
  {
    "value": "2",
    "children": [
      {
        "value": "2.1",
        "children": [
          {
            "value": "2.1.1",
            "children": []
          }
        ]
      },
      {
        "value": "2.2",
        "children": []
      }
    ]
  },
  {
    "value": "3",
    "children": []
  },
  {
    "value": "4",
    "children": []
  }
]

tree to flat

All that remains now is to convert the tree to flat list of paths -

function* paths(t) {
  switch (t?.constructor) {
    case Array:
      for (const child of t)
        yield* paths(child)
      break
    case Object:
      yield [t.value]
      for (const path of paths(t.children))
        yield [t.value, ...path]
      break
  }
}
const result =
  Array.from(paths(makeChildren(sanitize(data))), path => path.join("->"))
[
  "2",
  "2->2.1",
  "2->2.1->2.1.1",
  "2->2.2",
  "3",
  "4"
]

advantages

Decomposing the problem into smaller parts makes it easier to solve and yields reusable functions but those are not the only advantages. The intermediate tree representation gives you the ability to make other modifications in the context of the tree that the flat representation does not permit. Additionally, the paths function yields arrays of paths segments, allowing the caller to decide which final effect is desired, ie path.join("->"), or otherwise.

demo

Run the demo below to verify the result in your own browser -

const sanitize = (str = "") =>
  str.trim().replace(/\n\s*\n/g, "\n")
  
const makeChildren = (str = "") =>
  str === ""
    ? []
    : str.split(/\n(?!\s)/).map(make1)

const make1 = (str = "") => {
  const [ value, children ] = cut(str, "\n")
  return { value, children: makeChildren(outdent(children)) }
}

const cut = (str = "", char = "") => {
  const pos = str.search(char)
  return pos === -1
    ? [ str, "" ]
    : [ str.substr(0, pos), str.substr(pos   1) ]
}

const outdent = (str = "") => {
  const spaces = Math.max(0, str.search(/\S/))
  const re = new RegExp(`(^|\n)\\s{${spaces}}`, "g")
  return str.replace(re, "$1")
}

function* paths(t) {
  switch (t?.constructor) {
    case Array: for (const child of t) yield* paths(child); break
    case Object: yield [t.value];  for (const path of paths(t.children)) yield [t.value, ...path]; break
  }
}

const data = `\n2\n\t2.1\n\t\n\t2.1.1\n\t2.2\n3\n4`

console.log(
  Array.from(paths(makeChildren(sanitize(data))), path => path.join("->"))
)
.as-console-wrapper { min-height: 100%; top: 0; }

remarks

outdent is generic and works whether you use literal tabs, \t \t\t \t\t\t..., or some number of spaces. What matters is the whitespace is consistent. View the original Q&A for more insight on how each part works.

  • Related