/**
* 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.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
* A representation of business hours.
* <p>
* The business hours are specified as a string which adheres to the format:
*
* <pre>sub-period[, sub-period...]</pre>
*
* If the period is blank, then the business supposed to be always open.
* <br>
* A sub-period is of the form:
* <pre> scale {range [range ...]} [scale {range [range ...]}]</pre> Scale must
* be one of three different scales (or their equivalent codes):
* <table summary="valid period scales">
* <tr>
* <th>Scale</th>
* <th>Scale code</th>
* <th>Valid range values</th>
* </tr>
* <tr>
* <td>wday</td>
* <td>wd</td>
* <td>1 (Monday) to 7 (Sunday) or mo, tu, we, th, fr, sa, su</td>
* </tr>
* <tr>
* <td>hour</td>
* <td>hr</td>
* <td>0-23 or 12am 1am-11am 12noon 12pm 1pm-11pm</td>
* </tr>
* <tr>
* <td>minute</td>
* <td>min</td>
* <td>0-59</td>
* </tr>
* </table>
*
* The same scale type may be specified multiple times. Additional scales simply
* extend the range defined by previous scales of the same type.
* <br>
* The range for a given scale must be a valid value in the form of
* <code>v</code> or <code>v-v</code>.
* <br>
* For the range specification <code>v-v</code>, if the second value is larger
* than the first value, the range wraps around.
* <br>
* <code>v</code> isn't a point in time. In the context of the hour scale, 9
* specifies the time period from 9:00 am to 9:59. This is what most people
* would call 9-10.
* <p>
* Note that whitespaces can be anywhere. Furthermore, when using letters to
* specify week days, only the first two are significant and the case is not
* important: <code>Sunday</code> or <code>Sun</code> are valid specifications
* for <code>su</code>.
*
* <h3>Examples:</h3>
*
* To specify business hours that go from Monday through Friday, 9am to 5pm:
*
* <pre>wd {Mon-Fri} hr {9am-4pm}</pre>
*
* To specify business hours that go from Monday through Friday, 9am to 5pm on
* Monday, Wednesday, and Friday, and 9am to 3pm on Tuesday and Thursday, use a
* period such as:
*
* <pre>wd {Mon Wed Fri} hr {9am-4pm}, wd{Tue Thu} hr {9am-2pm}</pre>
*
* To specify business hours open every other half-hour, use something like:
* <pre>minute { 0-29 }</pre>
*
* To specify the morning, use:
*
* <pre>hour { 12am-11am }</pre>
*
* Remember, 11am is not 11:00am, but rather 11:00am - 11:59am.
*
* @author Maxime Suret
*/
public class BusinessHours {
private final String stringValue;
private final Set<BusinessPeriod> periods;
/**
* Build a new instance of BusinessHours from its string representation.
*
* @param stringValue the string representation of the business hours. See
* the class level Javadoc for more info on valid formats.
*/
public BusinessHours(String stringValue) {
this.stringValue = stringValue;
this.periods = new HashSet<>(BusinessHoursParser.parse(stringValue));
}
/**
* Tells if the business is open at the given time.
*
* @param temporal the time when we want to know if the business is open or
* closed.
* @return true if the business is open at the given time, false otherwise
*/
public boolean isOpen(Temporal temporal) {
return periods
.stream()
.anyMatch(period -> period.isInPeriod(temporal));
}
/**
* Get the time between the given temporal and the next business opening.
*
* @param temporal the temporal from which to compute the time before next
* opening
* @param unit the unit in which the result must be stated
* @return the duration,, or LONG.MAX_VALUE if the business is always open
*/
public long timeBeforeOpening(Temporal temporal, ChronoUnit unit) {
return periods
.stream()
.mapToLong(period -> period.timeBeforeOpening(temporal, unit))
.min()
.getAsLong();
}
/**
* Get a set of crons corresponding to each opening. The set is empty if the
* business is always open.
*
* @return the cron set
*/
public Set<String> getOpeningCrons() {
//get the start crons of all periods and merge them
return CronExpression
.merge(
periods
.stream()
.map(BusinessPeriod::getStartCron)
.filter(Objects::nonNull)
.collect(Collectors.toSet()))
.stream()
.map(CronExpression::toString)
.collect(Collectors.toSet());
}
/**
* Returns a hash code for this BusinessHours.
*
* @return a suitable hash code
*/
@Override
public int hashCode() {
return periods.hashCode();
}
/**
* Tells if these business hours are equals to the given ones.
* <p>
* Business hours are equals if they are open at exactly the
* same instants (regardless of the string representation used to build
* them).
*
* @param obj the other BusinessHours, null returns false
* @return true if the other BusinessHours is equals to this one
*/
@Override
public boolean equals(Object obj) {
return Optional
.ofNullable(obj)
.filter(BusinessHours.class::isInstance)
.filter(other -> periods.equals(((BusinessHours) other).periods))
.isPresent();
}
/**
* Return a string representation of this BusinessHours instance.
* <p>
* It is the same String as the one used to build this instance.
*
* @return the string representation of this BusinessHours instance.
*/
@Override
public String toString() {
return stringValue;
}
}