乐趣区

关于javascript:JS-根据汇总结果过滤

有如下这样一组学生问题的数据,须要把 7 年级的优秀学生(所有科目问题大于等于 80 分)找进去,按数学问题从大到小排序,如果数学问题一样则按姓名排序。

const table = [{ "name": "张三", "grade": 8, "subject": "语文", "score": 90},
    {"name": "张三", "grade": 8, "subject": "数学", "score": 76},
    {"name": "张三", "grade": 8, "subject": "英语", "score": 86},
    {"name": "李四", "grade": 7, "subject": "语文", "score": 78},
    {"name": "李四", "grade": 7, "subject": "数学", "score": 98},
    {"name": "李四", "grade": 7, "subject": "英语", "score": 70},
    {"name": "王五", "grade": 8, "subject": "语文", "score": 90},
    {"name": "王五", "grade": 8, "subject": "数学", "score": 89},
    {"name": "王五", "grade": 8, "subject": "英语", "score": 87},
    ...
];

这里提出了两个要求,一是过滤数据,二是排序。看起来简略,仿佛又不简略,为什么呢?

过滤条件有一项是“所有科目问题大于 80”,这是单纯的逐个判断,而是须要先聚合,再判断。而排序也不是简略的一次成型,而是双重排序。

来看看是怎么实现的(有些办法并不存在,先从办法名的字面意思来了解)

解决问题

const result = data
    // 把 7 年级的学生过滤出来
    .filter(({grade}) => grade === 7)
    // 按姓名分组,分组后是一个对象,形如 {"张三": [{}, {}, {}], "李四": [{}, {}, {}]}
    .groupBy("name")
    // 转换成 entry pair 数组,转后形如 [["张三", [{}, {}, {}]], ["李四", [{}, {}, {}]]]
    .toEntries()
    // 对 pair 的 value(即 pair[1])判断所有分数都在 80 分以上(含 80),符合条件的过滤出来
    .filter(([, its]) => its.every(({score}) => score >= 80))
    // 找出其中数学问题那条记录
    .map(([, its]) => its.find(({subject}) => subject === "数学"))
    // 用例数据不存在没有数学问题的,然而如果有,这里要用 .filter(it => it !== undefined) 过滤掉
    // 排序,先按分数从大到小排
    .sort((a, b) => a.score === b.score ? a.name.compare(b.name) : b.score - a.score);
    //                                    ^^^^^^^^^^^^^^^^^^^^^^ 分数雷同比拟姓名 

采纳链式调用的形式来解决数据,就跟谈话一样,行云流水地就写进去了。只惋惜这里用到了 groupBy()toEntries() 等办法都不存在。然而不要紧,JS 的类扩展性十分好,咱们能够在原型上挂办法函数

Array.prototype.groupBy = function (key) {const getKey = typeof key === "function" ? key : it => it[key];
    return this.reduce((agg, it) => ((agg[getKey(it)] ??= []).push(it), agg),
        {});
};

Object.prototype.toEntries = function () {return Object.entries(this);
};

还有一个 String 的 compare 扩大

String.prototype.compare = function (b) {return this < b ? -1 : this > b ? 1 : 0;};

模仿数据

在没有现成数据的状况下,模仿数据很有必要。先上网找个在线的随机起名的网站,生成几十个名字,于是咱们失去了姓名数组 names

每个人肯定在某一个年级:

names.map(name => ({name, grade: randInt(7, 8)}));

每个人都有三个科目的问题,这三个科目是 const subjects = ["语文", "数学", "英语"]

每个科目都有一个分数(为了更容易找到符合条件的,分数管制在 70~100):

subjects.map(subject => ({subject, score: randInt(70, 100)}));

randInt 当然是不存在的,须要本人写

function randInt(min, max) {return min + ~~(Math.random() * (max + 1 - min));
}

从下面第一个 map 咱们失去了一个对象,蕴含人以及他所在的年级。从下面第二个 map 也能失去一个对象,蕴含科目以及该科目标分数。两个 map 的后果是一对多的关系(一个人有 3 科问题),所以须要应用 flatMap 来开展。所以最终模仿数据是这样生成的:

const data = names
    .map(name => ({ name, grade: randInt(7, 8) }))
    .flatMap(student => subjects.map((subject) => ({...student, subject, score: randInt(75, 100) })
    ));

应用 Lodash 如何

本人扩大原生类是有危险的,万一某个库扩大了,同名,然而参数或者行为有所不同。笼罩扩大之后很容易引起计算凌乱,呈现一些莫名其妙的问题(就是有问题,但又不晓得在哪里)。本人写一套函数来解决当然没问题,不过有现成的 Lodash 为啥不必呢

const result = _(data)
    .filter(({grade}) => grade === 7)
    .groupBy("name")
    .toPairs()
    .filter(([, its]) => its.every(({score}) => score >= 80))
    .map(([, its]) => its.find(({subject}) => subject === "数学"))
    .orderBy(["score", "name"], ["desc", "asc"])
    .value();

基本上和后面的代码一样。

多思考一下

如果不是按每一科都上 80,而是要求总分在 240 分的线上而且最低单科不得低于 75 呢?

唔,这里要算总分,得用一个 reduce

Array.prototype.sumBy = function (key) {return this.reduce((sum, { [key]: value }) => sum + value, 0);
};

再加上不得低于 75 的条件(和不低于 80 相似)

.filter(([, its]) => its.sumBy("score") && its.every(({score}) => score >= 75))

那如果要把二重排序扩大为多重排序呢?

那就须要本人实现一个 sort,并且传入一个属性列表来批示须要按哪些字段来排序(暂且不思考方向)

Array.prototype.sortBy = function (props) {return this.sort((a, b) => {for (const prop of props) {if (a[prop] === b[prop]) {
                // 相等就判断下一项
                continue;
            }

            // 不等则曾经有后果了
            return a[prop] < b[prop] ? -1 : 1;
        }

        return 0;
    });
};

// 调用示例
data.sortBy(["grade", "name", "score"])

如果还要指定程序不审逆序,能够通过在字段名后加 ad 来批示。比方 "grade a""score d" 等。那么在解析的时候能够应用 split 拆分,还能够在没有指定程序的时候默认指定为 "a"

const [field, direct = "a"] = prop.split(/\s+/);

但理论利用中这种形式很受限,万一属性名中含有空格呢?那咱们能够把字符串批示的属性名为一个对象(同时兼容字符串默认升序),比方

["grade", { field: "name"}, {field: "score", desc: true}]
Array.prototype.sortBy = function (props) {return this.sort((a, b) => {for (const prop of props) {const { field, desc} = typeof prop === "string" ? {field: prop} : prop;
            // 依据 desc 来判断 a 小于 b 的时候是返回 -1(升)还是 1(降)const smallMark = desc ? 1 : -1;
            if (a[field] === b[field]) {continue;}
            return a[field] < b[field] ? smallMark : -smallMark;
        }

        return 0;
    });
};

当然像 Lodash 的 _.orderBy() 那样也是能够的,只是感觉把字段和程序分来到有点顺当。

退出移动版