@@ -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
0 commit comments