pyrtan / sourcev0.14.0 · main

pyrtan / tests / test_nlp.py

6,103 bytes · last changed 2026-10-25 in Read sentences with rules: rewrites, typed slots, synonyms, typos, pyrtan match

1"""The language rules: fixed tables and fixed rules, the same answer every
2time, and a refusal where a guess would be needed."""
3
4import datetime as dt
5
6import pytest
7
8from pyrtan import nlp
9
10SUNDAY = dt.date(2026, 10, 25)
11
12
13def test_clean_names_every_rule_that_fired():
14 text, steps = nlp.clean("Hey Pyrtan, what's the weather in Oslo please?")
15 assert text == "what is the weather in Oslo" # the place keeps its case
16 assert steps == ["fillers: 'Hey Pyrtan', 'please'", "contractions: what's->what is"]
17 text, steps = nlp.clean("Could you tell me twenty-one things")
18 assert text == "tell me 21 things" and steps == ["fillers: 'Could you'", "numbers: twenty-one->21"]
19 assert nlp.clean("is example.com up") == ("is example.com up", [])
20 assert nlp.clean("What’s New York like")[0] == "What is New York like" # curly quote folded
21
22
23def test_fillers_are_kept_when_a_trigger_starts_with_them():
24 assert nlp.clean("hi there")[0] == "there"
25 assert nlp.clean("hi there", keep=frozenset({"hi"}))[0] == "hi there"
26 assert nlp.clean("hey pyrtan, hi there", keep=frozenset({"hi"}))[0] == "hi there"
27 assert nlp.clean("say thanks", keep=frozenset({"thanks"}))[0] == "say thanks"
28 assert nlp.clean("say thanks")[0] == "say"
29
30
31@pytest.mark.parametrize("text,value", [
32 ("three", 3), ("twenty-one", 21), ("twenty one", 21), ("a dozen", 12), ("two hundred and five", 205),
33 ("one hundred thousand", 100000), ("1,000", 1000), ("2.5", 2.5), ("-4", -4), ("ninety nine", 99),
34])
35def test_numbers(text, value):
36 assert nlp.parse_number(text) == value
37
38
39def test_number_words_only_touch_number_runs():
40 assert nlp.words_to_digits("a dozen eggs and two hundred and five items") == ("12 eggs and 205 items", ["a dozen->12", "two hundred and five->205"])
41 assert nlp.words_to_digits("set a reminder and one more")[0] == "set a reminder and 1 more"
42 assert nlp.words_to_digits("one of a kind")[0] == "1 of a kind"
43 with pytest.raises(nlp.ParseError):
44 nlp.parse_number("many")
45
46
47@pytest.mark.parametrize("text,expected", [
48 ("today", "2026-10-25"), ("tomorrow", "2026-10-26"), ("yesterday", "2026-10-24"),
49 ("the day after tomorrow", "2026-10-27"), ("friday", "2026-10-30"), ("next friday", "2026-10-30"),
50 ("last friday", "2026-10-23"), ("sunday", "2026-10-25"), ("this sunday", "2026-10-25"), ("next sunday", "2026-11-01"),
51 ("mon", "2026-10-26"), ("in 3 days", "2026-10-28"), ("in two weeks", "2026-11-08"), ("2 weeks ago", "2026-10-11"),
52 ("oct 25", "2026-10-25"), ("25 october 2026", "2026-10-25"), ("October 3rd", "2026-10-03"), ("3rd of march", "2026-03-03"),
53 ("13/10", "2026-10-13"), ("25.10.2026", "2026-10-25"), ("2026-02-28", "2026-02-28"), ("next month", "2026-11-25"),
54 ("last year", "2025-10-25"), ("end of month", "2026-10-31"), ("end of year", "2026-12-31"), ("on tuesday", "2026-10-27"),
55])
56def test_dates_relative_to_a_fixed_now(text, expected):
57 assert nlp.parse_date(text, SUNDAY) == expected
58
59
60def test_dates_refuse_a_guess():
61 with pytest.raises(nlp.ParseError, match="could be day/month or month/day"):
62 nlp.parse_date("3/10/2026", SUNDAY)
63 with pytest.raises(nlp.ParseError):
64 nlp.parse_date("2026-02-30", SUNDAY)
65 with pytest.raises(nlp.ParseError):
66 nlp.parse_date("soonish", SUNDAY)
67 assert nlp.parse_date("2024-01-31", SUNDAY) == "2024-01-31"
68 assert nlp.parse_date("in 1 month", dt.date(2026, 1, 31)) == "2026-02-28" # clamped, not rolled over
69
70
71@pytest.mark.parametrize("text,expected", [
72 ("3pm", "15:00"), ("15:30", "15:30"), ("3:05 pm", "15:05"), ("noon", "12:00"), ("midnight", "00:00"),
73 ("9 o'clock", "09:00"), ("12am", "00:00"), ("12 pm", "12:00"), ("7.45am", "07:45"),
74])
75def test_times(text, expected):
76 assert nlp.parse_time(text) == expected
77
78
79def test_times_refuse():
80 for bad in ["25:00", "7", "13pm", "half past"]:
81 with pytest.raises(nlp.ParseError):
82 nlp.parse_time(bad)
83
84
85@pytest.mark.parametrize("text,seconds", [
86 ("2 hours", 7200), ("30 min", 1800), ("1h30m", 5400), ("an hour", 3600), ("two days", 172800), ("90s", 90),
87 ("1 hour 15 minutes", 4500), ("1 hour and a half", 5400), ("half an hour", 1800), ("a week", 604800),
88])
89def test_durations(text, seconds):
90 assert nlp.parse_duration(text) == seconds
91
92
93def test_durations_refuse():
94 for bad in ["3 fortnights", "soon", "2", "hours"]:
95 with pytest.raises(nlp.ParseError):
96 nlp.parse_duration(bad)
97
98
99def test_hosts_urls_emails():
100 assert nlp.parse_host("https://Example.com/x") == "example.com"
101 assert nlp.parse_host("api.github.com") == "api.github.com"
102 assert nlp.parse_host("localhost:8700") == "localhost"
103 assert nlp.parse_url("https://example.com/a?b=1") == "https://example.com/a?b=1"
104 assert nlp.parse_email("Someone@Example.COM") == "someone@example.com"
105 for fn, bad in [(nlp.parse_host, "example dot com"), (nlp.parse_host, "not a host"), (nlp.parse_url, "example.com"), (nlp.parse_email, "nope@")]:
106 with pytest.raises(nlp.ParseError):
107 fn(bad)
108
109
110@pytest.mark.parametrize("word,base", [
111 ("stories", "story"), ("news", "news"), ("boxes", "box"), ("items", "item"), ("status", "status"),
112 ("cities", "city"), ("bus", "bus"), ("this", "this"), ("series", "series"), ("addresses", "address"),
113 ("analysis", "analysis"), ("is", "is"), ("releases", "release"), ("up", "up"),
114])
115def test_singular_is_conservative(word, base):
116 assert nlp.singular(word) == base
117
118
119def test_forms_cover_singular_and_plural_only():
120 assert nlp.forms("story") == {"story", "stories"}
121 assert nlp.forms("stories") == {"story", "stories"}
122 assert nlp.forms("news") == {"news"}
123 assert nlp.forms("up") == {"up"}
124
125
126def test_distance():
127 assert nlp.distance("weather", "weather") == 0
128 assert nlp.distance("wether", "weather") == 1
129 assert nlp.distance("waether", "weather") == 1 # a transposition is one edit
130 assert nlp.distance("wather", "weather") == 1
131 assert nlp.distance("abc", "xyz") == 3
132 assert nlp.distance("a", "abcdef", 1) == 2 # stops early past the limit