feat(curator): KANJIDIC2 and the kana as a dictionary of characters; KanjiVG's strokes for the app
A dictionary file now says whether it holds words or characters (`kind`).
The kanjidic adapter turns KANJIDIC2 into one entry per kanji that has a
Japanese reading or an English meaning (12,633), with its readings,
meanings, school grade, strokes, frequency and JLPT level (Jonathan
Waller's, through kanji-data's kanji.json), labelled `jlpt:nX`,
`grade:N`/`grade:secondary`, followed by the 71 hiragana and katakana of
the kana table with their romaji. A kun reading is found whole; its kanji
part only as a loose key, so typing "ta" brings た before every kanji read
so.
`curate kanjivg` writes KanjiVG's strokes and components as the app's
stroke files, one gzipped JSON file per 256 code points, keyed by the code
point in each entry's id (KanjiVG's 橅 names itself 機).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-09 14:37:22 +02:00
|
|
|
import 'dart:convert';
|
|
|
|
|
import 'dart:io';
|
|
|
|
|
|
|
|
|
|
import 'package:halkyon_learn_curator/src/sources/kanjidic.dart';
|
|
|
|
|
import 'package:test/test.dart';
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
late List<Map<String, Object?>> lines;
|
|
|
|
|
|
|
|
|
|
setUpAll(() async {
|
|
|
|
|
final jlpt = await readKanjiJlpt(
|
|
|
|
|
File('test/fixtures/kanji-data-sample.json'),
|
|
|
|
|
);
|
|
|
|
|
lines = [
|
|
|
|
|
for (final line in kanjidicDictionary(
|
|
|
|
|
await File('test/fixtures/kanjidic2-sample.xml').readAsString(),
|
|
|
|
|
jlpt: jlpt,
|
|
|
|
|
))
|
|
|
|
|
jsonDecode(line) as Map<String, Object?>,
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Map<String, Object?> entry(String character) =>
|
|
|
|
|
lines.skip(1).firstWhere((line) => line['id'] == character);
|
|
|
|
|
Map<String, Object?> fields(String character) =>
|
|
|
|
|
entry(character)['entry']! as Map<String, Object?>;
|
|
|
|
|
|
|
|
|
|
test('the header says the file holds characters, and whose they are', () {
|
|
|
|
|
final header = lines.first;
|
|
|
|
|
expect(header['source'], 'kanjidic');
|
|
|
|
|
expect(header['kind'], 'characters');
|
|
|
|
|
expect(header['sourceVersion'], '2026-282');
|
|
|
|
|
expect((header['language'], header['glossLanguage']), ('ja', 'en'));
|
|
|
|
|
expect(header['license'], 'CC-BY-SA-4.0');
|
|
|
|
|
expect(
|
|
|
|
|
[
|
|
|
|
|
for (final credit in header['credits']! as List)
|
|
|
|
|
(credit as Map)['name'],
|
|
|
|
|
],
|
|
|
|
|
['KANJIDIC2', 'JLPT kanji lists'],
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('a kanji: its readings, English meanings only, grade, strokes, '
|
|
|
|
|
'frequency and JLPT level', () {
|
|
|
|
|
final eat = entry('食');
|
|
|
|
|
expect(eat['headword'], '食');
|
|
|
|
|
expect(eat['reading'], isNull);
|
feat(dictionary): the radicals, each listing the kanji written with it
Beside Words and Kanji & kana, a Radicals tab lists the 214 Kangxi
radicals by their strokes, with the forms Japanese writes them as inside
kanji (亻, 氵, 艹, …). One chosen lists the kanji KANJIDIC2 files under
it, in the dictionary's order, to add or to see how one is written; the
tab shares the characters' bloc, so a kanji added there shows as held in
a search too.
The curator labels each kanji with its classical radical (`radical:N`),
so the server's existing label queries serve the tab unchanged. Its
files now say which conversion made them (`.conversion`): an instance
running a curator that converts differently converts again at once,
rather than at its weekly update, so the radicals appear right after a
deploy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 02:13:06 +02:00
|
|
|
expect(eat['labels'], ['jlpt:n5', 'grade:2', 'radical:184']);
|
feat(curator): KANJIDIC2 and the kana as a dictionary of characters; KanjiVG's strokes for the app
A dictionary file now says whether it holds words or characters (`kind`).
The kanjidic adapter turns KANJIDIC2 into one entry per kanji that has a
Japanese reading or an English meaning (12,633), with its readings,
meanings, school grade, strokes, frequency and JLPT level (Jonathan
Waller's, through kanji-data's kanji.json), labelled `jlpt:nX`,
`grade:N`/`grade:secondary`, followed by the 71 hiragana and katakana of
the kana table with their romaji. A kun reading is found whole; its kanji
part only as a loose key, so typing "ta" brings た before every kanji read
so.
`curate kanjivg` writes KanjiVG's strokes and components as the app's
stroke files, one gzipped JSON file per 256 code points, keyed by the code
point in each entry's id (KanjiVG's 橅 names itself 機).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-09 14:37:22 +02:00
|
|
|
expect(fields('食'), {
|
|
|
|
|
'meanings': ['eat', 'food'],
|
|
|
|
|
'on': ['ショク', 'ジキ'],
|
|
|
|
|
'kun': ['く.う', 'く.らう', 'た.べる', 'は.む'],
|
|
|
|
|
'grade': 2,
|
|
|
|
|
'strokes': 9,
|
|
|
|
|
'frequency': 328,
|
|
|
|
|
'jlpt': 5,
|
|
|
|
|
});
|
|
|
|
|
expect(fields('一')['meanings'], isNot(contains('un')));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('a search finds a kanji by its on readings in either kana and in '
|
|
|
|
|
'romaji, by whole kun readings and by its meanings; the kanji part '
|
|
|
|
|
'of a kun reading only loosely', () {
|
|
|
|
|
final eat = entry('食');
|
|
|
|
|
expect(
|
|
|
|
|
eat['keys'],
|
|
|
|
|
containsAll([
|
|
|
|
|
'食',
|
|
|
|
|
'ショク',
|
|
|
|
|
'しょく',
|
|
|
|
|
'shoku',
|
|
|
|
|
'たべる',
|
|
|
|
|
'taberu',
|
|
|
|
|
'くらう',
|
|
|
|
|
'eat',
|
|
|
|
|
'food',
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
expect(eat['keys'], isNot(contains('た')));
|
|
|
|
|
expect(eat['looseKeys'], containsAll(['た', 'ta', 'く', 'ku']));
|
|
|
|
|
// ひと- and ひと.つ: the suffix's dash goes, its kanji part is whole.
|
|
|
|
|
expect(entry('一')['keys'], containsAll(['ひと', 'ひとつ', 'hitotsu']));
|
|
|
|
|
expect(entry('一')['looseKeys'], isNot(contains('ひと')));
|
|
|
|
|
});
|
|
|
|
|
|
feat(dictionary): the radicals, each listing the kanji written with it
Beside Words and Kanji & kana, a Radicals tab lists the 214 Kangxi
radicals by their strokes, with the forms Japanese writes them as inside
kanji (亻, 氵, 艹, …). One chosen lists the kanji KANJIDIC2 files under
it, in the dictionary's order, to add or to see how one is written; the
tab shares the characters' bloc, so a kanji added there shows as held in
a search too.
The curator labels each kanji with its classical radical (`radical:N`),
so the server's existing label queries serve the tab unchanged. Its
files now say which conversion made them (`.conversion`): an instance
running a curator that converts differently converts again at once,
rather than at its weekly update, so the radicals appear right after a
deploy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 02:13:06 +02:00
|
|
|
test('levels, grades and radicals label the kanji; level and grade rank '
|
|
|
|
|
'them: JLPT level, then school grade, then frequency', () {
|
|
|
|
|
expect(entry('亜')['labels'], ['jlpt:n1', 'grade:secondary', 'radical:7']);
|
|
|
|
|
// KANJIDIC2's classical radical, not Nelson's.
|
|
|
|
|
expect(entry('唖')['labels'], ['radical:30']);
|
feat(curator): KANJIDIC2 and the kana as a dictionary of characters; KanjiVG's strokes for the app
A dictionary file now says whether it holds words or characters (`kind`).
The kanjidic adapter turns KANJIDIC2 into one entry per kanji that has a
Japanese reading or an English meaning (12,633), with its readings,
meanings, school grade, strokes, frequency and JLPT level (Jonathan
Waller's, through kanji-data's kanji.json), labelled `jlpt:nX`,
`grade:N`/`grade:secondary`, followed by the 71 hiragana and katakana of
the kana table with their romaji. A kun reading is found whole; its kanji
part only as a loose key, so typing "ta" brings た before every kanji read
so.
`curate kanjivg` writes KanjiVG's strokes and components as the app's
stroke files, one gzipped JSON file per 256 code points, keyed by the code
point in each entry's id (KanjiVG's 橅 names itself 機).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-09 14:37:22 +02:00
|
|
|
final ranks = [
|
|
|
|
|
for (final kanji in ['一', '食', '亜', '唖']) entry(kanji)['rank']! as int,
|
|
|
|
|
];
|
|
|
|
|
expect(ranks, orderedEquals([...ranks]..sort()));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('a character with no Japanese reading and no English meaning is '
|
|
|
|
|
'left out', () {
|
|
|
|
|
expect(lines.skip(1).where((line) => line['id'] == '僲'), isEmpty);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('hiragana and katakana follow, the kana table each, before every '
|
|
|
|
|
'kanji, found by their romaji', () {
|
|
|
|
|
final kana = [
|
|
|
|
|
for (final line in lines.skip(1))
|
|
|
|
|
if ((line['labels']! as List).any(['hiragana', 'katakana'].contains))
|
|
|
|
|
line,
|
|
|
|
|
];
|
|
|
|
|
expect(kana, hasLength(142));
|
|
|
|
|
expect(kana.first['id'], 'あ');
|
|
|
|
|
expect(kana[71]['id'], 'ア');
|
|
|
|
|
expect(kana.last['id'], 'ポ');
|
|
|
|
|
expect(entry('ア')['labels'], ['katakana']);
|
|
|
|
|
expect(entry('ア')['keys'], containsAll(['ア', 'あ', 'a']));
|
|
|
|
|
expect(fields('ぢ'), {'romaji': 'ji', 'script': 'hiragana'});
|
|
|
|
|
// を is "wo" in the table, found by "o" too.
|
|
|
|
|
expect(fields('を')['romaji'], 'wo');
|
|
|
|
|
expect(entry('を')['keys'], containsAll(['wo', 'o']));
|
|
|
|
|
final lastKana = kana
|
|
|
|
|
.map((line) => line['rank']! as int)
|
|
|
|
|
.reduce((a, b) => a > b ? a : b);
|
|
|
|
|
expect(entry('食')['rank']! as int, greaterThan(lastKana));
|
|
|
|
|
});
|
|
|
|
|
}
|