TF-4009 Add AttachmentTextDetector class
This commit is contained in:
@@ -0,0 +1,101 @@
|
|||||||
|
import 'package:tmail_ui_user/main/localizations/language_code_constants.dart';
|
||||||
|
|
||||||
|
class AttachmentTextDetector {
|
||||||
|
const AttachmentTextDetector._();
|
||||||
|
// List of keywords by language
|
||||||
|
static final Map<String, List<String>> _keywordsByLang = {
|
||||||
|
LanguageCodeConstants.english: [
|
||||||
|
'attach',
|
||||||
|
'attachment',
|
||||||
|
],
|
||||||
|
LanguageCodeConstants.french: [
|
||||||
|
'pièce jointe',
|
||||||
|
'fichier joint',
|
||||||
|
'document joint',
|
||||||
|
'pj',
|
||||||
|
'jointe',
|
||||||
|
'joint',
|
||||||
|
],
|
||||||
|
LanguageCodeConstants.russian: [
|
||||||
|
'прикрепить',
|
||||||
|
'приложение',
|
||||||
|
'документ',
|
||||||
|
'файл',
|
||||||
|
'отчёт',
|
||||||
|
'вложение',
|
||||||
|
],
|
||||||
|
LanguageCodeConstants.vietnamese: [
|
||||||
|
'đính kèm',
|
||||||
|
'tài liệu',
|
||||||
|
'tệp',
|
||||||
|
'báo cáo',
|
||||||
|
'file',
|
||||||
|
'attach',
|
||||||
|
'attachment',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Detect if the text contains keywords suggesting there is an attachment.
|
||||||
|
/// [lang] is the language code (`en`, `fr`, `ru`, `vi`, ...).
|
||||||
|
static bool containsAttachmentKeyword(String text, {required String lang}) {
|
||||||
|
final lowerText = text.toLowerCase();
|
||||||
|
final keywords = _keywordsByLang[lang.toLowerCase()];
|
||||||
|
if (keywords == null) return false;
|
||||||
|
|
||||||
|
return keywords.any((k) => lowerText.contains(k.toLowerCase()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a list of matched keywords (if any) for the specified language
|
||||||
|
static List<String> matchedKeywords(String text, {required String lang}) {
|
||||||
|
final lowerText = text.toLowerCase();
|
||||||
|
final keywords = _keywordsByLang[lang.toLowerCase()];
|
||||||
|
if (keywords == null) return [];
|
||||||
|
|
||||||
|
return keywords
|
||||||
|
.where((k) => lowerText.contains(k.toLowerCase()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect if text contains keyword in any language
|
||||||
|
static bool containsAnyAttachmentKeyword(String text) {
|
||||||
|
final lowerText = text.toLowerCase();
|
||||||
|
for (final keywords in _keywordsByLang.values) {
|
||||||
|
if (keywords.any((k) => lowerText.contains(k.toLowerCase()))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a map of languages with a list of matched keywords
|
||||||
|
static Map<String, List<String>> matchedKeywordsAll(String text) {
|
||||||
|
final result = <String, List<String>>{};
|
||||||
|
final lowerText = text.toLowerCase();
|
||||||
|
|
||||||
|
_keywordsByLang.forEach((lang, keywords) {
|
||||||
|
final matches =
|
||||||
|
keywords.where((k) => lowerText.contains(k.toLowerCase())).toList();
|
||||||
|
if (matches.isNotEmpty) {
|
||||||
|
result[lang] = matches;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a list of matching keywords but removes duplicates
|
||||||
|
static List<String> matchedKeywordsUnique(String text) {
|
||||||
|
final lowerText = text.toLowerCase();
|
||||||
|
final result = <String>{};
|
||||||
|
|
||||||
|
_keywordsByLang.forEach((_, keywords) {
|
||||||
|
for (final k in keywords) {
|
||||||
|
if (lowerText.contains(k.toLowerCase())) {
|
||||||
|
result.add(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
import 'package:core/utils/app_logger.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:tmail_ui_user/features/composer/presentation/manager/attachment_text_detector.dart';
|
||||||
|
|
||||||
|
/// Helper: generate email about [targetLength] characters long
|
||||||
|
String generateLongEmail(int targetLength, {bool includeKeywords = true}) {
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
const sample = "This is a random paragraph with no attachments. ";
|
||||||
|
while (buffer.length < targetLength) {
|
||||||
|
buffer.write(sample);
|
||||||
|
}
|
||||||
|
if (includeKeywords) {
|
||||||
|
buffer.write("Here is the pièce jointe and báo cáo attached.");
|
||||||
|
}
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('AttachmentTextDetector.containsAttachmentKeyword', () {
|
||||||
|
test('English - should detect "attach" and "attachment"', () {
|
||||||
|
expect(
|
||||||
|
AttachmentTextDetector.containsAttachmentKeyword(
|
||||||
|
"Please see the attached document for details.",
|
||||||
|
lang: 'en',
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
AttachmentTextDetector.containsAttachmentKeyword(
|
||||||
|
"No attachments here.",
|
||||||
|
lang: 'en',
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
AttachmentTextDetector.containsAttachmentKeyword(
|
||||||
|
"Nothing relevant",
|
||||||
|
lang: 'en',
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('French - should detect common attachment words', () {
|
||||||
|
expect(
|
||||||
|
AttachmentTextDetector.containsAttachmentKeyword(
|
||||||
|
"Veuillez trouver la pièce jointe.",
|
||||||
|
lang: 'fr',
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
AttachmentTextDetector.containsAttachmentKeyword(
|
||||||
|
"Ci-joint le fichier joint pour votre examen.",
|
||||||
|
lang: 'fr',
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
AttachmentTextDetector.containsAttachmentKeyword(
|
||||||
|
"Aucun document ici",
|
||||||
|
lang: 'fr',
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Russian - should detect common attachment words', () {
|
||||||
|
expect(
|
||||||
|
AttachmentTextDetector.containsAttachmentKeyword(
|
||||||
|
"Смотрите приложение с отчетом.",
|
||||||
|
lang: 'ru',
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
AttachmentTextDetector.containsAttachmentKeyword(
|
||||||
|
"Документ прикрепить ниже.",
|
||||||
|
lang: 'ru',
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
AttachmentTextDetector.containsAttachmentKeyword(
|
||||||
|
"Текст без вложений",
|
||||||
|
lang: 'ru',
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Vietnamese - should detect common attachment words', () {
|
||||||
|
expect(
|
||||||
|
AttachmentTextDetector.containsAttachmentKeyword(
|
||||||
|
"Vui lòng xem tài liệu đính kèm.",
|
||||||
|
lang: 'vi',
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
AttachmentTextDetector.containsAttachmentKeyword(
|
||||||
|
"Đây là một file rất quan trọng.",
|
||||||
|
lang: 'vi',
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
AttachmentTextDetector.containsAttachmentKeyword(
|
||||||
|
"Không có gì cần gửi thêm.",
|
||||||
|
lang: 'vi',
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Unsupported language should return false', () {
|
||||||
|
expect(
|
||||||
|
AttachmentTextDetector.containsAttachmentKeyword(
|
||||||
|
"Some random text",
|
||||||
|
lang: 'de',
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('AttachmentTextDetector.matchedKeywords', () {
|
||||||
|
test('should return matched keywords for Vietnamese', () {
|
||||||
|
final matches = AttachmentTextDetector.matchedKeywords(
|
||||||
|
"Đây là báo cáo và tài liệu đính kèm.",
|
||||||
|
lang: 'vi',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(matches, containsAll(['báo cáo', 'tài liệu', 'đính kèm']));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return empty list when no keywords found', () {
|
||||||
|
final matches = AttachmentTextDetector.matchedKeywords(
|
||||||
|
"Nội dung không có gì liên quan",
|
||||||
|
lang: 'vi',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(matches, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return matched keywords for English', () {
|
||||||
|
final matches = AttachmentTextDetector.matchedKeywords(
|
||||||
|
"Please check the attachment and attach your signature.",
|
||||||
|
lang: 'en',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(matches, containsAll(['attachment', 'attach']));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('AttachmentTextDetector.containsAnyAttachmentKeyword', () {
|
||||||
|
test('should detect when email contains English keyword', () {
|
||||||
|
expect(
|
||||||
|
AttachmentTextDetector.containsAnyAttachmentKeyword(
|
||||||
|
"Please see the ATTACHed document.",
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should detect when email contains Vietnamese keyword', () {
|
||||||
|
expect(
|
||||||
|
AttachmentTextDetector.containsAnyAttachmentKeyword(
|
||||||
|
"Đây là tài liệu đính kèm.",
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should detect when email contains Russian keyword', () {
|
||||||
|
expect(
|
||||||
|
AttachmentTextDetector.containsAnyAttachmentKeyword(
|
||||||
|
"Документ прикрепить ниже.",
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should detect when email contains French keyword', () {
|
||||||
|
expect(
|
||||||
|
AttachmentTextDetector.containsAnyAttachmentKeyword(
|
||||||
|
"Veuillez trouver la pièce jointe.",
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return false when no keywords found', () {
|
||||||
|
expect(
|
||||||
|
AttachmentTextDetector.containsAnyAttachmentKeyword(
|
||||||
|
"This email has nothing special.",
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should detect when email contains multiple languages', () {
|
||||||
|
const email = """
|
||||||
|
Please see the attached document.
|
||||||
|
Vui lòng xem tài liệu đính kèm.
|
||||||
|
Смотрите приложение.
|
||||||
|
""";
|
||||||
|
|
||||||
|
expect(
|
||||||
|
AttachmentTextDetector.containsAnyAttachmentKeyword(email),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('AttachmentTextDetector.matchedKeywordsAll', () {
|
||||||
|
test('should return matches for multiple languages', () {
|
||||||
|
const email = """
|
||||||
|
Please see the attached document.
|
||||||
|
Vui lòng xem tài liệu đính kèm.
|
||||||
|
Смотрите приложение с отчётом.
|
||||||
|
""";
|
||||||
|
|
||||||
|
final matches = AttachmentTextDetector.matchedKeywordsAll(email);
|
||||||
|
|
||||||
|
expect(matches.keys, containsAll(['en', 'vi', 'ru']));
|
||||||
|
expect(matches['en'], contains('attach'));
|
||||||
|
expect(matches['vi'], containsAll(['tài liệu', 'đính kèm']));
|
||||||
|
expect(matches['ru'], containsAll(['приложение', 'отчёт']));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return matches only for one language', () {
|
||||||
|
const email = "Veuillez trouver la pièce jointe.";
|
||||||
|
|
||||||
|
final matches = AttachmentTextDetector.matchedKeywordsAll(email);
|
||||||
|
|
||||||
|
expect(matches.keys, equals(['fr']));
|
||||||
|
expect(matches['fr'], contains('pièce jointe'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return empty map when no keywords found', () {
|
||||||
|
const email = "Completely unrelated text.";
|
||||||
|
|
||||||
|
final matches = AttachmentTextDetector.matchedKeywordsAll(email);
|
||||||
|
|
||||||
|
expect(matches, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should be case-insensitive', () {
|
||||||
|
const email = "ATTACHMENT and PiÈce Jointe included.";
|
||||||
|
|
||||||
|
final matches = AttachmentTextDetector.matchedKeywordsAll(email);
|
||||||
|
|
||||||
|
expect(matches['en'], contains('attachment'));
|
||||||
|
expect(matches['fr'], contains('pièce jointe'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('AttachmentTextDetector.matchedKeywordsUnique', () {
|
||||||
|
test('should return unique matches across multiple languages', () {
|
||||||
|
const email = """
|
||||||
|
Please see the attached document.
|
||||||
|
Vui lòng xem tài liệu đính kèm.
|
||||||
|
Смотрите приложение с отчетом.
|
||||||
|
""";
|
||||||
|
|
||||||
|
final matches = AttachmentTextDetector.matchedKeywordsUnique(email);
|
||||||
|
|
||||||
|
expect(matches, containsAll(['attach', 'tài liệu', 'đính kèm', 'приложение']));
|
||||||
|
expect(matches.toSet().length, matches.length, reason: 'No duplicates allowed');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return unique matches even if repeated multiple times', () {
|
||||||
|
const email = """
|
||||||
|
file file file attach attach attach
|
||||||
|
đính kèm đính kèm
|
||||||
|
приложение приложение
|
||||||
|
""";
|
||||||
|
|
||||||
|
final matches = AttachmentTextDetector.matchedKeywordsUnique(email);
|
||||||
|
|
||||||
|
expect(matches, containsAll(['file', 'attach', 'đính kèm', 'приложение']));
|
||||||
|
expect(matches.toSet().length, matches.length, reason: 'Duplicates must be removed');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return empty list if no keywords present', () {
|
||||||
|
const email = "This email does not mention anything related to document.";
|
||||||
|
final matches = AttachmentTextDetector.matchedKeywordsUnique(email);
|
||||||
|
expect(matches, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should match Russian word with both ё and е', () {
|
||||||
|
const email = "Вот наш отчёт и вот наш снова.";
|
||||||
|
final matches = AttachmentTextDetector.matchedKeywordsUnique(email);
|
||||||
|
|
||||||
|
expect(matches, containsAll(['отчёт']));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Measure performance when detecting long emails, e.g. several thousand characters
|
||||||
|
group('AttachmentTextDetector Benchmark', () {
|
||||||
|
late String longEmail;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
// Generate email ~50,000 characters long (many unrelated paragraphs)
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
const sample = "This is a random paragraph with no attachments. ";
|
||||||
|
for (int i = 0; i < 1000; i++) {
|
||||||
|
buffer.write(sample);
|
||||||
|
}
|
||||||
|
// Insert some keywords in the middle
|
||||||
|
buffer.write("Here is the pièce jointe and báo cáo attached.");
|
||||||
|
longEmail = buffer.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Benchmark containsAnyAttachmentKeyword', () {
|
||||||
|
final sw = Stopwatch()..start();
|
||||||
|
final result =
|
||||||
|
AttachmentTextDetector.containsAnyAttachmentKeyword(longEmail);
|
||||||
|
sw.stop();
|
||||||
|
|
||||||
|
log(
|
||||||
|
'containsAnyAttachmentKeyword -> result=$result, elapsed=${sw.elapsedMicroseconds}µs',
|
||||||
|
);
|
||||||
|
expect(result, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Benchmark matchedKeywordsAll', () {
|
||||||
|
final sw = Stopwatch()..start();
|
||||||
|
final matches = AttachmentTextDetector.matchedKeywordsAll(longEmail);
|
||||||
|
sw.stop();
|
||||||
|
|
||||||
|
log(
|
||||||
|
'matchedKeywordsAll -> found=${matches.keys}, elapsed=${sw.elapsedMicroseconds}µs',
|
||||||
|
);
|
||||||
|
expect(matches.isNotEmpty, isTrue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Compare time when email is 100K, 500K, 1M characters long
|
||||||
|
group('AttachmentTextDetector Stress Benchmark', () {
|
||||||
|
final sizes = [100000, 500000, 1000000]; // 100K, 500K, 1M characters
|
||||||
|
|
||||||
|
for (final size in sizes) {
|
||||||
|
test('Benchmark size=$size containsAnyAttachmentKeyword', () {
|
||||||
|
final email = generateLongEmail(size);
|
||||||
|
final sw = Stopwatch()..start();
|
||||||
|
final result =
|
||||||
|
AttachmentTextDetector.containsAnyAttachmentKeyword(email);
|
||||||
|
sw.stop();
|
||||||
|
|
||||||
|
log(
|
||||||
|
'containsAnyAttachmentKeyword(size=$size) -> result=$result, elapsed=${sw.elapsedMilliseconds}ms',
|
||||||
|
);
|
||||||
|
expect(result, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Benchmark size=$size matchedKeywordsAll', () {
|
||||||
|
final email = generateLongEmail(size);
|
||||||
|
final sw = Stopwatch()..start();
|
||||||
|
final matches = AttachmentTextDetector.matchedKeywordsAll(email);
|
||||||
|
sw.stop();
|
||||||
|
|
||||||
|
log(
|
||||||
|
'matchedKeywordsAll(size=$size) -> found=${matches.keys}, elapsed=${sw.elapsedMilliseconds}ms',
|
||||||
|
);
|
||||||
|
expect(matches.isNotEmpty, isTrue);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user