package jp
import (
"bytes"
"compress/gzip"
_ "embed"
"encoding/json"
"os"
"reflect"
"testing"
)
func TestScriptPredicates(t *testing.T) {
// Hiragana
for _, r := range []rune{'あ', 'ん', 'ぁ', 'ゝ', 'ゞ'} {
if !IsHiragana(r) {
t.Errorf("IsHiragana(%c) = false, want true", r)
}
}
for _, r := range []rune{'ア', '漢', 'a', '1', 'ー'} {
if IsHiragana(r) {
t.Errorf("IsHiragana(%c) = true, want false", r)
}
}
// Katakana
for _, r := range []rune{'ア', 'ン', 'ァ', 'ー', 'ー', 'ア', 'ン', 'ヽ', 'ヾ'} {
if !IsKatakana(r) {
t.Errorf("IsKatakana(%c) = false, want true", r)
}
}
for _, r := range []rune{'あ', '漢', 'a', '1'} {
if IsKatakana(r) {
t.Errorf("IsKatakana(%c) = true, want false", r)
}
}
// Kana
for _, r := range []rune{'あ', 'ア', 'ー', 'ア'} {
if !IsKana(r) {
t.Errorf("IsKana(%c) = false, want true", r)
}
}
for _, r := range []rune{'漢', 'a', '1'} {
if IsKana(r) {
t.Errorf("IsKana(%c) = true, want false", r)
}
}
// Kanji: CJK unified ideographs + ext A + 々〆ヵヶ
for _, r := range []rune{'漢', '字', '々', '〆', 'ヵ', 'ヶ', rune(0x3400), rune(0x4DBF)} {
if !IsKanji(r) {
t.Errorf("IsKanji(%c / %U) = false, want true", r, r)
}
}
for _, r := range []rune{'あ', 'ア', 'a', '1', ' '} {
if IsKanji(r) {
t.Errorf("IsKanji(%c) = true, want false", r)
}
}
// HasJapanese
if !HasJapanese("hello 世界") {
t.Errorf("HasJapanese('hello 世界') = false, want true")
}
if !HasJapanese("こんにちは") {
t.Errorf("HasJapanese('こんにちは') = false, want true")
}
if !HasJapanese("ラーメン") {
t.Errorf("HasJapanese('ラーメン') = false, want true")
}
if HasJapanese("hello world 123 !?") {
t.Errorf("HasJapanese('hello world 123 !?') = true, want false")
}
// IsKanaOnly
if !IsKanaOnly("こんにちは") {
t.Errorf("IsKanaOnly('こんにちは') = false, want true")
}
if !IsKanaOnly("ラーメン") {
t.Errorf("IsKanaOnly('ラーメン') = false, want true")
}
if IsKanaOnly("こんにちは、世界!") {
t.Errorf("IsKanaOnly('こんにちは、世界!') = true, want false")
}
if !IsKanaOnly("こんにちは!") {
t.Errorf("IsKanaOnly('こんにちは!') = false, want true")
}
if IsKanaOnly("ーーー") {
t.Errorf("IsKanaOnly('ーーー') = true, want false (no kana letters)")
}
if IsKanaOnly("hello") {
t.Errorf("IsKanaOnly('hello') = true, want false")
}
}
func TestKanaConversion(t *testing.T) {
if got := ToHiragana("ラーメン"); got != "らーめん" {
t.Errorf("ToHiragana(ラーメン) = %q, want %q", got, "らーめん")
}
if got := ToKatakana("らーめん"); got != "ラーメン" {
t.Errorf("ToKatakana(らーめん) = %q, want %q", got, "ラーメン")
}
if got := ToHiragana("アイウエオ"); got != "あいうえお" {
t.Errorf("ToHiragana(アイウエオ) = %q, want %q", got, "あいうえお")
}
}
func TestMoras(t *testing.T) {
tests := []struct {
input string
want []string
}{
{"きょうと", []string{"きょ", "う", "と"}},
{"がっこう", []string{"が", "っ", "こ", "う"}},
{"コーヒー", []string{"コ", "ー", "ヒ", "ー"}},
{"ほんや", []string{"ほ", "ん", "や"}},
{"ティーパーティー", []string{"ティ", "ー", "パ", "ー", "ティ", "ー"}},
}
for _, tc := range tests {
got := Moras(tc.input)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("Moras(%q) = %v, want %v", tc.input, got, tc.want)
}
}
}
func TestRomaji(t *testing.T) {
tests := []struct {
input string
style Style
want string
}{
// きょう -> kyou in context-free Romaji
{"きょう", Macron, "kyou"},
{"きょう", Wapuro, "kyou"},
{"きょう", Plain, "kyou"},
// がっこう
{"がっこう", Macron, "gakkou"},
{"がっこう", Wapuro, "gakkou"},
// まっちゃ -> matcha
{"まっちゃ", Macron, "matcha"},
{"まっちゃ", Wapuro, "matcha"},
// ほんや -> hon'ya
{"ほんや", Macron, "hon'ya"},
{"ほんや", Wapuro, "hon'ya"},
// きんえん -> kin'en
{"きんえん", Macron, "kin'en"},
{"きんえん", Wapuro, "kin'en"},
// しんぶん -> shinbun (not shimbun)
{"しんぶん", Macron, "shinbun"},
{"しんぶん", Wapuro, "shinbun"},
// コーヒー
{"コーヒー", Macron, "kōhī"},
{"コーヒー", Wapuro, "koohii"},
{"コーヒー", Plain, "kohi"},
// Particle を / ヲ
{"を", Macron, "o"},
{"を", Plain, "o"},
{"を", Wapuro, "wo"},
{"ヲ", Macron, "o"},
{"ヲ", Plain, "o"},
{"ヲ", Wapuro, "wo"},
}
for _, tc := range tests {
got := Romaji(tc.input, tc.style)
if got != tc.want {
t.Errorf("Romaji(%q, style=%v) = %q, want %q", tc.input, tc.style, got, tc.want)
}
}
}
func TestRomajiPron(t *testing.T) {
tests := []struct {
reading string
pron string
style Style
want string
}{
// きょう -> kyō only via RomajiPron(キョウ, キョー)
{"キョウ", "キョー", Macron, "kyō"},
{"キョウ", "キョー", Wapuro, "kyou"},
{"キョウ", "キョー", Plain, "kyo"},
// トウキョウ
{"トウキョウ", "トーキョー", Macron, "tōkyō"},
{"トウキョウ", "トーキョー", Wapuro, "toukyou"},
{"トウキョウ", "トーキョー", Plain, "tokyo"},
// オモウ
{"オモウ", "オモウ", Macron, "omou"},
{"オモウ", "オモウ", Wapuro, "omou"},
{"オモウ", "オモウ", Plain, "omou"},
// センセイ
{"センセイ", "センセー", Macron, "sensei"},
{"センセイ", "センセー", Wapuro, "sensei"},
{"センセイ", "センセー", Plain, "sensei"},
// ハ -> wa
{"ハ", "ワ", Macron, "wa"},
{"ハ", "ワ", Wapuro, "wa"},
// オオキイ
{"オオキイ", "オーキイ", Macron, "ōkii"},
{"オオキイ", "オーキイ", Wapuro, "ookii"},
{"オオキイ", "オーキイ", Plain, "okii"},
// ヘ -> e
{"ヘ", "エ", Macron, "e"},
{"ヘ", "エ", Wapuro, "e"},
// ヲ -> o (in Macron and Plain, and when pron is オ)
{"ヲ", "オ", Macron, "o"},
{"ヲ", "オ", Plain, "o"},
{"ヲ", "オ", Wapuro, "o"},
// ヲ with pron ヲ: "o" in Macron & Plain per Hepburn, "wo" in Wapuro per WanaKana
{"ヲ", "ヲ", Macron, "o"},
{"ヲ", "ヲ", Plain, "o"},
{"ヲ", "ヲ", Wapuro, "wo"},
}
for _, tc := range tests {
got := RomajiPron(tc.reading, tc.pron, tc.style)
if got != tc.want {
t.Errorf("RomajiPron(%q, %q, style=%v) = %q, want %q", tc.reading, tc.pron, tc.style, got, tc.want)
}
}
}
func TestIterationMarks(t *testing.T) {
if got := Romaji("さゝ", Macron); got != "sasa" {
t.Errorf("Romaji('さゝ') = %q, want 'sasa'", got)
}
if got := Romaji("はゞ", Macron); got != "haba" {
t.Errorf("Romaji('はゞ') = %q, want 'haba'", got)
}
if got := Romaji("サヽ", Macron); got != "sasa" {
t.Errorf("Romaji('サヽ') = %q, want 'sasa'", got)
}
if got := Romaji("ハヾ", Macron); got != "haba" {
t.Errorf("Romaji('ハヾ') = %q, want 'haba'", got)
}
}
func TestRomajiFallbacks(t *testing.T) {
if got := RomajiPron("きょう", "", Macron); got != "kyou" {
t.Errorf("RomajiPron('きょう', '') = %q, want 'kyou'", got)
}
if got := RomajiPron("きょう", "トウキョウ", Macron); got != "kyou" {
t.Errorf("RomajiPron mismatched = %q, want 'kyou'", got)
}
}
func TestRomajiPunctuation(t *testing.T) {
if got := Romaji("こんにちは!", Macron); got != "konnichiha!" {
t.Errorf("Romaji('こんにちは!') = %q, want 'konnichiha!'", got)
}
}
//go:embed testdata/wanakana_sample_5k.json.gz
var wanakanaSampleGz []byte
// TestWanaKanaParity verifies 100% parity against real WanaKana over distinct JMdict kana readings.
// By default it tests the embedded 5k representative sample hermetically.
// If testdata/wanakana_pairs.json.gz is present, it tests the complete 193,414 readings.
func TestWanaKanaParity(t *testing.T) {
var gzReader *gzip.Reader
var err error
fullPath := "testdata/wanakana_pairs.json.gz"
if f, errOpen := os.Open(fullPath); errOpen == nil {
defer f.Close()
gzReader, err = gzip.NewReader(f)
} else {
gzReader, err = gzip.NewReader(bytes.NewReader(wanakanaSampleGz))
}
if err != nil {
t.Fatalf("failed to create gzip reader: %v", err)
}
defer gzReader.Close()
var pairs [][]string
if err := json.NewDecoder(gzReader).Decode(&pairs); err != nil {
t.Fatalf("failed to decode json: %v", err)
}
mismatches := 0
for _, p := range pairs {
kana := p[0]
expected := p[1]
got := Romaji(kana, Wapuro)
if got != expected {
mismatches++
if mismatches <= 10 {
t.Errorf("WanaKana mismatch for %q: got %q, want %q", kana, got, expected)
}
}
}
t.Logf("Tested %d distinct kana readings against WanaKana: %d mismatches", len(pairs), mismatches)
if mismatches != 0 {
t.Fatalf("expected 0 mismatches, got %d", mismatches)
}
}