排序---支持多字段、多类型、自定义排序类型权重

145 阅读4分钟

背景

  1. 项目排序需要根据以下不同类型字段排序。
    (null)  项目特殊的值
    ‘--’    项目为空的占位
    日期     2020-01-01
    区间     (10,20.1]
    数字     100
    版本号   10.01.17
    中文     支付宝
    英文     Alipay
    其它的

  1. 支持多字段排序

以下是代码实现

/* eslint-disable no-useless-escape */

// (null) 项目特殊的值
// ‘--’   项目为空的占位
// 日期   2020-01-01
// 区间   (10,20.1]
// 数字   100
// 版本号  10.01.17
// 中文   支付宝
// 英文   Alipay
// 其它的

const _number = "number";
const _zh = "zh";
const _en = "en";
const _date = "date";
const _range = "range";
const _version = "version";
const _empty = "--";
const _null = "null";
const _null2 = "(null)";

const numberReg = /^[-]?\d+(\.?\d)?\d*$/;
const versionReg = /^(\d{1,2}\.){2,}\d{1,2}$/;
const rangeReg =
  /^[\(|\[]([-|+]?[∞|\d]+(\.?\d)?\d*){1},(([-|+]?[∞|\d]+(\.?\d)?\d*){1})[\)|\]]$/;
const dateReg = /^\d{4}([-\/])\d{2}(\1\d{2})?$/;
const enReg = /^[a-zA-Z]+/;
const zhReg = /[\u4e00-\u9fa5]/;
const emptyReg = /^--$/;

const NULL = ["null", "(null)"];

const ORDER_PROP_ASC = "asc"; // 升序
const ORDER_PROP_DESC = "desc"; // 降序

/**
 * 排序的配置
 * sortWeight 不同类型字段的排序权重,升序--权重越小越靠前  降序反之
 * sortMappingFn 返回所有类型以及权重的映射
 */
const sortConfig = {
  sortWeight: {
    [_number]: 1,
    [_zh]: 2,
    [_en]: 3,
    [_date]: 4,
    [_range]: 5,
    [_version]: 6,
    // 这里还有其它的 ,权重99
    [_empty]: 100,
    [_null]: 101,
    [_null2]: 102,
  },
  sortMappingFn(sortWeight) {
    const _mapping = [
      {
        reg: emptyReg,
        weight: sortWeight[_empty],
      },
      {
        reg: numberReg,
        weight: sortWeight[_number],
      },
      {
        reg: versionReg,
        weight: sortWeight[_version],
      },
      {
        reg: rangeReg,
        weight: sortWeight[_range],
      },
      {
        reg: dateReg,
        weight: sortWeight[_date],
      },
      {
        reg: enReg,
        weight: sortWeight[_en],
      },
      {
        reg: zhReg,
        weight: sortWeight[_zh],
      },
    ];
    return _mapping;
  },
};

/**
 *
 * 获取排序的结果
 *
 * @param {*} a 数组某个对象
 * @param {*} b 数组某个对象
 * @param {*} props 比较的属性名
 * @param {*} rev 升序、降序的标识  升序 1   降序 -1
 * @param {*} fieldConfigs 多字段排序配置
 * @param {*} fieldConfigsIndex // 当前使用什么位置的字段排序
 * @param {*} mixedSort // 混合排序
 * @returns  正数  负数   0
 */
