Refactor function with nested for loop - Javascript

一世执手 提交于 2021-01-28 20:09:46

问题


I need to delete nested for loop in a function I created. My function receive an associative array and I return a new array based on certain properties in order to group messages to use later. For example, I have two schools, many students. So I group them based on gender and grade. I don't how to refactor this function because I don't know much about algorithms. It doesn't matter if my logic need be erased completely or need to be done again. I must delete the second for loop. Also, I can return either an common array, associative array or just object. I tried to replicate my function with the same logic but different data:

var studentsArray = new Array();

studentsArray["SCHOOL_1"] = [
    // girls
    {id: '1', school: 'SCHOOL_1', grade: 'A', message: 'Congratulations!', isMan: false},
    {id: '2', school: 'SCHOOL_1', grade: 'A', message: 'Good work!', isMan: false},
    {id: '3', school: 'SCHOOL_1', grade: 'A', message: 'Ok', isMan: false},
    // boys
    {id: '4', school: 'SCHOOL_1', grade: 'A', message: 'Congratulations!', isMan: true},
    {id: '5', school: 'SCHOOL_1', grade: 'B', message: 'Good work!', isMan: true},
    {id: '6', school: 'SCHOOL_1', grade: 'B', message: 'Good work!', isMan: true},
    {id: '7', school: 'SCHOOL_1', grade: 'A', message: 'Congratulations!', isMan: true},
    {id: '8', school: 'SCHOOL_1', grade: 'B', message: 'Good work!', isMan: true},

];
studentsArray["SCHOOL_2"] = [
    // girls
    {id: '9', school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: false},
    {id: '10', school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: false},
    {id: '11', school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: false},
    {id: '12', school: 'SCHOOL_2', grade: 'B', message: 'Good work!', isMan: false},
    {id: '13', school: 'SCHOOL_2', grade: 'B', message: 'Nice!', isMan: false},
    // boys
    {id: '14', school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: true},
    {id: '15', school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: true},
    {id: '16', school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: true},
    {id: '17', school: 'SCHOOL_2', grade: 'B', message: 'Congratulations!', isMan: true},
];

function GroupMessages(schools, gender) {
    // Initialize object to return
    var result = [];
    // First loop
    for (var school in schools) {
        // Group students by gender
        var girls = schools[school].filter(student => !student.isMan);
        var boys = schools[school].filter(student => student.isMan);

        // Flag to determine unique grade per gender
        var boysHaveUniqueGrade = boys.map(student => student.grade).filter((v, i, a) => a.indexOf(v) === i).length === 1;
        var girlsHaveUniqueGrade = girls.map(student => student.grade).filter((v, i, a) => a.indexOf(v) === i).length === 1;

        // If exists a single student per gender, return the same
        if (girls && girls.length === 1) result.push(girls[0]);
        if (boys && boys.length === 1) result.push(boys[0]); 
 
    
        //////////////////////////
        //    Group by grades   //
        /////////////////////////

        if (boys && boys.length > 1 && boysHaveUniqueGrade && gender === 'man') {
            // Combine messages
            let messages = boys.map(boy => boy.message);
            // First student is the reference
            let student = boys[0];
            // Join messages
            student.message = messages.join('|');
            // Update object to return
            result.push(student);
        }

        if (boys && boys.length > 1 && !boysHaveUniqueGrade && gender === 'man') {
            // Group messages by level (maybe I don't need GroupByProperty function neither)
            let studentsByGrade = GroupByProperty(boys, 'grade');
            // Second loop. I return a boys students based on 'grade' property. (I NEED TO DELETE THIS SECOND FOR LOOP)
            for (let grade in studentsByGrade) {
                // First student is the reference
                let student = studentsByGrade[grade][0];
                // Combine messages
                let messages = studentsByGrade[grade].map(student => student.message);
                // Join messages
                student.message = messages.join('|');
                // Update object to return
                result.push(student);
                // Code continue but I stop code here...
            }
        }

        if (girls && girls.length > 1 && girlsHaveUniqueGrade && gender !== 'man') {
            // Combine messages
            let messages = girls.map(girl => girl.message);
            // First student is the reference
            let student = girls[0];
            // Join messages
            student.message = messages.join('|');
            // Update object to return
            result.push(student);


        }

        if (girls && girls.length > 1 && !girlsHaveUniqueGrade && gender !== 'man') {
            // Group messages by level (maybe I don't need GroupByProperty function neither)
            let studentsByGrade = GroupByProperty(girls, 'grade');
            // Second loop. I return a girls students based on 'grade' property. (I NEED TO DELETE THIS SECOND FOR LOOP)
            for (let grade in studentsByGrade) {
                // First student is the reference
                let student = studentsByGrade[grade][0];
                // Combine messages
                let messages = studentsByGrade[grade].map(student => student.message);
                // Join messages
                student.message = messages.join('|');
                // Update object to return
                result.push(student);
                // Code continue but I stop code here...
            }
        }
    }

    return result;
}

function GroupByProperty(objectArray, property) {
    let result = objectArray.reduce((acc, obj) => {
       var key = obj[property];
       if (!acc[key]) acc[key] = [];
       acc[key].push(obj);
       return acc;
    }, {});

    return result;
}

GroupMessages(studentsArray, 'woman'); // any other gender works as 'man'

回答1:


reusable modules

This is great opportunity to learn about reusable modules. Your GroupMessages function is over almost 100 lines and it is tightly coupled with your data structure. The solution in this answer solves your specific problem without any modification of the modules written at an earlier time.

I will offer some code review at end of this answer, but for now we rename schools to grades becauase each item in the array represents a single grade of a single student at a particular school -

const grades =
  [ {id: 1, school: 'SCHOOL_1', grade: 'A', message: 'Congratulations!', isMan: false}
  , {id: 2, school: 'SCHOOL_1', grade: 'A', message: 'Good work!', isMan: false}
  , {id: 3, school: 'SCHOOL_1', grade: 'A', message: 'Ok', isMan: false}
  , {id: 4, school: 'SCHOOL_1', grade: 'A', message: 'Congratulations!', isMan: true}
  , {id: 5, school: 'SCHOOL_1', grade: 'B', message: 'Good work!', isMan: true}
  , {id: 6, school: 'SCHOOL_1', grade: 'B', message: 'Good work!', isMan: true}
  , {id: 7, school: 'SCHOOL_1', grade: 'A', message: 'Congratulations!', isMan: true}
  , {id: 8, school: 'SCHOOL_1', grade: 'B', message: 'Good work!', isMan: true}
  , {id: 9, school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: false}
  , {id: 10, school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: false}
  , {id: 11, school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: false}
  , {id: 12, school: 'SCHOOL_2', grade: 'B', message: 'Good work!', isMan: false}
  , {id: 13, school: 'SCHOOL_2', grade: 'B', message: 'Nice!', isMan: false}
  , {id: 14, school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: true}
  , {id: 15, school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: true}
  , {id: 16, school: 'SCHOOL_2', grade: 'A', message: 'Congratulations!', isMan: true}
  , {id: 17, school: 'SCHOOL_2', grade: 'B', message: 'Congratulations!', isMan: true}
  ]

As you learned, JavaScript does not have associative arrays. Nor does it have any native data structures which support lookups using compound keys (selecting a value with more than one key). We're going to import some functions from our binary tree module, btree, create an identifier for your records, myIdentifier, and use it to initialise your tree, myTree -

import { nil, fromArray, inorder } from "./btree.js"

const myIdentifier = record =>
  [ record?.school ?? "noschool" // if school property is blank, group by "noschool"
  , record?.grade ?? "NA"        // if grade property is blank, group by "NA"
  , record?.isMan ?? false       // if isMan property is blank, group by false
  ]

const myTree =
  nil(myIdentifier)

