Skip to content

Instantly share code, notes, and snippets.

@ikawaha
Created February 10, 2016 15:09
Show Gist options
  • Save ikawaha/180c8a9b47c8644d11c7 to your computer and use it in GitHub Desktop.
Save ikawaha/180c8a9b47c8644d11c7 to your computer and use it in GitHub Desktop.
Neologd normalizer
package neologd
import (
"bytes"
"strings"
"unicode"
"unicode/utf8"
)
const (
ProlongedSoundMark = '\u30FC'
)
var neologdReplacer = strings.NewReplacer(
"0", "0", "1", "1", "2", "2", "3", "3", "4", "4",
"5", "5", "6", "6", "7", "7", "8", "8", "9", "9",
"A", "A", "B", "B", "C", "C", "D", "D", "E", "E",
"F", "F", "G", "G", "H", "H", "I", "I", "J", "J",
"K", "K", "L", "L", "M", "M", "N", "N", "O", "O",
"P", "P", "Q", "Q", "R", "R", "S", "S", "T", "T",
"U", "U", "V", "V", "W", "W", "X", "X", "Y", "Y",
"Z", "Z",
"a", "a", "b", "b", "c", "c", "d", "d", "e", "e",
"f", "f", "g", "g", "h", "h", "i", "i", "j", "j",
"k", "k", "l", "l", "m", "m", "n", "n", "o", "o",
"p", "p", "q", "q", "r", "r", "s", "s", "t", "t",
"u", "u", "v", "v", "w", "w", "x", "x", "y", "y",
"z", "z",
//small case
"ァ", "ァ", "ィ", "ィ", "ゥ", "ゥ", "ェ", "ェ", "ォ", "ォ",
"ャ", "ャ", "ュ", "ュ", "ョ", "ョ", "ッ", "ッ",
"ア", "ア", "イ", "イ", "ウ", "ウ", "エ", "エ", "オ", "オ",
"ガ", "ガ", "ギ", "ギ", "グ", "グ", "ゲ", "ゲ", "ゴ", "ゴ",
"カ", "カ", "キ", "キ", "ク", "ク", "ケ", "ケ", "コ", "コ",
"ザ", "ザ", "ジ", "ジ", "ズ", "ズ", "ゼ", "ゼ", "ゾ", "ゾ",
"サ", "サ", "シ", "シ", "ス", "ス", "セ", "セ", "ソ", "ソ",
"ダ", "ダ", "ヂ", "ヂ", "ヅ", "ヅ", "デ", "デ", "ド", "ド",
"タ", "タ", "チ", "チ", "ツ", "ツ", "テ", "テ", "ト", "ト",
"ナ", "ナ", "ニ", "ニ", "ヌ", "ヌ", "ネ", "ネ", "ノ", "ノ",
"バ", "バ", "ビ", "ビ", "ブ", "ブ", "ベ", "ベ", "ボ", "ボ",
"パ", "パ", "ピ", "ピ", "プ", "プ", "ペ", "ペ", "ポ", "ポ",
"ハ", "ハ", "ヒ", "ヒ", "フ", "フ", "ヘ", "ヘ", "ホ", "ホ",
"マ", "マ", "ミ", "ミ", "ム", "ム", "メ", "メ", "モ", "モ",
"ヤ", "ヤ", "ユ", "ユ", "ヨ", "ヨ",
"ラ", "ラ", "リ", "リ", "ル", "ル", "レ", "レ", "ロ", "ロ",
"ワ", "ワ", "ヲ", "ヲ", "ン", "ン",
// hyphen
"\u02D7", "-", "\u058A", "-", "\u2010", "-", "\u2011", "-", "\u2012", "-",
"\u2013", "-", "\u2043", "-", "\u207B", "-", "\u208B", "-", "\u2212", "-",
// bar
"\u2014", string(ProlongedSoundMark), // エムダッシュ
"\u2015", string(ProlongedSoundMark), // ホリゾンタルバー
"\u2500", string(ProlongedSoundMark), // 横細罫線
"\u2501", string(ProlongedSoundMark), // 横太罫線
"\uFE63", string(ProlongedSoundMark), // SMALL HYPHEN-MINUS
"\uFF0D", string(ProlongedSoundMark), // 全角ハイフンマイナス
"\uFF70", string(ProlongedSoundMark), // 半角長音記号
// tilde
"~", "", "\u223C", "", "\u223E", "", "\u301C", "", "\u3030", "", "\uFF5E", "",
// zen -> han
"!", "!", "”", `"`, "#", "#", "$", "$", "%", "%",
"&", "&", `’`, `'`, "(", "(", ")", ")", "*", "*",
"+", "+", ",", ",", "−", "-", ".", ".", "/", "/",
":", ":", ";", ";", "<", "<", ">", ">", "?", "?",
"@", "@", "[", "[", "¥", "\u00A5", "]", "]", "^", "^",
"_", "_", "`", "`", "{", "{", "|", "|", "}", "}",
" ", " ",
// han -> zen
"。", "。", "、", "、", "・", "・", "=", "=", "「", "「", "」", "」",
)
type NeologdNormalizer struct {
replacer *strings.Replacer
}
func NewNeologdNormalizer() *NeologdNormalizer {
return &NeologdNormalizer{
replacer: neologdReplacer,
}
}
func (n NeologdNormalizer) Normalize(s string) string {
return n.EliminateSpace(
n.ShurinkProlongedSoundMark(
n.CharReplace(s)))
}
func (n NeologdNormalizer) CharReplace(s string) string {
return n.replacer.Replace(s)
}
func (n NeologdNormalizer) ShurinkProlongedSoundMark(s string) string {
var b bytes.Buffer
for p := 0; p < len(s); {
c, w := utf8.DecodeRuneInString(s[p:])
p += w
b.WriteRune(c)
if c != ProlongedSoundMark {
continue
}
for p < len(s) {
c0, w0 := utf8.DecodeRuneInString(s[p:])
p += w0
if c0 != ProlongedSoundMark {
b.WriteRune(c0)
break
}
}
}
return b.String()
}
func (n NeologdNormalizer) EliminateSpace(s string) string {
var (
b bytes.Buffer
prev rune
)
for p := 0; p < len(s); {
c, w := utf8.DecodeRuneInString(s[p:])
p += w
if !unicode.IsSpace(c) {
prev = c
b.WriteRune(c)
continue
}
for p < len(s) {
c0, w0 := utf8.DecodeRuneInString(s[p:])
p += w0
if !unicode.IsSpace(c0) {
if unicode.In(prev, unicode.Latin) && unicode.In(c0, unicode.Latin) {
b.WriteRune(' ')
}
prev = c0
b.WriteRune(c0)
break
}
}
}
return b.String()
}
package neologd
import (
"testing"
)
func TestNormalize(t *testing.T) {
type testData struct {
in, out string
}
data := []testData{
{
in: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
out: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
},
{
in: "abcdefghijklmnopqrstuvwxyz",
out: "abcdefghijklmnopqrstuvwxyz",
},
{
in: "!”#$%&’()*+,−./:;<>?@[¥]^_`{|}",
out: "!\"#$%&'()*+,-./:;<>?@[¥]^_`{|}",
},
{
in: "=。、・「」",
out: "=。、・「」",
},
{
in: "ハンカク",
out: "ハンカク",
},
{
in: "o₋o",
out: "o-o",
},
{
in: "majika━",
out: "majikaー",
},
{
in: "わ〰い",
out: "わい",
},
{
in: "スーパーーーー",
out: "スーパー",
},
{
in: "!#",
out: "!#",
},
{
in: "ゼンカク スペース",
out: "ゼンカクスペース",
},
{
in: "お お",
out: "おお",
},
{
in: " おお",
out: "おお",
},
{
in: "おお ",
out: "おお",
},
{
in: "検索 エンジン 自作 入門 を 買い ました!!!",
out: "検索エンジン自作入門を買いました!!!",
},
{
in: "アルゴリズム C",
out: "アルゴリズムC",
},
{
in: "   PRML  副 読 本   ",
out: "PRML副読本",
},
{
in: "Coding the Matrix",
out: "Coding the Matrix",
},
{
in: "南アルプスの 天然水 Sparking Lemon レモン一絞り",
out: "南アルプスの天然水Sparking Lemonレモン一絞り",
},
}
n := NewNeologdNormalizer()
for _, d := range data {
if x := n.Normalize(d.in); x != d.out {
t.Errorf("got %v, expected %v", x, d.out)
}
}
}
func TestCharReplace(t *testing.T) {
type testData struct {
in, out string
}
data := []testData{
{
in: "0123456789",
out: "0123456789",
},
{
in: "abcdefghijklmnopqrstuvwxyz",
out: "abcdefghijklmnopqrstuvwxyz",
},
{
in: "ガギグゲゴカキクケコパピプペポバビブベボ",
out: "ガギグゲゴカキクケコパピプペポバビブベボ",
},
{
in: "ァィゥェォャュョッ",
out: "ァィゥェォャュョッ",
},
{
in: " ",
out: " ",
},
}
n := NewNeologdNormalizer()
for _, d := range data {
if x := n.CharReplace(d.in); x != d.out {
t.Errorf("got %v, expected %v", x, d.out)
}
}
}
func TestHyphen(t *testing.T) {
l := "\u02D7\u058A\u2010\u2011\u2012\u2013\u2043\u207B\u208B\u2212"
n := NewNeologdNormalizer()
for _, c := range l {
if n.CharReplace(string(c)) != "-" {
t.Errorf("got 0x%X, expected 0x%X", c, '-')
}
}
}
func TestBar(t *testing.T) {
l := "\u2014\u2015\u2500\uFE63\uFF0D\uFF70\u30FC"
n := NewNeologdNormalizer()
for _, c := range l {
if n.CharReplace(string(c)) != "ー" {
t.Errorf("got 0x%X, expected 0x%X", c, '\u30FC')
}
}
}
func TestShrinkProlongedSoundMark(t *testing.T) {
type testData struct {
in, out string
}
data := []testData{
{
in: "スーーーーーーパーーーーーー",
out: "スーパー",
},
{
in: "スーパーーーーーー",
out: "スーパー",
},
}
n := NewNeologdNormalizer()
for _, d := range data {
if x := n.ShurinkProlongedSoundMark(d.in); x != d.out {
t.Errorf("got %v, expected %v", x, d.out)
}
}
}
func TestTilde(t *testing.T) {
l := "~∼∾〜〰~"
n := NewNeologdNormalizer()
for _, c := range l {
if n.CharReplace(string(c)) != "" {
t.Errorf("got 0x%X, expected empty", c)
}
}
}
func TestEliminateSpace(t *testing.T) {
type testData struct {
in, out string
}
data := []testData{
{
in: " abc ",
out: "abc",
},
{
in: "検索 エンジン 自作 入門 を 買い ました!!!",
out: "検索エンジン自作入門を買いました!!!",
},
{
in: "アルゴリズム C",
out: "アルゴリズムC",
},
{
in: "   PRML  副 読 本   ",
out: "PRML副読本",
},
{
in: "Coding the Matrix",
out: "Coding the Matrix",
},
{
in: "南アルプスの 天然水 Sparking Lemon レモン一絞り",
out: "南アルプスの天然水Sparking Lemonレモン一絞り",
},
}
n := NewNeologdNormalizer()
for _, d := range data {
if x := n.EliminateSpace(d.in); x != d.out {
t.Errorf("got %v, expected %v", x, d.out)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment