JavaScript中字母数字字符串的自然排序

290

我正在寻找一种最简单的方法来对由数字和文本以及这些组合组成的数组进行排序。

例如:

'123asd'
'19asd'
'12345asd'
'asd123'
'asd12'

变成

'19asd'
'123asd'
'12345asd'
'asd12'
'asd123'

这将与我在这里提出的另一个问题的解决方案结合使用。

排序函数本身是有效的,我需要一个能够说明'19asd'比'123asd'小的函数。

我正在用JavaScript编写此代码。

我正在寻找一个自然排序的函数。


请参见 https://dev59.com/AnVD5IYBdhLWcg3wNY1Z 上的“如何在JavaScript中进行字符串比较?” - Adriano
1
原始问题是在2010年提出的,所以这并不令人惊讶 :) - ptrn
1
可能是如何在JavaScript中对字符串进行排序的重复问题。 - feeela
1
@feeela 这不是自然排序 - Bergi
7个回答

572
现代浏览器中使用localeCompare,通过传递numeric:true选项,它会智能地识别数字。您可以使用sensitivity:'base'进行不区分大小写的比较。已在Chrome、Firefox和Internet Explorer 11中测试过。

这是一个例子,它返回1,表示10在2之后:

'10'.localeCompare('2', undefined, {numeric: true, sensitivity: 'base'})

在对大量字符串进行排序时,该文章指出:

如果要比较大量的字符串,例如对大型数组进行排序,则最好创建一个 Intl.Collator 对象并使用其 compare 属性提供的函数。

var collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
var myArray = ['1_Document', '11_Document', '2_Document'];
console.log(myArray.sort(collator.compare));


39
如果你想对一个对象数组进行排序,你也可以使用Collator:https://codepen.io/TimPietrusky/pen/rKzoGN - TimPietrusky
3
澄清上述评论: “如果未提供locales参数或该参数未定义,将使用运行时的默认区域设置。” - gkiely
@frodo2975 这里未定义的参数是什么意思?有什么用途呢? - Jayden
2
@Jayden 我们传递 undefined 是为了避免指定语言环境,它将使用浏览器的默认语言环境。 - frodo2975

72

如果你有一个对象数组,你可以这样做:

myArrayObjects = myArrayObjects.sort(function(a, b) {
  return a.name.localeCompare(b.name, undefined, {
    numeric: true,
    sensitivity: 'base'
  });
});

var myArrayObjects = [{
    "id": 1,
    "name": "1 example"
  },
  {
    "id": 2,
    "name": "100 example"
  },
  {
    "id": 3,
    "name": "12 example"
  },
  {
    "id": 4,
    "name": "5 example"
  },

]

myArrayObjects = myArrayObjects.sort(function(a, b) {
  return a.name.localeCompare(b.name, undefined, {
    numeric: true,
    sensitivity: 'base'
  });
});
console.log(myArrayObjects);


25
为了比较值,你可以使用一个比较方法 -
function naturalSorter(as, bs){
    var a, b, a1, b1, i= 0, n, L,
    rx=/(\.\d+)|(\d+(\.\d+)?)|([^\d.]+)|(\.\D+)|(\.$)/g;
    if(as=== bs) return 0;
    a= as.toLowerCase().match(rx);
    b= bs.toLowerCase().match(rx);
    L= a.length;
    while(i<L){
        if(!b[i]) return 1;
        a1= a[i],
        b1= b[i++];
        if(a1!== b1){
            n= a1-b1;
            if(!isNaN(n)) return n;
            return a1>b1? 1:-1;
        }
    }
    return b[i]? -1:0;
}

如果要快速排序一个数组,可以在排序之前先调整好数组的格式,这样你只需要在排序时进行一次小写转换和正则表达式操作,而不是在每一步中都进行。

function naturalSort(ar, index){
    var L= ar.length, i, who, next, 
    isi= typeof index== 'number', 
    rx=  /(\.\d+)|(\d+(\.\d+)?)|([^\d.]+)|(\.(\D+|$))/g;
    function nSort(aa, bb){
        var a= aa[0], b= bb[0], a1, b1, i= 0, n, L= a.length;
        while(i<L){
            if(!b[i]) return 1;
            a1= a[i];
            b1= b[i++];
            if(a1!== b1){
                n= a1-b1;
                if(!isNaN(n)) return n;
                return a1>b1? 1: -1;
            }
        }
        return b[i]!= undefined? -1: 0;
    }
    for(i= 0; i<L; i++){
        who= ar[i];
        next= isi? ar[i][index] || '': who;
        ar[i]= [String(next).toLowerCase().match(rx), who];
    }
    ar.sort(nSort);
    for(i= 0; i<L; i++){
        ar[i]= ar[i][1];
    }
}

这在我的情况下能行吗?内部数组决定外部数组的顺序? - ptrn
String.prototype.tlc()是什么?这是你自己的代码还是从其他地方获取的?如果是后者,请提供链接。 - Andy E
如果您想让a[1]和b[1]控制排序,请使用以下代码: a= String(a[1]).toLowerCase(); b= String(b[1]).toLowerCase(); - kennebec
我刚刚有一个数据列表需要排序,觉得在Chrome Dev Tools控制台中做应该很容易 - 感谢这个函数! - ajh158

8

想象一个数字零填充函数n => n.padStart(8, "0"),它接受任何数字并对其进行填充,例如:

  • "19" -> "00000019"
  • "123" -> "00000123"

