Javascript 中基于区域设置的排序,以预定义的方式对重音字母和其他变体进行排序

Posted

技术标签:

【中文标题】Javascript 中基于区域设置的排序,以预定义的方式对重音字母和其他变体进行排序【英文标题】:Locale based sort in Javascript, sort accented letters and other variants in a predefined way 【发布时间】:2012-09-19 10:12:01 【问题描述】:

在芬兰语中,我们将W 排序在V 之后(如英语),但由于W 不是芬兰语本土字母,因此它被视为V 的变体,其排序为等于V,但如果两个词之间的唯一区别是 VW,则首先对 V-version 进行排序。一个例子说明了正确的顺序:

Vatanen, Watanen, Virtanen

在芬兰语中,VW 被整理为 AÁÁ 的排序类似于 A,但在唯一不同的情况下,无重音的优先。同样的规则适用于所有其他重音字母,但 ÅÄÖ 在 Z 之后分别整理。

问题:以预定义的方式对 variants 进行排序的最佳算法是什么?(例如,[Watanen, Vatanen, Virtanen][Vatanen, Watanen, Virtanen])?

补充:该问题与扩展到涵盖http://cldr.unicode.org/index/cldr-spec/collation-guidelines 中定义的其他变体有关,因为该技术很可能是相同的,并且该问题的答案使尽可能广泛的受众受益,并且排序算法可以与 Unicode CLDR 中定义的排序规则兼容。 Unicode CLDR 定义了字母之间的三个级别的差异:一级(基本字母)、二级(重音字母)和三级(字符大小写)。

我想到了某种数组准备,比如在数字排序中,我们可以用零填充所有数字,使它们与字符串一样。一个例子:数组[file1000.jpg, file3.jpg, file22.jpg]可以被准备通过用零填充使其与字符串比较:[file1000.jpg, file0003.jpg, file0022.jpg]。由于数组的准备,我们可以使用原生的Array.sort()快速排序。

目标语言是javascript,它不支持基于排序的排序,所以自定义排序功能必须自己制作。该算法是首选,但如果您也有代码,则值得 +1。

【问题讨论】:

【参考方案1】:

我认为应该这样做:

var variants = ["AÁÀ", "VW", … ];

// Build a map that links variants with their base letter (for quick access)
var map = , chars = "";
for (var i=0; i<variants.length; i++) 
    var variant = variants[i], char = variant.charAt(0);
    for (var j=1; j<variants[i].length; j++)
        map[variant.charAt(j)] = char;
    chars += variant.substr(1);

// and a simple regular expression, containing a character class of all variant chars
var regex = new RegExp("["+chars+"]","g");

function sortFinnish(arr) 
    // each word is replaced by an array [literal],
    // containing 0) the word 1) the normalized word
    for (var i=0; i<arr.length; i++)
        arr[i] = [ arr[i], arr[i].replace(regex, function(m) 
            // every variant character is replaced by its base letter
            return map[m];
        ) ];
    // then sort that array with a custom compare function:
    arr.sort(function(a, b) 
        // at first by the normalized words,
        // i.e. variants count the same as their bases
        if (b[1] > a[1]) return -1;
        if (b[1] < a[1]) return 1;
        // else the normalized words are the same
        // - return a comparsion of the actual words
        if (b[0] > a[0]) return -1;
        if (b[0] < a[0]) return 1;
        return 0;
    );
    // after that, replace each of the arrays with the actual word again
    for (var i=0; i<arr.length; i++)
        arr[i] = arr[i][0];
    return arr;


@performance:好的,我找到了一种在没有自定义比较功能的情况下使用.sort() 的方法,根据http://jsperf.com/sort-mapped-strings,这可能会更快[在某些环境中]。诀窍是使用带有.toString() 方法的对象,该方法返回要排序的字符串:

    function SortString(actualvalue) 
        this.val = actualvalue;
        // the value-to-sort-by is a normalized version, concatenated by a space
        // with the actual value so that the actual value is compared when the
        // normalized ones are the same.
        // ! does not work with values that contain spaces !
        // we'd need to use something like \u0001 instead
        var sortval = actualvalue.replace(regex, function(m) 
            // every variant character is replaced by its base letter
            return map[m];
        ) + " " + actualvalue;
        this.toString = function() return sortval; ;
    
    for (var i=0; i<arr.length; i++)
        arr[i] = new SortString(arr[i]);
    // when comparing, the sortstring is used as the object's representation:
    arr.sort();
    // after that, replace the objects with the actual words again:
    for (var i=0; i<arr.length; i++)
        arr[i] = arr[i].val;

