From 730d5140a98f26ac87d9a9f1eae30af4d281bf60 Mon Sep 17 00:00:00 2001
From: uhensler <urs.hensler@frentix.com>
Date: Fri, 5 Jun 2020 15:14:56 +0200
Subject: [PATCH] OO-4630: GUI to create recurring appointments

---
 .../java/org/olat/core/util/DateUtils.java    |  65 +++++++
 .../ui/TopicCreateController.java             | 172 ++++++++++++++----
 ...s_create.html => appointments_single.html} |   0
 .../ui/_i18n/LocalStrings_de.properties       |   5 +
 .../ui/_i18n/LocalStrings_en.properties       |   5 +
 .../org/olat/core/util/DateUtilsTest.java     |  74 ++++++++
 6 files changed, 290 insertions(+), 31 deletions(-)
 rename src/main/java/org/olat/course/nodes/appointments/ui/_content/{appointments_create.html => appointments_single.html} (100%)

diff --git a/src/main/java/org/olat/core/util/DateUtils.java b/src/main/java/org/olat/core/util/DateUtils.java
index 269e2799641..06d3c682d6a 100644
--- a/src/main/java/org/olat/core/util/DateUtils.java
+++ b/src/main/java/org/olat/core/util/DateUtils.java
@@ -19,12 +19,17 @@
  */
 package org.olat.core.util;
 
+import java.time.DayOfWeek;
 import java.time.Instant;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
+import java.util.ArrayList;
 import java.util.Calendar;
+import java.util.Collection;
 import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.List;
 
 /**
  * 
@@ -58,6 +63,35 @@ public class DateUtils {
 		return Instant.ofEpochMilli(date.getTime()).atZone(ZoneId.systemDefault()).toLocalDateTime();
 	}
 	
+	public static Date setTime(Date date, int hour, int minutes, int seconds) {
+		Calendar calendar = new GregorianCalendar();
+		calendar.setTime(date);
+		calendar.set(Calendar.HOUR, hour);
+		calendar.set(Calendar.MINUTE, minutes);
+		calendar.set(Calendar.SECOND, seconds);
+		return calendar.getTime();
+	}
+	
+	/**
+	 * Keeps the day of the date but copies the date from the from date.
+	 *
+	 * @param to
+	 * @param from
+	 * @return
+	 */
+	public static Date copyTime(Date date, Date from) {
+		Calendar fromCalendar = new GregorianCalendar();
+		fromCalendar.setTime(from);
+		
+		Calendar toCalendar = new GregorianCalendar();
+		toCalendar.setTime(date);
+		toCalendar.set(Calendar.HOUR, fromCalendar.get(Calendar.HOUR));
+		toCalendar.set(Calendar.MINUTE, fromCalendar.get(Calendar.MINUTE));
+		toCalendar.set(Calendar.SECOND, fromCalendar.get(Calendar.SECOND));
+		
+		return toCalendar.getTime();
+	}
+	
 	public static Date addDays(Date date, int days) {
 		Calendar c = Calendar.getInstance();
 		c.setTime(date);
@@ -71,5 +105,36 @@ public class DateUtils {
 		
 		return date1.after(date2)? date1: date2;
 	}
+	
+	public static List<Date> getDaysInRange(Date start, Date end) {
+		List<Date> dates = new ArrayList<>();
+		Calendar calendar = new GregorianCalendar();
+		calendar.setTime(start);
+	 
+		while (calendar.getTime().before(end)) {
+			Date result = calendar.getTime();
+			dates.add(result);
+			calendar.add(Calendar.DATE, 1);
+		}
+		
+		return dates;
+	}
+	
+	public static List<Date> getDaysInRange(Date start, Date end, Collection<DayOfWeek> daysOfWeek) {
+		List<Date> dates = new ArrayList<>();
+		Calendar calendar = new GregorianCalendar();
+		calendar.setTime(start);
+	 
+		while (calendar.getTime().before(end)) {
+			DayOfWeek dayOfWeek = toLocalDate(calendar.getTime()).getDayOfWeek();
+			if (daysOfWeek.contains(dayOfWeek)) {
+				Date result = calendar.getTime();
+				dates.add(result);
+			}
+			calendar.add(Calendar.DATE, 1);
+		}
+		
+		return dates;
+	}
 
 }