function getCompareResult(
  a,
  b,
  props,
  rev,
  fieldConfigs = [],
  fieldConfigsIndex = 0,
  mixedSort
) {
  // '(null)' 'null' 在项目中排序固定,直接比较

  if (NULL.includes(a[props])) {
    return 1 * rev;
  }

  if (NULL.includes(b[props])) {
    return -1 * rev;
  }

  // 一样 ,比较下一个排序字段
  if (a[props] === b[props]) {
    return getCompareResultByDeep(
      a,
      b,
      fieldConfigsIndex,
      fieldConfigs,
      mixedSort
    );
  }

  // 日期全排
  if (dateReg.test(a[props]) && dateReg.test(b[props])) {
    // 直接返回 结果
    return (Date.parse(a[props]) - Date.parse(b[props])) * rev;
  }

  // 区间全排
  if (rangeReg.test(a[props]) && rangeReg.test(b[props])) {
    if (a[props].includes("-∞")) return -1 * rev;
    if (a[props].includes("+∞")) return 1 * rev;
    if (b[props].includes("-∞")) return 1 * rev;
    if (b[props].includes("+∞")) return -1 * rev;
    const n1 = a[props].slice(a[props].indexOf(",") + 1, a[props].length - 1);
    const n2 = b[props].slice(b[props].indexOf(",") + 1, b[props].length - 1);
    return (Number(n1) - Number(n2)) * rev;
  }

  // 数字全排
  if (numberReg.test(a[props]) && numberReg.test(b[props])) {
    return (Number(a[props]) - Number(b[props])) * rev;
  }

  // version 全排
  if (versionReg.test(a[props]) && versionReg.test(b[props])) {
    return (
      (a[props]
        .split(".")
        .map((v) => v.padStart(2, "0"))
        .join("") -
        b[props]
          .split(".")
          .map((v) => v.padStart(2, "0"))
          .join("")) *
      rev
    );
  }

  // 中文全排
  if (zhReg.test(a[props]) && zhReg.test(b[props])) {
    return (
      a[props].localeCompare(b[props], "zh", {
        numeric: true,
      }) * rev
    );
  }

  // 英文全排
  if (enReg.test(a[props]) && enReg.test(b[props])) {
    return (
      a[props].localeCompare(b[props], "en", {
        numeric: true,
      }) * rev
    );
  }

  // 当前字段的比较肯定不一样,就不需要再进行多字段排序的后续字段了,在fieldConfigsIndex的位置停止比较
  // 其他混合排序
  return mixedSort(a[props], b[props]) * rev;
}

/**
 *  获取排序的结果 ,多个字段组合排序使用
 *
 * @param {*} a 数组某个对象
 * @param {*} b 数组某个对象
 * @param {*} fieldConfigsIndex // 当前使用什么位置的字段排序
 * @param {*} fieldConfigs 多字段排序配置
 * @param {*} mixedSort // 混合排序
 * @returns  正数  负数   0
 */
function getCompareResultByDeep(
  a,
  b,
  fieldConfigsIndex,
  fieldConfigs,
  mixedSort
) {
  // 比较下一个多字段排序配置的字段 ,数组位置+1
  const _fieldConfigsIndex = fieldConfigsIndex + 1;
  // 下一个多字段排序配置已经没了,根据当前排序2项完全一样,返回 0
  if (!fieldConfigs[_fieldConfigsIndex]) {
    return 0;
  }
  const _props = fieldConfigs[_fieldConfigsIndex][0];
  const _rev = fieldConfigs[_fieldConfigsIndex][1] === ORDER_PROP_ASC ? 1 : -1;
  // 组合当前比较的参数
  return getCompareResult(
    a,
    b,
    _props,
    _rev,
    fieldConfigs,
    _fieldConfigsIndex,
    mixedSort
  );
}

/**
 *
 * @param fieldConfigs 默认使用value字段升序
 * @returns
 */
function sortHandle({ fieldConfigs = [], _sortConfig = {} }) {
  const fieldConfigsIndex = 0;
  if (!fieldConfigs[fieldConfigsIndex]) {
    // 可以抛出一个错误
    throw new Error("sortHandle的参数fieldConfigs不符合要求");
  }
  // 配置的需要排序的属性名
  const props = fieldConfigs[fieldConfigsIndex][0];
  // 配置的需要排序的升降序
  const rev = fieldConfigs[fieldConfigsIndex][1] === ORDER_PROP_ASC ? 1 : -1;

  // sortWeight排序权重配置
  // sortMappingFn方法返回排序对应的数据类型【正则】和权重的映射关系
  const { sortWeight, sortMappingFn } = { ...sortConfig, ..._sortConfig };
  // sortMapping 数据类型【正则】和权重的映射关系
  const sortMapping = sortMappingFn(sortWeight);

  // 混合排序,比如 (null) -- 时间  日期  版本  区间 等
  const mixedSort = (a, b) => {
    let _a = 99;
    let _b = 99;
    sortMapping.forEach((v) => {
      if (v.reg.test(a)) _a = v.weight;
      if (v.reg.test(b)) _b = v.weight;
    });
    return _a - _b;
  };

  return function (a, b) {
    // 返回比较的结果
    return getCompareResult(
      a,
      b,
      props,
      rev,
      fieldConfigs,
      fieldConfigsIndex,
      mixedSort
    );
  };
}

