[Golang] 組み合わせ絵文字を含んだ文字列の反転
解決したいこと
"
2️⃣
"のようなrune
時に[50, 65039, 8419]
となる組み合わせ絵文字を1つの文字として認識させたい。
- OS: 不問、Go: v1.15、質問者 Go 暦: 初学(配列とスライスの違いがやっとわかった程度)
- 解決済み → TL; DR
やりたいこと
hoge
を egoh
のように、Go 言語で UTF-8 文字列を反転させる(逆順にする)だけの関数を作りたいのですが、emoji
や組み合わせ絵文字を含んだ場合でも動作するようにしたいのです。
つまり 🙏2️⃣👌👁
を 👁👌2️⃣🙏
の逆順にしたいと言うことです。しかし、何をしても 👁👌⃣️2🙏
と崩れてしまいます。
「golang 文字列 反転」でググってみたのですが、問題を解決する Qiita 記事が見つけられませんでした。UTF-8 の可変バイト単位でなく Unicode のコードポイント単位かもしれないと、薄ら感じてはいるのですが、、、
パッケージを使うにしても、この関数のためだけにと言うジレンマがあり、できれば Pure Go で実装したいのですが煩雑になりそうな気がしています。しかし「実は...簡単な解決方法が...あるのでは?」と Qiita の GURU に Qiita 方が良いと思い質問してみました。
目から鱗な、スマートな解決方法があった場合は、別途 Qiita 記事にしたいと思いますので、アドバイスよろしくお願いします。の回答をいただいたので、質問の文末に TL; DR でまとめました。
問題のコード
Qiita でググったところ、UTF-8 の文字コードは可変長であるため UTF-8/マルチバイト文字列を扱う時は rune
で変換しておくのが定石と言うことで、下記のようなコードを組みました。
func reverseString(input string) (result string) {
var strRuned = []rune(input)
for _, v := range strRuned {
result = string(v) + result
}
return
}
上記は、1〜4 バイトの「単体で完結する文字」の場合は問題ないのですが、"2️⃣
"のような rune
時に [50, 65039, 8419]
となる組み合わせ絵文字(?)の場合に失敗します。
発生している問題・エラー
下記の簡易テストを実行すると、最後の「🙏2️⃣👌👁
」で反転に失敗します。
func main(){
fmt.Println("Go version :", runtime.Version())
// test(<input>, <expect>)
test("abcdefg", "gfedcba") // -> OK
test("表示機能テスト", "トステ能機示表") // -> OK
test("🙏🙇👉🎁", "🎁👉🙇🙏") // -> OK
test("🙏2️⃣👌👁", "👁👌️2️⃣🙏") // -> NG
}
- オンラインで実行結果をみる @ paiza.IO
$ go run sample.go
[OK] Input:abcdefg -> Output:gfedcba
[OK] Input:表示機能テスト -> Output:トステ能機示表
[OK] Input:🙏🙇👉🎁 -> Output:🎁👉🙇🙏
[ng] Input:🙏2️⃣👌👁 -> Output:👁👌⃣️2🙏 Expect:👁👌2️⃣🙏
- Input : []int32{128591, 50, 65039, 8419, 128076, 128065}
- Expect: []int32{128065, 128076, 50, 65039, 8419, 128591}
- Actual: []int32{128065, 128076, 8419, 65039, 50, 128591}
失敗したテストの値をみると「🙏2️⃣👌👁
」を rune
した際の 128591, 50, 65039, 8419, 128076, 128065
を4つのチャンク、つまり「128591
」「50, 65039, 8419
」「128076
」「128065
」と4つに分けられないため、スライスごとに反転してしまいます。そのためデータとしては当然の結果なのですが、意図する結果ではありません。
自分で試したこと(全て NG)
func reverseString(input string) (result string) {
for _, val := range input {
result = string([]rune{val}) + result
}
return
}
- ▲ 実行結果 → NG @ paiza.IO
import "unicode/utf8"
func reverseString(input string) (result string) {
var chunk = input
for len(chunk) > 0 {
v, size := utf8.DecodeRuneInString(chunk)
result = string(v) + result
chunk = chunk[size:]
}
return
}
- ▲ 実行結果 → NG @ paiza.IO
func reverseString(input string) string {
r := []rune(input)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
- ▲ 実行結果 → NG @ paiza.IO
- How to reverse a string in Go? @ StackOverflow
func reverseString(input string) (result string) {
for _, v := range input {
defer func(runed rune) { result += string(runed) }(v)
}
return
}
- ▲ 実行結果 → NG @ paiza.IO
- How to reverse a string in Go? @ StackOverflow
参考にした文献
- Rune literals | spec | ref @ golang.org
- Goで文字列を逆順にする方法とイミュータブルについて @ Qiita
- スライス | 他言語プログラマがgolangの基本を押さえる為のまとめ @ Qiita
- Goで文字列中に4バイト文字があるか探したい @ Qiita
- [The Go Programming Language Specification] Rune @ Qiita
- Go で UTF-8 の文字列を扱う @ Qiita
- How to reverse a string in Go? @ StackOverflow
TL; DR(解決した方法)
"
2️⃣
"のようなrune
時に[50, 65039, 8419]
となる組み合わせ絵文字を1つの文字として認識させたい。
Pure Go で書く方法も StackOverflow で見つけたのですが、回答にあったパッケージを使ったスマートな方法を採用することにしました。
- 人間にとって自然な1文字に見えるコードポイントの並びを書記素クラスタ(
grapheme cluster
)と呼ぶ。 - 書記素クラスタの境界でコードポイント列を区切るアルゴリズムは Unicode Text Segmentation という仕様で定められている。
- このアルゴリズムを使って文字列を分割し、それを反転・結合すればやりたいことを達成できそう。
- 変換テーブルがそれなりに巨大、Unicode のバージョンアップでアルゴリズムが修正されたりもするので、自前で実装するより専用のパッケージを使った方がいいでしょう。
Go ならこれがいいと思います
https://github.com/rivo/uniseg
import "github.com/rivo/uniseg"
func reverseString(input string) (result string) {
var grClusters = uniseg.NewGraphemes(input)
for grClusters.Next() {
result = string(grClusters.Runes()) + result
}
return
}
- オンラインで動作をみる @ paiza.IO
いささか冗長な書き方
Qiita 記事ではないのですが、StackOverflow にビルトイン・パッケージのみで解決する方法もありました。
反転するためだけに使うのは、いささか冗長なので回答にあった外部パッケージを使うことにしました。
どうやら、スライスごとに unicode.Is()
で変換可能か確認しながら、別のスライスに変換可能な単位(おそらく書記素クラスタ毎のブロック)で代入し、最後にフリップさせて文字列として結合しているようです。何をしているか解析仕切れていないのですが、語学のため理解次第 Qiita 記事にしたいと思います。
import (
"unicode"
)
func reverseString(input string) string {
textRunes := []rune(input)
textRunesLength := len(textRunes)
if textRunesLength <= 1 {
return input
}
i, j := 0, 0
for i < textRunesLength && j < textRunesLength {
j = i + 1
for j < textRunesLength && isMark(textRunes[j]) {
j++
}
if isMark(textRunes[j-1]) {
// Reverses Combined Characters
reverse(textRunes[i:j], j-i)
}
i = j
}
// Reverses the entire array
reverse(textRunes, textRunesLength)
return string(textRunes)
}
func reverse(runes []rune, length int) {
for i, j := 0, length-1; i < length/2; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
}
// isMark determines whether the rune is a marker
func isMark(r rune) bool {
return unicode.Is(unicode.Mn, r) || unicode.Is(unicode.Me, r) || unicode.Is(unicode.Mc, r)
}
- オンラインで動作をみる @ paiza.IO
- https://stackoverflow.com/a/44350406/8367711 @ StackOverflow