这个函数可以用来帮助排序字符串"19",使其出现在字符串"123"之前。

我们添加一个正则表达式/\d+/g,创建自然扩展函数str => str.replace(/\d+/g, n => n.padStart(8, "0")),该函数仅在字符串中查找数字部分并进行填充,例如:

  • "19asd" -> "00000019asd"
  • "123asd" -> "00000123asd"

现在,我们可以使用这个自然扩展函数来帮助实现自然顺序排序:

const list = [
    "123asd",
    "19asd",
    "12345asd",
    "asd123",
    "asd12"
];

const ne = str => str.replace(/\d+/g, n => n.padStart(8, "0"));
const nc = (a,b) => ne(a).localeCompare(ne(b));

console.log(list.map(ne).sort()); // intermediate values
console.log(list.sort(nc)); // result

list.map(ne).sort() 展示了 ne 自然扩展函数的中间结果。它仅在字符串的数字部分上实现数字零填充,并保留字母组件不变。
[
  "00000019asd",
  "00000123asd",
  "00012345asd",
  "asd00000012",
  "asd00000123"
]

解决方案的最终版本实现了一个自然顺序比较器nc,它被实现为(a,b) => ne(a).localeCompare(ne(b))并在list.sort(nc)中使用,以便正确排序。
[
  "19asd",
  "123asd",
  "12345asd",
  "asd12",
  "asd123"
]

6
截至2019年,处理此类问题最全面的库似乎是 natural-orderby
import { orderBy } from 'natural-orderby'

const unordered = [
  '123asd',
  '19asd',
  '12345asd',
  'asd123',
  'asd12'
]

const ordered = orderBy(unordered)

// [ '19asd',
//   '123asd',
//   '12345asd',
//   'asd12',
//   'asd123' ]

它不仅可以对字符串数组进行排序,还可以按照对象数组中某个键的值进行排序。它还可以自动识别和排序货币、日期、货币等字符串,以及其他一些内容。

令人惊讶的是,当使用gzip压缩后,它的大小只有1.6 kB。


1
虽然没有明确说明,但你的回答似乎是针对Node.JS特定的。 - Stephen Quan
@StephenQuan 谢谢 - 我更新了答案,使用了ES6模块语法,这样就不那么依赖NodeJS了。 - Julien

2

kennebec 的回答 基础上,使用 Brian HuismanDavid koelle 创建的代码,这里是一个修改后的原型排序,用于数组对象:

//Usage: unsortedArrayOfObjects.alphaNumObjectSort("name");
//Test Case: var unsortedArrayOfObjects = [{name: "a1"}, {name: "a2"}, {name: "a3"}, {name: "a10"}, {name: "a5"}, {name: "a13"}, {name: "a20"}, {name: "a8"}, {name: "8b7uaf5q11"}];
//Sorted: [{name: "8b7uaf5q11"}, {name: "a1"}, {name: "a2"}, {name: "a3"}, {name: "a5"}, {name: "a8"}, {name: "a10"}, {name: "a13"}, {name: "a20"}]

// **Sorts in place**
Array.prototype.alphaNumObjectSort = function(attribute, caseInsensitive) {
  for (var z = 0, t; t = this[z]; z++) {
    this[z].sortArray = new Array();
    var x = 0, y = -1, n = 0, i, j;

    while (i = (j = t[attribute].charAt(x++)).charCodeAt(0)) {
      var m = (i == 46 || (i >=48 && i <= 57));
      if (m !== n) {
        this[z].sortArray[++y] = "";
        n = m;
      }
      this[z].sortArray[y] += j;
    }
  }

  this.sort(function(a, b) {
    for (var x = 0, aa, bb; (aa = a.sortArray[x]) && (bb = b.sortArray[x]); x++) {
      if (caseInsensitive) {
        aa = aa.toLowerCase();
        bb = bb.toLowerCase();
      }
      if (aa !== bb) {
        var c = Number(aa), d = Number(bb);
        if (c == aa && d == bb) {
          return c - d;
        } else {
          return (aa > bb) ? 1 : -1;
        }
      }
    }

    return a.sortArray.length - b.sortArray.length;
  });

  for (var z = 0; z < this.length; z++) {
    // Here we're deleting the unused "sortArray" instead of joining the string parts
    delete this[z]["sortArray"];
  }
}

46、48和57是什么神奇的数字? - Peter Mortensen

0
我需要按多个字段对对象数组进行排序。在阅读了大量相关内容并尝试了各种解决方案后,我最终得到了一个整体上没有见过的结果,所以我决定在这里分享一下,以防对其他人有所帮助。
const orderBy = orders => (a, b) => {
    const sortDirection = { asc: 1, desc: -1 };
    const sortCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
    const totalOrders = orders.length;

    for (let index = 0; index < totalOrders; index++) {
        const { property, direction = 'desc' } = orders[index];
        const directionInt = sortDirection[direction];
        const compare = sortCollator.compare(a[property], b[property]);

        if (compare < 0) return directionInt;
        if (compare > 0) return -directionInt;
    }

    return 0;
};

// Example:

const unsorted = [
    { name: 'Bananas', count: 1 },
    { name: 'Apples', count: 3 },
    { name: 'Bananas', count: 3 },
    { name: 'bananas', count: 2 },
];

const sorted = unsorted.sort(orderBy([{ property: 'name' }, { property: 'count', direction: 'asc' }]));

console.log(sorted);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接