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>
125 lines
4.2 KiB
Dart
125 lines
4.2 KiB
Dart
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);
|
|
expect(eat['labels'], ['jlpt:n5', 'grade:2', 'radical:184']);
|
|
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('ひと')));
|
|
});
|
|
|
|
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']);
|
|
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));
|
|
});
|
|
}
|