Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
fix: handle out of range date error
  • Loading branch information
Jérôme Deuchnord committed Jun 6, 2020
1 parent a41296e commit c39cd3a
Show file tree
Hide file tree
Showing 20 changed files with 280 additions and 131 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/unit-tests.yml
Expand Up @@ -20,5 +20,5 @@ jobs:
env:
COVERALLS_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
run: |
pipenv run python -m coverage run -m unittest test
make test
COVERALLS_REPO_TOKEN=$COVERALLS_TOKEN pipenv run coveralls
2 changes: 2 additions & 0 deletions .scripts/tests-e2e.sh
Expand Up @@ -81,6 +81,8 @@ assertSuccess "kosmorro -h"
assertSuccess "kosmorro -d 2020-01-27"
assertFailure "kosmorro -d yolo-yo-lo"
assertFailure "kosmorro -d 2020-13-32"
assertFailure "kosmorro --date=1789-05-05"
assertFailure "kosmorro --date=3000-01-01"
assertSuccess "kosmorro --date='+3y 5m3d'"
assertSuccess "kosmorro --date='-1y3d'"
assertFailure "kosmorro --date='+3d4m"
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Expand Up @@ -80,7 +80,7 @@ Kosmorro's unit tests use Python's official `unittest` module. They live in the
You can also run them by invoking the following command:

```shell
pipenv run python -m unittest test
make test
```

Note: there are currently some memory leaks in the unit tests, making the result quite difficult to read. I am working to fix this.
Expand Down
8 changes: 8 additions & 0 deletions Makefile
@@ -1,3 +1,11 @@
.PHONY: test

test:
unset KOSMORRO_LATITUDE; \
unset KOSMORRO_LONGITUDE; \
unset KOSMORRO_TIMEZONE; \
LANG=C pipenv run python3 -m coverage run -m unittest test

build:
ronn --roff manpage/kosmorro.1.md
ronn --roff manpage/kosmorro.7.md
Expand Down
2 changes: 1 addition & 1 deletion Pipfile
Expand Up @@ -11,7 +11,7 @@ unittest-data-provider = "*"
coveralls = "*"

[packages]
skyfield = ">=1.13.0,<2.0.0"
skyfield = ">=1.21.0,<2.0.0"
tabulate = "*"
numpy = ">=1.17.0,<2.0.0"
termcolor = "*"
Expand Down
2 changes: 1 addition & 1 deletion Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file added kosmorrolib/assets/moonphases/png/unknown.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions kosmorrolib/assets/moonphases/svg/unknown.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions kosmorrolib/data.py
Expand Up @@ -36,7 +36,8 @@
'FULL_MOON': _('Full Moon'),
'WANING_GIBBOUS': _('Waning gibbous'),
'LAST_QUARTER': _('Last Quarter'),
'WANING_CRESCENT': _('Waning crescent')
'WANING_CRESCENT': _('Waning crescent'),
'UNKNOWN': _('Unavailable')
}

