0

I wanted to create a program in javascript without any library, that can take an input Depp nested 'object' of any type and convert to a 2d array with headers and back to 'object' from that 2d array.

The idea is to create a function that can convert any nested JSON imported as an obj and convert it into a 2dArray, which can then be inserted in a table [eg sheet], we should be able to build back the original obj after reading the earlier generate 2d array.

Managed to do the first part but I am getting stuck at figuring out the logic for array2Obj. Was also trying to avoid, forEach, filter, map, and reduce for certain reasons. Here is the sample JSON, which I converted to a 2d array. I have added headers, like id, depth, parent,entityName,type of entity which I feel are needed.

var sample = {
    "quiz": {
        "sport": {
            "q1": {
                "question": "Which one is correct team name in NBA?",
                "options": [
                    "New York Bulls",
                    "Los Angeles Kings",
                    "Golden State Warriros",
                    "Huston Rocket"
                ],
                "answer": "Huston Rocket"
            }
        },
        "maths": {
            "q1": {
                "question": "5 + 7 = ?",
                "options": [
                    "10",
                    "11",
                    "12",
                    "13"
                ],
                "answer": "12"
            },
            "q2": {
                "question": "12 - 8 = ?",
                "options": [
                    "1",
                    "2",
                    "3",
                    "4"
                ],
                "answer": "4"
            }
        }
    }
}

       
function arr2json2(input, output, currentRow,currentObj,d,children) {
    if (!output) { var output = {}; table = input; }
  
    maxDepth = Math.max(...splitArray(input, 2));
    

    for (d = 1; d <= maxDepth; d++) {
              for (i = 1; i < input.length; i++) { 
  
            if (input[i][1] === d) { 
                currentRow = input[i];
                 if (input[i][4] === 'Object') {
                    nwObj = {};
                    nwObj[currentRow[3]] = updateAttributesNvalues(input, output, currentRow, nwObj);
                  
                    children = table.filter((row, value) => { if (row[2] === currentRow[3] && row[1] === currentRow[1] + 1) return row;} );
                  //Unable to recurse here
                   // nwObj = { ...arr2json2(children, nwObj, currentRow, nwObj, d,children)}
                    output[currentRow[3]] = { ... nwObj[currentRow[3]]}
                   
                } else if (input[i][4] === 'Array') {
                    nwObj = [];
                 //   console.log("Array Found ", input[i]);
                         } else if (input[i][4] === 'String') {
                 // console.log("String Found ",input[i]);

                }
            }
          
        }
    }
      return output;
}

//This function takes an array as input and extract a column as a return array
function splitArray(input,column) { 
     var output = [];
    for (i = 1; i < input.length; i++) {
        output.push(input[i][1]);
       
    }

  //  console.log(output);
    return output;
}


function updateAttributesNvalues(input, output, currentRow, currentObj) {
    header = input[0];
    rowAttributes = currentRow.slice(2);
    if (rowAttributes.length > 0) {
        row = {};
       rowAttributes.forEach((value) => {
            if (value !== "") {
                key = input[0][currentRow.indexOf(value)];
                row[key] = value;
            }
        });
        return row;
    }
}

This is the intermediate Array.

["id", "d", "parent", "entity", "type", "question", "answer"],
[1, 1, "root", "quiz", "Object"],
[2, 2, "quiz", "sport", "Object"],
[3, 3, "sport", "q1", "Object", "Which one is correct team name in NBA?", "Huston Rocket"],
[4, 4, "q1", "options", "Array"],
[5, 5, "options", "New York Bulls", "String"],
[6, 5, "options", "Los Angeles Kings", "String"],
 [7, 5, "options", "Golden State Warriros", "String"],
[8, 5, "options", "Huston Rocket", "String"],
 [9, 2, "quiz", "maths", "Object"],
 [10, 3, "maths", "q1", "Object", "5 + 7 = ?", "12"],
