章节编号排序

写业务代码时遇到这样一个问题: 有一个数组,数组中每项是一个章节标题的字符串,每个章节的标题形式上一致。现在需要对这个数组进行排序,返回排序后的数组。

要排序,首先要搞清楚编号的格式,和比较大小的规则。

章节编号格式

章节编号的格式有很多种,常见的几种形式如下:

  1. 纯数字 1 2

  2. 数字和分隔符(点或者中划线) e.g. 1.2.3 2.3

    3-1

  3. 纯中文 e.g. 第一章 第一节

  4. 中文和数字

    e.g. 第 3 章 第 4 章 规则 1 规则 2

  5. 字母加数字

    e.g.

    rule1

    rule2

  6. 中文和标点符号 e.g. 二、 三、

  7. 数字和标点符号 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 保持ab 原来的顺序

详见: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 = ["七、", "一、", "六、"];
相关推荐
sinat_384241091 分钟前
在有网络连接的机器上打包 electron 及其依赖项,在没有网络连接的机器上安装这些离线包
javascript·arcgis·electron
小牛itbull25 分钟前
ReactPress vs VuePress vs WordPress
开发语言·javascript·reactpress
请叫我欧皇i33 分钟前
html本地离线引入vant和vue2(详细步骤)
开发语言·前端·javascript
533_36 分钟前
[vue] 深拷贝 lodash cloneDeep
前端·javascript·vue.js
GIS瞧葩菜1 小时前
局部修改3dtiles子模型的位置。
开发语言·javascript·ecmascript
zhang-zan1 小时前
nodejs操作selenium-webdriver
前端·javascript·selenium
ZBY520311 小时前
【Vue】 npm install amap-js-api-loader指南
javascript·vue.js·npm
前端拾光者2 小时前
利用D3.js实现数据可视化的简单示例
开发语言·javascript·信息可视化
木子02043 小时前
前端VUE项目启动方式
前端·javascript·vue.js
endingCode3 小时前
45.坑王驾到第九期:Mac安装typescript后tsc命令无效的问题
javascript·macos·typescript