0

Given a JSON like below

{
  "main": {
    "sub1": {
      "cat": {
        "subcat1":{
          "count": 1
        },
        "subcat2":{
          "count": 2
        }
      }
    },
    "sub2": {
      "cat": {
        "subcat1":{
          "count": 3
        },
        "subcat2":{
          "count": 5
        }
      }
    }
  }
}

Need to aggregate count at child level to its immediate parent till top-level parent like below

{
  "main": {
    "count": 11,
    "sub1": {
      "count": 3,
      "cat": {
        "count": 3,
        "subcat1":{
          "count": 1
        },
        "subcat2":{
          "count": 2
        }
      }
    },
    "sub2": {
      "count": 8,
      "cat": {
        "count": 8,
        "subcat1":{
          "count": 3
        },
        "subcat2":{
          "count": 5
        }
      }
    }
  }
}

Tried to think of logic for the same, could not get to write anything. What would be right code/logic for same? One this is for sure, that i will nee some kind of recursion that keeps adding counts till parent level.

Hacker
  • 7,798
  • 19
  • 84
  • 154
  • `"cat": { "count": 3,` ...but why you already have `"sub1": { "count": 3,` your expected result makes no sense from a data-wise standpoint. And I can hardly believe you have no code to show - of what you tried so far. – Roko C. Buljan Oct 06 '21 at 17:14

3 Answers3

1

all-in-one

You can write recount using recursion. This solution depends on the leaf nodes (deepest nesting) to contain a numeric count property -

function recount ({count, ...t}) {
  if (count != null) {
    return { ...t, count }
  }
  else {
    const children =
      Object.entries(t).map(([k, v]) => [k, recount(v)])
    return {
      count: children.reduce((r, [_, {count}]) => r + count, 0),  
      ...Object.fromEntries(children)
    }
  }
}

const myinput =
  {main:{sub1:{cat:{subcat1:{count:1},subcat2:{count:2}}},sub2:{cat:{subcat1:{count:3},subcat2:{count:5}}}}}

console.log(recount(myinput))
        
{
  "count": 11,
  "main": {
    "count": 11,
    "sub1": {
      "count": 3,
      "cat": {
        "count": 3,
        "subcat1": {
          "count": 1
        },
        "subcat2": {
          "count": 2
        }
      }
    },
    "sub2": {
      "count": 8,
      "cat": {
        "count": 8,
        "subcat1": {
          "count": 3
        },
        "subcat2": {
          "count": 5
        }
      }
    }
  }
}

decomposed

There are drawbacks to writing a large complex function that takes on many responsibilities. Here is another approach using combination of generic functions.

First we write recount which defines only the restructuring of nodes -

const recount = t =>
  t?.count
    ? t
    : bind
        ( r => ({ count: count(r), ...r })
        , map(t, recount)
        )

Then we define how to perform the actual count -

const count = t =>
  sum(Object.values(t).map(v => v.count))

And fill in the generic dependencies, bind, map, and sum -

const bind = (f, ...x) =>
  f(...x)
  
const map = (t, f) =>
  Object.fromEntries
    ( Object
        .entries(t)
        .map(([k,v]) => [k, f(v)])
    )

const sum = t =>
  t.reduce((r, v) => r + v, 0)

Functionality is the same -

const myinput =
  {main:{sub1:{cat:{subcat1:{count:1},subcat2:{count:2}}},sub2:{cat:{subcat1:{count:3},subcat2:{count:5}}}}}

console.log(recount(myinput))

Expand the snippet below to verify the result in your own browser -

const bind = (f, ...x) =>
  f(...x)
  
const map = (t, f) =>
  Object.fromEntries
    ( Object
        .entries(t)
        .map(([k,v]) => [k, f(v)])
    )

const sum = t =>
  t.reduce((r, v) => r + v, 0)

const recount = t =>
  t?.count
    ? t
    : bind
        ( r => ({ count: count(r), ...r })
        , map(t, recount)
        )

const count = t =>
  sum(Object.values(t).map(v => v.count))

const myinput =
  {main:{sub1:{cat:{subcat1:{count:1},subcat2:{count:2}}},sub2:{cat:{subcat1:{count:3},subcat2:{count:5}}}}}

