EDRDG makes JMdict_e_examp.gz anew every day: JMdict in English with Tatoeba's sentences on the meanings, the file jmdict-simplified's examples edition is made from once a week. The curator now reads it directly (a file not named .json is read as XML), streaming 72 MB of XML in some 30 seconds and 300 MB, each entry put into the shape jmdict-simplified gives it and made an entry by the same code: against jmdict-simplified's conversion of four days before, the entries differ only by JMdict's edits since. JMdict's entities stay codes (`&v1;` is `v1`), text cut between two chunks is joined before its entities are read (the xml package's normalizeEvents), and a sense without parts of speech has the previous sense's, as JMdict's DTD says. Its version is the day it was made. So an instance can keep its dictionaries current from the source alone, as EDRDG's licence asks of servers (at least monthly). 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>
124 lines
3.9 KiB
Dart
124 lines
3.9 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:halkyon_learn_curator/src/sources/jlpt.dart';
|
|
import 'package:halkyon_learn_curator/src/sources/jmdict_xml.dart';
|
|
import 'package:test/test.dart';
|
|
import 'package:xml/xml.dart';
|
|
|
|
/// [text] as a stream of pieces [size] characters long.
|
|
Stream<String> pieces(String text, int size) => Stream.fromIterable([
|
|
for (var at = 0; at < text.length; at += size)
|
|
text.substring(at, (at + size).clamp(0, text.length)),
|
|
]);
|
|
|
|
void main() {
|
|
late String xml;
|
|
late List<Map<String, Object?>> lines;
|
|
|
|
setUpAll(() async {
|
|
xml = await File('test/fixtures/jmdict-e-examp-sample.xml').readAsString();
|
|
final jlpt = await readJlptLevels(Directory('test/fixtures/jlpt'));
|
|
lines = [
|
|
for (final line in await jmdictXmlDictionary(
|
|
Stream.value(xml),
|
|
jlpt: jlpt,
|
|
examples: true,
|
|
).toList())
|
|
jsonDecode(line) as Map<String, Object?>,
|
|
];
|
|
});
|
|
|
|
Map<String, Object?> entry(String id) =>
|
|
lines.skip(1).firstWhere((line) => line['id'] == id);
|
|
List<Map<String, Object?>> senses(String id) => [
|
|
for (final sense in (entry(id)['entry']! as Map)['senses'] as List)
|
|
sense as Map<String, Object?>,
|
|
];
|
|
|
|
test("the header names EDRDG's JMdict and the day it was made, and "
|
|
'credits the sentences and the levels', () {
|
|
final header = lines.first;
|
|
expect(header['source'], 'jmdict');
|
|
expect(header['kind'], 'words');
|
|
expect(header['sourceVersion'], '2026-10-09');
|
|
expect(
|
|
[
|
|
for (final credit in header['credits']! as List)
|
|
(credit as Map)['name'],
|
|
],
|
|
['JMdict', 'JLPT vocabulary lists', 'Tatoeba'],
|
|
);
|
|
expect(lines, hasLength(6));
|
|
});
|
|
|
|
test('a word as from the JSON: its headword and reading, labels, keys, '
|
|
"and its senses with JMdict's codes as codes", () {
|
|
final taberu = entry('1358280');
|
|
|
|
expect((taberu['headword'], taberu['reading']), ('食べる', 'たべる'));
|
|
expect(taberu['labels'], ['common', 'jlpt:n5']);
|
|
expect(taberu['keys'], containsAll(['食べる', 'たべる', 'taberu', 'eat']));
|
|
expect(senses('1358280').first['pos'], ['v1', 'vt']);
|
|
});
|
|
|
|
test("a sense's sentences from Tatoeba, the word as it is in each", () {
|
|
expect((senses('1358280').first['examples']! as List).first, {
|
|
'text': '食べる',
|
|
'ja': 'もっと果物を食べるべきです。',
|
|
'en': 'You should eat more fruit.',
|
|
'tatoeba': '193344',
|
|
});
|
|
});
|
|
|
|
test('a word usually written in kana has a kana headword; a sense kept to '
|
|
'one reading says so', () {
|
|
final sono = entry('1006830');
|
|
|
|
expect(sono['headword'], 'その');
|
|
expect(senses('1006830').first['misc'], ['uk']);
|
|
expect(senses('1006830').last['readings'], ['その']);
|
|
});
|
|
|
|
test('the reading is the one restricted to the headword', () {
|
|
expect(entry('1000110')['reading'], 'シーディープレーヤー');
|
|
});
|
|
|
|
test(
|
|
'entities cut between two pieces of the stream are read whole',
|
|
() async {
|
|
final again = [
|
|
for (final line in await jmdictXmlDictionary(
|
|
pieces(xml, 7),
|
|
examples: true,
|
|
).toList())
|
|
jsonDecode(line) as Map<String, Object?>,
|
|
];
|
|
expect(
|
|
[for (final line in again.skip(1)) line['entry']],
|
|
[for (final line in lines.skip(1)) line['entry']],
|
|
);
|
|
},
|
|
);
|
|
|
|
test('a sense without parts of speech has those of the sense before', () {
|
|
final word = jmdictWordOf(
|
|
XmlDocument.parse('''
|
|
<entry><ent_seq>1</ent_seq><r_ele><reb>あ</reb></r_ele>
|
|
<sense><pos>n</pos><gloss>one</gloss></sense>
|
|
<sense><gloss>two</gloss></sense>
|
|
<sense><pos>adv</pos><gloss>three</gloss></sense></entry>''').rootElement,
|
|
);
|
|
expect(
|
|
[
|
|
for (final sense in word['sense']! as List)
|
|
(sense as Map)['partOfSpeech'],
|
|
],
|
|
[
|
|
['n'],
|
|
['n'],
|
|
['adv'],
|
|
],
|
|
);
|
|
});
|
|
}
|