3 回答
TA贡献1877条经验 获得超6个赞
可重复使用的模块
这是了解可重用模块的绝佳机会。您的GroupMessages函数几乎超过 100 行,并且与您的数据结构紧密耦合。此答案中的解决方案解决了您的特定问题,无需对之前编写的模块进行任何修改。
我将在这个答案的末尾提供一些代码审查,但现在我们重命名,因为schools数组grades中的每个项目代表特定学校单个学生的单个成绩 -
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}
]
正如您所知,JavaScript 没有关联数组。它也没有任何支持使用复合键查找(选择具有多个键的值)的本机数据结构。我们将从二叉树模块导入一些函数,btree为您的记录创建一个标识符,myIdentifier并使用它来初始化您的树,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)
二叉树根据可定制的标识符自动处理分组,并且可以使用任意数量的分组键。我们将使用 basicfilter来选择与查询匹配的所有成绩gender。fromArray选定的成绩数组与处理树更新的合并函数一起传递。inorder用于从树中提取分组值 -
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))
}
现在让我们看看输出 -
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!"
]
}
]
为了完成这篇文章,我们将展示以下的实现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 }
可重用性至关重要。模块可以导入其他模块!上面,从和btree导入- 下面包含部分模块 -funcordered
// 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 }
TA贡献1806条经验 获得超8个赞
我想提供一种稍微不同的方法。它同样构建在现有功能之上,但不像该方法那样低级别。
首先,这是我对问题的初步解决方案:
const call = (fn, ...args) => fn (...args)
const groupBy = (fn) => (xs) =>
xs .reduce ((a, x) => call (key => ((a [key] = [... (a [key] || []), x]), a), fn (x)), {})
const groupMessages = (students, gender) =>
Object .values (groupBy (x => `${x.school}|${x.grade}`) (Object .values (students)
.flat ()
.filter (({isMan}) => isMan == (gender == 'man'))))
.map ((students) => ({
... students [0],
message: students .map (s => s.message) .join ('|')
}))
const students = {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}], 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}]}
console .log (groupMessages (students, 'woman'))
.as-console-wrapper {max-height: 100% !important; top: 0}
它使用了groupBy我经常使用的一个功能。这又取决于call,它接受一个函数和参数列表,并使用这些参数调用该函数。(我在这里使用它只是为了将局部变量保持在最低限度。)
这是有效的,并且显然比原始代码短得多。但它有一个真正的丑陋之处,这是许多此类 JS 代码所共有的。它很紧凑,但操作顺序很难看出。需要深入理解代码才能看到这一点:
const groupMessages = (students, gender) =>
Object .values (groupBy (x => `${x.school}|${x.grade}`) (Object .values (students)
/* ^-- 5 */ /* ^-- 4 */ /* ^-- 1 */
.flat () /* <-- 2 */
.filter (({isMan}) => isMan == (gender == 'man')))) /* <-- 3 */
.map ((students) => ({ /* <-- 6 */
... students [0],
message: students .map (s => s.message) .join ('|')
}))
我倾向于使用一种pipe函数,它将一个函数的结果传递给下一个函数,并将该函数的结果传递给下一个函数,依此类推。这可以清理很多东西。但它确实需要具有具体化数组方法等内容的柯里化函数。
所以,我发现这个细分更清晰:
// reusable utility functions
const pipe = (...fns) => (arg) => fns .reduce ((a, f) => f (a), arg)
const map = (fn) => (xs) => xs .map (x => fn (x))
const filter = (fn) => (xs) => xs .filter (x => fn (x))
const call = (fn, ...args) => fn (...args)
const groupBy = (fn) => (xs) =>
xs .reduce ((a, x) => call (key => ((a [key] = [... (a [key] || []), x]), a), fn (x)), {})
const flat = (xs) => xs .flat ()
// helper function
const groupStudentMessages = (students) => ({
... students [0],
message: students .map (s => s.message) .join ('|')
})
// main function, as a pipeline
const groupMessages = (students, gender) => pipe (
Object .values, /* <-- 1 */
flat, /* <-- 2 */
filter (({isMan}) => isMan == (gender == 'man')), /* <-- 3 */
groupBy (x => `${x.school}|${x.grade}`), /* <-- 4 */
Object .values, /* <-- 5 */
map (groupStudentMessages) /* <-- 6 */
) (students)
const students = {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}], 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}]}
console .log (groupMessages (students, 'woman'))
.as-console-wrapper {max-height: 100% !important; top: 0}
我希望即使没有注释,主函数中的操作顺序也足够清晰。为了使这一点更加清晰,我们提取了groupStudentMessages
辅助函数,并使每个管道步骤成为单行代码。
请注意,前六个函数是非常有用的、可重用的函数。
添加回答
举报