[11, 4, "q1", "options", "Array"],
[12, 5, "options", "10", "String"],
[13, 5, "options", "11", "String"],
[14, 5, "options", "12", "String"],
[15, 5, "options", "13", "String"],
[16, 3, "maths", "q2", "Object", "12 - 8 = ?", "4"],
[17, 4, "q2", "options", "Array"],
[18, 5, "options", "1", "String"],
[19, 5, "options", "2", "String"],
[20, 5, "options", "3", "String"],
 [21, 5, "options", "4", "String"]]
  • 1
    Welcome to StackOverflow! Please visit the [Help Center](https://stackoverflow.com/help), take the [tour](https://stackoverflow.com/tour) and read up on [asking good questions](https://stackoverflow.com/help/asking) here. It's great that you posted your attempt so far, but you don't show how it's used. You also don't display the intermediate format you're trying to generate. Please [edit] the question to add these additional details. – Scott Sauyet Dec 03 '20 at 13:55
  • @ScottSauyet thank man... thanks for the update. I have updated my question as you asked. – Bronzwik Study Dec 03 '20 at 14:56
  • Two more questions. How do you call your code, using `sample` as input to generate that intermediate value? I see several functions, but all seem to take numerous parameters. Second, is that intermediate format fixed by external requirements or could you alter it if something better was offered? – Scott Sauyet Dec 03 '20 at 15:33
  • And third ;-), `d` and `parent` seem to be redundant information. Am I missing something? – Scott Sauyet Dec 03 '20 at 15:35
  • Since I am still developing it, have written a test function, which takes an Obj input..trigger obj2Array and then calls array to Obj with the previous output. I am looking at an intermediate format, which is 2d and can be easily inserted in a table. 'd' represents depth..I am sure we need it while building the OBj back. – Bronzwik Study Dec 04 '20 at 04:14

3 Answers3

1

We will write a pair of functions fromJS and toJS that convert from JS to your flattened type and back. You can modify the intermediate state however you wish.

We will start by writing fromJS -

function fromJS (v, r = [])
{ switch (v?.constructor)
  { case Object:
      return Object
        .entries(v)
        .flatMap
          ( ([ k, _ ]) =>
              fromJS(_, [ ...r, k ])
          )
    case Array:
      return v
        .flatMap
          ( (_, k) =>
              fromJS(_, [ ...r, k ])
          )
    default:
      return [[ ...r, v ]]
  }
}

for (const v of fromJS(input))
  console.log(v)
['quiz', 'sport', 'q1', 'question', 'Which one is correct team name in NBA?']
['quiz', 'sport', 'q1', 'options', 0, 'New York Bulls']
['quiz', 'sport', 'q1', 'options', 1, 'Los Angeles Kings']
['quiz', 'sport', 'q1', 'options', 2, 'Golden State Warriros']
['quiz', 'sport', 'q1', 'options', 3, 'Huston Rocket']
['quiz', 'sport', 'q1', 'answer', 'Huston Rocket']
['quiz', 'maths', 'q1', 'question', '5 + 7 = ?']
['quiz', 'maths', 'q1', 'options', 0, '10']
['quiz', 'maths', 'q1', 'options', 1, '11']
['quiz', 'maths', 'q1', 'options', 2, '12']
['quiz', 'maths', 'q1', 'options', 3, '13']
['quiz', 'maths', 'q1', 'answer', '12']

Next we move onto toJS -

function toJS (t)
{ return t.reduce
    ( (r, _) => merge(r, toJS1(_))
    , {}
    )
}

function toJS1 ([ v, ...more ])
{ if (more.length)
    return set
      ( Number.isInteger(v) ? [] : {}
      , [ v, toJS1(more) ]
      )
  else
    return v
}

for (const v of fromJS(input))
  console.log(toJS1(v))
{ quiz: { sport: { q1: { answer: "Huston Rocket" } } } }
{ quiz: { maths: { q1: { question: "5 + 7 = ?" } } } }
{ quiz: { maths: { q1: { options: ["10"] } } } }
{ quiz: { maths: { q1: { options: [null,"11"] } } } }
{ quiz: { maths: { q1: { options: [null,null,"12"] } } } }
{ quiz: { maths: { q1: { options: [null,null,null,"13"] } } } }
{ quiz: { maths: { q1: { answer: "12" } } } }
{ quiz: { maths: { q2: { question: "12 - 8 = ?" } } } }
{ quiz: { maths: { q2: { options: ["1"] } } } }
{ quiz: { maths: { q2: { options: [null,"2"] } } } }
{ quiz: { maths: { q2: { options: [null,null,"3"] } } } }
{ quiz: { maths: { q2: { options: [null,null,null,"4"] } } } }
{ quiz: { maths: { q2: { answer: "4" } } } }

