写业务代码时遇到这样一个问题: 有一个数组,数组中每项是一个章节标题的字符串,每个章节的标题形式上一致。现在需要对这个数组进行排序,返回排序后的数组。
要排序,首先要搞清楚编号的格式,和比较大小的规则。
章节编号格式
章节编号的格式有很多种,常见的几种形式如下:
-
纯数字 1 2
-
数字和分隔符(点或者中划线) e.g. 1.2.3 2.3
3-1
-
纯中文 e.g. 第一章 第一节
-
中文和数字
e.g. 第 3 章 第 4 章 规则 1 规则 2
-
字母加数字
e.g.
rule1
rule2
-
中文和标点符号 e.g. 二、 三、
-
数字和标点符号 e.g. 1. 2.
比较大小的规则
纯数字的情况是最简单的,直接比较数字大小即可。 数字和标点符号和数字类似,比较大小时去掉标点符号,作为数字比较大小。 中文加数字、字母加数字 也和数字类似,比较大小时去掉重复的中文,再比较大小。
带分隔符的场景,则需要分别比较分隔开的每个编号的大小(以数字形式比较,不能用字典序比较)如果两个字符串分隔的位数不一样(e.g. 3.1.1 和 3.1),也需要比较大小
e.g. ['1.1','10.3','2.2'] -> ['1.1','2.2','10.3']
['3.1.1','3.1'] -> ['3.1','3.1.1']
纯中文场景,按照中文对应的数字来比较大小,注意,不能直接比较两个中文字符串的字典序,或者拼音顺序 e.g. ['第七章','第一章','第六章'] -> ['第一章','第六章','第七章']
中文和标点符号 和纯中文类似,去掉标点符号再比较大小
代码开发
梳理好比较大小的逻辑后,我们就可以写代码实现了。
我们先约定好输入和输出:
输入:一个字符串数组,每个字符串是章节编号
输出:排好序的数组
js 的数组排序肯定离不开 js array 的 sort 函数,需要传入一个 compare 函数来进行比较。
compare 函数的返回值规范如下:
compareFn(a, b) 返回值 |
排序顺序 |
---|---|
> 0 | a 在 b 后,如 [b, a] |
< 0 | a 在 b 前,如 [a, b] |
=== 0 | 保持a 和 b 原来的顺序 |
详见:developer.mozilla.org/zh-CN/docs/...
根据上面列出的几种类型,编写相应的 compare 函数
取数组的第 0 项,来判断章节编号属于上面的哪种类型
根据类型,调用相应的 compare 函数
数组排序的核心逻辑就是我们编写的 compare 函数
对于纯数字,compare 函数如下:
javascript
const comparePureNumber = (a, b) => {
return Number.parseInt(a, 10) - Number.parseInt(b, 10);
};
对于带分隔符的数字,compare 函数需要逐项对比数字,并且要对比出 2.1 和 2.1.1 的大小。代码如下:
javascript
const compareNumberSeparator = (sep) => (a, b) => {
const arrA = a.split(sep);
const arrB = b.split(sep);
const maxLength = Math.max(arrA.length, arrB.length);
for (let i = 0; i < maxLength; i++) {
if (arrA[i] === undefined) {
return -1;
} else if (arrB[i] === undefined) {
return 1;
} else if (arrA[i] === arrB[i]) {
continue; // 继续比较下一项
} else {
return comparePureNumber(arrA[i], arrB[i]);
}
}
return 0;
};
对于纯中文,比较大小前,需要先将中文数字转化成阿拉伯数字,再比较大小
中文数字转阿拉伯数字,用了一个依赖包:zh-to-number@0.0.1
如果中文还有公共前缀和公共后缀,需要先去除,再比较大小
javascript
const { zhToNumber } = require("zh-to-number");
const compareChinese = (prefixLength, postfixLength) => (a, b) => {
const numA = zhToNumber(a.substring(prefixLength, a.length - postfixLength));
const numB = zhToNumber(b.substring(prefixLength, b.length - postfixLength));
return comparePureNumber(numA, numB);
};
中文加数字,先去掉公共前缀(中文),再比较大小
中文加标点符号、数字加标点符号,先去掉公共后缀(标点符号),再比较大小
使用闭包来保存传入的前缀长度和后缀长度,供排序时获取
javascript
const compareChineseAndChar = (postfixLength) => (a, b) => {
return compareChinese(
a.substring(0, a.length - postfixLength),
b.substring(0, b.length - postfixLength)
);
};
const compareWordAndNumber = (prefixLength, postfixLength) => (a, b) => {
return comparePureNumber(
a.substring(prefixLength, a.length - postfixLength),
b.substring(prefixLength, b.length - postfixLength)
);
};
入口函数如下
javascript
const getCompareFn = (list) => {
const first = list[0];
if (/^\d+$/.test(first)) {
// 纯数字
return comparePureNumber;
} else if (/^(\d+\.)+\d+$/.test(first)) {
// 数字和分隔符
return compareNumberSeparator(".");
} else if (/^[\u4e00-\u9fa5]+\d+[\u4e00-\u9fa5]*$/.test(first)) {
// 中文加数字
const prefix = first.match(/^[\u4e00-\u9fa5]+/);
const prefixLength = prefix ? prefix[0].length : 0;
return compareWordAndNumber(prefixLength, 0);
} else if (/^[\u4e00-\u9fa5]+$/.test(first)) {
// 纯中文
return compareChinese(1, 1);
} else if (/^[\u4e00-\u9fa5]+、$/.test(first)) {
// 中文加标点符号
return compareChineseAndChar(1);
} else if (/^\d+、$/.test(first)) {
// 数字加标点符号
return compareWordAndNumber(0, 1);
}
};
测试代码
javascript
const compareFn = getCompareFn(array);
const result = array.sort(compareFn);
console.log(result);
运行结果
纯数字
javascript
const array = ["1", "10", "2"];
数字和分隔符
javascript
const array = ["1.1.2", "1.1", "1.1.3", "3.1", "2.1.1"];
中文加数字
javascript
const array = ["规则2", "规则10", "规则1"];
数字加标点符号
javascript
const array = ["2、", "3、", "1、"];
纯中文
javascript
const array = ["第七章", "第一章", "第六章"];
中文加标点符号
javascript
const array = ["七、", "一、", "六、"];