From 4803cfcaf22c1b2545075b3de93a1846a148e7e3 Mon Sep 17 00:00:00 2001 From: Parth Baraiya Date: Thu, 23 Nov 2023 17:31:35 +0530 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Adds=20option=20to=20decide=20wheth?= =?UTF-8?q?er=20to=20include=20the=20edge=20while=20merging=20the=20overla?= =?UTF-8?q?pping=20events=20or=20not.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixes issue #290 --- lib/src/calendar_event_data.dart | 2 +- lib/src/event_arrangers/event_arrangers.dart | 10 +- .../event_arrangers/merge_event_arranger.dart | 35 +- .../event_arrangers/side_event_arranger.dart | 14 +- .../merge_event_arranger_test.dart | 473 ++++++++++++++++++ 5 files changed, 517 insertions(+), 17 deletions(-) create mode 100644 test/event_arranger_test/merge_event_arranger_test.dart diff --git a/lib/src/calendar_event_data.dart b/lib/src/calendar_event_data.dart index 807433c7..da9ac3b2 100644 --- a/lib/src/calendar_event_data.dart +++ b/lib/src/calendar_event_data.dart @@ -70,7 +70,7 @@ class CalendarEventData { }; @override - String toString() => toJson().toString(); + String toString() => '${toJson()}'; @override bool operator ==(Object other) { diff --git a/lib/src/event_arrangers/event_arrangers.dart b/lib/src/event_arrangers/event_arrangers.dart index c73e52ee..0ac4f0c6 100644 --- a/lib/src/event_arrangers/event_arrangers.dart +++ b/lib/src/event_arrangers/event_arrangers.dart @@ -13,6 +13,12 @@ import '../extensions.dart'; part 'merge_event_arranger.dart'; part 'side_event_arranger.dart'; +/// {@template event_arranger_arrange_method_doc} +/// This method will arrange all the events in and return List of +/// [OrganizedCalendarEventData]. +/// +/// {@endtemplate} + abstract class EventArranger { /// [EventArranger] defines how simultaneous events will be arranged. /// Implement [arrange] method to define how events will be arranged. @@ -23,9 +29,7 @@ abstract class EventArranger { /// const EventArranger(); - /// This method will arrange all the events in and return List of - /// [OrganizedCalendarEventData]. - /// + /// {@macro event_arranger_arrange_method_doc} List> arrange({ required List> events, required double height, diff --git a/lib/src/event_arrangers/merge_event_arranger.dart b/lib/src/event_arrangers/merge_event_arranger.dart index 37e9ca6a..c906c456 100644 --- a/lib/src/event_arrangers/merge_event_arranger.dart +++ b/lib/src/event_arrangers/merge_event_arranger.dart @@ -9,8 +9,16 @@ class MergeEventArranger extends EventArranger { /// events. and that will act like one single event. /// [OrganizedCalendarEventData.events] will gives /// list of all the combined events. - const MergeEventArranger(); + const MergeEventArranger({ + this.includeEdges = true, + }); + final bool includeEdges; + + /// {@macro event_arranger_arrange_method_doc} + /// + /// Make sure that all the events that are passed in [events], must be in + /// ascending order of start time. @override List> arrange({ required List> events, @@ -18,9 +26,13 @@ class MergeEventArranger extends EventArranger { required double width, required double heightPerMinute, }) { + // TODO: Right now all the events that are passed in this function must be + // sorted in ascending order of the start time. + // final arrangedEvents = >[]; for (final event in events) { + // Checks if an event has valid start and end time. if (event.startTime == null || event.endTime == null || event.endTime!.getTotalMinutes <= event.startTime!.getTotalMinutes) { @@ -56,6 +68,7 @@ class MergeEventArranger extends EventArranger { for (var i = 0; i < arrangeEventLen; i++) { final arrangedEventStart = arrangedEvents[i].startDuration.getTotalMinutes; + final arrangedEventEnd = arrangedEvents[i].endDuration.getTotalMinutes == 0 ? Constants.minutesADay @@ -122,17 +135,17 @@ class MergeEventArranger extends EventArranger { return arrangedEvents; } - bool _checkIsOverlapping(int arrangedEventStart, int arrangedEventEnd, - int eventStart, int eventEnd) { - var result = (arrangedEventStart >= eventStart && - arrangedEventStart <= eventEnd) || - (arrangedEventEnd >= eventStart && arrangedEventEnd <= eventEnd) || - (eventStart >= arrangedEventStart && eventStart <= arrangedEventEnd) || - (eventEnd >= arrangedEventStart && eventEnd <= arrangedEventEnd); + bool _checkIsOverlapping(int eStart1, int eEnd1, int eStart2, int eEnd2) { + final result = (eStart1 >= eStart2 && eStart1 < eEnd2) || + (eEnd1 > eStart2 && eEnd1 <= eEnd2) || + (eStart2 >= eStart1 && eStart2 < eEnd1) || + (eEnd2 > eStart1 && eEnd2 <= eEnd1) || + (includeEdges && + (eStart1 == eEnd2 || + eEnd1 == eStart2 || + eStart2 == eEnd1 || + eEnd2 == eStart1)); - if (result) { - result = result && (arrangedEventEnd != eventStart); - } return result; } } diff --git a/lib/src/event_arrangers/side_event_arranger.dart b/lib/src/event_arrangers/side_event_arranger.dart index e629f094..50a16567 100644 --- a/lib/src/event_arrangers/side_event_arranger.dart +++ b/lib/src/event_arrangers/side_event_arranger.dart @@ -7,8 +7,16 @@ part of 'event_arrangers.dart'; class SideEventArranger extends EventArranger { /// This class will provide method that will arrange /// all the events side by side. - const SideEventArranger(); + const SideEventArranger({ + this.includeEdges = false, + }); + + final bool includeEdges; + /// {@macro event_arranger_arrange_method_doc} + /// + /// Make sure that all the events that are passed in [events], must be in + /// ascending order of start time. @override List> arrange({ required List> events, @@ -16,7 +24,9 @@ class SideEventArranger extends EventArranger { required double width, required double heightPerMinute, }) { - final mergedEvents = MergeEventArranger().arrange( + final mergedEvents = MergeEventArranger( + includeEdges: includeEdges, + ).arrange( events: events, height: height, width: width, diff --git a/test/event_arranger_test/merge_event_arranger_test.dart b/test/event_arranger_test/merge_event_arranger_test.dart new file mode 100644 index 00000000..33a32de4 --- /dev/null +++ b/test/event_arranger_test/merge_event_arranger_test.dart @@ -0,0 +1,473 @@ +import 'package:calendar_view/calendar_view.dart'; +import 'package:flutter_test/flutter_test.dart'; + +const height = 1440.0; +const width = 500.0; +const heightPerMinute = 1.0; + +void main() { + final now = DateTime.now().withoutTime; + + group('MergeEventArrangerTest', () { + test('Events which does not overlap.', () { + final events = [ + CalendarEventData( + title: 'Event 1', + date: now, + startTime: now.add(Duration(hours: 1)), + endTime: now.add( + Duration(hours: 2), + ), + ), + CalendarEventData( + title: 'Event 2', + date: now, + startTime: now.add(Duration(hours: 2, minutes: 15)), + endTime: now.add( + Duration(hours: 3), + ), + ), + CalendarEventData( + title: 'Event 3', + date: now, + startTime: now.add(Duration(hours: 3, minutes: 15)), + endTime: now.add( + Duration(hours: 4), + ), + ), + CalendarEventData( + title: 'Event 4', + date: now, + startTime: now.add(Duration(hours: 4, minutes: 15)), + endTime: now.add( + Duration(hours: 5), + ), + ), + CalendarEventData( + title: 'Event 5', + date: now, + startTime: now.add(Duration(hours: 10)), + endTime: now.add( + Duration(hours: 13), + ), + ), + ]; + + final mergedEvents = MergeEventArranger().arrange( + events: events, + height: height, + width: width, + heightPerMinute: heightPerMinute, + ); + + expect(mergedEvents.length, events.length); + }); + test('Only start time is overlapping', () { + final events = [ + CalendarEventData( + title: 'Event 1', + date: now, + startTime: now.add(Duration(hours: 10)), + endTime: now.add( + Duration(hours: 12), + ), + ), + CalendarEventData( + title: 'Event 2', + date: now, + startTime: now.add(Duration(hours: 8)), + endTime: now.add( + Duration(hours: 10), + ), + ), + ]; + + final mergedEvents = MergeEventArranger().arrange( + events: events, + height: height, + width: width, + heightPerMinute: heightPerMinute, + ); + + expect(mergedEvents.length, 1); + }); + test('Only end time is overlapping', () { + final events = [ + CalendarEventData( + title: 'Event 1', + date: now, + startTime: now.add(Duration(hours: 8)), + endTime: now.add( + Duration(hours: 10), + ), + ), + CalendarEventData( + title: 'Event 2', + date: now, + startTime: now.add(Duration(hours: 10)), + endTime: now.add( + Duration(hours: 12), + ), + ), + ]; + + final mergedEvents = MergeEventArranger().arrange( + events: events, + height: height, + width: width, + heightPerMinute: heightPerMinute, + ); + + expect(mergedEvents.length, 1); + }); + test('Event1 is smaller than event 2 and overlapping', () { + final events = [ + CalendarEventData( + title: 'Event 1', + date: now, + startTime: now.add(Duration(hours: 10)), + endTime: now.add( + Duration(hours: 12), + ), + ), + CalendarEventData( + title: 'Event 2', + date: now, + startTime: now.add(Duration(hours: 8)), + endTime: now.add( + Duration(hours: 14), + ), + ), + ]; + + final mergedEvents = MergeEventArranger().arrange( + events: events, + height: height, + width: width, + heightPerMinute: heightPerMinute, + ); + + expect(mergedEvents.length, 1); + }); + test('Event2 is smaller than event 1 and overlapping', () { + final events = [ + CalendarEventData( + title: 'Event 1', + date: now, + startTime: now.add(Duration(hours: 8)), + endTime: now.add( + Duration(hours: 14), + ), + ), + CalendarEventData( + title: 'Event 2', + date: now, + startTime: now.add(Duration(hours: 10)), + endTime: now.add( + Duration(hours: 12), + ), + ), + ]; + + final mergedEvents = MergeEventArranger().arrange( + events: events, + height: height, + width: width, + heightPerMinute: heightPerMinute, + ); + + expect(mergedEvents.length, 1); + }); + test('Both events are of same duration and occurs at the same time', () { + final events = [ + CalendarEventData( + title: 'Event 1', + date: now, + startTime: now.add(Duration(hours: 10)), + endTime: now.add( + Duration(hours: 12), + ), + ), + CalendarEventData( + title: 'Event 2', + date: now, + startTime: now.add(Duration(hours: 10)), + endTime: now.add( + Duration(hours: 12), + ), + ), + ]; + + final mergedEvents = MergeEventArranger().arrange( + events: events, + height: height, + width: width, + heightPerMinute: heightPerMinute, + ); + + expect(mergedEvents.length, 1); + }); + test('Only few events overlaps', () { + final events = [ + CalendarEventData( + title: 'Event 1', + date: now, + startTime: now.add(Duration(hours: 1)), + endTime: now.add( + Duration(hours: 2), + ), + ), + CalendarEventData( + title: 'Event 4', + date: now, + startTime: now.add(Duration(hours: 7)), + endTime: now.add( + Duration(hours: 11), + ), + ), + CalendarEventData( + title: 'Event 6', + date: now, + startTime: now.add(Duration(hours: 3)), + endTime: now.add( + Duration(hours: 3, minutes: 30), + ), + ), + CalendarEventData( + title: 'Event 5', + date: now, + startTime: now.add(Duration(hours: 1, minutes: 15)), + endTime: now.add( + Duration(hours: 2, minutes: 15), + ), + ), + CalendarEventData( + title: 'Event 3', + date: now, + startTime: now.add(Duration(hours: 5)), + endTime: now.add( + Duration(hours: 6), + ), + ), + CalendarEventData( + title: 'Event 2', + date: now, + startTime: now.add(Duration(hours: 8)), + endTime: now.add( + Duration(hours: 9), + ), + ), + ]; + + final mergedEvents = MergeEventArranger().arrange( + events: events, + height: height, + width: width, + heightPerMinute: heightPerMinute, + ); + + expect(mergedEvents.length, 4); + }); + test('All events overlaps with each other', () { + final events = [ + CalendarEventData( + title: 'Event 1', + date: now, + startTime: now.add(Duration(hours: 1)), + endTime: now.add( + Duration(hours: 2), + ), + ), + CalendarEventData( + title: 'Event 2', + date: now, + startTime: now.add(Duration(hours: 4)), + endTime: now.add( + Duration(hours: 5), + ), + ), + CalendarEventData( + title: 'Event 3', + date: now, + startTime: now.add(Duration(hours: 2)), + endTime: now.add( + Duration(hours: 6), + ), + ), + CalendarEventData( + title: 'Event 4', + date: now, + startTime: now.add(Duration(hours: 7)), + endTime: now.add( + Duration(hours: 10), + ), + ), + CalendarEventData( + title: 'Event 5', + date: now, + startTime: now.add(Duration(hours: 5)), + endTime: now.add( + Duration(hours: 7), + ), + ), + CalendarEventData( + title: 'Event 6', + date: now, + startTime: now.add(Duration(hours: 3)), + endTime: now.add( + Duration(hours: 6), + ), + ), + ]; + + final mergedEvents = MergeEventArranger().arrange( + events: events + ..sort((e1, e2) => + (e1.startTime?.getTotalMinutes ?? 0) - + (e2.startTime?.getTotalMinutes ?? 0)), + height: height, + width: width, + heightPerMinute: heightPerMinute, + ); + + expect(mergedEvents.length, 1); + }); + + group('Edge event should not merge', () { + test('End of Event 1 and Start of Event 2 is same', () { + final events = [ + CalendarEventData( + title: 'Event 1', + date: now, + startTime: now.add(Duration(hours: 1)), + endTime: now.add( + Duration(hours: 2), + ), + ), + CalendarEventData( + title: 'Event 2', + date: now, + startTime: now.add(Duration(hours: 2)), + endTime: now.add( + Duration(hours: 3), + ), + ), + ]; + + final mergedEvents = MergeEventArranger(includeEdges: false).arrange( + events: events + ..sort((e1, e2) => + (e1.startTime?.getTotalMinutes ?? 0) - + (e2.startTime?.getTotalMinutes ?? 0)), + height: height, + width: width, + heightPerMinute: heightPerMinute, + ); + + expect(mergedEvents.length, 2); + }); + + test('Start of Event 1 and End of Event 2 is same', () { + final events = [ + CalendarEventData( + title: 'Event 1', + date: now, + startTime: now.add(Duration(hours: 2)), + endTime: now.add( + Duration(hours: 3), + ), + ), + CalendarEventData( + title: 'Event 2', + date: now, + startTime: now.add(Duration(hours: 1)), + endTime: now.add( + Duration(hours: 2), + ), + ), + ]; + + final mergedEvents = MergeEventArranger(includeEdges: false).arrange( + events: events + ..sort((e1, e2) => + (e1.startTime?.getTotalMinutes ?? 0) - + (e2.startTime?.getTotalMinutes ?? 0)), + height: height, + width: width, + heightPerMinute: heightPerMinute, + ); + + expect(mergedEvents.length, 2); + }); + }); + group('Edge event should merge', () { + test('End of Event 1 and Start of Event 2 is same', () { + final events = [ + CalendarEventData( + title: 'Event 1', + date: now, + startTime: now.add(Duration(hours: 1)), + endTime: now.add( + Duration(hours: 2), + ), + ), + CalendarEventData( + title: 'Event 2', + date: now, + startTime: now.add(Duration(hours: 2)), + endTime: now.add( + Duration(hours: 3), + ), + ), + ]; + + final mergedEvents = MergeEventArranger(includeEdges: true).arrange( + events: events + ..sort((e1, e2) => + (e1.startTime?.getTotalMinutes ?? 0) - + (e2.startTime?.getTotalMinutes ?? 0)), + height: height, + width: width, + heightPerMinute: heightPerMinute, + ); + + expect(mergedEvents.length, 1); + }); + + test('Start of Event 1 and End of Event 2 is same', () { + final events = [ + CalendarEventData( + title: 'Event 1', + date: now, + startTime: now.add(Duration(hours: 2)), + endTime: now.add( + Duration(hours: 3), + ), + ), + CalendarEventData( + title: 'Event 2', + date: now, + startTime: now.add(Duration(hours: 1)), + endTime: now.add( + Duration(hours: 2), + ), + ), + ]; + + final mergedEvents = MergeEventArranger(includeEdges: true).arrange( + events: events + ..sort((e1, e2) => + (e1.startTime?.getTotalMinutes ?? 0) - + (e2.startTime?.getTotalMinutes ?? 0)), + height: height, + width: width, + heightPerMinute: heightPerMinute, + ); + + expect(mergedEvents.length, 1); + }); + }); + +// TODO: add tests for the events where start or end time is not valid. + }); +}