diff --git a/.gitignore b/.gitignore index 3b735ec..627108c 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,9 @@ # Go workspace file go.work + +# MacOS +.DS_Store + +# json folder +/data diff --git a/README.md b/README.md new file mode 100644 index 0000000..537759a --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +
+ +

EMOZI

+参考古埃及圣书体设计的一种基于颜文字的汉字抽象转写法
🐑🚬🧗👤🕸️😐🧗✍️👈🌞😨🏠🌹🧦😨👥🌹🔐😨💦⬅️☀️😨🏡💦💡🍉🌱🍵💡🧗🪓🍆👔😨🐶

+ +
+ +
+ +W.I.P. + +## 实用工具 +### 拼音识别拆分 +将带声调的拼音拆分为以国际音标表示的声母韵母。 +```go +s, y, t, err := emozi.SplitPinyin("jiǒng") +if err != nil { + panic(err) +} +fmt.Println(s, y, tone) // tɕ i̯ʊŋ 上声 +``` diff --git a/codegen/radical/main.go b/codegen/radical/main.go new file mode 100644 index 0000000..29f929f --- /dev/null +++ b/codegen/radical/main.go @@ -0,0 +1,54 @@ +package main + +import ( + "html/template" + "os" + "time" + + sql "github.com/FloatTech/sqlite" + "github.com/fumiama/emozi" +) + +const head = `// Code generated by codegen/radical. 已经经过修改, 不要手动重新运行. + +package emozi + +// 部首后备 内嵌的部首到颜文字的映射, 是第二优先查询顺位. 第一顺位是数据库的部首表. 第三位是回落到 🈳️. +var 部首后备 = map[rune]string{ {{range .}} + '{{.R}}': "🈳️",{{end}} +} +` + +func main() { + db := sql.Sqlite{DBPath: emozi.EmoziDatabasePath} + err := db.Open(time.Minute) + if err != nil { + panic(err) + } + defer db.Close() + type r struct { + R rune + } + type sr struct { + R string + } + var s r + var ss = []sr{} + db.QueryFor("SELECT DISTINCT R FROM emozi;", &s, func() error { + ss = append(ss, sr{R: string(s.R)}) + return nil + }) + t, err := template.New("list").Parse(head) + if err != nil { + panic(err) + } + f, err := os.Create("radical.go") + if err != nil { + panic(err) + } + defer f.Close() + err = t.Execute(f, ss) + if err != nil { + panic(err) + } +} diff --git a/coder.go b/coder.go new file mode 100644 index 0000000..9bfd272 --- /dev/null +++ b/coder.go @@ -0,0 +1,178 @@ +package emozi + +import ( + "errors" + "strconv" + "strings" + "time" + + sql "github.com/FloatTech/sqlite" +) + +// Coder encoder/decoder +type Coder struct { + db sql.Sqlite + isRandom bool +} + +// NewCoder israndom 随机挑选声母韵母的颜文字, 否则固定使用第一个 +func NewCoder(israndom bool, cachettl time.Duration) (c Coder, err error) { + c.db.DBPath = EmoziDatabasePath + c.isRandom = israndom + err = c.db.Open(cachettl) + if err != nil { + return + } + err = c.db.Create(主字表名, &字表{}) + if err != nil { + return + } + err = c.db.Create(附字表名, &字表{}) + if err != nil { + return + } + err = c.db.Create(部首表名, &部首表{}) + if err != nil { + return + } + _ = c.db.Query("CREATE INDEX IF NOT EXISTS IE ON "+部首表名+" (E);", nil) + return +} + +// Close ... +func (c *Coder) Close() error { + return c.db.Close() +} + +// Encode 从汉字序列生成 EmoziString +func (c *Coder) Encode(s string, selections ...int) (EmoziString, error) { + sb := strings.Builder{} + x := &字表{} + lst := []字表{} + write := func(x *字表) { + sb.WriteString(c.查声母(x.S)) + sb.WriteString(c.查韵母(x.Y)) + sb.WriteString(c.查声调(x.T)) + sb.WriteString(c.查部首(x.R)) + } + 多音字计数 := 0 + for _, ch := range s { // nolint: go-staticcheck + lst = lst[:0] + err := c.db.FindFor(附字表名, x, "WHERE W="+strconv.Itoa(int(ch)), func() error { + lst = append(lst, *x) + return nil + }) + if err != nil { + lst = lst[:0] + err = c.db.FindFor(主字表名, x, "WHERE W="+strconv.Itoa(int(ch)), func() error { + lst = append(lst, *x) + return nil + }) + } + if err != nil || len(lst) == 0 { + sb.WriteRune(ch) + continue + } + if len(lst) == 1 { + write(x) + continue + } + if len(selections) > 多音字计数 { + idx := selections[多音字计数] + 多音字计数++ + if idx >= 0 && idx < len(lst) { + write(&lst[idx]) + continue + } + } + // 多音字 + sb.WriteString("[") + write(&lst[0]) + for _, x := range lst[1:] { + sb.WriteString("|") + write(&x) + } + sb.WriteString("]") + } + return WrapRawEmoziString(sb.String()), nil +} + +// Add 向主库添加一个新字 +// +// w: 字, r: 部首, p: 不带声调的拼音(可空), f: 带声调的拼音 +func (c *Coder) Add(w, r, p, f string) error { + if p == "" { + p = 去调(f) + } + s, y, t, rw, rr, err := 拆音识字(w, r, p, f) + if err != nil { + return err + } + err = c.db.InsertUnique(主字表名, &字表{ + ID: 颜表ID(rw, s, y, t), + W: rw, S: s, Y: y, T: t, + R: rr, P: p, F: f, + }) + if err != nil { + return errors.New("已有同音同形的字 '" + w + "'") + } + return nil +} + +// Overlay 向附加库添加一个新字, 覆盖在主库之上 +// +// w: 字, r: 部首, p: 不带声调的拼音(可空), f: 带声调的拼音 +func (c *Coder) Overlay(w, r, p, f string) error { + if p == "" { + p = 去调(f) + } + s, y, t, rw, rr, err := 拆音识字(w, r, p, f) + if err != nil { + return err + } + return c.overlay(w, p, f, s, y, t, rw, rr) +} + +func (c *Coder) overlay(w, p, f string, s 声母枚举, y 韵母枚举, t 声调枚举, rw rune, rr rune) error { + err := c.db.InsertUnique(附字表名, &字表{ + ID: 颜表ID(rw, s, y, t), + W: rw, S: s, Y: y, T: t, + R: rr, P: p, F: f, + }) + if err != nil { + return errors.New("已有同音同形的字 '" + w + "'") + } + return nil +} + +// ChangeOverlay 更改附加库的一项 +func (c *Coder) ChangeOverlay(oldw, oldr, oldf, neww, newr, newf string) error { + s, y, t, rw, rr, err := 拆音识字(oldw, oldr, 去调(oldf), oldf) + if err != nil { + return err + } + newp := 去调(newf) + ns, ny, nt, nrw, nrr, err := 拆音识字(neww, newr, newp, newf) + if err != nil { + return err + } + q := "WHERE ID=" + strconv.FormatInt(颜表ID(rw, s, y, t), 10) + x := 字表{} + err = c.db.Find(附字表名, &x, q) + if err != nil { + return err + } + if x.R != rr { + return errors.New("提供的旧部首 '" + string(rr) + "' 与记载的 '" + string(x.R) + "' 不符") + } + err = c.db.Del(附字表名, q) + if err != nil { + return err + } + return c.overlay(neww, newp, newf, ns, ny, nt, nrw, nrr) +} + +// OverlayRadical 添加一个部首 +func (c *Coder) OverlayRadical(r rune, e string) error { + return c.db.InsertUnique(部首表名, &部首表{R: r, E: e}) +} diff --git a/coder_test.go b/coder_test.go new file mode 100644 index 0000000..1396bb1 --- /dev/null +++ b/coder_test.go @@ -0,0 +1,29 @@ +package emozi + +import ( + "testing" + "time" +) + +func TestEncode(t *testing.T) { + c, err := NewCoder(false, time.Minute) + if err != nil { + t.Fatal(err) + } + es, err := c.Encode("你好,世界!看看多音字:行。") + if err != nil { + t.Fatal(err) + } + t.Log(es.String()) + if es.String() != "🥛👔🐴👤🌹🐱🐴👩,💦🌞😨🌍➕✌😨👨‍🌾!😭🔐🍉👁️😭🔐🍉👁️🔪🌀🍉🪩🐑🎵🍉🎵👈🌞😨🚼:[👇🦅🧗⛕|🌹👍🧗⛕]。" { + t.Fatal("got", es.String()) + } + es, err = c.Encode("你好,世界!指定多音字:银行行。", 1, 0) + if err != nil { + t.Fatal(err) + } + t.Log(es.String()) + if es.String() != "🥛👔🐴👤🌹🐱🐴👩,💦🌞😨🌍➕✌😨👨‍🌾!🐽🌞🐴✋🔪🦅😨🏠🔪🌀🍉🪩🐑🎵🍉🎵👈🌞😨🚼:🐑🎵🧗💰🌹👍🧗⛕👇🦅🧗⛕。" { + t.Fatal("got", es.String()) + } +} diff --git a/data.go b/data.go new file mode 100644 index 0000000..48891b8 --- /dev/null +++ b/data.go @@ -0,0 +1,38 @@ +package emozi + +import _ "embed" + +// 字数据库 数据来自 https://github.com/shuowenjiezi/shuowen +// +//var 字数据库 []byte + +// DatabasePath 字数据库的路径 如找不到会向对应路径写入内嵌的字数据库 +var EmoziDatabasePath = "字.db" + +const ( + 主字表名 = "emozi" + 附字表名 = "altzi" + 部首表名 = "radcl" +) + +// 字表 emozi表 定义 +type 字表 struct { + ID int64 // ID 高 32 位 W 的 rune, 低 32 位 保留8 S8 Y8 T8 + W rune + S 声母枚举 + Y 韵母枚举 + T 声调枚举 + R rune + P string + F string +} + +func 颜表ID(w rune, s 声母枚举, y 韵母枚举, t 声调枚举) int64 { + return int64((uint64(w) << 32) | (uint64(s) << 16) | (uint64(y) << 8) | (uint64(t))) +} + +// 从表 从部首表 +type 部首表 struct { + R rune // R 该部首 + E string `db:"E,UNIQUE"` // E 该部首对应的颜文字 +} diff --git a/define.go b/define.go new file mode 100644 index 0000000..83d012f --- /dev/null +++ b/define.go @@ -0,0 +1,210 @@ +package emozi + +import ( + "strings" + + base14 "github.com/fumiama/go-base16384" +) + +type 声母枚举 uint8 + +// String 国际音标 +func (sm 声母枚举) String() string { + a := sm * 3 + b := a + 3 + s := []rune("p pʰ m f t tʰ n l k kʰ x tɕ tɕʰɕ ʈʂ ʈʂʰʂ ɻ ts tsʰs j w ɥ 0 ")[a:b] + return strings.TrimSpace(string(s)) +} + +const ( + 声母b 声母枚举 = iota + 声母p + 声母m + 声母f + 声母d + 声母t + 声母n + 声母l + 声母g + 声母k + 声母h + 声母j + 声母q + 声母x + 声母zh + 声母ch + 声母sh + 声母r + 声母z + 声母c + 声母s + 声母yi + 声母w + 声母yu + 声母0 +) + +// 声母 可以有多个颜文字与之对应 但有时系统可能无法显示 +var 声母 = [...][]string{ + // b: 鼻笔八 + {"👃", "🖊️", "🎱"}, + // p: 跑跑跑票票牌牌 + {"🏃", "🏃‍♀️", "🏃‍♂️", "🎫", "🎟️", "🃏", "🎴"}, + // m: 马马猫猫猫猫锚面面门麦米木蜜帽帽帽麦 + {"🐴", "🐎", "🐱", "🐈", "🐈‍⬛", "😺", "⚓️", "🍜", "🍝", "🚪", "🌾", "🍚", "🪵", "🍯", "🎩", "👒", "🧢", "🎙️"}, + // f: 斧肺 + {"🪓", "🫁"}, + // d: 刀豆蛋灯灯洞 + {"🔪", "🫘", "🥚", "💡", "🪔", "🕳️"}, + // t: 腿糖兔兔桶桃 + {"🦵", "🍬", "🐰", "🐇", "🪣", "🍑"}, + // n: 奶鸟牛 + {"🥛", "🐦", "🐮"}, + // l: 鹿鹿冷龙龙莲 + {"🦌", "🫎", "🥶", "🐲", "🐉", "🪷"}, + // g: 瓜鼓龟狗勾跪跪跪 + {"🍉", "🥁", "🐢", "🐶", "🪝", "🧎", "🧎‍♀️", "🧎‍♂️"}, + // k: 哭裤 + {"😭", "👖"}, + // h: 花花花花花花花花火虎虎猴猴 + {"🌹", "🌼", "💐", "🌷", "🌸", "🌺", "🥀", "🪻", "🔥", "🐯", "🐅", "🐒", "🐵"}, + // j: 加减酒酒鸡鸡剑姜镜脚 + {"➕", "➖", "🍷", "🍺", "🐔", "🐓", "🗡️", "🫚", "🪞", "🦶"}, + // q: 茄钱钱钱钱钱棋 + {"🍆", "💰", "💴", "💵", "💶", "💸", "♟️"}, + // x: 下下虾虾虾雪星星星星熊象蟹 + {"👇", "⬇️", "🦐", "🍤", "🦞", "❄️", "🌟", "✨", "⭐️", "💫", "🐻", "🐘", "🦀️"}, + // zh: 猪猪针针 + {"🐽", "🐷", "🪡", "💉"}, + // ch: 茶茶车车车车床床除虫 + {"🍵", "☕️", "🚗", "🚘", "🚙", "🛻", "🛏️", "🛌", "➗", "🐛"}, + // sh: 水水书书书书书书上上上石山山山树树树蛇鼠鼠鼠珊 + {"💦", "💧", "📖", "📕", "📗", "📘", "📙", "📚", "⬆️", "☝️", "👆", "🪨", "⛰️", "🏔️", "🗻", "🌲", "🌳", "🌴", "🐍", "🐭", "🐀", "🐁", "🪸"}, + // r: 肉日日日 + {"🥩", "🌞", "☀️", "🌅"}, + // z: 左左左 c: 错错错错 s: 锁伞伞伞伞蒜 + {"👈", "⬅️", "⬅"}, {"❌", "🙅", "🙅‍♀️", "🙅‍♂️"}, {"🔒", "🌂", "☔️", "⛱️", "☂️", "🧄"}, + // yi: 羊羊牙药盐 w: 袜雾网碗 yu: 雨鱼鱼 0: 两种椅子 + {"🐑", "🐐", "🦷", "💊", "🧂"}, {"🧦", "🌫️", "🕸️", "🥣"}, {"🐟", "🌧️", "🐠"}, {"🪑", "💺"}, +} + +type 韵母枚举 uint8 + +// String 国际音标 +func (ym 韵母枚举) String() string { + return [...]string{ + "ä", "ɔ", "ɤ", "i", "ɿ", "u", "y", "ɐɚ̯", "aɪ̯", "u̯aɪ̯", "eɪ̯", "u̯eɪ̯", "ɑʊ̯", "i̯ɑʊ̯", + "oʊ̯", "i̯oʊ̯", "i̯ä", "u̯ä", "u̯o", "i̯ɛ", "y̯ɛ", "an", "i̯ɛn", "u̯an", "y̯ɛn", "ən", "u̯ən", + "in", "yn", "ɑŋ", "i̯ɑŋ", "u̯ɑŋ", "ɤŋ", "iŋ", "u̯əŋ", "ʊŋ", "i̯ʊŋ", + }[ym] +} + +const ( + 韵母a 韵母枚举 = iota + 韵母o + 韵母e + 韵母yi + 韵母ri + 韵母wu + 韵母yu + 韵母er + 韵母ai + 韵母uai + 韵母ei + 韵母ui + 韵母ao + 韵母iao + 韵母ou + 韵母iu + 韵母ia + 韵母ua + 韵母uo + 韵母ie + 韵母yue + 韵母an + 韵母ian + 韵母uan + 韵母yuan + 韵母en + 韵母un + 韵母in + 韵母yun + 韵母ang + 韵母iang + 韵母uang + 韵母eng + 韵母ing + 韵母ueng + 韵母ong + 韵母iong +) + +// 韵母 仅由一种意象表示 +var 韵母 = [...][]string{ + {"😧", "🤔️"}, {"😲"}, {"😋"}, {"👔", "👗", "👕", "👚"}, {"🌞", "☀️", "🌅"}, + {string([]rune("🌫️")[0]), "🌫️"}, {"🐟", "🌧️"}, {"👂"}, + {string([]rune("❤️")[0]), "❤️", "💗", "💓", "🫶", "💕", "💖", "💘", "💙", "💚", "💛", "💜", "💝", "💞", "🧡"}, {"🥢"}, {"😩"}, {"🐢"}, + {"🐱", "🐈", "🐈‍⬛", "😺"}, {"💊"}, {"🤮", "🤢"}, {"👉", "➡️"}, + {"🦷"}, {"🧦"}, {"🌀"}, {string([]rune("✌️")[0]), "✌️"}, {"🌙", "🌕", "🌛", "🌝"}, {"🔐"}, {"🚬"}, {"🥣", "🍚"}, + {string([]rune("⭕️")[0]), "⭕️"}, {"😐"}, {"😘", "💋"}, {"🎵"}, {string([]rune("☁️")[0]), "☁️"}, {"👍"}, {"🐑"}, + {string([]rune("🕸️")[0]), "🕸️"}, {"💡", "🪔"}, {"🦅"}, {"🐝"}, {"🌈"}, {"😳"}, // jiong 囧 +} + +type 声调枚举 uint8 + +// String 返回传统调名 +func (t 声调枚举) String() string { + return [...]string{ + "阴平", "阳平", "上声", "去声", "轻声", + }[t] +} + +const ( + 阴平 声调枚举 = iota + 阳平 + 上声 + 去声 + 轻声 +) + +var 声调 = [...][]string{{"🍉"}, {"🧗", "🧗‍♀️", "🧗‍♂️", "🦎"}, {"🐴", "🐎"}, {"😨"}, {"😯"}} + +// 校验表 用 校验表长度 个unicode控制字符做校验和验证此序列是由本程序生成而非手写的 +// +// 具体做法是先对后面的文本做crc32然后取 校验表长度^校验字节数 的模 +var 校验表 = func() []rune { + x, err := base14.UTF16BE2UTF8(base14.StringToBytes("\x20\x0b\x20\x0c\x20\x0d\x20\x0e\x20\x0f\x20\x2a\x20\x2b\x20\x2c\x20\x2d\x20\x2e\x20\x60\x20\x61\x20\x62\x20\x63\x20\x64\x20\x65\x20\x66\x20\x68\x20\x69\x20\x6a\x20\x6b\x20\x6c\x20\x6d\x20\x6e\x20\x6f")) + if err != nil { + panic(err) + } + return []rune(base14.BytesToString(x)) +}() + +// 校验表长度 25 +var 校验表长度 = len(校验表) + +// 校验字节数 默认为 3, 不得大于 3 +const 校验字节数 = 3 + +// 校验模 校验表长度^校验字节数 +var 校验模 = uint32(0) + +// 校验倍数 校验表长度乘方表 +var 校验倍数 = func() []uint32 { + tab := make([]uint32, 校验字节数) + tab[0] = 1 + for i := 1; i < 校验字节数; i++ { + tab[i] = tab[i-1] * uint32(校验表长度) + } + 校验模 = tab[校验字节数-1] * uint32(校验表长度) + return tab +}() + +// 逆校验表 还原校验和用 +var 逆校验表 = func() map[rune]uint8 { + m := make(map[rune]uint8, 64) + for i, c := range 校验表 { + m[c] = uint8(i) + } + return m +}() diff --git a/define_test.go b/define_test.go new file mode 100644 index 0000000..920ffc3 --- /dev/null +++ b/define_test.go @@ -0,0 +1,22 @@ +package emozi + +import "testing" + +func TestFirstEmojiSingle(t *testing.T) { + for i, lst := range 声母 { + if len([]rune(lst[0])) != 1 { + t.Fatal("声母", i, "长度", len([]rune(lst[0])), "字", lst[0]) + } + } + t.Log(string([]rune("🌫️")[0]), string([]rune("❤️")[0]), string([]rune("✌️")[0]), string([]rune("⭕️")[0]), string([]rune("☁️")[0]), string([]rune("🕸️")[0])) + for i, lst := range 韵母 { + if len([]rune(lst[0])) != 1 { + t.Fatal("韵母", i, "长度", len([]rune(lst[0])), "字", lst[0]) + } + } + for i, lst := range 声调 { + if len([]rune(lst[0])) != 1 { + t.Fatal("声调", i, "长度", len([]rune(lst[0])), "字", lst[0]) + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..21df239 --- /dev/null +++ b/go.mod @@ -0,0 +1,29 @@ +module github.com/fumiama/emozi + +go 1.20 + +require ( + github.com/FloatTech/sqlite v1.6.3 + github.com/fumiama/go-base16384 v1.7.0 + github.com/liuzl/gocc v0.0.0-20231231122217-0372e1059ca5 +) + +require ( + github.com/FloatTech/ttl v0.0.0-20220715042055-15612be72f5b // indirect + github.com/adamzy/cedar-go v0.0.0-20170805034717-80a9c64b256d // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/liuzl/cedar-go v0.0.0-20170805034717-80a9c64b256d // indirect + github.com/liuzl/da v0.0.0-20180704015230-14771aad5b1d // indirect + github.com/mattn/go-isatty v0.0.16 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect + golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect + golang.org/x/text v0.3.7 // indirect + modernc.org/libc v1.21.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.4.0 // indirect + modernc.org/sqlite v1.20.0 // indirect +) + +replace modernc.org/sqlite => github.com/fumiama/sqlite3 v1.20.0-with-win386 + +replace github.com/remyoudompheng/bigfft => github.com/fumiama/bigfft v0.0.0-20211011143303-6e0bfa3c836b diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d739478 --- /dev/null +++ b/go.sum @@ -0,0 +1,43 @@ +github.com/FloatTech/sqlite v1.6.3 h1:MQkqBNlkPuCoKQQgoNLuTL/2Ci3tBTFAnVYBdD0Wy4M= +github.com/FloatTech/sqlite v1.6.3/go.mod h1:zFbHzRfB+CJ+VidfjuVbrcin3DAz283F7hF1hIeHzpY= +github.com/FloatTech/ttl v0.0.0-20220715042055-15612be72f5b h1:tvciXWq2nuvTbFeJGLDNIdRX3BI546D3O7k7vrVueZw= +github.com/FloatTech/ttl v0.0.0-20220715042055-15612be72f5b/go.mod h1:fHZFWGquNXuHttu9dUYoKuNbm3dzLETnIOnm1muSfDs= +github.com/adamzy/cedar-go v0.0.0-20170805034717-80a9c64b256d h1:ir/IFJU5xbja5UaBEQLjcvn7aAU01nqU/NUyOBEU+ew= +github.com/adamzy/cedar-go v0.0.0-20170805034717-80a9c64b256d/go.mod h1:PRWNwWq0yifz6XDPZu48aSld8BWwBfr2JKB2bGWiEd4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/fumiama/bigfft v0.0.0-20211011143303-6e0bfa3c836b h1:Zt3pFQditAdWTHCOVkiloc9ZauBoWrb37guFV4iIRvE= +github.com/fumiama/bigfft v0.0.0-20211011143303-6e0bfa3c836b/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/fumiama/go-base16384 v1.7.0 h1:6fep7XPQWxRlh4Hu+KsdH+6+YdUp+w6CwRXtMWSsXCA= +github.com/fumiama/go-base16384 v1.7.0/go.mod h1:OEn+947GV5gsbTAnyuUW/SrfxJYUdYupSIQXOuGOcXM= +github.com/fumiama/sqlite3 v1.20.0-with-win386 h1:ZR1AXGBEtkfq9GAXehOVcwn+aaCG8itrkgEsz4ggx5k= +github.com/fumiama/sqlite3 v1.20.0-with-win386/go.mod h1:Os58MHwYCcYZCy2PGChBrQtBAw5/LS1ZZOkfc+C/I7s= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/liuzl/cedar-go v0.0.0-20170805034717-80a9c64b256d h1:qSmEGTgjkESUX5kPMSGJ4pcBUtYVDdkNzMrjQyvRvp0= +github.com/liuzl/cedar-go v0.0.0-20170805034717-80a9c64b256d/go.mod h1:x7SghIWwLVcJObXbjK7S2ENsT1cAcdJcPl7dRaSFog0= +github.com/liuzl/da v0.0.0-20180704015230-14771aad5b1d h1:hTRDIpJ1FjS9ULJuEzu69n3qTgc18eI+ztw/pJv47hs= +github.com/liuzl/da v0.0.0-20180704015230-14771aad5b1d/go.mod h1:7xD3p0XnHvJFQ3t/stEJd877CSIMkH/fACVWen5pYnc= +github.com/liuzl/gocc v0.0.0-20231231122217-0372e1059ca5 h1:wnbHIeP1UX8ClYEWKGnw66PfYvReCHu9G5lXSte3Sqc= +github.com/liuzl/gocc v0.0.0-20231231122217-0372e1059ca5/go.mod h1:7KaV9YIR92M1FpbczAcfYQ3UZ5ayT27pNtunDmXvLBo= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +modernc.org/libc v1.21.5 h1:xBkU9fnHV+hvZuPSRszN0AXDG4M7nwPLwTWwkYcvLCI= +modernc.org/libc v1.21.5/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.4.0 h1:crykUfNSnMAXaOJnnxcSzbUGMqkLWjklJKkBK2nwZwk= +modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= diff --git a/lookup.go b/lookup.go new file mode 100644 index 0000000..5b28bf4 --- /dev/null +++ b/lookup.go @@ -0,0 +1,53 @@ +package emozi + +import ( + "math/rand" + "strconv" +) + +var 空 = "🈳️" + +func (c *Coder) 查声母(s 声母枚举) string { + lst := 声母[s] + if len(lst) == 0 { + return 空 + } + if len(lst) == 1 || !c.isRandom { + return lst[0] + } + return lst[rand.Intn(len(lst))] +} + +func (c *Coder) 查韵母(y 韵母枚举) string { + lst := 韵母[y] + if len(lst) == 0 { + return 空 + } + if len(lst) == 1 || !c.isRandom { + return lst[0] + } + return lst[rand.Intn(len(lst))] +} + +func (c *Coder) 查声调(t 声调枚举) string { + lst := 声调[t] + if len(lst) == 0 { + return 空 + } + if len(lst) == 1 || !c.isRandom { + return lst[0] + } + return lst[rand.Intn(len(lst))] +} + +func (c *Coder) 查部首(r rune) string { + x := &部首表{} + err := c.db.Find(部首表名, x, "WHERE R="+strconv.Itoa(int(r))) + if err == nil && len(x.E) > 0 && x.E != 空 { + return x.E + } + if e, ok := 部首后备[r]; ok { + return e + } + return 空 +} diff --git a/pinyin.go b/pinyin.go new file mode 100644 index 0000000..d9b28a3 --- /dev/null +++ b/pinyin.go @@ -0,0 +1,336 @@ +package emozi + +import ( + "errors" + "strings" +) + +var smap = map[string]声母枚举{ + "b": 声母b, + "p": 声母p, + "m": 声母m, + "f": 声母f, + "d": 声母d, + "t": 声母t, + "n": 声母n, + "l": 声母l, + "g": 声母g, + "k": 声母k, + "h": 声母h, + "j": 声母j, + "q": 声母q, + "x": 声母x, + "r": 声母r, + "w": 声母w, +} + +const 双字声母 = "zcs" + +var amap = map[string]韵母枚举{ + "a": 韵母a, + "ai": 韵母ai, + "ao": 韵母ao, + "an": 韵母an, + "ang": 韵母ang, +} + +var wamap = map[string]韵母枚举{ + "a": 韵母ua, + "ai": 韵母uai, + "an": 韵母uan, + "ang": 韵母uang, +} + +var omap = map[string]韵母枚举{ + "o": 韵母o, + "ou": 韵母ou, + "ong": 韵母ong, +} + +var emap = map[string]韵母枚举{ + "e": 韵母e, + "er": 韵母er, + "ei": 韵母ei, + "en": 韵母en, + "eng": 韵母eng, +} + +var wemap = map[string]韵母枚举{ + "ei": 韵母ei, + "en": 韵母en, + "eng": 韵母ueng, +} + +var imap = map[string]韵母枚举{ + "i": 韵母yi, + "iao": 韵母iao, + "iu": 韵母iu, + "ia": 韵母ia, + "ie": 韵母ie, + "ian": 韵母ian, + "in": 韵母in, + "iang": 韵母iang, + "ing": 韵母ing, + "iong": 韵母iong, +} + +var umap = map[string]韵母枚举{ + "u": 韵母wu, + "uai": 韵母uai, + "ui": 韵母ui, + "ua": 韵母ua, + "uo": 韵母uo, + "uan": 韵母uan, + "un": 韵母un, + "uang": 韵母uang, + "ueng": 韵母ueng, +} + +var yumap = map[string]韵母枚举{ + "u": 韵母yu, + "ue": 韵母yue, + "uan": 韵母yuan, + "un": 韵母yun, + "ü": 韵母yu, + "üe": 韵母yue, + "üan": 韵母yuan, + "ün": 韵母yun, + "v": 韵母yu, + "ve": 韵母yue, + "van": 韵母yuan, + "vn": 韵母yun, +} + +func combine(maps ...map[string]韵母枚举) map[string]韵母枚举 { + newmap := make(map[string]韵母枚举, 128) + for _, m := range maps { + for k, v := range m { + newmap[k] = v + } + } + return newmap +} + +var aoeiu = combine(amap, omap, emap, imap, umap) + +var aoeu = combine(amap, omap, emap, umap) + +const ( + 阴平字母 = "āōēīūǖ" + 阳平字母 = "áóéíúǘń" + 上声字母 = "ǎǒěǐǔǚň" + 去声字母 = "àòèìùǜ" + G = 'ɡ' + A = 'ɑ' +) + +var notonemap = map[rune]string{ + 'ā': "a", 'á': "a", 'ǎ': "a", 'à': "a", + 'ō': "o", 'ó': "o", 'ǒ': "o", 'ò': "o", + 'ē': "e", 'é': "e", 'ě': "e", 'è': "e", + 'ī': "i", 'í': "i", 'ǐ': "i", 'ì': "i", + 'ū': "u", 'ú': "u", 'ǔ': "u", 'ù': "u", + 'ǖ': "ü", 'ǘ': "ü", 'ǚ': "ü", 'ǜ': "ü", + G: "g", A: "a", 'ń': "n", 'ň': "n", +} + +func 去调(pf string) string { + sb := strings.Builder{} + for _, c := range pf { + if nc, ok := notonemap[c]; ok { + sb.WriteString(nc) + } else { + sb.WriteRune(c) + } + } + return sb.String() +} + +// 识调 从拼音获得声调 +func 识调(pf string) 声调枚举 { + switch { + case strings.ContainsAny(pf, 阴平字母): + return 阴平 + case strings.ContainsAny(pf, 阳平字母): + return 阳平 + case strings.ContainsAny(pf, 上声字母): + return 上声 + case strings.ContainsAny(pf, 去声字母): + return 去声 + } + return 轻声 +} + +// 拆音 拆分拼音为声母韵母 +func 拆音(p string) (sm 声母枚举, ym 韵母枚举, err error) { + if len(p) == 1 { + sm = 声母0 + // 韵母ê, 因为文字稀少, 并入 ie + if p == "ê" { + ym = 韵母ie + return + } + if y, ok := aoeiu[p]; ok { + ym = y + return + } + err = errors.New("无法识别零声母拼音" + p) + return + } + if s, ok := smap[p[:1]]; ok { + sm = s + ok = false + switch p[1:2] { + case "a": + if p[:1] == "w" { + ym, ok = wamap[p[1:]] + } else { + ym, ok = amap[p[1:]] + } + case "o": + if p[:1] == "w" { + ym = 韵母uo + ok = true + } else { + ym, ok = omap[p[1:]] + } + case "e": + if p[:1] == "w" { + ym, ok = wemap[p[1:]] + } else { + ym, ok = emap[p[1:]] + } + case "i": + if p[:1] == "r" { + ym = 韵母ri + ok = true + } else { + ym, ok = imap[p[1:]] + } + case "u": + if strings.Contains("jqx", p[:1]) { + ym, ok = yumap[p[1:]] + } else { + ym, ok = umap[p[1:]] + if !ok { + ym, ok = yumap[p[1:]] + } + } + case "v", "ü"[:1]: + ym, ok = yumap[p[1:]] + } + if !ok { + err = errors.New("无法识别拼音" + p + "的韵母部分" + p[1:]) + } + return + } + ok := false + if strings.Contains(双字声母, p[:1]) { + if p[1:2] == "h" { // zh ch sh + switch p[:1] { + case "z": + sm = 声母zh + case "c": + sm = 声母ch + case "s": + sm = 声母sh + } + ym, ok = aoeu[p[2:]] + if !ok { + if p[2:] == "i" { + ym = 韵母ri + } else { + err = errors.New("无法识别拼音" + p) + } + } + return + } + switch p[:1] { + case "z": + sm = 声母z + case "c": + sm = 声母c + case "s": + sm = 声母s + } + ym, ok = aoeu[p[1:]] + if !ok { + if p[1:] == "i" { + ym = 韵母ri + } else { + err = errors.New("无法识别拼音" + p) + } + } + return + } + if p[:1] == "y" { // /j/ or /y/ + if strings.Contains("uvü"[:3], p[1:2]) { // /y/ + sm = 声母yu + ym, ok = yumap[p[1:]] + if !ok { + err = errors.New("无法识别拼音" + p) + } + return + } + if p[1:] == "ong" { // yong + sm = 声母yu + ym = 韵母iong + ok = true + return + } + sm = 声母yi + ym, ok = aoeiu[p[1:]] + if !ok { + err = errors.New("无法识别拼音" + p) + } + return + } + sm = 声母0 + ym, ok = aoeiu[p] + if !ok { + err = errors.New("无法识别拼音" + p) + } + return +} + +func 拆音识字(w, r, p, f string) (s 声母枚举, y 韵母枚举, t 声调枚举, rw, rr rune, err error) { + myp := 去调(f) + s, y, err = 拆音(p) + if err != nil { + return + } + mys, myy, err := 拆音(myp) + if err != nil { + return + } + if mys != s || myy != y { + err = errors.New("无声调拼音" + p + "与有声调拼音" + f + "不符") + return + } + t = 识调(f) + rws := []rune(w) + if len(rws) != 1 { + err = errors.New("无法正确识别文字" + w) + return + } + rw = rws[0] + rrs := []rune(r) + if len(rrs) != 1 { + err = errors.New("无法正确识别部首" + w) + return + } + rr = rrs[0] + return +} + +// SplitPinyin 拆分带声调拼音 +func SplitPinyin(pf string) (sm, ym, sd string, err error) { + p := 去调(pf) + s, y, err := 拆音(p) + if err != nil { + return + } + t := 识调(pf) + println(s, y, t) + return s.String(), y.String(), t.String(), nil +} diff --git a/pinyin_test.go b/pinyin_test.go new file mode 100644 index 0000000..0f289d4 --- /dev/null +++ b/pinyin_test.go @@ -0,0 +1,46 @@ +package emozi + +import "testing" + +func TestSplitPinyin(t *testing.T) { + s, y, tone, err := SplitPinyin("yōng") + if err != nil { + t.Fatal(err) + } + t.Log(s, y, tone) + if s+y+tone != "ɥi̯ʊŋ阴平" { + t.Fail() + } + s, y, tone, err = SplitPinyin("hóng") + if err != nil { + t.Fatal(err) + } + t.Log(s, y, tone) + if s+y+tone != "xʊŋ阳平" { + t.Fail() + } + s, y, tone, err = SplitPinyin("yǜn") + if err != nil { + t.Fatal(err) + } + t.Log(s, y, tone) + if s+y+tone != "ɥyn去声" { + t.Fail() + } + s, y, tone, err = SplitPinyin("jiǒng") + if err != nil { + t.Fatal(err) + } + t.Log(s, y, tone) + if s+y+tone != "tɕi̯ʊŋ上声" { + t.Fail() + } + s, y, tone, err = SplitPinyin("e") + if err != nil { + t.Fatal(err) + } + t.Log(s, y, tone) + if s+y+tone != "0ɤ轻声" { + t.Fail() + } +} diff --git a/preprocess/main.go b/preprocess/main.go new file mode 100644 index 0000000..2dfb8ae --- /dev/null +++ b/preprocess/main.go @@ -0,0 +1,81 @@ +package main + +// 数据来自 https://github.com/shuowenjiezi/shuowen + +import ( + "encoding/json" + "fmt" + "os" + "strings" + "time" + + "github.com/liuzl/gocc" + + "github.com/fumiama/emozi" +) + +type data struct { + W string `json:"wordhead"` + R string `json:"radical"` + P string `json:"pinyin"` + F string `json:"pinyin_full"` + A []string `json:"pinyin_alternative"` +} + +func main() { + x := data{} + t2s, err := gocc.New("t2s") + if err != nil { + panic(fmt.Sprintf("ERROR: creating gocc: %v", err)) + } + _ = os.RemoveAll(emozi.EmoziDatabasePath) + c, err := emozi.NewCoder(false, time.Minute) + if err != nil { + panic(fmt.Sprintf("ERROR: creating emozi coder: %v", err)) + } + for i := 1; i <= 9833; i++ { + f, err := os.Open(fmt.Sprintf("./data/%d.json", i)) + if err != nil { + panic(fmt.Sprintf("ERROR: opening data/%d.json: %v", i, err)) + } + x.A = x.A[:0] + err = json.NewDecoder(f).Decode(&x) + if err != nil { + panic(fmt.Sprintf("ERROR: decoding data/%d.json: %v", i, err)) + } + _ = f.Close() + x.P = strings.ReplaceAll(x.P, string(emozi.G), "g") + if len(x.P) == 0 { + panic(fmt.Sprintf("ERROR: decoding data/%d.json: p: %s, f: %s", i, x.P, x.F)) + } + insert := func(w string) error { + err = c.Add(w, x.R, x.P, x.F) + if err != nil { + return fmt.Errorf("inserting table emozi of data/%d.json: %v", i, err) + } + for _, a := range x.A { + err = c.Add(w, x.R, "", a) + if err != nil { + return fmt.Errorf("inserting table emozi of data/%d.json, alter %s: %v", i, a, err) + } + } + return nil + } + fmt.Print("\r[", i, "/9833] Insert char: ", x.W, " ") + err = insert(x.W) + if err != nil { + fmt.Println("\n\t\tWARN:", err) + } + sc, err := t2s.Convert(x.W) + if err != nil { + continue + } + if sc != x.W { + fmt.Print("\r[", i, "/9833] insert simplified char: ", sc, " ") + err = insert(sc) + if err != nil { + fmt.Println("\n\t\tWARN:", err) + } + } + } +} diff --git a/radical.go b/radical.go new file mode 100644 index 0000000..37de774 --- /dev/null +++ b/radical.go @@ -0,0 +1,547 @@ +// Code generated by codegen/radical. 已经经过修改, 不要手动重新运行. + +package emozi + +// 部首后备 内嵌的部首到颜文字的映射, 是第二优先查询顺位. 第一顺位是数据库的部首表. 第三位是回落到 🈳️. +var 部首后备 = map[rune]string{ + '一': "1⃣️", + '丄': "☝️", + '示': "😇", + '三': "3⃣️", + '王': "🫅", + '玉': "📿️", + '玨': "💎", + '气': "🌬", + '士': "💼", + '丨': "🥖", + '屮': "🌱", + '艸': "🍀", + '蓐': "🧣", + '茻': "🌿", + '小': "⏳", + '八': "8⃣️", + '釆': "🙌", + '半': "🐂", + '牛': "🐮", + '犛': "🦬", + '告': "🗣️", + '口': "👄", + '人': "👤", + '凵': "💋", + '吅': "😱", + '哭': "😭", + '走': "🏃", + '止': "🦶", + '癶': "🐾", + '步': "👣", + '此': "⛷️", + '正': "🎯", + '是': "⚖", + '辵': "🚶", + '彳': "🚦", + '廴': "🚥", + '㢟': "🈳️", + '行': "⛕", + '齒': "🈳️", + '牙': "🈳️", + '足': "🈳️", + '疋': "🈳️", + '品': "🈳️", + '龠': "🈳️", + '冊': "🈳️", + '㗊': "🈳️", + '舌': "👅", + '干': "🈳️", + '𧮫': "🈳️", + '只': "🈳️", + '㕯': "🈳️", + '句': "🈳️", + '丩': "🈳️", + '古': "🏺", + '十': "🔟", + '卅': "🌍", + '言': "💬", + '田': "👨‍🌾", + '誩': "🈳️", + '音': "🎵", + '䇂': "🈳️", + '丵': "🈳️", + '菐': "🈳️", + '𠬞': "🈳️", + '𠬜': "🈳️", + '共': "🈳️", + '異': "🈳️", + '舁': "🈳️", + '𦥑': "🈳️", + '䢅': "🈳️", + '爨': "🈳️", + '革': "🈳️", + '鬲': "🈳️", + '䰜': "🈳️", + '爪': "🤏", + '丮': "🈳️", + '鬥': "👊", + '又': "🈳️", + '𠂇': "🈳️", + '史': "🈳️", + '支': "🈳️", + '𦘒': "🈳️", + '聿': "✍", + '畫': "🎨", + '隶': "🈳️", + '臤': "🈳️", + '臣': "🈳️", + '殳': "🈳️", + '殺': "🪚", + '𠘧': "🈳️", + '寸': "🈳️", + '皮': "🈳️", + '㼱': "🈳️", + '攴': "🈳️", + '女': "👩", + '教': "🈳️", + '卜': "🔮", + '用': "🈳️", + '爻': "🈳️", + '㸚': "🈳️", + '𡕥': "🈳️", + '目': "👁️", + '䀠': "👀", + '眉': "🤨", + '盾': "🈳️", + '自': "👃", + '𪞶': "🈳️", + '鼻': "👃", + '皕': "🈳️", + '習': "🈳️", + '羽': "🪶", + '隹': "🐦", + '奞': "🈳️", + '雈': "🈳️", + '𦫳': "🈳️", + '𥄕': "🈳️", + '羊': "🐑", + '羴': "🈳️", + '瞿': "🈳️", + '雔': "🈳️", + '雥': "🈳️", + '鳥': "🦢", + '烏': "🐦‍⬛", + '𠦒': "🈳", + '冓': "🈳️", + '幺': "🈳️", + '𢆶': "🈳️", + '叀': "🈳️", + '玄': "🈳️", + '予': "🈳️", + '放': "🈳️", + '𠬪': "🈳️", + '𣦼': "🈳️", + '歺': "🈳️", + '死': "🈳️", + '冎': "🈳️", + '骨': "🈳️", + '肉': "🈳️", + '筋': "🈳️", + '刀': "🔪", + '刃': "🔪", + '㓞': "🈳️", + '丯': "🈳️", + '耒': "🈳️", + '角': "🥐", + '竹': "🎋", + '箕': "🈳️", + '丌': "🈳️", + '左': "👈", + '工': "🈳️", + '㠭': "🈳️", + '巫': "🈳️", + '甘': "🈳️", + '曰': "🈳️", + '乃': "🈳️", + '丂': "🈳️", + '可': "🈳️", + '兮': "🈳️", + '号': "🈳️", + '亏': "🈳️", + '旨': "🈳️", + '喜': "🈳️", + '壴': "🈳️", + '鼓': "🈳️", + '豈': "🈳️", + '豆': "🈳️", + '豊': "🈳️", + '豐': "🈳️", + '䖒': "🈳️", + '虍': "🈳️", + '虎': "🈳️", + '虤': "🈳️", + '皿': "🈳️", + '𠙴': "🈳️", + '去': "🈳️", + '血': "🈳️", + '丶': "🈳️", + '丹': "🈳️", + '青': "🈳️", + '井': "🈳️", + '皀': "🈳️", + '鬯': "🈳️", + '食': "🈳️", + '亼': "🈳️", + '會': "🈳️", + '倉': "🈳️", + '入': "🈳️", + '缶': "🈳️", + '矢': "🈳️", + '高': "🈳️", + '冂': "🈳️", + '𩫖': "🈳️", + '京': "🈳️", + '亯': "🈳️", + '㫗': "🈳️", + '畗': "🈳️", + '㐭': "🈳️", + '嗇': "🈳️", + '來': "🈳️", + '麥': "🈳️", + '夊': "🈳️", + '舛': "🈳️", + '舜': "🈳️", + '韋': "🈳️", + '弟': "🈳️", + '夂': "🈳️", + '久': "🈳️", + '桀': "🈳️", + '木': "🈳️", + '東': "🈳️", + '林': "🈳️", + '才': "🈳️", + '叒': "🈳️", + '之': "🈳️", + '帀': "🈳️", + '出': "🈳️", + '𣎵': "🈳️", + '生': "🈳️", + '乇': "🈳️", + '𠂹': "🈳️", + '𠌶': "🈳️", + '華': "🈳️", + '𥝌': "🈳️", + '稽': "🈳️", + '巢': "🈳️", + '桼': "🈳️", + '束': "🈳️", + '㯻': "🈳️", + '囗': "🈳️", + '員': "🈳️", + '貝': "🈳️", + '邑': "🈳️", + '𨛜': "🈳️", + '日': "🈳️", + '旦': "🈳️", + '倝': "🈳️", + '㫃': "🈳️", + '冥': "🈳️", + '晶': "🈳️", + '月': "🈳️", + '有': "🈳️", + '朙': "🈳️", + '囧': "🈳️", + '夕': "🌇", + '多': "🪩", + '毌': "🈳️", + '𢎘': "🈳️", + '𣐺': "🈳️", + '𠧪': "🈳️", + '齊': "🈳️", + '朿': "🈳️", + '片': "🈳️", + '鼎': "🈳️", + '克': "🈳️", + '彔': "🈳️", + '禾': "🈳️", + '秝': "🈳️", + '黍': "🈳️", + '香': "🈳️", + '米': "🈳️", + '毇': "🈳️", + '臼': "🈳️", + '凶': "🈳️", + '朩': "🈳️", + '𣏟': "🈳️", + '麻': "🈳️", + '尗': "🈳️", + '耑': "🈳️", + '韭': "🈳️", + '瓜': "🈳️", + '瓠': "🈳️", + '宀': "🏠", + '宮': "🏛", + '呂': "🩻", + '穴': "🕳️", + '㝱': "🈳️", + '疒': "😷", + '冖': "🗃", + '𠔼': "🈳️", + '冃': "🈳️", + '㒳': "🈳️", + '网': "🈳️", + '襾': "🈳️", + '巾': "🧣", + '巿': "🈳️", + '帛': "🈳️", + '白': "⚪", + '㡀': "🈳️", + '黹': "🈳️", + '𠤎': "🈳️", + '匕': "🈳️", + '从': "🈳️", + '比': "🈳️", + '北': "🈳️", + '丘': "🈳️", + '㐺': "🈳️", + '𡈼': "🈳️", + '重': "🈳️", + '臥': "🈳️", + '身': "🈳️", + '㐆': "🈳️", + '衣': "🈳️", + '裘': "🈳️", + '老': "🈳️", + '毛': "🈳️", + '毳': "🈳️", + '尸': "🈳️", + '尺': "🈳️", + '尾': "🈳️", + '履': "🈳️", + '舟': "🛶", + '方': "🈳️", + '儿': "🈳️", + '兄': "🈳️", + '兂': "🈳️", + '皃': "🈳️", + '𠑹': "🈳️", + '先': "🈳️", + '禿': "🈳️", + '見': "🈳️", + '覞': "🈳️", + '欠': "🈳️", + '㱃': "🈳️", + '㳄': "🈳️", + '旡': "🈳️", + '頁': "🈳️", + '𦣻': "🈳️", + '面': "🈳️", + '丏': "🈳️", + '首': "🈳️", + '𥄉': "🈳️", + '須': "🈳️", + '彡': "🈳️", + '彣': "🈳️", + '文': "🈳️", + '髟': "🈳️", + '后': "🈳️", + '司': "🈳️", + '卮': "🈳️", + '卩': "🈳️", + '印': "🈳️", + '色': "🈳️", + '𠨍': "🈳️", + '辟': "🈳️", + '勹': "🈳️", + '包': "🈳️", + '茍': "🈳️", + '鬼': "🈳️", + '甶': "🈳️", + '厶': "🈳️", + '嵬': "🈳️", + '山': "🈳️", + '屾': "🈳️", + '屵': "🈳️", + '广': "🈳️", + '厂': "🈳️", + '丸': "🈳️", + '危': "🈳️", + '石': "🈳️", + '長': "🈳️", + '勿': "🈳️", + '冄': "🈳️", + '而': "🈳️", + '豕': "🈳️", + '㣇': "🈳️", + '彑': "🈳️", + '豚': "🈳️", + '豸': "🈳️", + '𤉡': "🈳️", + '易': "🈳️", + '象': "🈳️", + '馬': "🈳️", + '𢊁': "🈳️", + '鹿': "🈳️", + '麤': "🈳️", + '㲋': "🈳️", + '兔': "🈳️", + '萈': "🈳️", + '犬': "🈳️", + '㹜': "🈳️", + '鼠': "🈳️", + '能': "🈳️", + '熊': "🈳️", + '火': "🈳️", + '炎': "🈳️", + '黑': "🈳️", + '囪': "🈳️", + '焱': "🈳️", + '炙': "🈳️", + '赤': "🈳️", + '大': "🈳️", + '亦': "🈳️", + '夨': "🈳️", + '夭': "🈳️", + '交': "🈳️", + '尣': "🈳️", + '壺': "🈳️", + '壹': "🈳️", + '㚔': "🈳️", + '奢': "🈳️", + '亢': "🈳️", + '夲': "🈳️", + '夰': "🈳️", + '亣': "🈳️", + '夫': "🈳️", + '立': "🈳️", + '竝': "🈳️", + '囟': "🈳️", + '思': "🈳️", + '心': "🈳️", + '惢': "🈳️", + '水': "🈳️", + '沝': "🈳️", + '瀕': "🈳️", + '𡿨': "🈳️", + '巜': "🈳️", + '川': "🈳️", + '泉': "🈳️", + '灥': "🈳️", + '永': "🈳️", + '𠂢': "🈳️", + '谷': "🈳️", + '仌': "🈳️", + '雨': "🈳️", + '雲': "🈳️", + '魚': "🈳️", + '𩺰': "🈳️", + '燕': "🈳️", + '龍': "🈳️", + '飛': "🈳️", + '非': "🈳️", + '卂': "🈳️", + '𠃉': "🈳️", + '不': "🈳️", + '至': "🈳️", + '西': "🈳️", + '鹵': "🈳️", + '鹽': "🈳️", + '戶': "🈳️", + '門': "🈳️", + '耳': "🈳️", + '𦣞': "🈳️", + '手': "✋", + '𠦬': "🈳️", + '毋': "🈳️", + '民': "🈳️", + '丿': "🈳️", + '𠂆': "🈳️", + '乁': "🈳️", + '氏': "🈳️", + '氐': "🈳️", + '戈': "🈳️", + '戉': "🈳️", + '我': "🈳️", + '亅': "🈳️", + '珡': "🈳️", + '𠃊': "🈳️", + '亾': "🈳️", + '匸': "🈳️", + '匚': "🈳️", + '曲': "🈳️", + '甾': "🈳️", + '瓦': "🈳️", + '弓': "🈳️", + '弜': "🈳️", + '弦': "🈳️", + '系': "🈳️", + '糸': "🈳️", + '素': "🈳️", + '絲': "🈳️", + '率': "🈳️", + '虫': "🈳️", + '䖵': "🈳️", + '蟲': "🈳️", + '風': "🈳️", + '它': "🈳️", + '龜': "🈳️", + '黽': "🈳️", + '卵': "🈳️", + '二': "🈳️", + '土': "🈳️", + '垚': "🈳️", + '堇': "🈳️", + '里': "🈳️", + '畕': "🈳️", + '黃': "🈳️", + '男': "🈳️", + '力': "🈳️", + '劦': "🈳️", + '金': "💰", + '幵': "🈳️", + '勺': "🈳️", + '几': "🈳️", + '且': "🈳️", + '斤': "🈳️", + '斗': "🈳️", + '矛': "🈳️", + '車': "🈳️", + '𠂤': "🈳️", + '𨸏': "🈳️", + '𨺅': "🈳️", + '厽': "🈳️", + '四': "🈳️", + '宁': "🈳️", + '叕': "🈳️", + '亞': "🈳️", + '五': "🈳️", + '六': "🈳️", + '七': "🈳️", + '九': "🈳️", + '禸': "🈳️", + '嘼': "🈳️", + '甲': "🈳️", + '乙': "🈳️", + '丙': "🈳️", + '丁': "🈳️", + '戊': "🈳️", + '己': "🈳️", + '巴': "🈳️", + '庚': "🈳️", + '辛': "🈳️", + '辡': "🈳️", + '壬': "🈳️", + '癸': "🈳️", + '子': "🚼", + '了': "🈳️", + '孨': "🈳️", + '𠫓': "🈳️", + '丑': "🈳️", + '寅': "🈳️", + '戼': "🈳️", + '辰': "🈳️", + '巳': "🈳️", + '午': "🈳️", + '未': "🈳️", + '申': "🈳️", + '酉': "🈳️", + '酋': "🈳️", + '戌': "🈳️", + '亥': "🈳️", +} diff --git a/string.go b/string.go new file mode 100644 index 0000000..fa49c86 --- /dev/null +++ b/string.go @@ -0,0 +1,63 @@ +package emozi + +import ( + "hash/crc32" + "strings" + + base14 "github.com/fumiama/go-base16384" +) + +// EmoziString 一个颜文字汉字转写串, 包含串头 校验字节数*2 字节校验和 +type EmoziString string + +// WrapRawEmoziString 为不包含串头的转写串加一个头使其成为合法 EmoziString +func WrapRawEmoziString(s string) EmoziString { + rs := []rune(s) + if len(rs) < 4 { + return "" + } + h := crc32.NewIEEE() + h.Write(base14.StringToBytes(s)) + sum := h.Sum32() % 校验模 + sb := strings.Builder{} + buf := [校验字节数]uint8{} + for i := 校验字节数 - 1; i >= 0; i-- { + buf[i] = uint8((sum / 校验倍数[i]) % uint32(校验表长度)) + } + for i, n := range buf { + sb.WriteRune(rs[i]) + sb.WriteRune(校验表[n]) + } + sb.WriteString(string(rs[len(buf):])) + return EmoziString(sb.String()) +} + +// String 输出不包含串头的转写串 +func (es EmoziString) String() string { + if !es.IsValid() { + return "ERROR: invalid EmoziString" + } + rs := []rune(es) + sb := strings.Builder{} + for i := 0; i < 校验字节数; i++ { + sb.WriteRune(rs[i*2]) + } + sb.WriteString(string(rs[校验字节数*2:])) + return sb.String() +} + +// IsValid 判断是否是合法 EmoziString +func (es EmoziString) IsValid() bool { + rs := []rune(es) + if len(rs) < 校验字节数+4 { + return false + } + h := crc32.NewIEEE() + sum := uint32(0) + for i := 0; i < 校验字节数; i++ { + h.Write(base14.StringToBytes(string(rs[i*2]))) + sum += 校验倍数[i] * uint32(逆校验表[rs[i*2+1]]) + } + h.Write(base14.StringToBytes(string(rs[校验字节数*2:]))) + return h.Sum32()%校验模 == sum +} diff --git a/string_test.go b/string_test.go new file mode 100644 index 0000000..dcd97c7 --- /dev/null +++ b/string_test.go @@ -0,0 +1,20 @@ +package emozi + +import "testing" + +func TestWrapUnwrap(t *testing.T) { + t.Log(校验表长度) + if !WrapRawEmoziString("😨😨😨😨").IsValid() { + t.Fail() + } + if EmoziString("😨😨😨😨😨😨😨").IsValid() { + t.Fail() + } +} + +func TestString(t *testing.T) { + t.Log(校验表长度) + if WrapRawEmoziString("😨😨😨😨").String() != "😨😨😨😨" { + t.Fail() + } +} diff --git a/字.db b/字.db new file mode 100644 index 0000000..5300278 Binary files /dev/null and b/字.db differ