/**
 * 
 * @param {*} arr 需要排序的原数组
 * @param {*} fieldConfigs  多字段排序的配置
    [
      ["version", "desc"],  // version字段降序
      ["value", "desc"],
      ["date", "asc"],  // date字段升序
    ]
 * @param {*} _sortConfig  配置列混合类型数据排序的权重
 */
function orderBy(
  arr = [],
  fieldConfigs = [["value", ORDER_PROP_ASC]],
  _sortConfig = {}
) {
  arr.sort(
    sortHandle({
      fieldConfigs,
      _sortConfig,
    })
  );
}

// const _arr = [
//   { value: 2, date: "2020-01-01", version: "0.1.3" },
//   { value: 2, date: "2020-12-01", version: "1.1.3" },
//   { value: 2, date: "2020-03-01", version: "1.1.3" },
//   { value: 2, date: "2020-10-01", version: "1.1.3" },
//   { value: 1, date: "2020-02-01", version: "1.1.3" },
//   { value: 1, date: "2020-10-01", version: "10.1.3" },
//   { value: 1, date: "2020-08-01", version: "1.1.3" },
//   { value: 1, date: "2020-12-08", version: "12.1.3" },

//   { value: 3, date: "2020-02-01", version: "1.1.3" },
//   { value: 3, date: "2020-02-01", version: "1.1.3" },
//   { value: 3, date: "2020-02-01", version: "10.1.3" },
//   { value: 3, date: "2020-08-01", version: "1.1.3" },
//   { value: 3, date: "2020-02-01", version: "0.1.0" },
//   { value: 3, date: "2020-02-01", version: "1.1.3" },
//   { value: 3, date: "2020-08-01", version: "11.1.3" },
//   { value: 3, date: "2020-02-01", version: "5.1.3" },

//   { value: 4, date: "2020-12-01", version: "1.1.3" },
//   { value: 4, date: "2020-12-01", version: "1.1.3" },
//   { value: 4, date: "2020-12-01", version: "10.1.3" },
//   { value: 4, date: "2020-06-01", version: "1.1.3" },
//   { value: 4, date: "2020-05-01", version: "0.1.0" },
//   { value: 4, date: "2020-05-01", version: "1.1.3" },
//   { value: 4, date: "2020-05-05", version: "11.1.3" },
//   { value: 4, date: "2020-05-06", version: "5.1.3" },
// ];

