如何对多个 属性 数组项进行排序,其中每个项的 属性 值具有不同的优先级和排序方向规则,但也可以未定义?

How to sort multi-property array-items where each item's property-value has different precedence and sort orientation rules but can be undefined too?

我有一组员工,每个员工至少有全名和员工 ID。 有些员工有 crewNumber 和 cmpId。

这种排序发生在 NestJS 服务器上。 客户端有一个 AG-Grid,它传递需要对服务器上的数据进行排序的数组。

当涉及到数字、字符串,尤其是某些数组的未定义值时,我无法弄清楚如何在服务器上对多个“列”或“属性”的数组进行排序数字属性。

const employees = [
    {
        "employeeId": "JACKAB",
        "fullName": "Jack Absolute",
        "cmpId": 2
    },
    {
        "employeeId": "BLABLA",
        "fullName": "Joe Smith"
    },
    {
        "employeeId": "FORFIVE",
        "fullName": "Tom Scott",
        "cmpId": 109
    },
    {
        "employeeId": "RONBURN",
        "fullName": "Morty Smith"
    },
];

console.log("employees before sorting: ", employees)

const sortBy = [
  { prop: 'fullName', direction: 1, sortIndex: 0 },
  { prop: 'employeeId', direction: -1, sortIndex: 1 },
  { prop: 'cmpId', direction: 1, sortIndex: 2 }
];

employees.sort(function (a, b) {
    let i = 0, result = 0;
    while (i < sortBy.length && result === 0) {

        const prop = sortBy[i].prop;
        const dataA = a[prop];
        const dataB = b[prop];
        const direction = sortBy[i].direction;

        const numberProperties = ["cmpId", "crewNumber"];

        const isNumber = numberProperties.includes(prop);

        if (dataA == undefined && dataB == undefined) {
            result = 0;
        }
        else if (dataA == undefined) {
            result = -1 * direction
        }
        else if (dataB == undefined) {
            result = direction;
        }
        else {
            if (isNumber) {
                result = direction * (dataA < dataB ? -1 : (dataA > dataB ? 1 : 0));
            }
            else {
                result = direction * Intl.Collator().compare(dataA, dataB);
            }
        }

        i++;
    }
            
    return result;
})

console.log("employees after sorting: ", employees);

如果我在具有未定义值的列上上升,我希望未定义值位于顶部。 如果我下降到具有未定义值的列,我希望底部有未定义的值。

目前的问题是 while 循环在结果 != 0 时取消,并且在检查 dataA 是否未定义或 dataB 是否未定义时会发生这种情况。这会导致 while 循环不循环遍历需要发生的其余排序。

我尝试在其中一个未定义时返回 0,但这会导致未定义的值不会移动到任何地方。

由于简单的对象项,这里 employee 项,需要通过一些作为配置对象提供的规则进行比较,人们真的想通过一种通用的方法解决这个问题,因此打破了它分成小 steps/tasks 并为每个任务提供方便的功能。

规则以配置对象列表的形式出现,每个都提供比较项...

  • 每个规则通过自己的 sortIndex 属性.
  • 提供其排序顺序 precedence/importance
  • 提供了一个 property 名称,从而标识将要比较对象的 key-value
  • direction表示升序(1)还是降序(-1)排序。

知道了这一切,我们开始编写两个几乎相似的比较函数,每个比较函数都考虑了绑定的 key 配置对象。因此,对于它传递的两个项目,这样的函数将比较 key 指示的 属性 值。

函数名为 compareByBoundKeyAscendingcompareByBoundKeyDescending。两者都尝试利用第一个传递参数的 localeCompare 方法。如果这样的方法不可用,则排序回退到一般比较,无论是在其升序还是在其降序实现中。两个回退函数是 basicCompareAscendingbasicCompareDescending.

现在已经可以开始使用第一个通用比较函数了...OP 示例代码的 sortBy 数组(比较项列表)按每个项的值升序排序-item 的 sortIndex 键 ...

sortBy
  .sort(compareByBoundKeyAscending
    .bind({
      key: 'sortIndex',
    })
  )

现在有了一组可用的比较项,每个项都已经按照其重要性的正确顺序排列,现在要映射每个比较项,同时从中为以后的每个比较项创建自定义比较函数被比较对象的key-值对...

const precedenceConditionList = sortBy
  .sort(compareByBoundKeyAscending
    .bind({
      key: 'sortIndex',
    })
  )
  .map(({ prop: key, direction }) => ({
      "1": compareByBoundKeyAscending.bind({ key }),
      "-1": compareByBoundKeyDescending.bind({ key }),
    })[direction]
  );