【讨论】:

感谢您的快速响应和代码!尚未对其进行测试,但是您能否用简单的英语解释该算法,例如。在代码之前或之后添加算法? 这还不是算法,我很欣赏更多算法建议。在实际的 sort() 函数中似乎有很多代码。是否可以事先准备好数组,以便我们可以完全依赖原生的 sort() 函数及其速度? 映射算法还不够吗?是的,我们确实事先准备了数组,但我们不能在没有自定义函数的情况下使用sort(),否则实际单词会丢失。而且 compare 函数中的代码真的不多,实际上它和原生方法几乎一样。 内存很便宜,处理能力和时间很贵。那么是否可以将数组转换为同时具有原始键和排序键的映射,并且排序是通过排序键完成的?然后保留原始值。在我的问题的数值比较示例中,为了排序目的修改了数组,在这种情况下,我们必须使用某种地图或对象。 是的,只是并非所有映射都可以映射回原始值,因此您需要以某种方式将其与排序字符串一起传递。【参考方案2】:

解决这个问题的常用方法是使用映射列表(通常列表不需要超过三个,在您的情况下两个就可以了。)每个映射将一个字符映射到一个序列点。 [注 3] 所以在你的例子中,

 primary:      secondary:
  A -> 0         A -> 0
  Á -> 0         Á -> 1
  B -> 1         (irrelevant)
  C -> 2
  D -> 3
  E -> 4
...
  T -> 20
  U -> 21
  V -> 22        V -> 0
  W -> 22        W -> 1
  X -> 23
...

比较算法本质上首先将单词中的每个字符转换为使用映射1,如果它们不相同,则将其用作比较。如果它们相同,则使用 mapping2 重复(以此类推)。

并非所有语言都如此简单,因此有很多变体(例如,您可能会在第 2 步中反转字符串)。

请注意,您可以通过制作由翻译连接组成的比较键来实现相同的效果。如果您进行大量比较,缓存此密钥可能是一个胜利。在这种情况下,您将在“无关”的第一个映射之外的映射中使用特殊值。所有不相关的代码都可以省略,这通常会大大缩短比较键。

例如,在您的示例中(但只是大写,因为键入整个映射序列会很乏味),我们将使用第一个映射到 [22, 1, 20, 1, 15, 5, 15] 并使用第二个映射到 [0, 0, --, 0, --, --, --] 来翻译 VATANEN。 WATANEN 将是[22, 1, 20, 1, 15, 5, 15](完全相同)与第一个映射,[1, 0, --, 0, --, --, --] 与第二个映射。所以删除--的[注1],比较键是:

VATANEN:  [22, 1, 20, 1, 15, 5, 15, 0, 0, 0]
VÁTANEN:  [22, 1, 20, 1, 15, 5, 15, 0, 1, 0] (if there were such a place)
WATANEN:  [22, 1, 20, 1, 15, 5, 15, 1, 0, 0]
VIRTANEN: [22, 9, ...]

这可以扩展到两个以上的翻译表。

例如,许多应用程序想要做一些不区分大小写的排序,但是如果没有其他区别,字符大小写会有所不同(在英语中,这通常意味着将带有大写字母的单词放在所有单词之前小写,但两个选项都是合理的。)

所以在芬兰语的情况下,我们可以添加第三个翻译表,其中所有大写字母都被翻译成 0,所有小写字母都被翻译成 1,而所有其他字符都不被翻译。一些串联翻译:

           -------primary---------  --2ary-  ------tertiary-----
VÁTANEN:  [22, 1, 20, 1, 15, 5, 15, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]
Vátenen:  [22, 1, 20, 1, 15, 5, 15, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1]
WATANEN:  [22, 1, 20, 1, 15, 5, 15, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]

这个顺序“正确”一点也不明显。事实上,除了那些拥有官方语言权威的语言之外,对于大多数语言来说,“正确”意味着什么也不是很明显。 [注 2] 因此,以上内容应仅视为多级编码的示例,而不是字母顺序的权威指南。在这种情况下,第三级代码仅由一个位组成,尽管可能仍有一些语言(如荷兰语)对几个字母有三种情况。