const _arr = [
  // { value: 2, date: "2020-01-01", version: "0.1.3" },
  // { value: 2, date: "2020-12-01", version: "1.1.3" },
  // { value: 2, date: "2020-03-01", version: "1.1.3" },
  // { value: 2, date: "2020-10-01", version: "1.1.3" },
  // { value: 1, date: "2020-02-01", version: "1.1.3" },
  // { value: 1, date: "2020-10-01", version: "10.1.3" },
  // { value: 1, date: "2020-08-01", version: "1.1.3" },
  // { value: 1, date: "2020-12-08", version: "12.1.3" },

  // { value: 3, date: "2020-02-01", version: "1.1.3" },
  // { value: 3, date: "2020-02-01", version: "1.1.3" },
  // { value: 3, date: "2020-02-01", version: "10.1.3" },
  // { value: 3, date: "2020-08-01", version: "1.1.3" },
  // { value: 3, date: "2020-02-01", version: "0.1.0" },
  // { value: 3, date: "2020-02-01", version: "1.1.3" },
  // { value: 3, date: "2020-08-01", version: "11.1.3" },
  // { value: 3, date: "2020-02-01", version: "5.1.3" },

  // { value: 4, date: "2020-12-01", version: "1.1.3" },
  // { value: 4, date: "2020-12-01", version: "1.1.3" },
  // { value: 4, date: "2020-12-01", version: "10.1.3" },
  // { value: 4, date: "2020-06-01", version: "1.1.3" },
  // { value: 4, date: "2020-05-01", version: "0.1.0" },
  // { value: 4, date: "2020-05-01", version: "1.1.3" },
  // { value: 4, date: "2020-05-05", version: "11.1.3" },
  { value: 4, date: "2020-05-06", version: "5.1.3" },

  { value: 5, date: "2020-05-01", version: "0.1.0" },
  { value: "(null)", date: "2020-05-01", version: "0.1.0" },
  { value: "--", date: "2020-05-01", version: "0.1.0" },
  { value: "2020-01-01", date: "2020-05-01", version: "0.1.0" },
  { value: "(10,20.1]", date: "2020-05-01", version: "0.1.0" },
  { value: "12你好", date: "2020-05-01", version: "0.1.0" },
  { value: "bdc", date: "2020-05-01", version: "0.1.0" },

  { value: 5, date: "2020-05-01", version: "0.1.0" },
  { value: "(null)", date: "2020-05-01", version: "0.1.0" },
  { value: "--", date: "2020-04-01", version: "0.1.0" },
  { value: "--", date: "2020-06-01", version: "0.1.0" },
  { value: "--", date: "2020-05-01", version: "0.1.0" },
  { value: "2020-01-02", date: "2020-05-01", version: "0.1.0" },
  { value: "(20.1,100]", date: "2020-05-01", version: "0.1.0" },
  { value: "啊好", date: "2020-05-01", version: "0.1.0" },
  { value: "ed", date: "2020-05-01", version: "0.1.0" },
  { value: "12.90.98", date: "2020-05-01", version: "0.1.0" },

  { value: "null", date: "2020-05-01", version: "0.1.0" },
];

const _arr2 = [];
for (let index = 0; index < 3; index++) {
  _arr2.push(..._arr);
}
console.time("renderTime");

// 自定义排序的权重
const config = {
  sortWeight: {
    zh: 0,
    version: 1,
    number: 2,
    range: 3,
    en: 4,
    "--": 5,
    date: 6,
    null: 7,
    "(null)": 8,
  },
};

/////////////////////////////// 默认权重
orderBy(
  _arr2,
  [
    ["value", ORDER_PROP_DESC],
    // ["version", ORDER_PROP_ASC],
    ["date", ORDER_PROP_DESC],
  ]
);
console.timeEnd("renderTime");
console.log(_arr2);

/////////////////////////////// 自定义权重
orderBy(
  _arr2,
  [
    ["value", ORDER_PROP_DESC],
    // ["version", ORDER_PROP_ASC],
    ["date", ORDER_PROP_DESC],
  ],
  config
);
console.timeEnd("renderTime");
console.log(_arr2);

2023-07-27更新,上述版本拓展正则比较困难,所以改了一版

/* eslint-disable */

// (null) 项目特殊的值
// ‘--’   项目为空的占位
// 日期   2020-01-01
// 区间   (10,20.1]
// 数字   100
// 版本号  10.01.17
// 中文   支付宝
// 英文   Alipay
// 其它的

const _number = "number";
const _zh = "zh";
const _en = "en";
const _date = "date";
const _range = "range";
const _version = "version";
const _empty = "--";
const _null = "null";
const _null2 = "(null)";

const numberReg = [/^[-]?\d+(\.?\d)?\d*%?$/];
const versionReg = [/^(\d{1,2}\.){2,}\d{1,2}$/];
const rangeReg = [
  /^[\(|\[]([-|+]?[∞|\d]+(\.?\d)?\d*){1},\s?(([-|+]?[∞|\d]+(\.?\d)?\d*){1})[\)|\]]$/,
];
const dateReg = [/^\d{4}([-\/])\d{2}(\1\d{2})?$/];
// const enReg = /^(?![0-9]*$)[a-zA-Z0-9\.]+$/;
const enReg = [/^(?![0-9]*$)[a-zA-Z0-9\.\s-_]+$/];
const zhReg = [/[\u4e00-\u9fa5]/];
const emptyReg = [/^--$/];

const NULL = ["null", "(null)"];

const ORDER_PROP_ASC = "asc"; // 升序
const ORDER_PROP_DESC = "desc"; // 降序