Above, our program relies on a generic merge and set functions which were written at an earlier time and can be used here without any modification -

const isObject = t =>
  Object(t) === t

const set = (t, [ k, v ]) =>
  (t[k] = v, t)

const merge = (l = {}, r = {}) =>
  Object
    .entries(r)
    .map
      ( ([ k, _ ]) =>
          isObject(_) && isObject(l[k])
            ? [ k, merge(l[k], _) ]
            : [ k, _ ]
      )
    .reduce(set, l)

Now we can test out a round trip -

const result =
  toJS(fromJS(input))

Expand the snippet below and verify the results in your own browser -

const isObject = t =>
  Object(t) === t

const set = (t, [ k, v ]) =>
  (t[k] = v, t)

const merge = (l = {}, r = {}) =>
  Object
    .entries(r)
    .map
      ( ([ k, _ ]) =>
          isObject(_) && isObject(l[k])
            ? [ k, merge(l[k], _) ]
            : [ k, _ ]
      )
    .reduce(set, l)


function fromJS (v, r = [])
{ switch (v?.constructor)
  { case Object:
      return Object
        .entries(v)
        .flatMap
          ( ([ k, _ ]) =>
              fromJS(_, [ ...r, k ])
          )
    case Array:
      return v
        .flatMap
          ( (_, k) =>
              fromJS(_, [ ...r, k ])
          )
    default:
      return [[ ...r, v ]]
  }
}

function toJS (t)
{ return t.reduce
    ( (r, _) => merge(r, toJS1(_))
    , {}
    )
}

function toJS1 ([ v, ...more ])
{ if (more.length)
    return set
      ( Number.isInteger(v) ? [] : {}
      , [ v, toJS1(more) ]
      )
  else
    return v
}

const input =
  {quiz:{sport:{q1:{question:"Which one is correct team name in NBA?",options:["New York Bulls","Los Angeles Kings","Golden State Warriros","Huston Rocket"],answer:"Huston Rocket"}},maths:{q1:{question:"5 + 7 = ?",options:["10","11","12","13"],answer:"12"},q2:{question:"12 - 8 = ?",options:["1","2","3","4"],answer:"4"}}}}

const result =
  toJS(fromJS(input))

console.log(JSON.stringify(result, null, 2))
{
  quiz: {
    sport: {
      q1: {
        question: "Which one is correct team name in NBA?",
        options: [
          "New York Bulls",
          "Los Angeles Kings",
          "Golden State Warriros",
          "Huston Rocket"
        ],
        answer: "Huston Rocket"
      }
    },
    maths: {
      q1: {
        question: "5 + 7 = ?",
        options: [
          "10",
          "11",
          "12",
          "13"
        ],
        answer: "12"
      },
      q2: {
        question: "12 - 8 = ?",
        options: [
          "1",
          "2",
          "3",
          "4"
        ],
        answer: "4"
      }
    }
  }
}

Related reading -

