penguin/golifehk

@golifehk bot in Telegram

commit d54f106dee225e40824f6b49b4b49c06f15d2fe1

author斟酌 鵬兄 <tgckpg@gmail.com>
date2026-02-20T13:06:47Z
subjectRewrite some struct linkages
commit d54f106dee225e40824f6b49b4b49c06f15d2fe1
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date:   2026-02-20T13:06:47Z

    Rewrite some struct linkages
---
 datasources/cjlookup/CChar.go                      | 57 +++++++++++---
 datasources/cjlookup/QueryResult.go                | 70 ++++++-----------
 datasources/cjlookup/humanumarts.go                | 49 +++++++++++-
 datasources/cjlookup/humanumarts_test.go           |  7 +-
 datasources/cjlookup/maps.go                       | 88 +++++++++++++++-------
 .../cjlookup/maps/jyut6ping3.chars.dict.yaml       |  2 +-
 datasources/cjlookup/objects_test.go               | 28 +++----
 datasources/cjlookup/query_test.go                 | 13 +++-
 datasources/cjlookup/rime-cantonese                |  2 +-
 datasources/cjlookup/searchables.go                | 24 +++---
 10 files changed, 220 insertions(+), 120 deletions(-)

diff --git a/datasources/cjlookup/CChar.go b/datasources/cjlookup/CChar.go
index 2e7c0cd..4898051 100644
--- a/datasources/cjlookup/CChar.go
+++ b/datasources/cjlookup/CChar.go
@@ -2,25 +2,29 @@ package cjlookup
 
 import (
 	query "github.com/tgckpg/golifehk/query"
+	"golang.org/x/exp/slices"
 	"strconv"
 )
 
 type CChar struct {
 	Face      string
 	CangJie   string
-	JyutPing  string
-	TungJamZi *[]*CChar
-	YiDukJam  *[]string
+	JyutPing  *CJyutPing
+	DukJam    *[]*DukJam
+	_JiDukJam *[]*CJyutPing
+	query.Searchable
 }
 
