Home > Back-end >  Calculating on objects in recursive function
Calculating on objects in recursive function

Time:03-18

I'm trying to make a function that calculates the percentage of "completed tasks" in a recursive manner.

Here's what I have now, but this only calculates the percentage on the parent, not grandparent etc:

const tasks = [
    { id: 1, task: 'Clean apartment', completed: null, parentId: null},
    { id: 2, task: 'Make app', completed: null, parentId: null},
    { id: 3, task: 'Clean bathroom', completed: null, parentId: 1},
    { id: 4, task: 'Clean kitchen', completed: null, parentId: 1},
    { id: 5, task: 'Wash sink', completed: true, parentId: 3},
    { id: 6, task: 'Wash shower', completed: null, parentId: 3},
    { id: 7, task: 'Wash glass panes', completed: null, parentId: 6},
    { id: 8, task: 'Wash floor', completed: null, parentId: 6},
]

function compileTask(tasks, taskId) {
    var task = (typeof taskId === 'number') ? tasks.find(task => task.id === taskId) : taskId;
    var children = tasks.filter(t => t.parentId === task.id);

    task.percent = children.filter(c => c.completed === true).length / children.length * 100


    task.children = children.map(child => compileTask(tasks, child));
    return task
}

console.log(compileTask(tasks, 1))

As you see, task id 3 is 50% completed because one of the two child tasks are marked completed, but task id 1 is 0% completed. If I'm calculating right in my head, task id 1 should return 25% completed.

Any tips?

CodePudding user response:

I'm not quite sure of the output format you want, so I'm going to generate something like the following. We can probably alter it to fit your needs:

[
  {id: 1, task: 'Clean apartment', percent: 25, parentId: null}
  {id: 2, task: 'Make app', percent: 0, parentId: null}
  {id: 3, task: 'Clean bathroom', percent: 50, parentId: 1}
  {id: 4, task: 'Clean kitchen', percent: 0, parentId: 1}
  {id: 5, task: 'Wash sink', percent: 100, parentId: 3}
  {id: 6, task: 'Wash shower', percent: 0, parentId: 3}
  {id: 7, task: 'Wash glass panes', percent: 0, parentId: 6}
  {id: 8, task: 'Wash floor', percent: 0, parentId: 6}
]

Initial Approach

This is a fairly simple recursive technique:

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

const findPercent = (task, tasks, children = tasks .filter (({parentId}) => task .id == parentId)) => 
  children .length 
    ? sum (children .map (c => findPercent(c, tasks))) / children .length
    : task .completed ? 100 : 0

const compileTasks = (tasks) => tasks .map (
  (task, _, __, {completed, parentId, ...rest} = task) => ({
    ...rest, 
    percent: findPercent (task, tasks), 
    parentId
  })
)

const tasks = [{id: 1, task: 'Clean apartment', completed: null, parentId: null}, {id: 2, task: 'Make app', completed: null, parentId: null}, {id: 3, task: 'Clean bathroom', completed: null, parentId: 1}, {id: 4, task: 'Clean kitchen', completed: null, parentId: 1}, {id: 5, task: 'Wash sink', completed: true, parentId: 3}, {id: 6, task: 'Wash shower', completed: null, parentId: 3}, {id: 7, task: 'Wash glass panes', completed: null, parentId: 6}, {id: 8, task: 'Wash floor', completed: null, parentId: 6}]

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

findPercent is a helper function that recursively looks through the children, and averages their percentages. It bottoms out when there are no children, and then chooses 0% or 100% depending upon whether the task is completed. It uses a trivial sum helper, which adds up an array of numbers.

The main function is compileTasks. It simply maps the list of tasks to slightly altered versions, removing completed and adding percent, which is found by calling findPercent.

There is a potential problem with this technique, though. It doesn't matter at all for small tests, but there is a lot of work that is done multiple times. We average the percentages of 7 and 8, when building 1, then do it again when building 3, and again when building 6. This is problematic, and so I will suggest a:

More Performant Approach

The problem here is that our flat list is not the best way to track what is fundamentally a tree structure. This would be much easier to work with:

[
    {id: 1, task: "Clean apartment", completed: null, children: [
        {id: 3, task: "Clean bathroom", completed: null, children: [
            {id: 5, task: "Wash sink", completed: true},
            {id: 6, task: "Wash shower", completed: null, children: [
                {id: 7, task: "Wash glass panes", completed: null},
                {id: 8, task: "Wash floor", completed: null}
            ]}
        ]},
        {id: 4, task: "Clean kitchen", completed: null}
    ]},
    {id: 2, task: "Make app", completed: null}
]

I would suggest that you consider using this more useful structure internally all the time, and only using the flat list for storage. But if you can't, do this, then we can convert back and forth between the two structure, and do your calculations on that nicer tree structure. It might look like this:

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

const nest = (tasks, parent = null) =>
  tasks .filter (({parentId}) => parentId === parent) .map (
    ({id, parentId, ...rest}, _, __, children = nest (tasks, id)) => ({
      id, ...rest, ...(children.length ? {children} : {})
    }))

const unnest = (nodes, parentId = null) => [
  ...nodes .map (({children, ...rest}) => ({...rest, parentId})),  
  ...nodes .flatMap (node => unnest (node .children || [], node.id))
]

const calcPcts = (tasks) =>
  tasks .map (({completed, children = [], ...rest}, _, __, kids = calcPcts (children)) => ({
    ... rest,
    ... (kids .length 
          ? {percent: sum (kids .map (k => k.percent)) / kids .length, children: kids} 
          : {percent: completed ? 100 : 0}
        )
  }))

const compileTasks = (tasks) => 
  unnest (calcPcts (nest (tasks)))

const tasks = [{id: 1, task: 'Clean apartment', completed: null, parentId: null}, {id: 2, task: 'Make app', completed: null, parentId: null}, {id: 3, task: 'Clean bathroom', completed: null, parentId: 1}, {id: 4, task: 'Clean kitchen', completed: null, parentId: 1}, {id: 5, task: 'Wash sink', completed: true, parentId: 3}, {id: 6, task: 'Wash shower', completed: null, parentId: 3}, {id: 7, task: 'Wash glass panes', completed: null, parentId: 6}, {id: 8, task: 'Wash floor', completed: null, parentId: 6}]

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

Here, after the sum helper, we have a nest function that converts your flat list into that tree structure, and an unnest function that converts it such a tree back into the flat list.

Our main function is compileTasks, which simply composes these two functions and runs the core function calcPcts between them to generate this intermediate format:

[
    {id: 1, task: "Clean apartment", percent: 25, children: [
        {id: 3, task: "Clean bathroom", percent: 50, children: [
            {id: 5, task: "Wash sink", percent: 100},
            {id: 6, task: "Wash shower", percent: 0, children: [
                {id: 7, task: "Wash glass panes", percent: 0},
                {id: 8, task: "Wash floor", percent: 0}
            ]}
        ]},
        {id: 4, task: "Clean kitchen", percent: 0}
    ]},
    {id: 2, task: "Make app", percent: 0}
]

And now we need to discuss calcPcts. This works much like above, except that because we're calling top-down, and rolling up the results from the bottom up, we don't have to re-call it for nested nodes.

One interesting thing about this approach is that you can reuse nest and unnest around other recursive node conversion techniques. The roll-up from completed to percent is isolated in that one place, but the overall mechanism is shared.

CodePudding user response:

You are mixing objects and integers.

var task = (typeof taskId === 'number') ? tasks.find(task => task.id === taskId) : taskId;

returns a task in one case and an id in another

then here

task.children = children.map(child => compileTask(tasks, child));

You are passing a child instead of the child.id. It should be

task.children = children.map(child => compileTask(tasks, child.id));
  • Related