Mulan
  • 129,518
  • 31
  • 228
  • 259
  • This is a much nicer intermediate format than was in the (edited) question. It's not clear to me whether that format is a hard requirement or only a guideline. As always, this code is cleaner than my own. And also as always, a very nice answer! – Scott Sauyet Dec 03 '20 at 17:36
  • The intermediate format is not a hard requirement, but a logical one from me. I am trying to build a 2d Array that can visual showcase a schema in a 2d Array as well. I am assuming [ Did some trials ], if I could get enough info to build a tree, info 1 as in col 0-col5, I can easily then write a helper function which will generating an visually pleasing view, just like a tree. https://docs.google.com/spreadsheets/d/e/2PACX-1vSpSuRFEi8sU52S4Yj90O4J5AguaaWvY9TZoAEgxhD_fBe1loAifEqRDAI7VMJ1vlck6qhrsTNjGsvA/pubhtml You can check the sheet1/tab which has the current intermediate array and sheet 2 – Bronzwik Study Dec 04 '20 at 04:51
  • @Thank you, I am unable to understand what does r, _,l represent. Along with the logic of merge. – Bronzwik Study Dec 04 '20 at 05:53
  • @BronzwikStudy: For `l`/`r` think `left`/`right`. For the `_`, you might want to look at [this comment](https://stackoverflow.com/q/65096877/#comment115108509_65100441). It's just a variable Thankyou chooses not to name. A question about the intermediate format. Would the structure above do what you want? Because it is a very clean flat description of your object. It shares with yours the slightly unfortunate fact that rows are not all the same length, but that seems unavoidable when you tree has leaves at different levels. – Scott Sauyet Dec 04 '20 at 14:27
  • Ahh..these arrow functions...make things a little difficult for me to understand....am still wrapping my head around understnading what happeing with " const set = (t, [ k, v ]) => (t[k] = v, t)" specially here. (t[k] = v, t) – Bronzwik Study Dec 07 '20 at 07:45
  • function set(inputObj, [key, value]) { inputObj[key] = value, inputObj; return inputObj } is this the correct representation of the set method mentioned by @Thank you – Bronzwik Study Dec 07 '20 at 08:21
  • @BronzwikStudy i apologise i have been a bit too busy to update my post with additional explanation. your representation of `set` is _almost_ correct: `function set (inputObj, [ key, value ]) { inputObj[key] = value; return inputObj }`. this version is 100% identical to the arrow expression of `set` in the answer. – Mulan Dec 07 '20 at 20:05
1

This handles your input format. It ignores the id and parent parameters, choosing to build the hierarchy based upon your d ("depth"?) parameter. With some work, we could choose to use parent instead.

It is not code to love. I really don't like mutating accumulator variables. And I will now go and study the answer from Thankyou to see if it includes a way to do this without such mutation. (While I know of a way for this case, it seems to me that it would lead to nastier code than this.)

const convert = (intermediate) => 
  intermediate .slice (1) .reduce (
    (hierarchy, [_, d, __, entity, type, question, answer]) => {
      const node = type == "Object" 
        ? {...(question ? {question} : {}), ...(answer ? {answer} : {})} 
          : type == "Array" 
        ? [] 
          : entity
      const parentNode = hierarchy [d - 1]
      if (Array .isArray (parentNode)) {
        parentNode .push (node)
      } else {
        parentNode [entity] = node
      }
      hierarchy [d] = node
      return hierarchy .slice (0, d + 1)
    },
    [{}]
  ) [0]

const intermediate = [["id", "d", "parent", "entity", "type", "question", "answer"], [1, 1, "root", "quiz", "Object"], [2, 2, "quiz", "sport", "Object"], [3, 3, "sport", "q1", "Object", "Which one is correct team name in NBA?", "Huston Rocket"], [4, 4, "q1", "options", "Array"], [5, 5, "options", "New York Bulls", "String"], [6, 5, "options", "Los Angeles Kings", "String"], [7, 5, "options", "Golden State Warriros", "String"], [8, 5, "options", "Huston Rocket", "String"], [9, 2, "quiz", "maths", "Object"], [10, 3, "maths", "q1", "Object", "5 + 7 = ?", "12"], [11, 4, "q1", "options", "Array"], [12, 5, "options", "10", "String"], [13, 5, "options", "11", "String"], [14, 5, "options", "12", "String"], [15, 5, "options", "13", "String"], [16, 3, "maths", "q2", "Object", "12 - 8 = ?", "4"], [17, 4, "q2", "options", "Array"], [18, 5, "options", "1", "String"], [19, 5, "options", "2", "String"], [20, 5, "options", "3", "String"], [21, 5, "options", "4", "String"]]

console .log (
  convert (intermediate)
)
.as-console-wrapper {max-height: 100% !important; top: 0}

We start by ignoring the header row of your data, and create an accumulator object, which is an array holding a single empty object. Then we reduce the remaining rows by creating a node based on the type parameter, finding the parent node, which will be the one at the depth one less than our d parameter. (That is, if d == 4, the parent node will be hierarchy[3].) Then, depending upon the type of that parent node, we either push our new node onto it or add a new property whose name is the value of entity and whose value is our new node. We add our node to the hierarchy at the right place and (for cleanliness reasons) drop the remaining elements off the end of the hierarchy, before returning the hierarchy.

At the end of our reduce call, we simply return the first element of the hierarchy.

There would be some sense in converting this to use the parent node instead of the hierarchy. I'm assuming the reason for including this in a spreadsheet is for simpler editing by non-technical folks; they are probably more likely to understand the parallel parent-name feature than the integer-depth one and would be less likely to mess it up with that.

Never mind. This idea was easy, and it's shown in the next snippet. A sketch for how to do that would be to replace the hierarchy array with an object that initially contains only a root property holding an empty object. Then when you update it on each row, create the node just as we do, find the parent property on that object, and update it just as we do above. Then set the entity property on the object to the current node and return the whole object. (Doing the clean-up on this object would be trickier, but it's not really necessary, even in the other solution.) If you want to do that and can't manage it based on this sketch, feel free to ping me with a comment here or (better) open a new question and if you want me to look at it, add a comment here with a link to it.