... precedenceConditionList 本身是一组自定义比较函数,按每个函数的重要性排序,其中每个函数比较传递的项目,按此类项目的专用 属性 值升序或降序.

当然,人们仍然需要一个函数,该函数实际上会比较 OP 的 employees 数组中提供的 employee 项之类的东西。

此函数确实对绑定条件列表进行操作,例如我们刚刚在上面创建的 precedenceConditionList。它的实现也是通用的,并利用了 3 个可能的 return 值 -110。只要操作绑定列表中的条件不是 return -11 而是 0,就需要调用绑定列表中的下一个可用条件。因此,可以很容易地利用 Array.prototype.some 来检索正确的 return 值,并尽早从循环条件中中断 ...

function compareByBoundConditionList(a, b) {
  let result = 0;
  this.some(condition => {
    result = condition(a, b);
    return (result !== 0);
  })
  return result;
}

示例代码...

// ... introduce `compareIncomparables` ...
//
// in order to solve the OP's problem of a meaningful
// sorting while dealing with undefined property values.
//
function compareIncomparables(a, b) {
  const index = {
    'undefined':  5,
    'null':       4,
    'NaN':        3,
    'Infinity':   2,
    '-Infinity':  1,
  };
  return (index[String(a)] || 0) - (index[String(b)] || 0);
}

function basicCompareAscending(a, b) {
//return ((a < b) && -1) || ((a > b) && 1) || 0;
  return (

    ((a < b) && -1) ||
    ((a > b) && 1) ||

    // try to furtherly handle incomparable values like ...
    // undefined, null, NaN, Infinity, -Infinity or object types.

    (a === b) ? 0 : compareIncomparables(a, b)
  );
}
function basicCompareDescending(a, b) {
//return ((a > b) && -1) || ((a < b) && 1) || 0;
  return (

    ((a > b) && -1) ||
    ((a < b) && 1) ||

    // try to furtherly handle incomparable values like ...
    // undefined, null, NaN, Infinity, -Infinity or object types.

    (a === b) ? 0 : compareIncomparables(a, b)
  //(a === b) ? 0 : compareIncomparables(b, a)
  );
}

function compareByBoundKeyAscending(a, b) {
  const { key } = this;
  const aValue = a[key];
  const bValue = b[key];

  // take care of undefined and null values as well as of
  // other values which do not feature a `localeCompare`.
  return aValue?.localeCompare
    ? aValue.localeCompare(bValue)
    : basicCompareAscending(aValue, bValue);
}
function compareByBoundKeyDescending(a, b) {
  const { key } = this;
  const aValue = a[key];
  const bValue = b[key];

  // take care of undefined and null values as well as of
  // other values which do not feature a `localeCompare`.
  return bValue?.localeCompare
    ? bValue.localeCompare(aValue)
    : basicCompareDescending(aValue, bValue);
}

function compareByBoundConditionList(a, b) {
  let result = 0;
  this.some(condition => {
    result = condition(a, b);
    return (result !== 0);
  });
  return result;
}


const employees = [{
  "employeeId": "FORFIVE",
  "fullName": "Tom Scott",
}, {
  "employeeId": "BLABLA",
  "fullName": "Joe Smith",
}, {
  "employeeId": "FORFIVE",
  "fullName": "Tom Scott",
  "cmpId": 109
}, {
  "employeeId": "JACKAB",
  "fullName": "Jack Absolute",
  "cmpId": 4
}, {
  "employeeId": "JACKAB",
  "fullName": "Jack Absolute",
  "cmpId": 2
}, {
  "employeeId": "RONBURN",
  "fullName": "Morty Smith",
}, {
  "employeeId": "BLABLU",
  "fullName": "Joe Smith",
}];

const sortBy = [
  { prop: 'fullName', direction: 1, sortIndex: 0 },
  { prop: 'employeeId', direction: -1, sortIndex: 1 },
  { prop: 'cmpId', direction: 1, sortIndex: 2 },
];

const precedenceConditionList = sortBy
  .sort(compareByBoundKeyAscending
    .bind({
      key: 'sortIndex',
    })
  )
  .map(({ prop: key, direction }) => ({
      "1": compareByBoundKeyAscending.bind({ key }),
      "-1": compareByBoundKeyDescending.bind({ key }),
    })[direction]
  );

console.log(
  employees.sort(compareByBoundConditionList
    .bind(precedenceConditionList)
  )
);
.as-console-wrapper { min-height: 100%!important; top: 0; }