package grammar_test
import (
"strings"
"testing"
"chirami/internal/grammar"
)
func hasTermReasons(candidates []grammar.Result, expectedTerm string, checkRule bool, expectedRule string, checkReasons bool, expectedReasons []string) bool {
for _, r := range candidates {
if r.Base != expectedTerm {
continue
}
if checkRule {
if !r.MatchesCondition(expectedRule) {
continue
}
}
if checkReasons {
if len(r.Steps) != len(expectedReasons) {
continue
}
matched := true
for i, reason := range expectedReasons {
if r.Steps[i].Name != reason {
matched = false
break
}
}
if !matched {
continue
}
}
return true
}
return false
}
// TestYomitanSuite runs the complete test suite ported from Yomitan's
// test/language/japanese-transforms.test.js.
func TestYomitanSuite(t *testing.T) {
totalCases := 0
passCount := 0
validCount := 0
invalidCount := 0
for _, cat := range yomitanTestCategories {
t.Run(cat.Category, func(t *testing.T) {
for _, tc := range cat.Tests {
totalCases++
if cat.Valid {
validCount++
} else {
invalidCount++
}
results := grammar.Deinflect(tc.Source)
has := hasTermReasons(results, tc.Term, tc.CheckRule, tc.Rule, tc.CheckReasons, tc.Reasons)
if has == cat.Valid {
passCount++
} else {
t.Errorf("%s: source=%q expected base=%q rule=%q reasons=%v want valid=%v got has=%v",
cat.Category, tc.Source, tc.Term, tc.Rule, tc.Reasons, cat.Valid, has)
}
}
})
}
t.Logf("Yomitan test suite: %d/%d cases passed (valid: %d, invalid: %d)",
passCount, totalCases, validCount, invalidCount)
}
func TestSuffixSplitting(t *testing.T) {
cases := []struct {
surface string
targetBase string
expectedSteps []string
expectedSuffix []string
expectedStem string
}{
{
surface: "食べさせられなかった",
targetBase: "食べる",
expectedSteps: []string{"causative", "potential or passive", "negative", "-た"},
expectedSuffix: []string{"させ", "られ", "なかっ", "た"},
expectedStem: "食べ",
},
{
surface: "読んでいる",
targetBase: "読む",
expectedSteps: []string{"-て", "-いる"},
expectedSuffix: []string{"んで", "いる"},
expectedStem: "読",
},
{
surface: "行ってきます",
targetBase: "行ってくる",
expectedSteps: []string{"-ます"},
expectedSuffix: []string{"きます"},
expectedStem: "行って",
},
{
surface: "書かれた",
targetBase: "書く",
expectedSteps: []string{"passive", "-た"},
expectedSuffix: []string{"かれ", "た"},
expectedStem: "書",
},
{
surface: "見たくない",
targetBase: "見る",
expectedSteps: []string{"-たい", "negative"},
expectedSuffix: []string{"たく", "ない"},
expectedStem: "見",
},
{
surface: "来なかった",
targetBase: "来る",
expectedSteps: []string{"negative", "-た"},
expectedSuffix: []string{"なかっ", "た"},
expectedStem: "来",
},
{
surface: "しなければならない",
targetBase: "しなければなる",
expectedSteps: []string{"negative"},
expectedSuffix: []string{"らない"},
expectedStem: "しなければな",
},
}
for _, tc := range cases {
t.Run(tc.surface, func(t *testing.T) {
results := grammar.Deinflect(tc.surface)
var found *grammar.Result
for i := range results {
r := &results[i]
if r.Base == tc.targetBase && len(r.Steps) == len(tc.expectedSteps) {
match := true
for j, name := range tc.expectedSteps {
if r.Steps[j].Name != name {
match = false
break
}
}
if match {
found = r
break
}
}
}
if found == nil {
t.Fatalf("Deinflect(%q): target candidate Base=%q with steps=%v not found in %d results",
tc.surface, tc.targetBase, tc.expectedSteps, len(results))
}
// Verify suffixes
var actualSuffixes []string
var totalSuffix strings.Builder
for _, step := range found.Steps {
actualSuffixes = append(actualSuffixes, step.Suffix)
totalSuffix.WriteString(step.Suffix)
if step.Description == "" && step.Name != "imperative negative slang" {
t.Errorf("step %q missing description", step.Name)
}
}
for j := range tc.expectedSuffix {
if actualSuffixes[j] != tc.expectedSuffix[j] {
t.Errorf("step %d (%s) suffix = %q, want %q",
j, tc.expectedSteps[j], actualSuffixes[j], tc.expectedSuffix[j])
}
}
// Verify base stem
stem := found.BaseStem()
if tc.expectedStem != "" && stem != tc.expectedStem {
t.Errorf("BaseStem() = %q, want %q", stem, tc.expectedStem)
}
// Verify reconstruction
spelled := stem + totalSuffix.String()
if spelled != tc.surface {
t.Errorf("stem + suffixes = %q, want surface %q", spelled, tc.surface)
}
})
}
}
func BenchmarkDeinflect(b *testing.B) {
word := "食べさせられなかった"
b.ResetTimer()
for i := 0; i < b.N; i++ {
res := grammar.Deinflect(word)
_ = res
}
}
func BenchmarkDeinflectShort(b *testing.B) {
word := "食べた"
b.ResetTimer()
for i := 0; i < b.N; i++ {
res := grammar.Deinflect(word)
_ = res
}
}
func BenchmarkDeinflectMedium(b *testing.B) {
word := "読んでいる"
b.ResetTimer()
for i := 0; i < b.N; i++ {
res := grammar.Deinflect(word)
_ = res
}
}