-type CJyutPing struct {
-	Ref  *CChar
-	SKey string
-	query.Searchable
+type DukJam struct {
+	Weight   int
+	JyutPing *CJyutPing
 }
 
-type CFace struct {
-	Ref *CChar
+type CJyutPing struct {
+	Roman     string
+	SearchKey string // Searchable key
+	TungJamZi *[]*CChar
+	tSorted   bool
 	query.Searchable
 }
 
@@ -32,7 +36,7 @@ func (this *CJyutPing) Test(val string) bool {
 	return false
 }
 
-func (this *CFace) Test(val string) bool {
+func (this *CChar) Test(val string) bool {
 	_, err := strconv.Atoi(val)
 	if err == nil {
 		return true
@@ -40,6 +44,35 @@ func (this *CFace) Test(val string) bool {
 	return false
 }
 
-func (m *CChar) String() string {
-	return m.Face
+func (this *CChar) JiDukJam() *[]*CJyutPing {
+	if this._JiDukJam == nil {
+		jdj := &[]*CJyutPing{}
+		for _, dj := range *this.DukJam {
+			if dj.JyutPing != this.JyutPing {
+				*jdj = append(*jdj, dj.JyutPing)
+			}
+		}
+		this._JiDukJam = jdj
+	}
+
+	return this._JiDukJam
+}
+
+func (this *CJyutPing) SortedTungJamZi() *[]*CChar {
+	if this.tSorted {
+		return this.TungJamZi
+	}
+
+	slices.SortFunc(*this.TungJamZi, func(a, b *CChar) int {
+		switch {
+		case a.Face < b.Face:
+			return -1
+		case a.Face > b.Face:
+			return 1
+		default:
+			return 0
+		}
+	})
+
+	return this.TungJamZi
 }
diff --git a/datasources/cjlookup/QueryResult.go b/datasources/cjlookup/QueryResult.go
index fac6df2..a824bbc 100644
--- a/datasources/cjlookup/QueryResult.go
+++ b/datasources/cjlookup/QueryResult.go
@@ -7,7 +7,6 @@ import (
 
 	query "github.com/tgckpg/golifehk/query"
 	utils "github.com/tgckpg/golifehk/utils"
-	"golang.org/x/exp/slices"
 )
 
 type QueryResult struct {
@@ -19,11 +18,11 @@ type QueryResult struct {
 func writeCCharInfo(sb *strings.Builder, cc *CChar) {
 	sb.WriteString(getCUHARTSUrlForChar(cc.Face))
 	sb.WriteString(" ")
-	utils.WriteMDv2Text(sb, cc.JyutPing)
-	if cc.YiDukJam != nil {
+	sb.WriteString(getCUHARTSUrlForPronounce(cc.JyutPing.Roman))
+	if 0 < len(*cc.JiDukJam()) {
 		sb.WriteString("\n")
 		utils.WriteMDv2Text(sb, "異讀: ")
-		utils.WriteMDv2Text(sb, strings.Join(*cc.YiDukJam, " "))
+		sb.WriteString(strings.Join(getCUHARTSUrlsForPronounce(*cc.JiDukJam()), " "))
 	}
 	sb.WriteString("\n")
 	utils.WriteMDv2Text(sb, "倉: ")
@@ -31,18 +30,7 @@ func writeCCharInfo(sb *strings.Builder, cc *CChar) {
 	sb.WriteString("\n")
 	utils.WriteMDv2Text(sb, "同音字:")
 
-	slices.SortFunc(*cc.TungJamZi, func(a, b *CChar) int {
-		switch {
-		case a.Face < b.Face:
-			return -1
-		case a.Face > b.Face:
-			return 1
-		default:
-			return 0
-		}
-	})
-
-	for i, cchar := range *cc.TungJamZi {
+	for i, cchar := range *cc.JyutPing.SortedTungJamZi() {
 		sb.WriteString(" ")
 		utils.WriteMDv2Text(sb, cchar.Face)
 		utils.WriteMDv2Text(sb, strconv.Itoa(i+1))
@@ -74,26 +62,24 @@ func (this QueryResult) Message() (string, error) {
 		return "", fmt.Errorf("Not Found: \"%s\"", strings.Join(terms, "\", \""))
 	}
 
-	slices.SortFunc(*q.Results, func(a, b query.ISearchable) int {
-		aa := a.(*CJyutPing)
-		bb := b.(*CJyutPing)
-		switch {
-		case aa.Ref.Face < bb.Ref.Face:
-			return -1
-		case aa.Ref.Face > bb.Ref.Face:
-			return 1
-		default:
-			return 0
-		}
-	})
-
 	if utils.IsASCIIAlnum(argv[0].Value) {
+		// Roman Search
+		cjp := any((*q.Results)[0]).(*CJyutPing)
 		if args == 1 {
-			for i, entry := range *q.Results {
-				cjp := any(entry).(*CJyutPing)
-				sb.WriteString(" ")
-				utils.WriteMDv2Text(&sb, cjp.Ref.Face)
-				utils.WriteMDv2Text(&sb, strconv.Itoa(i+1))
+			if len(*q.Results) == 1 {
+				if len(*cjp.SortedTungJamZi()) == 1 {
+					for _, cchar := range *cjp.SortedTungJamZi() {
+						writeCCharInfo(&sb, cchar)
+					}
+				} else {
+					for i, cchar := range *cjp.SortedTungJamZi() {
+						sb.WriteString(" ")
+						utils.WriteMDv2Text(&sb, cchar.Face)
+						utils.WriteMDv2Text(&sb, strconv.Itoa(i+1))
+					}
+				}
+			} else {
+				return "", fmt.Errorf("Multiple results found: %s", *q.Results)
 			}
 		} else if args == 2 {
 			var err error
@@ -101,26 +87,20 @@ func (this QueryResult) Message() (string, error) {
 			if err != nil {
 				return "", fmt.Errorf("Invalid index: %s", argv[1].Org)
 			}
-			if 0 < x {
-				for i, entry := range *q.Results {
-					cjp := any(entry).(*CJyutPing)
-					if (i + 1) == x {
-						writeCCharInfo(&sb, cjp.Ref)
-					}
-				}
-			}
+			writeCCharInfo(&sb, (*cjp.SortedTungJamZi())[x-1])
 		}
 	} else {
-		cjp := any((*q.Results)[0]).(*CFace)
+		// Char Search
+		cchar := any((*q.Results)[0]).(*CChar)
 		if args == 1 {
-			writeCCharInfo(&sb, cjp.Ref)
+			writeCCharInfo(&sb, cchar)
 		} else if args == 2 {
 			var err error
 			x, err := strconv.Atoi(argv[1].Value)
 			if err != nil {
 				return "", fmt.Errorf("Invalid index: %s", argv[1].Org)
 			}
-			tjz := (*(*cjp.Ref).TungJamZi)[x-1]
+			tjz := (*(cchar.JyutPing).SortedTungJamZi())[x-1]
 
 			if tjz != nil {
 				writeCCharInfo(&sb, tjz)
diff --git a/datasources/cjlookup/humanumarts.go b/datasources/cjlookup/humanumarts.go
index 520c1de..dc484c8 100644
--- a/datasources/cjlookup/humanumarts.go
+++ b/datasources/cjlookup/humanumarts.go
@@ -4,22 +4,69 @@ import (
 	"fmt"
 	"log"
 	"net/url"
+	"strings"
 
 	"golang.org/x/text/encoding/traditionalchinese"
 )
 
 var CUHARTS_FACE = "[%s](https://humanum.arts.cuhk.edu.hk/Lexis/lexi-can/search.php?q=%s)"
+var CUHARTS_PHO = "[%s](https://humanum.arts.cuhk.edu.hk/Lexis/lexi-can/pho-rel.php?%s&classified=classified.php?st=0)"
 var ZDIC_FACE = "[%s](https://www.zdic.net/hans/%s)"
+var SING_MOU_S1 = []string{"b", "c", "d", "f", "g", "gw", "h", "j", "k", "kw", "l", "m", "n", "ng", "p", "s", "t", "w", "z"}
+var WAN_MOU_S2 = []string{"aa", "aai", "aau", "aam", "aan", "aang", "aap", "aat", "aak", "ai", "au", "am", "an", "ang", "ap", "at", "ak", "e", "ei", "eu", "em", "eng", "ep", "ek", "i", "iu", "im", "in", "ing", "ip", "it", "ik", "o", "oi", "ou", "on", "ong", "ot", "ok", "oe", "oeng", "oek", "eoi", "eon", "eot", "u", "ui", "un", "ung", "ut", "uk", "yu", "yun", "yut", "m", "ng"}
+var SING_DIU_S3 = []string{"1", "2", "3", "4", "5", "6"}
 
 func getCUHARTSUrlForChar(c string) string {
 	b, err := Big5UrlParam(c)
 	if err != nil {
-		log.Printf("Failed to encode:", c, err)
+		log.Print("Failed to encode:", c, err)
 		return fmt.Sprintf(ZDIC_FACE, c, c)
 	}
 	return fmt.Sprintf(CUHARTS_FACE, c, b)
 }
 
+func SParams(p string) string {
+	s1 := "-"
+	for _, x := range SING_MOU_S1 {
+		if strings.HasPrefix(p, x) {
+			s1 = x
+			break
+		}
+	}
+
+	s3 := "-"
+	for _, x := range SING_DIU_S3 {
+		if strings.HasSuffix(p, x) {
+			s3 = x
+			break
+		}
+	}
+
+	s2 := ""
+	bestMatch := 0
+	for _, x := range WAN_MOU_S2 {
+		if strings.HasSuffix(strings.TrimSuffix(strings.TrimPrefix(p, s1), s3), x) {
+			if bestMatch < len(x) {
+				s2 = x
+				bestMatch = len(x)
+			}
+		}
+	}
+	return fmt.Sprintf("s1=%s&s2=%s&s3=%s", s1, s2, s3)
+}
+
+func getCUHARTSUrlsForPronounce(p []*CJyutPing) []string {
+	urls := []string{}
+	for _, x := range p {
+		urls = append(urls, getCUHARTSUrlForPronounce(x.Roman))
+	}
+	return urls
+}
+
+func getCUHARTSUrlForPronounce(p string) string {
+	return fmt.Sprintf(CUHARTS_PHO, p, SParams(p))
+}
+
 func Big5UrlParam(input string) (string, error) {
 	enc := traditionalchinese.Big5.NewEncoder()
 	encodedBytes, err := enc.Bytes([]byte(input))
diff --git a/datasources/cjlookup/humanumarts_test.go b/datasources/cjlookup/humanumarts_test.go
index 34a64a6..4cc6f48 100644
--- a/datasources/cjlookup/humanumarts_test.go
+++ b/datasources/cjlookup/humanumarts_test.go
@@ -5,7 +5,12 @@ import (
 )
 
 func TestCUHARTLinks(t *testing.T) {
-	if Big5UrlParam("呀") != "%A7r" {
+	s, err := Big5UrlParam("呀")
+	if s != "%A7r" {
 		t.Errorf("Faild to encode Big5 string: %A7r", err)
 	}
+
+	if SParams("caang4") != "s1=c&s2=aang&s3=4" {
+		t.Error("Faild to parse s params for: caang4")
+	}
 }
diff --git a/datasources/cjlookup/maps.go b/datasources/cjlookup/maps.go
index 1a2d90f..82d8345 100644
--- a/datasources/cjlookup/maps.go
+++ b/datasources/cjlookup/maps.go
@@ -86,6 +86,7 @@ func ReadCangJieTable(charMap map[string]*CChar, repl *strings.Replacer) error {
 		} else {
 			c := CChar{}
 			c.Face = face
+			c.Key = &c.Face
 			c.CangJie = repl.Replace(record[1])
 			charMap[face] = &c
 		}
@@ -94,7 +95,7 @@ func ReadCangJieTable(charMap map[string]*CChar, repl *strings.Replacer) error {
 	return nil
 }
 
-func ReadJyutPingTable(charMap map[string]*CChar) (map[string]*[]*CChar, error) {
+func ReadJyutPingTable(charMap map[string]*CChar) (map[string]*CJyutPing, error) {
 	f, err := os.Open(YAML_JYUTPING_CHARS)
 	if err != nil {
 		log.Fatal(err)
@@ -118,7 +119,7 @@ func ReadJyutPingTable(charMap map[string]*CChar) (map[string]*[]*CChar, error)
 	r.Comma = '\t'
 	r.FieldsPerRecord = -1
 
-	JyutPingMap := map[string]*[]*CChar{}
+	JyutPingMap := map[string]*CJyutPing{}
 	for {
 		record, err := r.Read()
 		if err == io.EOF {
@@ -131,41 +132,70 @@ func ReadJyutPingTable(charMap map[string]*CChar) (map[string]*[]*CChar, error)
 		face := record[0]
 		jyutping := record[1]
 
-		var c *CChar
-		c, ok := charMap[face]
+		var tjz *[]*CChar
+		cjp, ok := JyutPingMap[jyutping]
 		if ok {
-			if len(record) == 2 {
-				c.JyutPing = record[1]
-			} else if len(record) == 3 {
-				s := record[2]
-				if strings.HasSuffix(s, "%") {
-					_, err := strconv.Atoi(s[:len(s)-1])
-					if err != nil {
-						return nil, err
-					}
-				}
-				yList := c.YiDukJam
-				if yList == nil {
-					yList = &[]string{}
-				}
-				*yList = append(*yList, record[1])
-				c.YiDukJam = yList
-			}
+			tjz = cjp.TungJamZi
 		} else {
+			cjp = &CJyutPing{}
+			cjp.Roman = jyutping
+			cjp.SearchKey = strings.ToUpper(jyutping)
+			cjp.Key = &cjp.SearchKey
+			tjz = &[]*CChar{}
+			cjp.TungJamZi = tjz
+			JyutPingMap[jyutping] = cjp
+		}
+
+		var c *CChar
+		c, ok = charMap[face]
+		if !ok {
 			c = &CChar{}
 			c.Face = face
-			c.JyutPing = jyutping
+			c.Key = &c.Face
 			charMap[face] = c
 		}
 
-		pListPtr, ok := JyutPingMap[jyutping]
-		if !ok {
-			pList := []*CChar{}
-			pListPtr = &pList
-			JyutPingMap[jyutping] = pListPtr
+		*tjz = append(*tjz, c)
+
+		var dj *DukJam
+
+		if len(record) == 2 {
+			c.JyutPing = cjp
+			dj = &DukJam{}
+			dj.Weight = 100
+			dj.JyutPing = cjp
+		} else if len(record) == 3 {
+			s := record[2]
+			var w int
+			if strings.HasSuffix(s, "%") {
+				w, err = strconv.Atoi(s[:len(s)-1])
+				if err != nil {
+					return nil, err
+				}
+			}
+			dj = &DukJam{}
+			dj.Weight = w
+			dj.JyutPing = cjp
+		}
+
+		if dj != nil {
+			if c.DukJam == nil {
+				c.DukJam = &[]*DukJam{}
+			}
+			*c.DukJam = append(*c.DukJam, dj)
+		}
+	}
+
+	for _, c := range charMap {
+		if c.JyutPing == nil && c.DukJam != nil {
+			_max := -1
+			for _, dj := range *c.DukJam {
+				if _max < dj.Weight {
+					_max = dj.Weight
+					c.JyutPing = dj.JyutPing
+				}
+			}
 		}
-		*pListPtr = append(*pListPtr, c)
-		c.TungJamZi = pListPtr
 	}
 
 	return JyutPingMap, nil
diff --git a/datasources/cjlookup/maps/jyut6ping3.chars.dict.yaml b/datasources/cjlookup/maps/jyut6ping3.chars.dict.yaml
index d83f195..cb87409 100644
--- a/datasources/cjlookup/maps/jyut6ping3.chars.dict.yaml
+++ b/datasources/cjlookup/maps/jyut6ping3.chars.dict.yaml
@@ -7,7 +7,7 @@
 
 ---
 name: jyut6ping3.chars
-version: "2025.11.27"
+version: "2025.12.24"
 sort: by_weight
 ...
 
diff --git a/datasources/cjlookup/objects_test.go b/datasources/cjlookup/objects_test.go
index 7d02137..d28e9f1 100644
--- a/datasources/cjlookup/objects_test.go
+++ b/datasources/cjlookup/objects_test.go
@@ -5,21 +5,21 @@ import (
 )
 
 func TestDataRead(t *testing.T) {
-	chars := map[string]*CChar{}
-	cjRepl, err := ReadCangJieKeys()
-	if err != nil {
-		t.Error(err)
-	}
-	ReadCangJieTable(chars, cjRepl)
-	JyutPingMap, err := ReadJyutPingTable(chars)
-	if err != nil {
-		t.Error(err)
-	}
-	j := *JyutPingMap["sing4"]
-	if j[0].TungJamZi != j[1].TungJamZi {
-		t.Errorf("%s & %s does not point to the same reference.", j[0].Face, j[1].Face)
-	}
 	/*
+		chars := map[string]*CChar{}
+		cjRepl, err := ReadCangJieKeys()
+		if err != nil {
+			t.Error(err)
+		}
+		ReadCangJieTable(chars, cjRepl)
+		JyutPingMap, err := ReadJyutPingTable(chars)
+		if err != nil {
+			t.Error(err)
+		}
+		j := *JyutPingMap["sing4"]
+		if j.TungJamZi[0].JyutPing != j.TungJamZi[1].JyutPing {
+			t.Errorf("%s & %s does not point to the same reference.", j[0].Face, j[1].Face)
+		}
 		for i, c := range j {
 			t.Log(i, c.CangJie)
 		}
diff --git a/datasources/cjlookup/query_test.go b/datasources/cjlookup/query_test.go
index b48338c..5dedb85 100644
--- a/datasources/cjlookup/query_test.go
+++ b/datasources/cjlookup/query_test.go
@@ -1,7 +1,6 @@
 package cjlookup
 
 import (
-	"fmt"
 	"strings"
 	"testing"
 )
@@ -41,5 +40,15 @@ func TestQuery(t *testing.T) {
 		t.Errorf("Expected 閲62 in response, got \"%s\" instead", mesg)
 	}
 
-	fmt.Println(mesg)
+	qo = Query("zh-Hant", "jp jeng4 1")
+	mesg, err = qo.Message()
+	if strings.Contains(mesg, "同音字") {
+		t.Error("Unexpected TungJamZi for jeng4")
+	}
+
+	qo = Query("zh-Hant", "jp 數")
+	mesg, err = qo.Message()
+	if !strings.Contains(mesg, "異讀: [cuk1]") {
+		t.Error("Expecting 異讀 from 數")
+	}
 }
diff --git a/datasources/cjlookup/rime-cantonese b/datasources/cjlookup/rime-cantonese
index 77776c0..ce0be9e 160000
--- a/datasources/cjlookup/rime-cantonese
+++ b/datasources/cjlookup/rime-cantonese
@@ -1 +1 @@
-Subproject commit 77776c0aad31c78ba743877780fae26f8b1e0dbe
+Subproject commit ce0be9ee5d7e92a0bd82c6b41e80a51190a4fd22
diff --git a/datasources/cjlookup/searchables.go b/datasources/cjlookup/searchables.go
index f7baa7a..db0adf7 100644
--- a/datasources/cjlookup/searchables.go
+++ b/datasources/cjlookup/searchables.go
@@ -1,9 +1,8 @@
 package cjlookup
 
 import (
-	"strings"
-
 	query "github.com/tgckpg/golifehk/query"
+	"log"
 )
 
 func getSearchables() (*[]query.ISearchable, error) {
@@ -21,21 +20,18 @@ func getSearchables() (*[]query.ISearchable, error) {
 		return nil, err
 	}
 
-	for jyutping, chars := range jpMap {
-		for _, c := range *chars {
-			cjp := CJyutPing{}
-			cjp.Ref = c
-			cjp.SKey = strings.ToUpper(jyutping)
-			cjp.Key = &cjp.SKey
-			searchables = append(searchables, &cjp)
+	for _, x := range jpMap {
+		if x.Key == nil {
+			log.Fatal("getSearchables CJP: ", x)
 		}
+		searchables = append(searchables, x)
 	}
 
-	for _, c := range chars {
-		ccj := CFace{}
-		ccj.Ref = c
-		ccj.Key = &c.Face
-		searchables = append(searchables, &ccj)
+	for _, x := range chars {
+		if x.Key == nil {
+			log.Fatal("getSearchables CChar: ", x)
+		}
+		searchables = append(searchables, x)
 	}
 
 	return &searchables, nil