
org.opentripplanner.model.calendar.openinghours.OHCalendarBuilder Maven / Gradle / Ivy
The newest version!
package org.opentripplanner.model.calendar.openinghours;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.Month;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.opentripplanner.transit.model.framework.Deduplicator;
public class OHCalendarBuilder {
private final Deduplicator deduplicator;
private final LocalDate startOfPeriod;
private final LocalDate endOfPeriod;
private final int daysInPeriod;
private final ZoneId zoneId;
private final List openingHours = new ArrayList<>();
public OHCalendarBuilder(
Deduplicator deduplicator,
LocalDate startOfPeriod,
int daysInPeriod,
ZoneId zoneId
) {
this.deduplicator = deduplicator;
this.startOfPeriod = startOfPeriod;
this.endOfPeriod = startOfPeriod.plusDays(daysInPeriod);
this.daysInPeriod = daysInPeriod;
this.zoneId = zoneId;
}
public OpeningHoursBuilder openingHours(
String periodDescription,
LocalTime startTime,
LocalTime endTime
) {
return new OpeningHoursBuilder(periodDescription, startTime, endTime, false);
}
public OpeningHoursBuilder openingHours(
String periodDescription,
LocalTime startTime,
LocalTime endTime,
boolean isAfterMidnight
) {
return new OpeningHoursBuilder(periodDescription, startTime, endTime, isAfterMidnight);
}
public OHCalendar build() {
// We sort the opening hours for the deduplicator to work a little better and to simplify
// the check can Enter/Exit later. Even if the opening hours are not on the same dates they
// will still be sorted in the right order after day filtering
Collections.sort(openingHours);
return new OHCalendar(
startOfPeriod,
endOfPeriod,
zoneId,
deduplicator.deduplicateImmutableList(OpeningHours.class, openingHours)
);
}
/**
* Record that can be used for builder methods that create new builders that should be returned
* in addition to the original builder.
*/
public record OpeningHoursBuilderAndNewBuilders(
OpeningHoursBuilder originalBuilder,
List newBuilders
) {}
public class OpeningHoursBuilder {
private String periodDescription;
private final LocalTime startTime;
private final LocalTime endTime;
private boolean afterMidnight;
private final BitSet openingDays = new BitSet(daysInPeriod);
public OpeningHoursBuilder(
String periodDescription,
LocalTime startTime,
LocalTime endTime,
boolean afterMidnight
) {
this.periodDescription = periodDescription;
this.startTime = startTime;
this.endTime = endTime;
this.afterMidnight = afterMidnight;
}
public boolean isAfterMidnight() {
return afterMidnight;
}
/**
* @return if the builder has any days set as open
*/
public boolean isEverOn() {
return !openingDays.isEmpty();
}
/**
* Sets the defined date as open if it's within the defined period. If the builder is set be
* for times after midnight, the date is shifted one day forward.
*/
public OpeningHoursBuilder on(LocalDate date) {
var shiftedDate = date.plusDays(afterMidnight ? 1 : 0);
if (shiftedDate.isBefore(startOfPeriod) || shiftedDate.isAfter(endOfPeriod)) {
return this;
}
openingDays.set((int) ChronoUnit.DAYS.between(startOfPeriod, shiftedDate));
return this;
}
/**
* Sets the defined week day to be open on every instance it exists within the defined period.
* If the builder is set be for times after midnight, the weekday is shifted one day forward.
*/
public OpeningHoursBuilder on(DayOfWeek dayOfWeek) {
var shiftedDayOfWeek = dayOfWeek.plus(afterMidnight ? 1 : 0);
// This counts how many days there are in between the startOfPeriod and
// when the specified dayOfWeek occurs for the first time.
int rawWeekDayDifference =
shiftedDayOfWeek.getValue() - startOfPeriod.getDayOfWeek().getValue();
int firstOccurrenceDaysFromStart = rawWeekDayDifference >= 0
? rawWeekDayDifference
: 7 - Math.abs(rawWeekDayDifference);
for (int i = firstOccurrenceDaysFromStart; i < daysInPeriod; i += 7) {
openingDays.set(i);
}
return this;
}
/**
* Sets every weekday in the range to be open on every instance they exist within the defined period.
* The range is inclusive in both ends.
* If the builder is set be for times after midnight, the weekdays are shifted one day forward.
*/
public OpeningHoursBuilder on(DayOfWeek fromDayOfWeek, DayOfWeek untilDayOfWeek) {
if (fromDayOfWeek == null) {
return this;
}
if (untilDayOfWeek == null) {
on(fromDayOfWeek);
return this;
}
int untilAdjusted = fromDayOfWeek.getValue() > untilDayOfWeek.getValue()
? untilDayOfWeek.getValue() + 7
: untilDayOfWeek.getValue();
for (int i = fromDayOfWeek.getValue(); i <= untilAdjusted; i++) {
int dayValue = i > 7 ? i - 7 : i;
on(DayOfWeek.of(dayValue));
}
return this;
}
/**
* Sets every weekday in the range to be open on every instance they exist within the defined period
* on the defined month range. Both ranges are inclusive in both ends.
* If the builder is set be for times after midnight, the days are shifted one day forward
* so that first day of the first month is never on but the first day after the last month can be.
*/
public OpeningHoursBuilder on(
Month fromMonth,
Month untilMonth,
DayOfWeek fromDayOfWeek,
DayOfWeek untilDayOfWeek
) {
if (fromMonth == null || fromDayOfWeek == null) {
return this;
}
Set months = new HashSet<>();
if (untilMonth == null) {
months.add(fromMonth);
} else {
int untilMonthAdjusted = fromMonth.getValue() > untilMonth.getValue()
? untilMonth.getValue() + 12
: untilMonth.getValue();
for (int i = fromMonth.getValue(); i <= untilMonthAdjusted; i++) {
int monthValue = i > 12 ? i - 12 : i;
months.add(Month.of(monthValue));
}
}
Set daysOfWeek = new HashSet<>();
if (untilDayOfWeek == null) {
daysOfWeek.add(fromDayOfWeek);
} else {
int untilDayAdjusted = fromDayOfWeek.getValue() > untilDayOfWeek.getValue()
? untilDayOfWeek.getValue() + 7
: untilDayOfWeek.getValue();
for (int i = fromDayOfWeek.getValue(); i <= untilDayAdjusted; i++) {
int dayValue = i > 7 ? i - 7 : i;
daysOfWeek.add(DayOfWeek.of(dayValue));
}
}
var dateToProcess = afterMidnight ? startOfPeriod.minusDays(1) : startOfPeriod;
int i = 0;
while (i < daysInPeriod) {
if (months.contains(dateToProcess.getMonth())) {
if (daysOfWeek.contains(dateToProcess.getDayOfWeek())) {
openingDays.set(i);
}
dateToProcess = dateToProcess.plusDays(1);
i += 1;
} else {
int daysToSkip =
YearMonth.of(dateToProcess.getYear(), dateToProcess.getMonth()).lengthOfMonth() -
dateToProcess.getDayOfMonth() +
1;
dateToProcess = dateToProcess.plusDays(daysToSkip);
i += daysToSkip;
}
}
return this;
}
/**
* Sets every day to be on.
*/
public OpeningHoursBuilder everyDay() {
openingDays.set(0, daysInPeriod);
return this;
}
/**
* Sets the days that are on in the given {@link OpeningHoursBuilder} to be off in this builder
* and updates this builder's description to reflect that. The provided builder is unmodified.
* If the builder is set be for times after midnight, we check if the previous day is set
* in the provided bitset.
*/
public OpeningHoursBuilder offWithTimeShift(OpeningHoursBuilder otherBuilder) {
BitSet daysOff = otherBuilder.getOpeningDays();
String offDescription = otherBuilder.getPeriodDescription();
if (afterMidnight) {
boolean intersects = false;
// Java doesn't seem to have operations for shifting bits
for (int i = 1; i < daysInPeriod; i++) {
if (openingDays.get(i) && daysOff.get(i - 1)) {
intersects = true;
openingDays.clear(i);
}
if (intersects) {
appendDescription(" except " + offDescription);
}
}
} else {
off(daysOff, offDescription);
}
return this;
}
/**
* Edits this builder and potentially creates one or two new {@link OpeningHoursBuilder} based
* on the provided {@link OpeningHoursBuilder} according to the following rules:
* 1. If time spans or days don't overlap with this builder, do nothing and return 0 new builders.
* 2. if the provided builder covers the whole period from this builder's start time to end time,
* edit this builder to be off on the common days and return 0 new builders.
* 3. if the provided builder covers only the beginning or end part of this builder's opening
* period, edit this builder to be off on the common days and return a new builder that is
* open on those common days for the remaining part not covered by the provided builder.
* 4. if the provided builder covers a period in the middle of this builder's opening period,
* edit this builder to be off on the common days and return two new builders that are open
* on the common days, one for the beginning and one for the end part of this builder's
* opening period
*
* @return a list of new {@link org.opentripplanner.model.calendar.openinghours.OHCalendarBuilder.OpeningHoursBuilder} created while
* splitting existing builders.
*/
public OpeningHoursBuilderAndNewBuilders createBuildersForRelativeComplement(
OpeningHoursBuilder otherBuilder
) {
LocalTime otherStartTime = otherBuilder.getStartTime();
LocalTime otherEndTime = otherBuilder.getEndTime();
if (
otherEndTime.equals(startTime) ||
otherEndTime.isBefore(startTime) ||
endTime.equals(otherStartTime) ||
endTime.isBefore(otherStartTime)
) {
return new OpeningHoursBuilderAndNewBuilders(this, List.of());
}
String offDescription = otherBuilder.getPeriodDescription();
if (
(otherStartTime.isBefore(startTime) || otherStartTime.equals(startTime)) &&
((endTime.isBefore(otherEndTime) || endTime.equals(otherEndTime)))
) {
off(otherBuilder.getOpeningDays(), offDescription);
return new OpeningHoursBuilderAndNewBuilders(this, List.of());
}
BitSet commonDays = this.getCommonDays(otherBuilder);
if (commonDays.isEmpty()) {
return new OpeningHoursBuilderAndNewBuilders(this, List.of());
}
String newDescription = String.format(
"Days overlapping between %s and %s",
getPeriodDescription(),
otherBuilder.getPeriodDescription()
);
if (otherStartTime.equals(startTime) || otherStartTime.isBefore(startTime)) {
var newOpeningHoursBuilder = openingHours(newDescription, otherEndTime, endTime);
newOpeningHoursBuilder.on(commonDays);
off(commonDays, offDescription);
return new OpeningHoursBuilderAndNewBuilders(this, List.of(newOpeningHoursBuilder));
}
if (endTime.equals(otherEndTime) || endTime.isBefore(otherEndTime)) {
var newOpeningHoursBuilder = openingHours(newDescription, startTime, otherStartTime);
newOpeningHoursBuilder.on(commonDays);
off(commonDays, offDescription);
return new OpeningHoursBuilderAndNewBuilders(this, List.of(newOpeningHoursBuilder));
}
var firstNewOpeningHoursBuilder = openingHours(newDescription, startTime, otherStartTime);
firstNewOpeningHoursBuilder.on(commonDays);
var secondNewOpeningHoursBuilder = openingHours(newDescription, otherEndTime, endTime);
secondNewOpeningHoursBuilder.on(commonDays);
off(commonDays, offDescription);
return new OpeningHoursBuilderAndNewBuilders(
this,
List.of(firstNewOpeningHoursBuilder, secondNewOpeningHoursBuilder)
);
}
/**
* Adds the opening hours to the {@link OHCalendar} that is being build here.
*/
public OHCalendarBuilder add() {
var days = deduplicator.deduplicateBitSet(openingDays);
var hours = deduplicator.deduplicateObject(
OpeningHours.class,
new OpeningHours(periodDescription, startTime, endTime, days)
);
openingHours.add(hours);
return OHCalendarBuilder.this;
}
private BitSet getOpeningDays() {
return openingDays;
}
private String getPeriodDescription() {
return periodDescription;
}
private LocalTime getStartTime() {
return startTime;
}
private LocalTime getEndTime() {
return endTime;
}
private BitSet getCommonDays(OpeningHoursBuilder otherBuilder) {
var openingDaysClone = (BitSet) openingDays.clone();
openingDaysClone.and(otherBuilder.getOpeningDays());
return openingDaysClone;
}
/**
* Adds the given description addition to the end of the current description
*/
private void appendDescription(String descriptionAddition) {
periodDescription = periodDescription + descriptionAddition;
}
/**
* Sets the days that are true in the given {@link BitSet} to be on without setting any days off.
* If the builder is set be for times after midnight, the days are not shifted in this case.
*/
private OpeningHoursBuilder on(BitSet days) {
if (days.size() != openingDays.size()) {
return this;
}
openingDays.or(days);
return this;
}
/**
* Sets the days that are true in the given {@link BitSet} to be off.
* If the builder is set be for times after midnight, the days are not shifted in this case.
*/
private OpeningHoursBuilder off(BitSet daysOff, String offDescription) {
if (openingDays.intersects(daysOff)) {
openingDays.andNot(daysOff);
appendDescription(" except " + offDescription);
}
return this;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy