问题
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