const convert = (intermediate) => 
  intermediate .slice (1) .reduce (
    (hierarchy, [_, __, parent, entity, type, question, answer]) => {
      const node = type == "Object" 
        ? {...(question ? {question} : {}), ...(answer ? {answer} : {})} 
          : type == "Array" 
        ? [] 
          : entity
      const parentNode = hierarchy [parent]
      if (Array .isArray (parentNode)) {
        parentNode .push (node)
      } else {
        parentNode [entity] = node
      }
      hierarchy [entity] = node
      return hierarchy
    },
    {root:{}}
  ) .root

const intermediate = [["id", "d", "parent", "entity", "type", "question", "answer"], [1, 1, "root", "quiz", "Object"], [2, 2, "quiz", "sport", "Object"], [3, 3, "sport", "q1", "Object", "Which one is correct team name in NBA?", "Huston Rocket"], [4, 4, "q1", "options", "Array"], [5, 5, "options", "New York Bulls", "String"], [6, 5, "options", "Los Angeles Kings", "String"], [7, 5, "options", "Golden State Warriros", "String"], [8, 5, "options", "Huston Rocket", "String"], [9, 2, "quiz", "maths", "Object"], [10, 3, "maths", "q1", "Object", "5 + 7 = ?", "12"], [11, 4, "q1", "options", "Array"], [12, 5, "options", "10", "String"], [13, 5, "options", "11", "String"], [14, 5, "options", "12", "String"], [15, 5, "options", "13", "String"], [16, 3, "maths", "q2", "Object", "12 - 8 = ?", "4"], [17, 4, "q2", "options", "Array"], [18, 5, "options", "1", "String"], [19, 5, "options", "2", "String"], [20, 5, "options", "3", "String"], [21, 5, "options", "4", "String"]]

console .log (
  convert (intermediate)
)
.as-console-wrapper {max-height: 100% !important; top: 0}

One more approach, simplifying by offloading some of the work to simple helper functions. It still has the same overall design:

// Utility functions
const set = (t) => (k) => (v) =>
  (t[k] = v, t)
const push = (a) => (v) =>
  (a.push(v), a)
const call = (fn, ...args) =>
  fn (...args)
const simplify = (o) =>
  Object .fromEntries (Object .entries (o) .filter (([k, v]) => v != null))

// Main function
const convert = (intermediate) => 
  intermediate .slice (1) .reduce (
    (hierarchy, [_, __, parent, entity, type, question, answer]) => 
      call ((
        node = type == "Object" ? simplify ({question, answer}) : type == "Array" ? [] : entity, 
        parentNode = hierarchy [parent]
      ) => (
        (Array .isArray (parentNode) ? push (parentNode) : set (parentNode) (entity)) (node),
        set (hierarchy) (entity) (node)
      )),
    {root: {}}
  ) .root


