|
| 1 | +:mod:`suntime` -- sunrise and sunset time |
| 2 | +========================================= |
| 3 | + |
| 4 | +.. module:: suntime |
| 5 | + :synopsis: sunrise and sunset time |
| 6 | + |
| 7 | +This library provides an approximated calculation of sunrise and sunset time. |
| 8 | +It contains two classes, :class:`Sundatetime` and :class:`Suntime`, which |
| 9 | +differ from the input and output data types. |
| 10 | + |
| 11 | + |
| 12 | +Class :class:`Sundatetime` |
| 13 | +-------------------------- |
| 14 | + |
| 15 | +The following class makes use of module :mod:`datetime` for expressing input |
| 16 | +date and output time. |
| 17 | + |
| 18 | +.. class:: Sundatetime(latitude, longitude, altitude=0) |
| 19 | + |
| 20 | + Arguments *latitude* and *longitude* are floats representing the |
| 21 | + coordinates of a place on Earth. *altitude* is an integer number for |
| 22 | + observer's elevation in meters. |
| 23 | + |
| 24 | +.. method:: Sundatetime.calc_sunrise_sunset(date) |
| 25 | + |
| 26 | + Calculate the sunrise and sunset for the given date. *date* must be an |
| 27 | + *aware* `datetime.datetime` object in the range [2000-01-01; 2100-01-01). |
| 28 | + Time information is ignored, whereas time zone ``tzinfo`` is used to |
| 29 | + provide meaningful output. The results are cached in :data:`sunrise` and |
| 30 | + :data:`sunset` . |
| 31 | + |
| 32 | +.. method:: Sundatetime.is_daytime(now) |
| 33 | + Sundatetime.is_nighttime(now) |
| 34 | + |
| 35 | + Argument *now* is an *aware* `datetime.datetime` object representing a |
| 36 | + point in time. A boolean value is returned whether Sun is above/below the |
| 37 | + horizon or not, ``None`` when data are meaningless. |
| 38 | + |
| 39 | +.. method:: Sundatetime.is_sunrise(now) |
| 40 | + Sundatetime.is_sunset(now) |
| 41 | + |
| 42 | + Argument *now* is an *aware* `datetime.datetime` object representing a |
| 43 | + point in time. A boolean value is returned whether *now* matches |
| 44 | + sunrise/sunset time or not, ``None`` when data are meaningless. |
| 45 | + |
| 46 | +The following instance variables are accessible: |
| 47 | + |
| 48 | +.. data:: Sundatetime.latitude |
| 49 | + Sundatetime.longitude |
| 50 | + |
| 51 | + Float numbers for coordinates on Earth. |
| 52 | + |
| 53 | +.. data:: Sundatetime.altitude |
| 54 | + |
| 55 | + Elevation in meters for observations above the sea horizon. It corrects |
| 56 | + for both apparent dip and terrestrial refraction. |
| 57 | + |
| 58 | +.. data:: Sundatetime.sunrise |
| 59 | + Sundatetime.sunset |
| 60 | + |
| 61 | + They hold ``None`` when an instance is created, an *aware* |
| 62 | + `datetime.datetime` after :meth:`calc_sunrise_sunset` is called. |
| 63 | + |
| 64 | +.. note:: |
| 65 | + :data:`sunrise` may occur before 00:00 and :data:`sunset` after 23:59 |
| 66 | + on calculated *date*. See :ref:`unexpected-results`. |
| 67 | + |
| 68 | + |
| 69 | +Class :class:`Suntime` |
| 70 | +---------------------- |
| 71 | + |
| 72 | +The following class makes use of plain integers for expressing input date and |
| 73 | +output time. |
| 74 | + |
| 75 | +.. class:: Suntime(latitude, longitude, altitude=0, timezone=0) |
| 76 | + |
| 77 | + Arguments *latitude* and *longitude* are floats representing the |
| 78 | + coordinates of a place on Earth. *altitude* is an integer number for |
| 79 | + observer's elevation in meters. *timezone* is an integer holding the |
| 80 | + timezone offset from UTC in minutes. The results are cached in |
| 81 | + :data:`sunrise` and :data:`sunset`. |
| 82 | + |
| 83 | +.. method:: Suntime.calc_sunrise_sunset(year, month, day, dst=0) |
| 84 | + |
| 85 | + Calculate the sunrise and sunset for the given year, month and day. |
| 86 | + *year* must be in the range [2000; 2100). *dst* is an integer holding the |
| 87 | + offset in minute (usually 60) that accounts for Daylight Saving Time. |
| 88 | + |
| 89 | +.. method:: Suntime.is_daytime(now) |
| 90 | + Suntime.is_nighttime(now) |
| 91 | + |
| 92 | + Argument *now* is an integer holding the number of minutes since midnight. |
| 93 | + A boolean value is returned whether Sun is above/below the horizon or not, |
| 94 | + ``None`` when data are meaningless. |
| 95 | + |
| 96 | +.. method:: Suntime.is_sunrise(now) |
| 97 | + Suntime.is_sunset(now) |
| 98 | + |
| 99 | + Argument *now* is an an integer holding the number of minutes since midnight. |
| 100 | + A boolean value is returned whether *now* matches sunrise/sunset time or not, |
| 101 | + ``None`` when data are meaningless. |
| 102 | + |
| 103 | +The following instance variables are accessible: |
| 104 | + |
| 105 | +.. data:: Suntime.latitude |
| 106 | + Suntime.longitude |
| 107 | + |
| 108 | + Float numbers for coordinates on Earth. |
| 109 | + |
| 110 | +.. data:: Suntime.altitude |
| 111 | + |
| 112 | + Elevation in meters for observations above the sea horizon. It corrects |
| 113 | + for both apparent dip and terrestrial refraction. |
| 114 | + |
| 115 | +.. data:: Suntime.sunrise |
| 116 | + Suntime.sunset |
| 117 | + |
| 118 | + It holds ``None`` when an instance is created, an integer for the |
| 119 | + difference in minutes since 00:00 after :meth:`calc_sunrise_sunset` |
| 120 | + is called. |
| 121 | + |
| 122 | +.. note:: |
| 123 | + ``sunrise`` may be negative and ``sunset`` may be greater than 1439 |
| 124 | + (24 hours). See :ref:`unexpected-results`. |
| 125 | + |
| 126 | +.. _unexpected-results: |
| 127 | + |
| 128 | +Unexpected results |
| 129 | +------------------ |
| 130 | + |
| 131 | +:class:`Sundatetime` may return unexpected results: :data:`Sundatetime.sunrise` |
| 132 | +may come before 00:00 and/or :data:`Sundatetime.sunset` may come after 23:59. |
| 133 | +Similarly, :class:`Suntime` may return a negative :data:`Suntime.sunrise` |
| 134 | +and/or a :data:`Suntime.sunset` greater or equal to 1440 (24 hours). |
| 135 | + |
| 136 | +Assuming ``date`` is the current date and ``now`` is the current time, |
| 137 | +the conditions which may lead to unexpected results are: |
| 138 | + |
| 139 | +* *incorrect time zone*: *date*'s time zone is not consistent with provided |
| 140 | + *longitude*. Suitable values for *timezone* and *dst* should be provided. |
| 141 | + See example #3 (Novosibirsk) below. |
| 142 | + |
| 143 | +* *Sun is up all day*: close to the poles, Sun never sets in summer/winter |
| 144 | + time. For this dates, ``is_daytime()`` holds true for the whole ``date``. |
| 145 | + Note that the following holds true: ``sunrise ≤ now < sunset``. |
| 146 | + |
| 147 | +* *Sun is down all day*: close to the poles, Sun never raises in summer/winter |
| 148 | + time. For this dates, ``is_nighttime()`` holds true for the whole ``date``. |
| 149 | + Note that the following holds false: ``sunrise < sunset``. |
| 150 | + |
| 151 | +* *Sun sets after midnight*: close to the poles, Sun may sets after |
| 152 | + 23:59. In this case, ``is_daytime()`` and ``is_nighttime()`` behave as |
| 153 | + expected. The following condition is true: ``sunrise ≤ now ≤ 23:59 < sunset``. |
| 154 | + |
| 155 | + |
| 156 | +Examples of usage |
| 157 | +----------------- |
| 158 | + |
| 159 | +A typical use case is:: |
| 160 | + |
| 161 | + import datetime, suntime, time |
| 162 | + |
| 163 | + class Cet(datetime.timezone): |
| 164 | + # See `datetime` documentation |
| 165 | + |
| 166 | + # initialization |
| 167 | + CET = Cet() |
| 168 | + Rome = suntime.Sundatetime(42.5966460, 12.4360233) |
| 169 | + Rome.calc_sunrise_sunset(datetime.datetime(2000, 1, 1, tzinfo=CET)) |
| 170 | + |
| 171 | + # main loop (every minute or more) |
| 172 | + now = datetime.datetime(*time.localtime()[:5], tzinfo=CET) |
| 173 | + if (now.date() > Rome.sunset.date()): |
| 174 | + Rome.calc_sunrise_sunset(now) |
| 175 | + Rome.is_daytime(now) |
| 176 | + |
| 177 | +The following script shows sunrise and sunset time for several places and dates:: |
| 178 | + |
| 179 | + # place: latitude longitude |
| 180 | + pl1 = ( 42.5966460, 12.4360233) # Rome |
| 181 | + pl2 = ( 51.1627938,-122.9593616) # Vancouver |
| 182 | + pl3 = (-33.9252192, 18.4240762) # CapeTown |
| 183 | + pl4 = ( 55.1574890, 82.8547661) # Novosibirsk |
| 184 | + pl5 = ( 78.6560170, 16.3447384) # Pyramiden |
| 185 | + pl6 = pl5 |
| 186 | + pl7 = (-77.7817838, 166.4561470) # McMurdo |
| 187 | + pl8 = pl7 |
| 188 | + |
| 189 | + # date: YY MM DD sunrise sunset |
| 190 | + dt1 = (2000, 1, 1) # 7:37 16:49 - https://www.timeanddate.com/sun/italy/rome?month=1&year=2000 |
| 191 | + dt2 = (2014, 10, 3) # 7:15 18:46 - https://www.timeanddate.com/sun/canada/vancouver?month=10&year=2014 |
| 192 | + dt3 = (2016, 12, 21) # 5:32 19:57 - https://www.timeanddate.com/sun/south-africa/cape-town?month=12&year=2016 |
| 193 | + dt4 = (2021, 4, 24) # 6:04 20:50 - https://www.timeanddate.com/sun/russia/novosibirsk?month=4&year=2021 |
| 194 | + dt5 = (2040, 8, 25) # up all day - https://www.timeanddate.com/sun/@2729216?month=8&year=2033 |
| 195 | + dt6 = (2040, 8, 26) # 00:09 |
| 196 | + # 1:45 23:41 - https://www.timeanddate.com/sun/@2729216?month=8&year=2040 |
| 197 | + dt7 = (2033, 8, 10) # down all day - https://www.timeanddate.com/sun/antarctica/mcmurdo?month=8&year=2033 |
| 198 | + dt8 = (2033, 10, 21) # 3:00 24:13 - https://www.timeanddate.com/sun/antarctica/mcmurdo?month=10&year=2033 |
| 199 | + |
| 200 | + # timezone offsets and DSTs (in hours) |
| 201 | + tz1 = ( 1, 0) |
| 202 | + tz2 = (-8, 1) |
| 203 | + tz3 = ( 2, 0) |
| 204 | + tz4 = ( 0, 0) # wrong; it generates negative hour because actual timezone is (7, 0) |
| 205 | + tz5 = ( 1, 1) |
| 206 | + tz6 = ( 1, 1) |
| 207 | + tz7 = (13,-1) |
| 208 | + tz8 = (13, 0) |
| 209 | + |
| 210 | +The following snippet of code makes use of class :class:`Sundatetime`:: |
| 211 | + |
| 212 | + from suntime import Sundatetime |
| 213 | + from datetime import datetime, timedelta, timezone |
| 214 | + |
| 215 | + class Tz(timezone): |
| 216 | + def __init__(self, hours, dst=0): |
| 217 | + super().__init__(timedelta(hours=hours)) |
| 218 | + self._dst = dst |
| 219 | + |
| 220 | + def dst(self, dt): |
| 221 | + return timedelta(hours=self._dst) if self.isdst(dt) else timedelta(0) |
| 222 | + |
| 223 | + def isdst(self, dt): |
| 224 | + return self._dst != 0 |
| 225 | + |
| 226 | + print('Rome:') |
| 227 | + sd1 = Sundatetime(*pl1) |
| 228 | + sd1.calc_sunrise_sunset(datetime(*dt1, tzinfo=Tz(*tz1))) |
| 229 | + print('>', sd1.sunrise) # 2000-01-01 07:40:00+01:00 |
| 230 | + print('>', sd1.sunset ) # 2000-01-01 16:47:00+01:00 |
| 231 | + |
| 232 | + print('Vancouver:') |
| 233 | + sd2 = Sundatetime(*pl2) |
| 234 | + sd2.calc_sunrise_sunset(datetime(*dt2, tzinfo=Tz(*tz2))) |
| 235 | + print('>', sd2.sunrise) # 2014-10-03 07:16:00-08:00 |
| 236 | + print('>', sd2.sunset ) # 2014-10-03 18:46:00-08:00 |
| 237 | + |
| 238 | + print('Cape Town:') |
| 239 | + sd3 = Sundatetime(*pl3) |
| 240 | + sd3.calc_sunrise_sunset(datetime(*dt3, tzinfo=Tz(*tz3))) |
| 241 | + print('>', sd3.sunrise) # 2016-12-21 05:32:00+02:00 |
| 242 | + print('>', sd3.sunset ) # 2016-12-21 19:57:00+02:00 |
| 243 | + |
| 244 | + print('Novosibirsk:') |
| 245 | + sd4 = Sundatetime(*pl4) |
| 246 | + sd4.calc_sunrise_sunset(datetime(*dt4, tzinfo=Tz(*tz4))) |
| 247 | + print('>', sd4.sunrise) # 2021-04-23 23:04:00+00:00 |
| 248 | + print('>', sd4.sunset ) # 2021-04-24 13:49:00+00:00 |
| 249 | + |
| 250 | + print('Pyramiden:') |
| 251 | + sd5 = Sundatetime(*pl5) |
| 252 | + sd5.calc_sunrise_sunset(datetime(*dt5, tzinfo=Tz(*tz5))) |
| 253 | + print('>', sd5.sunrise) # 2040-08-24 12:57:00+02:00 |
| 254 | + print('>', sd5.sunset ) # 2040-08-26 12:57:00+02:00 |
| 255 | + |
| 256 | + print('Pyramiden:') |
| 257 | + sd6 = Sundatetime(*pl6) |
| 258 | + sd6.calc_sunrise_sunset(datetime(*dt6, tzinfo=Tz(*tz6))) |
| 259 | + print('>', sd6.sunrise) # 2040-08-26 01:35:00+02:00 |
| 260 | + print('>', sd6.sunset ) # 2040-08-27 00:18:00+02:00 |
| 261 | + |
| 262 | + print('McMurdo:') |
| 263 | + sd7 = Sundatetime(*pl7) |
| 264 | + sd7.calc_sunrise_sunset(datetime(*dt7, tzinfo=Tz(*tz7))) |
| 265 | + print('>', sd7.sunrise) # 2033-08-11 13:00:00+12:00 |
| 266 | + print('>', sd7.sunset ) # 2033-08-09 13:00:00+12:00 |
| 267 | + |
| 268 | + print('McMurdo:') |
| 269 | + sd8 = Sundatetime(*pl8) |
| 270 | + sd8.calc_sunrise_sunset(datetime(*dt8, tzinfo=Tz(*tz8))) |
| 271 | + print('>', sd8.sunrise) # 2033-10-21 03:06:00+13:00 |
| 272 | + print('>', sd8.sunset ) # 2033-10-22 00:12:00+13:00 |
| 273 | + |
| 274 | +If :mod:`datetime` module is not available, the same input data can feed class |
| 275 | +:class:`Suntime`:: |
| 276 | + |
| 277 | + from suntime import Suntime |
| 278 | + |
| 279 | + st1 = Suntime(*pl1, timezone=tz1[0]*60) |
| 280 | + st1.calc_sunrise_sunset(*dt1, dst=tz1[1]*60) |
| 281 | + print('Rome:') |
| 282 | + print('>', divmod(st1.sunrise, 60)) # (7, 40) |
| 283 | + print('>', divmod(st1.sunset , 60)) # (16, 47) |
| 284 | + |
| 285 | + st2 = Suntime(*pl2, timezone=tz2[0]*60) |
| 286 | + st2.calc_sunrise_sunset(*dt2, dst=tz2[1]*60) |
| 287 | + print('Vancouver:') |
| 288 | + print('>', divmod(st2.sunrise, 60)) # (7, 16) |
| 289 | + print('>', divmod(st2.sunset , 60)) # (18, 46) |
| 290 | + |
| 291 | + st3 = Suntime(*pl3, timezone=tz3[0]*60) |
| 292 | + st3.calc_sunrise_sunset(*dt3, dst=tz3[1]*60) |
| 293 | + print('Cape Town:') |
| 294 | + print('>', divmod(st3.sunrise, 60)) # (5, 32) |
| 295 | + print('>', divmod(st3.sunset , 60)) # (19, 57) |
| 296 | + |
| 297 | + st4 = Suntime(*pl4, timezone=tz4[0]*60) |
| 298 | + st4.calc_sunrise_sunset(*dt4, dst=tz4[1]*60) |
| 299 | + print('Novosibirsk:') |
| 300 | + print('>', divmod(st4.sunrise, 60)) # (-1, 4) |
| 301 | + print('>', divmod(st4.sunset , 60)) # (13, 49) |
| 302 | + |
| 303 | + st5 = Suntime(*pl5, timezone=tz5[0]*60) |
| 304 | + st5.calc_sunrise_sunset(*dt5, dst=tz5[1]*60) |
| 305 | + print('Pyramiden:') |
| 306 | + print('>', divmod(st5.sunrise, 60)) # (-12, 57) |
| 307 | + print('>', divmod(st5.sunset , 60)) # (36, 57) |
| 308 | + |
| 309 | + st6 = Suntime(*pl6, timezone=tz6[0]*60) |
| 310 | + st6.calc_sunrise_sunset(*dt6, dst=tz6[1]*60) |
| 311 | + print('Pyramiden:') |
| 312 | + print('>', divmod(st6.sunrise, 60)) # (1, 35) |
| 313 | + print('>', divmod(st6.sunset , 60)) # (24, 18) |
| 314 | + |
| 315 | + st7 = Suntime(*pl7, timezone=tz7[0]*60) |
| 316 | + st7.calc_sunrise_sunset(*dt7, dst=tz7[1]*60) |
| 317 | + print('McMurdo:') |
| 318 | + print('>', divmod(st7.sunrise, 60)) # (37, 0) |
| 319 | + print('>', divmod(st7.sunset , 60)) # (-11, 0) |
| 320 | + |
| 321 | + st8 = Suntime(*pl8, timezone=tz8[0]*60) |
| 322 | + st8.calc_sunrise_sunset(*dt8, dst=tz8[1]*60) |
| 323 | + print('McMurdo:') |
| 324 | + print('>', divmod(st8.sunrise, 60)) # (3, 6) |
| 325 | + print('>', divmod(st8.sunset , 60)) # (24, 12) |
0 commit comments