File size: 3,436 Bytes
b34c92a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
"""Open-now awareness: OSM opening_hours parsing + plan-time demotion."""
from __future__ import annotations

from datetime import datetime

from discoverroute.routing.hours import (PARIS_TZ, apply_open_now, is_open)

TUE_15H = datetime(2026, 6, 9, 15, 0, tzinfo=PARIS_TZ)   # Tuesday 15:00
TUE_23H = datetime(2026, 6, 9, 23, 30, tzinfo=PARIS_TZ)  # Tuesday 23:30
MON_15H = datetime(2026, 6, 8, 15, 0, tzinfo=PARIS_TZ)   # Monday 15:00
WED_01H = datetime(2026, 6, 10, 1, 0, tzinfo=PARIS_TZ)   # Wednesday 01:00


def test_always_open():
    assert is_open("24/7", TUE_15H) is True


def test_simple_weekday_range():
    assert is_open("Mo-Fr 08:00-18:00", TUE_15H) is True
    assert is_open("Mo-Fr 08:00-18:00", TUE_23H) is False


def test_multi_rule_with_off_day():
    spec = "Tu-Su 10:00-18:00; Mo off"
    assert is_open(spec, TUE_15H) is True
    assert is_open(spec, MON_15H) is False


def test_lunch_split_spans():
    spec = "Mo-Fr 09:00-12:00,14:00-18:00"
    assert is_open(spec, TUE_15H) is True
    assert is_open(spec, datetime(2026, 6, 9, 13, 0, tzinfo=PARIS_TZ)) is False


def test_daily_no_days():
    assert is_open("07:00-23:30", TUE_15H) is True


def test_overnight_span():
    spec = "Tu 18:00-02:00"
    assert is_open(spec, TUE_23H) is True
    assert is_open(spec, WED_01H) is True   # spills past midnight into Wednesday
    assert is_open(spec, TUE_15H) is False


def test_abstains_on_complex():
    assert is_open("sunrise-sunset", TUE_15H) is None
    assert is_open("Mar-Oct 08:00-20:00", TUE_15H) is None
    assert is_open(None, TUE_15H) is None
    assert is_open("", TUE_15H) is None


class P:
    def __init__(self, category, hours, score=1.0):
        self.category, self.opening_hours, self.score = category, hours, score


def test_demotion_by_posture():
    closed_cafe = P("cafe", "Mo-Fr 08:00-12:00")        # closed Tue 15:00
    closed_monument = P("monument_historic", "Mo-Fr 08:00-12:00")
    open_cafe = P("cafe", "Mo-Fr 08:00-18:00")
    unknown = P("cafe", None)
    posture = {"cafe": "stop", "monument_historic": "pass"}
    apply_open_now([closed_cafe, closed_monument, open_cafe, unknown],
                   posture, when=TUE_15H)
    assert closed_cafe.score == 0.2          # stop-at closed -> heavy demotion
    assert closed_monument.score == 0.7      # pass-by closed -> mild demotion
    assert open_cafe.score == 1.0 and open_cafe.open_state is True
    assert unknown.score == 1.0 and unknown.open_state is None


def test_holiday_rules_dont_block_weekday_decisions():
    # "PH off" must not force abstention on a decidable weekday
    assert is_open("Mo-Fr 08:00-18:00; PH off", TUE_15H) is True
    assert is_open("Mo-Fr 08:00-18:00; PH off", TUE_23H) is False
    # PH inside a day list extends to holidays; weekdays still decidable
    assert is_open("PH,Sa,Su 10:00-18:00; Mo-Fr 08:30-17:00", TUE_15H) is True


def test_night_demotes_unknown_daytime_categories():
    night = datetime(2026, 6, 9, 23, 30, tzinfo=PARIS_TZ)
    day = datetime(2026, 6, 9, 15, 0, tzinfo=PARIS_TZ)
    cafe_n, bar_n = P("cafe", None), P("bar_pub", None)
    apply_open_now([cafe_n, bar_n], {}, when=night)
    assert cafe_n.score == 0.5     # unknown café at 23:30 -> poor bet
    assert bar_n.score == 1.0      # unknown bar at night -> plausible
    cafe_d = P("cafe", None)
    apply_open_now([cafe_d], {}, when=day)
    assert cafe_d.score == 1.0     # daytime unknown untouched