17 Commits

Author SHA1 Message Date
oysteikt 1aea3c5874 WIP: add kanjivg data
Build and test / build (push) Successful in 5m47s
2026-06-04 00:54:03 +09:00
oysteikt a46d61c335 search: let radical search API take RadkfileRadical inputs
Build and test / build (push) Successful in 5m11s
2026-06-03 20:18:02 +09:00
oysteikt f3dba3ffab const_data/radicals: store both formal and informal variant of radical
Build and test / build (push) Successful in 5m47s
2026-06-03 17:42:09 +09:00
oysteikt 5556a73899 test/models: test json serialization roundtrips for all models
Build and test / build (push) Successful in 5m17s
2026-06-03 17:01:53 +09:00
oysteikt 541680b95d models/word_search: clean up json de/serialization 2026-06-03 17:01:17 +09:00
oysteikt 505380d2cc lib/_data_ingestion: add xref exception list for jmdict
Build and test / build (push) Successful in 7m30s
2026-06-02 03:19:52 +09:00
oysteikt c4abcabca2 search: add api for fetching datasource versions
Build and test / build (push) Failing after 3m45s
2026-06-02 02:56:51 +09:00
oysteikt 5629198539 version: add const schema version number
Build and test / build (push) Failing after 3m55s
2026-06-02 02:36:59 +09:00
oysteikt 7360144136 flake.lock: bump, pubspec.{yaml,lock}: bump 2026-06-02 02:36:59 +09:00
oysteikt c43285c78a .gitea/workflows/build-and-test: don't autoupdate datasources
Build and test / build (push) Successful in 5m6s
2026-05-06 00:19:59 +09:00
oysteikt c22e96b7f9 flake.lock: bump nixpkgs
Build and test / build (push) Failing after 3m54s
2026-05-06 00:13:32 +09:00
oysteikt d3516495ab data_ingestion/jmdict: throw proper errors on invalid xrefs 2026-05-05 23:48:05 +09:00
oysteikt be4dd72dcb migrations/JMdict: no default value for EntryScore.score
Build and test / build (push) Failing after 4m4s
2026-05-05 23:23:01 +09:00
oysteikt 32ec34a150 migrations/JMdict: only index common entries in EntryScore_byCommon 2026-05-05 23:22:37 +09:00
oysteikt bd0822b740 .gitea/workflows/build-and-test: remove duplicate nix config
Build and test / build (push) Successful in 5m32s
2026-04-29 08:39:52 +09:00
oysteikt 8827893101 search: prefer Iterable over List in some public APIs
Build and test / build (push) Successful in 7m51s
2026-04-14 17:55:40 +09:00
oysteikt 28c4403e2d test/romaji_transliteration: add tests for iteration marks and yori ligature
Build and test / build (push) Successful in 6m56s
2026-04-13 22:06:03 +09:00
29 changed files with 1194 additions and 325 deletions
+2 -3
View File
@@ -16,15 +16,14 @@ jobs:
uses: https://github.com/cachix/install-nix-action@v31
with:
extra_nix_config: |
experimental-features = nix-command flakes
show-trace = true
max-jobs = auto
trusted-users = root
experimental-features = nix-command flakes
build-users-group =
- name: Update database inputs
run: nix flake update datasources
# - name: Update database inputs
# run: nix flake update datasources
- name: Build database
run: nix build .#database -L
Generated
+24 -7
View File
@@ -7,11 +7,11 @@
]
},
"locked": {
"lastModified": 1776081209,
"narHash": "sha256-zR1115tcOPnYLk6NznSf7YslyaJLc/MGayEHShitx18=",
"lastModified": 1780302182,
"narHash": "sha256-IfC+dpdjjlkzrWlm+p851T43GsR04wMAPqGn63jisJ4=",
"ref": "refs/heads/main",
"rev": "7fe3552bb16e1d315c0b27b243e5eb53cd9e86fc",
"revCount": 13,
"rev": "c116674dd1e0b879660e6237e54904aa825d4511",
"revCount": 29,
"type": "git",
"url": "https://git.pvv.ntnu.no/Mugiten/datasources.git"
},
@@ -20,13 +20,29 @@
"url": "https://git.pvv.ntnu.no/Mugiten/datasources.git"
}
},
"kanjivg-src": {
"flake": false,
"locked": {
"lastModified": 1775218066,
"narHash": "sha256-iYv9xakgoGt/JwwdKDUCpSAF36hBtKlX9oN7xiLowjs=",
"ref": "refs/heads/master",
"rev": "544d319f79348c092d567b662f27f33dacfa60cd",
"revCount": 2215,
"type": "git",
"url": "https://git.pvv.ntnu.no/mugiten/kanjivg.git"
},
"original": {
"type": "git",
"url": "https://git.pvv.ntnu.no/mugiten/kanjivg.git"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1775423009,
"narHash": "sha256-vPKLpjhIVWdDrfiUM8atW6YkIggCEKdSAlJPzzhkQlw=",
"lastModified": 1779560665,
"narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "68d8aa3d661f0e6bd5862291b5bb263b2a6595c9",
"rev": "64c08a7ca051951c8eae34e3e3cb1e202fe36786",
"type": "github"
},
"original": {
@@ -38,6 +54,7 @@
"root": {
"inputs": {
"datasources": "datasources",
"kanjivg-src": "kanjivg-src",
"nixpkgs": "nixpkgs"
}
}
+6
View File
@@ -8,12 +8,18 @@
url = "git+https://git.pvv.ntnu.no/Mugiten/datasources.git";
inputs.nixpkgs.follows = "nixpkgs";
};
kanjivg-src = {
url = "git+https://git.pvv.ntnu.no/mugiten/kanjivg.git";
flake = false;
};
};
outputs = {
self,
nixpkgs,
datasources,
kanjivg-src,
}: let
inherit (nixpkgs) lib;
systems = [
+15 -6
View File
@@ -45,9 +45,7 @@ class KanjiElement extends Element {
});
@override
Map<String, Object?> get sqlValue => {
...super.sqlValue,
};
Map<String, Object?> get sqlValue => {...super.sqlValue};
}
class ReadingElement extends Element {
@@ -129,6 +127,19 @@ class XRefParts {
'readingRef': readingRef,
'senseOrderNum': senseOrderNum,
};
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is XRefParts &&
other.kanjiRef == kanjiRef &&
other.readingRef == readingRef &&
other.senseOrderNum == senseOrderNum;
}
@override
int get hashCode => Object.hash(kanjiRef, readingRef, senseOrderNum);
}
class XRef {
@@ -168,9 +179,7 @@ class Sense extends SQLWritable {
});
@override
Map<String, Object?> get sqlValue => {
'senseId': senseId,
};
Map<String, Object?> get sqlValue => {'senseId': senseId};
bool get isEmpty =>
antonyms.isEmpty &&
+65 -9
View File
@@ -15,6 +15,12 @@ class ResolvedXref {
const ResolvedXref(this.entry, this.ambiguous);
}
// A constant map of xref parts to jmdict id for unresolvable xrefs.
final xrefExceptions = {
// NOTE: see https://www.edrdg.org/jmwsgi/entr.py?svc=jmdict&g=2870981.1~2369718 for details
XRefParts(kanjiRef: 'プレストレスト', readingRef: 'コンクリート'): 2472380,
};
/// Resolves an xref (pair of kanji, optionally reading, and optionally sense number) to an a specific
/// JMdict entry, if possible.
///
@@ -27,15 +33,65 @@ ResolvedXref resolveXref(
SplayTreeMap<String, Set<Entry>> entriesByReading,
XRefParts xref,
) {
List<Entry> candidateEntries = switch ((xref.kanjiRef, xref.readingRef)) {
(null, null) => throw Exception(
'Xref $xref has no kanji or reading reference',
),
(final String k, null) => entriesByKanji[k]!.toList(),
(null, final String r) => entriesByReading[r]!.toList(),
(final String k, final String r) =>
entriesByKanji[k]!.intersection(entriesByReading[r]!).toList(),
};
late List<Entry> candidateEntries;
if (xrefExceptions.containsKey(xref)) {
final exceptionEntryId = xrefExceptions[xref]!;
// NOTE: this is slow, but we have few exceptions. Let's wait for JMdict XML-NG to be released so we can delete this :)
final exceptionEntry =
entriesByKanji.values
.expand((set) => set)
.firstWhereOrNull((entry) => entry.entryId == exceptionEntryId) ??
entriesByReading.values
.expand((set) => set)
.firstWhereOrNull((entry) => entry.entryId == exceptionEntryId);
if (exceptionEntry != null) {
return ResolvedXref(exceptionEntry, false);
} else {
throw Exception(
'Xref $xref matches an exception entry ID $exceptionEntryId, but that entry was not found among the candidates.',
);
}
}
switch ((xref.kanjiRef, xref.readingRef)) {
case (null, null):
throw Exception('Xref $xref has no kanji or reading reference');
case (final String k, null):
if (!entriesByKanji.containsKey(k)) {
throw Exception(
'Xref $xref has kanji reference "$k" but no entries found with that kanji',
);
}
candidateEntries = entriesByKanji[k]!.toList();
break;
case (null, final String r):
if (!entriesByReading.containsKey(r)) {
throw Exception(
'Xref $xref has reading reference "$r" but no entries found with that reading',
);
}
candidateEntries = entriesByReading[r]!.toList();
break;
case (final String k, final String r):
if (!entriesByKanji.containsKey(k)) {
throw Exception(
'Xref $xref has kanji reference "$k" but no entries found with that kanji',
);
}
if (!entriesByReading.containsKey(r)) {
throw Exception(
'Xref $xref has reading reference "$r" but no entries found with that reading',
);
}
candidateEntries = entriesByKanji[k]!
.intersection(entriesByReading[r]!)
.toList();
}
// Filter out entries that don't have the number of senses specified in the xref
if (xref.senseOrderNum != null) {
+92
View File
@@ -0,0 +1,92 @@
import 'package:jadb/_data_ingestion/sql_writable.dart';
/// Enum set in the kvg:position attribute, used by `<g>` elements in the KanjiVG SVG files.
enum KanjiPathGroupPosition {
bottom,
kamae,
kamaec,
left,
middle,
nyo,
nyoc,
right,
tare,
tarec,
top,
}
/// Contents of a \<g> element in the KanjiVG SVG files.
class KanjiPathGroupTreeNode extends SQLWritable {
final String id;
final List<KanjiPathGroupTreeNode> children;
final String? element;
final String? original;
final KanjiPathGroupPosition? position;
final String? radical;
final int? part;
KanjiPathGroupTreeNode({
required this.id,
this.children = const [],
this.element,
this.original,
this.position,
this.radical,
this.part,
});
@override
Map<String, Object?> get sqlValue => {
'id': id,
'element': element,
'original': original,
'position': position?.name,
'radical': radical,
'part': part,
};
}
/// Contents of a `<text>` element in the StrokeNumber's group in the KanjiVG SVG files
class KanjiStrokeNumber extends SQLWritable {
final int num;
final double x;
final double y;
KanjiStrokeNumber(this.num, this.x, this.y);
@override
Map<String, Object?> get sqlValue => {'num': num, 'x': x, 'y': y};
}
/// Contents of a `<path>` element in the KanjiVG SVG files
class KanjiVGPath extends SQLWritable {
final String id;
final String type;
final String svgPath;
KanjiVGPath({required this.id, required this.type, required this.svgPath});
@override
Map<String, Object?> get sqlValue => {
'id': id,
'type': type,
'svgPath': svgPath,
};
}
class KanjiVGItem extends SQLWritable {
final String character;
final List<KanjiVGPath> paths;
final List<KanjiStrokeNumber> strokeNumbers;
final List<KanjiPathGroupTreeNode> pathGroups;
KanjiVGItem({
required this.character,
required this.paths,
required this.strokeNumbers,
required this.pathGroups,
});
@override
Map<String, Object?> get sqlValue => {'character': character};
}
@@ -0,0 +1,7 @@
import 'package:sqflite_common/sqflite.dart';
Future<void> seedKanjiVGData(Iterable<String> xmlContents, Database db) async {
final b = db.batch();
await b.commit(noResult: true);
}
+297 -204
View File
@@ -1,217 +1,310 @@
const Map<int, List<String>> radicals = {
1: ['', '', '', '', '', ''],
import 'package:collection/collection.dart';
class RadkfileRadical {
/// The formal variant of the radical
///
/// This is the one you want to use for searching.
final String formalVariant;
/// The informal variant of the radical (if it differs from the formal one).
///
/// This is the one you should display to the user.
final String? informalVariant;
/// The number of strokes in this radical.
final int strokeCount;
const RadkfileRadical({
required this.formalVariant,
this.informalVariant,
required this.strokeCount,
});
}
const Map<int, List<RadkfileRadical>> radicals = {
1: [
RadkfileRadical(formalVariant: '', strokeCount: 1),
RadkfileRadical(formalVariant: '', strokeCount: 1),
RadkfileRadical(formalVariant: '', strokeCount: 1),
RadkfileRadical(formalVariant: '', strokeCount: 1),
RadkfileRadical(formalVariant: '', strokeCount: 1),
RadkfileRadical(formalVariant: '', strokeCount: 1),
],
2: [
'',
'',
'',
'',
'𠆢',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'𠂉',
RadkfileRadical(formalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', informalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', informalVariant: '𠆢', strokeCount: 2),
RadkfileRadical(formalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', strokeCount: 2),
RadkfileRadical(formalVariant: '', strokeCount: 2),
],
3: [
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'广',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
RadkfileRadical(formalVariant: '', informalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', informalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '广', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', informalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', informalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', informalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', informalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', informalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', informalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', informalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
RadkfileRadical(formalVariant: '', strokeCount: 3),
],
4: [
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
RadkfileRadical(formalVariant: '', informalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
RadkfileRadical(formalVariant: '', strokeCount: 4),
],
5: [
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
RadkfileRadical(formalVariant: '', strokeCount: 5),
RadkfileRadical(formalVariant: '', strokeCount: 5),
RadkfileRadical(formalVariant: '', strokeCount: 5),
RadkfileRadical(formalVariant: '', strokeCount: 5),
RadkfileRadical(formalVariant: '', strokeCount: 5),
RadkfileRadical(formalVariant: '', strokeCount: 5),
RadkfileRadical(formalVariant: '', strokeCount: 5),
RadkfileRadical(formalVariant: '', strokeCount: 5),
RadkfileRadical(formalVariant: '', strokeCount: 5),
RadkfileRadical(formalVariant: '', strokeCount: 5),
RadkfileRadical(formalVariant: '', strokeCount: 5),
RadkfileRadical(formalVariant: '', strokeCount: 5),
RadkfileRadical(formalVariant: '', strokeCount: 5),
RadkfileRadical(formalVariant: '', strokeCount: 5),
RadkfileRadical(formalVariant: '', strokeCount: 5),
RadkfileRadical(formalVariant: '', strokeCount: 5),
RadkfileRadical(formalVariant: '', strokeCount: 5),
RadkfileRadical(formalVariant: '', strokeCount: 5),
RadkfileRadical(formalVariant: '', strokeCount: 5),
RadkfileRadical(formalVariant: '', strokeCount: 5),
RadkfileRadical(formalVariant: '', strokeCount: 5),
RadkfileRadical(formalVariant: '', strokeCount: 5),
RadkfileRadical(formalVariant: '', strokeCount: 5),
RadkfileRadical(formalVariant: '', strokeCount: 5),
RadkfileRadical(formalVariant: '', strokeCount: 5),
RadkfileRadical(formalVariant: '', strokeCount: 5),
RadkfileRadical(formalVariant: '', strokeCount: 5),
RadkfileRadical(formalVariant: '', strokeCount: 5),
],
6: [
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'西',
RadkfileRadical(formalVariant: '', strokeCount: 6),
RadkfileRadical(formalVariant: '', strokeCount: 6),
RadkfileRadical(formalVariant: '', strokeCount: 6),
RadkfileRadical(formalVariant: '', strokeCount: 6),
RadkfileRadical(formalVariant: '', strokeCount: 6),
RadkfileRadical(formalVariant: '', strokeCount: 6),
RadkfileRadical(formalVariant: '', strokeCount: 6),
RadkfileRadical(formalVariant: '', strokeCount: 6),
RadkfileRadical(formalVariant: '', strokeCount: 6),
RadkfileRadical(formalVariant: '', strokeCount: 6),
RadkfileRadical(formalVariant: '', strokeCount: 6),
RadkfileRadical(formalVariant: '', strokeCount: 6),
RadkfileRadical(formalVariant: '', strokeCount: 6),
RadkfileRadical(formalVariant: '', strokeCount: 6),
RadkfileRadical(formalVariant: '', strokeCount: 6),
RadkfileRadical(formalVariant: '', strokeCount: 6),
RadkfileRadical(formalVariant: '', strokeCount: 6),
RadkfileRadical(formalVariant: '', strokeCount: 6),
RadkfileRadical(formalVariant: '', strokeCount: 6),
RadkfileRadical(formalVariant: '', strokeCount: 6),
RadkfileRadical(formalVariant: '', strokeCount: 6),
RadkfileRadical(formalVariant: '', strokeCount: 6),
RadkfileRadical(formalVariant: '', strokeCount: 6),
RadkfileRadical(formalVariant: '', strokeCount: 6),
RadkfileRadical(formalVariant: '西', strokeCount: 6),
],
7: [
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
RadkfileRadical(formalVariant: '', strokeCount: 7),
RadkfileRadical(formalVariant: '', strokeCount: 7),
RadkfileRadical(formalVariant: '', strokeCount: 7),
RadkfileRadical(formalVariant: '', strokeCount: 7),
RadkfileRadical(formalVariant: '', strokeCount: 7),
RadkfileRadical(formalVariant: '', strokeCount: 7),
RadkfileRadical(formalVariant: '', strokeCount: 7),
RadkfileRadical(formalVariant: '', strokeCount: 7),
RadkfileRadical(formalVariant: '', strokeCount: 7),
RadkfileRadical(formalVariant: '', strokeCount: 7),
RadkfileRadical(formalVariant: '', strokeCount: 7),
RadkfileRadical(formalVariant: '', strokeCount: 7),
RadkfileRadical(formalVariant: '', strokeCount: 7),
RadkfileRadical(formalVariant: '', strokeCount: 7),
RadkfileRadical(formalVariant: '', strokeCount: 7),
RadkfileRadical(formalVariant: '', strokeCount: 7),
RadkfileRadical(formalVariant: '', strokeCount: 7),
RadkfileRadical(formalVariant: '', strokeCount: 7),
RadkfileRadical(formalVariant: '', strokeCount: 7),
RadkfileRadical(formalVariant: '', strokeCount: 7),
RadkfileRadical(formalVariant: '', strokeCount: 7),
],
8: ['', '', '', '', '', '', '', '', '', '', '', ''],
9: ['', '', '', '', '', '', '', '', '', '', ''],
10: ['', '', '', '', '', '', '', '', '', ''],
11: ['', '', '', '鹿', '', '', '', '', ''],
12: ['', '', '', ''],
13: ['', '', '', ''],
14: ['', ''],
17: [''],
8: [
RadkfileRadical(formalVariant: '', strokeCount: 8),
RadkfileRadical(formalVariant: '', strokeCount: 8),
RadkfileRadical(formalVariant: '', strokeCount: 8),
RadkfileRadical(formalVariant: '', strokeCount: 8),
RadkfileRadical(formalVariant: '', strokeCount: 8),
RadkfileRadical(formalVariant: '', strokeCount: 8),
RadkfileRadical(formalVariant: '', strokeCount: 8),
RadkfileRadical(formalVariant: '', strokeCount: 8),
RadkfileRadical(formalVariant: '', strokeCount: 8),
RadkfileRadical(formalVariant: '', strokeCount: 8),
RadkfileRadical(formalVariant: '', strokeCount: 8),
RadkfileRadical(formalVariant: '', strokeCount: 8),
],
9: [
RadkfileRadical(formalVariant: '', strokeCount: 9),
RadkfileRadical(formalVariant: '', strokeCount: 9),
RadkfileRadical(formalVariant: '', strokeCount: 9),
RadkfileRadical(formalVariant: '', strokeCount: 9),
RadkfileRadical(formalVariant: '', strokeCount: 9),
RadkfileRadical(formalVariant: '', strokeCount: 9),
RadkfileRadical(formalVariant: '', strokeCount: 9),
RadkfileRadical(formalVariant: '', strokeCount: 9),
RadkfileRadical(formalVariant: '', strokeCount: 9),
RadkfileRadical(formalVariant: '', strokeCount: 9),
RadkfileRadical(formalVariant: '', strokeCount: 9),
],
10: [
RadkfileRadical(formalVariant: '', strokeCount: 10),
RadkfileRadical(formalVariant: '', strokeCount: 10),
RadkfileRadical(formalVariant: '', strokeCount: 10),
RadkfileRadical(formalVariant: '', strokeCount: 10),
RadkfileRadical(formalVariant: '', strokeCount: 10),
RadkfileRadical(formalVariant: '', strokeCount: 10),
RadkfileRadical(formalVariant: '', strokeCount: 10),
RadkfileRadical(formalVariant: '', strokeCount: 10),
RadkfileRadical(formalVariant: '', strokeCount: 10),
RadkfileRadical(formalVariant: '', strokeCount: 10),
],
11: [
RadkfileRadical(formalVariant: '', strokeCount: 11),
RadkfileRadical(formalVariant: '', strokeCount: 11),
RadkfileRadical(formalVariant: '', strokeCount: 11),
RadkfileRadical(formalVariant: '鹿', strokeCount: 11),
RadkfileRadical(formalVariant: '', strokeCount: 11),
RadkfileRadical(formalVariant: '', strokeCount: 11),
RadkfileRadical(formalVariant: '', strokeCount: 11),
RadkfileRadical(formalVariant: '', strokeCount: 11),
RadkfileRadical(formalVariant: '', strokeCount: 11),
],
12: [
RadkfileRadical(formalVariant: '', strokeCount: 12),
RadkfileRadical(formalVariant: '', strokeCount: 12),
RadkfileRadical(formalVariant: '', strokeCount: 12),
RadkfileRadical(formalVariant: '', strokeCount: 12),
],
13: [
RadkfileRadical(formalVariant: '', strokeCount: 13),
RadkfileRadical(formalVariant: '', strokeCount: 13),
RadkfileRadical(formalVariant: '', strokeCount: 13),
RadkfileRadical(formalVariant: '', strokeCount: 13),
],
14: [
RadkfileRadical(formalVariant: '', strokeCount: 14),
RadkfileRadical(formalVariant: '', strokeCount: 14),
],
17: [RadkfileRadical(formalVariant: '', strokeCount: 17)],
};
final Map<String, RadkfileRadical> radicalsByFormalVariant = {
for (final r in radicals.values.flattened) r.formalVariant: r,
};
+2
View File
@@ -1,5 +1,6 @@
import 'package:jadb/table_names/jmdict.dart';
import 'package:jadb/table_names/kanjidic.dart';
import 'package:jadb/table_names/kanjivg.dart';
import 'package:jadb/table_names/radkfile.dart';
import 'package:jadb/table_names/tanos_jlpt.dart';
import 'package:sqflite_common/sqlite_api.dart';
@@ -21,6 +22,7 @@ Future<void> verifyTablesWithDbConnection(DatabaseExecutor db) async {
...KANJIDICTableNames.allTables,
...RADKFILETableNames.allTables,
...TanosJLPTTableNames.allTables,
...KanjiVGTableNames.allTables,
};
final missingTables = expectedTables.difference(tables);
@@ -33,6 +33,7 @@ class WordSearchMatchSpan {
Map<String, Object?> toJson() => {
'spanType': spanType.toString().split('.').last,
'index': index,
'subIndex': subIndex,
'start': start,
'end': end,
};
@@ -43,12 +44,13 @@ class WordSearchMatchSpan {
(e) => e.toString().split('.').last == json['spanType'],
),
index: json['index'] as int,
subIndex: json['subIndex'] as int? ?? 0,
start: json['start'] as int,
end: json['end'] as int,
);
@override
int get hashCode => Object.hash(spanType, index, start, end);
int get hashCode => Object.hash(spanType, index, subIndex, start, end);
@override
bool operator ==(Object other) {
@@ -56,6 +58,7 @@ class WordSearchMatchSpan {
return other is WordSearchMatchSpan &&
other.spanType == spanType &&
other.index == index &&
other.subIndex == subIndex &&
other.start == start &&
other.end == end;
}
@@ -81,7 +81,7 @@ class WordSearchSense {
'dialects': dialects.map((e) => e.toJson()).toList(),
'misc': misc.map((e) => e.toJson()).toList(),
'info': info,
'languageSource': languageSource,
'languageSource': languageSource.map((e) => e.toJson()).toList(),
};
factory WordSearchSense.fromJson(Map<String, dynamic> json) =>
@@ -16,6 +16,8 @@ class WordSearchXrefEntry {
final bool ambiguous;
/// The result of the cross-reference, may or may not be included in the query.
///
/// Be careful not to introduce circular references when using this field.
final WordSearchResult? xrefResult;
const WordSearchXrefEntry({
@@ -40,6 +42,10 @@ class WordSearchXrefEntry {
ambiguous: json['ambiguous'] as bool,
baseWord: json['baseWord'] as String,
furigana: json['furigana'] as String?,
xrefResult: null,
xrefResult: json['xrefResult'] != null
? WordSearchResult.fromJson(
Map<String, dynamic>.from(json['xrefResult'] as Map),
)
: null,
);
}
+16 -7
View File
@@ -1,9 +1,11 @@
import 'package:jadb/const_data/radicals.dart';
import 'package:jadb/models/kanji_search/kanji_search_result.dart';
import 'package:jadb/models/verify_tables.dart';
import 'package:jadb/models/word_search/word_search_result.dart';
import 'package:jadb/search/filter_kanji.dart';
import 'package:jadb/search/kanji_search.dart';
import 'package:jadb/search/radical_search.dart';
import 'package:jadb/search/versions.dart';
import 'package:jadb/search/word_search/word_search.dart';
import 'package:sqflite_common/sqlite_api.dart';
@@ -18,12 +20,13 @@ extension JaDBConnection on DatabaseExecutor {
searchKanjiWithDbConnection(this, kanji);
/// Search for a kanji in the database.
Future<Map<String, KanjiSearchResult>> jadbGetManyKanji(Set<String> kanji) =>
searchManyKanjiWithDbConnection(this, kanji);
Future<Map<String, KanjiSearchResult>> jadbGetManyKanji(
Iterable<String> kanji,
) => searchManyKanjiWithDbConnection(this, kanji);
/// Filter a list of characters, and return the ones that are listed in the kanji dictionary.
Future<List<String>> filterKanji(
List<String> kanji, {
Iterable<String> kanji, {
bool deduplicate = false,
}) => filterKanjiWithDbConnection(this, kanji, deduplicate);
@@ -61,11 +64,17 @@ extension JaDBConnection on DatabaseExecutor {
/// of the radicals, find their other radicals, and return those.
/// This is used to figure out which remaining combinations of radicals
/// the user can search for without getting zero results.
Future<List<String>> jadbSearchRemainingRadicals(List<String> radicals) =>
searchRemainingRadicalsWithDbConnection(this, radicals);
Future<List<RadkfileRadical>> jadbSearchRemainingRadicals(
List<RadkfileRadical> radicals,
) => searchRemainingRadicalsWithDbConnection(this, radicals);
/// Given a list of radicals, search which kanji contains all
/// of the radicals, and return those.
Future<List<String>> jadbSearchKanjiByRadicals(List<String> radicals) =>
searchKanjiByRadicalsWithDbConnection(this, radicals);
Future<List<String>> jadbSearchKanjiByRadicals(
List<RadkfileRadical> radicals,
) => searchKanjiByRadicalsWithDbConnection(this, radicals);
/// Retrieve the version information for all datasources in the database.
Future<DatasourceVersions> jadbGetDatasourceVersions() =>
getDatasourceVersions(this);
}
+2 -2
View File
@@ -6,7 +6,7 @@ import 'package:sqflite_common/sqflite.dart';
/// If [deduplicate] is true, the returned list will deduplicate the input kanji list before returning the filtered results.
Future<List<String>> filterKanjiWithDbConnection(
DatabaseExecutor connection,
List<String> kanji,
Iterable<String> kanji,
bool deduplicate,
) async {
final Set<String> filteredKanji = await connection
@@ -14,7 +14,7 @@ Future<List<String>> filterKanjiWithDbConnection(
SELECT "literal"
FROM "${KANJIDICTableNames.character}"
WHERE "literal" IN (${kanji.map((_) => '?').join(',')})
''', kanji)
''', kanji.toList())
.then((value) => value.map((e) => e['literal'] as String).toSet());
if (deduplicate) {
+1 -1
View File
@@ -274,7 +274,7 @@ Future<KanjiSearchResult?> searchKanjiWithDbConnection(
/// Searches for multiple kanji at once, returning a map of kanji to their search results.
Future<Map<String, KanjiSearchResult>> searchManyKanjiWithDbConnection(
DatabaseExecutor connection,
Set<String> kanji,
Iterable<String> kanji,
) async {
if (kanji.isEmpty) {
return {};
+9 -5
View File
@@ -1,3 +1,4 @@
import 'package:jadb/const_data/radicals.dart';
import 'package:jadb/table_names/radkfile.dart';
import 'package:sqflite_common/sqlite_api.dart';
@@ -7,9 +8,9 @@ import 'package:sqflite_common/sqlite_api.dart';
///
/// This can be used to limit the choices of additional radicals provided to a user,
/// so that any choice they make will still yield at least one kanji.
Future<List<String>> searchRemainingRadicalsWithDbConnection(
Future<List<RadkfileRadical>> searchRemainingRadicalsWithDbConnection(
DatabaseExecutor connection,
List<String> radicals,
List<RadkfileRadical> radicals,
) async {
final distinctRadicals = radicals.toSet();
@@ -25,11 +26,14 @@ Future<List<String>> searchRemainingRadicalsWithDbConnection(
HAVING COUNT(DISTINCT "radical") = ?
)
''',
[...distinctRadicals, distinctRadicals.length],
[...distinctRadicals.map((r) => r.formalVariant), distinctRadicals.length],
);
final remainingRadicals = queryResult
.map((row) => row['radical'] as String)
// TODO: maybe we should do some runtime checking here, just to throw a sensible error
// if something goes horribly wrong?
.map((r) => radicalsByFormalVariant[r]!)
.toList();
return remainingRadicals;
@@ -38,7 +42,7 @@ Future<List<String>> searchRemainingRadicalsWithDbConnection(
/// Returns a list of kanji that contain all of the input radicals.
Future<List<String>> searchKanjiByRadicalsWithDbConnection(
DatabaseExecutor connection,
List<String> radicals,
List<RadkfileRadical> radicals,
) async {
final distinctRadicals = radicals.toSet();
@@ -50,7 +54,7 @@ Future<List<String>> searchKanjiByRadicalsWithDbConnection(
GROUP BY "kanji"
HAVING COUNT(DISTINCT "radical") = ?
''',
[...distinctRadicals, distinctRadicals.length],
[...distinctRadicals.map((r) => r.formalVariant), distinctRadicals.length],
);
final kanji = queryResult.map((row) => row['kanji'] as String).toList();
+84
View File
@@ -0,0 +1,84 @@
import 'package:jadb/table_names/jmdict.dart';
import 'package:jadb/table_names/kanjidic.dart';
import 'package:jadb/table_names/radkfile.dart';
import 'package:jadb/table_names/tanos_jlpt.dart';
import 'package:sqflite_common/sqlite_api.dart';
class DatasourceVersions {
final String jmdictVersion;
final DateTime jmdictDate;
final String jmdictHash;
final String kanjidic2Version;
final DateTime kanjidic2Date;
final String kanjidic2Hash;
final String radkfileVersion;
final DateTime radkfileDate;
final String radkfileHash;
final String tanosJlptVersion;
final DateTime tanosJlptDate;
final String tanosJlptHash;
const DatasourceVersions({
required this.jmdictVersion,
required this.jmdictDate,
required this.jmdictHash,
required this.kanjidic2Version,
required this.kanjidic2Date,
required this.kanjidic2Hash,
required this.radkfileVersion,
required this.radkfileDate,
required this.radkfileHash,
required this.tanosJlptVersion,
required this.tanosJlptDate,
required this.tanosJlptHash,
});
}
DateTime _parseDateTime(String dateString) {
try {
return DateTime.parse(dateString);
} catch (e) {
if (RegExp(r'^\d{4}-\d{2}$').hasMatch(dateString)) {
return DateTime.parse('$dateString-01');
} else if (RegExp(r'^\d{4}$').hasMatch(dateString)) {
return DateTime.parse('$dateString-01-01');
} else {
throw FormatException('Invalid date format: $dateString');
}
}
}
Future<DatasourceVersions> getDatasourceVersions(
final DatabaseExecutor connection,
) async {
final jmdictVersion = await connection
.query(JMdictTableNames.version)
.then((rows) => rows.first);
final kanjidic2Version = await connection
.query(KANJIDICTableNames.version)
.then((rows) => rows.first);
final radkfileVersion = await connection
.query(RADKFILETableNames.version)
.then((rows) => rows.first);
final tanosJlptVersion = await connection
.query(TanosJLPTTableNames.version)
.then((rows) => rows.first);
return DatasourceVersions(
jmdictVersion: jmdictVersion['version'] as String,
jmdictDate: _parseDateTime(jmdictVersion['date'].toString()),
jmdictHash: jmdictVersion['hash'] as String,
kanjidic2Version: kanjidic2Version['version'] as String,
kanjidic2Date: _parseDateTime(kanjidic2Version['date'].toString()),
kanjidic2Hash: kanjidic2Version['hash'] as String,
radkfileVersion: radkfileVersion['version'] as String,
radkfileDate: _parseDateTime(radkfileVersion['date'].toString()),
radkfileHash: radkfileVersion['hash'] as String,
tanosJlptVersion: tanosJlptVersion['version'] as String,
tanosJlptDate: _parseDateTime(tanosJlptVersion['date'].toString()),
tanosJlptHash: tanosJlptVersion['hash'] as String,
);
}
+9
View File
@@ -0,0 +1,9 @@
abstract class KanjiVGTableNames {
static const String version = 'KanjiVG_Version';
static const String entry = 'KanjiVG_Entry';
static const String path = 'KanjiVG_Path';
static const String strokeNumber = 'KanjiVG_StrokeNumber';
static const String pathGroup = 'KanjiVG_PathGroup';
static Set<String> get allTables => {version, entry, path, strokeNumber, pathGroup};
}
+1
View File
@@ -0,0 +1 @@
const int jadbSchemaVersion = 1;
+1 -1
View File
@@ -229,7 +229,7 @@ CREATE TABLE "JMdict_SenseGlossary" (
PRIMARY KEY ("senseId", "phrase")
) WITHOUT ROWID;
-- CREATE INDEX "JMdict_SenseGlossary_byPhrase" ON JMdict_SenseGlossary("phrase");
CREATE INDEX "JMdict_SenseGlossary_byPhrase" ON JMdict_SenseGlossary("phrase");
CREATE TABLE "JMdict_SenseGlossaryType" (
"senseId" INTEGER NOT NULL REFERENCES "JMdict_Sense"("senseId"),
@@ -1,6 +1,6 @@
CREATE TABLE "JMdict_EntryScore" (
"elementId" INTEGER PRIMARY KEY,
"score" INTEGER NOT NULL DEFAULT 0,
"score" INTEGER NOT NULL,
"common" BOOLEAN NOT NULL DEFAULT FALSE,
"entryId" INTEGER NOT NULL GENERATED ALWAYS AS (("elementId" / 100) % 10000000) STORED,
@@ -15,8 +15,7 @@ CREATE TABLE "JMdict_EntryScore" (
CREATE INDEX "JMdict_EntryScore_byElementId_byScore" ON "JMdict_EntryScore"("elementId", "score");
CREATE INDEX "JMdict_EntryScore_byScore" ON "JMdict_EntryScore"("score");
CREATE INDEX "JMdict_EntryScore_byElementId_byCommon" ON "JMdict_EntryScore"("elementId", "common");
CREATE INDEX "JMdict_EntryScore_byCommon" ON "JMdict_EntryScore"("common");
CREATE INDEX "JMdict_EntryScore_byCommon" ON "JMdict_EntryScore"("common") WHERE "common" = TRUE;
-- NOTE: these views are deduplicated in order not to perform an unnecessary
-- UNION on every trigger
+45
View File
@@ -0,0 +1,45 @@
CREATE TABLE "KanjiVG_Version" (
"version" VARCHAR(10) PRIMARY KEY NOT NULL,
"date" DATE NOT NULL,
"hash" VARCHAR(64) NOT NULL
) WITHOUT ROWID;
CREATE TRIGGER "KanjiVG_Version_SingleRow"
BEFORE INSERT ON "KanjiVG_Version"
WHEN (SELECT COUNT(*) FROM "KanjiVG_Version") >= 1
BEGIN
SELECT RAISE(FAIL, 'Only one row allowed in KanjiVG_Version');
END;
CREATE TABLE "KanjiVG_Entry" (
"character" CHAR(1) PRIMARY KEY NOT NULL
) WITHOUT ROWID;
CREATE TABLE "KanjiVG_StrokeNumber" (
"character" CHAR(1) NOT NULL REFERENCES "KanjiVG_Entry"("character"),
"strokeNum" INTEGER NOT NULL,
"x" REAL NOT NULL,
"y" REAL NOT NULL,
PRIMARY KEY ("character", "strokeNum")
) WITHOUT ROWID;
CREATE TABLE "KanjiVG_Path" (
"character" CHAR(1) NOT NULL REFERENCES "KanjiVG_Entry"("character"),
"pathId" TEXT NOT NULL,
"type" VARCHAR(10) NOT NULL,
"svgPath" TEXT NOT NULL,
PRIMARY KEY ("character", "pathId")
) WITHOUT ROWID;
CREATE TABLE "KanjiVG_PathGroup" (
"character" CHAR(1) NOT NULL REFERENCES "KanjiVG_Entry"("character"),
"groupId" TEXT NOT NULL,
"parentGroupId" TEXT REFERENCES "KanjiVG_PathGroup"("groupId"),
"element" TEXT,
"original" TEXT,
"position" VARCHAR(10),
"radical" TEXT,
"part" INTEGER,
PRIMARY KEY ("character", "groupId"),
CHECK ("position" IN ('bottom', 'kamae', 'kamaec', 'left', 'middle', 'nyo', 'nyoc', 'right', 'tare', 'tarec', 'top') OR "position" IS NULL)
) WITHOUT ROWID;
+39 -31
View File
@@ -5,18 +5,18 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "8d718c5c58904f9937290fd5dbf2d6a0e02456867706bfb6cd7b81d394e738d5"
sha256: cd6add6f846f35fb79f3c315296703c1a24f3cfd7f4739d91a74961c1c7e9f1b
url: "https://pub.dev"
source: hosted
version: "98.0.0"
version: "100.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "6141ad5d092d1e1d13929c0504658bbeccc1703505830d7c26e859908f5efc88"
sha256: "6ba98576948803398b69e3a444df24eacdbe12ed699c7014e120ea38552debbf"
url: "https://pub.dev"
source: hosted
version: "12.0.0"
version: "13.0.0"
args:
dependency: "direct main"
description:
@@ -61,10 +61,10 @@ packages:
dependency: transitive
description:
name: code_assets
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
url: "https://pub.dev"
source: hosted
version: "1.0.0"
version: "1.2.1"
collection:
dependency: "direct main"
description:
@@ -149,10 +149,10 @@ packages:
dependency: transitive
description:
name: hooks
sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388
sha256: "62ae9bb76d02526c7c2110a19b6e6ad788fe28d35e553e35efb02a41a46ab43a"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
version: "2.0.1"
http_multi_server:
dependency: transitive
description:
@@ -197,10 +197,10 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
url: "https://pub.dev"
source: hosted
version: "0.12.19"
version: "0.12.20"
meta:
dependency: transitive
description:
@@ -221,10 +221,10 @@ packages:
dependency: transitive
description:
name: native_toolchain_c
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
sha256: f59351d28f49520cd3a74eb1f41c5f19ae15e53c65a3231d14af672e46510a96
url: "https://pub.dev"
source: hosted
version: "0.17.6"
version: "0.19.1"
node_preamble:
dependency: transitive
description:
@@ -273,6 +273,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.0"
record_use:
dependency: transitive
description:
name: record_use
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
shelf:
dependency: transitive
description:
@@ -333,26 +341,26 @@ packages:
dependency: "direct main"
description:
name: sqflite_common
sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6"
sha256: "1581ffbf7a0e333b380d6a30737d78516b826cb35beb7fb0bf8a3ea0c678b465"
url: "https://pub.dev"
source: hosted
version: "2.5.6"
version: "2.5.8"
sqflite_common_ffi:
dependency: "direct main"
description:
name: sqflite_common_ffi
sha256: c59fcdc143839a77581f7a7c4de018e53682408903a0a0800b95ef2dc4033eff
sha256: cd0c7f7de39a08f2d54ef144d9058c46eca8461879aaa648025643455c1e5a20
url: "https://pub.dev"
source: hosted
version: "2.4.0+2"
version: "2.4.0+3"
sqlite3:
dependency: "direct main"
description:
name: sqlite3
sha256: caa693ad15a587a2b4fde093b728131a1827903872171089dedb16f7665d3a91
sha256: "9488c7d2cdb1091c91cacf7e207cff81b28bff8e366f042bad3afe7d34afe189"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
version: "3.3.2"
stack_trace:
dependency: transitive
description:
@@ -381,10 +389,10 @@ packages:
dependency: transitive
description:
name: synchronized
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
sha256: "63896c27e81b28f8cb4e69ead0d3e8f03f1d1e5fc531a3e579cabed6a2c7c9e5"
url: "https://pub.dev"
source: hosted
version: "3.4.0"
version: "3.4.0+1"
term_glyph:
dependency: transitive
description:
@@ -397,26 +405,26 @@ packages:
dependency: "direct dev"
description:
name: test
sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20"
sha256: ca578dc12bb8b2f40b67b7d3bd2fac4f31c01a6ff7130a14e2597b919934507f
url: "https://pub.dev"
source: hosted
version: "1.31.0"
version: "1.31.1"
test_api:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
version: "0.7.12"
test_core:
dependency: transitive
description:
name: test_core
sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34"
sha256: d2e98ec12998368dc59ddd47ab709f2cd55acd6b66dc7db764455a44082f4bc5
url: "https://pub.dev"
source: hosted
version: "0.6.17"
version: "0.6.18"
typed_data:
dependency: transitive
description:
@@ -429,10 +437,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev"
source: hosted
version: "15.0.2"
version: "15.2.0"
watcher:
dependency: transitive
description:
@@ -477,10 +485,10 @@ packages:
dependency: "direct main"
description:
name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4"
url: "https://pub.dev"
source: hosted
version: "6.6.1"
version: "7.0.1"
yaml:
dependency: transitive
description:
@@ -490,4 +498,4 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.10.1 <4.0.0"
dart: ">=3.11.0 <4.0.0"
+1 -1
View File
@@ -15,7 +15,7 @@ dependencies:
sqflite_common: ^2.5.0
sqflite_common_ffi: ^2.3.0
sqlite3: ^3.1.6
xml: ^6.5.0
xml: '>=6.0.0 < 8.0.0'
dev_dependencies:
benchmark_harness: ^2.4.0
+93
View File
@@ -0,0 +1,93 @@
import 'dart:convert';
import 'package:jadb/models/kanji_search/kanji_search_radical.dart';
import 'package:jadb/models/kanji_search/kanji_search_result.dart';
import 'package:test/test.dart';
Object? _roundTripJson(Object? value) => jsonDecode(jsonEncode(value));
Map<String, dynamic> _roundTripMap(Object? json) =>
Map<String, dynamic>.from(_roundTripJson(json) as Map);
void main() {
test('KanjiSearchRadical JSON serialization roundtrip', () {
const radical = KanjiSearchRadical(
symbol: '',
names: ['ひと', 'にんべん'],
forms: [''],
meanings: ['person', 'human'],
);
final restored = KanjiSearchRadical.fromJson(
_roundTripMap(radical.toJson()),
);
expect(restored, equals(radical));
});
test('KanjiSearchResult JSON serialization roundtrip', () {
const result = KanjiSearchResult(
kanji: '',
taughtIn: 1,
jlptLevel: 'N5',
newspaperFrequencyRank: 1234,
strokeCount: 6,
meanings: ['rest', 'day off'],
kunyomi: ['やす.む', 'やす.まる'],
onyomi: ['キュウ'],
radical: KanjiSearchRadical(
symbol: '',
names: ['ひと', 'にんべん'],
forms: [''],
meanings: ['person', 'human'],
),
parts: ['', ''],
codepoints: {'ucs': '4F11', 'jis208': '1-22-57'},
nanori: ['やす'],
alternativeLanguageReadings: {
'korean': [''],
'pinyin': ['xiū'],
},
strokeMiscounts: [5, 7],
queryCodes: {
'skip': ['1-2-4'],
'fourCorner': ['2429.0'],
},
dictionaryReferences: {'nelson_c': '122', 'heisig': '457'},
);
final restored = KanjiSearchResult.fromJson(_roundTripMap(result.toJson()));
expect(restored, equals(result));
});
test(
'KanjiSearchResult JSON serialization roundtrip - nullable and empty fields',
() {
const result = KanjiSearchResult(
kanji: '',
taughtIn: null,
jlptLevel: null,
newspaperFrequencyRank: null,
strokeCount: 3,
meanings: ['iteration mark'],
kunyomi: [],
onyomi: [],
radical: null,
parts: [],
codepoints: {'ucs': '3005'},
nanori: [],
alternativeLanguageReadings: {},
strokeMiscounts: [],
queryCodes: {},
dictionaryReferences: {},
);
final restored = KanjiSearchResult.fromJson(
_roundTripMap(result.toJson()),
);
expect(restored, equals(result));
},
);
}
+282
View File
@@ -0,0 +1,282 @@
import 'dart:convert';
import 'package:jadb/models/common/jlpt_level.dart';
import 'package:jadb/models/jmdict/jmdict_dialect.dart';
import 'package:jadb/models/jmdict/jmdict_field.dart';
import 'package:jadb/models/jmdict/jmdict_kanji_info.dart';
import 'package:jadb/models/jmdict/jmdict_misc.dart';
import 'package:jadb/models/jmdict/jmdict_pos.dart';
import 'package:jadb/models/jmdict/jmdict_reading_info.dart';
import 'package:jadb/models/word_search/word_search_match_span.dart';
import 'package:jadb/models/word_search/word_search_result.dart';
import 'package:jadb/models/word_search/word_search_ruby.dart';
import 'package:jadb/models/word_search/word_search_sense.dart';
import 'package:jadb/models/word_search/word_search_sense_language_source.dart';
import 'package:jadb/models/word_search/word_search_sources.dart';
import 'package:jadb/models/word_search/word_search_xref_entry.dart';
import 'package:test/test.dart';
Object? _roundTripJson(Object? value) => jsonDecode(jsonEncode(value));
Map<String, dynamic> _roundTripMap(Object? json) =>
Map<String, dynamic>.from(_roundTripJson(json) as Map);
Map<String, Object?> _roundTripObjectMap(Object? json) =>
Map<String, Object?>.from(_roundTripJson(json) as Map);
void _expectScalarRoundTrip<T>({
required Iterable<T> values,
required Object? Function(T value) toJson,
required T Function(Object? json) fromJson,
}) {
for (final value in values) {
expect(
fromJson(_roundTripJson(toJson(value))),
equals(value),
reason: 'Roundtrip failed for $value',
);
}
}
void _expectMapRoundTrip<T>({
required Iterable<T> values,
required Map<String, Object?> Function(T value) toJson,
required T Function(Map<String, Object?> json) fromJson,
}) {
for (final value in values) {
expect(
fromJson(_roundTripObjectMap(toJson(value))),
equals(value),
reason: 'Roundtrip failed for $value',
);
}
}
WordSearchResult _buildNestedXrefResult() => WordSearchResult(
score: 7,
entryId: 300,
isCommon: false,
japanese: [WordSearchRuby(base: '補助', furigana: 'ほじょ')],
kanjiInfo: const {},
readingInfo: const {},
senses: const [],
jlptLevel: JlptLevel.none,
sources: const WordSearchSources(jmdict: true, jmnedict: false),
);
WordSearchSense _buildSense() => WordSearchSense(
englishDefinitions: ['kana', 'syllabary'],
partsOfSpeech: [JMdictPOS.n],
seeAlso: [
WordSearchXrefEntry(
entryId: 300,
ambiguous: false,
baseWord: '仮名遣い',
furigana: 'かなづかい',
xrefResult: _buildNestedXrefResult(),
),
],
antonyms: const [
WordSearchXrefEntry(
entryId: 301,
ambiguous: true,
baseWord: '漢字',
furigana: 'かんじ',
xrefResult: null,
),
],
restrictedToReading: ['かな'],
restrictedToKanji: ['仮名'],
fields: [JMdictField.linguistics],
dialects: [JMdictDialect.kansai],
misc: [JMdictMisc.onlyKana, JMdictMisc.rare],
info: ['Typically written using kana alone.'],
languageSource: const [
WordSearchSenseLanguageSource(
language: 'por',
phrase: 'canoa',
fullyDescribesSense: false,
constructedFromSmallerWords: true,
),
],
);
WordSearchResult _buildWordSearchResult({
List<WordSearchMatchSpan>? matchSpans,
}) => WordSearchResult(
score: 42,
entryId: 123,
isCommon: true,
japanese: [
WordSearchRuby(base: '仮名', furigana: 'かな'),
WordSearchRuby(base: 'かな'),
],
kanjiInfo: {'仮名': JMdictKanjiInfo.rK, '': JMdictKanjiInfo.ateji},
readingInfo: {'かな': JMdictReadingInfo.gikun, 'カナ': JMdictReadingInfo.rk},
senses: [_buildSense()],
jlptLevel: JlptLevel.n5,
sources: const WordSearchSources(jmdict: true, jmnedict: true),
matchSpans: matchSpans,
);
void main() {
test('JlptLevel JSON serialization roundtrip', () {
_expectScalarRoundTrip<JlptLevel>(
values: JlptLevel.values,
toJson: (value) => value.toJson(),
fromJson: JlptLevel.fromJson,
);
});
test('JMdictDialect JSON serialization roundtrip', () {
_expectMapRoundTrip<JMdictDialect>(
values: JMdictDialect.values,
toJson: (value) => value.toJson(),
fromJson: JMdictDialect.fromJson,
);
});
test('JMdictField JSON serialization roundtrip', () {
_expectMapRoundTrip<JMdictField>(
values: JMdictField.values,
toJson: (value) => value.toJson(),
fromJson: JMdictField.fromJson,
);
});
test('JMdictKanjiInfo JSON serialization roundtrip', () {
_expectMapRoundTrip<JMdictKanjiInfo>(
values: JMdictKanjiInfo.values,
toJson: (value) => value.toJson(),
fromJson: JMdictKanjiInfo.fromJson,
);
});
test('JMdictMisc JSON serialization roundtrip', () {
_expectMapRoundTrip<JMdictMisc>(
values: JMdictMisc.values,
toJson: (value) => value.toJson(),
fromJson: JMdictMisc.fromJson,
);
});
test('JMdictPOS JSON serialization roundtrip', () {
_expectMapRoundTrip<JMdictPOS>(
values: JMdictPOS.values,
toJson: (value) => value.toJson(),
fromJson: JMdictPOS.fromJson,
);
});
test('JMdictReadingInfo JSON serialization roundtrip', () {
_expectMapRoundTrip<JMdictReadingInfo>(
values: JMdictReadingInfo.values,
toJson: (value) => value.toJson(),
fromJson: JMdictReadingInfo.fromJson,
);
});
test('WordSearchMatchSpan JSON serialization roundtrip', () {
final span = WordSearchMatchSpan(
spanType: WordSearchMatchSpanType.sense,
index: 2,
subIndex: 3,
start: 4,
end: 8,
);
final restored = WordSearchMatchSpan.fromJson(_roundTripMap(span.toJson()));
expect(restored, equals(span));
expect(restored.subIndex, equals(span.subIndex));
});
test('WordSearchRuby JSON serialization roundtrip', () {
final ruby = WordSearchRuby(base: '仮名', furigana: 'かな');
final restored = WordSearchRuby.fromJson(_roundTripMap(ruby.toJson()));
expect(restored.toJson(), equals(ruby.toJson()));
});
test('WordSearchSenseLanguageSource JSON serialization roundtrip', () {
const source = WordSearchSenseLanguageSource(
language: 'por',
phrase: 'canoa',
fullyDescribesSense: false,
constructedFromSmallerWords: true,
);
final restored = WordSearchSenseLanguageSource.fromJson(
_roundTripMap(source.toJson()),
);
expect(restored.toJson(), equals(source.toJson()));
});
test('WordSearchSources JSON serialization roundtrip', () {
const sources = WordSearchSources(jmdict: false, jmnedict: true);
final restored = WordSearchSources.fromJson(
_roundTripMap(sources.toJson()),
);
expect(restored.toJson(), equals(sources.toJson()));
expect(restored.sqlValue, equals(sources.sqlValue));
});
test('WordSearchXrefEntry JSON serialization roundtrip', () {
final entry = WordSearchXrefEntry(
entryId: 300,
ambiguous: false,
baseWord: '仮名遣い',
furigana: 'かなづかい',
xrefResult: _buildNestedXrefResult(),
);
final restored = WordSearchXrefEntry.fromJson(
_roundTripMap(entry.toJson()),
);
expect(restored.toJson(), equals(entry.toJson()));
expect(restored.xrefResult?.toJson(), equals(entry.xrefResult?.toJson()));
});
test('WordSearchSense JSON serialization roundtrip', () {
final sense = _buildSense();
final restored = WordSearchSense.fromJson(_roundTripMap(sense.toJson()));
expect(restored.toJson(), equals(sense.toJson()));
});
test('WordSearchResult JSON serialization roundtrip', () {
final result = _buildWordSearchResult();
final restored = WordSearchResult.fromJson(_roundTripMap(result.toJson()));
expect(restored.toJson(), equals(result.toJson()));
expect(restored.matchSpans, isNull);
});
test('WordSearchResult leaves matchSpans out of JSON', () {
final result = _buildWordSearchResult(
matchSpans: [
WordSearchMatchSpan(
spanType: WordSearchMatchSpanType.sense,
index: 0,
subIndex: 1,
start: 0,
end: 4,
),
],
);
final json = _roundTripMap(result.toJson());
final restored = WordSearchResult.fromJson(json);
expect(json.containsKey('matchSpans'), isFalse);
expect(restored.matchSpans, isNull);
expect(restored.toJson(), equals(result.toJson()));
});
}
+12
View File
@@ -0,0 +1,12 @@
import 'package:jadb/search.dart';
import 'package:test/test.dart';
import 'setup_database_connection.dart';
void main() {
test('Retrieve datasource versions', () async {
final connection = await setupDatabaseConnection();
final result = await connection.jadbGetDatasourceVersions();
expect(result, isNotNull);
});
}
+44 -38
View File
@@ -7,32 +7,29 @@ import 'package:test/test.dart';
import 'setup_database_connection.dart';
void main() {
test(
'All constant radicals should exist in the database',
() async {
final connection = await setupDatabaseConnection();
final allRadicalsInDb = await connection.query(
RADKFILETableNames.radkfile,
columns: ['radical'],
distinct: true,
);
test('All constant radicals should exist in the database', () async {
final connection = await setupDatabaseConnection();
final allRadicalsInDb = await connection.query(
RADKFILETableNames.radkfile,
columns: ['radical'],
distinct: true,
);
final radicalsInDb = allRadicalsInDb
.map((e) => e['radical'] as String)
.toSet();
final radicalsInDb = allRadicalsInDb
.map((e) => e['radical'] as String)
.toSet();
final missingRadicals = radicals.values.flattenedToSet.difference(
radicalsInDb,
);
final missingRadicals = radicals.values.flattenedToSet
.map((e) => e.formalVariant)
.toSet()
.difference(radicalsInDb);
expect(
missingRadicals,
isEmpty,
reason: 'Missing radicals in database: $missingRadicals',
);
},
skip: 'Test is valid, code is broken, fix me',
);
expect(
missingRadicals,
isEmpty,
reason: 'Missing radicals in database: $missingRadicals',
);
});
test(
'All radicals in database should be in the constant radical list',
@@ -49,7 +46,7 @@ void main() {
.toSet();
final extraRadicals = radicalsInDb.difference(
radicals.values.flattenedToSet,
radicals.values.flattenedToSet.map((e) => e.formalVariant).toSet(),
);
expect(
@@ -59,21 +56,30 @@ void main() {
'Extra radicals in database missing in the constant list: $extraRadicals',
);
},
skip: 'Test is valid, code is broken, fix me',
);
group(
'All radicals should return results',
() {
for (final radical in radicals.values.flattened) {
test(' - $radical', () async {
final connection = await setupDatabaseConnection();
final result = await connection.jadbSearchKanjiByRadicals([radical]);
expect(result, isNotEmpty);
});
test('All constant radicals are located in the correct stroke count group', () {
for (final mapEntry in radicals.entries) {
final strokeCount = mapEntry.key;
final radicalsInGroup = mapEntry.value;
for (final radical in radicalsInGroup) {
expect(
strokeCount,
radical.strokeCount,
reason:
'Radical ${radical.formalVariant} should have stroke count $strokeCount but has ${radical.strokeCount}',
);
}
},
skip:
'These will be automatically fixed once the other radical tests are passing',
);
}
});
group('All radicals should return results', () {
for (final radical in radicals.values.flattened) {
test(' - $radical', () async {
final connection = await setupDatabaseConnection();
final result = await connection.jadbSearchKanjiByRadicals([radical]);
expect(result, isNotEmpty);
});
}
});
}
+31 -4
View File
@@ -8,7 +8,7 @@ void main() {
expect(result, 'かたまり');
});
test('Basic test with diacritics', () {
test('Basic test with dakuten', () {
final result = transliterateLatinToHiragana('gadamari');
expect(result, 'がだまり');
});
@@ -54,7 +54,7 @@ void main() {
test('Basic test', expectSpans('katamari', ['', '', '', '']));
test(
'Basic test with diacritics',
'Basic test with dakuten',
expectSpans('gadamari', ['', '', '', '']),
);
test('wi and we', expectSpans('wiwe', ['うぃ', 'うぇ']));
@@ -72,7 +72,7 @@ void main() {
expect(result, 'katamari');
});
test('Basic test with diacritics', () {
test('Basic test with dakuten', () {
final result = transliterateHiraganaToLatin('がだまり');
expect(result, 'gadamari');
});
@@ -91,6 +91,21 @@ void main() {
final result = transliterateHiraganaToLatin('かっぱ');
expect(result, 'kappa');
});
test('Iteration mark', () {
final result = transliterateHiraganaToLatin('さゝき');
expect(result, 'sasaki');
}, skip: 'Not yet implemented');
test('Iteration mark with dakuten', () {
final result = transliterateHiraganaToLatin('あひゞき');
expect(result, 'ahibiki');
}, skip: 'Not yet implemented');
test('Yori', () {
final result = transliterateHiraganaToLatin('');
expect(result, 'yori');
}, skip: 'Not yet implemented');
});
group('Hiragana -> Romaji Spans', () {
@@ -110,7 +125,7 @@ void main() {
test('Basic test', expectSpans('かたまり', ['ka', 'ta', 'ma', 'ri']));
test(
'Basic test with diacritics',
'Basic test with dakuten',
expectSpans('がだまり', ['ga', 'da', 'ma', 'ri']),
);
test('wi and we', expectSpans('うぃうぇ', ['whi', 'whe']));
@@ -118,5 +133,17 @@ void main() {
// TODO: fix the implementation
// test('Double consonant', expectSpans('かっぱ', ['ka', 'ppa']));
test(
'Iteration mark',
expectSpans('さゝき', ['sa', 'sa', 'ki']),
skip: 'Not yet implemented',
);
test(
'Iteration mark with dakuten',
expectSpans('あひゞき', ['a', 'hi', 'bi', 'ki']),
skip: 'Not yet implemented',
);
test('Yori', expectSpans('', ['yori']), skip: 'Not yet implemented');
});
}