[mobile][photos] Fix date parsing (#6637)

## Description
Support more date format.

## Tests
Add more test cases to the test file.
This commit is contained in:
Neeraj
2025-07-25 15:06:06 +05:30
committed by GitHub
2 changed files with 143 additions and 92 deletions

View File

@@ -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<String, int> _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<int> numbers = [];
final List<String> 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);
}
}

View File

@@ -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);
});