const intermediate = [["id", "d", "parent", "entity", "type", "question", "answer"], [1, 1, "root", "quiz", "Object"], [2, 2, "quiz", "sport", "Object"], [3, 3, "sport", "q1", "Object", "Which one is correct team name in NBA?", "Huston Rocket"], [4, 4, "q1", "options", "Array"], [5, 5, "options", "New York Bulls", "String"], [6, 5, "options", "Los Angeles Kings", "String"], [7, 5, "options", "Golden State Warriros", "String"], [8, 5, "options", "Huston Rocket", "String"], [9, 2, "quiz", "maths", "Object"], [10, 3, "maths", "q1", "Object", "5 + 7 = ?", "12"], [11, 4, "q1", "options", "Array"], [12, 5, "options", "10", "String"], [13, 5, "options", "11", "String"], [14, 5, "options", "12", "String"], [15, 5, "options", "13", "String"], [16, 3, "maths", "q2", "Object", "12 - 8 = ?", "4"], [17, 4, "q2", "options", "Array"], [18, 5, "options", "1", "String"], [19, 5, "options", "2", "String"], [20, 5, "options", "3", "String"], [21, 5, "options", "4", "String"]]

console .log (
  convert (intermediate)
)
.as-console-wrapper {max-height: 100% !important; top: 0}

Update -- more generic handling of additional fields

