diff --git a/pom.xml b/pom.xml
index 2f007cbb214..941807c63e8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -912,6 +912,11 @@
OpeningHoursParser
0.28.2
+
+ io.leonard
+ opening-hours-evaluator
+ 1.3.0
+
diff --git a/src/main/java/io/leonard/OpeningHoursEvaluator.java b/src/main/java/io/leonard/OpeningHoursEvaluator.java
new file mode 100644
index 00000000000..d2e5786c148
--- /dev/null
+++ b/src/main/java/io/leonard/OpeningHoursEvaluator.java
@@ -0,0 +1,241 @@
+package io.leonard;
+
+import static ch.poole.openinghoursparser.RuleModifier.Modifier.*;
+
+import ch.poole.openinghoursparser.*;
+import java.time.DayOfWeek;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.*;
+import java.util.stream.Stream;
+
+public class OpeningHoursEvaluator {
+
+ private static final Set CLOSED_MODIFIERS = Set.of(CLOSED, OFF);
+ private static final Set OPEN_MODIFIERS = Set.of(OPEN, UNKNOWN);
+ private static final Map weekDayToDayOfWeek = Map.of(
+ WeekDay.MO,
+ DayOfWeek.MONDAY,
+ WeekDay.TU,
+ DayOfWeek.TUESDAY,
+ WeekDay.WE,
+ DayOfWeek.WEDNESDAY,
+ WeekDay.TH,
+ DayOfWeek.THURSDAY,
+ WeekDay.FR,
+ DayOfWeek.FRIDAY,
+ WeekDay.SA,
+ DayOfWeek.SATURDAY,
+ WeekDay.SU,
+ DayOfWeek.SUNDAY
+ );
+
+ // when calculating the next time the hours are open, how many days should you go into the future
+ // this protects against stack overflows when the place is never going to open again
+ private static final int MAX_SEARCH_DAYS = 365 * 10;
+
+ public static boolean isOpenAt(LocalDateTime time, List rules) {
+ var closed = getClosedRules(rules);
+ var open = getOpenRules(rules);
+ return (
+ closed.noneMatch(rule -> timeMatchesRule(time, rule)) &&
+ open.anyMatch(rule -> rule.isTwentyfourseven() || timeMatchesRule(time, rule))
+ );
+ }
+
+ /**
+ * @return LocalDateTime in Optional, representing next closing time ; or empty Optional if place
+ * is either closed at time or never closed at all.
+ */
+ public static Optional isOpenUntil(LocalDateTime time, List rules) {
+ var closed = getClosedRules(rules);
+ var open = getOpenRules(rules);
+ if (closed.anyMatch(rule -> timeMatchesRule(time, rule))) return Optional.empty();
+ return getTimeRangesOnThatDay(time, open)
+ .filter(r -> r.surrounds(time.toLocalTime()))
+ .findFirst()
+ .map(r -> time.toLocalDate().atTime(r.end));
+ }
+
+ public static Optional wasLastOpen(LocalDateTime time, List rules) {
+ return isOpenIterative(time, rules, false, MAX_SEARCH_DAYS);
+ }
+
+ public static Optional wasLastOpen(
+ LocalDateTime time,
+ List rules,
+ int searchDays
+ ) {
+ return isOpenIterative(time, rules, false, searchDays);
+ }
+
+ public static Optional isOpenNext(LocalDateTime time, List rules) {
+ return isOpenIterative(time, rules, true, MAX_SEARCH_DAYS);
+ }
+
+ public static Optional isOpenNext(
+ LocalDateTime time,
+ List rules,
+ int searchDays
+ ) {
+ return isOpenIterative(time, rules, true, searchDays);
+ }
+
+ /**
+ * This is private function, this doc-string means only help onboard new devs.
+ *
+ * @param initialTime Starting point in time to search from.
+ * @param rules From parser
+ * @param forward Whether to search in future (true)? or in the past(false)?
+ * @param searchDays Limit search scope in days.
+ * @return an Optional LocalDateTime
+ */
+ private static Optional isOpenIterative(
+ final LocalDateTime initialTime,
+ final List rules,
+ boolean forward,
+ final int searchDays
+ ) {
+ var nextTime = initialTime;
+ for (var iterations = 0; iterations <= searchDays; ++iterations) {
+ var open = getOpenRules(rules);
+ var closed = getClosedRules(rules);
+
+ var time = nextTime;
+ if (isOpenAt(time, rules)) return Optional.of(time); else {
+ var openRangesOnThatDay = getTimeRangesOnThatDay(time, open);
+ var closedRangesThatDay = getTimeRangesOnThatDay(time, closed);
+
+ var endOfExclusion = closedRangesThatDay
+ .filter(r -> r.surrounds(time.toLocalTime()))
+ .findFirst()
+ .map(r -> time.toLocalDate().atTime(forward ? r.end : r.start));
+
+ var startOfNextOpening = forward
+ ? openRangesOnThatDay
+ .filter(range -> range.start.isAfter(time.toLocalTime()))
+ .min(TimeRange.startComparator)
+ .map(timeRange -> time.toLocalDate().atTime(timeRange.start))
+ : openRangesOnThatDay
+ .filter(range -> range.end.isBefore(time.toLocalTime()))
+ .max(TimeRange.endComparator)
+ .map(timeRange -> time.toLocalDate().atTime(timeRange.end));
+
+ var opensNextThatDay = endOfExclusion.or(() -> startOfNextOpening);
+ if (opensNextThatDay.isPresent()) {
+ return opensNextThatDay;
+ }
+
+ // if we cannot find time on the same day when the POI is open, we skip forward to the start
+ // of the following day and try again
+ nextTime =
+ forward
+ ? time.toLocalDate().plusDays(1).atStartOfDay()
+ : time.toLocalDate().minusDays(1).atTime(LocalTime.MAX);
+ }
+ }
+
+ return Optional.empty();
+ }
+
+ private static Stream getTimeRangesOnThatDay(
+ LocalDateTime time,
+ Stream ruleStream
+ ) {
+ return ruleStream
+ .filter(rule -> timeMatchesDayRanges(time, rule.getDays()))
+ .filter(r -> !Objects.isNull(r.getTimes()))
+ .flatMap(r -> r.getTimes().stream().map(TimeRange::new));
+ }
+
+ private static Stream getOpenRules(List rules) {
+ return rules
+ .stream()
+ .filter(r -> {
+ var modifier = r.getModifier();
+ return modifier == null || OPEN_MODIFIERS.contains(modifier.getModifier());
+ });
+ }
+
+ private static Stream getClosedRules(List rules) {
+ return rules
+ .stream()
+ .filter(r -> {
+ var modifier = r.getModifier();
+ return modifier != null && CLOSED_MODIFIERS.contains(modifier.getModifier());
+ });
+ }
+
+ private static boolean timeMatchesRule(LocalDateTime time, Rule rule) {
+ return (
+ (
+ timeMatchesDayRanges(time, rule.getDays()) ||
+ rule.getDays() == null &&
+ dateMatchesDateRanges(time, rule.getDates())
+ ) &&
+ nullToEntireDay(rule.getTimes())
+ .stream()
+ .anyMatch(timeSpan -> timeMatchesHours(time, timeSpan))
+ );
+ }
+
+ private static boolean timeMatchesDayRanges(LocalDateTime time, List ranges) {
+ return nullToEmptyList(ranges).stream().anyMatch(dayRange -> timeMatchesDay(time, dayRange));
+ }
+
+ private static boolean timeMatchesDay(LocalDateTime time, WeekDayRange range) {
+ // if the end day is null it means that it's just a single day like in "Th
+ // 10:00-18:00"
+ if (range.getEndDay() == null) {
+ return time.getDayOfWeek().equals(weekDayToDayOfWeek.getOrDefault(range.getStartDay(), null));
+ }
+ int ordinal = time.getDayOfWeek().ordinal();
+ return range.getStartDay().ordinal() <= ordinal && range.getEndDay().ordinal() >= ordinal;
+ }
+
+ private static boolean dateMatchesDateRanges(LocalDateTime time, List ranges) {
+ return nullToEmptyList(ranges)
+ .stream()
+ .anyMatch(dateRange -> dateMatchesDateRange(time, dateRange));
+ }
+
+ private static boolean dateMatchesDateRange(LocalDateTime time, DateRange range) {
+ // if the end date is null it means that it's just a single date like in "2020 Aug 11"
+ DateWithOffset startDate = range.getStartDate();
+ boolean afterStartDate =
+ time.getYear() >= startDate.getYear() &&
+ time.getMonth().ordinal() >= startDate.getMonth().ordinal() &&
+ time.getDayOfMonth() >= startDate.getDay();
+ if (range.getEndDate() == null) {
+ return afterStartDate;
+ }
+ DateWithOffset endDate = range.getEndDate();
+ boolean beforeEndDate =
+ time.getYear() <= endDate.getYear() &&
+ time.getMonth().ordinal() <= endDate.getMonth().ordinal() &&
+ time.getDayOfMonth() <= endDate.getDay();
+ return beforeEndDate;
+ }
+
+ private static boolean timeMatchesHours(LocalDateTime time, TimeSpan timeSpan) {
+ var minutesAfterMidnight = minutesAfterMidnight(time.toLocalTime());
+ return timeSpan.getStart() <= minutesAfterMidnight && timeSpan.getEnd() >= minutesAfterMidnight;
+ }
+
+ private static int minutesAfterMidnight(LocalTime time) {
+ return time.getHour() * 60 + time.getMinute();
+ }
+
+ private static List nullToEmptyList(List list) {
+ if (list == null) return Collections.emptyList(); else return list;
+ }
+
+ private static List nullToEntireDay(List span) {
+ if (span == null) {
+ var allDay = new TimeSpan();
+ allDay.setStart(TimeSpan.MIN_TIME);
+ allDay.setEnd(TimeSpan.MAX_TIME);
+ return List.of(allDay);
+ } else return span;
+ }
+}
diff --git a/src/main/java/io/leonard/TimeRange.java b/src/main/java/io/leonard/TimeRange.java
new file mode 100644
index 00000000000..61141c2ad12
--- /dev/null
+++ b/src/main/java/io/leonard/TimeRange.java
@@ -0,0 +1,29 @@
+package io.leonard;
+
+import ch.poole.openinghoursparser.TimeSpan;
+import java.time.LocalTime;
+import java.util.Comparator;
+
+public class TimeRange {
+
+ public final LocalTime start;
+ public final LocalTime end;
+
+ public TimeRange(TimeSpan span) {
+ this.start = LocalTime.ofSecondOfDay(span.getStart() * 60L);
+ this.end =
+ LocalTime.ofSecondOfDay(Math.min(span.getEnd() * 60L, LocalTime.MAX.toSecondOfDay()));
+ }
+
+ public boolean surrounds(LocalTime time) {
+ return time.isAfter(start) && time.isBefore(end);
+ }
+
+ public static Comparator startComparator = Comparator.comparing(timeRange ->
+ timeRange.start
+ );
+
+ public static Comparator endComparator = Comparator.comparing(timeRange ->
+ timeRange.end
+ );
+}
diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmDatabase.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmDatabase.java
index df40e2cb647..36991c43b96 100644
--- a/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmDatabase.java
+++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmDatabase.java
@@ -1,5 +1,6 @@
package org.opentripplanner.graph_builder.module.osm;
+import ch.poole.openinghoursparser.OpeningHoursParseException;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Iterables;
@@ -45,6 +46,7 @@
import org.opentripplanner.openstreetmap.model.OSMTag;
import org.opentripplanner.openstreetmap.model.OSMWay;
import org.opentripplanner.openstreetmap.model.OSMWithTags;
+import org.opentripplanner.street.model.OHRulesRestriction;
import org.opentripplanner.street.model.RepeatingTimePeriod;
import org.opentripplanner.street.model.StreetTraversalPermission;
import org.opentripplanner.street.model.TurnRestrictionType;
@@ -886,13 +888,16 @@ private void processRestriction(OSMRelation relation) {
}
TurnRestrictionTag tag;
- if (relation.isTag("restriction", "no_right_turn")) {
+ String restriction = relation.hasTag("restriction")
+ ? relation.getTag("restriction")
+ : relation.getConditionalTag("restriction:conditional");
+ if ("no_right_turn".equals(restriction)) {
tag =
new TurnRestrictionTag(via, TurnRestrictionType.NO_TURN, Direction.RIGHT, relation.getId());
- } else if (relation.isTag("restriction", "no_left_turn")) {
+ } else if ("no_left_turn".equals(restriction)) {
tag =
new TurnRestrictionTag(via, TurnRestrictionType.NO_TURN, Direction.LEFT, relation.getId());
- } else if (relation.isTag("restriction", "no_straight_on")) {
+ } else if ("no_straight_on".equals(restriction)) {
tag =
new TurnRestrictionTag(
via,
@@ -900,9 +905,9 @@ private void processRestriction(OSMRelation relation) {
Direction.STRAIGHT,
relation.getId()
);
- } else if (relation.isTag("restriction", "no_u_turn")) {
+ } else if ("no_u_turn".equals(restriction)) {
tag = new TurnRestrictionTag(via, TurnRestrictionType.NO_TURN, Direction.U, relation.getId());
- } else if (relation.isTag("restriction", "only_straight_on")) {
+ } else if ("only_straight_on".equals(restriction)) {
tag =
new TurnRestrictionTag(
via,
@@ -910,7 +915,7 @@ private void processRestriction(OSMRelation relation) {
Direction.STRAIGHT,
relation.getId()
);
- } else if (relation.isTag("restriction", "only_right_turn")) {
+ } else if ("only_right_turn".equals(restriction)) {
tag =
new TurnRestrictionTag(
via,
@@ -918,7 +923,7 @@ private void processRestriction(OSMRelation relation) {
Direction.RIGHT,
relation.getId()
);
- } else if (relation.isTag("restriction", "only_left_turn")) {
+ } else if ("only_left_turn".equals(restriction)) {
tag =
new TurnRestrictionTag(
via,
@@ -926,7 +931,7 @@ private void processRestriction(OSMRelation relation) {
Direction.LEFT,
relation.getId()
);
- } else if (relation.isTag("restriction", "only_u_turn")) {
+ } else if ("only_u_turn".equals(restriction)) {
tag =
new TurnRestrictionTag(via, TurnRestrictionType.ONLY_TURN, Direction.U, relation.getId());
} else {
@@ -936,22 +941,34 @@ private void processRestriction(OSMRelation relation) {
tag.modes = modes.clone();
// set the time periods for this restriction, if applicable
- if (
+ if (relation.hasTag("restriction:conditional")) {
+ String tagWithCondition = relation.getTag("restriction:conditional");
+ try {
+ OHRulesRestriction.parseFromCondition(
+ tagWithCondition,
+ relation.getOsmProvider()::getZoneId
+ );
+ } catch (OpeningHoursParseException e) {
+ LOG.info("Unparseable conditional turn restriction: {}", relation.getId());
+ }
+ } else if (
relation.hasTag("day_on") &&
relation.hasTag("day_off") &&
relation.hasTag("hour_on") &&
relation.hasTag("hour_off")
) {
+ // TODO tagging schemes day_on/day_off(hour_on/hour_off is deprecated and should be converted
+ // to restriction:conditional
try {
tag.time =
- RepeatingTimePeriod.parseFromOsmTurnRestriction(
+ OHRulesRestriction.parseFromOsmTurnRestriction(
relation.getTag("day_on"),
relation.getTag("day_off"),
relation.getTag("hour_on"),
relation.getTag("hour_off"),
relation.getOsmProvider()::getZoneId
);
- } catch (NumberFormatException e) {
+ } catch (OpeningHoursParseException e) {
LOG.info("Unparseable turn restriction: {}", relation.getId());
}
}
diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/TurnRestrictionTag.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/TurnRestrictionTag.java
index 8bd1464667c..82e5e736367 100644
--- a/src/main/java/org/opentripplanner/graph_builder/module/osm/TurnRestrictionTag.java
+++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/TurnRestrictionTag.java
@@ -2,7 +2,7 @@
import java.util.ArrayList;
import java.util.List;
-import org.opentripplanner.street.model.RepeatingTimePeriod;
+import org.opentripplanner.street.model.TimeRestriction;
import org.opentripplanner.street.model.TurnRestrictionType;
import org.opentripplanner.street.model.edge.StreetEdge;
import org.opentripplanner.street.search.TraverseModeSet;
@@ -18,7 +18,7 @@ class TurnRestrictionTag {
long relationOSMID;
TurnRestrictionType type;
Direction direction;
- RepeatingTimePeriod time;
+ TimeRestriction time;
public List possibleFrom = new ArrayList<>();
public List possibleTo = new ArrayList<>();
public TraverseModeSet modes;
diff --git a/src/main/java/org/opentripplanner/openstreetmap/model/OSMWithTags.java b/src/main/java/org/opentripplanner/openstreetmap/model/OSMWithTags.java
index b53739bf6a1..ab96f011b82 100644
--- a/src/main/java/org/opentripplanner/openstreetmap/model/OSMWithTags.java
+++ b/src/main/java/org/opentripplanner/openstreetmap/model/OSMWithTags.java
@@ -195,6 +195,20 @@ public String getTag(String tag) {
return null;
}
+ /** @return a conditional tag's (tag with ':conditional' appended) restriction value
+ * (value before the first @, without condition).
+ * Note: a conditional tag might have multiple conditions, which is not yet supported. */
+ @Nullable
+ public String getConditionalTag(String tag) {
+ tag = tag.toLowerCase();
+ if (tags != null && tags.containsKey(tag)) {
+ String conditionalValue = tags.get(tag);
+ int indexOfAt = conditionalValue.indexOf('@');
+ return indexOfAt > 0 ? conditionalValue.substring(0, indexOfAt).trim() : conditionalValue;
+ }
+ return null;
+ }
+
/**
*
* @return A tags value converted to lower case. An empty Optional if tags is not present.
diff --git a/src/main/java/org/opentripplanner/street/model/OHRulesRestriction.java b/src/main/java/org/opentripplanner/street/model/OHRulesRestriction.java
new file mode 100644
index 00000000000..64bbd656c30
--- /dev/null
+++ b/src/main/java/org/opentripplanner/street/model/OHRulesRestriction.java
@@ -0,0 +1,95 @@
+package org.opentripplanner.street.model;
+
+import ch.poole.openinghoursparser.OpeningHoursParseException;
+import ch.poole.openinghoursparser.OpeningHoursParser;
+import ch.poole.openinghoursparser.Rule;
+import io.leonard.OpeningHoursEvaluator;
+import java.io.ByteArrayInputStream;
+import java.io.Serializable;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.List;
+import java.util.function.Supplier;
+import javax.annotation.Nullable;
+
+/**
+ * OHRulesRestriction
represents timezone aware OSM opening hours rules
+ * to represent temporal restrictions.
+ */
+public class OHRulesRestriction implements Serializable, TimeRestriction {
+
+ /**
+ * The timezone this is represented in.
+ */
+ private final ZoneId timeZone;
+ /**
+ * The opening hours rules as parsed by OpeningHoursParser.
+ */
+ private final List rules;
+ /**
+ * Should the time restriction be reversed (in case the condition's restriction value is "none")
+ */
+ private final boolean inverse;
+
+ private OHRulesRestriction(ZoneId timeZone, List rules, boolean inverse) {
+ this.timeZone = timeZone;
+ this.rules = rules;
+ this.inverse = inverse;
+ }
+
+ /**
+ * Parse the time specification from an OSM turn restriction
+ */
+ public static TimeRestriction parseFromOsmTurnRestriction(
+ String day_on,
+ String day_off,
+ String hour_on,
+ String hour_off,
+ Supplier timeZoneSupplier
+ ) throws OpeningHoursParseException {
+ return parseFromCondition(
+ day_on.substring(0, 3) + " - " + day_off.substring(0, 3) + " " + hour_on + "-" + hour_off,
+ timeZoneSupplier
+ );
+ }
+
+ /**
+ *
+ * @param tagValueWithCondition
+ * @param timeZoneSupplier
+ * @return
+ * @throws OpeningHoursParseException
+ */
+ public static TimeRestriction parseFromCondition(
+ String tagValueWithCondition,
+ Supplier timeZoneSupplier
+ ) throws OpeningHoursParseException {
+ ZoneId timeZone = timeZoneSupplier.get();
+ if (timeZone == null) {
+ return null;
+ }
+ int indexOfAt = tagValueWithCondition.indexOf('@');
+ String temporalCondition = indexOfAt > 0
+ ? tagValueWithCondition.substring(indexOfAt + 1).trim()
+ : tagValueWithCondition;
+ OpeningHoursParser openingHoursParser = new OpeningHoursParser(
+ new ByteArrayInputStream(temporalCondition.getBytes())
+ );
+ boolean inverse =
+ indexOfAt > 0 && "none".equals(tagValueWithCondition.substring(0, indexOfAt).trim());
+
+ return new OHRulesRestriction(timeZone, openingHoursParser.rules(false), inverse);
+ }
+
+ @Override
+ public boolean active(long time) {
+ return (
+ OpeningHoursEvaluator.isOpenAt(
+ LocalDateTime.ofInstant(Instant.ofEpochSecond(time), timeZone),
+ rules
+ ) ^
+ inverse
+ );
+ }
+}
diff --git a/src/main/java/org/opentripplanner/street/model/RepeatingTimePeriod.java b/src/main/java/org/opentripplanner/street/model/RepeatingTimePeriod.java
index d16c0ed7993..055664446de 100644
--- a/src/main/java/org/opentripplanner/street/model/RepeatingTimePeriod.java
+++ b/src/main/java/org/opentripplanner/street/model/RepeatingTimePeriod.java
@@ -14,7 +14,7 @@
*
* @author mattwigway
*/
-public class RepeatingTimePeriod implements Serializable {
+public class RepeatingTimePeriod implements Serializable, TimeRestriction {
/**
* The timezone this is represented in.
@@ -107,6 +107,7 @@ public static RepeatingTimePeriod parseFromOsmTurnRestriction(
return ret;
}
+ @Override
public boolean active(long time) {
ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(Instant.ofEpochSecond(time), timeZone);
DayOfWeek dayOfWeek = zonedDateTime.getDayOfWeek();
diff --git a/src/main/java/org/opentripplanner/street/model/TimeRestriction.java b/src/main/java/org/opentripplanner/street/model/TimeRestriction.java
new file mode 100644
index 00000000000..5f128b7e817
--- /dev/null
+++ b/src/main/java/org/opentripplanner/street/model/TimeRestriction.java
@@ -0,0 +1,10 @@
+package org.opentripplanner.street.model;
+
+/**
+ * Represents a time restriction, used for opening hours, time dependant turn restrictions etc.
+ *
+ * @author hbruch
+ */
+public interface TimeRestriction {
+ boolean active(long time);
+}
diff --git a/src/main/java/org/opentripplanner/street/model/TurnRestriction.java b/src/main/java/org/opentripplanner/street/model/TurnRestriction.java
index 395a200e754..655a41c5713 100644
--- a/src/main/java/org/opentripplanner/street/model/TurnRestriction.java
+++ b/src/main/java/org/opentripplanner/street/model/TurnRestriction.java
@@ -9,7 +9,7 @@ public class TurnRestriction implements Serializable {
public final TurnRestrictionType type;
public final StreetEdge from;
public final StreetEdge to;
- public final RepeatingTimePeriod time;
+ public final TimeRestriction time;
public final TraverseModeSet modes;
public TurnRestriction(
@@ -17,7 +17,7 @@ public TurnRestriction(
StreetEdge to,
TurnRestrictionType type,
TraverseModeSet modes,
- RepeatingTimePeriod time
+ TimeRestriction time
) {
this.from = from;
this.to = to;
diff --git a/src/test/java/org/opentripplanner/street/model/OHRulesRestrictionTest.java b/src/test/java/org/opentripplanner/street/model/OHRulesRestrictionTest.java
new file mode 100644
index 00000000000..294abcb2acb
--- /dev/null
+++ b/src/test/java/org/opentripplanner/street/model/OHRulesRestrictionTest.java
@@ -0,0 +1,53 @@
+package org.opentripplanner.street.model;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import ch.poole.openinghoursparser.OpeningHoursParseException;
+import java.time.Instant;
+import java.time.ZoneId;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.opentripplanner._support.time.ZoneIds;
+
+public class OHRulesRestrictionTest {
+
+ private final ZoneId zoneId = ZoneIds.PARIS;
+
+ @Test
+ public void testRepeatingTimePeriod() throws OpeningHoursParseException {
+ // Note: day_on/day_off is currently required, relations like e.g.
+ // https://www.openstreetmap.org/relation/54249 would not work, as day_on/day_off is missing
+ TimeRestriction timePeriod = OHRulesRestriction.parseFromOsmTurnRestriction(
+ "monday",
+ "sunday",
+ "06",
+ "23",
+ () -> {
+ return zoneId;
+ }
+ );
+ Instant dateTime = Instant.parse("2024-07-26T10:30:00Z");
+ long time = dateTime.getEpochSecond();
+ assertTrue(timePeriod.active(time));
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ "mo-su 06:00-23:00", "fri", "none @ sat", "2024 Jul 26", "2024 Jul 25 - 2024 Jul 27",
+ }
+ ) // six numbers
+ // Note: hours only restrictions (e.g. "06:00-23:00") are not yet supported
+ public void testOHCalendarRestrictionOpen(String condition) throws OpeningHoursParseException {
+ TimeRestriction tr = OHRulesRestriction.parseFromCondition(
+ condition,
+ () -> {
+ return zoneId;
+ }
+ );
+ Instant dateTime = Instant.parse("2024-07-26T10:30:00Z");
+ long time = dateTime.getEpochSecond();
+ assertTrue(tr.active(time));
+ }
+}