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