From 5d239470173d1a7a55d45f7623789e434d127a74 Mon Sep 17 00:00:00 2001 From: nicfb Date: Sat, 22 Jan 2022 16:28:32 -0600 Subject: [PATCH] feat: add capability to search events uncomment test do not include duplicate events in search results requested changes improve readability of tests add test for when start and end dates are the same refactor tests --- kosmorrolib/__init__.py | 2 +- kosmorrolib/events.py | 110 +++++++++++++++++++++++++++++++++++++- kosmorrolib/exceptions.py | 13 +++++ kosmorrolib/model.py | 10 ++++ 4 files changed, 133 insertions(+), 2 deletions(-) diff --git a/kosmorrolib/__init__.py b/kosmorrolib/__init__.py index d46f4ba..8fac5e3 100644 --- a/kosmorrolib/__init__.py +++ b/kosmorrolib/__init__.py @@ -18,5 +18,5 @@ from .model import Position, Event, AsterEphemerides, Object from .ephemerides import get_ephemerides, get_moon_phase -from .events import get_events +from .events import get_events, search_events from .enum import * diff --git a/kosmorrolib/events.py b/kosmorrolib/events.py index 7b4c9f8..bca575a 100644 --- a/kosmorrolib/events.py +++ b/kosmorrolib/events.py @@ -36,7 +36,7 @@ from kosmorrolib.model import ( ) from kosmorrolib.dateutil import translate_to_timezone from kosmorrolib.enum import EventType, ObjectIdentifier, SeasonType, LunarEclipseType -from kosmorrolib.exceptions import OutOfRangeDateError +from kosmorrolib.exceptions import InvalidDateRangeError, OutOfRangeDateError from kosmorrolib.core import get_timescale, get_skf_objects, flatten_list @@ -528,3 +528,111 @@ def get_events(for_date: date = date.today(), timezone: int = 0) -> [Event]: end_date = date(end_date.year, end_date.month, end_date.day) raise OutOfRangeDateError(start_date, end_date) from error + + +def search_events( + event_types: [EventType], end: date, start: date = date.today(), timezone: int = 0 +) -> [Event]: + """Search between `start` and `end` dates, and return a list of matching events for the given time range, adjusted to a given timezone. + + Find all events between January 27th, 2020 and January 29th, 2020: + + >>> event_types = [EventType.OPPOSITION, EventType.CONJUNCTION, EventType.OCCULTATION, EventType.MAXIMAL_ELONGATION, EventType.APOGEE, EventType.PERIGEE, EventType.SEASON_CHANGE, EventType.LUNAR_ECLIPSE] + >>> search_events(event_types, end=date(2020, 1, 29), start=date(2020, 1, 27)) # doctest: +NORMALIZE_WHITESPACE + [, ] start=2020-01-27 20:00:23.242428+00:00 end=None details=None />, + , ] start=2020-01-28 09:33:45.000618+00:00 end=None details=None />, + , ] start=2020-01-28 11:01:51.909499+00:00 end=None details=None />, + ] start=2020-01-29 21:32:13.884314+00:00 end=None details={'distance_km': 405426.4150890029} />] + + Find Apogee events between January 27th, 2020 and January 29th, 2020: + + >>> search_events([EventType.APOGEE], end=date(2020, 1, 29), start=date(2020, 1, 27)) + [] start=2020-01-29 21:32:13.884314+00:00 end=None details={'distance_km': 405426.4150890029} />] + + Find Apogee events between January 27th, 2020 and January 29th, 2020 (show times in UTC-6): + + >>> search_events([EventType.APOGEE], end=date(2020, 1, 29), start=date(2020, 1, 27), timezone=-6) + [] start=2020-01-29 15:32:13.884314-06:00 end=None details={'distance_km': 405426.4150890029} />] + + If no events occurred in the given time range, an empty list is returned. + + >>> event_types = [EventType.OPPOSITION, EventType.CONJUNCTION, EventType.SEASON_CHANGE, EventType.LUNAR_ECLIPSE] + >>> search_events(event_types, end=date(2021, 5, 15), start=date(2021, 5, 14)) + [] + + Note that the events can only be found for a date range. + Asking for the events with an out of range date will result in an exception: + + >>> event_types = [EventType.OPPOSITION, EventType.CONJUNCTION] + >>> search_events(event_types, end=date(1000, 1, 2), start=date(1000, 1, 1)) + Traceback (most recent call last): + ... + kosmorrolib.exceptions.OutOfRangeDateError: The date must be between 1899-07-28 and 2053-10-08 + + If the start date does not occur before the end date, an exception will be thrown + + >>> event_types = [EventType.OPPOSITION, EventType.CONJUNCTION] + >>> search_events(event_types, end=date(2021, 1, 26), start=date(2021, 1, 28)) + Traceback (most recent call last): + ... + kosmorrolib.exceptions.InvalidDateRangeError: The start date (2021-01-28) must be before the end date (2021-01-26) + + If the start and end dates are the same, then events for that one day will be returned. + + >>> event_types = [EventType.OPPOSITION, EventType.CONJUNCTION] + >>> search_events(event_types, end=date(2021, 1, 28), start=date(2021, 1, 28)) + [, ] start=2021-01-28 16:18:05.483029+00:00 end=None details=None />] + """ + + moon = ASTERS[1] + sun = ASTERS[0] + + def search_all_apogee_events(start: date, end: date, timezone: int = 0) -> [Event]: + moon_apogee_events = _search_apogee(moon)(start, end, timezone) + earth_apogee_events = _search_apogee(EARTH, from_aster=sun)( + start, end, timezone + ) + return moon_apogee_events + earth_apogee_events + + def search_all_perigee_events(start: date, end: date, timezone: int = 0) -> [Event]: + moon_perigee_events = _search_perigee(moon)(start, end, timezone) + earth_perigee_events = _search_perigee(EARTH, from_aster=sun)( + start, end, timezone + ) + return moon_perigee_events + earth_perigee_events + + search_funcs = { + EventType.OPPOSITION: _search_oppositions, + EventType.CONJUNCTION: _search_conjunctions_occultations, + EventType.OCCULTATION: _search_conjunctions_occultations, + EventType.MAXIMAL_ELONGATION: _search_maximal_elongations, + EventType.APOGEE: search_all_apogee_events, + EventType.PERIGEE: search_all_perigee_events, + EventType.SEASON_CHANGE: _search_earth_season_change, + EventType.LUNAR_ECLIPSE: _search_lunar_eclipse, + } + + if start > end: + raise InvalidDateRangeError(start, end) + + start_time = get_timescale().utc(start.year, start.month, start.day, -timezone) + end_time = get_timescale().utc(end.year, end.month, end.day + 1, -timezone) + + try: + found_events = [] + for event_type in event_types: + fun = search_funcs[event_type] + events = fun(start_time, end_time, timezone) + for event in events: + if event not in found_events: + found_events.append(event) + + return sorted(flatten_list(found_events), key=lambda event: event.start_time) + except EphemerisRangeError as error: + start_date = translate_to_timezone(error.start_time.utc_datetime(), timezone) + end_date = translate_to_timezone(error.end_time.utc_datetime(), timezone) + + start_date = date(start_date.year, start_date.month, start_date.day) + end_date = date(end_date.year, end_date.month, end_date.day) + + raise OutOfRangeDateError(start_date, end_date) from error diff --git a/kosmorrolib/exceptions.py b/kosmorrolib/exceptions.py index bc67d18..36979c9 100644 --- a/kosmorrolib/exceptions.py +++ b/kosmorrolib/exceptions.py @@ -30,3 +30,16 @@ class OutOfRangeDateError(ValueError): ) self.min_date = min_date self.max_date = max_date + + +class InvalidDateRangeError(ValueError): + def __init__(self, start_date: date, end_date: date): + super().__init__( + "The start date (%s) must be before the end date (%s)" + % ( + start_date.strftime("%Y-%m-%d"), + end_date.strftime("%Y-%m-%d"), + ) + ) + self.start_date = start_date + self.end_date = end_date diff --git a/kosmorrolib/model.py b/kosmorrolib/model.py index 7a04003..9a1fe20 100644 --- a/kosmorrolib/model.py +++ b/kosmorrolib/model.py @@ -239,6 +239,16 @@ class Event(Serializable): self.details, ) + def __eq__(self, other: object) -> bool: + return ( + isinstance(other, Event) + and self.event_type.name == other.event_type.name + and self.objects == other.objects + and self.start_time == other.start_time + and self.end_time == other.end_time + and self.details == other.details + ) + @deprecated( "kosmorrolib.Event.get_description method is deprecated since version 1.1 " "and will be removed in version 2.0. "