/*
* Copyright 2011-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package kr.debop4j.timeperiod.calendars;
import kr.debop4j.core.Guard;
import kr.debop4j.core.NotSupportException;
import kr.debop4j.core.Pair;
import kr.debop4j.core.tools.StringTool;
import kr.debop4j.timeperiod.*;
import kr.debop4j.timeperiod.timeline.TimeGapCalculator;
import kr.debop4j.timeperiod.timerange.WeekRange;
import kr.debop4j.timeperiod.tools.Durations;
import kr.debop4j.timeperiod.tools.TimeSpec;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static kr.debop4j.core.Guard.shouldBe;
import static kr.debop4j.core.Guard.shouldNotBeNull;
import static org.joda.time.Duration.ZERO;
/**
* 특정 Calendar 기준으로 특정 시각과 기간(Duration)을 이용하여 상대 시각을 구합니다.
*
* @author 배성혁 sunghyouk.bae@gmail.com
* @since 13. 5. 20. 오후 9:36
*/
@Slf4j
public class CalendarDateAdd extends DateAdd {
@Getter private final ITimeCalendar timeCalendar;
/** 일하는 요일 */
@Getter private final List<DayOfWeek> weekDays = new ArrayList<>();
/** 하루중 일하는 시간들 (오전, 오후, 야간으로 나뉠 수 있으므로 */
@Getter private final List<HourRangeInDay> workingHours = new ArrayList<>();
/** 특정요일의 일하는 시간을 표현 (툐요일 반일 근무 등) */
@Getter private final List<DayHourRange> workingDayHours = new ArrayList<>();
public CalendarDateAdd() {
this(TimeCalendar.getEmptyOffset());
}
public CalendarDateAdd(ITimeCalendar calendar) {
shouldNotBeNull(calendar, "calendar");
shouldBe(calendar.getStartOffset().isEqual(ZERO), "Calendar.StartOffset은 Duration.ZERO 이어야 합니다.");
shouldBe(calendar.getEndOffset().isEqual(ZERO), "Calendar.EndOffset은 Duration.ZERO 이어야 합니다.");
this.timeCalendar = calendar;
}
@Override
public ITimePeriodCollection getIncludePeriods() {
throw new NotSupportException("IncludePeriods는 지원하지 않습니다.");
}
/** 주중 (월-금)을 working day로 추가합니다. */
public void addWorkingWeekDays() {
addWeekDays(TimeSpec.Weekdays);
}
/** 주말 (토-일)을 working day로 추가합니다. */
public void addWeekendWeekDays() {
addWeekDays(TimeSpec.Weekends);
}
private void addWeekDays(DayOfWeek... dayOfWeeks) {
Collections.addAll(weekDays, dayOfWeeks);
}
/** start 시각으로부터 offset 기간이 지난 시각을 계산합니다. */
public DateTime add(DateTime start, Duration offset) {
return add(start, offset, SeekBoundaryMode.Next);
}
@Override
public DateTime add(DateTime start, Duration offset, SeekBoundaryMode seekBoundary) {
log.trace("add. start [{}] + offset [{}]의 시각을 계산합니다... seekBoundary=[{}]", start, offset, seekBoundary);
if (getWeekDays().size() == 0 && getExcludePeriods().size() == 0 && getWorkingHours().size() == 0)
return start.plus(offset);
Pair<DateTime, Duration> endPair =
(offset.compareTo(ZERO) < 0)
? calculateEnd(start, Durations.negate(offset), SeekDirection.Backward, seekBoundary)
: calculateEnd(start, offset, SeekDirection.Forward, seekBoundary);
DateTime end = endPair.getV1();
log.trace("add. start [{}] + offset [{}] => end=[{}] seekBoundary=[{}]", start, offset, end, seekBoundary);
return end;
}
@Override
public DateTime subtract(DateTime start, Duration offset, SeekBoundaryMode seekBoundary) {
log.trace("subtract. start [{}] - offset [{}]의 시각을 계산합니다... seekBoundary=[{}]", start, offset, seekBoundary);
if (getWeekDays().size() == 0 && getExcludePeriods().size() == 0 && getWorkingHours().size() == 0)
return start.minus(offset);
Pair<DateTime, Duration> endTuple = (offset.compareTo(ZERO) < 0)
? calculateEnd(start, Durations.negate(offset), SeekDirection.Forward, seekBoundary)
: calculateEnd(start, offset, SeekDirection.Backward, seekBoundary);
DateTime end = endTuple.getV1();
log.trace("subtract. start [{}] - offset [{}] => end=[{}] seekBoundary=[{}]", start, offset, end, seekBoundary);
return end;
}
/**
* 기준 시각으로부터 offset 만큼 떨어진 시각을 구합니다.
*
* @param start 시작 시각
* @param offset 기간
* @param seekDir 탐색 방향
* @param seekBoundary 경계 값 포함 여부
* @return 기준 시각으로터 오프셋만큼 떨어진 시각, 짜투리 시
*/
@Override
protected Pair<DateTime, Duration> calculateEnd(DateTime start, Duration offset, SeekDirection seekDir, SeekBoundaryMode seekBoundary) {
log.trace("기준시각으로부터 offset 기간만큼 떨어진 시각을 구합니다... start=[{}], offset=[{}], seekDir=[{}], seekBoundary=[{}]",
start, offset, seekDir, seekBoundary);
Guard.shouldBe(offset.compareTo(ZERO) >= 0, "offset 값은 0 이상 이어야 합니다. offset=[%d]", offset.getMillis());
DateTime end = null;
DateTime moment = start;
Duration remaining = offset;
WeekRange week = new WeekRange(start, getTimeCalendar());
while (week != null) {
super.getIncludePeriods().clear();
super.getIncludePeriods().addAll(getAvailableWeekPeriods(week));
log.trace("가능한 기간=[{}]", StringTool.listToString(super.getIncludePeriods()));
Pair<DateTime, Duration> result = super.calculateEnd(moment, remaining, seekDir, seekBoundary);
end = result.getV1();
remaining = result.getV2();
log.trace("완료기간을 구했습니다. end=[{}], remaining=[{}]", end, remaining);
if (end != null || remaining == null)
break;
if (seekDir == SeekDirection.Forward) {
week = findNextWeek(week);
if (week != null)
moment = week.getStart();
} else {
week = findPreviousWeek(week);
if (week != null)
moment = week.getEnd();
}
}
log.trace("기준시각으로부터 offset 기간만큼 떨어진 시각을 구했습니다. start=[{}], offset=[{}], seekDir=[{}], seekBoundary=[{}], end=[{}], remaining=[{}]",
start, offset, seekDir, seekBoundary, end, remaining);
return Pair.create(end, remaining);
}
/**
* current 기준으로 예외 기간 등을 고려한 후행의 가장 근접한 WeekRange를 구합니다.
*
* @param current 기준 주(Week)
* @return 다음
*/
private WeekRange findNextWeek(WeekRange current) {
log.trace("현 week[{}]의 이후 week 기간을 구합니다...", current);
WeekRange next = null;
if (getExcludePeriods().size() == 0) {
next = current.nextWeek();
} else {
TimeRange limits = new TimeRange(current.getEnd().plusMillis(1), (DateTime) null);
TimeGapCalculator<TimeRange> gapCalculator = new TimeGapCalculator<>(getTimeCalendar());
ITimePeriodCollection remainingPeriods = gapCalculator.getGaps(getExcludePeriods(), limits);
next = (remainingPeriods.size() > 0)
? new WeekRange(remainingPeriods.get(0).getStart(), getTimeCalendar())
: null;
}
log.trace("현 week[{}]의 이후 week 는 [{}] ", current, next);
return next;
}
/**
* current 기준으로 예외 기간 등을 고려한 선행의 가장 근접한 WeekRange를 구합니다.
*
* @param current 기준 주(Week)
* @return 선행
*/
private WeekRange findPreviousWeek(WeekRange current) {
log.trace("현 week[{}]의 이전 week 기간을 구합니다...", current);
WeekRange previous = null;
if (getExcludePeriods().size() == 0) {
previous = current.previousWeek();
} else {
TimeRange limits = new TimeRange(TimeSpec.MinPeriodTime, current.getStart().plusMillis(-1));
TimeGapCalculator<TimeRange> gapCalculator = new TimeGapCalculator<>(getTimeCalendar());
ITimePeriodCollection remainingPeriods = gapCalculator.getGaps(getExcludePeriods(), limits);
previous = (remainingPeriods.size() > 0)
? new WeekRange(remainingPeriods.get(remainingPeriods.size() - 1).getEnd(), getTimeCalendar())
: null;
}
log.trace("현 week[{}]의 이전 week 는 [{}] ", current, previous);
return previous;
}
/**
* 지정한 기간 내에서 예외 기간등을 제외한 기간들을 HourRange 컬렉션으로 단위로 반환합니다.
*
* @param limits 전체 기간
* @return 제외할 기간을 뺀 기간들
*/
private Iterable<ITimePeriod> getAvailableWeekPeriods(ITimePeriod limits) {
shouldNotBeNull(limits, "limits");
log.trace("가능한 기간을 추출합니다... 전체 기간=[{}]", limits);
if (weekDays.size() == 0 && workingHours.size() == 0 && workingDayHours.size() == 0) {
TimePeriodCollection result = new TimePeriodCollection();
result.add(limits);
return result;
}
CalendarPeriodCollectorFilter filter = new CalendarPeriodCollectorFilter();
filter.getWeekDays().addAll(weekDays);
filter.getCollectingHours().addAll(workingHours);
filter.getCollectingDayHours().addAll(workingDayHours);
CalendarPeriodCollector weekCollector = new CalendarPeriodCollector(filter, limits, SeekDirection.Forward, getTimeCalendar());
weekCollector.collectHours();
return weekCollector.getPeriods();
}
private static final long serialVersionUID = -2499923637191503226L;
}