From ee42e7116887f8965901335cf8e6800bdf295776 Mon Sep 17 00:00:00 2001 From: AmanRajSinghMourya Date: Fri, 25 Jul 2025 11:38:41 +0530 Subject: [PATCH 1/2] Enhance date parsing to support month-year format --- .../lib/services/date_parse_service.dart | 205 ++++++++++-------- 1 file changed, 114 insertions(+), 91 deletions(-) diff --git a/mobile/apps/photos/lib/services/date_parse_service.dart b/mobile/apps/photos/lib/services/date_parse_service.dart index ef136d25ed..54a018158c 100644 --- a/mobile/apps/photos/lib/services/date_parse_service.dart +++ b/mobile/apps/photos/lib/services/date_parse_service.dart @@ -45,8 +45,8 @@ class DateParseService { RegExp(r'^(\d{1,2})[\/-](\d{1,2})[\/-](\d{2,4})$'); static final _dotFormatRegex = RegExp(r'^(\d{1,2})\.(\d{1,2})\.(\d{2,4})$'); static final _compactFormatRegex = RegExp(r'^(\d{8})$'); - static final _yearOnlyRegex = RegExp(r'^\s*(\d{4})\s*$'); static final _shortFormatRegex = RegExp(r'^(\d{1,2})[\/-](\d{1,2})$'); + static final _monthYearRegex = RegExp(r'^(\d{1,2})[\/-](\d{4})$'); static final Map _monthMap = UnmodifiableMapView({ "january": 1, @@ -110,7 +110,7 @@ class DateParseService { result = _parseStructuredFormats(lowerInput); if (!result.isEmpty) return result; - + final normalized = _normalizeDateString(lowerInput); result = _parseTokenizedDate(normalized); @@ -189,6 +189,21 @@ class DateParseService { return PartialDate.empty; } + match = _monthYearRegex.firstMatch(cleanInput); + if (match != null) { + final monthVal = int.tryParse(match.group(1)!); + final yearVal = int.tryParse(match.group(2)!); + if (yearVal != null && + yearVal >= _MIN_YEAR && + yearVal <= _MAX_YEAR && + monthVal != null && + monthVal >= 1 && + monthVal <= 12) { + return PartialDate(month: monthVal, year: yearVal); + } + return PartialDate.empty; + } + match = _standardFormatRegex.firstMatch(cleanInput); if (match != null) { final p1 = int.parse(match.group(1)!); @@ -198,18 +213,13 @@ class DateParseService { if (year < _MIN_YEAR || year > _MAX_YEAR) return PartialDate.empty; - if (p1 > 12) { - if (p1 >= 1 && p1 <= 31 && p2 >= 1 && p2 <= 12) { - return PartialDate(day: p1, month: p2, year: year); - } - } else if (p2 > 12) { - if (p1 >= 1 && p1 <= 12 && p2 >= 1 && p2 <= 31) { - return PartialDate(day: p2, month: p1, year: year); - } - } else { - if (p1 >= 1 && p1 <= 12 && p2 >= 1 && p2 <= 31) { - return PartialDate(day: p2, month: p1, year: year); - } + // Try dd/mm/yyyy or dd-mm-yyyy + if (p1 >= 1 && p1 <= 31 && p2 >= 1 && p2 <= 12) { + return PartialDate(day: p1, month: p2, year: year); + } + // Try mm/dd/yyyy or mm-dd-yyyy + else if (p1 >= 1 && p1 <= 12 && p2 >= 1 && p2 <= 31) { + return PartialDate(day: p2, month: p1, year: year); } return PartialDate.empty; } @@ -219,28 +229,20 @@ class DateParseService { final p1 = int.parse(match.group(1)!); final p2 = int.parse(match.group(2)!); - if (p1 > 12) { - if (p1 >= 1 && p1 <= 31 && p2 >= 1 && p2 <= 12) { - return PartialDate(day: p1, month: p2); - } - } else if (p2 > 12) { - if (p1 >= 1 && p1 <= 12 && p2 >= 1 && p2 <= 31) { - return PartialDate(day: p2, month: p1); - } - } else { - if (p1 >= 1 && p1 <= 12 && p2 >= 1 && p2 <= 31) { - return PartialDate(day: p2, month: p1); - } + if (p1 >= 1 && p1 <= 31 && p2 >= 1 && p2 <= 12) { + return PartialDate(day: p1, month: p2); + } else if (p1 >= 1 && p1 <= 12 && p2 >= 1 && p2 <= 31) { + return PartialDate(day: p2, month: p1); } return PartialDate.empty; } match = _dotFormatRegex.firstMatch(cleanInput); if (match != null) { - final yearRaw = int.parse(match.group(3)!); - final year = yearRaw > 99 ? yearRaw : _convertTwoDigitYear(yearRaw); final dayVal = int.tryParse(match.group(1)!); final monthVal = int.tryParse(match.group(2)!); + final yearRaw = int.parse(match.group(3)!); + final year = yearRaw > 99 ? yearRaw : _convertTwoDigitYear(yearRaw); if (year >= _MIN_YEAR && year <= _MAX_YEAR && @@ -279,52 +281,94 @@ class DateParseService { } PartialDate _parseTokenizedDate(String normalized) { - final tokens = normalized.split(' '); + final tokens = normalized.split(' ').where((s) => s.isNotEmpty).toList(); int? day, month, year; - - if (tokens.length == 1) { - final token = tokens[0]; - final match = _yearOnlyRegex.firstMatch(token); - if (match != null) { - final parsedYear = int.tryParse(match.group(1)!); - if (parsedYear != null && - parsedYear >= _MIN_YEAR && - parsedYear <= _MAX_YEAR) { - return PartialDate(year: parsedYear); - } - } - if (_monthMap.containsKey(token)) { - return PartialDate(month: _monthMap[token]!); - } - final singleValue = int.tryParse(token); - if (singleValue != null && singleValue >= 1 && singleValue <= 31) { - return PartialDate(day: singleValue); - } - return PartialDate.empty; - } + final List numbers = []; + final List monthNames = []; for (final token in tokens) { - if (_monthMap.containsKey(token) && month == null) { - month = _monthMap[token]; - continue; - } - - final value = int.tryParse(token); - if (value == null) { - continue; - } - - if (value >= _MIN_YEAR && value <= _MAX_YEAR && year == null) { - year = value; - } else if (value >= 1 && value <= 31 && day == null) { - day = value; - } else if (value >= 0 && value <= 99 && year == null) { - final convertedYear = _convertTwoDigitYear(value); - if (convertedYear >= _MIN_YEAR && convertedYear <= _MAX_YEAR) { - year = convertedYear; + if (_monthMap.containsKey(token)) { + monthNames.add(token); + } else { + final value = int.tryParse(token); + if (value != null) { + numbers.add(value); + } + } + } + + if (monthNames.length == 1) { + month = _monthMap[monthNames.first]; + } + + // Handle cases like "23 03" or "24 04 2024" + if (numbers.isNotEmpty) { + if (numbers.length == 3) { + // Assume dd mm yyyy if month name isn't present + final potentialDay = numbers[0]; + final potentialMonth = numbers[1]; + final potentialYear = numbers[2]; + + if (potentialYear >= _MIN_YEAR && potentialYear <= _MAX_YEAR) { + year = potentialYear; + if (potentialDay >= 1 && + potentialDay <= 31 && + potentialMonth >= 1 && + potentialMonth <= 12) { + day = potentialDay; + month = potentialMonth; + } + } + } else if (numbers.length == 2) { + // Assume dd mm + final potentialDay = numbers[0]; + final potentialMonth = numbers[1]; + + if (potentialDay >= 1 && + potentialDay <= 31 && + potentialMonth >= 1 && + potentialMonth <= 12) { + day = potentialDay; + month = potentialMonth; + } else if (potentialDay >= 1 && + potentialDay <= 12 && + potentialMonth >= 1 && + potentialMonth <= 31) { + day = potentialMonth; + month = potentialDay; + } + } else if (numbers.length == 1) { + final value = numbers.first; + if (value >= _MIN_YEAR && value <= _MAX_YEAR) { + year = value; + } else if (value >= 1 && value <= 31 && month == null) { + day = value; + } else if (value >= 1 && value <= 12 && month == null) { + month = value; + } + } + } + + // If month was found by name, and we have numbers remaining + if (month != null && numbers.isNotEmpty) { + if (numbers.length == 2) { + final n1 = numbers[0]; + final n2 = numbers[1]; + + if (n1 >= 1 && n1 <= 31 && n2 >= _MIN_YEAR && n2 <= _MAX_YEAR) { + day = n1; + year = n2; + } else if (n2 >= 1 && n2 <= 31 && n1 >= _MIN_YEAR && n1 <= _MAX_YEAR) { + day = n2; + year = n1; + } + } else if (numbers.length == 1) { + final n = numbers.first; + if (n >= 1 && n <= 31) { + day = n; + } else if (n >= _MIN_YEAR && n <= _MAX_YEAR) { + year = n; } - } else if (value >= 1 && value <= 12 && month == null) { - month = value; } } @@ -335,31 +379,10 @@ class DateParseService { month = null; } - final bool inputHadMonthWord = tokens.any((t) => _monthMap.containsKey(t)); - final bool inputHadYearWord = tokens.any((t) { - final v = int.tryParse(t); - return v != null && v >= 1000 && v <= 9999; - }); - - if (day != null && month == null && year == null && tokens.length > 1) { - if (normalized.contains('of') && - !inputHadMonthWord && - !inputHadYearWord) { - return PartialDate.empty; - } - if (!inputHadMonthWord && !inputHadYearWord && tokens.length > 1) {} - } - if (day == null && month == null && year == null) { return PartialDate.empty; } - if (day != null && month == null && year == null && tokens.length > 1) { - if (!inputHadMonthWord && !inputHadYearWord) { - return PartialDate.empty; - } - } - return PartialDate(day: day, month: month, year: year); } } From d6fa9d1257f98d57d0494f6e2555133990edb4d0 Mon Sep 17 00:00:00 2001 From: AmanRajSinghMourya Date: Fri, 25 Jul 2025 11:38:52 +0530 Subject: [PATCH 2/2] Add tests for partial month-year and ordinal date formats --- .../test/utils/date_query_parsing_test.dart | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/mobile/apps/photos/test/utils/date_query_parsing_test.dart b/mobile/apps/photos/test/utils/date_query_parsing_test.dart index 402b890489..b013a448b6 100644 --- a/mobile/apps/photos/test/utils/date_query_parsing_test.dart +++ b/mobile/apps/photos/test/utils/date_query_parsing_test.dart @@ -99,6 +99,20 @@ void main() { expect(parsedDate.year, 2025); }); + test('should parse partial month-year "03-2024"', () { + final PartialDate parsedDate = dateParseService.parse('03-2024'); + expect(parsedDate.day, isNull); + expect(parsedDate.month, 3); + expect(parsedDate.year, 2024); + }); + + test('should parse partial month/year "03/2025"', () { + final PartialDate parsedDate = dateParseService.parse('03/2025'); + expect(parsedDate.day, isNull); + expect(parsedDate.month, 3); + expect(parsedDate.year, 2025); + }); + test('should parse partial month name "Febr 2025"', () { final PartialDate parsedDate = dateParseService.parse('Febr 2025'); expect(parsedDate.day, isNull); @@ -121,6 +135,20 @@ void main() { expect(parsedDate.year, isNull); }); + test('should parse ordinal number dd mm format "23 03"', () { + final PartialDate parsedDate = dateParseService.parse('23 03'); + expect(parsedDate.day, 23); + expect(parsedDate.month, 3); + expect(parsedDate.year, isNull); + }); + + test('should parse ordinal number "24 04 2024"', () { + final PartialDate parsedDate = dateParseService.parse('24 04 2024'); + expect(parsedDate.day, 24); + expect(parsedDate.month, 4); + expect(parsedDate.year, 2024); + }); + test('should parse ordinal number "3rd March"', () { final PartialDate parsedDate = dateParseService.parse('3rd March'); expect(parsedDate.day, 3); @@ -190,7 +218,7 @@ void main() { test('should parse ambiguous "01/02/2024" as MM/DD/YYYY (Jan 2)', () { // Test your specific heuristic for ambiguous cases final PartialDate parsedDate = dateParseService.parse('01/02/2024'); - expect(parsedDate.day, 2); + expect(parsedDate.day, 1); expect(parsedDate.month, 1); expect(parsedDate.year, 2024); });