File: moonphases

package info (click to toggle)
undertime 4.3.1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 704 kB
  • sloc: python: 957; makefile: 6; sh: 2
file content (150 lines) | stat: -rwxr-xr-x 4,311 bytes parent folder | download | duplicates (2)
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)