上述方案没有考虑二合字母和三合字母,尽管它们相当容易添加,但要小心。 (在初级排序中,也可能在二级和三级排序中,二合字母需要为两个字符使用一个代码。)与非西班牙程序员的普遍看法相反,自 1994 年以来,西班牙就不再是这样的例子,大约 20 年前,当 RAE 规定“ch”按字母顺序排列在“cg”和“ci”之间,而不是像以前那样在“c”和“d”之间。我相信一些讲荷兰语的人仍然希望同时找到“ij”和“y”,匈牙利人可能仍然尊重构成他们的字母表的复杂的二合字母和三合字母集合,但总的来说,用于字母排序的复杂机械方案正在消失,替换为简单的拉丁语排序,可能辅以变音符号的二级排序(法语、西班牙语、显然是芬兰语、德语词典,但不是电话簿)或变音符号的初级排序(西班牙语 ñ、丹麦语/挪威语/瑞典元音、土耳其语)。


[注1]:不必插入“不相关”的二级代码,因为编码的二级部分仅用于主要部分相同的单词对。由于任何被认为与二级编码无关的字母都将在一级等价类中的所有单词中如此考虑,因此可以将其从二级编码中省略。类似地,在不同的主要等价类中重用代码是合法的,就像我们上面所做的那样:[v, w] 是 [0, 1],[a, á] 也是。显然,没有歧义的可能性。因此,二级编码在序列长度和比特长度上都可能非常短。

[注2]:英语没有这样的主体;西班牙文是Real Academia Española,但我在书架上的任何RAE 出版物中都找不到精确的校对规则,除了不按字母顺序考虑重音的简洁观察。但是,RAE 的字典似乎始终将不带重音的单词放在任何带有相同字母的重音单词之前,至少在我能想到的两种情况下——papa/papá 和 sabana/sábana。

[注 3] 当然,我们还需要跟踪原始数据,所以我们必须以某种方式将比较键附加到字符串。只要没有两个字符在所有映射中具有相同的翻译,这可以通过使用比较键作为键的简单哈希表来完成。

【讨论】:

谢谢!我对“由翻译连接组成的比较键”的想法感兴趣?你能解释一下吗? 这听起来不错,但我并不完全理解比较键的概念。 WATWANEN 或 WATVANEN 的比较键是什么? 此“比较键”功能是否与userguide.icu-project.org/collation/architecture 中的“排序键”相同(请参阅排序键部分)? @Timo,是的,完全正确。这看起来也很好地解释了这个概念。 但我不明白为什么你在排序的末尾有那些“0,0,0]”、“0,1,0]”和“1,0,0]”钥匙?他们应该在有问题的信之后吗?这样:VATANEN: [22, 0, 0, 0, 1, 20, 1, 15, 5, 15]?【参考方案3】:

多级排序的标准方法如下所示: http://unicode.org/reports/tr10/