function regularOk(regs = [], value) {
  return regs.some((v) => v.test(value));
}

/**
 * 排序的配置
 * sortWeight 不同类型字段的排序权重,升序--权重越小越靠前  降序反之
 * sortMappingFn 返回所有类型以及权重的映射
 */
const sortConfig = {
  sortWeight: {
    [_number]: 1,
    [_zh]: 2,
    [_en]: 3,
    [_date]: 4,
    [_range]: 5,
    [_version]: 6,
    // 这里还有其它的 ,权重99
    [_empty]: 100,
    [_null]: 101,
    [_null2]: 102,
  },
  sortMappingFn(sortWeight) {
    const _mapping = [
      {
        reg: emptyReg,
        weight: sortWeight[_empty],
      },
      {
        reg: numberReg,
        weight: sortWeight[_number],
      },
      {
        reg: versionReg,
        weight: sortWeight[_version],
      },
      {
        reg: rangeReg,
        weight: sortWeight[_range],
      },
      {
        reg: dateReg,
        weight: sortWeight[_date],
      },
      {
        reg: enReg,
        weight: sortWeight[_en],
      },
      {
        reg: zhReg,
        weight: sortWeight[_zh],
      },
    ];
    return _mapping;
  },
};

/**
 *
 * 获取排序的结果
 *
 * @param {*} a 数组某个对象
 * @param {*} b 数组某个对象
 * @param {*} props 比较的属性名
 * @param {*} rev 升序、降序的标识  升序 1   降序 -1
 * @param {*} fieldConfigs 多字段排序配置
 * @param {*} fieldConfigsIndex // 当前使用什么位置的字段排序
 * @param {*} mixedSort // 混合排序
 * @returns  正数  负数   0
 */
function getCompareResult(
  a,
  b,
  props,
  rev,
  fieldConfigs = [],
  fieldConfigsIndex = 0,
  mixedSort
) {
  // '(null)' 'null' 在项目中排序固定,直接比较
  if (NULL.includes(a[props])) {
    return 1 * rev;
  }

  if (NULL.includes(b[props])) {
    return -1 * rev;
  }

  if (a[props] === b[props]) {
    // 比较下一个多字段排序配置的字段 ,数组位置+1
    const _fieldConfigsIndex = fieldConfigsIndex + 1;
    // 下一个多字段排序配置已经没了,根据当前排序2项完全一样,返回 0
    if (!fieldConfigs[_fieldConfigsIndex]) {
      return 0;
    }
    const _props = fieldConfigs[_fieldConfigsIndex][0];
    const _rev =
      fieldConfigs[_fieldConfigsIndex][1] === ORDER_PROP_ASC ? 1 : -1;
    // 组合当前比较的参数
    return getCompareResult(
      a,
      b,
      _props,
      _rev,
      fieldConfigs,
      _fieldConfigsIndex,
      mixedSort
    );
  }

  const fieldConfigsRow = fieldConfigs[fieldConfigsIndex];
  if (fieldConfigsRow.length === 3) {
    const { config, sort } = fieldConfigsRow[2];
    return sort(a[props], b[props], config) * rev;
  }

  // 日期全排
  if (regularOk(dateReg, a[props]) && regularOk(dateReg, b[props])) {
    // 直接返回 结果
    return (Date.parse(a[props]) - Date.parse(b[props])) * rev;
  }

  // 区间全排
  //   if (rangeReg.test(a[props]) && rangeReg.test(b[props])) {
  if (regularOk(rangeReg, a[props]) && regularOk(rangeReg, b[props])) {
    if (a[props].includes("-∞")) return -1 * rev;
    if (a[props].includes("+∞")) return 1 * rev;
    if (b[props].includes("-∞")) return 1 * rev;
    if (b[props].includes("+∞")) return -1 * rev;
    const n1 = a[props].slice(a[props].indexOf(",") + 1, a[props].length - 1);
    const n2 = b[props].slice(b[props].indexOf(",") + 1, b[props].length - 1);
    return (Number(n1) - Number(n2)) * rev;
  }

  // 数字全排
  //   if (numberReg.test(a[props]) && numberReg.test(b[props])) {

  if (regularOk(numberReg, a[props]) && regularOk(numberReg, b[props])) {
    const _a = (a[props] + "").endsWith("%")
      ? a[props].slice(0, a[props].length - 1)
      : a[props];
    const _b = (b[props] + "").endsWith("%")
      ? b[props].slice(0, b[props].length - 1)
      : b[props];
    return (_a - _b) * rev;
  }

  // version 全排
  //   if (versionReg.test(a[props]) && versionReg.test(b[props])) {
  if (regularOk(versionReg, a[props]) && regularOk(versionReg, b[props])) {
    return (
      (a[props]
        .split(".")
        .map((v) => v.padStart(2, "0"))
        .join("") -
        b[props]
          .split(".")
          .map((v) => v.padStart(2, "0"))
          .join("")) *
      rev
    );
  }

  // 中文全排
  //   if (zhReg.test(a[props]) && zhReg.test(b[props])) {
  if (regularOk(zhReg, a[props]) && regularOk(zhReg, b[props])) {
    return (
      a[props].localeCompare(b[props], "zh", {
        numeric: true,
      }) * rev
    );
  }

  // 英文全排
  //   if (enReg.test(a[props]) && enReg.test(b[props])) {
  if (regularOk(enReg, a[props]) && regularOk(enReg, b[props])) {
    return (
      a[props].localeCompare(b[props], "en", {
        numeric: true,
      }) * rev
    );
  }

  // 当前字段的比较肯定不一样,就不需要再进行多字段排序的后续字段了,在fieldConfigsIndex的位置停止比较
  // 其他混合排序
  return mixedSort(a[props], b[props]) * rev;
}