console.log(recount(myinput))
{
  "count": 11,
  "main": {
    "count": 11,
    "sub1": {
      "count": 3,
      "cat": {
        "count": 3,
        "subcat1": {
          "count": 1
        },
        "subcat2": {
          "count": 2
        }
      }
    },
    "sub2": {
      "count": 8,
      "cat": {
        "count": 8,
        "subcat1": {
          "count": 3
        },
        "subcat2": {
          "count": 5
        }
      }
    }
  }
}
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • Saw the question, wrote my solution, and came back to post it, only to find that you'd done so long before I started. Mine is just enough different syntactically to post as a separate answer, but "great minds" and all that! – Scott Sauyet Oct 06 '21 at 20:11
  • @ScottSauyet this edit includes the [CPS](https://en.wikipedia.org/wiki/Continuation-passing_style)-inspired idea i had last night. i'm inclined to reuse our [universal `map`](https://stackoverflow.com/a/63960623/633183) and perhaps a universal `reduce` is applicable here too... i'll circle back after letting the ideas marinate a bit – Mulan Oct 07 '21 at 15:59
  • I'm so torn about that style for SO answers. I use a number of similar small functions in all my day-to-day coding; they make for much more robust and understandable code. And of course, I also have Ramda, which is all about such tools. But in SO, adding a `sum` function in every answer which needs one quickly gets tedious, and I wonder if readers see the underlying simplicity or just see the wall of functions needed to get the job done and wander away. – Scott Sauyet Oct 07 '21 at 19:40
  • 1
    you're right, it is certainly a trade off. functional programming is hard to teach/demonstrate as the effect is systemic and we often look at these problems through a pinhole. it's challenging but i still think it's worth our time to make the effort and expand the ways in which learners think/see. and it's especially difficult when the asker is looking for a hit-and-run solution and has no interest in learning fundamentals or coding philosophies. i suppose i'm always hoping that the right person(s) will find these answers and they will resonate with them. – Mulan Oct 07 '21 at 23:33
  • 1
    I waver between brief hit-and-run answers (never code-only, I do have some self-respect!) and the longer form you obviously specialize in. Often that ends up summoning my [inner instructor](https://stackoverflow.com/a/69483922) for questions that probably don't warrant it, but yes, I can hope someone later stumbles on it and learns something from it. The biggest question I stumble on here is how much to write bespoke solutions that are self-contained and short, but dense, versus well-factored solutions with many very simple helpers. The latter (your usual style) makes sense, but is laborious. – Scott Sauyet Oct 08 '21 at 12:36
1

This is not very different from the answer from Mulan, but it shows the same process working with somewhat different syntax:

const sum = (ns) => ns .reduce ((a, b) => a + b, 0)

const total = (
  {count, ...rest},
  kids = Object .entries (rest) .map (([k, v]) => [k, total (v)]),
  kidsCount = sum (kids .map (([k, v]) => v .count))
) => count == undefined 
  ? Object .fromEntries ([['count', kidsCount], ...kids])
  : {count, ...rest}

const data = {main: {sub1: {cat: {subcat1: {count: 1}, subcat2: {count: 2}}}, sub2: {cat: {subcat1: {count: 3}, subcat2: {count: 5}}}}}

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

We pull out a count property if it exists, then recur on the other properties of this object, leaving them in a useful form for Object .fromEntries. From those, we extract and sum the count properties, and then, if the count already exists, we return a copy of the original object, and if not, we add one more entry to the kids properties, and call Object .fromEntries on that.

Note that we add a count property to the root here, which wasn't in your requested output, but simply makes sense to me. If you don't want that, you can add a wrapper function, perhaps something like (data) => ({main: total (data .main || {})}).

This uses some optional, defaulted parameters. There are times when that is a bad idea. If you want to avoid them, we can include them in an IIFE instead, like this:

const total = ({count, ...rest}) => ((
  kids = Object .entries (rest) .map (([k, v]) => [k, total (v)]),
  kidsCount = sum (kids .map (([k, v]) => v .count))
) => count == undefined 
  ? Object .fromEntries ([['count', kidsCount], ...kids])
  : {count, ...rest}
) () 

or we can just follow the technique from Mulan, where a local variable means you don't need them.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • 1
    great explanation of the algorithm. i didn’t have time when i made the post so this is a nice add for whomever finds this question. later, i was working on a cheeky alternative in my mind. i’ll try to post it tomorrow if it checks out ^^ – Mulan Oct 07 '21 at 03:35
0

const data = {
  "main": {
    "sub1": {
      "cat": {
        "subcat1":{
          "count": 1
        },
        "subcat2":{
          "count": 2
        }
      }
    },
    "sub2": {
      "cat": {
        "subcat1":{
          "count": 3
        },
        "subcat2":{
          "count": 5
        }
      }
    }
  }
};

const getCount = (thing) => {
  if (!thing.count) {
    thing.count = Object.values(thing).reduce((acc, el) => acc + getCount(el), 0);
  }
  return thing.count;
}

const n = getCount(data);
console.log(n);
console.log(data);
James
  • 20,957
  • 5
  • 26
  • 41