在芬兰语中,我们在W
之后对V
进行排序(如英文),但由于W
不是芬兰语母语,因此被视为V
的变体,它被排序为等于V
,但如果两个单词之间的唯一差异是V
是W
,那么V
- 版本首先排序。一个例子说明了正确的顺序:
Vatanen, Watanen, Virtanen
芬兰语V
和W
归为A
和Á
。 Á
的排序方式与A
类似,但在唯一不同的情况下,首先是非重音的。所有其他重音字母的规则相同,但{Z}后,Å
,Ä
和Ö
会单独整理。
问题:以预定义方式对变体进行排序的最佳算法是什么?(例如[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。
答案 0 :(得分:11)
自从您最初提出这个问题以来,JavaScript终于获得了一些体面的语言环境支持,包括整理。
阅读新的EcmaScript 6 / Harmony功能Intl
,特别是Intl.Collator
。
文档实际上并没有明确表示现代和传统的排序顺序都支持芬兰语,但我已经尝试过它们了。
要获得传统订单的整理程序,您需要传递一个“奇特”的语言代码字符串:fi-u-co-trad
。对于“重组”排序顺序,有fi-u-co-reformed
。这打破了:
fi
- 芬兰语的ISO 639语言代码。u
- 启用Unicode功能/选项。 (没有详细记录)co
- 整理选项。trad
- 传统的排序顺序。我读了关于西班牙语的这个选项,但发现它适用于芬兰语以及测试。 (没有详细记录)reformed
- 改革后的排序顺序。似乎是'trad'的反义词。如果您既未指定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”] 改革:未经共同改革 - >改革[“Vatanen”,“Virtanen”,“Watanen”] 默认值:fi - >默认[“Vatanen”,“Virtanen”,“Watanen”]
在谷歌浏览器中进行过测试,根据我在网上看到的内容,谷歌在这方面落后于Firefox。
答案 1 :(得分:4)
我今天遇到了这个问题并遇到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
似乎可以正常排序芬兰语字符串,我想它也适用于许多其他语言环境。
答案 2 :(得分:3)
解决这个问题的常用方法是使用映射列表(通常列表不需要超过3个,在你的情况下,两个会这样做。)每个映射都将一个字符映射到一个序列点。 [注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
...
比较算法本质上首先将单词中的每个字符翻译为使用mapping1,如果它们不相同,则将其用作比较。如果它们相同,则使用mapping2(依此类推)重复。
并非所有语言都如此简单,因此存在许多变体(例如,您可能会颠倒第2行中的字符串)。
请注意,您可以通过使比较键由翻译的串联组成来实现相同的效果。如果你做了很多比较,缓存这个键可能是一个胜利。在这种情况下,除了第一个映射“不相关”之外,您将在映射中使用特殊值。可以省略所有不相关的代码,这通常会缩短比较密钥。
例如,在您的示例中(但只是大写,因为键入整个映射序列会很繁琐),我们将使用第一个映射到[22, 1, 20, 1, 15, 5, 15]
并使用第二个映射到{{1来转换VATANEN }}。 WATANEN与第一个映射[0, 0, --, 0, --, --, --]
(完全相同),第二个映射为[22, 1, 20, 1, 15, 5, 15]
。因此,删除[1, 0, --, 0, --, --, --]
的[注1],比较键将是:
--
这可以扩展到两个以上的转换表。
例如,许多应用程序想要执行不区分大小写的排序,但是如果没有其他差异,则字符大小写会有所不同(在英语中,这通常意味着在单词之前放置带有大写字母的单词小写,但两种选择都是合理的。)
所以在芬兰的情况下,我们可以添加第三个翻译表,其中所有大写字母都翻译为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, ...]
这个订单“正确”并不明显。事实上,除了具有官方语言权威的语言之外,大多数语言的“正确”意味着什么也不明显。 [注2]因此,上述内容应仅视为多级编码的示例,而不是字母顺序的权威指南。在这种情况下,第三级代码只包含一个位,尽管可能仍然存在语言(如荷兰语),其中有几个字母有三种情况。
上述方案没有考虑有向图和三字母,尽管它们相当容易添加,但要小心。 (在初级排序中,也可能在二级和三级中,有向图需要为两个角色都有一个代码。)西班牙语,与非西班牙语程序员的普遍看法相反,自1994年以来就没有这样的例子,大约二十年前,当RAE下令'ch'在'cg'和'ci'之间按字母顺序排列,而不是像之前那样在'c'和'd'之间。我相信一些荷兰人仍然期望找到'ij'和'y'在一起,匈牙利人可能仍然会尊重包含他们字母表的复杂的有向图和三字母集合,但总的来说,字母顺序的复杂机械方案正在消失,替换为简单的拉丁语排序,可能补充了变音符号的二级排序(法语,西班牙语,显然是芬兰语,德语词典但不是电话簿)或变音符号的初级排序(西班牙语,丹麦语/挪威语/瑞典语元音,土耳其语)。 / p>
[注1]:没有必要插入“不相关的”二级代码,因为编码的次要部分仅参考主要部分相同的单词对。由于任何被认为与次级编码无关的字母都将在主要等价类的所有字中被如此考虑,因此可以从次级编码中省略它。类似地,重复使用不同主要等价类中的代码是合法的,如上所述:[v,w]是[0,1],因此[a,á]。显然,不存在歧义的可能性。因此,二级编码可能非常短,无论是序列长度还是位长。
[注2]:英语没有这样的身体;西班牙语是Real Academia Española,但我在书架上的任何RAE出版物中找不到精确的整理规则,除了简洁的观察结果不按字母顺序考虑重音。然而,RAE的字典似乎始终在任何带有相同字母的重音词之前放置不重音的单词,至少在我能想到的两个案例中 - papa /papá和sabana /sábana。
[注3]当然,我们也需要跟踪原件,因此我们必须以某种方式将比较键附加到字符串上。只要在所有映射中没有两个字符具有相同的转换,就可以使用比较键作为键来使用简单的哈希表来完成。
答案 3 :(得分:0)
我认为应该这样做:
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;
答案 4 :(得分:0)
多级排序的标准方法如下: http://unicode.org/reports/tr10/
原则是使用基于区域设置的定制来覆盖默认Unicode排序规则元素表(DUCET, http://www.unicode.org/Public/UCA/latest/allkeys.txt)。 DUCET是字符的基本排序顺序。如果语言环境有一些特殊规则在DUCET中无法实现或无法在性能方面实现,则需要定制。
目录core / common / collation / in http://unicode.org/Public/cldr/22/core.zip有87个xml文件。 fi.xml中芬兰语定制的一个例子:
<collation type="standard" >
<rules>
<!-- SNIP -->
<reset>V</reset>
<s>w</s>
<t>W</t>
<!-- SNIP -->
</rules>
</collation>
基于区域设置的排序实现起来相当繁琐,并且为了足够快,需要在尽可能低的(机器)级别使用资源,因此我将围绕它进行查看。最好等到Javascript原生支持它。
但可能正在等待永远不会结束:Javascript仍然缺乏对数字排序的支持,这应该很容易在机器级别实现。
如果某个程序员有足够的动力在Javascript中实现基于区域设置的排序,我会很高兴看到结果并支持它。