From 6748a2440cc6b3f2265d085fddd981e3597e402f Mon Sep 17 00:00:00 2001 From: Joey Boerwinkel Date: Wed, 3 Jul 2024 10:30:54 +0200 Subject: [PATCH] feat(flutter_availability_data_interface): add implementation for apply method on availability templates --- .../lib/src/models/templates.dart | 63 ++++++- .../test/models_templates_test.dart | 163 ++++++++++++++++++ 2 files changed, 220 insertions(+), 6 deletions(-) diff --git a/packages/flutter_availability_data_interface/lib/src/models/templates.dart b/packages/flutter_availability_data_interface/lib/src/models/templates.dart index ee22306..a11efdf 100644 --- a/packages/flutter_availability_data_interface/lib/src/models/templates.dart +++ b/packages/flutter_availability_data_interface/lib/src/models/templates.dart @@ -23,6 +23,8 @@ class AvailabilityTemplateModel { this.id, }); + /// Automatically parses [templateData] based on the dynamic data map and + /// template type. factory AvailabilityTemplateModel.fromType({ required String userId, required String name, @@ -63,7 +65,12 @@ class AvailabilityTemplateModel { /// The specific data for this template final TemplateData templateData; + /// returns the [templateData] in its serialized form Map get rawTemplateData => templateData.toMap(); + + /// applies the template to a range of dates + List apply(DateTime start, DateTime end) => + templateData.apply(userId: userId, start: start, end: end); } /// Used as the key for defining week-based templates @@ -114,6 +121,7 @@ abstract interface class TemplateData { /// Applies the current template to all days found between [start] and [end], /// inclusive List apply({ + required String userId, required DateTime start, required DateTime end, }); @@ -147,7 +155,7 @@ class WeekTemplateData implements TemplateData { if (thursday != null) WeekDay.thursday: thursday, if (friday != null) WeekDay.friday: friday, if (saturday != null) WeekDay.saturday: saturday, - if (sunday != null) WeekDay.monday: sunday, + if (sunday != null) WeekDay.sunday: sunday, }, ); @@ -188,11 +196,21 @@ class WeekTemplateData implements TemplateData { @override List apply({ + required String userId, required DateTime start, required DateTime end, }) { - // TODO(Joey): Implement the apply method - throw UnimplementedError(); + var dates = _getDatesBetween(start, end); + + return [ + for (var date in dates) + if (data.containsKey(WeekDay.fromDateTime(date))) + ...data[WeekDay.fromDateTime(date)]!.apply( + start: date, + end: date, + userId: userId, + ), + ]; } } @@ -245,15 +263,34 @@ class DayTemplateData implements TemplateData { @override List apply({ + required String userId, required DateTime start, required DateTime end, }) { - // TODO(Joey): Implement the apply method - throw UnimplementedError(); + var dates = _getDatesBetween(start, end); + + return [ + for (var date in dates) ...[ + AvailabilityModel( + userId: userId, + startDate: date.mergeTime(startTime), + endDate: date.mergeTime(endTime), + breaks: [ + for (var templateBreak in breaks) ...[ + AvailabilityBreakModel( + startTime: date.mergeTime(templateBreak.startTime), + endTime: date.mergeTime(templateBreak.endTime), + duration: templateBreak.isTight ? null : templateBreak.duration, + ), + ], + ], + ), + ], + ]; } @override - Map toMap() => { + Map toMap() => { "startTime": startTime.toIso8601String(), "endTime": endTime.toIso8601String(), "breaks": [ @@ -263,3 +300,17 @@ class DayTemplateData implements TemplateData { ], }; } + +List _getDatesBetween(DateTime startDate, DateTime endDate) { + var diff = endDate.difference(startDate).inDays; + return [ + for (var i = 0; i <= diff; i++) ...[ + DateTime(startDate.year, startDate.month, startDate.day + i), + ], + ]; +} + +extension _MergeTime on DateTime { + DateTime mergeTime(DateTime time) => + DateTime(year, month, day, time.hour, time.minute); +} diff --git a/packages/flutter_availability_data_interface/test/models_templates_test.dart b/packages/flutter_availability_data_interface/test/models_templates_test.dart index d399f29..5e3b44c 100644 --- a/packages/flutter_availability_data_interface/test/models_templates_test.dart +++ b/packages/flutter_availability_data_interface/test/models_templates_test.dart @@ -3,6 +3,126 @@ import "package:flutter_test/flutter_test.dart"; void main() { group("AvailabilityTemplate", () { + group("Apply", () { + test("applying a week template should generate correct availability", () { + DateTime asTime(int hours, [int minutes = 0]) => + DateTime(1, 1, 1, hours, minutes); + + AvailabilityBreakModel createBreak(int startHour) => + AvailabilityBreakModel( + startTime: asTime(startHour), + endTime: asTime(startHour + 1), + duration: const Duration(minutes: 30), + ); + + DayTemplateData createTemplate(int startHour) => DayTemplateData( + startTime: asTime(startHour), + endTime: asTime(startHour + 7), + breaks: [createBreak(startHour + 3)], + ); + + var monday = createTemplate(1); + var tuesday = createTemplate(2); + var wednesday = createTemplate(3); + var thursday = createTemplate(4); + var friday = createTemplate(5); + var saturday = createTemplate(6); + var sunday = createTemplate(7); + + var sut = AvailabilityTemplateModel( + userId: "", + name: "", + color: 0, + templateType: AvailabilityTemplateType.week, + templateData: WeekTemplateData.forDays( + monday: monday, + tuesday: tuesday, + wednesday: wednesday, + thursday: thursday, + friday: friday, + saturday: saturday, + sunday: sunday, + ), + ); + + var startOfRangeToApply = DateTime(1999, 1, 1); + var endOfRangeToApply = DateTime(2024, 12, 31); + + var availabilities = sut.apply(startOfRangeToApply, endOfRangeToApply); + + AvailabilityModel findAvailabilityByDate(DateTime date) => + availabilities + .where((value) => value.startDate.date == date.date) + .first; + + var firstDay = startOfRangeToApply; + expect(findAvailabilityByDate(firstDay), isA()); + + var lastDay = endOfRangeToApply; + expect(findAvailabilityByDate(lastDay), isA()); + + var leapDayMillenial = DateTime(2000, 2, 29); + expect( + findAvailabilityByDate(leapDayMillenial).matchesTemplate(tuesday), + isTrue, + ); + + var savingsTimeSwitch = DateTime(2020, 10, 25); + expect( + findAvailabilityByDate(savingsTimeSwitch).matchesTemplate(sunday), + isTrue, + ); + + var dayAfterSavingsTime = DateTime(2020, 10, 26); + expect( + findAvailabilityByDate(dayAfterSavingsTime).matchesTemplate(monday), + isTrue, + ); + + var aMondayInJuly = DateTime(2024, 7, 8); + expect( + findAvailabilityByDate(aMondayInJuly).matchesTemplate(monday), + isTrue, + ); + + var aTuesdayInJuly = DateTime(2024, 7, 9); + expect( + findAvailabilityByDate(aTuesdayInJuly).matchesTemplate(tuesday), + isTrue, + ); + + var aWednesdayInJuly = DateTime(2024, 7, 10); + expect( + findAvailabilityByDate(aWednesdayInJuly).matchesTemplate(wednesday), + isTrue, + ); + + var aThursdayInJuly = DateTime(2024, 7, 11); + expect( + findAvailabilityByDate(aThursdayInJuly).matchesTemplate(thursday), + isTrue, + ); + + var aFridayInJuly = DateTime(2024, 7, 12); + expect( + findAvailabilityByDate(aFridayInJuly).matchesTemplate(friday), + isTrue, + ); + + var aSaturdayInJuly = DateTime(2024, 7, 13); + expect( + findAvailabilityByDate(aSaturdayInJuly).matchesTemplate(saturday), + isTrue, + ); + + var aSundayInJuly = DateTime(2024, 7, 14); + expect( + findAvailabilityByDate(aSundayInJuly).matchesTemplate(sunday), + isTrue, + ); + }); + }); + group("Serialization", () { test("week template should serialize and deserialize correctly", () { var baseDate = DateTime(1994, 10, 18, 10, 0); @@ -62,3 +182,46 @@ void main() { }); }); } + +extension on AvailabilityModel { + bool matchesTemplate(DayTemplateData template) { + var startDateMatches = template.startTime.timeMatches(startDate); + var endDateMatches = template.endTime.timeMatches(endDate); + var breaksMatch = template.breaks.matches(breaks); + + return startDateMatches && endDateMatches && breaksMatch; + } +} + +extension on List { + bool matches(List other) { + if (other.length != length) { + return false; + } + + for (var otherBreak in other) { + if (!any((e) => e.matches(otherBreak))) { + return false; + } + } + + return true; + } +} + +extension on AvailabilityBreakModel { + bool matches(AvailabilityBreakModel other) { + var startTimeMatches = other.startTime.timeMatches(startTime); + var endTimeMatches = other.endTime.timeMatches(endTime); + var durationMatches = other.duration == duration; + + return startTimeMatches && endTimeMatches && durationMatches; + } +} + +extension on DateTime { + DateTime get date => DateTime(year, month, day); + + bool timeMatches(DateTime other) => + other.hour == hour && other.minute == minute; +}