EVENTS = {
Expand All @@ -54,7 +55,7 @@ def serialize(self) -> dict:


class MoonPhase(Serializable):
def __init__(self, identifier: str, time: Union[datetime, None], next_phase_date: Union[datetime, None]):
def __init__(self, identifier: str, time: datetime = None, next_phase_date: datetime = None):
if identifier not in MOON_PHASES.keys():
raise ValueError('identifier parameter must be one of %s (got %s)' % (', '.join(MOON_PHASES.keys()),
identifier))
Expand Down
18 changes: 11 additions & 7 deletions kosmorrolib/dumper.py
Expand Up @@ -25,20 +25,14 @@
from numpy import int64
from termcolor import colored
from .data import ASTERS, Object, AsterEphemerides, MoonPhase, Event
from .i18n import _
from .i18n import _, FULL_DATE_FORMAT, SHORT_DATETIME_FORMAT, TIME_FORMAT
from .version import VERSION
from .exceptions import UnavailableFeatureError
try:
from latex import build_pdf
except ImportError:
build_pdf = None

FULL_DATE_FORMAT = _('{day_of_week} {month} {day_number}, {year}').format(day_of_week='%A', month='%B',
day_number='%d', year='%Y')
SHORT_DATETIME_FORMAT = _('{month} {day_number}, {hours}:{minutes}').format(month='%b', day_number='%d',
hours='%H', minutes='%M')
TIME_FORMAT = _('{hours}:{minutes}').format(hours='%H', minutes='%M')


class Dumper(ABC):
def __init__(self, ephemerides: [AsterEphemerides] = None, moon_phase: MoonPhase = None, events: [Event] = None,
Expand All @@ -60,6 +54,9 @@ def get_date_as_string(self, capitalized: bool = False) -> str:

return date

def __str__(self):
return self.to_string()

@abstractmethod
def to_string(self):
pass
Expand Down Expand Up @@ -189,6 +186,9 @@ def get_events(self, events: [Event]) -> str:
return tabulate(data, tablefmt='plain', stralign='left')

def get_moon(self, moon_phase: MoonPhase) -> str:
if moon_phase is None:
return _('Moon phase is unavailable for this date.')

current_moon_phase = ' '.join([self.style(_('Moon phase:'), 'strong'), moon_phase.get_phase()])
new_moon_phase = _('{next_moon_phase} on {next_moon_phase_date} at {next_moon_phase_time}').format(
next_moon_phase=moon_phase.get_next_phase_name(),
Expand All @@ -212,6 +212,10 @@ def to_string(self):
def _make_document(self, template: str) -> str:
kosmorro_logo_path = os.path.join(os.path.abspath(os.path.dirname(__file__)),
'assets', 'png', 'kosmorro-logo.png')

if self.moon_phase is None:
self.moon_phase = MoonPhase('UNKNOWN')

moon_phase_graphics = os.path.join(os.path.abspath(os.path.dirname(__file__)),
'assets', 'moonphases', 'png',
'.'.join([self.moon_phase.identifier.lower().replace('_', '-'),
Expand Down
82 changes: 51 additions & 31 deletions kosmorrolib/ephemerides.py
Expand Up @@ -21,15 +21,17 @@
from skyfield.searchlib import find_discrete, find_maxima
from skyfield.timelib import Time
from skyfield.constants import tau
from skyfield.errors import EphemerisRangeError

from .data import Position, AsterEphemerides, MoonPhase, Object, ASTERS, skyfield_to_moon_phase
from .dateutil import translate_to_timezone
from .core import get_skf_objects, get_timescale, get_iau2000b
from .exceptions import OutOfRangeDateError

RISEN_ANGLE = -0.8333


def get_moon_phase(compute_date: datetime.date) -> MoonPhase:
def get_moon_phase(compute_date: datetime.date, timezone: int = 0) -> MoonPhase:
earth = get_skf_objects()['earth']
moon = get_skf_objects()['moon']
sun = get_skf_objects()['sun']
Expand All @@ -47,7 +49,16 @@ def moon_phase_at(time: Time):
time1 = get_timescale().utc(compute_date.year, compute_date.month, compute_date.day - 10)
time2 = get_timescale().utc(compute_date.year, compute_date.month, compute_date.day + 10)

times, phase = find_discrete(time1, time2, moon_phase_at)
try:
times, phase = find_discrete(time1, time2, moon_phase_at)
except EphemerisRangeError as error:
start = translate_to_timezone(error.start_time.utc_datetime(), timezone)
end = translate_to_timezone(error.end_time.utc_datetime(), timezone)

start = datetime.date(start.year, start.month, start.day) + datetime.timedelta(days=12)
end = datetime.date(end.year, end.month, end.day) - datetime.timedelta(days=12)

raise OutOfRangeDateError(start, end)

return skyfield_to_moon_phase(times, phase, today)

Expand All @@ -71,34 +82,43 @@ def fun(time: Time) -> bool:
start_time = get_timescale().utc(date.year, date.month, date.day, -timezone)
end_time = get_timescale().utc(date.year, date.month, date.day, 23 - timezone, 59, 59)

for aster in ASTERS:
rise_times, arr = find_discrete(start_time, end_time, is_risen(aster))
try:
culmination_time, _ = find_maxima(start_time, end_time, f=get_angle(aster), epsilon=1./3600/24, num=12)
culmination_time = culmination_time[0] if len(culmination_time) > 0 else None
except ValueError:
culmination_time = None

if len(rise_times) == 2:
rise_time = rise_times[0 if arr[0] else 1]
set_time = rise_times[1 if not arr[1] else 0]
else:
rise_time = rise_times[0] if arr[0] else None
set_time = rise_times[0] if not arr[0] else None

# Convert the Time instances to Python datetime objects
if rise_time is not None:
rise_time = translate_to_timezone(rise_time.utc_datetime().replace(microsecond=0),
to_tz=timezone)

if culmination_time is not None:
culmination_time = translate_to_timezone(culmination_time.utc_datetime().replace(microsecond=0),
to_tz=timezone)

if set_time is not None:
set_time = translate_to_timezone(set_time.utc_datetime().replace(microsecond=0),
to_tz=timezone)

ephemerides.append(AsterEphemerides(rise_time, culmination_time, set_time, aster=aster))
try:
for aster in ASTERS:
rise_times, arr = find_discrete(start_time, end_time, is_risen(aster))
try:
culmination_time, _ = find_maxima(start_time, end_time, f=get_angle(aster), epsilon=1./3600/24, num=12)
culmination_time = culmination_time[0] if len(culmination_time) > 0 else None
except ValueError:
culmination_time = None

if len(rise_times) == 2:
rise_time = rise_times[0 if arr[0] else 1]
set_time = rise_times[1 if not arr[1] else 0]
else:
rise_time = rise_times[0] if arr[0] else None
set_time = rise_times[0] if not arr[0] else None

# Convert the Time instances to Python datetime objects
if rise_time is not None:
rise_time = translate_to_timezone(rise_time.utc_datetime().replace(microsecond=0),
to_tz=timezone)

if culmination_time is not None:
culmination_time = translate_to_timezone(culmination_time.utc_datetime().replace(microsecond=0),
to_tz=timezone)

if set_time is not None:
set_time = translate_to_timezone(set_time.utc_datetime().replace(microsecond=0),
to_tz=timezone)

ephemerides.append(AsterEphemerides(rise_time, culmination_time, set_time, aster=aster))
except EphemerisRangeError as error:
start = translate_to_timezone(error.start_time.utc_datetime(), timezone)
end = translate_to_timezone(error.end_time.utc_datetime(), timezone)

start = datetime.date(start.year, start.month, start.day + 1)
end = datetime.date(end.year, end.month, end.day - 1)

raise OutOfRangeDateError(start, end)

return ephemerides
21 changes: 16 additions & 5 deletions kosmorrolib/events.py
Expand Up @@ -18,12 +18,14 @@

from datetime import date as date_type

from skyfield.errors import EphemerisRangeError
from skyfield.timelib import Time
from skyfield.searchlib import find_discrete, find_maxima
from numpy import pi

from .data import Event, Star, Planet, ASTERS
from .dateutil import translate_to_timezone
from .exceptions import OutOfRangeDateError
from .core import get_timescale, get_skf_objects, flatten_list


Expand Down Expand Up @@ -137,8 +139,17 @@ def search_events(date: date_type, timezone: int = 0) -> [Event]:
start_time = get_timescale().utc(date.year, date.month, date.day, -timezone)
end_time = get_timescale().utc(date.year, date.month, date.day + 1, -timezone)

return sorted(flatten_list([
_search_oppositions(start_time, end_time, timezone),
_search_conjunction(start_time, end_time, timezone),
_search_maximal_elongations(start_time, end_time, timezone)
]), key=lambda event: event.start_time)
try:
return sorted(flatten_list([
_search_oppositions(start_time, end_time, timezone),
_search_conjunction(start_time, end_time, timezone),
_search_maximal_elongations(start_time, end_time, timezone)
]), 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_type(start_date.year, start_date.month, start_date.day)
end_date = date_type(end_date.year, end_date.month, end_date.day)

raise OutOfRangeDateError(start_date, end_date)
13 changes: 13 additions & 0 deletions kosmorrolib/exceptions.py
Expand Up @@ -16,8 +16,21 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from datetime import date
from .i18n import _, SHORT_DATE_FORMAT


class UnavailableFeatureError(RuntimeError):
def __init__(self, msg: str):
super(UnavailableFeatureError, self).__init__()
self.msg = msg


class OutOfRangeDateError(RuntimeError):
def __init__(self, min_date: date, max_date: date):
super(OutOfRangeDateError, self).__init__()
self.min_date = min_date
self.max_date = max_date
self.msg = _('The date must be between {minimum_date}'
' and {maximum_date}').format(minimum_date=min_date.strftime(SHORT_DATE_FORMAT),
maximum_date=max_date.strftime(SHORT_DATE_FORMAT))
7 changes: 7 additions & 0 deletions kosmorrolib/i18n.py
Expand Up @@ -24,6 +24,13 @@

_ = _TRANSLATION.gettext

FULL_DATE_FORMAT = _('{day_of_week} {month} {day_number}, {year}').format(day_of_week='%A', month='%B',
day_number='%d', year='%Y')
SHORT_DATETIME_FORMAT = _('{month} {day_number}, {hours}:{minutes}').format(month='%b', day_number='%d',
hours='%H', minutes='%M')
SHORT_DATE_FORMAT = _('{month} {day_number}, {year}').format(month='%b', day_number='%d', year='%Y')
TIME_FORMAT = _('{hours}:{minutes}').format(hours='%H', minutes='%M')


def ngettext(msgid1, msgid2, number):
# Not using ngettext = _TRANSLATION.ngettext because the linter will give an invalid-name error otherwise
Expand Down

0 comments on commit c39cd3a

Please sign in to comment.