From 55c9f80b23030e6c644437c0db963b78014b8b37 Mon Sep 17 00:00:00 2001 From: Jama Mohamed Date: Fri, 30 Dec 2022 16:12:41 +0300 Subject: [PATCH 1/2] [ISSUE-172] Implement the new datetime format functionality --- lib/src/default_display.dart | 1 + lib/src/display.dart | 57 ++++++----- lib/src/getter.dart | 1 - lib/src/parser.dart | 14 ++- test/src/display_test.dart | 189 ++++++++++++++++++++++++++++++++++- test/src/parser_test.dart | 61 +++++------ 6 files changed, 257 insertions(+), 66 deletions(-) diff --git a/lib/src/default_display.dart b/lib/src/default_display.dart index 589203f..080df94 100644 --- a/lib/src/default_display.dart +++ b/lib/src/default_display.dart @@ -3,6 +3,7 @@ import 'package:intl/intl.dart'; class DefaultDisplay { // todo add examples as docs comments here // check of doc comments from here will be shown or is it from the jiffy file + // todo also test in other locales e.g E(locale).format String E(DateTime dateTime) => DateFormat.E().format(dateTime); String EEEE(DateTime dateTime) => DateFormat.EEEE().format(dateTime); diff --git a/lib/src/display.dart b/lib/src/display.dart index 222e363..912d1f0 100644 --- a/lib/src/display.dart +++ b/lib/src/display.dart @@ -2,6 +2,7 @@ import 'package:intl/intl.dart'; import 'getter.dart'; import 'enums/units.dart'; +import 'locale/locale.dart'; import 'manipulator.dart'; import 'utils/exception.dart'; @@ -13,13 +14,20 @@ class Display { String formatToISO8601(DateTime dateTime) => dateTime.toIso8601String(); - String format(DateTime dateTime, String pattern, String ordinal) { - final escapedPattern = _replaceEscapePattern(pattern); - final newPattern = _replaceOrdinalDatePattern(escapedPattern, ordinal); + String format(DateTime dateTime, String pattern, Locale locale) { + if (pattern.trim().isEmpty) { + throw JiffyException('The provided pattern for datetime `$dateTime` ' + 'cannot be blank'); + } try { + final escapedPattern = _replaceEscapePattern(pattern); + final ordinal = locale.ordinal(getter.date(dateTime)); + final newPattern = _replaceOrdinalDatePattern(escapedPattern, ordinal); return DateFormat(newPattern).format(dateTime); - } on Exception catch (e) { - throw JiffyException(e.toString()); + } catch (error, stackTrace) { + throw JiffyException('The pattern `$pattern` might be invalid: \n' + 'Error: $error' + 'Stack Trace: $stackTrace'); } } @@ -72,25 +80,30 @@ class Display { } String _replaceEscapePattern(String input) { - return input.replaceAll('[', '\'').replaceAll(']', '\''); + return input + .replaceAll('\'', '\'\'') + .replaceAll('[', '\'') + .replaceAll(']', '\''); } - String _replaceOrdinalDatePattern(String input, String suffix) { - final regex = _matchesOrdinalDatePattern().allMatches(input); + String _replaceOrdinalDatePattern(String input, String ordinal) { + var matches = _matchesOrdinalDatePattern(input); var pattern = input; - regex.forEach((match) { - if (match.group(1) == 'do') { - pattern = input.replaceRange( - match.start, match.end, 'd${suffix.isNotEmpty ? "'$suffix'" : ''}'); - } - }); + + while (matches.isNotEmpty) { + final match = matches.first; + pattern = pattern.replaceRange( + match.start, match.end, 'd${ordinal.isNotEmpty ? "'$ordinal'" : ''}'); + matches = _matchesOrdinalDatePattern(pattern); + } return pattern; } - // todo understand what this regex pattern does - Pattern _matchesOrdinalDatePattern() { - return RegExp( - '''(?[^"'\\s]\\w*)|(?:["][^"]+?["])|(?:['][^']+?['])'''); + List _matchesOrdinalDatePattern(String input) { + return RegExp('''\'[^\']*\'|(do)''') + .allMatches(input) + .where((match) => match.group(1) == 'do') + .toList(); } num _monthDiff(DateTime firstDateTime, DateTime secondDateTime) { @@ -127,13 +140,7 @@ class Display { return -(monthDiff + offset); } - int _absFloor(num number) { - if (number < 0) { - return number.ceil(); - } else { - return number.floor(); - } - } + int _absFloor(num number) => number < 0 ? number.ceil() : number.floor(); DateTime _addMonths(DateTime dateTime, int months) { return manipulator.add(dateTime, 0, 0, 0, 0, 0, 0, 0, months, 0); diff --git a/lib/src/getter.dart b/lib/src/getter.dart index 556a7d9..597c957 100644 --- a/lib/src/getter.dart +++ b/lib/src/getter.dart @@ -56,7 +56,6 @@ class Getter { return int.parse(DateFormat('Q').format(dateTime)); } - // todo see if you can use a formatter class int dayOfYear(DateTime dateTime) { return int.parse(DateFormat('D').format(dateTime)); } diff --git a/lib/src/parser.dart b/lib/src/parser.dart index 9c7edb3..f2e787a 100644 --- a/lib/src/parser.dart +++ b/lib/src/parser.dart @@ -2,6 +2,7 @@ import 'package:intl/intl.dart'; import 'enums/units.dart'; import 'getter.dart'; +import 'locale/locale.dart'; import 'utils/exception.dart'; class Parser { @@ -9,12 +10,13 @@ class Parser { Parser(this.getter); - DateTime fromString(String input, String? pattern, List ordinals) { + DateTime fromString(String input, String? pattern, Locale locale) { if (pattern != null) { if (pattern.trim().isEmpty) { throw JiffyException('The provided pattern for `$input` cannot ' - 'be empty'); + 'be blank'); } + final ordinals = locale.ordinals()!; return parseString( _replaceParseInput(input, ordinals), _replacePatternInput(pattern)); } @@ -81,14 +83,16 @@ class Parser { return input .replaceFirst(' pm', ' PM') .replaceFirst(' am', ' AM') - .replaceFirst(_matchesOrdinalDates(ordinals), ''); + .replaceFirst(_matchesOrdinalDates(input, ordinals), ''); } // todo fix this regex replacement of the ordinals when there is a space between // example `12 th` // todo test if empty ordinals are provided - Pattern _matchesOrdinalDates(List ordinals) { - return RegExp(r'(?<=[0-9])(?:' + ordinals.join('|') + ')'); + String _matchesOrdinalDates(String input, List ordinals) { + final matches = + RegExp(r'\d+\s*(' + ordinals.join('|') + ')').allMatches(input); + return matches.isNotEmpty ? matches.first.group(1) ?? '' : ''; } bool _matchesHyphenStringDateTime(String input) { diff --git a/test/src/display_test.dart b/test/src/display_test.dart index 232d81d..0988129 100644 --- a/test/src/display_test.dart +++ b/test/src/display_test.dart @@ -1,8 +1,10 @@ import 'package:jiffy/src/display.dart'; +import 'package:jiffy/src/enums/startOfWeek.dart'; import 'package:jiffy/src/enums/units.dart'; import 'package:jiffy/src/getter.dart'; +import 'package:jiffy/src/locale/enLocale.dart'; import 'package:jiffy/src/manipulator.dart'; -import 'package:test/scaffolding.dart'; +import 'package:jiffy/src/utils/exception.dart'; import 'package:test/test.dart'; void main() { @@ -11,6 +13,96 @@ void main() { final underTest = Display(getter, manipulator); + test('Should successfully format datetime is iso when pattern not provided', + () { + // Setup + final dateTime = DateTime(1997, 9, 23, 12, 11, 22, 123, 456); + + final expectedFormat = '1997-09-23T12:11:22.123456'; + + // Execute + final actualFormat = underTest.formatToISO8601(dateTime); + + // Verify + expect(actualFormat, expectedFormat); + }); + + group('Test format with pattern', () { + for (var testData in formatWithPatternDateTimeTestData()) { + test('Should successfully format datetime when pattern is provided', () { + // Setup + final locale = EnLocale(StartOfWeek.MONDAY); + + // Execute + final actualFormat = + underTest.format(testData['dateTime'], testData['pattern'], locale); + + // Verify + expect(actualFormat, testData['expectedFormat']); + }); + } + + for (var testData in formatWithEscapedPatternDateTimeTestData()) { + test('Should successfully format datetime with escaped pattern', () { + // Setup + final locale = EnLocale(StartOfWeek.MONDAY); + + // Execute + final actualFormat = + underTest.format(testData['dateTime'], testData['pattern'], locale); + + // Verify + expect(actualFormat, testData['expectedFormat']); + }); + } + + for (var testData in formatWithOrdinalPatternDateTimeTestData()) { + test('Should successfully format datetime with ordinal pattern', () { + // Setup + final locale = EnLocale(StartOfWeek.MONDAY); + + // Execute + final actualFormat = + underTest.format(testData['dateTime'], testData['pattern'], locale); + + // Verify + expect(actualFormat, testData['expectedFormat']); + }); + } + + test('Should throw JiffyException if provided pattern in blank', () { + // Setup + final dateTime = DateTime(1997, 9, 23, 12, 11, 22, 123, 456); + final pattern = ''; + final locale = EnLocale(StartOfWeek.MONDAY); + + final expectedExceptionMessage = 'The provided pattern for datetime ' + '`$dateTime` cannot be blank'; + + // Execute and Verify + expect( + () => underTest.format(dateTime, pattern, locale), + throwsA(isA().having((e) => e.message, 'message', + contains(expectedExceptionMessage)))); + }); + + test('Should throw JiffyException if provided pattern is invalid', () { + // Setup + final dateTime = DateTime(1997, 9, 23, 12, 11, 22, 123, 456); + final pattern = 'invalid-pattern'; + final locale = EnLocale(StartOfWeek.MONDAY); + + final expectedExceptionMessage = + 'The pattern `$pattern` might be invalid'; + + // Execute and Verify + expect( + () => underTest.format(dateTime, pattern, locale), + throwsA(isA().having((e) => e.message, 'message', + contains(expectedExceptionMessage)))); + }); + }); + group('Test diff', () { for (var testData in diffDateTimeTestData()) { test('Should successfully get difference between two datetime', () { @@ -45,6 +137,101 @@ void main() { }); } +List> formatWithPatternDateTimeTestData() { + return [ + { + 'dateTime': DateTime(1997, 9, 23, 12, 11, 22, 123, 456), + 'pattern': 'yyyy MMM dd hh:mm:ss', + 'expectedFormat': '1997 Sep 23 12:11:22' + }, + { + 'dateTime': DateTime(1997, 9, 23, 12, 11, 22, 123, 456), + 'pattern': 'yyyy MMM, dd hh:mm a', + 'expectedFormat': '1997 Sep, 23 12:11 PM' + }, + { + 'dateTime': DateTime(1997, 9, 23, 11, 11, 22, 123, 456), + 'pattern': 'yyyy MMM dd hh:mm a', + 'expectedFormat': '1997 Sep 23 11:11 AM' + }, + { + 'dateTime': DateTime(1997, 9, 23, 11, 11, 22, 123, 456), + 'pattern': 'do MMMM', + 'expectedFormat': '23rd September' + }, + { + 'dateTime': DateTime(1997, 9, 23, 11, 11, 22, 123, 456), + 'pattern': 'EEEE', + 'expectedFormat': 'Tuesday' + } + ]; +} + +List> formatWithEscapedPatternDateTimeTestData() { + return [ + { + 'dateTime': DateTime(1969, 7, 20, 20, 18, 04), + 'pattern': '[The moon landing was on] do MMMM, yyyy', + 'expectedFormat': 'The moon landing was on 20th July, 1969' + }, + { + 'dateTime': DateTime(1997, 9, 23, 12, 11, 22, 123, 456), + 'pattern': '[Today\'s date is] do [in the month of] MMMM', + 'expectedFormat': 'Today\'s date is 23rd in the month of September' + }, + { + 'dateTime': DateTime(1997, 9, 23, 12, 11, 22, 123, 456), + 'pattern': '[It\'s] hh [o\'clock]', + 'expectedFormat': 'It\'s 12 o\'clock' + }, + { + 'dateTime': DateTime(1997, 9, 23, 12, 11, 22, 123, 456), + 'pattern': "[It's] hh [o'clock]", + 'expectedFormat': "It's 12 o'clock" + } + ]; +} + +List> formatWithOrdinalPatternDateTimeTestData() { + return [ + { + 'dateTime': DateTime(2022, 1, 1), + 'pattern': 'do', + 'expectedFormat': '1st' + }, + { + 'dateTime': DateTime(2022, 1, 12), + 'pattern': 'do', + 'expectedFormat': '12th' + }, + { + 'dateTime': DateTime(2022, 1, 21), + 'pattern': 'do', + 'expectedFormat': '21st' + }, + { + 'dateTime': DateTime(2022, 1, 2), + 'pattern': 'do', + 'expectedFormat': '2nd' + }, + { + 'dateTime': DateTime(2022, 1, 3), + 'pattern': 'do', + 'expectedFormat': '3rd' + }, + { + 'dateTime': DateTime(2022, 1, 4), + 'pattern': 'do', + 'expectedFormat': '4th' + }, + { + 'dateTime': DateTime(2022, 1, 11), + 'pattern': 'do', + 'expectedFormat': '11th' + } + ]; +} + List> diffDateTimeTestData() { return [ { diff --git a/test/src/parser_test.dart b/test/src/parser_test.dart index cd6644c..7b0e1f9 100644 --- a/test/src/parser_test.dart +++ b/test/src/parser_test.dart @@ -1,5 +1,7 @@ +import 'package:jiffy/src/enums/startOfWeek.dart'; import 'package:jiffy/src/enums/units.dart'; import 'package:jiffy/src/getter.dart'; +import 'package:jiffy/src/locale/enLocale.dart'; import 'package:jiffy/src/parser.dart'; import 'package:jiffy/src/utils/exception.dart'; import 'package:test/test.dart'; @@ -8,17 +10,16 @@ void main() { final getter = Getter(); final underTest = Parser(getter); + final locale = EnLocale(StartOfWeek.MONDAY); + group('Test parsing datetime from string', () { test('Should successfully parse datetime without a pattern', () {}); for (var testData in fromStringWithPatternTestData()) { test('Should successfully parse datetime from pattern', () { - // Setup - final ordinals = ['st', 'nd', 'rd', 'th']; - // Execute final actualDateTime = underTest.fromString( - testData['input'], testData['pattern'], ordinals); + testData['input'], testData['pattern'], locale); // Verify expect(actualDateTime, testData['expected']); @@ -27,12 +28,9 @@ void main() { for (var testData in fromStringWithPatternAMAndPMTestData()) { test('Should successfully parse datetime if pattern contains am pm', () { - // Setup - final ordinals = ['st', 'nd', 'rd', 'th']; - // Execute final actualDateTime = underTest.fromString( - testData['input'], testData['pattern'], ordinals); + testData['input'], testData['pattern'], locale); // Verify expect(actualDateTime, testData['expected']); @@ -43,14 +41,13 @@ void main() { // Setup final input = '2022-12-25'; final pattern = ''; - final ordinals = ['st', 'nd', 'rd', 'th']; final expectedExceptionMessage = 'The provided pattern for `$input` ' - 'cannot be empty'; + 'cannot be blank'; // Execute and Verify expect( - () => underTest.fromString(input, pattern, ordinals), + () => underTest.fromString(input, pattern, locale), throwsA(isA().having((e) => e.message, 'message', contains(expectedExceptionMessage)))); }); @@ -59,14 +56,13 @@ void main() { // Setup final input = '2022-12-25'; final pattern = 'invalid-pattern'; - final ordinals = ['st', 'nd', 'rd', 'th']; final expectedExceptionMessage = 'JiffyException: Could not parse input ' '`$input`, failed with the following error:'; // Execute and Verify expect( - () => underTest.fromString(input, pattern, ordinals), + () => underTest.fromString(input, pattern, locale), throwsA(isA().having((e) => e.message, 'message', contains(expectedExceptionMessage)))); }); @@ -75,11 +71,10 @@ void main() { test('Should successfully parse basic datetime format', () { // Setup final pattern = null; - final ordinals = ['st', 'nd', 'rd', 'th']; // Execute final actualDateTime = - underTest.fromString(testData['input'], pattern, ordinals); + underTest.fromString(testData['input'], pattern, locale); // Verify expect(actualDateTime, testData['expected']); @@ -90,11 +85,10 @@ void main() { test('Should successfully parse DateTime and ISO datetime format', () { // Setup final pattern = null; - final ordinals = ['st', 'nd', 'rd', 'th']; // Execute final actualDateTime = - underTest.fromString(testData['input'], pattern, ordinals); + underTest.fromString(testData['input'], pattern, locale); // Verify expect(actualDateTime, testData['expected']); @@ -105,14 +99,13 @@ void main() { // Setup final input = 'invalid-input-date-time'; final pattern = null; - final ordinals = ['st', 'nd', 'rd', 'th']; final expectedExceptionMessage = 'Could not read date time `$input`, ' 'try using a pattern, e.g. Jiffy("12, Oct", "dd, MMM")'; // Execute and Verify expect( - () => underTest.fromString(input, pattern, ordinals), + () => underTest.fromString(input, pattern, locale), throwsA(isA().having((e) => e.message, 'message', contains(expectedExceptionMessage)))); }); @@ -209,21 +202,21 @@ List> fromStringWithPatternAMAndPMTestData() { 'pattern': 'yyyy MMM do h:mm a', 'expected': DateTime(1997, 9, 23, 15, 14) }, - { - 'input': '1997 Sep 23th 3 pm', - 'pattern': 'yyyy MMM do h a', - 'expected': DateTime(1997, 9, 23, 15, 0) - }, - { - 'input': '1997 Sep 23th 3:14 am', - 'pattern': 'yyyy MMM do h:mm a', - 'expected': DateTime(1997, 9, 23, 3, 14) - }, - { - 'input': '1997 Sep 23th 3 am', - 'pattern': 'yyyy MMM do h a', - 'expected': DateTime(1997, 9, 23, 3, 0) - }, + // { + // 'input': '1997 Sep 23th 3 pm', + // 'pattern': 'yyyy MMM do h a', + // 'expected': DateTime(1997, 9, 23, 15, 0) + // }, + // { + // 'input': '1997 Sep 23th 3:14 am', + // 'pattern': 'yyyy MMM do h:mm a', + // 'expected': DateTime(1997, 9, 23, 3, 14) + // }, + // { + // 'input': '1997 Sep 23th 3 am', + // 'pattern': 'yyyy MMM do h a', + // 'expected': DateTime(1997, 9, 23, 3, 0) + // }, ]; } From d67e586de406d75961488fa11ab004413b655270 Mon Sep 17 00:00:00 2001 From: Jama Mohamed Date: Fri, 30 Dec 2022 16:16:51 +0300 Subject: [PATCH 2/2] [ISSUE-172] Update tests --- test/src/parser_test.dart | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/test/src/parser_test.dart b/test/src/parser_test.dart index 7b0e1f9..cac7c41 100644 --- a/test/src/parser_test.dart +++ b/test/src/parser_test.dart @@ -202,21 +202,21 @@ List> fromStringWithPatternAMAndPMTestData() { 'pattern': 'yyyy MMM do h:mm a', 'expected': DateTime(1997, 9, 23, 15, 14) }, - // { - // 'input': '1997 Sep 23th 3 pm', - // 'pattern': 'yyyy MMM do h a', - // 'expected': DateTime(1997, 9, 23, 15, 0) - // }, - // { - // 'input': '1997 Sep 23th 3:14 am', - // 'pattern': 'yyyy MMM do h:mm a', - // 'expected': DateTime(1997, 9, 23, 3, 14) - // }, - // { - // 'input': '1997 Sep 23th 3 am', - // 'pattern': 'yyyy MMM do h a', - // 'expected': DateTime(1997, 9, 23, 3, 0) - // }, + { + 'input': '1997 Sep 23th 3 pm', + 'pattern': 'yyyy MMM do h a', + 'expected': DateTime(1997, 9, 23, 15, 0) + }, + { + 'input': '1997 Sep 23th 3:14 am', + 'pattern': 'yyyy MMM do h:mm a', + 'expected': DateTime(1997, 9, 23, 3, 14) + }, + { + 'input': '1997 Sep 23th 3 am', + 'pattern': 'yyyy MMM do h a', + 'expected': DateTime(1997, 9, 23, 3, 0) + }, ]; }