/**
 *
 * @param fieldConfigs 默认使用value字段升序
 * @returns
 */
function sortHandle({ fieldConfigs = [], _sortConfig = {} }) {
  const fieldConfigsIndex = 0;
  if (!fieldConfigs[fieldConfigsIndex]) {
    // 可以抛出一个错误
    throw new Error("sortHandle的参数fieldConfigs不符合要求");
  }
  // 配置的需要排序的属性名
  const props = fieldConfigs[fieldConfigsIndex][0];
  // 配置的需要排序的升降序
  const rev = fieldConfigs[fieldConfigsIndex][1] === ORDER_PROP_ASC ? 1 : -1;

  // sortWeight排序权重配置
  // sortMappingFn方法返回排序对应的数据类型【正则】和权重的映射关系
  const { sortWeight, sortMappingFn } = { ...sortConfig, ..._sortConfig };
  // sortMapping 数据类型【正则】和权重的映射关系
  const sortMapping = sortMappingFn(sortWeight);

  // 混合排序,比如 (null) -- 时间  日期  版本  区间 等
  const mixedSort = (a, b) => {
    let _a = 99;
    let _b = 99;
    sortMapping.forEach((v) => {
      if (regularOk(v.reg, a)) _a = v.weight;
      if (regularOk(v.reg, b)) _b = v.weight;
    });
    return _a - _b;
  };

  return function (a, b) {
    // 返回比较的结果
    return getCompareResult(
      a,
      b,
      props,
      rev,
      fieldConfigs,
      fieldConfigsIndex,
      mixedSort
    );
  };
}

/**
 * 
 * @param {*} arr 需要排序的原数组
 * @param {*} fieldConfigs  多字段排序的配置
    [
      ["version", "desc"],  // version字段降序
      ["value", "desc"],
      ["date", "asc"],  // date字段升序
    ]
 * @param {*} _sortConfig  配置列混合类型数据排序的权重
 */
function sortPlusUtil(
  arr = [],
  fieldConfigs = [["value", ORDER_PROP_ASC]],
  _sortConfig = {}
) {
  arr.sort(
    sortHandle({
      fieldConfigs,
      _sortConfig,
    })
  );
}

const arr = [
  { name: "a", age: 100 },
  { name: "a", age: 2 },
  { name: "a", age: 10 },
];

// sortPlusUtil(arr, [["name", "asc"]]);
sortPlusUtil(arr, [
  ["name", "desc"],
  ["age", "desc"],
]);
console.log(arr);