The binary tree automatically handles grouping based on the customisable identifier, and any number of grouping keys can be used. We will use a basic filter to select all grades matching the queried gender. The selected grades array is passed to fromArray along with a merging function that handles tree updates. inorder is used to extract the grouped values from the tree -

function groupMessages (grades, gender)
{ const t =
    fromArray
      ( myTree
      , grades.filter(x => !x.isMan || gender === "man")
      , ({ messages = [] } = {}, { message = "", ...r }) =>
          ({ ...r, messages: [ ...messages, message ]})
      )
  return Array.from(inorder(t))
}

Let's see the output now -

console.log(groupMessages(grades, 'woman'))
[
  {
    "id": "3",
    "school": "SCHOOL_1",
    "grade": "A",
    "isMan": false,
    "messages": [
      "Congratulations!",
      "Good work!",
      "Ok"
    ]
  },
  {
    "id": "11",
    "school": "SCHOOL_2",
    "grade": "A",
    "isMan": false,
    "messages": [
      "Congratulations!",
      "Congratulations!",
      "Congratulations!"
    ]
  },
  {
    "id": "13",
    "school": "SCHOOL_2",
    "grade": "B",
    "isMan": false,
    "messages": [
      "Good work!",
      "Nice!"
    ]
  }
]

To complete this post, we will show the implementations of btree,

// btree.js

import { memo } from "./func.js"
import * as ordered from "./ordered.js"

const nil =
  memo
    ( compare =>
        ({ nil, compare, cons:btree(compare) })
    )

const btree =
  memo
    ( compare =>
        (value, left = nil(compare), right = nil(compare)) =>
          ({ btree, compare, cons:btree(compare), value, left, right })
    )

const isNil = t =>
  t === nil(t.compare)

const compare = (t, q) =>
  ordered.all
    ( Array.from(t.compare(q))
    , Array.from(t.compare(t.value))
    )

function get (t, q)
{ if (isNil(t))
    return undefined
  else switch (compare(t, q))
  { case ordered.lt:
      return get(t.left, q)
    case ordered.gt:
      return get(t.right, q)
    case ordered.eq:
      return t.value
  }
}

function update (t, q, f)
{ if (isNil(t))
    return t.cons(f(undefined))
  else switch (compare(t, q))
  { case ordered.lt:
      return t.cons(t.value, update(t.left, q, f), t.right)
    case ordered.gt:
      return t.cons(t.value, t.left, update(t.right, q, f))
    case ordered.eq:
      return t.cons(f(t.value), t.left, t.right)
  }
}

const insert = (t, q) =>
  update(t, q, _ => q)

const fromArray = (t, a, merge) =>
  a.reduce
    ( (r, v) =>
       update
          ( r
          , v
          , _ => merge ? merge(_, v) : v
          )
    , t
    )

function* inorder (t)
{ if (isNil(t)) return
  yield* inorder(t.left)
  yield t.value
  yield* inorder(t.right)
}

export { btree, fromArray, get, inorder, insert, isNil, nil, update }

Reusability is essential. Modules can import other modules! Above, btree imports from func and ordered — partial modules included below -

// func.js

function memo (f)
{ const r = new Map
  return x =>
    r.has(x)
      ? r.get(x)
      : (r.set(x, f(x)), r.get(x))
}

export { memo }
// ordered.js

const lt =
  -1

const gt =
  1

const eq =
  0

const empty =
  eq

const compare = (a, b) =>
  a < b
    ? lt
: a > b
    ? gt
: eq

const all = (a = [], b = []) =>
  a.reduce
    ( (r, e, i) =>
        concat(r, compare(e, b[i]))
    , eq
    )

const concat = (a, b) =>
  a === eq ? b : a

export { all, compare, concat, empty, eq, gt, lt }

code review

pending...




回答2:


It seems odd to me that you have to delete the second for loop.

But still, this is the kind of problem that relational databases are meant to solve. If no other option exists, https://github.com/sql-js/sql.js is SQLite compiled to JavaScript.



来源:https://stackoverflow.com/questions/64919293/refactor-function-with-nested-for-loop-javascript

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!