Compare commits
13 Commits
v0.5.0
...
match-span
| Author | SHA1 | Date | |
|---|---|---|---|
|
a696ed9733
|
|||
|
00b963bfed
|
|||
|
4376012f18
|
|||
|
8ae1d882a0
|
|||
|
81db60ccf7
|
|||
|
f57cc68ef3
|
|||
|
48f50628a1
|
|||
|
1783338b2a
|
|||
|
e92e99922b
|
|||
|
05b56466e7
|
|||
|
33016ca751
|
|||
|
98d92d370d
|
|||
|
5252936bdc
|
31
.gitea/workflows/test.yml
Normal file
31
.gitea/workflows/test.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
name: "Run tests"
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
jobs:
|
||||
evals:
|
||||
runs-on: debian-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install sudo
|
||||
run: apt-get update && apt-get -y install sudo
|
||||
|
||||
- name: Install nix
|
||||
uses: https://github.com/cachix/install-nix-action@v31
|
||||
|
||||
- name: Configure nix
|
||||
run: echo -e "show-trace = true\nmax-jobs = auto\ntrusted-users = root\nexperimental-features = nix-command flakes\nbuild-users-group =" > /etc/nix/nix.conf
|
||||
|
||||
- name: Update database inputs
|
||||
run: |
|
||||
nix flake update jmdict-src
|
||||
nix flake update jmdict-with-examples-src
|
||||
nix flake update radkfile-src
|
||||
nix flake update kanjidic2-src
|
||||
|
||||
- name: Build database
|
||||
run: nix build .#database -L
|
||||
|
||||
- name: Run tests
|
||||
run: nix develop .# --command dart test
|
||||
12
flake.lock
generated
12
flake.lock
generated
@@ -3,7 +3,7 @@
|
||||
"jmdict-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"narHash": "sha256-coRi0AIx02GIrVms4C1GMCjgtdIVRpS7WEpN2UdUX1E=",
|
||||
"narHash": "sha256-1if5Z1ynrCd05ySrvD6ZA1PfKBayhBFzUOe5vplwYXM=",
|
||||
"type": "file",
|
||||
"url": "http://ftp.edrdg.org/pub/Nihongo/JMdict_e.gz"
|
||||
},
|
||||
@@ -15,7 +15,7 @@
|
||||
"jmdict-with-examples-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"narHash": "sha256-UD9QVy9dtPeExN/yvvR8Mi4r+3PvxlbGJA+oRNIGUGk=",
|
||||
"narHash": "sha256-3Eb8iVSZFvuf4yH/53tDdN6Znt+tvvra6kd7GIv4LYE=",
|
||||
"type": "file",
|
||||
"url": "http://ftp.edrdg.org/pub/Nihongo/JMdict_e_examp.gz"
|
||||
},
|
||||
@@ -27,7 +27,7 @@
|
||||
"kanjidic2-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"narHash": "sha256-NwQ9SmlwxyXXxSS8cVh1PL17m/LKXdIhyyitTIbB2DI=",
|
||||
"narHash": "sha256-mg2cP3rX1wm+dTAQCNHthVcKUH5PZRhGbHv1AP2EwJQ=",
|
||||
"type": "file",
|
||||
"url": "https://www.edrdg.org/kanjidic/kanjidic2.xml.gz"
|
||||
},
|
||||
@@ -38,11 +38,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1765779637,
|
||||
"narHash": "sha256-KJ2wa/BLSrTqDjbfyNx70ov/HdgNBCBBSQP3BIzKnv4=",
|
||||
"lastModified": 1771369470,
|
||||
"narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "1306659b587dc277866c7b69eb97e5f07864d8c4",
|
||||
"rev": "0182a361324364ae3f436a63005877674cf45efb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
sqlite-interactive
|
||||
sqlite-analyzer
|
||||
sqlite-web
|
||||
sqlint
|
||||
# sqlint
|
||||
sqlfluff
|
||||
];
|
||||
env = {
|
||||
@@ -109,10 +109,14 @@
|
||||
in !(lib.any (b: b) [
|
||||
(!(lib.cleanSourceFilter path type))
|
||||
(baseName == ".github" && type == "directory")
|
||||
(baseName == ".gitea" && type == "directory")
|
||||
|
||||
(baseName == "nix" && type == "directory")
|
||||
(baseName == ".envrc" && type == "regular")
|
||||
(baseName == "flake.lock" && type == "regular")
|
||||
(baseName == "flake.nix" && type == "regular")
|
||||
|
||||
(baseName == ".sqlfluff" && type == "regular")
|
||||
])) ./.;
|
||||
|
||||
in forAllSystems (system: pkgs: {
|
||||
@@ -123,7 +127,7 @@
|
||||
'';
|
||||
|
||||
jmdict = pkgs.callPackage ./nix/jmdict.nix {
|
||||
inherit jmdict-src jmdict-with-examples-src edrdgMetadata;
|
||||
inherit jmdict-src jmdict-with-examples-src edrdgMetadata;
|
||||
};
|
||||
|
||||
radkfile = pkgs.callPackage ./nix/radkfile.nix {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:jadb/search.dart';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
import 'package:sqlite3/open.dart';
|
||||
|
||||
Future<Database> openLocalDb({
|
||||
String? libsqlitePath,
|
||||
@@ -12,32 +10,17 @@ Future<Database> openLocalDb({
|
||||
bool verifyTablesExist = true,
|
||||
bool walMode = false,
|
||||
}) async {
|
||||
libsqlitePath ??= Platform.environment['LIBSQLITE_PATH'];
|
||||
jadbPath ??= Platform.environment['JADB_PATH'];
|
||||
jadbPath ??= Directory.current.uri.resolve('jadb.sqlite').path;
|
||||
|
||||
libsqlitePath = (libsqlitePath == null)
|
||||
? null
|
||||
: File(libsqlitePath).resolveSymbolicLinksSync();
|
||||
jadbPath = File(jadbPath).resolveSymbolicLinksSync();
|
||||
|
||||
if (libsqlitePath == null) {
|
||||
throw Exception('LIBSQLITE_PATH is not set');
|
||||
}
|
||||
|
||||
if (!File(libsqlitePath).existsSync()) {
|
||||
throw Exception('LIBSQLITE_PATH does not exist: $libsqlitePath');
|
||||
}
|
||||
|
||||
if (!File(jadbPath).existsSync()) {
|
||||
throw Exception('JADB_PATH does not exist: $jadbPath');
|
||||
}
|
||||
|
||||
final db =
|
||||
await createDatabaseFactoryFfi(
|
||||
ffiInit: () =>
|
||||
open.overrideForAll(() => DynamicLibrary.open(libsqlitePath!)),
|
||||
).openDatabase(
|
||||
await createDatabaseFactoryFfi().openDatabase(
|
||||
jadbPath,
|
||||
options: OpenDatabaseOptions(
|
||||
onConfigure: (db) async {
|
||||
|
||||
@@ -3,12 +3,20 @@ import 'dart:io';
|
||||
|
||||
import 'package:csv/csv.dart';
|
||||
import 'package:jadb/_data_ingestion/tanos-jlpt/objects.dart';
|
||||
import 'package:xml/xml_events.dart';
|
||||
|
||||
Future<List<JLPTRankedWord>> parseJLPTRankedWords(
|
||||
Map<String, File> files,
|
||||
) async {
|
||||
final List<JLPTRankedWord> result = [];
|
||||
|
||||
final codec = CsvCodec(
|
||||
fieldDelimiter: ',',
|
||||
lineDelimiter: '\n',
|
||||
quoteMode: QuoteMode.strings,
|
||||
escapeCharacter: '\\',
|
||||
);
|
||||
|
||||
for (final entry in files.entries) {
|
||||
final jlptLevel = entry.key;
|
||||
final file = entry.value;
|
||||
@@ -17,42 +25,42 @@ Future<List<JLPTRankedWord>> parseJLPTRankedWords(
|
||||
throw Exception('File $jlptLevel does not exist');
|
||||
}
|
||||
|
||||
final rows = await file
|
||||
final words = await file
|
||||
.openRead()
|
||||
.transform(utf8.decoder)
|
||||
.transform(CsvToListConverter())
|
||||
.transform(codec.decoder)
|
||||
.flatten()
|
||||
.map((row) {
|
||||
if (row.length != 3) {
|
||||
throw Exception('Invalid line in $jlptLevel: $row');
|
||||
}
|
||||
return row;
|
||||
})
|
||||
.map((row) => row.map((e) => e as String).toList())
|
||||
.map((row) {
|
||||
final kanji = row[0].isEmpty
|
||||
? null
|
||||
: row[0]
|
||||
.replaceFirst(RegExp('^お・'), '')
|
||||
.replaceAll(RegExp(r'(.*)'), '');
|
||||
|
||||
final readings = row[1]
|
||||
.split(RegExp('[・/、(:?s+)]'))
|
||||
.map((e) => e.trim())
|
||||
.toList();
|
||||
|
||||
final meanings = row[2].split(',').expand(cleanMeaning).toList();
|
||||
|
||||
return JLPTRankedWord(
|
||||
readings: readings,
|
||||
kanji: kanji,
|
||||
jlptLevel: jlptLevel,
|
||||
meanings: meanings,
|
||||
);
|
||||
})
|
||||
.toList();
|
||||
|
||||
for (final row in rows) {
|
||||
if (row.length != 3) {
|
||||
throw Exception('Invalid line in $jlptLevel: $row');
|
||||
}
|
||||
|
||||
final kanji = (row[0] as String).isEmpty
|
||||
? null
|
||||
: (row[0] as String)
|
||||
.replaceFirst(RegExp('^お・'), '')
|
||||
.replaceAll(RegExp(r'(.*)'), '');
|
||||
|
||||
final readings = (row[1] as String)
|
||||
.split(RegExp('[・/、(:?s+)]'))
|
||||
.map((e) => e.trim())
|
||||
.toList();
|
||||
|
||||
final meanings = (row[2] as String)
|
||||
.split(',')
|
||||
.expand(cleanMeaning)
|
||||
.toList();
|
||||
|
||||
result.add(
|
||||
JLPTRankedWord(
|
||||
readings: readings,
|
||||
kanji: kanji,
|
||||
jlptLevel: jlptLevel,
|
||||
meanings: meanings,
|
||||
),
|
||||
);
|
||||
}
|
||||
result.addAll(words);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
62
lib/models/word_search/word_search_match_span.dart
Normal file
62
lib/models/word_search/word_search_match_span.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
enum WordSearchMatchSpanType { kanji, kana, sense }
|
||||
|
||||
/// A span of a word search result that corresponds to a match for a kanji, kana, or sense.
|
||||
class WordSearchMatchSpan {
|
||||
/// Which subtype of the word search result this span corresponds to - either a kanji, a kana, or a sense.
|
||||
final WordSearchMatchSpanType spanType;
|
||||
|
||||
/// The index of the kanji/kana/sense in the word search result that this span corresponds to.
|
||||
final int index;
|
||||
|
||||
/// When matching a 'sense', this is the index of the English definition in that sense that this span corresponds to. Otherwise, this is always 0.
|
||||
final int subIndex;
|
||||
|
||||
/// The start of the span (inclusive)
|
||||
final int start;
|
||||
|
||||
/// The end of the span (inclusive)
|
||||
final int end;
|
||||
|
||||
WordSearchMatchSpan({
|
||||
required this.spanType,
|
||||
required this.index,
|
||||
required this.start,
|
||||
required this.end,
|
||||
this.subIndex = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'WordSearchMatchSpan(spanType: $spanType, index: $index, start: $start, end: $end)';
|
||||
}
|
||||
|
||||
Map<String, Object?> toJson() => {
|
||||
'spanType': spanType.toString().split('.').last,
|
||||
'index': index,
|
||||
'start': start,
|
||||
'end': end,
|
||||
};
|
||||
|
||||
factory WordSearchMatchSpan.fromJson(Map<String, dynamic> json) =>
|
||||
WordSearchMatchSpan(
|
||||
spanType: WordSearchMatchSpanType.values.firstWhere(
|
||||
(e) => e.toString().split('.').last == json['spanType'],
|
||||
),
|
||||
index: json['index'] as int,
|
||||
start: json['start'] as int,
|
||||
end: json['end'] as int,
|
||||
);
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(spanType, index, start, end);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is WordSearchMatchSpan &&
|
||||
other.spanType == spanType &&
|
||||
other.index == index &&
|
||||
other.start == start &&
|
||||
other.end == end;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import 'package:jadb/models/common/jlpt_level.dart';
|
||||
import 'package:jadb/models/jmdict/jmdict_kanji_info.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_ruby.dart';
|
||||
import 'package:jadb/models/word_search/word_search_sense.dart';
|
||||
import 'package:jadb/models/word_search/word_search_sources.dart';
|
||||
import 'package:jadb/search/word_search/word_search.dart';
|
||||
|
||||
/// A class representing a single dictionary entry from a word search.
|
||||
class WordSearchResult {
|
||||
@@ -34,7 +36,15 @@ class WordSearchResult {
|
||||
/// A class listing the sources used to make up the data for this word search result.
|
||||
final WordSearchSources sources;
|
||||
|
||||
const WordSearchResult({
|
||||
/// A list of spans, specifying which part of this word result matched the search keyword.
|
||||
///
|
||||
/// Note that this is considered ephemeral data - it does not originate from the dictionary,
|
||||
/// and unlike the rest of the class it varies based on external information (the searchword).
|
||||
/// It will *NOT* be exported to JSON, but can be reinferred by invoking [inferMatchSpans] with
|
||||
/// the original searchword.
|
||||
List<WordSearchMatchSpan>? matchSpans;
|
||||
|
||||
WordSearchResult({
|
||||
required this.score,
|
||||
required this.entryId,
|
||||
required this.isCommon,
|
||||
@@ -44,6 +54,7 @@ class WordSearchResult {
|
||||
required this.senses,
|
||||
required this.jlptLevel,
|
||||
required this.sources,
|
||||
this.matchSpans,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
@@ -81,6 +92,77 @@ class WordSearchResult {
|
||||
sources: WordSearchSources.fromJson(json['sources']),
|
||||
);
|
||||
|
||||
factory WordSearchResult.empty() => WordSearchResult(
|
||||
score: 0,
|
||||
entryId: 0,
|
||||
isCommon: false,
|
||||
japanese: [],
|
||||
kanjiInfo: {},
|
||||
readingInfo: {},
|
||||
senses: [],
|
||||
jlptLevel: JlptLevel.none,
|
||||
sources: WordSearchSources.empty(),
|
||||
);
|
||||
|
||||
/// Infers which part(s) of this word search result matched the search keyword, and populates [matchSpans] accordingly.
|
||||
void inferMatchSpans(
|
||||
String searchword, {
|
||||
SearchMode searchMode = SearchMode.Auto,
|
||||
}) {
|
||||
// TODO: handle wildcards like '?' and '*' when that becomes supported in the search.
|
||||
// TODO: If the searchMode is provided, we can use that to narrow down which part of the word search results to look at.
|
||||
|
||||
final regex = RegExp(RegExp.escape(searchword));
|
||||
final matchSpans = <WordSearchMatchSpan>[];
|
||||
|
||||
for (final (i, japanese) in japanese.indexed) {
|
||||
final baseMatches = regex.allMatches(japanese.base);
|
||||
matchSpans.addAll(
|
||||
baseMatches.map(
|
||||
(match) => WordSearchMatchSpan(
|
||||
spanType: WordSearchMatchSpanType.kanji,
|
||||
index: i,
|
||||
start: match.start,
|
||||
end: match.end,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (japanese.furigana != null) {
|
||||
final furiganaMatches = regex.allMatches(japanese.furigana!);
|
||||
matchSpans.addAll(
|
||||
furiganaMatches.map(
|
||||
(match) => WordSearchMatchSpan(
|
||||
spanType: WordSearchMatchSpanType.kana,
|
||||
index: i,
|
||||
start: match.start,
|
||||
end: match.end,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (final (i, sense) in senses.indexed) {
|
||||
for (final (k, definition) in sense.englishDefinitions.indexed) {
|
||||
final definitionMatches = regex.allMatches(definition);
|
||||
matchSpans.addAll(
|
||||
definitionMatches.map(
|
||||
(match) => WordSearchMatchSpan(
|
||||
spanType: WordSearchMatchSpanType.sense,
|
||||
index: i,
|
||||
subIndex: k,
|
||||
start: match.start,
|
||||
end: match.end,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.matchSpans = matchSpans;
|
||||
}
|
||||
|
||||
String _formatJapaneseWord(WordSearchRuby word) =>
|
||||
word.furigana == null ? word.base : '${word.base} (${word.furigana})';
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ class WordSearchSources {
|
||||
|
||||
const WordSearchSources({this.jmdict = true, this.jmnedict = false});
|
||||
|
||||
factory WordSearchSources.empty() => const WordSearchSources();
|
||||
|
||||
Map<String, Object?> get sqlValue => {'jmdict': jmdict, 'jmnedict': jmnedict};
|
||||
|
||||
Map<String, dynamic> toJson() => {'jmdict': jmdict, 'jmnedict': jmnedict};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'package:jadb/table_names/kanjidic.dart';
|
||||
import 'package:sqflite_common/sqflite.dart';
|
||||
|
||||
/// Filters a list of kanji characters, returning only those that exist in the database.
|
||||
///
|
||||
/// 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,
|
||||
|
||||
@@ -3,9 +3,9 @@ import 'package:jadb/models/kanji_search/kanji_search_radical.dart';
|
||||
import 'package:jadb/models/kanji_search/kanji_search_result.dart';
|
||||
import 'package:jadb/table_names/kanjidic.dart';
|
||||
import 'package:jadb/table_names/radkfile.dart';
|
||||
import 'package:jadb/util/romaji_transliteration.dart';
|
||||
import 'package:sqflite_common/sqflite.dart';
|
||||
|
||||
/// Searches for a kanji character and returns its details, or null if the kanji is not found in the database.
|
||||
Future<KanjiSearchResult?> searchKanjiWithDbConnection(
|
||||
DatabaseExecutor connection,
|
||||
String kanji,
|
||||
@@ -214,6 +214,7 @@ Future<KanjiSearchResult?> searchKanjiWithDbConnection(
|
||||
|
||||
// TODO: Use fewer queries with `IN` clauses to reduce the number of queries
|
||||
|
||||
/// 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,
|
||||
|
||||
@@ -3,10 +3,16 @@ import 'package:sqflite_common/sqlite_api.dart';
|
||||
|
||||
// TODO: validate that the list of radicals all are valid radicals
|
||||
|
||||
/// Returns a list of radicals that are part of any kanji that contains all of the input radicals.
|
||||
///
|
||||
/// 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(
|
||||
DatabaseExecutor connection,
|
||||
List<String> radicals,
|
||||
) async {
|
||||
final distinctRadicals = radicals.toSet();
|
||||
|
||||
final queryResult = await connection.rawQuery(
|
||||
'''
|
||||
SELECT DISTINCT "radical"
|
||||
@@ -14,12 +20,12 @@ Future<List<String>> searchRemainingRadicalsWithDbConnection(
|
||||
WHERE "kanji" IN (
|
||||
SELECT "kanji"
|
||||
FROM "${RADKFILETableNames.radkfile}"
|
||||
WHERE "radical" IN (${List.filled(radicals.length, '?').join(',')})
|
||||
WHERE "radical" IN (${List.filled(distinctRadicals.length, '?').join(',')})
|
||||
GROUP BY "kanji"
|
||||
HAVING COUNT(DISTINCT "radical") = ?
|
||||
)
|
||||
''',
|
||||
[...radicals, radicals.length],
|
||||
[...distinctRadicals, distinctRadicals.length],
|
||||
);
|
||||
|
||||
final remainingRadicals = queryResult
|
||||
@@ -29,19 +35,22 @@ Future<List<String>> searchRemainingRadicalsWithDbConnection(
|
||||
return remainingRadicals;
|
||||
}
|
||||
|
||||
/// Returns a list of kanji that contain all of the input radicals.
|
||||
Future<List<String>> searchKanjiByRadicalsWithDbConnection(
|
||||
DatabaseExecutor connection,
|
||||
List<String> radicals,
|
||||
) async {
|
||||
final distinctRadicals = radicals.toSet();
|
||||
|
||||
final queryResult = await connection.rawQuery(
|
||||
'''
|
||||
SELECT "kanji"
|
||||
FROM "${RADKFILETableNames.radkfile}"
|
||||
WHERE "radical" IN (${List.filled(radicals.length, '?').join(',')})
|
||||
WHERE "radical" IN (${List.filled(distinctRadicals.length, '?').join(',')})
|
||||
GROUP BY "kanji"
|
||||
HAVING COUNT(DISTINCT "radical") = ?
|
||||
''',
|
||||
[...radicals, radicals.length],
|
||||
[...distinctRadicals, distinctRadicals.length],
|
||||
);
|
||||
|
||||
final kanji = queryResult.map((row) => row['kanji'] as String).toList();
|
||||
|
||||
@@ -15,6 +15,7 @@ import 'package:sqflite_common/sqlite_api.dart';
|
||||
|
||||
enum SearchMode { Auto, English, Kanji, MixedKanji, Kana, MixedKana }
|
||||
|
||||
/// Searches for an input string, returning a list of results with their details. Returns null if the input string is empty.
|
||||
Future<List<WordSearchResult>?> searchWordWithDbConnection(
|
||||
DatabaseExecutor connection,
|
||||
String word, {
|
||||
@@ -51,9 +52,14 @@ Future<List<WordSearchResult>?> searchWordWithDbConnection(
|
||||
linearWordQueryData: linearWordQueryData,
|
||||
);
|
||||
|
||||
for (final resultEntry in result) {
|
||||
resultEntry.inferMatchSpans(word, searchMode: searchMode);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Searches for an input string, returning the amount of results that the search would yield without pagination.
|
||||
Future<int?> searchWordCountWithDbConnection(
|
||||
DatabaseExecutor connection,
|
||||
String word, {
|
||||
@@ -72,6 +78,7 @@ Future<int?> searchWordCountWithDbConnection(
|
||||
return entryIdCount;
|
||||
}
|
||||
|
||||
/// Fetches a single word by its entry ID, returning null if not found.
|
||||
Future<WordSearchResult?> getWordByIdWithDbConnection(
|
||||
DatabaseExecutor connection,
|
||||
int id,
|
||||
@@ -107,6 +114,7 @@ Future<WordSearchResult?> getWordByIdWithDbConnection(
|
||||
return result.firstOrNull;
|
||||
}
|
||||
|
||||
/// Fetches multiple words by their entry IDs, returning a map from entry ID to result.
|
||||
Future<Map<int, WordSearchResult>> getWordsByIdsWithDbConnection(
|
||||
DatabaseExecutor connection,
|
||||
Set<int> ids,
|
||||
|
||||
@@ -7,6 +7,29 @@ buildDartApplication {
|
||||
version = "1.0.0";
|
||||
inherit src;
|
||||
|
||||
dartEntryPoints."bin/jadb" = "bin/jadb.dart";
|
||||
|
||||
# NOTE: the default dart hooks are using `dart compile`, which is not able to call the
|
||||
# new dart build hooks required to use package:sqlite3 >= 3.0.0. So we override
|
||||
# these phases to use `dart build` instead.
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
|
||||
mkdir -p "$out/bin"
|
||||
dart build cli --target "bin/jadb.dart"
|
||||
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
mkdir -p "$out"
|
||||
mv build/cli/*/bundle/* "$out/"
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
autoPubspecLock = ../pubspec.lock;
|
||||
|
||||
meta.mainProgram = "jadb";
|
||||
|
||||
86
pubspec.lock
86
pubspec.lock
@@ -5,18 +5,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: "5b7468c326d2f8a4f630056404ca0d291ade42918f4a3c6233618e724f39da8e"
|
||||
sha256: "3b19a47f6ea7c2632760777c78174f47f6aec1e05f0cd611380d4593b8af1dbc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "92.0.0"
|
||||
version: "96.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "70e4b1ef8003c64793a9e268a551a82869a8a96f39deb73dea28084b0e8bf75e"
|
||||
sha256: "0c516bc4ad36a1a75759e54d5047cb9d15cded4459df01aa35a0b5ec7db2c2a0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.0.0"
|
||||
version: "10.2.0"
|
||||
args:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -49,6 +49,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
code_assets:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_assets
|
||||
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
collection:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -85,26 +93,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: csv
|
||||
sha256: c6aa2679b2a18cb57652920f674488d89712efaf4d3fdf2e537215b35fc19d6c
|
||||
sha256: bef2950f7a753eb82f894a2eabc3072e73cf21c17096296a5a992797e50b1d0d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
version: "7.1.0"
|
||||
equatable:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: equatable
|
||||
sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
|
||||
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.7"
|
||||
version: "2.0.8"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
|
||||
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.2.0"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -129,6 +137,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
hooks:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hooks
|
||||
sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -157,10 +173,10 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: lints
|
||||
sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0
|
||||
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
version: "6.1.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -181,10 +197,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
sha256: "9f29b9bcc8ee287b1a31e0d01be0eae99a930dbffdaecf04b3f3d82a969f296f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
version: "1.18.1"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -193,6 +209,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
native_toolchain_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: native_toolchain_c
|
||||
sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.17.4"
|
||||
node_preamble:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -221,10 +245,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
|
||||
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
version: "7.0.2"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -293,10 +317,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.1"
|
||||
version: "1.10.2"
|
||||
sqflite_common:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -309,18 +333,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqflite_common_ffi
|
||||
sha256: "9faa2fedc5385ef238ce772589f7718c24cdddd27419b609bb9c6f703ea27988"
|
||||
sha256: c59fcdc143839a77581f7a7c4de018e53682408903a0a0800b95ef2dc4033eff
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.6"
|
||||
version: "2.4.0+2"
|
||||
sqlite3:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqlite3
|
||||
sha256: "3145bd74dcdb4fd6f5c6dda4d4e4490a8087d7f286a14dee5d37087290f0f8a2"
|
||||
sha256: b7cf6b37667f6a921281797d2499ffc60fb878b161058d422064f0ddc78f6aa6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.9.4"
|
||||
version: "3.1.6"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -365,26 +389,26 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: test
|
||||
sha256: "77cc98ea27006c84e71a7356cf3daf9ddbde2d91d84f77dbfe64cf0e4d9611ae"
|
||||
sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.28.0"
|
||||
version: "1.29.0"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8"
|
||||
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.8"
|
||||
version: "0.7.9"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
sha256: f1072617a6657e5fc09662e721307f7fb009b4ed89b19f47175d11d5254a62d4
|
||||
sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.14"
|
||||
version: "0.6.15"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -405,10 +429,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: f52385d4f73589977c80797e60fe51014f7f2b957b5e9a62c3f6ada439889249
|
||||
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.2.1"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -458,4 +482,4 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.9.0 <4.0.0"
|
||||
dart: ">=3.10.1 <4.0.0"
|
||||
|
||||
@@ -9,12 +9,12 @@ environment:
|
||||
dependencies:
|
||||
args: ^2.7.0
|
||||
collection: ^1.19.0
|
||||
csv: ^6.0.0
|
||||
csv: ^7.1.0
|
||||
equatable: ^2.0.0
|
||||
path: ^1.9.1
|
||||
sqflite_common: ^2.5.0
|
||||
sqflite_common_ffi: ^2.3.0
|
||||
sqlite3: ^2.9.4
|
||||
sqlite3: ^3.1.6
|
||||
xml: ^6.5.0
|
||||
|
||||
dev_dependencies:
|
||||
@@ -24,6 +24,11 @@ dev_dependencies:
|
||||
executables:
|
||||
jadb: jadb
|
||||
|
||||
hooks:
|
||||
user_defines:
|
||||
sqlite3:
|
||||
source: system
|
||||
|
||||
topics:
|
||||
- database
|
||||
- dictionary
|
||||
|
||||
@@ -4,20 +4,11 @@ import 'dart:io';
|
||||
import 'package:jadb/models/create_empty_db.dart';
|
||||
import 'package:jadb/search.dart';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
import 'package:sqlite3/open.dart';
|
||||
// import 'package:sqlite3/open.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
Future<DatabaseExecutor> setup_inmemory_database() async {
|
||||
final libsqlitePath = Platform.environment['LIBSQLITE_PATH'];
|
||||
|
||||
if (libsqlitePath == null) {
|
||||
throw Exception('LIBSQLITE_PATH is not set');
|
||||
}
|
||||
|
||||
final dbConnection = await createDatabaseFactoryFfi(
|
||||
ffiInit: () =>
|
||||
open.overrideForAll(() => DynamicLibrary.open(libsqlitePath)),
|
||||
).openDatabase(':memory:');
|
||||
final dbConnection = await createDatabaseFactoryFfi().openDatabase(':memory:');
|
||||
|
||||
return dbConnection;
|
||||
}
|
||||
|
||||
194
test/search/search_match_inference_test.dart
Normal file
194
test/search/search_match_inference_test.dart
Normal file
@@ -0,0 +1,194 @@
|
||||
import 'package:jadb/models/common/jlpt_level.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_sources.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
test('Infer match whole word', () {
|
||||
final wordSearchResult = WordSearchResult(
|
||||
entryId: 0,
|
||||
score: 0,
|
||||
isCommon: false,
|
||||
jlptLevel: JlptLevel.none,
|
||||
kanjiInfo: {},
|
||||
readingInfo: {},
|
||||
japanese: [WordSearchRuby(base: '仮名')],
|
||||
senses: [],
|
||||
sources: WordSearchSources.empty(),
|
||||
);
|
||||
|
||||
wordSearchResult.inferMatchSpans('仮名');
|
||||
|
||||
expect(wordSearchResult.matchSpans, [
|
||||
WordSearchMatchSpan(
|
||||
spanType: WordSearchMatchSpanType.kanji,
|
||||
start: 0,
|
||||
end: 2,
|
||||
index: 0,
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
test('Infer match part of word', () {
|
||||
final wordSearchResult = WordSearchResult(
|
||||
entryId: 0,
|
||||
score: 0,
|
||||
isCommon: false,
|
||||
jlptLevel: JlptLevel.none,
|
||||
kanjiInfo: {},
|
||||
readingInfo: {},
|
||||
japanese: [WordSearchRuby(base: '仮名')],
|
||||
senses: [],
|
||||
sources: WordSearchSources.empty(),
|
||||
);
|
||||
|
||||
wordSearchResult.inferMatchSpans('仮');
|
||||
|
||||
expect(wordSearchResult.matchSpans, [
|
||||
WordSearchMatchSpan(
|
||||
spanType: WordSearchMatchSpanType.kanji,
|
||||
start: 0,
|
||||
end: 1,
|
||||
index: 0,
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
test('Infer match in middle of word', () {
|
||||
final wordSearchResult = WordSearchResult(
|
||||
entryId: 0,
|
||||
score: 0,
|
||||
isCommon: false,
|
||||
jlptLevel: JlptLevel.none,
|
||||
kanjiInfo: {},
|
||||
readingInfo: {},
|
||||
japanese: [WordSearchRuby(base: 'ありがとう')],
|
||||
senses: [],
|
||||
sources: WordSearchSources.empty(),
|
||||
);
|
||||
|
||||
wordSearchResult.inferMatchSpans('りがと');
|
||||
|
||||
expect(wordSearchResult.matchSpans, [
|
||||
WordSearchMatchSpan(
|
||||
spanType: WordSearchMatchSpanType.kanji,
|
||||
start: 1,
|
||||
end: 4,
|
||||
index: 0,
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
test('Infer match in furigana', () {
|
||||
final wordSearchResult = WordSearchResult(
|
||||
entryId: 0,
|
||||
score: 0,
|
||||
isCommon: false,
|
||||
jlptLevel: JlptLevel.none,
|
||||
kanjiInfo: {},
|
||||
readingInfo: {},
|
||||
japanese: [WordSearchRuby(base: '仮名', furigana: 'かな')],
|
||||
senses: [],
|
||||
sources: WordSearchSources.empty(),
|
||||
);
|
||||
|
||||
wordSearchResult.inferMatchSpans('かな');
|
||||
|
||||
expect(wordSearchResult.matchSpans, [
|
||||
WordSearchMatchSpan(
|
||||
spanType: WordSearchMatchSpanType.kana,
|
||||
start: 0,
|
||||
end: 2,
|
||||
index: 0,
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
test('Infer match in sense', () {
|
||||
final wordSearchResult = WordSearchResult(
|
||||
entryId: 0,
|
||||
score: 0,
|
||||
isCommon: false,
|
||||
jlptLevel: JlptLevel.none,
|
||||
kanjiInfo: {},
|
||||
readingInfo: {},
|
||||
japanese: [WordSearchRuby(base: '仮名')],
|
||||
senses: [
|
||||
WordSearchSense(
|
||||
antonyms: [],
|
||||
dialects: [],
|
||||
englishDefinitions: ['kana'],
|
||||
fields: [],
|
||||
info: [],
|
||||
languageSource: [],
|
||||
misc: [],
|
||||
partsOfSpeech: [],
|
||||
restrictedToKanji: [],
|
||||
restrictedToReading: [],
|
||||
seeAlso: [],
|
||||
),
|
||||
],
|
||||
sources: WordSearchSources.empty(),
|
||||
);
|
||||
|
||||
wordSearchResult.inferMatchSpans('kana');
|
||||
|
||||
expect(wordSearchResult.matchSpans, [
|
||||
WordSearchMatchSpan(
|
||||
spanType: WordSearchMatchSpanType.sense,
|
||||
start: 0,
|
||||
end: 4,
|
||||
index: 0,
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
test('Infer multiple matches', () {
|
||||
final wordSearchResult = WordSearchResult(
|
||||
entryId: 0,
|
||||
score: 0,
|
||||
isCommon: false,
|
||||
jlptLevel: JlptLevel.none,
|
||||
kanjiInfo: {},
|
||||
readingInfo: {},
|
||||
japanese: [WordSearchRuby(base: '仮名', furigana: 'かな')],
|
||||
senses: [
|
||||
WordSearchSense(
|
||||
antonyms: [],
|
||||
dialects: [],
|
||||
englishDefinitions: ['kana', 'the kana'],
|
||||
fields: [],
|
||||
info: [],
|
||||
languageSource: [],
|
||||
misc: [],
|
||||
partsOfSpeech: [],
|
||||
restrictedToKanji: [],
|
||||
restrictedToReading: [],
|
||||
seeAlso: [],
|
||||
),
|
||||
],
|
||||
sources: WordSearchSources.empty(),
|
||||
);
|
||||
|
||||
wordSearchResult.inferMatchSpans('kana');
|
||||
|
||||
expect(wordSearchResult.matchSpans, [
|
||||
WordSearchMatchSpan(
|
||||
spanType: WordSearchMatchSpanType.sense,
|
||||
start: 0,
|
||||
end: 4,
|
||||
index: 0,
|
||||
),
|
||||
WordSearchMatchSpan(
|
||||
spanType: WordSearchMatchSpanType.sense,
|
||||
start: 4,
|
||||
end: 8,
|
||||
index: 0,
|
||||
subIndex: 1,
|
||||
),
|
||||
]);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user