在将其标记为重复之前,请先阅读底部
我希望能够按字母顺序对字符串数组(或基于一个字符串值的结构片段)进行排序,但基于自定义字母或unicode字母。
大多数时候,人们建议使用支持不同预定义语言环境/字母的整理程序。 (请参阅this answer for Java),但是对于这些语言环境捆绑包中不可用的稀有语言/字母,该怎么办?
我想使用的语言不可用 in the list of languages受Golangs的collate支持和使用,因此我需要能够定义自定义字母,或Unicode字符/符文的排序顺序。
其他人建议先将字符串转换为英语/ ASCII可排序字母,然后再对其进行排序。这就是this solution done in Javascript或this solution in Ruby中类似问题的建议。 但是,肯定有一种使用Go执行此操作的更有效方法。
是否可以在Go语言中使用自定义字母/字符集创建Collator?那是func NewFromTable的目的吗?
似乎我应该能够使用Reorder函数,但是看来尚未用该语言实现? source code显示如下:>
func Reorder(s ...string) Option {
// TODO: need fractional weights to implement this.
panic("TODO: implement")
}
如何定义用于比较和排序字符串的自定义字母顺序?
答案 0 :(得分:3)
事先注意:
以下解决方案已经过清理和优化,并在此处发布为可重用的库:github.com/icza/abcsort
。
使用abcsort
,对字符串切片进行自定义排序(使用自定义字母)非常简单:
sorter := abcsort.New("bac")
ss := []string{"abc", "bac", "cba", "CCC"}
sorter.Strings(ss)
fmt.Println(ss)
// Output: [CCC bac abc cba]
通过struct字段之一对结构切片进行自定义排序,如下所示:
type Person struct {
Name string
Age int
}
ps := []Person{{Name: "alice", Age: 21}, {Name: "bob", Age: 12}}
sorter.Slice(ps, func(i int) string { return ps[i].Name })
fmt.Println(ps)
// Output: [{bob 12} {alice 21}]
原始答案如下:
我们可以实现使用自定义字母的自定义排序。我们只需要创建适当的less(i, j int) bool
函数,然后sort
包就可以完成剩下的工作。
问题是如何创建这样的less()
函数?
让我们从定义自定义字母开始。方便的方法是创建一个string
,其中包含自定义字母的字母(从最小到最大枚举(排序))。例如:
const alphabet = "bca"
让我们根据该字母表创建一个地图,该地图将告诉我们自定义字母表中每个字母的重量或顺序:
var weights = map[rune]int{}
func init() {
for i, r := range alphabet {
weights[r] = i
}
}
(注意:以上循环中的i
是字节索引,而不是rune
索引,但是由于两者都是单调递增的,因此两者都适合符文权重。)< / sup>
现在我们可以创建我们的less()
函数。为了具有“可接受的”性能,我们应该避免将输入的string
值转换为字节或符文分片。为此,我们可以从utf8.DecodeRuneInString()
函数调用辅助功能,该功能对rune
的第一个string
进行解码。
因此,我们逐符进行比较。如果两个符文都是自定义字母的字母,我们可以使用它们的权重来告诉它们如何相互比较。如果至少其中一个符文不是来自我们的自定义字母,我们将退回到简单的数字符文比较。
如果两个输入字符串开头的2个符文相等,则我们继续每个输入字符串中的下一个符文。我们可以这样对输入字符串进行切片:对它们进行切片不会生成副本,它只会返回一个指向原始字符串数据的新字符串标题。
好的,现在让我们看看这个less()
函数的实现:
func less(s1, s2 string) bool {
for {
switch e1, e2 := len(s1) == 0, len(s2) == 0; {
case e1 && e2:
return false // Both empty, they are equal (not less)
case !e1 && e2:
return false // s1 not empty but s2 is: s1 is greater (not less)
case e1 && !e2:
return true // s1 empty but s2 is not: s1 is less
}
r1, size1 := utf8.DecodeRuneInString(s1)
r2, size2 := utf8.DecodeRuneInString(s2)
// Check if both are custom, in which case we use custom order:
custom := false
if w1, ok1 := weights[r1]; ok1 {
if w2, ok2 := weights[r2]; ok2 {
custom = true
if w1 != w2 {
return w1 < w2
}
}
}
if !custom {
// Fallback to numeric rune comparison:
if r1 != r2 {
return r1 < r2
}
}
s1, s2 = s1[size1:], s2[size2:]
}
}
让我们看一下此less()
函数的一些琐碎测试:</ p>
pairs := [][2]string{
{"b", "c"},
{"c", "a"},
{"b", "a"},
{"a", "b"},
{"bca", "bac"},
}
for _, pair := range pairs {
fmt.Printf("\"%s\" < \"%s\" ? %t\n", pair[0], pair[1], less(pair[0], pair[1]))
}
输出(在Go Playground上尝试):
"b" < "c" ? true
"c" < "a" ? true
"b" < "a" ? true
"a" < "b" ? false
"bca" < "bac" ? true
现在让我们在实际排序中测试此less()
函数:
ss := []string{
"abc",
"abca",
"abcb",
"abcc",
"bca",
"cba",
"bac",
}
sort.Slice(ss, func(i int, j int) bool {
return less(ss[i], ss[j])
})
fmt.Println(ss)
输出(在Go Playground上尝试):
[bca bac cba abc abcb abcc abca]
同样,如果性能对您很重要,则不应使用sort.Slice()
,因为它必须在幕后使用反射,而应在实现中创建自己的实现sort.Interface
的切片类型您可以不使用反射就能知道如何做到。
它是这样的:
type CustStrSlice []string
func (c CustStrSlice) Len() int { return len(c) }
func (c CustStrSlice) Less(i, j int) bool { return less(c[i], c[j]) }
func (c CustStrSlice) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
当您要使用自定义字母对字符串切片进行排序时,只需将切片转换为CustStrSlice
,以便可以将其直接传递给sort.Sort()
(此类型转换不会复制slice或其元素,它只是更改类型信息):
ss := []string{
"abc",
"abca",
"abcb",
"abcc",
"bca",
"cba",
"bac",
}
sort.Sort(CustStrSlice(ss))
fmt.Println(ss)
以上内容再次输出(在Go Playground上尝试):
[bca bac cba abc abcb abcc abca]
一些注意事项:
默认的字符串比较按字节比较字符串。也就是说,如果输入字符串包含无效的UTF-8序列,则仍将使用实际字节。
在这方面,我们的解决方案有所不同,因为我们对符文进行了解码(我们之所以必须这样做,是因为我们使用了自定义字母,在该字母表中,允许符文不一定以UTF-8编码映射到字节1到1)。这意味着,如果输入不是有效的UTF-8序列,则该行为可能与默认顺序不一致。但是,如果您输入的内容是有效的UTF-8序列,则可以完成您期望的操作。
最后一个注释:
我们已经看到了如何对字符串切片进行自定义排序。如果我们有一个结构切片(或结构指针切片),则排序算法(less()
函数)可能是相同的,但是当比较切片的元素时,我们必须比较元素的字段,而不是struct元素本身。
因此,假设我们具有以下结构:
type Person struct {
Name string
Age int
}
func (p *Person) String() string { return fmt.Sprint(*p) }
(添加了String()
方法,因此我们将看到结构的实际内容,而不仅仅是它们的地址...)
假设我们要使用[]*Person
元素的Name
字段对类型为Person
的切片应用自定义排序。因此,我们只需定义此自定义类型:
type PersonSlice []*Person
func (p PersonSlice) Len() int { return len(p) }
func (p PersonSlice) Less(i, j int) bool { return less(p[i].Name, p[j].Name) }
func (p PersonSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
仅此而已。其余的相同,例如:
ps := []*Person{
{Name: "abc"},
{Name: "abca"},
{Name: "abcb"},
{Name: "abcc"},
{Name: "bca"},
{Name: "cba"},
{Name: "bac"},
}
sort.Sort(PersonSlice(ps))
fmt.Println(ps)
输出(在Go Playground上尝试):
[{bca 0} {bac 0} {cba 0} {abc 0} {abcb 0} {abcc 0} {abca 0}]
答案 1 :(得分:0)
使用 table_test.go
[1] 作为起点,我想出了以下内容。这
Builder.Add
[2] 正在做真正的工作:
package main
import (
"golang.org/x/text/collate"
"golang.org/x/text/collate/build"
)
type entry struct {
r rune
w int
}
func newCollator(ents []entry) (*collate.Collator, error) {
b := build.NewBuilder()
for _, ent := range ents {
err := b.Add([]rune{ent.r}, [][]int{{ent.w}}, nil)
if err != nil { return nil, err }
}
t, err := b.Build()
if err != nil { return nil, err }
return collate.NewFromTable(t), nil
}
结果:
package main
import "fmt"
func main() {
a := []entry{
{'a', 3}, {'b', 2}, {'c', 1},
}
c, err := newCollator(a)
if err != nil {
panic(err)
}
x := []string{"alfa", "bravo", "charlie"}
c.SortStrings(x)
fmt.Println(x) // [charlie bravo alfa]
}