001/*
002 *  Copyright 2015 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.workspaces.calendars.helper;
017
018import java.time.LocalDate;
019import java.time.ZoneId;
020import java.time.ZonedDateTime;
021import java.time.temporal.ChronoUnit;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.List;
025
026import org.ametys.plugins.messagingconnector.EventRecurrenceTypeEnum;
027import org.ametys.runtime.config.Config;
028
029/**
030 * Helper for recurrent event operation
031 */
032public final class RecurrentEventHelper
033{
034    private RecurrentEventHelper()
035    {
036        // Helper class
037    }
038    
039    /**
040     * Get the next occurrence date
041     * @param recurrenceType recurrence type
042     * @param eventStartDate the event start date
043     * @param date the occurrence date
044     * @return the next occurrence date
045     */
046    public static ZonedDateTime getNextDate(EventRecurrenceTypeEnum recurrenceType, ZonedDateTime eventStartDate, ZonedDateTime date)
047    {
048        ZonedDateTime nextDate = null;
049        switch (recurrenceType)
050        {
051            case ALL_DAY:
052                nextDate = _nextAllDayDate(date);
053                break;
054            case ALL_WORKING_DAY:
055                nextDate = _nextAllWorkingDayDate(date);
056                break;
057            case WEEKLY:
058                nextDate = _nextWeeklyDate(date);
059                break;
060            case BIWEEKLY:
061                nextDate = _nextBiweeklyDate(date);
062                break;
063            case MONTHLY:
064                nextDate = _nextMonthlyDate(date, eventStartDate);
065                break;
066            default:
067                break;
068        }
069        
070        return nextDate;
071    }
072    
073    private static ZonedDateTime _nextAllDayDate(ZonedDateTime date)
074    {
075        return date.plusDays(1);
076    }
077    
078    /**
079     * get the list of working days
080     * @return a list of integer, matching {@link java.util.Calendar} days
081     */
082    public static List<Integer> getWorkingDays()
083    {
084        List<Integer> result = new ArrayList<>();
085        String workingDayAsString = Config.getInstance().getValue("workspaces.calendar.event.working.day");
086        List<String> workingDaysStrings = Arrays.asList(workingDayAsString.split(","));
087        for (String workkingDayString : workingDaysStrings)
088        {
089            result.add(Integer.valueOf(workkingDayString));
090        }
091        return result;
092    }
093
094    private static ZonedDateTime _nextAllWorkingDayDate(ZonedDateTime date)
095    {
096        String workingDayAsString = Config.getInstance().getValue("workspaces.calendar.event.working.day");
097        
098        int dayId = date.getDayOfWeek().getValue() + 1;
099        
100        int nbDay = 1;
101        int nextDayId = (dayId % 7) + 1;
102        List<String> workingDay = Arrays.asList(workingDayAsString.split(","));
103        while (!workingDay.contains(String.valueOf(nextDayId)))
104        {
105            nextDayId = (nextDayId % 7) + 1;
106            nbDay++;
107        }
108        
109        return date.plusDays(nbDay);
110    }
111    
112    private static ZonedDateTime _nextWeeklyDate(ZonedDateTime date)
113    {
114        return  date.plusWeeks(1);
115    }
116    
117    private static ZonedDateTime _nextBiweeklyDate(ZonedDateTime date)
118    {
119        return  date.plusWeeks(2);
120    }
121    
122    private static ZonedDateTime _nextMonthlyDate(ZonedDateTime date, ZonedDateTime eventStartDate)
123    {
124        int dayOfMonth = eventStartDate.getDayOfMonth();
125        
126        ZonedDateTime nextDate = date.plusMonths(1);
127        int nextDayOfMonth = nextDate.getDayOfMonth();
128        LocalDate nextDateAsLocal = nextDate.toLocalDate();
129        int nextDayMaxOfMonth = nextDateAsLocal.lengthOfMonth();
130
131        if (nextDayOfMonth < dayOfMonth)
132        {
133            if (dayOfMonth < nextDayMaxOfMonth)
134            {
135                nextDate = nextDate.withDayOfMonth(dayOfMonth);
136            }
137            else
138            {
139                nextDate = nextDate.withDayOfMonth(nextDayMaxOfMonth);
140            }
141        }
142        
143        return nextDate;
144    }
145    
146    /**
147     * Compute the occurrence of an event
148     * @param startDate the start of the range to compute occurrences
149     * @param endDate the end of the range to compute occurrences
150     * @param eventStartDate the start date of the occurrence
151     * @param originalOccurrenceStartDate the original start date of the occurrence (different as the eventStartDate when editing an occurrence)
152     * @param recurrenceType the recurrence type
153     * @param excludedOccurences the occurrences to exclude
154     * @param zoneId the zoneId used for the dates
155     * @param untilDate until date of the recurring event
156     * @return a list of occurrence start dates
157     */
158    public static List<ZonedDateTime> getOccurrences(ZonedDateTime startDate, ZonedDateTime endDate, ZonedDateTime eventStartDate, ZonedDateTime originalOccurrenceStartDate, EventRecurrenceTypeEnum recurrenceType, List<ZonedDateTime> excludedOccurences, ZoneId zoneId, ZonedDateTime untilDate)
159    {
160        long diffInSeconds = ChronoUnit.SECONDS.between(originalOccurrenceStartDate, eventStartDate);
161        List<ZonedDateTime> occurences = new ArrayList<>();
162        
163        ZonedDateTime firstDate = startDate.equals(eventStartDate) ? startDate : startDate.plusSeconds(diffInSeconds);
164        firstDate = firstDate.withZoneSameInstant(zoneId);
165
166        if (firstDate.isAfter(endDate))
167        {
168            return occurences;
169        }
170        
171        ZonedDateTime firstDateCalendar = firstDate.withZoneSameInstant(zoneId).truncatedTo(ChronoUnit.DAYS);
172        
173        if (excludedOccurences.stream().noneMatch(excludedOccurrence -> excludedOccurrence.isEqual(firstDateCalendar)))
174        {
175            occurences.add(firstDate);
176        }
177        
178        ZonedDateTime nextDate = getNextDate(recurrenceType, eventStartDate.withZoneSameInstant(zoneId), firstDate.withZoneSameInstant(zoneId));
179        while (nextDate != null && nextDate.isBefore(endDate) && (untilDate == null || untilDate.isAfter(nextDate)))
180        {
181            ZonedDateTime nextDateCalendar = nextDate.truncatedTo(ChronoUnit.DAYS);
182            
183            if (excludedOccurences.stream().noneMatch(excludedOccurrence -> excludedOccurrence.isEqual(nextDateCalendar)))
184            {
185                occurences.add(nextDate);
186            }
187            nextDate = getNextDate(recurrenceType, eventStartDate.withZoneSameInstant(zoneId), nextDate.withZoneSameInstant(zoneId));
188        }
189        
190        return occurences;
191    }
192}