diff --git a/src/main/java/org/olat/course/nodes/appointments/ui/TopicCreateController.java b/src/main/java/org/olat/course/nodes/appointments/ui/TopicCreateController.java
index fbb6e8a7ea7..06422bf1954 100644
--- a/src/main/java/org/olat/course/nodes/appointments/ui/TopicCreateController.java
+++ b/src/main/java/org/olat/course/nodes/appointments/ui/TopicCreateController.java
@@ -22,6 +22,8 @@ package org.olat.course.nodes.appointments.ui;
 import static org.olat.core.gui.components.util.KeyValues.VALUE_ASC;
 import static org.olat.core.gui.components.util.KeyValues.entry;
 
+import java.time.DayOfWeek;
+import java.time.format.TextStyle;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Date;
@@ -46,6 +48,7 @@ import org.olat.core.gui.control.Controller;
 import org.olat.core.gui.control.Event;
 import org.olat.core.gui.control.WindowControl;
 import org.olat.core.id.Identity;
+import org.olat.core.util.DateUtils;
 import org.olat.core.util.StringHelper;
 import org.olat.course.nodes.appointments.Appointment;
 import org.olat.course.nodes.appointments.AppointmentsSecurityCallback;
@@ -74,7 +77,11 @@ public class TopicCreateController extends FormBasicController {
 	private MultipleSelectionElement organizerEl;
 	private TextElement locationEl;
 	private TextElement maxParticipationsEl;
-	private FormLayoutContainer appointmentsCont;
+	private MultipleSelectionElement recurringEl;
+	private FormLayoutContainer singleCont;
+	private DateChooser recurringFirstEl;
+	private MultipleSelectionElement recurringDaysOfWeekEl;
+	private DateChooser recurringLastEl;
 	
 	private RepositoryEntry entry;
 	private String subIdent;
@@ -112,16 +119,11 @@ public class TopicCreateController extends FormBasicController {
 	@Override
 	protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
 		// Topic
-		String title = topic == null ? "" : topic.getTitle();
-		titleEl = uifactory.addTextElement("topic.title", "topic.title", 128, title, formLayout);
+		titleEl = uifactory.addTextElement("topic.title", "topic.title", 128, null, formLayout);
 		titleEl.setMandatory(true);
-		if(!StringHelper.containsNonWhitespace(title)) {
-			titleEl.setFocus(true);
-		}
 		
-		String description = topic == null ? "" : topic.getDescription();
 		descriptionEl = uifactory.addTextAreaElement("topic.description", "topic.description", 2000, 4, 72, false,
-				false, description, formLayout);
+				false, null, formLayout);
 		
 		// Organizer
 		KeyValues coachesKV = new KeyValues();
@@ -143,25 +145,62 @@ public class TopicCreateController extends FormBasicController {
 		
 		maxParticipationsEl = uifactory.addTextElement("appointment.max.participations", 5, null, formLayout);
 		
-		appointmentsCont = FormLayoutContainer.createCustomFormLayout("appointmentsCont", getTranslator(), velocity_root + "/appointments_create.html");
-		formLayout.add(appointmentsCont);
-		appointmentsCont.setRootForm(mainForm);
-		appointmentsCont.setLabel("appointments", null);
+		recurringEl = uifactory.addCheckboxesHorizontal("appointments.recurring", formLayout,
+				new String[] { "xx" }, new String[] { null });
+		recurringEl.addActionListener(FormEvent.ONCHANGE);
+		
+		// Single appointments
+		singleCont = FormLayoutContainer.createCustomFormLayout("singleCont", getTranslator(), velocity_root + "/appointments_single.html");
+		formLayout.add(singleCont);
+		singleCont.setRootForm(mainForm);
+		singleCont.setLabel("appointments", null);
 		
 		appointmentWrappers = new ArrayList<>();
 		doCreateAppointmentWrapper(null);
-		appointmentsCont.contextPut("appointments", appointmentWrappers);
+		singleCont.contextPut("appointments", appointmentWrappers);
+		
+		// Reccuring appointments
+		recurringFirstEl = uifactory.addDateChooser("appointments.recurring.first", null, formLayout);
+		recurringFirstEl.setDateChooserTimeEnabled(true);
+		recurringFirstEl.setSecondDate(true);
+		recurringFirstEl.setSameDay(true);
+		recurringFirstEl.setMandatory(true);
+		
+		DayOfWeek[] dayOfWeeks = DayOfWeek.values();
+		KeyValues dayOfWeekKV = new KeyValues();
+		for (int i = 0; i < dayOfWeeks.length; i++) {
+			dayOfWeekKV.add(entry(dayOfWeeks[i].name(), dayOfWeeks[i].getDisplayName(TextStyle.FULL_STANDALONE, getLocale())));
+		}
+		recurringDaysOfWeekEl = uifactory.addCheckboxesHorizontal("appointments.recurring.days.of.week", formLayout,
+				dayOfWeekKV.keys(), dayOfWeekKV.values());
+		recurringDaysOfWeekEl.setMandatory(true);
+		
+		recurringLastEl = uifactory.addDateChooser("appointments.recurring.last", null, formLayout);
+		recurringLastEl.setMandatory(true);
 		
+		// Buttons
 		FormLayoutContainer buttonsCont = FormLayoutContainer.createButtonLayout("buttons", getTranslator());
 		formLayout.add(buttonsCont);
 		buttonsCont.setRootForm(mainForm);
 		uifactory.addFormSubmitButton("save", buttonsCont);
 		uifactory.addFormCancelButton("cancel", buttonsCont, ureq, getWindowControl());
+		
+		updateUI();
+	}
+
+	private void updateUI() {
+		boolean recurring = recurringEl.isAtLeastSelected(1);
+		singleCont.setVisible(!recurring);
+		recurringFirstEl.setVisible(recurring);
+		recurringDaysOfWeekEl.setVisible(recurring);
+		recurringLastEl.setVisible(recurring);
 	}
 
 	@Override
 	protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) {
-		if (source instanceof DateChooser) {
+		if (source == recurringEl) {
+			updateUI();
+		} else if (source instanceof DateChooser) {
 			DateChooser dateChooser = (DateChooser)source;
 			AppointmentWrapper wrapper = (AppointmentWrapper)dateChooser.getUserObject();
 			doInitEndDate(wrapper);
@@ -204,26 +243,59 @@ public class TopicCreateController extends FormBasicController {
 			}
 		}
 		
+		boolean recurring = recurringEl.isAtLeastSelected(1);
+			
 		for (AppointmentWrapper wrapper : appointmentWrappers) {
 			DateChooser startEl = wrapper.getStartEl();
 			DateChooser endEl = wrapper.getEndEl();
 			startEl.clearError();
 			endEl.clearError();
-			if (startEl.getDate() == null && endEl.getDate() != null) {
-				startEl.setErrorKey("form.legende.mandatory", null);
+			if (!recurring) {
+				if (startEl.getDate() == null && endEl.getDate() != null) {
+					startEl.setErrorKey("form.legende.mandatory", null);
+					allOk &= false;
+				}
+				if (endEl.getDate() == null && startEl.getDate() != null) {
+					endEl.setErrorKey("form.legende.mandatory", null);
+					allOk &= false;
+				}
+				if (startEl.getDate() != null && endEl.getDate() != null) {
+					Date start = startEl.getDate();
+					Date end = endEl.getDate();
+					if(end.before(start)) {
+						endEl.setErrorKey("error.start.after.end", null);
+						allOk &= false;
+					}
+				}
+			}
+		}
+		
+		recurringFirstEl.clearError();
+		recurringDaysOfWeekEl.clearError();
+		recurringLastEl.clearError();
+		if (recurring) {
+			if (recurringFirstEl.getDate() == null || recurringFirstEl.getSecondDate() == null) {
+				recurringFirstEl.setErrorKey("form.legende.mandatory", null);
+				allOk &= false;
+			} else if (recurringFirstEl.getDate().after(recurringFirstEl.getSecondDate())) {
+				recurringFirstEl.setErrorKey("error.start.after.end", null);
 				allOk &= false;
 			}
-			if (endEl.getDate() == null && startEl.getDate() != null) {
-				endEl.setErrorKey("form.legende.mandatory", null);
+			
+			if (!recurringDaysOfWeekEl.isAtLeastSelected(1)) {
+				recurringDaysOfWeekEl.setErrorKey("form.legende.mandatory", null);
 				allOk &= false;
 			}
-			if (startEl.getDate() != null && endEl.getDate() != null) {
-				Date start = startEl.getDate();
-				Date end = endEl.getDate();
-				if(end.before(start)) {
-					endEl.setErrorKey("error.start.after.end", null);
-					allOk &= false;
-				}
+			
+			if (recurringLastEl.getDate() == null) {
+				recurringLastEl.setErrorKey("form.legende.mandatory", null);
+				allOk &= false;
+			}
+			
+			if (recurringFirstEl.getDate() != null && recurringLastEl.getDate() != null
+					&& recurringFirstEl.getDate().after(recurringLastEl.getDate())) {
+				recurringLastEl.setErrorKey("error.first.after.start", null);
+				allOk &= false;
 			}
 		}
 		
@@ -244,7 +316,13 @@ public class TopicCreateController extends FormBasicController {
 	private void doSave() {
 		doSaveTopic();
 		doSaveOrganizers();
-		doSaveAppointments();
+		
+		boolean reccuring = recurringEl.isAtLeastSelected(1);
+		if (reccuring) {
+			doSaveReccuringAppointments();
+		} else {
+			doSaveSingleAppointments();
+		}
 	}
 
 	private void doSaveTopic() {
@@ -282,7 +360,7 @@ public class TopicCreateController extends FormBasicController {
 				.forEach(coach -> appointmentsService.createOrganizer(topic, coach));
 	}
 
-	private void doSaveAppointments() {
+	private void doSaveSingleAppointments() {
 		for (AppointmentWrapper wrapper : appointmentWrappers) {
 			DateChooser startEl = wrapper.getStartEl();
 			DateChooser endEl = wrapper.getEndEl();
@@ -308,26 +386,58 @@ public class TopicCreateController extends FormBasicController {
 			}
 		}
 	}
+
+	private void doSaveReccuringAppointments() {
+		Date firstStart = recurringFirstEl.getDate();
+		Date firstEnd = recurringFirstEl.getSecondDate();
+		Date last = recurringLastEl.getDate();
+		last = DateUtils.setTime(last, 23, 59, 59);
+		
+		Collection<DayOfWeek> daysOfWeek = recurringDaysOfWeekEl.getSelectedKeys().stream()
+				.map(DayOfWeek::valueOf)
+				.collect(Collectors.toList());
+		
+		List<Date> starts = DateUtils.getDaysInRange(firstStart, last, daysOfWeek);
+		for (Date start : starts) {
+			Appointment appointment = appointmentsService.createUnsavedAppointment(topic);
+			
+			appointment.setStart(start);
+			
+			Date end = DateUtils.copyTime(start, firstEnd);
+			appointment.setEnd(end);
+			
+			String location = locationEl.getValue();
+			appointment.setLocation(location);
+			
+			String maxParticipationsValue = maxParticipationsEl.getValue();
+			Integer maxParticipations = StringHelper.containsNonWhitespace(maxParticipationsValue)
+					? Integer.valueOf(maxParticipationsValue)
+					: null;
+			appointment.setMaxParticipations(maxParticipations);
+			
+			appointmentsService.saveAppointment(appointment);
+		}
+	}
 	
 	private void doCreateAppointmentWrapper(AppointmentWrapper after) {
 		AppointmentWrapper wrapper = new AppointmentWrapper();
 		
-		DateChooser startEl = uifactory.addDateChooser("start_" + counter++, null, appointmentsCont);
+		DateChooser startEl = uifactory.addDateChooser("start_" + counter++, null, singleCont);
 		startEl.setDateChooserTimeEnabled(true);
 		startEl.setUserObject(wrapper);
 		startEl.addActionListener(FormEvent.ONCHANGE);
 		wrapper.setStartEl(startEl);
 		
-		DateChooser endEl = uifactory.addDateChooser("end_" + counter++, null, appointmentsCont);
+		DateChooser endEl = uifactory.addDateChooser("end_" + counter++, null, singleCont);
 		endEl.setDateChooserTimeEnabled(true);
 		wrapper.setEndEl(endEl);
 		
-		FormLink addEl = uifactory.addFormLink("add_" + counter++, CMD_ADD, "", null, appointmentsCont, Link.NONTRANSLATED + Link.BUTTON);
+		FormLink addEl = uifactory.addFormLink("add_" + counter++, CMD_ADD, "", null, singleCont, Link.NONTRANSLATED + Link.BUTTON);
 		addEl.setIconLeftCSS("o_icon o_icon-lg o_icon_add");
 		addEl.setUserObject(wrapper);
 		wrapper.setAddEl(addEl);
 		
-		FormLink removeEl = uifactory.addFormLink("remove_" + counter++, CMD_REMOVE, "", null, appointmentsCont, Link.NONTRANSLATED + Link.BUTTON);
+		FormLink removeEl = uifactory.addFormLink("remove_" + counter++, CMD_REMOVE, "", null, singleCont, Link.NONTRANSLATED + Link.BUTTON);
 		removeEl.setIconLeftCSS("o_icon o_icon-lg o_icon_delete");
 		removeEl.setUserObject(wrapper);
 		wrapper.setRemoveEl(removeEl);
diff --git a/src/main/java/org/olat/course/nodes/appointments/ui/_content/appointments_create.html b/src/main/java/org/olat/course/nodes/appointments/ui/_content/appointments_single.html
similarity index 100%
rename from src/main/java/org/olat/course/nodes/appointments/ui/_content/appointments_create.html
rename to src/main/java/org/olat/course/nodes/appointments/ui/_content/appointments_single.html
diff --git a/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_de.properties
index b46259a5b64..b2c8c42f543 100644
--- a/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_de.properties
@@ -28,6 +28,10 @@ appointments.confirmable.one=Ein Termin muss noch best\u00E4tigt werden.
 appointments.free=Es sind noch {0} Termine frei.
 appointments.free.one=Es ist noch ein Termin frei.
 appointments.open=Termine anzeigen
+appointments.recurring=Wiederkehrende Termine
+appointments.recurring.days.of.week=Wochentage
+appointments.recurring.first=Erster Termin
+appointments.recurring.last=Letzter Termin
 appointments.select=Termin ausw\u00E4hlen
 appointments.selected=Sie haben {0} Termine ausgew\u00E4hlt.
 appointments.selected.one=Sie haben einen Termin ausgew\u00E4hlt.
@@ -53,6 +57,7 @@ edit.topic=Thema bearbeiten
 email.title=Neue Nachricht
 email.organizer.recipients=Organisatoren
 email.organizer.subject=Termin "{0}"
+error.first.after.start=Der letzte Termin darf nicht vor dem ersten Termin liegen.
 error.positiv.number=Geben Sie eine positive Ganzzahl ein.
 error.select.appointment=Sie m\u00FCssen einen Termin ausw\u00E4hlen.
 error.select.participant=Sie m\u00FCssen einen Teilnehmer ausw\u00E4hlen.
diff --git a/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_en.properties
index d4faf593289..b07d2a38c89 100644
--- a/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_en.properties
@@ -28,6 +28,10 @@ appointments.confirmable.one=One appointment has to be confirmed.
 appointments.free=There are {0} appointments left.
 appointments.free.one=There is one appointment left.
 appointments.open=Show appointments
+appointments.recurring=Recurring appointments
+appointments.recurring.days.of.week=Days of week
+appointments.recurring.first=First appointment
+appointments.recurring.last=Last appointment
 appointments.select=Select appointment
 appointments.selected=You have {0} appointments selected.
 appointments.selected.one=You have one appointment selected.
@@ -53,6 +57,7 @@ edit.topic=Edit topic
 email.title=New message
 email.organizer.recipients=Organizers
 email.organizer.subject=Appointment "{0}"
+error.first.after.start=The last appointment must not be before the first appointment.
 error.positiv.number=It has to be a positive integer.
 error.select.appointment=You have to select an appointment.
 error.select.participant=You have to select a participant.
diff --git a/src/test/java/org/olat/core/util/DateUtilsTest.java b/src/test/java/org/olat/core/util/DateUtilsTest.java
index 34a47320935..f0472676ae3 100644
--- a/src/test/java/org/olat/core/util/DateUtilsTest.java
+++ b/src/test/java/org/olat/core/util/DateUtilsTest.java
@@ -22,8 +22,12 @@ package org.olat.core.util;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.olat.core.util.DateUtils.toDate;
 
+import java.time.DayOfWeek;
 import java.time.LocalDate;
 import java.util.Date;
+import java.util.EnumSet;
+import java.util.GregorianCalendar;
+import java.util.List;
 
 import org.junit.Test;
 
@@ -35,6 +39,25 @@ import org.junit.Test;
  */
 public class DateUtilsTest {
 	
+	@Test
+	public void shouldSetTime() {
+		Date date = new GregorianCalendar(2020, 5, 1, 10, 0, 0).getTime();
+		
+		date = DateUtils.setTime(date, 20, 10, 5);
+		
+		assertThat(date).isEqualTo(new GregorianCalendar(2020, 5, 1, 20, 10, 5).getTime());
+	}
+
+	@Test
+	public void shouldCopyTime() {
+		Date date = new GregorianCalendar(2020, 5, 1, 10, 0, 0).getTime();
+		Date from = new GregorianCalendar(2020, 8, 20, 8, 3, 2).getTime();
+		
+		date = DateUtils.copyTime(date, from);
+		
+		assertThat(date).isEqualTo(new GregorianCalendar(2020, 5, 1, 8, 3, 2).getTime());
+	}
+	
 	@Test
 	public void shouldGetLaterIfFirstIsLater() {
 		Date date1 = toDate(LocalDate.of(2011, 10, 12));
@@ -68,6 +91,57 @@ public class DateUtilsTest {
 		
 		assertThat(later).isEqualTo(expected);
 	}
+	
+	@Test
+	public void shouldGetDaysInRange() {
+		Date start = new GregorianCalendar(2020, 5, 1, 10, 0, 0).getTime();
+		Date end = new GregorianCalendar(2020, 5, 10, 0, 0, 0).getTime();
+		
+		List<Date> days = DateUtils.getDaysInRange(start, end);
+		
+		assertThat(days)
+				.containsExactly(
+					new GregorianCalendar(2020, 5, 1, 10, 0, 0).getTime(),
+					new GregorianCalendar(2020, 5, 2, 10, 0, 0).getTime(),
+					new GregorianCalendar(2020, 5, 3, 10, 0, 0).getTime(),
+					new GregorianCalendar(2020, 5, 4, 10, 0, 0).getTime(),
+					new GregorianCalendar(2020, 5, 5, 10, 0, 0).getTime(),
+					new GregorianCalendar(2020, 5, 6, 10, 0, 0).getTime(),
+					new GregorianCalendar(2020, 5, 7, 10, 0, 0).getTime(),
+					new GregorianCalendar(2020, 5, 8, 10, 0, 0).getTime(),
+					new GregorianCalendar(2020, 5, 9, 10, 0, 0).getTime())
+				.doesNotContain(
+					// because of the time
+					end
+				);
+	}
+	
 
+	@Test
+	public void shouldGetDaysOfWeekInRange() {
+		Date start = new GregorianCalendar(2020, 4, 1, 10, 0, 0).getTime();
+		Date end = new GregorianCalendar(2020, 5, 10, 0, 0, 0).getTime();
+		
+		EnumSet<DayOfWeek> daysOfWeek = EnumSet.of(DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY);
+		List<Date> days = DateUtils.getDaysInRange(start, end, daysOfWeek);
+		
+		assertThat(days)
+				.containsExactly(
+					new GregorianCalendar(2020, 4, 4, 10, 0, 0).getTime(),
+					new GregorianCalendar(2020, 4, 6, 10, 0, 0).getTime(),
+					new GregorianCalendar(2020, 4, 11, 10, 0, 0).getTime(),
+					new GregorianCalendar(2020, 4, 13, 10, 0, 0).getTime(),
+					new GregorianCalendar(2020, 4, 18, 10, 0, 0).getTime(),
+					new GregorianCalendar(2020, 4, 20, 10, 0, 0).getTime(),
+					new GregorianCalendar(2020, 4, 25, 10, 0, 0).getTime(),
+					new GregorianCalendar(2020, 4, 27, 10, 0, 0).getTime(),
+					new GregorianCalendar(2020, 5, 1, 10, 0, 0).getTime(),
+					new GregorianCalendar(2020, 5, 3, 10, 0, 0).getTime(),
+					new GregorianCalendar(2020, 5, 8, 10, 0, 0).getTime())
+				.doesNotContain(
+					end
+				);
+
+	}
 
 }
-- 
GitLab