/**
* Copyright 2015 Dhatim
*
* 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 org.dhatim.businesshours;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BinaryOperator;
/**
* A continous period of business opening.
* @author Maxime Suret
*/
public class BusinessPeriod {
private final BusinessTemporal start;
private final BusinessTemporal end;
/**
* Builds a new instance of {@link BusinessPeriod}.
* @param start when the period opens
* @param end when the period closes
*/
public BusinessPeriod(BusinessTemporal start, BusinessTemporal end) {
this.start = Objects.requireNonNull(start);
this.end = Objects.requireNonNull(end);
}
/**
* Tells if this period is always open.
* @return true if the business is always open, false otherwise
*/
public boolean alwaysOpen() {
return end.increment().equals(start);
}
/**
* Tells if the given temporal lies within this period.
* @param temporal the temporal
* @return true if the temporal is included in this period, false otherwise.
*/
public boolean isInPeriod(Temporal temporal) {
return start.compareTo(temporal) <= 0 && end.compareTo(temporal) >= 0;
}
/**
* Gives the amount of time between the given temporal and the next opening of this period.
* @param temporal the temporal
* @param unit the unit in which the result must be given
* @return {@link Long#MAX_VALUE} if the business is always open, else the amount of time expressed in <code>unit</code>.
*/
public long timeBeforeOpening(Temporal temporal, ChronoUnit unit) {
return alwaysOpen() ? Long.MAX_VALUE : start.since(temporal, unit);
}
/**
* Get a {@link CronExpression} that triggers at each period opening.
* e.g. if the period is 9am-18pm, the result will be <code>0 9 * * *</code>
* @return <code>null</code> if the period is always open, else the cron expression
*/
public CronExpression getStartCron() {
return alwaysOpen() ? null : new CronExpression(start);
}
/**
* Get the opening time of this period.
* @return when this period opens
*/
public BusinessTemporal getStart() {
return start;
}
/**
* Get the closing time of this period.
* @return when this period closes
*/
public BusinessTemporal getEnd() {
return end;
}
/** {@inheritDoc} */
@Override
public int hashCode() {
int hash = 5;
hash = 59 * hash + Objects.hashCode(this.start);
hash = 59 * hash + Objects.hashCode(this.end);
return hash;
}
/**
* Tells if this business period is equals to the given one.
* Business periods are equals if they are open at exactly the same times.
* @param obj the object to comapre this period to
* @return true if the period are equals, false otherwise.
*/
@Override
public boolean equals(Object obj) {
return Optional
.ofNullable(obj)
.filter(BusinessPeriod.class::isInstance)
.filter(other -> start.equals(((BusinessPeriod) other).start))
.filter(other -> end.equals(((BusinessPeriod) other).end))
.isPresent();
}
/**
* Merge intersecting or adjacent periods.
* @param periods the periods to merge
* @return the merged periods.
* Their opening time spans will be exactly the same as the input periods.
*/
public static Set<BusinessPeriod> merge(Collection<BusinessPeriod> periods) {
//sort the periods by start date
List<BusinessPeriod> sortedPeriods = new ArrayList<>(periods);
Collections.sort(sortedPeriods, Comparator.comparing(BusinessPeriod::getStart));
Set<BusinessPeriod> mergedPeriods = new HashSet<>(periods.size());
BusinessPeriod currentPeriod = null;
for (BusinessPeriod period : sortedPeriods) {
if (currentPeriod == null) {
currentPeriod = period;
} else {
//check if the start of the period is included in the previous period
if (currentPeriod.isInPeriod(period.getStart())) {
currentPeriod = new BusinessPeriod(
currentPeriod.getStart(),
BinaryOperator.maxBy(Comparator.<BusinessTemporal>naturalOrder()).apply(currentPeriod.getEnd(), period.getEnd()));
} else if (currentPeriod.getEnd().increment().equals(period.getStart())) {
//check if the two periods are contiguous
currentPeriod = new BusinessPeriod(currentPeriod.getStart(), period.getEnd());
} else {
//the periods can not be merged
mergedPeriods.add(currentPeriod);
currentPeriod = period;
}
}
}
Optional
.ofNullable(currentPeriod)
.ifPresent(mergedPeriods::add);
return mergedPeriods;
}
}