package analyze
import (
"slices"
"strings"
"unicode/utf8"
"chirami/internal/dict"
"chirami/internal/jp"
)
// Entries finds dictionary entries for w, best first: the exact surface (or
// dictionary form first for inflected verbs/adjectives), then the dictionary
// form of its head, then the longest known prefix.
func (a *Analyzer) Entries(w Word) []dict.Entry {
if a.Dict == nil || w.Symbol {
return nil
}
if w.Key != "" && w.Key != w.Surface && (w.pos == "動詞" || w.pos == "形容詞") {
if es := a.Dict.LookupReading(w.Key, w.KeyReading); len(es) > 0 {
return rankByPOS(es, w)
}
}
if es := a.Dict.LookupReading(w.Surface, w.Reading); len(es) > 0 {
return rankByPOS(es, w)
}
if w.Key != "" && w.Key != w.Surface {
if es := a.Dict.LookupReading(w.Key, w.KeyReading); len(es) > 0 {
return rankByPOS(es, w)
}
}
if _, es := a.Dict.LongestPrefix(w.Surface, 12); len(es) > 0 {
return rankByPOS(es, w)
}
return nil
}
// rankByPOS prefers entries whose JMdict part of speech matches the
// tokenizer's POS for kana words (e.g. かも as particle vs 鴨 as noun).
func rankByPOS(es []dict.Entry, w Word) []dict.Entry {
if w.pos == "" || len(es) <= 1 || !jp.IsKanaOnly(w.Surface) {
return es
}
slices.SortStableFunc(es, func(a, b dict.Entry) int {
return posRank(b, w) - posRank(a, w)
})
return es
}
func posRank(e dict.Entry, w Word) int {
if !dict.WrittenInKana(e) {
return 0
}
common := hasCommonKana(e, w.Reading)
if !common && w.Content {
return 0
}
s := 0
if common {
s += 2
}
if matchesPOS(e, w.pos) {
s += 4
}
return s
}
func hasCommonKana(e dict.Entry, reading string) bool {
for _, r := range e.Kana {
if r.Common && jp.ToHiragana(r.Text) == reading {
return true
}
}
return false
}
func matchesPOS(e dict.Entry, pos string) bool {
for _, s := range e.Senses {
for _, p := range s.POS {
switch pos {
case "助詞":
if p == "prt" || p == "exp" || p == "conj" {
return true
}
case "助動詞":
if p == "cop" || p == "aux" || p == "aux-v" || p == "aux-adj" {
return true
}
case "接続詞":
if p == "conj" || p == "prt" || p == "exp" {
return true
}
case "副詞":
if p == "adv" || p == "adv-to" {
return true
}
case "動詞":
if strings.HasPrefix(p, "v") {
return true
}
case "形容詞":
if p == "adj-i" || p == "adj-ix" {
return true
}
case "名詞":
if p == "n" || p == "pn" || p == "adj-na" || p == "adj-no" || p == "num" || p == "ctr" {
return true
}
}
}
}
return false
}
// Kanji returns KANJIDIC entries for the distinct kanji in s, at most max.
func (a *Analyzer) Kanji(s string, max int) []dict.Kanji {
if a.Dict == nil {
return nil
}
var out []dict.Kanji
seen := map[rune]bool{}
for _, r := range s {
if !jp.IsKanji(r) || seen[r] || len(out) == max {
continue
}
seen[r] = true
if k, ok := a.Dict.Kanji(r); ok {
out = append(out, k)
}
}
return out
}
// Romaji romanizes reading, the reading of word. Long vowels need morpheme
// boundaries (東京 tōkyō, but 思う omou), so it tokenizes word and uses the
// tokens' pronunciations when they spell out reading; otherwise it falls back
// to the context-free conversion.
func (a *Analyzer) Romaji(word, reading string) string {
if a.Morph != nil && word != "" {
var kana, pron string
for _, t := range a.Morph.Tokenize(word) {
if t.Reading == "" {
kana = ""
break
}
kana += t.Reading
pron += cmp(t.Pron, t.Reading)
}
if kana != "" && jp.ToHiragana(kana) == jp.ToHiragana(reading) {
return jp.RomajiPron(kana, pron, a.Style)
}
}
return jp.Romaji(reading, a.Style)
}
func cmp(s, fallback string) string {
if s != "" {
return s
}
return fallback
}
// Phrase returns the whole selection as one word when the dictionary knows
// it as a whole (an idiom such as 猫に小判, a proverb), so the card can offer
// its entry while keeping each word clickable.
func (a *Analyzer) Phrase(text string, words []Word) (Word, bool) {
if a.Dict == nil || len(words) < 2 || utf8.RuneCountInString(text) > 24 {
return Word{}, false
}
for _, w := range words {
if w.Symbol {
return Word{}, false
}
}
if len(a.Dict.Lookup(text)) == 0 {
return Word{}, false
}
return a.join(words), true
}