├── .gitignore ├── LICENSE ├── README.md ├── main.py ├── pregenerated ├── lunar_calendar_2019.html ├── lunar_calendar_2020.html ├── lunar_calendar_2021.html ├── lunar_calendar_2022.html ├── lunar_calendar_2023.html ├── lunar_calendar_2024.html ├── lunar_calendar_2025.html ├── lunar_calendar_2026.html ├── lunar_calendar_2027.html ├── lunar_calendar_2028.html ├── lunar_calendar_2029.html ├── lunar_calendar_2030.html ├── lunar_calendar_2031.html ├── lunar_calendar_2032.html ├── lunar_calendar_2033.html ├── lunar_calendar_2034.html ├── lunar_calendar_2035.html ├── lunar_calendar_2036.html ├── lunar_calendar_2037.html ├── lunar_calendar_2038.html ├── lunar_calendar_2039.html ├── lunar_calendar_2040.html ├── lunar_calendar_2041.html ├── lunar_calendar_2042.html ├── lunar_calendar_2043.html ├── lunar_calendar_2044.html ├── lunar_calendar_2045.html ├── lunar_calendar_2046.html ├── lunar_calendar_2047.html ├── lunar_calendar_2048.html ├── lunar_calendar_2049.html └── lunar_calendar_2050.html └── template.html /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.pyc 3 | .idea 4 | ENV 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Rob Dawson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lunar Calendar 2 | 3 | This Python utility will generate an HTML Lunar Calendar for the year that you specify. 4 | To run the utility, pass the year as a command-line argument - for example: 5 | 6 | ``` 7 | python main.py 2018 8 | ``` 9 | 10 | The code uses the PyEphem library, which [you will need to install](https://rhodesmill.org/pyephem/#installation) before running it. 11 | 12 | When running the utility, the file `template.html` must be present in the current working directory. 13 | 14 | An example calendar is shown below: 15 | 16 | Lunar Calendar for 2018
17 | [link to generated HTML file](https://codebox.net/raw/lunar_calendar_2018.html) 18 | 19 | As well as displaying the phase of the moon for each day of the year, the calendar lists the exact dates and times of each full moon 20 | and new moon, highlighting any [blue moons](https://en.wikipedia.org/wiki/Blue_moon) or [black moons](https://en.wikipedia.org/wiki/Black_moon) that occur. 21 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import calendar 2 | import datetime 3 | import ephem # see http://rhodesmill.org/pyephem/ 4 | import math 5 | import sys 6 | 7 | ''' 8 | Lunar Calendar Generator 9 | 10 | This utility will generate an HTML Lunar Calendar for the year that you specify. 11 | To run the utility, pass the year as a command-line argument - for example: 12 | 13 | python main.py 2018 14 | 15 | When running the utility, the file 'template.html' must be present in the current 16 | working directory. 17 | 18 | Latest version of source code available from: 19 | https://github.com/codebox/lunar-calendar 20 | 21 | Project home page: 22 | https://codebox.net/pages/lunar-calendar 23 | 24 | This source code is released under the MIT Open Source License 25 | 26 | © 2018 Rob Dawson 27 | ''' 28 | 29 | class Calendar: 30 | def __init__(self): 31 | self.html = open('template.html').read() 32 | 33 | def _replace_in_html(self, key, value): 34 | self.html = self.html.replace(''.format(key), value) 35 | 36 | def _calc_terminator_arc(self, lunation, disc_radius): 37 | right_of_centre = None 38 | lit_from_left = None 39 | L = None 40 | 41 | if lunation <= 0.25: 42 | L = lunation 43 | right_of_centre = True 44 | lit_from_left = False 45 | 46 | elif lunation <= 0.5: 47 | L = 0.5 - lunation 48 | right_of_centre = False 49 | lit_from_left = False 50 | 51 | elif lunation <= 0.75: 52 | L = lunation - 0.5 53 | right_of_centre = True 54 | lit_from_left = True 55 | 56 | else: 57 | L = 1 - lunation 58 | right_of_centre = False 59 | lit_from_left = True 60 | 61 | x = disc_radius * (1 - math.cos(2 * math.pi * L)) 62 | n = disc_radius - x 63 | terminator_arc_radius = (disc_radius * disc_radius + n * n) / (2 * n) 64 | 65 | return terminator_arc_radius, right_of_centre, lit_from_left 66 | 67 | def _make_path(self, lunation, view_box_size): 68 | terminator_arc_radius, right_of_centre, lit_from_left = self._calc_terminator_arc(lunation, view_box_size/2) 69 | 70 | LIGHT_CSS_CLASS = 'light' 71 | SHADOW_CSS_CLASS = 'shadow' 72 | 73 | colour_left = LIGHT_CSS_CLASS if lit_from_left else SHADOW_CSS_CLASS 74 | colour_right = SHADOW_CSS_CLASS if lit_from_left else LIGHT_CSS_CLASS 75 | 76 | move_to_top = 'M{0},0'.format(view_box_size/2) 77 | disc_left_arc = 'A {0} {0} 0 0 1 {0} 0'.format(view_box_size/2) 78 | disc_right_arc = 'A {0} {0} 0 0 0 {0} 0'.format(view_box_size/2) 79 | terminator_arc = 'A {0} {0} 0 0 {1} {2} {3}'.format( 80 | terminator_arc_radius, 1 if right_of_centre else 0, view_box_size/2, view_box_size) 81 | 82 | path_left = ''.format(move_to_top, terminator_arc, disc_left_arc, colour_left) 83 | path_right = ''.format(move_to_top, terminator_arc, disc_right_arc, colour_right) 84 | 85 | return path_left + path_right 86 | 87 | def _generate_moon(self, year, month, day): 88 | date = ephem.Date(datetime.date(year, month, day)) 89 | 90 | preceding_new_moon = ephem.previous_new_moon(date) 91 | following_new_moon = ephem.next_new_moon(date) 92 | 93 | lunation = (date - preceding_new_moon) / (following_new_moon - preceding_new_moon) 94 | 95 | VIEW_BOX_SIZE = 100 96 | return '{1}'.format(VIEW_BOX_SIZE, self._make_path(lunation, VIEW_BOX_SIZE)) 97 | 98 | def _get_moon_dates(self, year, next_fn): 99 | start_of_year = ephem.Date(datetime.date(year, 1, 1)) 100 | end_of_year = ephem.Date(datetime.date(year + 1, 1, 1)) 101 | 102 | moon_dates = [] 103 | 104 | date = start_of_year 105 | previous_month = None 106 | while date < end_of_year: 107 | date = next_fn(date) 108 | date_and_time = date.datetime() 109 | 110 | formatted_date = date_and_time.strftime('%d %b %H:%M') 111 | second_in_month = date_and_time.month == previous_month 112 | 113 | moon_dates.append((formatted_date, second_in_month)) 114 | previous_month = date_and_time.month 115 | 116 | return moon_dates[:-1] 117 | 118 | def _moon_key(self, m, d): 119 | return 'MOON_{:02d}_{:02d}'.format(m, d) 120 | 121 | def populate(self, year): 122 | for month in range(1, 13): 123 | _, days_in_month = calendar.monthrange(year, month) 124 | for day in range(1, days_in_month + 1): 125 | key = self._moon_key(month, day) 126 | moon = self._generate_moon(year, month, day) 127 | self._replace_in_html(key, moon) 128 | 129 | new_moon_dates = self._get_moon_dates(year, ephem.next_new_moon) 130 | full_moon_dates = self._get_moon_dates(year, ephem.next_full_moon) 131 | 132 | def build_markup(moon_dates, second_in_month_class): 133 | markup = [] 134 | for moon_date in moon_dates: 135 | date, second_in_month = moon_date 136 | markup.append('{}'.format(second_in_month_class if second_in_month else '', date)) 137 | 138 | return markup 139 | 140 | self._replace_in_html('YEAR', str(year)) 141 | 142 | new_moon_markup = build_markup(new_moon_dates, 'blackMoon') 143 | full_moon_markup = build_markup(full_moon_dates, 'blueMoon') 144 | 145 | self._replace_in_html('NEW_MOONS', ''.join(new_moon_markup)) 146 | self._replace_in_html('FULL_MOONS', ''.join(full_moon_markup)) 147 | 148 | 149 | def save(self, path): 150 | open(path, 'w').write(self.html) 151 | 152 | if __name__ == '__main__': 153 | year = None 154 | try: 155 | year = int(sys.argv[1]) 156 | except: 157 | print('Error, please specify a year: python {} '.format(sys.argv[0])) 158 | sys.exit(1) 159 | 160 | cal = Calendar() 161 | cal.populate(year) 162 | output_file = 'lunar_calendar_{}.html'.format(year) 163 | cal.save(output_file) 164 | 165 | print('Success! Calendar saved to file {}'.format(output_file)) 166 | -------------------------------------------------------------------------------- /pregenerated/lunar_calendar_2021.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Lunar Calendar 2021 5 | 80 | 81 | 82 | 83 | 84 |

Lunar Calendar 2021

85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 104 | 105 | 106 | 109 | 110 |
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
Jan
Feb
Mar
Apr
May
Jun
Jul
Aug
Sep
Oct
Nov
Dec
 
102 | ⬤

Full Moon:

28 Jan 19:1627 Feb 08:1728 Mar 18:4827 Apr 03:3126 May 11:1324 Jun 18:3924 Jul 02:3622 Aug 12:0120 Sep 23:5420 Oct 14:5619 Nov 08:5719 Dec 04:35 103 |
107 | ◯

New Moon:

13 Jan 05:0011 Feb 19:0513 Mar 10:2112 Apr 02:3011 May 18:5910 Jun 10:5210 Jul 01:1608 Aug 13:5007 Sep 00:5106 Oct 11:0504 Nov 21:1404 Dec 07:42 108 |
111 | 112 |
113 | 114 | 115 | -------------------------------------------------------------------------------- /pregenerated/lunar_calendar_2029.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Lunar Calendar 2029 5 | 80 | 81 | 82 | 83 | 84 |

Lunar Calendar 2029

85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 104 | 105 | 106 | 109 | 110 |
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
Jan
Feb
Mar
Apr
May
Jun
Jul
Aug
Sep
Oct
Nov
Dec
 
102 | ⬤

Full Moon:

30 Jan 06:0328 Feb 17:1030 Mar 02:2628 Apr 10:3627 May 18:3726 Jun 03:2225 Jul 13:3524 Aug 01:5122 Sep 16:2922 Oct 09:2721 Nov 04:0220 Dec 22:46 103 |
107 | ◯

New Moon:

14 Jan 17:2413 Feb 10:3115 Mar 04:1913 Apr 21:4013 May 13:4112 Jun 03:5011 Jul 15:5010 Aug 01:5508 Sep 10:4407 Oct 19:1406 Nov 04:2305 Dec 14:51 108 |
111 | 112 |
113 | 114 | 115 | -------------------------------------------------------------------------------- /template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Lunar Calendar <!-- YEAR --> 5 | 80 | 81 | 82 | 83 | 84 |

Lunar Calendar

85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 104 | 105 | 106 | 109 | 110 |
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
Jan
Feb
Mar
Apr
May
Jun
Jul
Aug
Sep
Oct
Nov
Dec
 
102 | ⬤

Full Moon:

103 |
107 | ◯

New Moon:

108 |
111 | 112 |
113 | 114 | 115 | --------------------------------------------------------------------------------