feat(species): auto-classify variety species from its label
Infer the catalog species a free-text variety label names ("Maiz de la
abuela" -> Zea mays) and prefill the category from the species family.
- Pure, testable matcher (domain/species_autoclassify.dart): whole-word,
accent/case-insensitive, Unicode-aware (any script), longest-name-wins,
ambiguous names left unclassified, light plural fold.
- Quick-add and draft naming auto-link the species when the field is empty
(non-destructive; an explicit category is kept).
- Edit sheet offers a one-tap suggestion from the typed name.
Tests: matcher unit, repository integration, SpeciesRepository.classifyLabel,
and an edit-sheet widget test.
This commit is contained in:
parent
0e6ef13d00
commit
b840b83c42
15 changed files with 510 additions and 3 deletions
68
apps/app_seeds/test/domain/species_autoclassify_test.dart
Normal file
68
apps/app_seeds/test/domain/species_autoclassify_test.dart
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tane/domain/species_autoclassify.dart';
|
||||
|
||||
/// A small catalog spanning the tricky cases: accents, multi-word names, a
|
||||
/// name shared across two species, and substrings that must NOT fire.
|
||||
const _names = [
|
||||
SpeciesNameEntry(speciesId: 'zea', name: 'Maíz'),
|
||||
SpeciesNameEntry(speciesId: 'zea', name: 'Maize'),
|
||||
SpeciesNameEntry(speciesId: 'zea', name: 'Zea mays'),
|
||||
SpeciesNameEntry(speciesId: 'tomato', name: 'Tomate'),
|
||||
SpeciesNameEntry(speciesId: 'tomato', name: 'Tomato'),
|
||||
SpeciesNameEntry(speciesId: 'bean', name: 'Common bean'),
|
||||
SpeciesNameEntry(speciesId: 'faba', name: 'Broad bean'),
|
||||
SpeciesNameEntry(speciesId: 'faba', name: 'Haba'),
|
||||
SpeciesNameEntry(speciesId: 'pea', name: 'Pea'),
|
||||
// "Calabaza" is deliberately shared → ambiguous, must stay unclassified.
|
||||
SpeciesNameEntry(speciesId: 'pepo', name: 'Calabaza'),
|
||||
SpeciesNameEntry(speciesId: 'maxima', name: 'Calabaza'),
|
||||
];
|
||||
|
||||
void main() {
|
||||
String? match(String label) => matchSpeciesInLabel(label, _names);
|
||||
|
||||
test('matches a common name embedded in a free-text label', () {
|
||||
expect(match('Maíz de la abuela'), 'zea');
|
||||
expect(match('Tomate cherry'), 'tomato');
|
||||
});
|
||||
|
||||
test('is accent- and case-insensitive', () {
|
||||
expect(match('MAIZ dulce'), 'zea'); // no accent, upper-case
|
||||
expect(match('tomate rosa'), 'tomato');
|
||||
});
|
||||
|
||||
test('matches the scientific name too', () {
|
||||
expect(match('Zea mays landrace'), 'zea');
|
||||
});
|
||||
|
||||
test('matches multi-word names as a contiguous phrase', () {
|
||||
expect(match('Broad bean Aquadulce'), 'faba');
|
||||
expect(match('Common bean from grandma'), 'bean');
|
||||
});
|
||||
|
||||
test('prefers the longest (most specific) name', () {
|
||||
// "bean" alone is not in the catalog, but the two-word names should win
|
||||
// over any single-word coincidence.
|
||||
expect(match('Broad bean'), 'faba');
|
||||
});
|
||||
|
||||
test('folds a plural label word to a singular catalog name', () {
|
||||
expect(match('Tomates de rama'), 'tomato');
|
||||
expect(match('Habas de mi huerto'), 'faba');
|
||||
});
|
||||
|
||||
test('does not fire on a substring that is not a whole word', () {
|
||||
expect(match('Habanero picante'), isNull); // contains "haba" as substring
|
||||
expect(match('Peach tree'), isNull); // contains "pea" as substring
|
||||
});
|
||||
|
||||
test('returns null when a name is shared across species (ambiguous)', () {
|
||||
expect(match('Calabaza de invierno'), isNull);
|
||||
});
|
||||
|
||||
test('returns null when nothing matches', () {
|
||||
expect(match('Girasol gigante'), isNull);
|
||||
expect(match(' '), isNull);
|
||||
expect(match(''), isNull);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue