Skip to content

Commit 0477e2f

Browse files
authored
Split date-time runtime fixes from locale PR (#185)
* Split date-time runtime fixes from locale PR * Move datetime locale exceptions into resources * Split datetime locale resources into separate PR * Tidy datetime runtime location handling * Fix loader test imports
1 parent 4d420e5 commit 0477e2f

2 files changed

Lines changed: 99 additions & 24 deletions

File tree

__init__.py

Lines changed: 97 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,22 @@ def use_24hour(self):
137137
"""
138138
return self.time_format == 'full'
139139

140+
@staticmethod
141+
def _normalize_phrase(text: str) -> str:
142+
"""Normalize phrases for case-insensitive locale resource matching."""
143+
return text.strip(" \t,;:.!?").casefold()
144+
145+
def _load_locale_phrase_set(self, name: str):
146+
"""Load a locale phrase list once and return a normalized set."""
147+
cache_key = f"{name}.list.normalized"
148+
if cache_key not in self.resources.static:
149+
self.resources.static[cache_key] = {
150+
self._normalize_phrase(phrase)
151+
for phrase in self.resources.load_list_file(name)
152+
if phrase.strip()
153+
}
154+
return self.resources.static[cache_key]
155+
140156
######################################################################
141157
# parsing
142158
def _extract_location(self, utt: str) -> str:
@@ -155,14 +171,51 @@ def _extract_location(self, utt: str) -> str:
155171
pat = pat.strip()
156172
if pat and pat[0] == "#":
157173
continue
158-
res = re.search(pat, utt)
174+
res = re.search(pat, utt, flags=re.IGNORECASE)
159175
if res:
160176
try:
161-
return res.group("Location")
177+
return res.group("Location").strip(" \t,;:.!?")
162178
except IndexError:
163179
pass
164180
return None
165181

182+
def _is_ambiguous_location(self, location_string: str) -> bool:
183+
"""Return True when a locale marks a location name as timezone-ambiguous."""
184+
return (
185+
self._normalize_phrase(location_string)
186+
in self._load_locale_phrase_set("ambiguous_locations")
187+
)
188+
189+
def _sanitize_location(self, location_string: Optional[str]) -> Optional[str]:
190+
"""Discard locale-specific phrases accidentally captured as locations."""
191+
if not location_string:
192+
return None
193+
cleaned = location_string.strip(" \t,;:.!?")
194+
if (
195+
self._normalize_phrase(cleaned)
196+
in self._load_locale_phrase_set("non_location_phrases")
197+
):
198+
return None
199+
return cleaned
200+
201+
def _resolve_location(self,
202+
location_string: Optional[str] = None,
203+
utterance: str = "") -> Optional[str]:
204+
"""Resolve a sanitized location from an explicit slot or from the utterance."""
205+
if location_string:
206+
return self._sanitize_location(location_string)
207+
if utterance:
208+
return self._sanitize_location(self._extract_location(utterance))
209+
return None
210+
211+
def _mentions_current_weekend(self, utterance: str) -> bool:
212+
"""Check whether the active locale explicitly asked for the current weekend."""
213+
current_weekend_phrases = self._load_locale_phrase_set("current_weekend_phrases")
214+
if not current_weekend_phrases:
215+
return False
216+
normalized_utterance = utterance.casefold()
217+
return any(phrase in normalized_utterance for phrase in current_weekend_phrases)
218+
166219
@staticmethod
167220
def _get_timezone_from_builtins(location_string: str) -> Optional[datetime.tzinfo]:
168221
"""Attempt to resolve a timezone from a location name using geocoding.
@@ -262,6 +315,8 @@ def get_timezone_in_location(self, location_string: str) -> datetime.tzinfo:
262315
Returns:
263316
datetime.tzinfo: The timezone object if resolved, else None.
264317
"""
318+
if self._is_ambiguous_location(location_string):
319+
return None
265320
timezone = self._get_timezone_from_builtins(location_string)
266321
if not timezone:
267322
timezone = self._get_timezone_from_table(location_string)
@@ -296,7 +351,7 @@ def get_datetime(self, location: str = None,
296351
return dt
297352

298353
def get_spoken_time(self, location: str = None, force_ampm=False,
299-
anchor_date: datetime.datetime = None) -> str:
354+
anchor_date: datetime.datetime = None) -> Optional[str]:
300355
"""Get a human-readable spoken version of the current time.
301356
302357
Args:
@@ -308,6 +363,8 @@ def get_spoken_time(self, location: str = None, force_ampm=False,
308363
str: A spoken-friendly representation of the time.
309364
"""
310365
dt = self.get_datetime(location, anchor_date)
366+
if not dt:
367+
return None
311368

312369
# speak AM/PM when talking about somewhere else
313370
say_am_pm = bool(location) or force_ampm
@@ -320,7 +377,7 @@ def get_spoken_time(self, location: str = None, force_ampm=False,
320377
return s
321378

322379
def get_display_time(self, location: str = None, force_ampm=False,
323-
anchor_date: datetime.datetime = None) -> str:
380+
anchor_date: datetime.datetime = None) -> Optional[str]:
324381
"""Get a display-friendly version of the current time.
325382
326383
Args:
@@ -332,6 +389,8 @@ def get_display_time(self, location: str = None, force_ampm=False,
332389
str: A string representing the display time.
333390
"""
334391
dt = self.get_datetime(location, anchor_date)
392+
if not dt:
393+
return None
335394
# speak AM/PM when talking about somewhere else
336395
say_am_pm = bool(location) or force_ampm
337396
return nice_time(dt, lang=self.lang,
@@ -363,30 +422,32 @@ def get_display_date(self, location: str = None,
363422

364423
######################################################################
365424
# Time queries / display
366-
def speak_time(self, dialog: str, location: str = None):
425+
def speak_time(self, dialog: str, location: str = None,
426+
anchor_date: datetime.datetime = None):
367427
"""Speak the current time. Optionally at a location
368428
speaks an error if timezone for requested location could not be detected"""
369429
if location:
370-
current_time = self.get_spoken_time(location)
430+
current_time = self.get_spoken_time(location, anchor_date=anchor_date)
371431
if not current_time:
372432
self.speak_dialog("time.tz.not.found", {"location": location})
373433
return
374-
time_string = self.get_display_time(location)
434+
time_string = self.get_display_time(location, anchor_date=anchor_date)
375435
else:
376-
current_time = self.get_spoken_time()
377-
time_string = self.get_display_time()
436+
current_time = self.get_spoken_time(anchor_date=anchor_date)
437+
time_string = self.get_display_time(anchor_date=anchor_date)
378438

379439
# speak it
380440
self.speak_dialog(dialog, {"time": current_time})
381441

382442
# and briefly show the time
383-
self.show_time(time_string)
443+
if time_string:
444+
self.show_time(time_string)
384445

385446
@intent_handler("what.time.is.it.intent")
386447
def handle_query_time(self, message):
387448
"""Handle queries about the current time."""
388449
utt = message.data.get('utterance', "")
389-
location = message.data.get("location") or self._extract_location(utt)
450+
location = self._resolve_location(message.data.get("location"), utt)
390451
# speak it
391452
self.speak_time("time.current", location=location)
392453

@@ -400,10 +461,10 @@ def handle_query_future_time(self, message):
400461
self.handle_query_time(message)
401462
return
402463

403-
location = message.data.get("location") or self._extract_location(utt)
464+
location = self._resolve_location(message.data.get("location"), utt)
404465

405466
# speak it
406-
self.speak_time("time.future", location=location)
467+
self.speak_time("time.future", location=location, anchor_date=dt)
407468

408469
######################################################################
409470
# Date queries
@@ -418,7 +479,7 @@ def handle_query_date(self, message, response_type="simple"):
418479
dt = now
419480

420481
# handle questions ~ "what is the day in sydney"
421-
location_string = message.data.get("location") or self._extract_location(utt)
482+
location_string = self._resolve_location(message.data.get("location"), utt)
422483

423484
if location_string:
424485
dt = self.get_datetime(location_string, anchor_date=dt)
@@ -470,7 +531,12 @@ def handle_current_day(self, message):
470531
Args:
471532
message: The message object triggering the intent.
472533
"""
473-
now = self.get_datetime() # session aware
534+
utt = message.data.get("utterance", "")
535+
location = self._resolve_location(message.data.get("location"), utt)
536+
now = self.get_datetime(location)
537+
if location and not now:
538+
self.speak_dialog("time.tz.not.found", {"location": location})
539+
return
474540
self.speak_dialog("day.current",
475541
{"day": nice_day(now, lang=self.lang)})
476542

@@ -530,19 +596,28 @@ def handle_current_year(self, message):
530596

531597
@intent_handler("date.future.weekend.intent")
532598
def handle_date_future_weekend(self, message):
533-
# Strip year off nice_date as request is inherently close
534-
# Don't pass `now` to `nice_date` as a
535-
# request on Friday will return "tomorrow"
536599
"""
537600
Handles queries about the upcoming weekend's dates.
538601
539602
Determines the dates for the next Saturday and Sunday, formats them for speech, and responds with a dialog containing both dates.
540603
"""
541604
now = self.get_datetime()
542-
dt = extract_datetime('this saturday', anchorDate=now, lang='en-us')[0]
543-
saturday_date = ', '.join(nice_date(dt, lang=self.lang).split(', ')[:2])
544-
dt = extract_datetime('this sunday', anchorDate=now, lang='en-us')[0]
545-
sunday_date = ', '.join(nice_date(dt, lang=self.lang).split(', ')[:2])
605+
utt = message.data.get("utterance", "").lower()
606+
weekday = now.weekday()
607+
608+
# On Saturday/Sunday, default to the upcoming weekend unless the user
609+
# explicitly asked for the current weekend in this locale.
610+
if weekday <= 5:
611+
saturday_dt = now + datetime.timedelta(days=5 - weekday)
612+
else:
613+
saturday_dt = now - datetime.timedelta(days=1)
614+
615+
if weekday >= 5 and not self._mentions_current_weekend(utt):
616+
saturday_dt += datetime.timedelta(days=7)
617+
618+
sunday_dt = saturday_dt + datetime.timedelta(days=1)
619+
saturday_date = ', '.join(nice_date(saturday_dt, lang=self.lang).split(', ')[:2])
620+
sunday_date = ', '.join(nice_date(sunday_dt, lang=self.lang).split(', ')[:2])
546621
self.speak_dialog('date.future.weekend', {
547622
'saturday_date': saturday_date,
548623
'sunday_date': sunday_date

test/unittests/test_skill_loading.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import unittest
22
from os.path import dirname
33

4-
from mycroft.skills.skill_loader import PluginSkillLoader, SkillLoader
54
from ovos_plugin_manager.skills import find_skill_plugins
65
from ovos_utils.messagebus import FakeBus
7-
from skill_ovos_date_time import TimeSkill
6+
from ovos_workshop.skill_launcher import PluginSkillLoader, SkillLoader
7+
from ovos_skill_date_time import TimeSkill
88

99

1010
class TestSkillLoading(unittest.TestCase):

0 commit comments

Comments
 (0)