initial checkin

This commit is contained in:
Alex Kelly 2025-05-02 16:37:45 -04:00
commit ec09a98a24
2 changed files with 430 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
kana_stats.db

429
kana.py Normal file
View file

@ -0,0 +1,429 @@
import random
import time
import sqlite3
from typing import Dict, List, Tuple
from wcwidth import wcswidth
# Hiragana monographs (basic)
hiragana_monographs = {
'': 'a', '': 'i', '': 'u', '': 'e', '': 'o',
'': 'ka', '': 'ki', '': 'ku', '': 'ke', '': 'ko',
'': 'sa', '': 'shi', '': 'su', '': 'se', '': 'so',
'': 'ta', '': 'chi', '': 'tsu', '': 'te', '': 'to',
'': 'na', '': 'ni', '': 'nu', '': 'ne', '': 'no',
'': 'ha', '': 'hi', '': 'fu', '': 'he', '': 'ho',
'': 'ma', '': 'mi', '': 'mu', '': 'me', '': 'mo',
'': 'ya', '': 'yu', '': 'yo',
'': 'ra', '': 'ri', '': 'ru', '': 're', '': 'ro',
'': 'wa', '': 'wo', '': 'n'
}
# Hiragana monographs with diacritics
hiragana_monographs_diacritics = {
'': 'ga', '': 'gi', '': 'gu', '': 'ge', '': 'go',
'': 'za', '': 'ji', '': 'zu', '': 'ze', '': 'zo',
'': 'da', '': 'ji', '': 'zu', '': 'de', '': 'do',
'': 'ba', '': 'bi', '': 'bu', '': 'be', '': 'bo',
'': 'pa', '': 'pi', '': 'pu', '': 'pe', '': 'po'
}
# Hiragana digraphs (yōon)
hiragana_digraphs = {
'きゃ': 'kya', 'きゅ': 'kyu', 'きょ': 'kyo',
'しゃ': 'sha', 'しゅ': 'shu', 'しょ': 'sho',
'ちゃ': 'cha', 'ちゅ': 'chu', 'ちょ': 'cho',
'にゃ': 'nya', 'にゅ': 'nyu', 'にょ': 'nyo',
'ひゃ': 'hya', 'ひゅ': 'hyu', 'ひょ': 'hyo',
'みゃ': 'mya', 'みゅ': 'myu', 'みょ': 'myo',
'りゃ': 'rya', 'りゅ': 'ryu', 'りょ': 'ryo',
'ぎゃ': 'gya', 'ぎゅ': 'gyu', 'ぎょ': 'gyo',
'じゃ': 'ja', 'じゅ': 'ju', 'じょ': 'jo',
'びゃ': 'bya', 'びゅ': 'byu', 'びょ': 'byo',
'ぴゃ': 'pya', 'ぴゅ': 'pyu', 'ぴょ': 'pyo',
}
# Hiragana digraphs with diacritics (subset already included above)
hiragana_digraphs_diacritics = {
'ぎゃ': 'gya', 'ぎゅ': 'gyu', 'ぎょ': 'gyo',
'じゃ': 'ja', 'じゅ': 'ju', 'じょ': 'jo',
'ぢゃ': 'ja', 'ぢゅ': 'ju', 'ぢょ': 'jo',
'びゃ': 'bya', 'びゅ': 'byu', 'びょ': 'byo',
'ぴゃ': 'pya', 'ぴゅ': 'pyu', 'ぴょ': 'pyo',
}
# Katakana monographs (basic)
katakana_monographs = {
'': 'a', '': 'i', '': 'u', '': 'e', '': 'o',
'': 'ka', '': 'ki', '': 'ku', '': 'ke', '': 'ko',
'': 'sa', '': 'shi', '': 'su', '': 'se', '': 'so',
'': 'ta', '': 'chi', '': 'tsu', '': 'te', '': 'to',
'': 'na', '': 'ni', '': 'nu', '': 'ne', '': 'no',
'': 'ha', '': 'hi', '': 'fu', '': 'he', '': 'ho',
'': 'ma', '': 'mi', '': 'mu', '': 'me', '': 'mo',
'': 'ya', '': 'yu', '': 'yo',
'': 'ra', '': 'ri', '': 'ru', '': 're', '': 'ro',
'': 'wa', '': 'wo', '': 'n'
}
# Katakana monographs with diacritics
katakana_monographs_diacritics = {
'': 'ga', '': 'gi', '': 'gu', '': 'ge', '': 'go',
'': 'za', '': 'ji', '': 'zu', '': 'ze', '': 'zo',
'': 'da', '': 'ji', '': 'zu', '': 'de', '': 'do',
'': 'ba', '': 'bi', '': 'bu', '': 'be', '': 'bo',
'': 'pa', '': 'pi', '': 'pu', '': 'pe', '': 'po'
}
# Katakana digraphs (yōon)
katakana_digraphs = {
'キャ': 'kya', 'キュ': 'kyu', 'キョ': 'kyo',
'シャ': 'sha', 'シュ': 'shu', 'ショ': 'sho',
'チャ': 'cha', 'チュ': 'chu', 'チョ': 'cho',
'ニャ': 'nya', 'ニュ': 'nyu', 'ニョ': 'nyo',
'ヒャ': 'hya', 'ヒュ': 'hyu', 'ヒョ': 'hyo',
'ミャ': 'mya', 'ミュ': 'myu', 'ミョ': 'myo',
'リャ': 'rya', 'リュ': 'ryu', 'リョ': 'ryo',
'ギャ': 'gya', 'ギュ': 'gyu', 'ギョ': 'gyo',
'ジャ': 'ja', 'ジュ': 'ju', 'ジョ': 'jo',
'ビャ': 'bya', 'ビュ': 'byu', 'ビョ': 'byo',
'ピャ': 'pya', 'ピュ': 'pyu', 'ピョ': 'pyo',
}
# Katakana digraphs with diacritics (subset already included above)
katakana_digraphs_diacritics = {
'ギャ': 'gya', 'ギュ': 'gyu', 'ギョ': 'gyo',
'ジャ': 'ja', 'ジュ': 'ju', 'ジョ': 'jo',
'ヂャ': 'ja', 'ヂュ': 'ju', 'ヂョ': 'jo',
'ビャ': 'bya', 'ビュ': 'byu', 'ビョ': 'byo',
'ピャ': 'pya', 'ピュ': 'pyu', 'ピョ': 'pyo',
}
# Extended Katakana for foreign sounds
katakana_extended = {
'ウィ': 'wi', 'ウェ': 'we', 'ウォ': 'wo',
'ヴァ': 'va', 'ヴィ': 'vi', '': 'vu', 'ヴェ': 've', 'ヴォ': 'vo',
'ファ': 'fa', 'フィ': 'fi', 'フェ': 'fe', 'フォ': 'fo', 'フュ': 'fyu',
'ティ': 'ti', 'ディ': 'di', 'トゥ': 'tu', 'ドゥ': 'du',
'チェ': 'che', 'シェ': 'she', 'ジェ': 'je',
'ツァ': 'tsa', 'ツィ': 'tsi', 'ツェ': 'tse', 'ツォ': 'tso',
'ティ': 'ti', 'ディ': 'di',
'デュ': 'dyu', 'テュ': 'tyu',
'イェ': 'ye', 'ウュ': 'wyu',
'キェ': 'kye', 'ギェ': 'gye',
'クァ': 'kwa', 'クィ': 'kwi', 'クェ': 'kwe', 'クォ': 'kwo',
'グァ': 'gwa', 'グィ': 'gwi', 'グェ': 'gwe', 'グォ': 'gwo',
'スィ': 'si', 'ズィ': 'zi',
'ツュ': 'tyu',
'ニェ': 'nye', 'ヒェ': 'hye', 'ビェ': 'bye', 'ピェ': 'pye',
'ミェ': 'mye', 'リェ': 'rye',
'フョ': 'fyo',
'ホゥ': 'hu',
'モゥ': 'mo',
'ロゥ': 'ro',
'シィ': 'syi', 'ジィ': 'jyi',
'ティ': 'tyi', 'ディ': 'dyi',
'トゥ': 'twu', 'ドゥ': 'dwu',
'テャ': 'tya', 'テュ': 'tyu', 'テョ': 'tyo',
'デャ': 'dya', 'デュ': 'dyu', 'デョ': 'dyo',
'フャ': 'fya', 'フュ': 'fyu', 'フョ': 'fyo',
'グァ': 'gwa',
}
def choose_kana_sets() -> Dict[str, str]:
"""Let user choose which kana sets to practice with. Supports multiple selections."""
print("\nWhich kana would you like to practice? (You can select multiple, e.g. 1,3,5)")
print("1. Hiragana Monographs")
print("2. Hiragana Monographs with Diacritics")
print("3. Hiragana Digraphs")
print("4. Hiragana Digraphs with Diacritics")
print("5. Katakana Monographs")
print("6. Katakana Monographs with Diacritics")
print("7. Katakana Digraphs")
print("8. Katakana Digraphs with Diacritics")
print("9. Extended Katakana (foreign sounds)")
print("0. All of the above")
choice = input("Enter your choice(s) (0-9, comma-separated): ")
selected = [c.strip() for c in choice.split(',') if c.strip()]
if '0' in selected or choice.strip() == '0':
combined = {}
combined.update(hiragana_monographs)
combined.update(hiragana_monographs_diacritics)
combined.update(hiragana_digraphs)
combined.update(hiragana_digraphs_diacritics)
combined.update(katakana_monographs)
combined.update(katakana_monographs_diacritics)
combined.update(katakana_digraphs)
combined.update(katakana_digraphs_diacritics)
combined.update(katakana_extended)
return combined
group_map = {
'1': hiragana_monographs,
'2': hiragana_monographs_diacritics,
'3': hiragana_digraphs,
'4': hiragana_digraphs_diacritics,
'5': katakana_monographs,
'6': katakana_monographs_diacritics,
'7': katakana_digraphs,
'8': katakana_digraphs_diacritics,
'9': katakana_extended,
}
combined = {}
for sel in selected:
if sel in group_map:
combined.update(group_map[sel])
if not combined:
print("No valid selection, defaulting to all groups.")
combined.update(hiragana_monographs)
combined.update(hiragana_monographs_diacritics)
combined.update(hiragana_digraphs)
combined.update(hiragana_digraphs_diacritics)
combined.update(katakana_monographs)
combined.update(katakana_monographs_diacritics)
combined.update(katakana_digraphs)
combined.update(katakana_digraphs_diacritics)
combined.update(katakana_extended)
return combined
def update_kana_db(conn, kana, is_correct, response_time):
c = conn.cursor()
c.execute('SELECT right, wrong, total_time FROM kana_stats WHERE kana = ?', (kana,))
row = c.fetchone()
if row is None:
right = 1 if is_correct else 0
wrong = 0 if is_correct else 1
c.execute('INSERT INTO kana_stats (kana, right, wrong, total_time) VALUES (?, ?, ?, ?)',
(kana, right, wrong, response_time))
else:
right, wrong, total_time = row
if is_correct:
right += 1
else:
wrong += 1
total_time += response_time
c.execute('UPDATE kana_stats SET right = ?, wrong = ?, total_time = ? WHERE kana = ?',
(right, wrong, total_time, kana))
conn.commit()
def practice_kana(kana_dict: Dict[str, str], conn=None) -> Dict[str, Dict[str, List[float] or int]]:
"""Practice kana and return per-character stats. Optionally update stats in DB."""
stats = {kana: {'right': 0, 'wrong': 0, 'times': []} for kana in kana_dict}
kana_list = list(kana_dict.items())
print("\nType 'quit' to end the practice session.")
print("Press Enter to start...")
input()
while True:
kana, romaji = random.choice(kana_list)
print(f"\nWhat is the romaji for: {kana}")
start_time = time.time()
answer = input("> ").lower().strip()
if answer == 'quit' or answer == '':
break
response_time = time.time() - start_time
is_correct = answer == romaji
if is_correct:
print("Correct!")
stats[kana]['right'] += 1
else:
print(f"Incorrect. The correct answer is: {romaji}")
stats[kana]['wrong'] += 1
stats[kana]['times'].append(response_time)
if conn is not None:
update_kana_db(conn, kana, is_correct, response_time)
return stats
def fetch_overall_stats(conn, kana_list):
c = conn.cursor()
stats = {}
for kana in kana_list:
c.execute('SELECT right, wrong, total_time FROM kana_stats WHERE kana = ?', (kana,))
row = c.fetchone()
if row:
right, wrong, total_time = row
stats[kana] = {'right': right, 'wrong': wrong, 'total_time': total_time}
else:
stats[kana] = {'right': 0, 'wrong': 0, 'total_time': 0.0}
return stats
def display_results(session_stats: Dict[str, Dict[str, List[float] or int]], conn=None) -> None:
"""Display per-character practice session and overall results."""
if not session_stats or all((v['right'] + v['wrong']) == 0 for v in session_stats.values()):
print("\nNo results to display.")
return
kana_list = list(session_stats.keys())
overall_stats = fetch_overall_stats(conn, kana_list) if conn else None
print("\n=== Per-Character Practice Results ===")
print(f"{'Kana':^6} | {'SessR':^5} | {'SessW':^5} | {'SessAcc':^8} | {'SessAvgT':^9} | {'AllR':^5} | {'AllW':^5} | {'AllAcc':^7} | {'AllAvgT':^8}")
print("-" * 74)
for kana in sorted(kana_list):
s = session_stats[kana]
sess_total = s['right'] + s['wrong']
if sess_total == 0:
continue
sess_acc = (s['right'] / sess_total) * 100
sess_avg_time = sum(s['times']) / sess_total if s['times'] else 0.0
if overall_stats:
o = overall_stats[kana]
all_total = o['right'] + o['wrong']
all_acc = (o['right'] / all_total) * 100 if all_total else 0.0
all_avg_time = o['total_time'] / all_total if all_total else 0.0
print(f"{kana:^6} | {s['right']:^5} | {s['wrong']:^5} | {sess_acc:7.1f}% | {sess_avg_time:8.2f} | {o['right']:^5} | {o['wrong']:^5} | {all_acc:6.1f}% | {all_avg_time:7.2f}")
else:
print(f"{kana:^6} | {s['right']:^5} | {s['wrong']:^5} | {sess_acc:7.1f}% | {sess_avg_time:8.2f}")
def init_db(db_path: str = "kana_stats.db"):
conn = sqlite3.connect(db_path)
c = conn.cursor()
c.execute('''
CREATE TABLE IF NOT EXISTS kana_stats (
kana TEXT PRIMARY KEY,
right INTEGER DEFAULT 0,
wrong INTEGER DEFAULT 0,
total_time REAL DEFAULT 0.0
)
''')
conn.commit()
return conn
def pad_display_width(s, width):
display_len = wcswidth(s)
if display_len >= width:
return s
return s + ' ' * (width - display_len)
def show_all_stats(conn, columns=3):
# Define groupings
kana_groups = [
("Hiragana Monographs", hiragana_monographs),
("Hiragana Monographs with Diacritics", hiragana_monographs_diacritics),
("Hiragana Digraphs", hiragana_digraphs),
("Hiragana Digraphs with Diacritics", hiragana_digraphs_diacritics),
("Katakana Monographs", katakana_monographs),
("Katakana Monographs with Diacritics", katakana_monographs_diacritics),
("Katakana Digraphs", katakana_digraphs),
("Katakana Digraphs with Diacritics", katakana_digraphs_diacritics),
("Extended Katakana (foreign sounds)", katakana_extended),
]
c = conn.cursor()
kana_col_width = 8
col_header = f"{'Kana':<{kana_col_width}} {'Right':>5} {'Wrong':>5} {'Total':>5} {'Acc%':>6} {'AvgT':>6}"
sep = ' '
print("\n=== Overall Kana Stats ===")
for group_name, group_dict in kana_groups:
sorted_kana = sorted(group_dict.keys())
if not sorted_kana:
continue
# Gather stats for this group
stats = {}
for kana in sorted_kana:
c.execute('SELECT right, wrong, total_time FROM kana_stats WHERE kana = ?', (kana,))
row = c.fetchone()
if row:
right, wrong, total_time = row
else:
right, wrong, total_time = 0, 0, 0.0
stats[kana] = {'right': right, 'wrong': wrong, 'total_time': total_time}
print(f"\n--- {group_name} ---")
print(sep.join([col_header]*columns))
print(sep.join(['-'*35]*columns))
# Prepare rows
rows = []
group_right = group_wrong = group_total = 0
group_time = 0.0
for kana in sorted_kana:
s = stats[kana]
total = s['right'] + s['wrong']
accuracy = (s['right'] / total) * 100 if total else 0.0
avg_time = s['total_time'] / total if total else 0.0
kana_disp = pad_display_width(kana, kana_col_width)
row = f"{kana_disp}{s['right']:>5} {s['wrong']:>5} {total:>5} {accuracy:6.1f} {avg_time:6.2f}"
rows.append(row)
group_right += s['right']
group_wrong += s['wrong']
group_total += total
group_time += s['total_time']
# Print in columns
for i in range(0, len(rows), columns):
print(sep.join(rows[i:i+columns]))
# Print summary under the last column
group_acc = (group_right / group_total) * 100 if group_total else 0.0
group_avg_time = group_time / group_total if group_total else 0.0
last_row_len = len(rows) % columns if len(rows) % columns != 0 else columns
empty_col = ' ' * 35
print(sep.join([empty_col] * (last_row_len - 1)), end='' if last_row_len > 1 else '')
summary = f"{'[Summary]':<{kana_col_width}}{group_right:>5} {group_wrong:>5} {group_total:>5} {group_acc:6.1f} {group_avg_time:6.2f}"
print(summary)
def show_most_wrong_kana(conn, top_n=10):
# Gather all kana from all groups
all_kana = {}
all_kana.update(hiragana_monographs)
all_kana.update(hiragana_monographs_diacritics)
all_kana.update(hiragana_digraphs)
all_kana.update(hiragana_digraphs_diacritics)
all_kana.update(katakana_monographs)
all_kana.update(katakana_monographs_diacritics)
all_kana.update(katakana_digraphs)
all_kana.update(katakana_digraphs_diacritics)
all_kana.update(katakana_extended)
c = conn.cursor()
stats = []
for kana in all_kana:
c.execute('SELECT right, wrong, total_time FROM kana_stats WHERE kana = ?', (kana,))
row = c.fetchone()
if row:
right, wrong, total_time = row
else:
right, wrong, total_time = 0, 0, 0.0
stats.append((kana, right, wrong, total_time))
# Sort by wrong answers descending, then by kana
stats.sort(key=lambda x: (-x[2], x[0]))
print(f"\n=== Kana with Most Wrong Answers (Top {top_n}) ===")
print(f"{'Kana':^6} | {'Wrong':^5} | {'Right':^5} | {'Total':^5} | {'Accuracy':^8} | {'Avg Time (s)':^12}")
print("-" * 56)
shown = 0
for kana, right, wrong, total_time in stats:
total = right + wrong
if wrong == 0:
continue
accuracy = (right / total) * 100 if total else 0.0
avg_time = total_time / total if total else 0.0
print(f"{kana:^6} | {wrong:^5} | {right:^5} | {total:^5} | {accuracy:7.1f}% | {avg_time:11.2f}")
shown += 1
if shown >= top_n:
break
if shown == 0:
print("No wrong answers yet! Great job!")
def stats_menu(conn):
while True:
print("\n=== Stats Menu ===")
print("1. Show all stats")
print("2. Show most wrong kana")
print("q. Return to main menu")
choice = input("Enter your choice (1, 2, or q): ").strip().lower()
if choice == '1':
show_all_stats(conn)
input("\nPress Enter to return to the stats menu...")
elif choice == '2':
show_most_wrong_kana(conn)
input("\nPress Enter to return to the stats menu...")
elif choice == 'q':
break
else:
print("Invalid choice. Please enter 1, 2, or q.")
def main_menu():
conn = init_db()
while True:
print("\n=== Kana Practice Main Menu ===")
print("1. Practice kana")
print("2. Show stats")
print("q. Quit")
choice = input("Enter your choice (1, 2, or q): ").strip().lower()
if choice == '1':
kana_dict = choose_kana_sets()
stats = practice_kana(kana_dict, conn)
display_results(stats, conn)
input("\nPress Enter to return to the main menu...")
elif choice == '2':
stats_menu(conn)
elif choice == 'q':
print("Thanks for practicing! さようなら!")
break
else:
print("Invalid choice. Please enter 1, 2, or q.")
if __name__ == "__main__":
main_menu()