A comment notes that the input format as supplied is important to you for editing as a table. But you also want to treat this more generically than I do above. The only thing I can think of that is not generic is my handling of the question and answer fields. We can adjust that, using the headers you supply. All the fields before these two are necessary for the processing. (That's not really true. I never use the id field and I only need one of d and parent.) After showing the code I will discuss a bit more about why I don't think this can be nearly as flexible as you seem to think, but first, here's a variant that uses the question and answer headers to set the properties on the object:

const convert = (intermediate, [headers, ...rows] = intermediate, custom = headers.slice(5)) => 
  rows .reduce (
    (hierarchy, [_, __, parent, entity, type, ...fields]) => {
      const node = 
        type == "Object" 
          ?  fields.reduce((a, f, i) => ({...a, [custom[i]]: f}), {})
          : type == "Array" 
            ? [] 
            : entity
      const parentNode = hierarchy [parent]
      if (Array .isArray (parentNode)) {
        parentNode .push (node)
      } else {
        parentNode [entity] = node
      }
      hierarchy [entity] = node
      return hierarchy
    },
    {root: {}}
  ) .root
  
const intermediate = [["id", "d", "parent", "entity", "type", "question", "answer"], [1, 1, "root", "quiz", "Object"], [2, 2, "quiz", "sport", "Object"], [3, 3, "sport", "q1", "Object", "Which one is correct team name in NBA?", "Huston Rocket"], [4, 4, "q1", "options", "Array"], [5, 5, "options", "New York Bulls", "String"], [6, 5, "options", "Los Angeles Kings", "String"], [7, 5, "options", "Golden State Warriros", "String"], [8, 5, "options", "Huston Rocket", "String"], [9, 2, "quiz", "maths", "Object"], [10, 3, "maths", "q1", "Object", "5 + 7 = ?", "12"], [11, 4, "q1", "options", "Array"], [12, 5, "options", "10", "String"], [13, 5, "options", "11", "String"], [14, 5, "options", "12", "String"], [15, 5, "options", "13", "String"], [16, 3, "maths", "q2", "Object", "12 - 8 = ?", "4"], [17, 4, "q2", "options", "Array"], [18, 5, "options", "1", "String"], [19, 5, "options", "2", "String"], [20, 5, "options", "3", "String"], [21, 5, "options", "4", "String"]]

console .log (
  convert (intermediate)
)
.as-console-wrapper {max-height: 100% !important; top: 0}

And, as suggested, we could skip the first two fields, simplifying a bit more:

const convert = (intermediate, [headers, ...rows] = intermediate, custom = headers .slice (3)) => 
  rows .reduce (
    (hierarchy, [parent, entity, type, ...fields]) => {
      const node = 
        type == "Object" 
          ?  fields.reduce((a, f, i) => ({...a, [custom[i]]: f}), {})
          : type == "Array" 
            ? [] 
            : entity
      const parentNode = hierarchy [parent]
      if (Array .isArray (parentNode)) {
        parentNode .push (node)
      } else {
        parentNode [entity] = node
      }
      hierarchy [entity] = node
      return hierarchy
    },
    {root:{}}
  ) .root

const intermediate = [
  ["parent", "entity", "type", "question", "answer"],
  ["root", "quiz", "Object"],
  ["quiz", "sport", "Object"],
  ["sport", "q1", "Object", "Which one is correct team name in NBA?", "Huston Rocket"],
  ["q1", "options", "Array"],
  ["options", "New York Bulls", "String"],
  ["options", "Los Angeles Kings", "String"],
  ["options", "Golden State Warriros", "String"],
  ["options", "Huston Rocket", "String"],
  ["quiz", "maths", "Object"],
  ["maths", "q1", "Object", "5 + 7 = ?", "12"],
  ["q1", "options", "Array"],
  ["options", "10", "String"],
  ["options", "11", "String"],
  ["options", "12", "String"],
  ["options", "13", "String"],
  ["maths", "q2", "Object", "12 - 8 = ?", "4"],
  ["q2", "options", "Array"],
  ["options", "1", "String"],
  ["options", "2", "String"],
  ["options", "3", "String"],
  ["options", "4", "String"]
]

console .log (
  convert (intermediate)
)
.as-console-wrapper {max-height: 100% !important; top: 0}

But it sounds as though you want this to be something that you can work with in some generic table-editing tool. I think you're bound for disappointment with that. Here we have a number of different rows, all with the same fields, except for three that include two additional fields ('question' and 'answer'.) We handle that by putting extra columns in the headers for those two, and whenever an Object field has those two values, they get added to it.

That works for this case only because there is a single type of record with extra fields. Let's imagine that your scoring system changes. Instead of answers being only right or wrong, each answer is worth from zero to five points, depending upon how close it is to being right, and there is some hint for wrong answers. (I know this may not be logical for your quizzes, but bear with me, this is simply the first example I thought of; the details hardly matter.) So we might have

{
  // ...
  "options": [
    {val: "New York Bulls", points: 2, hint: 'The Bulls are from Chicago. New York has the Knicks and the Nets'},
    {val: "Los Angeles Kings", points: 0, hint: 'The LA Kings play hockey'},
    {val: "Golden State Warriros", points: 4, hint: 'It is spelled "Warriors"'},
    {val: "Houston Rockets", points: 5, hint: 'Correct'},
  ],
  // ...
}

Now we no longer have the need for the answer field, since it's embedded in these, but we still need the question one, and we also now need val, points, and hint, so perhaps our header looks like this:

["id", "d", "parent", "entity", "type", "question", "val", "points", "hint"],

But then how should the various records look? Like this?:

[3, 3, "sport", "q1", "Object", "Which one is correct team name in NBA?"],

And like this?:

[5, 5, "options", 1, "Object", "Los Angeles Kings", 0, "The LA Kings play hockey'],

How would a generic processor know that one node gets the "answer" field and another the val, points, and hint ones? And of course it will get much worse with more complex input structures.

And that is why Thankyou described a different intermediate format. That one is not really editable as tables, and would be difficult to do with spreadsheets, although probably not impossible. But it is a logical flat structure to describe generic JS objects. The only other one I've seen with any regularity is some sort of key-value pair, where the key captures a whole hierarchy, something like:

['quiz.sport.q1.question', '"Which one is correct team name in NBA?']
// ...
['quiz.sport.q1.options[1].val', 'Los Angeles Kings']
['quiz.sport.q1.options[1].points', 0]
['quiz.sport.q1.options[1].hint', 'The LA Kings play hockey']

But that's no more easily editable than the original JSON.

So I'm afraid I don't have any great answers for you. Flat tables and hierarchical trees simply don't map with any great ease.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • 1
    you worked with the provided state shape and i'm very impressed with the result. you're right, i think it would be hard to refactor without using mutation due to the "machinery" of your solution. my program uses mutation too and makes a few other assumptions. i'll nail it down and make a few notes in awhile. – Mulan Dec 03 '20 at 19:43
  • This is impressive. Thank you sir...Though I was trying to avoid any definition of the output, besides the necessary in the intermediate. – Bronzwik Study Dec 04 '20 at 04:43
  • @BronzwikStudy: Then I would suggest you switch to an intermediate format like that provided by Thankyou. That's why I asked the question, " Is that intermediate format fixed by external requirements or could you alter it if something better was offered?". There is probably enough information in your format to make something work, but it would never be as clean as the one from Thankyou, which has a simpler intermediate format and generic code to process it. – Scott Sauyet Dec 04 '20 at 13:33
  • The intermediate format is important for me... because thats how a table with header is created. I do agree from the coding part ur intermediate format works...but I need to render the 2d array as a table for user to edit. The format is very simple, you have all the info in 2d to build an object back. In terms of heirchy, we have depth along with that we can path as an element during obj2arr – Bronzwik Study Dec 05 '20 at 15:31
  • Added a long edit making the code slightly more generic, but also warning that there's only so far that could go. – Scott Sauyet Dec 06 '20 at 17:13
1

I rarely add a second answer, updating and reupdating as appropriate. But this is something entirely different.

I think that the answer from Thankyou is wonderful, and I suggest that there is a lot to learn from it. I believe, though, that variety is the spice of life; there's room for more than one implementation. I am offering somewhat different code to do the same thing, using the identical intermediate format as that answer. That format might not meet the OP's needs, but it is to my eyes much more logical and consistent.

I won't go into it in detail. Most of the function here are to be found in my own JS/recursion answers or are only slight variants of them. We should note that several of the utility functions here are inspired by Ramda, of which I'm one of the principal authors. The main two functions are toFlatEntries and hydrate, which convert an arbitrary object into this flat array-of-arrays format, and turns that format back into an object, respectively.

// Utility functions
const isInt = Number .isInteger
const isArray = Array .isArray

const getPaths = (obj) =>
  Object (obj) === obj
    ? Object .entries (obj) .flatMap (
        ([k, v]) => getPaths (v) .map (p => [isArray(obj) ? Number(k) : k, ... p])
      )
    : [[]]

// Ramda-inspired utility functions
const path = (ps = []) => (obj = {}) =>
  ps .reduce ((o, p) => (o || {}) [p], obj)

const set = (prop, val, obj) =>  // cf Ramda's `assoc` 
  isInt (prop) && isArray (obj)
    ? [... obj .slice (0, prop), val, ...obj .slice (prop + 1)]
    : {...obj, [prop]: val}

const setPath = ([p = undefined, ...ps], val, obj) =>  // cf Ramda's `assocPath`
  p == undefined
    ? obj
    : ps.length == 0
      ? set (p, val, obj)
      : set (p, setPath (ps, val, obj[p] || (obj [p] = isInt (ps [0]) ? [] : {})), obj)

// Main functions
const toFlatEntries = (obj) => 
  getPaths (obj) .map (p => [...p, path (p) (obj)])

const hydrate = (rows) =>
  rows .map (row => [row .slice (0, -1), row [row .length - 1]]) 
       .reduce ((a, [p, v]) => setPath (p, v, a), isInt (rows [0] [0]) ? [] : {})

const sample = {quiz: {sport: {q1: {question: "Which one is correct team name in NBA?", options: ["New York Bulls", "Los Angeles Kings", "Golden State Warriros", "Huston Rocket"], answer: "Huston Rocket"}}, maths: {q1: {question: "5 + 7 = ?", options: ["10", "11", "12", "13"], answer: "12"}, q2: {question: "12 - 8 = ?", options: ["1", "2", "3", "4"], answer: "4"}}}}

console .log ('Original: ', sample)

const intermediate = toFlatEntries (sample)

console .log ('Intermediate: ', intermediate)

const reconstituted = hydrate (intermediate)

console .log ('Reconstituted: ', reconstituted)
.as-console-wrapper {max-height: 100% !important; top: 0}
Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • 1
    Nice to see another solution from you on this. I ended up giving up on the table format because the input data was nested at different depths and couldn't be converted generically. – Mulan Dec 15 '20 at 22:00