かなカナ変換
今回は仮名文字を色々と変換することを考えてみる。
全角⇔半角 変換
いわゆる全角文字と半角文字の変換は golang.org/x/text/
width
パッケージを使えばいいのだが,仮名文字の場合は少しクセがある。
たとえば以下のように「ペンギン」を半角カナに変換しようとすると
package main
import (
"fmt"
"golang.org/x/text/width"
)
func unicodePrint(s string) {
sep := ""
for _, r := range s {
fmt.Printf("%s{%#U}", sep, r)
sep = " "
}
fmt.Println()
}
func main() {
fullwidth := "ペンギン"
unicodePrint(fullwidth)
unicodePrint(width.Narrow.String(fullwidth))
}
実行結果は
$ go run sample1.go
{U+30DA 'ペ'} {U+30F3 'ン'} {U+30AE 'ギ'} {U+30F3 'ン'}
{U+30DA 'ペ'} {U+FF9D 'ン'} {U+30AE 'ギ'} {U+FF9D 'ン'}
のように濁点・半濁点を上手く処理できないようだ。
これを解消するには,安直な手段だが,いったん NFD 正規化で合成列に変換してから変換するとよい。
func main() {
fullwidth := "ペンギン"
unicodePrint(fullwidth)
unicodePrint(width.Narrow.String(norm.NFD.String(fullwidth)))
}
これで実行すると
$ go run sample1b.go
{U+30DA 'ペ'} {U+30F3 'ン'} {U+30AE 'ギ'} {U+30F3 'ン'}
{U+FF8D 'ヘ'} {U+FF9F '゚'} {U+FF9D 'ン'} {U+FF77 'キ'} {U+FF9E '゙'} {U+FF9D 'ン'}
と,綺麗に変換してくれる1。
逆に golang.org/x/text/
width
パッケージで半角カナから全角カナに変換しようとすると
func main() {
fullwidth := "ペンギン"
halfwidth := width.Narrow.String(norm.NFD.String(fullwidth))
unicodePrint(fullwidth)
unicodePrint(halfwidth)
unicodePrint(width.Widen.String(halfwidth))
}
実行結果は当然ながら
$ go run sample1c.go
{U+30DA 'ペ'} {U+30F3 'ン'} {U+30AE 'ギ'} {U+30F3 'ン'}
{U+FF8D 'ヘ'} {U+FF9F '゚'} {U+FF9D 'ン'} {U+FF77 'キ'} {U+FF9E '゙'} {U+FF9D 'ン'}
{U+30D8 'ヘ'} {U+309A '゚'} {U+30F3 'ン'} {U+30AD 'キ'} {U+3099 '゙'} {U+30F3 'ン'}
合成列となる。 これを事前合成形にするのであれば
func main() {
fullwidth := "ペンギン"
halfwidth := width.Narrow.String(norm.NFD.String(fullwidth))
unicodePrint(fullwidth)
unicodePrint(halfwidth)
unicodePrint(norm.NFC.String(width.Widen.String(halfwidth)))
}
と NFC 正規化をすることで
$ go run sample1c.go
{U+30DA 'ペ'} {U+30F3 'ン'} {U+30AE 'ギ'} {U+30F3 'ン'}
{U+FF8D 'ヘ'} {U+FF9F '゚'} {U+FF9D 'ン'} {U+FF77 'キ'} {U+FF9E '゙'} {U+FF9D 'ン'}
{U+30DA 'ペ'} {U+30F3 'ン'} {U+30AE 'ギ'} {U+30F3 'ン'}
とできる1。
ついでに, 'ヰ'
, 'ヱ'
文字, 'ヽ'
, 'ヾ'
といった繰り返し記号,あるいは 'ヵ'
, 'ヶ'
といった拗音の一部は半角カナにはないので,これらを含む文字列の変換には注意が必要である。
ひらがな⇔カタカナ 変換
Go 言語では,ひらがなとカタカナを相互変換するパッケージは標準では用意されていないので自作する必要がある。
まずは,ひらがなとカタカナの Unicode 符号点を眺めてみる2。
+0 | +1 | +2 | +3 | +4 | +5 | +6 | +7 | +8 | +9 | +A | +B | +C | +D | +E | +E | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
U+3040 | ぁ | あ | ぃ | い | ぅ | う | ぇ | え | ぉ | お | か | が | き | ぎ | く | |
U+3050 | ぐ | け | げ | こ | ご | さ | ざ | し | じ | す | ず | せ | ぜ | そ | ぞ | た |
U+3060 | だ | ち | ぢ | っ | つ | づ | て | で | と | ど | な | に | ぬ | ね | の | は |
U+3070 | ば | ぱ | ひ | び | ぴ | ふ | ぶ | ぷ | へ | べ | ぺ | ほ | ぼ | ぽ | ま | み |
U+3080 | む | め | も | ゃ | や | ゅ | ゆ | ょ | よ | ら | り | る | れ | ろ | ゎ | わ |
U+3090 | ゐ | ゑ | を | ん | ゔ | ゕ | ゖ | ゙ | ゚ | ゛ | ゜ | ゝ | ゞ |
+0 | +1 | +2 | +3 | +4 | +5 | +6 | +7 | +8 | +9 | +A | +B | +C | +D | +E | +E | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
U+30A0 | ァ | ア | ィ | イ | ゥ | ウ | ェ | エ | ォ | オ | カ | ガ | キ | ギ | ク | |
U+30B0 | グ | ケ | ゲ | コ | ゴ | サ | ザ | シ | ジ | ス | ズ | セ | ゼ | ソ | ゾ | タ |
U+30C0 | ダ | チ | ヂ | ッ | ツ | ヅ | テ | デ | ト | ド | ナ | ニ | ヌ | ネ | ノ | ハ |
U+30D0 | バ | パ | ヒ | ビ | ピ | フ | ブ | プ | ヘ | ベ | ペ | ホ | ボ | ポ | マ | ミ |
U+30E0 | ム | メ | モ | ャ | ヤ | ュ | ユ | ョ | ヨ | ラ | リ | ル | レ | ロ | ヮ | ワ |
U+30F0 | ヰ | ヱ | ヲ | ン | ヴ | ヵ | ヶ | ヷ | ヸ | ヹ | ヺ | ・ | ー | ヽ | ヾ |
これを見るとひらがなの U+3041 〜 U+3096 と片仮名の U+30A1 〜 U+30F6 の領域が1対1で対応していることが分かる。
繰り返し記号の 'ゝ'
, 'ゞ'
, 'ヽ'
, 'ヾ'
も同様。
これなら標準の strings
パッケージを使って変換できそうだ。
たとえば strings
.ToUpperSpecial()
関数でひらがな→カタカナ変換を, strings
.ToLowerSpecial()
関数でカタカナ→ひらがな変換を行うように unicode
.SpecialCase
の値を設定すればよい。
unicode
.SpecialCase
構造体の定義はこうなっている。
// CaseRange represents a range of Unicode code points for simple (one
// code point to one code point) case conversion.
// The range runs from Lo to Hi inclusive, with a fixed stride of 1. Deltas
// are the number to add to the code point to reach the code point for a
// different case for that character. They may be negative. If zero, it
// means the character is in the corresponding case. There is a special
// case representing sequences of alternating corresponding Upper and Lower
// pairs. It appears with a fixed Delta of
// {UpperLower, UpperLower, UpperLower}
// The constant UpperLower has an otherwise impossible delta value.
type CaseRange struct {
Lo uint32
Hi uint32
Delta d
}
// SpecialCase represents language-specific case mappings such as Turkish.
// Methods of SpecialCase customize (by overriding) the standard mappings.
type SpecialCase []CaseRange
// BUG(r): There is no mechanism for full case folding, that is, for
// characters that involve multiple runes in the input or output.
// Indices into the Delta arrays inside CaseRanges for case mapping.
const (
UpperCase = iota
LowerCase
TitleCase
MaxCase
)
type d [MaxCase]rune // to make the CaseRanges text shorter
ほんじゃあ,さくっとコードを書いてみよう。 こんな感じかな。
package main
import (
"fmt"
"strings"
"unicode"
)
var kanaCase = unicode.SpecialCase{
unicode.CaseRange{'ぁ', 'ゖ', [unicode.MaxCase]rune{'ァ' - 'ぁ', 0, 0}},
unicode.CaseRange{'ゝ', 'ゞ', [unicode.MaxCase]rune{'ヽ' - 'ゝ', 0, 0}},
unicode.CaseRange{'ァ', 'ヶ', [unicode.MaxCase]rune{0, 'ぁ' - 'ァ', 0}},
unicode.CaseRange{'ヽ', 'ヾ', [unicode.MaxCase]rune{0, 'ゝ' - 'ヽ', 0}},
}
func main() {
kana := "あいうえおわゐゑをんゔゕゖゝゞアイウエオワヰヱヲンヴヵヶヽヾ"
fmt.Println(strings.ToUpperSpecial(kanaCase, kana))
fmt.Println(strings.ToLowerSpecial(kanaCase, kana))
}
この実行結果は
$ go run sample2.go
アイウエオワヰヱヲンヴヵヶヽヾアイウエオワヰヱヲンヴヵヶヽヾ
あいうえおわゐゑをんゔゕゖゝゞあいうえおわゐゑをんゔゕゖゝゞ
となる。 よーし,うむうむ,よーし。
なお,カタカナの 'ヷ'
, 'ヸ'
, 'ヹ'
, 'ヺ'
文字は対応する事前合成形のひらがな文字がないため,必要なら個別に処理する必要がある。
拗音・促音⇔直音 変換
'ゃ'
, 'ゅ'
, 'ょ'
など)や促音(小さい 'っ'
)と直音('や'
, 'ゆ'
, 'よ'
, 'つ'
)とを相互変換することを考える。
この機能も標準では用意されてないけど,前節と同じように unicode
.SpecialCase
の値を決めればいいかな。
長いコードだけど,ご容赦。
package main
import (
"fmt"
"sort"
"strings"
"unicode"
)
var kanaCase2 = unicode.SpecialCase{
unicode.CaseRange{'あ', 'あ', [unicode.MaxCase]rune{0, 'ぁ' - 'あ', 0}},
unicode.CaseRange{'い', 'い', [unicode.MaxCase]rune{0, 'ぃ' - 'い', 0}},
unicode.CaseRange{'う', 'う', [unicode.MaxCase]rune{0, 'ぅ' - 'う', 0}},
unicode.CaseRange{'え', 'え', [unicode.MaxCase]rune{0, 'ぇ' - 'え', 0}},
unicode.CaseRange{'お', 'お', [unicode.MaxCase]rune{0, 'ぉ' - 'お', 0}},
unicode.CaseRange{'か', 'か', [unicode.MaxCase]rune{0, 'ゕ' - 'か', 0}},
unicode.CaseRange{'け', 'け', [unicode.MaxCase]rune{0, 'ゖ' - 'け', 0}},
unicode.CaseRange{'つ', 'つ', [unicode.MaxCase]rune{0, 'っ' - 'つ', 0}},
unicode.CaseRange{'や', 'や', [unicode.MaxCase]rune{0, 'ゃ' - 'や', 0}},
unicode.CaseRange{'ゆ', 'ゆ', [unicode.MaxCase]rune{0, 'ゅ' - 'ゆ', 0}},
unicode.CaseRange{'よ', 'よ', [unicode.MaxCase]rune{0, 'ょ' - 'よ', 0}},
unicode.CaseRange{'わ', 'わ', [unicode.MaxCase]rune{0, 'ゎ' - 'わ', 0}},
unicode.CaseRange{'ぁ', 'ぁ', [unicode.MaxCase]rune{'あ' - 'ぁ', 0, 0}},
unicode.CaseRange{'ぃ', 'ぃ', [unicode.MaxCase]rune{'い' - 'ぃ', 0, 0}},
unicode.CaseRange{'ぅ', 'ぅ', [unicode.MaxCase]rune{'う' - 'ぅ', 0, 0}},
unicode.CaseRange{'ぇ', 'ぇ', [unicode.MaxCase]rune{'え' - 'ぇ', 0, 0}},
unicode.CaseRange{'ぉ', 'ぉ', [unicode.MaxCase]rune{'お' - 'ぉ', 0, 0}},
unicode.CaseRange{'ゕ', 'ゕ', [unicode.MaxCase]rune{'か' - 'ゕ', 0, 0}},
unicode.CaseRange{'ゖ', 'ゖ', [unicode.MaxCase]rune{'け' - 'ゖ', 0, 0}},
unicode.CaseRange{'っ', 'っ', [unicode.MaxCase]rune{'つ' - 'っ', 0, 0}},
unicode.CaseRange{'ゃ', 'ゃ', [unicode.MaxCase]rune{'や' - 'ゃ', 0, 0}},
unicode.CaseRange{'ゅ', 'ゅ', [unicode.MaxCase]rune{'ゆ' - 'ゅ', 0, 0}},
unicode.CaseRange{'ょ', 'ょ', [unicode.MaxCase]rune{'よ' - 'ょ', 0, 0}},
unicode.CaseRange{'ゎ', 'ゎ', [unicode.MaxCase]rune{'わ' - 'ゎ', 0, 0}},
unicode.CaseRange{'ア', 'ア', [unicode.MaxCase]rune{0, 'ァ' - 'ア', 0}},
unicode.CaseRange{'イ', 'イ', [unicode.MaxCase]rune{0, 'ィ' - 'イ', 0}},
unicode.CaseRange{'ウ', 'ウ', [unicode.MaxCase]rune{0, 'ゥ' - 'ウ', 0}},
unicode.CaseRange{'エ', 'エ', [unicode.MaxCase]rune{0, 'ェ' - 'エ', 0}},
unicode.CaseRange{'オ', 'オ', [unicode.MaxCase]rune{0, 'ォ' - 'オ', 0}},
unicode.CaseRange{'カ', 'カ', [unicode.MaxCase]rune{0, 'ヵ' - 'カ', 0}},
unicode.CaseRange{'ケ', 'ケ', [unicode.MaxCase]rune{0, 'ヶ' - 'ケ', 0}},
unicode.CaseRange{'ツ', 'ツ', [unicode.MaxCase]rune{0, 'ッ' - 'ツ', 0}},
unicode.CaseRange{'ヤ', 'ヤ', [unicode.MaxCase]rune{0, 'ャ' - 'ヤ', 0}},
unicode.CaseRange{'ユ', 'ユ', [unicode.MaxCase]rune{0, 'ュ' - 'ユ', 0}},
unicode.CaseRange{'ヨ', 'ヨ', [unicode.MaxCase]rune{0, 'ョ' - 'ヨ', 0}},
unicode.CaseRange{'ワ', 'ワ', [unicode.MaxCase]rune{0, 'ヮ' - 'ワ', 0}},
unicode.CaseRange{'ァ', 'ァ', [unicode.MaxCase]rune{'ア' - 'ァ', 0, 0}},
unicode.CaseRange{'ィ', 'ィ', [unicode.MaxCase]rune{'イ' - 'ィ', 0, 0}},
unicode.CaseRange{'ゥ', 'ゥ', [unicode.MaxCase]rune{'ウ' - 'ゥ', 0, 0}},
unicode.CaseRange{'ェ', 'ェ', [unicode.MaxCase]rune{'エ' - 'ェ', 0, 0}},
unicode.CaseRange{'ォ', 'ォ', [unicode.MaxCase]rune{'オ' - 'ォ', 0, 0}},
unicode.CaseRange{'ヵ', 'ヵ', [unicode.MaxCase]rune{'カ' - 'ヵ', 0, 0}},
unicode.CaseRange{'ヶ', 'ヶ', [unicode.MaxCase]rune{'ケ' - 'ヶ', 0, 0}},
unicode.CaseRange{'ッ', 'ッ', [unicode.MaxCase]rune{'ツ' - 'ッ', 0, 0}},
unicode.CaseRange{'ャ', 'ャ', [unicode.MaxCase]rune{'ヤ' - 'ャ', 0, 0}},
unicode.CaseRange{'ュ', 'ュ', [unicode.MaxCase]rune{'ユ' - 'ュ', 0, 0}},
unicode.CaseRange{'ョ', 'ョ', [unicode.MaxCase]rune{'ヨ' - 'ョ', 0, 0}},
unicode.CaseRange{'ヮ', 'ヮ', [unicode.MaxCase]rune{'ワ' - 'ヮ', 0, 0}},
unicode.CaseRange{'ア', 'ア', [unicode.MaxCase]rune{0, 'ァ' - 'ア', 0}},
unicode.CaseRange{'イ', 'イ', [unicode.MaxCase]rune{0, 'ィ' - 'イ', 0}},
unicode.CaseRange{'ウ', 'ウ', [unicode.MaxCase]rune{0, 'ゥ' - 'ウ', 0}},
unicode.CaseRange{'エ', 'エ', [unicode.MaxCase]rune{0, 'ェ' - 'エ', 0}},
unicode.CaseRange{'オ', 'オ', [unicode.MaxCase]rune{0, 'ォ' - 'オ', 0}},
unicode.CaseRange{'ツ', 'ツ', [unicode.MaxCase]rune{0, 'ッ' - 'ツ', 0}},
unicode.CaseRange{'ヤ', 'ヤ', [unicode.MaxCase]rune{0, 'ャ' - 'ヤ', 0}},
unicode.CaseRange{'ユ', 'ユ', [unicode.MaxCase]rune{0, 'ュ' - 'ユ', 0}},
unicode.CaseRange{'ヨ', 'ヨ', [unicode.MaxCase]rune{0, 'ョ' - 'ヨ', 0}},
unicode.CaseRange{'ァ', 'ァ', [unicode.MaxCase]rune{'ア' - 'ァ', 0, 0}},
unicode.CaseRange{'ィ', 'ィ', [unicode.MaxCase]rune{'イ' - 'ィ', 0, 0}},
unicode.CaseRange{'ゥ', 'ゥ', [unicode.MaxCase]rune{'ウ' - 'ゥ', 0, 0}},
unicode.CaseRange{'ェ', 'ェ', [unicode.MaxCase]rune{'エ' - 'ェ', 0, 0}},
unicode.CaseRange{'ォ', 'ォ', [unicode.MaxCase]rune{'オ' - 'ォ', 0, 0}},
unicode.CaseRange{'ッ', 'ッ', [unicode.MaxCase]rune{'ツ' - 'ッ', 0, 0}},
unicode.CaseRange{'ャ', 'ャ', [unicode.MaxCase]rune{'ヤ' - 'ャ', 0, 0}},
unicode.CaseRange{'ュ', 'ュ', [unicode.MaxCase]rune{'ユ' - 'ュ', 0, 0}},
unicode.CaseRange{'ョ', 'ョ', [unicode.MaxCase]rune{'ヨ' - 'ョ', 0, 0}},
}
func init() {
sort.Slice(kanaCase2, func(i, j int) bool {
return kanaCase2[i].Lo < kanaCase2[j].Lo
})
}
func main() {
kanaLower := "ぁぃぅぇぉゕゖっゃゅょゎ ァィゥェォヵヶッャュョヮ ァィゥェォッャュョ"
kanaUpper := strings.ToUpperSpecial(kanaCase2, kanaLower)
fmt.Println(kanaLower)
fmt.Println(kanaUpper)
fmt.Println(strings.ToLowerSpecial(kanaCase2, kanaUpper))
}
これを実行すると
$ go run sample3.go
ぁぃぅぇぉゕゖっゃゅょゎ ァィゥェォヵヶッャュョヮ ァィゥェォッャュョ
あいうえおかけつやゆよわ アイウエオカケツヤユヨワ アイウエオツヤユヨ
ぁぃぅぇぉゕゖっゃゅょゎ ァィゥェォヵヶッャュョヮ ァィゥェォッャュョ
となる。
ブックマーク
- Go言語で文字列の変換(全角・半角、ひらがな・カタカナ)をする : Serendip – Webデザイン・プログラミング
- Go 言語と Unicode 正規化
- Go 言語による Unicode 半角/全角変換
参考図書
- プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
- Alan A.A. Donovan (著), Brian W. Kernighan (著), 柴田 芳樹 (翻訳)
- 丸善出版 2016-06-20
- 単行本(ソフトカバー)
- 4621300253 (ASIN), 9784621300251 (EAN), 4621300253 (ISBN), 9784621300251 (ISBN)
- 評価
著者のひとりは(あの「バイブル」とも呼ばれる)通称 “K&R” の K のほうである。この本は Go 言語の教科書と言ってもいいだろう。
-
Unicode 正規化には副作用があり,実際のところコード変換用途にはお勧めできない。詳しくは拙文「Go 言語と Unicode 正規化」を参照のこと。 ↩︎ ↩︎
-
'ゟ'
や'ヿ'
といった合略仮名文字については,今回は無視する。片仮名拡張や仮名補助も同様に扱わない。 ↩︎