1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
|
#!/usr/bin/python3
# XXX: can be written in 5 lines of Python with Skyfield:
#
# https://rhodesmill.org/skyfield/almanac.html#phases-of-the-moon
#
# Valhalla did something like this with the "astral" package:
#
# https://git.trueelena.org/software/pdfscripts/tree/planner/planner_generator.py?id=a8c5954f9c48a2485f3d198a949268ba78f1b339#n266
# https://github.com/sffjunkie/astral
#
# RFP filed in Debian:
# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=911646
"""
Show moon phases for a given year.
All times returned are local. Use the TZ variable to change.
"""
import argparse
from collections import namedtuple
from itertools import cycle
import ephem
MoonPhase = namedtuple("MoonPhase", "name motion target")
# logic extracted from ephem.next_new_moon() and following next*
# functions
moon_phases = (
MoonPhase("new", ephem.twopi, 0),
MoonPhase("first", ephem.twopi, ephem.halfpi),
MoonPhase("full", ephem.twopi, ephem.pi),
MoonPhase("third", ephem.twopi, ephem.pi + ephem.halfpi),
)
def find_sequence(date):
"""find the first moon after the given date
PyEphem only gives us mechanisms to find the next X type of moon,
not *any* moon. So we need to know the *first* kind of moon we
will get (regardless of type).
Then range_phases can iterate over those in the proper order
starting from the date.
>>> [ phase.name for phase in find_sequence('2018') ]
['full', 'third', 'new', 'first']
>>> [ phase.name for phase in find_sequence('2019') ]
['new', 'first', 'full', 'third']
>>> [ phase.name for phase in find_sequence('2020') ]
['first', 'full', 'third', 'new']
>>> [ phase.name for phase in find_sequence('2021') ]
['third', 'new', 'first', 'full']
"""
dates = [
(ephem._find_moon_phase(date, phase.motion, phase.target), phase)
for phase in moon_phases
]
return [phase for date, phase in sorted(dates)]
def range_phases(start, end, sequence):
"""generate the moon phases in the given range"""
p = start
for phase in cycle(sequence):
p = ephem._find_moon_phase(p, phase.motion, phase.target)
if p > end:
break
yield p, phase
def main(start, end=None):
"""Test data was checked against:
https://www.timeanddate.com/moon/phases/canada/montreal?year=...
Current year, starts with a full moon:
>>> main('2018') # doctest: +ELLIPSIS
2018-01-01 21:24:05 full
2018-01-08 17:25:16 third
2018-01-16 21:17:14 new
2018-01-24 17:20:23 first
2018-01-31 08:26:44 full
2018-02-07 10:53:56 third
...
2018-11-29 19:18:49 third
2018-12-07 02:20:21 new
2018-12-15 06:49:15 first
2018-12-22 12:48:35 full
2018-12-29 04:34:18 third
Test year, does not:
>>> main('2009') # doctest: +ELLIPSIS
2009-01-04 06:56:15 first
2009-01-10 22:26:46 full
2009-01-17 21:45:45 third
2009-01-26 02:55:18 new
2009-02-02 18:13:08 first
...
2009-11-24 16:39:14 first
2009-12-02 02:30:27 full
2009-12-08 19:13:20 third
2009-12-16 07:02:06 new
2009-12-24 12:35:58 first
2009-12-31 14:12:45 full
Another test:
>>> main('2042') # doctest: +ELLIPSIS
2042-01-06 03:53:29 full
2042-01-14 06:24:00 third
2042-01-21 15:41:39 new
2042-01-28 07:48:09 first
2042-02-04 20:57:20 full
...
2042-11-27 01:05:33 full
2042-12-04 04:18:21 third
2042-12-12 09:29:13 new
2042-12-19 19:27:06 first
2042-12-26 12:42:16 full
"""
if start is None:
start = ephem.now()
else:
start = ephem.Date(start)
if end is None:
# dates are in days (float). this adds a year
end = start + 30.0
else:
end = ephem.Date(end)
sequence = find_sequence(start)
for date, phase in range_phases(start, end, sequence):
print("{:%Y-%m-%d %H:%M:%S} {.name}".format(ephem.localtime(date), phase))
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description=__doc__, epilog="Should be rewritten with Skyfield."
)
parser.add_argument("start", nargs="?", help="start date (default: now)")
parser.add_argument("end", nargs="?", help="end date (default: next 30 days)")
args = parser.parse_args()
main(args.start, args.end)
|