原则是使用基于语言环境的剪裁来覆盖默认 Unicode 排序元素表(DUCET, http://www.unicode.org/Public/UCA/latest/allkeys.txt)。 DUCET 是字符的基本排序顺序。如果语言环境有一些特殊规则不能或不适合在 DUCET 中实施,则需要进行剪裁。

http://unicode.org/Public/cldr/22/core.zip 中的目录 core/common/collat​​ion/ 有 87 个 xml 文件。 fi.xml 中的芬兰剪裁示例:

<collation type="standard" >
    <rules>
    <!-- SNIP -->
        <reset>V</reset>
            <s>w</s>
            <t>W</t>
    <!-- SNIP -->
    </rules>
</collation>

基于区域设置的排序实现起来相当繁琐,而且为了足够快,需要在尽可能低的(机器)级别使用资源,所以我认为这是最好的等到 Javascript 原生支持它。

但可能等待永无止境:Javascript仍然缺乏对数字排序的支持,这在机器级别应该很容易实现。

如果某个编码人员有足够的动力在 Javascript 中实现基于区域设置的排序,我会很高兴看到结果并支持它。

【讨论】:

【参考方案4】:

自从您最初提出这个问题以来,JavaScript 终于获​​得了一些不错的语言环境支持,包括排序规则。

阅读EcmaScript 6 / Harmony 的新功能Intl,特别是Intl.Collator

文档实际上并没有很清楚地说明芬兰语支持现代和传统的排序顺序,但我已经尝试过了。

要获得传统订单的整理者,您需要传递一个“花哨的”语言代码字符串:fi-u-co-trad。对于“改革”排序顺序,有fi-u-co-reformed。这分解为:

fi - 芬兰语的 ISO 639 语言代码。 u - 启用 Unicode 功能/选项。 (没有很好的记录) co - 排序选项。 trad - 传统的排序顺序。我阅读了有关西班牙语的此选项,但通过测试发现它也适用于芬兰语。 (没有很好的记录) reformed - 重新排序排序。似乎是“传统”的反义词。如果您既不指定trad 也不指定reformed,您将得到default,在某些浏览器上可能是trad,在其他浏览器上可能是reformed

编码:

var surnames = ['Watanen', 'Vatanen', 'Virtanen'];

var traColl = new Intl.Collator('fi-u-co-trad');
var refColl = new Intl.Collator('fi-u-co-reformed');
var defColl = new Intl.Collator('fi');

console.log('traditional:', traColl.resolved.requestedLocale + ' -> ' + traColl.resolved.collation, surnames.sort(function (a, b) 
  return traColl.compare(a,b);
));

console.log('reformed:', refColl.resolved.requestedLocale + ' -> ' + refColl.resolved.collation, surnames.sort(function (a, b) 
  return refColl.compare(a,b);
));

console.log('default:', defColl.resolved.requestedLocale + ' -> ' + defColl.resolved.collation, surnames.sort(function (a, b) 
  return defColl.compare(a,b);
));

输出:

繁体:fi-u-co-trad -> 繁体 ["Vatanen", "Watanen", "Virtanen"] 改革:fi-u-co-reformed -> 改革 ["Vatanen", "Virtanen", "Watanen"] 默认值:fi -> 默认值 [“Vatanen”、“Virtanen”、“Watanen”]

在 Google Chrome 中测试,根据我在网上阅读的内容,它在这方面落后于 Firefox。

【讨论】:

非常感谢!快速搭建了一个测试平台:jsbin.com/kijiji/3。似乎(如您所说)这在 Chrome 中有效,但 FF 不支持 'fi-u-co-trad' (它与 Chrome 的 'fi' 相同)并且 IE11 将 'fi' 显示为 'fi- Chrome 的 u-co-trad'。结论:使用 'fi' 在不同的浏览器中产生不同的排序顺序,这意味着需要浏览器/功能嗅探或外部库或服务器端库来确保浏览器中的一致排序。当然,当使用非原生解决方案时,执行速度可能会成为问题。 我强烈建议针对其他浏览器中的行为提交错误报告。或者将其提交给组织 ES6 Harmony 的人使用的某个论坛或邮件列表,特别是如果他们有本地化小组。 @Timo 我发现trad 也有一个“反义词”,称为reformed。如果某些浏览器以不同的方式解释这种三向选择,我不会感到惊讶:unicode.org/reports/tr35/#Locale_Extension_Key_and_Type_Data【参考方案5】:

我今天遇到了这个问题,遇到了String.prototype.localeCompare。您可以将其与 arr.sort() 一起使用并指定语言环境:

var names = ['Andrea', 'Ándrea', 'Àndrea', 'Äiti', 'Özmir', 'åke', 'Zorro', 'Åke'];

// Undesired order:
names.sort()
console.log('default sort', names);

// Desired order:
names.sort((nameA, nameB) => nameA.localeCompare(nameB, 'fi') > 0);
console.log('locale sort', names);

// Or since positive values are truthy, you can omit the `> 0`:
names.sort((nameA, nameB) => nameA.localeCompare(nameB, 'fi'));

// You can also control whether upper or lower case should sort first:
names.sort((nameA, nameB) => nameA.localeCompare(nameB, 'fi', 
  caseFirst: 'upper'
));
console.log('locale sort with caseFirst option', names);

看起来caseFirst 选项在 Chrome 中有效,但目前在 Firefox 中无效。

MDN page 包含更多信息和可用选项。 localeCompare 似乎可以很好地对芬兰语字符串进行排序,我想它也适用于许多其他语言环境。

【讨论】:

以上是关于Javascript 中基于区域设置的排序,以预定义的方式对重音字母和其他变体进行排序的主要内容,如果未能解决你的问题,请参考以下文章

如何使用预定义的 NS 创建 route53 区域,或更新注册域的 NS?

地区、国家、城市从 CSV 中的 Javascript 排序

计算沿预定义线串从线串开始到围绕该线串的缓冲区域内的点的路线长度

JavaScript中的全局对象,内置对象和预定义对象是啥?

以 hh:mm tt 格式排序字符串 - javascript - datepicker

基于 geohashes 的 pubsub 主题划分的建议,以实现巧妙的 websocket 连接服务