/* * 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; import kr.debop4j.timeperiod.tools.TimeSpec; import kr.debop4j.timeperiod.tools.Times; import lombok.extern.slf4j.Slf4j; import org.joda.time.DateTime; import org.joda.time.Duration; import java.util.Arrays; import static kr.debop4j.core.Guard.*; /** * {@link ITimePeriod}를 시간의 흐름 순으로 Chain (Linked List) 형태로 표현한 클래스입니다. * * @author 배성혁 sunghyouk.bae@gmail.com * @since 13. 5. 17. 오후 4:17 */ @Slf4j public class TimePeriodChain extends TimePeriodContainer implements ITimePeriodChain { private static final long serialVersionUID = -5838724440389574448L; public TimePeriodChain() {} public TimePeriodChain(ITimePeriod... periods) { if (periods != null) addAll(Arrays.asList(periods)); } public TimePeriodChain(Iterable<? extends ITimePeriod> collection) { if (collection != null) addAll(collection); } @Override public DateTime getStart() { return (getFirst() != null) ? getFirst().getStart() : TimeSpec.MinPeriodTime; } /** period chain의 시작 시각을 지정합니다. */ @Override public void setStart(DateTime value) { if (getFirst() != null) move(new Duration(getStart(), value)); } @Override public DateTime getEnd() { return (getLast() != null) ? getLast().getEnd() : TimeSpec.MaxPeriodTime; } @Override public void setEnd(DateTime value) { if (getLast() != null) move(new Duration(getEnd(), value)); } /** * Period 체인의 첫번째 요소, 없으면 null을 반환합니다. * * @return */ @Override public ITimePeriod getFirst() { return (size() > 0) ? getPeriods().get(0) : null; } /** * period chain의 마지막 요소, 없으면 null을 반환합니다. * * @return */ @Override public ITimePeriod getLast() { return (size() > 0) ? getPeriods().get(size() - 1) : null; } @Override public ITimePeriod set(int index, ITimePeriod element) { shouldBeInRange(index, 0, size(), "index"); remove(index); add(index, element); return element; } /** * 기간을 추가합니다. * * @param period 추가할 기간 * @return 추가 여부 */ @Override public boolean add(ITimePeriod period) { shouldNotBeNull(period, "period"); Times.assertMutable(period); ITimePeriod last = getLast(); if (last != null) { assertSpaceAfter(last.getEnd(), period.getDuration()); period.setup(last.getEnd(), last.getEnd().plus(period.getDuration())); } log.trace("Period chain의 끝에 추가합니다. period=[{}]", period); return getPeriods().add(period); } /** * 기간들을 모두 추가합니다. * * @param periods 추가할 기간들 */ @Override public void addAll(final Iterable<? extends ITimePeriod> periods) { shouldNotBeNull(periods, "periods"); for (ITimePeriod period : periods) add(period); } /** * {@link ITimePeriod}의 Chain의 index 번째에 item을 삽입합니다. 선행 Period와 후행 Period의 기간 값이 조정됩니다. * * @param index 삽입할 순서 * @param item 삽입할 요소 */ @Override public void add(int index, ITimePeriod item) { shouldNotBeNull(item, "item"); shouldBeInRange(index, 0, size(), "index"); Times.assertMutable(item); log.trace("Chain의 인덱스[{}]에 새로운 요소[{}]를 삽입합니다...", index, item); Duration itemDuration = item.getDuration(); ITimePeriod prevItem = null; ITimePeriod nextItem = null; if (size() > 0) { log.trace("시간적 삽입 공간이 존재하는지 검사합니다..."); if (index > 0) { prevItem = get(index - 1); assertSpaceAfter(getEnd(), itemDuration); } if (index < size() - 1) { nextItem = get(index); assertSpaceBefore(getStart(), itemDuration); } } getPeriods().add(index, item); if (prevItem != null) { log.trace("선행 period에 기초하여 삽입한 period와 후행 period들의 시간을 조정합니다..."); item.setup(prevItem.getEnd(), prevItem.getEnd().plus(itemDuration)); for (int i = index + 1; i < size(); i++) { ITimePeriod p = get(i); DateTime startTime = p.getStart().plus(itemDuration); p.setup(startTime, startTime.plus(p.getDuration())); } } if (nextItem != null) { log.trace("후행 period에 기초하여 삽입한 period와 선행 period들의 시간을 조정합니다..."); DateTime nextStart = nextItem.getStart().minus(itemDuration); item.setup(nextStart, nextStart.plus(itemDuration)); for (int i = 0; i < index - 1; i++) { ITimePeriod p = get(i); nextStart = p.getStart().minus(itemDuration); p.setup(nextStart, nextStart.plus(p.getDuration())); } } } /** 지정한 요소를 제거하고, 후속 ITimePeriod 들의 기간을 재조정합니다. (앞으로 당깁니다) */ @Override public boolean remove(Object o) { shouldNotBeNull(o, "o"); shouldBe(o instanceof ITimePeriod, "o is not ITimePeriod type. class=[%s]", o.getClass()); if (size() <= 0) return false; ITimePeriod item = (ITimePeriod) o; log.trace("요소 [{}]를 컬렉션에서 제거합니다...", item); Duration itemDuration = item.getDuration(); int index = indexOf(item); ITimePeriod next = null; if (itemDuration.getMillis() > 0 && index >= 0 && index < size() - 1) next = get(index); boolean removed = getPeriods().remove(item); if (removed && next != null) { log.trace("요소[{}]를 제거하고, chain의 후속 periods 들의 기간을 조정합니다...", item); for (int i = index; i < size(); i++) { DateTime start = get(i).getStart().minus(itemDuration); get(i).setup(start, start.plus(get(i).getDuration())); } } log.trace("요소[{}]를 제거한 결과=[{}]", item, removed); return removed; } /** 지정한 요소를 제거하고, 후속 ITimePeriod 들의 기간을 재조정합니다. (앞으로 당깁니다) */ @Override public ITimePeriod remove(int index) { shouldBeInRange(index, 0, size(), "index"); ITimePeriod removed = get(index); return remove(removed) ? removed : null; } /** moment 이전에 duration 만큼의 시간적 공간이 있는지 여부 (새로운 기간을 추가하기 위해서는 공간이 필요합니다) */ protected void assertSpaceBefore(DateTime moment, Duration duration) { boolean hasSpace = moment != TimeSpec.MinPeriodTime; if (hasSpace) { Duration remaining = new Duration(TimeSpec.MinPeriodTime, moment); hasSpace = duration.compareTo(remaining) <= 0; } shouldBe(hasSpace, "duration [%s] is out of range.", duration); } /** moment 이후에 duration 만큼의 시간적 공간이 있는지 여부 (새로운 기간을 추가하기 위해서는 공간이 필요합니다) */ protected void assertSpaceAfter(DateTime moment, Duration duration) { boolean hasSpace = moment != TimeSpec.MaxPeriodTime; if (hasSpace) { Duration remaining = new Duration(moment, TimeSpec.MaxPeriodTime); hasSpace = duration.compareTo(remaining) <= 0; } shouldBe(hasSpace, "duration [%s] is out of range.", duration); } }