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.