File Coverage

blib/lib/Astro/Sunrise.pm
Criterion Covered Total %
statement 221 257 85.9
branch 72 84 85.7
condition 33 40 82.5
subroutine 40 42 95.2
pod 11 21 52.3
total 377 444 84.9


line stmt bran cond sub pod time code
1             # -*- encoding: utf-8; indent-tabs-mode: nil -*-
2             #
3             # Perl extension for computing the sunrise/sunset on a given day
4             # Copyright (C) 1999-2003, 2013, 2015, 2017, 2019 Ron Hill and Jean Forget
5             #
6             # See the license in the embedded documentation below.
7             #
8             package Astro::Sunrise;
9              
10 11     11   1135605 use strict;
  11         122  
  11         331  
11 11     11   59 use warnings;
  11         35  
  11         330  
12 11     11   1074 use POSIX qw(floor);
  11         16649  
  11         76  
13 11     11   14086 use Math::Trig;
  11         171234  
  11         1571  
14 11     11   99 use Carp;
  11         23  
  11         694  
15 11     11   80 use vars qw( $VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $RADEG $DEGRAD );
  11         62  
  11         13611  
16              
17             require Exporter;
18              
19             @ISA = qw( Exporter );
20             @EXPORT = qw( sunrise sun_rise sun_set );
21             @EXPORT_OK = qw( DEFAULT CIVIL NAUTICAL AMATEUR ASTRONOMICAL sind cosd tand asind acosd atand atan2d equal );
22             %EXPORT_TAGS = (
23             constants => [ qw/DEFAULT CIVIL NAUTICAL AMATEUR ASTRONOMICAL/ ],
24             trig => [ qw/sind cosd tand asind acosd atand atan2d equal/ ],
25             );
26              
27             $VERSION = '0.98';
28             $RADEG = ( 180 / pi );
29             $DEGRAD = ( pi / 180 );
30             my $INV360 = ( 1.0 / 360.0 );
31              
32             sub sun_rise {
33 11     11 1 4693 my ($sun_rise, undef) = sun_rise_sun_set(@_);
34 8         32 return $sun_rise;
35             }
36             sub sun_set {
37 8     8 1 2353 my (undef, $sun_set) = sun_rise_sun_set(@_);
38 8         30 return $sun_set;
39             }
40              
41             sub sun_rise_sun_set {
42 19     19 0 52 my %arg;
43 19 100       54 if (ref($_[0]) eq 'HASH') {
44 11         19 %arg = %{$_[0]};
  11         48  
45             }
46             else {
47 8         27 @arg{ qw/lon lat alt offset/ } = @_;
48             }
49              
50             # This trick aims to fulfill two antagonistic purposes:
51             # -- do not load DateTime if the only function called is "sunrise"
52             # -- load DateTime implicitly if the user calls "sun_rise" or "sun_set". This is to be backward
53             # compatible with 0.92 or earlier, when Astro::Sunrise would load DateTime and thus, allow
54             # the user to remove this line from his script.
55 19 50       55 unless ($INC{DateTime}) {
56 19     3   1288 eval "use DateTime";
  3     3   28  
  3     3   5  
  3     2   52  
  3     1   21  
  3     1   6  
  3     1   41  
  3     1   25  
  3     1   6  
  3     1   39  
  2     1   14  
  2     1   11  
  2         28  
  1         7  
  1         3  
  1         14  
  1         7  
  1         27  
  1         17  
  1         7  
  1         2  
  1         29  
  1         7  
  1         1  
  1         14  
  1         7  
  1         6  
  1         19  
  1         6  
  1         2  
  1         14  
  1         8  
  1         2  
  1         14  
  1         7  
  1         2  
  1         14  
57 19 50       64 croak $@
58             if $@;
59             }
60              
61 19         59 my ($longitude, $latitude) = @arg{ qw/lon lat/ };
62 19 100       53 my $alt = defined($arg{alt} ) ? $arg{alt} : -0.833;
63 19 100       45 my $offset = defined($arg{offset} ) ? int($arg{offset}) : 0 ;
64 19 100       48 my $tz = defined($arg{time_zone}) ? $arg{time_zone} : 'local';
65 19   50     90 $arg{precise} ||= 0;
66 19   50     75 $arg{upper_limb} ||= 0;
67 19   100     76 $arg{polar} ||= 'warn';
68 19   50     85 $arg{trace} ||= 0;
69 19 100       139 croak "Longitude parameter (keyword: 'lon') is mandatory"
70             unless defined $longitude;
71 18 100       146 croak "Latitude parameter (keyword: 'lat') is mandatory"
72             unless defined $latitude;
73             croak "Wrong value of the 'polar' argument: should be either 'warn' or 'retval'"
74 17 100 66     156 if $arg{polar} ne 'warn' and $arg{polar} ne 'retval';
75              
76 16         61 my $today = DateTime->today(time_zone => $tz);
77 16         40703 $today->set( hour => 12 );
78 16         8834 $today->add( days => $offset );
79              
80             my( $sun_rise, $sun_set ) = sunrise( { year => $today->year,
81             month => $today->mon,
82             day => $today->mday,
83             lon => $longitude,
84             lat => $latitude,
85             tz => ( $today->offset / 3600 ),
86             #
87             # DST is always 0 because DateTime
88             # currently (v 0.16) adds one to the
89             # offset during DST hours
90             isdst => 0,
91             alt => $alt,
92             precise => $arg{precise},
93             upper_limb => $arg{upper_limb},
94             polar => $arg{polar},
95             trace => $arg{trace},
96 16         2753 } );
97 16         146 return ($sun_rise, $sun_set);
98             }
99              
100             sub sunrise {
101 790     790 1 734906 my %arg;
102 790 100       2207 if (ref($_[0]) eq 'HASH') {
103 672         979 %arg = %{$_[0]};
  672         3992  
104             }
105             else {
106 118         607 @arg{ qw/year month day lon lat tz isdst alt precise/ } = @_;
107             }
108             my ( $year, $month, $day, $lon, $lat, $TZ, $isdst, $trace)
109 790         2958 = @arg{ qw/year month day lon lat tz isdst trace/ };
110 790 100       1947 my $altit = defined($arg{alt} ) ? $arg{alt} : -0.833;
111 790   100     3252 $arg{precise} ||= 0;
112 790   100     2495 $arg{upper_limb} ||= 0;
113 790   100     2183 $arg{polar} ||= 'warn';
114 790   50     2642 $trace ||= 0;
115 790 100       1533 croak "Year parameter is mandatory"
116             unless defined $year;
117 789 100       1541 croak "Month parameter is mandatory"
118             unless defined $month;
119 788 100       1444 croak "Day parameter is mandatory"
120             unless defined $day;
121 787 100       1434 croak "Longitude parameter (keyword: 'lon') is mandatory"
122             unless defined $lon;
123 786 100       1445 croak "Latitude parameter (keyword: 'lat') is mandatory"
124             unless defined $lat;
125             croak "Wrong value of the 'polar' argument: should be either 'warn' or 'retval'"
126 785 100 100     2511 if $arg{polar} ne 'warn' and $arg{polar} ne 'retval';
127              
128 784 100       1485 if ($arg{precise}) {
129             # This is the initial start
130 182         413 my $d = days_since_2000_Jan_0($year, $month, $day) - $lon / 360.0;
131              
132 182 50       388 if ($trace) {
133 0         0 print $trace "Precise computation of sunrise for $year-$month-$day, lon $lon, lat $lat, altitude $altit, upper limb $arg{upper_limb}\n";
134             }
135 182         276 my $h1 = 12; # noon, then sunrise
136 182         362 for my $counter (1..9) {
137             # 9 is a failsafe precaution against a possibly runaway loop
138             # but hopefully, we will leave the loop long before, with "last"
139 267         338 my $h2;
140 267         819 ($h2, undef) = sun_rise_set($d + $h1 / 24, $lon, $lat, $altit, 15.04107, $arg{upper_limb}, $arg{polar}, $trace);
141 267 100 100     1202 if ($h2 eq 'day' or $h2 eq 'night') {
142 150         210 $h1 = $h2;
143 150         240 last;
144             }
145 117 100       258 if (equal($h1, $h2, 5)) {
146             # equal within 1e-5 hour, a little less than a second
147 31         49 $h1 = $h2;
148 31         53 last;
149             }
150 86         217 $h1 = $h2;
151             }
152              
153 182 50       327 if ($trace) {
154 0         0 print $trace "Precise computation of sunset for $year-$month-$day, lon $lon, lat $lat, altitude $altit, upper limb $arg{upper_limb}\n";
155             }
156 182         237 my $h3 = 12; # noon at first, then sunset
157 182         337 for my $counter (1..9) {
158             # 9 is a failsafe precaution against a possibly runaway loop
159             # but hopefully, we will leave the loop long before, with "last"
160 256         382 my $h4;
161 256         656 (undef, $h4) = sun_rise_set($d + $h3 / 24, $lon, $lat, $altit, 15.04107, $arg{upper_limb}, $arg{polar}, $trace);
162 256 100 100     1274 if ($h4 eq 'day' or $h4 eq 'night') {
163 150         209 $h3 = $h4;
164 150         223 last;
165             }
166 106 100       230 if (equal($h3, $h4, 5)) {
167             # equal within 1e-5 hour, a little less than a second
168 31         50 $h3 = $h4;
169 31         52 last;
170             }
171 75         207 $h3 = $h4;
172             }
173              
174 182         373 return convert_hour($h1, $h3, $TZ, $isdst);
175              
176             }
177             else {
178 602 50       1052 if ($trace) {
179 0         0 print $trace "Basic computation of sunrise and sunset for $year-$month-$day, lon $lon, lat $lat, altitude $altit, upper limb $arg{upper_limb}\n";
180             }
181 602         1224 my $d = days_since_2000_Jan_0( $year, $month, $day ) + 0.5 - $lon / 360.0;
182 602         1560 my ($h1, $h2) = sun_rise_set($d, $lon, $lat, $altit, 15.0, $arg{upper_limb}, $arg{polar}, $trace);
183 602 50 100     6045 if ($h1 eq 'day' or $h1 eq 'night' or $h2 eq 'day' or $h2 eq 'night') {
      66        
      66        
184 150         766 return ($h1, $h2);
185             }
186 452         1039 return convert_hour($h1, $h2, $TZ, $isdst);
187             }
188             }
189             #######################################################################################
190             # end sunrise
191             ###################################################################################
192              
193             #
194             #
195             # FUNCTIONAL SEQUENCE for days_since_2000_Jan_0
196             #
197             # _GIVEN
198             # year, month, day
199             #
200             # _THEN
201             #
202             # process the year month and day (counted in days)
203             # Day 0.0 is at Jan 1 2000 0.0 UT
204             # Note that ALL divisions here should be INTEGER divisions
205             #
206             # _RETURN
207             #
208             # day number
209             #
210             sub days_since_2000_Jan_0 {
211 11     11   6082 use integer;
  11         166  
  11         64  
212 784     784 0 1500 my ( $year, $month, $day ) = @_;
213              
214 784         2593 my $d = 367 * $year
215             - int( ( 7 * ( $year + ( ($month + 9) / 12 ) ) ) / 4 )
216             + int( (275 * $month) / 9 )
217             + $day - 730530;
218              
219 784         2106 return $d;
220              
221             }
222              
223             #
224             #
225             # FUNCTIONAL SEQUENCE for convert_hour
226             #
227             # _GIVEN
228             # Hour_rise, Hour_set, Time zone offset, DST setting
229             # hours are in UT
230             #
231             # _THEN
232             #
233             # convert to local time
234             #
235             #
236             # _RETURN
237             #
238             # hour:min rise and set
239             #
240              
241             sub convert_hour {
242 634     634 0 1218 my ($hour_rise_ut, $hour_set_ut, $TZ, $isdst) = @_;
243 634         1124 return (convert_1_hour($hour_rise_ut, $TZ, $isdst),
244             convert_1_hour($hour_set_ut, $TZ, $isdst));
245             }
246             #
247             #
248             # FUNCTIONAL SEQUENCE for convert_1_hour
249             #
250             # _GIVEN
251             # Hour, Time zone offset, DST setting
252             # hours are in UT
253             #
254             # _THEN
255             #
256             # convert to local time
257             #
258             #
259             # _RETURN
260             #
261             # hour:min
262             #
263              
264             sub convert_1_hour {
265 1268     1268 0 2114 my ($hour_ut, $TZ, $isdst) = @_;
266              
267 1268 100 100     5893 if ($hour_ut eq 'day' or $hour_ut eq 'night') {
268 300         1111 return $hour_ut;
269             }
270              
271 968         1531 my $hour_local = $hour_ut + $TZ;
272 968 50       1574 if ($isdst) {
273 0         0 $hour_local ++;
274             }
275              
276             # The hour should be between 0 and 24;
277 968 100       2067 if ($hour_local < 0) {
    100          
278 1         3 $hour_local += 24;
279             }
280             elsif ($hour_local > 24) {
281 21         39 $hour_local -= 24;
282             }
283              
284 968         1424 my $hour = int ($hour_local);
285              
286 968         2154 my $min = floor(($hour_local - $hour) * 60 + 0.5);
287              
288 968 100       1939 if ($min >= 60) {
289 7         18 $min -= 60;
290 7         14 $hour++;
291 7 50       18 $hour -= 24 if $hour >= 24;
292             }
293              
294 968         5070 return sprintf("%02d:%02d", $hour, $min);
295             }
296              
297              
298             sub sun_rise_set {
299 1125     1125 0 2719 my ($d, $lon, $lat,$altit, $h, $upper_limb, $polar, $trace) = @_;
300              
301             # Compute local sidereal time of this moment
302 1125         1928 my $sidtime = revolution( GMST0($d) + 180.0 + $lon );
303              
304             # Compute Sun's RA + Decl + distance at this moment
305 1125         2188 my ( $sRA, $sdec, $sr ) = sun_RA_dec($d, $lon, $trace);
306              
307             # Compute time when Sun is at south - in hours UT
308 1125         2224 my $tsouth = 12.0 - rev180( $sidtime - $sRA ) / 15.0;
309 1125 50       2232 if ($trace) {
310 0         0 printf $trace "For day $d (%s), sidereal time $sidtime, right asc $sRA\n", _fmt_hr(24 * ($d - int($d)), $lon);
311 0         0 printf $trace "For day $d (%s), solar noon at $tsouth (%s)\n", _fmt_hr(24 * ($d - int($d)), $lon), _fmt_hr($tsouth, $lon);
312             }
313              
314 1125 100       1892 if ($upper_limb) {
315             # Compute the Sun's apparent radius, degrees
316 486         702 my $sradius = 0.2666 / $sr;
317 486         1081 $altit -= $sradius;
318             }
319              
320             # Compute the diurnal arc that the Sun traverses to reach
321             # the specified altitude altit:
322 1125         1751 my $cost = ( sind($altit) - sind($lat) * sind($sdec) )
323             / ( cosd($lat) * cosd($sdec) );
324              
325 1125         1629 my $t;
326 1125 100       2648 if ( $cost >= 1.0 ) {
    100          
327 142 100       289 if ($polar eq 'retval') {
328 138         398 return ('night', 'night');
329             }
330 4         744 carp "Sun never rises!!\n";
331 4         303 $t = 0.0; # Sun always below altit
332             }
333             elsif ( $cost <= -1.0 ) {
334 320 100       697 if ($polar eq 'retval') {
335 312         896 return ('day', 'day');
336             }
337 8         1099 carp "Sun never sets!!\n";
338 8         487 $t = 12.0; # Sun always above altit
339             }
340             else {
341 663         1151 my $arc = acosd($cost); # The diurnal arc
342 663         4249 $t = $arc / $h; # Time to traverse the diurnal arc, hours
343 663 50       1314 if ($trace) {
344 0         0 printf $trace "Diurnal arc $arc -> $t hours (%s)\n", _fmt_dur($t);
345             }
346             }
347              
348             # Store rise and set times - in hours UT
349              
350 675         975 my $hour_rise_ut = $tsouth - $t;
351 675         931 my $hour_set_ut = $tsouth + $t;
352 675 50       1537 if ($trace) {
353 0         0 printf $trace "For day $d (%s), sunrise at $hour_rise_ut (%s)\n", _fmt_hr(24 * ($d - int($d)), $lon),
354             _fmt_hr($hour_rise_ut, $lon);
355 0         0 printf $trace "For day $d (%s), sunset at $hour_set_ut (%s)\n", _fmt_hr(24 * ($d - int($d)), $lon),
356             _fmt_hr($hour_set_ut , $lon);
357             }
358 675         1528 return($hour_rise_ut, $hour_set_ut);
359             }
360              
361             #########################################################################################################
362             #
363             #
364             # FUNCTIONAL SEQUENCE for GMST0
365             #
366             # _GIVEN
367             # Day number
368             #
369             # _THEN
370             #
371             # computes GMST0, the Greenwich Mean Sidereal Time
372             # at 0h UT (i.e. the sidereal time at the Greenwich meridian at
373             # 0h UT). GMST is then the sidereal time at Greenwich at any
374             # time of the day..
375             #
376             #
377             # _RETURN
378             #
379             # Sidtime
380             #
381             sub GMST0 {
382 1125     1125 0 1767 my ($d) = @_;
383              
384 1125         2308 my $sidtim0 = revolution( ( 180.0 + 356.0470 + 282.9404 )
385             + ( 0.9856002585 + 4.70935E-5 ) * $d );
386 1125         2327 return $sidtim0;
387              
388             }
389              
390             #
391             #
392             # FUNCTIONAL SEQUENCE for sun_RA_dec
393             #
394             # _GIVEN
395             # day number, $r and $lon (from sunpos)
396             #
397             # _THEN
398             #
399             # compute RA and dec
400             #
401             #
402             # _RETURN
403             #
404             # Sun's Right Ascension (RA), Declination (dec) and distance (r)
405             #
406             #
407             sub sun_RA_dec {
408 1125     1125 0 1892 my ($d, $lon_noon, $trace) = @_;
409              
410             # Compute Sun's ecliptical coordinates
411 1125         1853 my ( $r, $lon ) = sunpos($d);
412 1125 50       2095 if ($trace) {
413 0         0 printf $trace "For day $d (%s), solar noon at ecliptic longitude $lon\n", _fmt_hr(24 * ($d - int($d)), $lon_noon),;
414             }
415              
416             # Compute ecliptic rectangular coordinates (z=0)
417 1125         1774 my $x = $r * cosd($lon);
418 1125         1768 my $y = $r * sind($lon);
419              
420             # Compute obliquity of ecliptic (inclination of Earth's axis)
421 1125         1743 my $obl_ecl = 23.4393 - 3.563E-7 * $d;
422              
423             # Convert to equatorial rectangular coordinates - x is unchanged
424 1125         1555 my $z = $y * sind($obl_ecl);
425 1125         1684 $y = $y * cosd($obl_ecl);
426              
427             # Convert to spherical coordinates
428 1125         1752 my $RA = atan2d( $y, $x );
429 1125         2058 my $dec = atan2d( $z, sqrt( $x * $x + $y * $y ) );
430              
431 1125         2580 return ( $RA, $dec, $r );
432              
433             } # sun_RA_dec
434              
435              
436             #
437             #
438             # FUNCTIONAL SEQUENCE for sunpos
439             #
440             # _GIVEN
441             # day number
442             #
443             # _THEN
444             #
445             # Computes the Sun's ecliptic longitude and distance
446             # at an instant given in d, number of days since
447             # 2000 Jan 0.0.
448             #
449             #
450             # _RETURN
451             #
452             # ecliptic longitude and distance
453             # ie. $True_solar_longitude, $Solar_distance
454             #
455             sub sunpos {
456 1125     1125 0 1680 my ($d) = @_;
457              
458             # Mean anomaly of the Sun
459             # Mean longitude of perihelion
460             # Note: Sun's mean longitude = M + w
461             # Eccentricity of Earth's orbit
462             # Eccentric anomaly
463             # x, y coordinates in orbit
464             # True anomaly
465              
466             # Compute mean elements
467 1125         1907 my $Mean_anomaly_of_sun = revolution( 356.0470 + 0.9856002585 * $d );
468 1125         1833 my $Mean_longitude_of_perihelion = 282.9404 + 4.70935E-5 * $d;
469 1125         1615 my $Eccentricity_of_Earth_orbit = 0.016709 - 1.151E-9 * $d;
470              
471             # Compute true longitude and radius vector
472 1125         1980 my $Eccentric_anomaly = $Mean_anomaly_of_sun
473             + $Eccentricity_of_Earth_orbit * $RADEG
474             * sind($Mean_anomaly_of_sun)
475             * ( 1.0 + $Eccentricity_of_Earth_orbit * cosd($Mean_anomaly_of_sun) );
476              
477 1125         1840 my $x = cosd($Eccentric_anomaly) - $Eccentricity_of_Earth_orbit;
478              
479 1125         2071 my $y = sqrt( 1.0 - $Eccentricity_of_Earth_orbit * $Eccentricity_of_Earth_orbit )
480             * sind($Eccentric_anomaly);
481              
482 1125         1823 my $Solar_distance = sqrt( $x * $x + $y * $y ); # Solar distance
483 1125         1762 my $True_anomaly = atan2d( $y, $x ); # True anomaly
484              
485 1125         1659 my $True_solar_longitude = $True_anomaly + $Mean_longitude_of_perihelion; # True solar longitude
486              
487 1125 100       2394 if ( $True_solar_longitude >= 360.0 ) {
488 447         632 $True_solar_longitude -= 360.0; # Make it 0..360 degrees
489             }
490              
491 1125         2476 return ( $Solar_distance, $True_solar_longitude );
492             }
493              
494              
495              
496             sub sind {
497 7881     7881 1 18103 sin( ( $_[0] ) * $DEGRAD );
498             }
499              
500             sub cosd {
501 6756     6756 1 11370 cos( ( $_[0] ) * $DEGRAD );
502             }
503              
504             sub tand {
505 6     6 1 47 tan( ( $_[0] ) * $DEGRAD );
506             }
507              
508             sub atand {
509 6     6 1 19 ( $RADEG * atan( $_[0] ) );
510             }
511              
512             sub asind {
513 6     6 1 20 ( $RADEG * asin( $_[0] ) );
514             }
515              
516             sub acosd {
517 669     669 1 1786 ( $RADEG * acos( $_[0] ) );
518             }
519              
520             sub atan2d {
521 3382     3382 1 8420 ( $RADEG * atan2( $_[0], $_[1] ) );
522             }
523              
524             #
525             #
526             # FUNCTIONAL SEQUENCE for revolution
527             #
528             # _GIVEN
529             # any angle
530             #
531             # _THEN
532             #
533             # reduces any angle to within the first revolution
534             # by subtracting or adding even multiples of 360.0
535             #
536             #
537             # _RETURN
538             #
539             # the value of the input is >= 0.0 and < 360.0
540             #
541              
542             sub revolution {
543 3375     3375 0 4389 my $x = $_[0];
544 3375         7572 return ( $x - 360.0 * floor( $x * $INV360 ) );
545             }
546              
547             #
548             #
549             # FUNCTIONAL SEQUENCE for rev180
550             #
551             # _GIVEN
552             #
553             # any angle
554             #
555             # _THEN
556             #
557             # Reduce input to within -180..+180 degrees
558             #
559             #
560             # _RETURN
561             #
562             # angle that was reduced
563             #
564             sub rev180 {
565 1125     1125 0 1626 my ($x) = @_;
566            
567 1125         3145 return ( $x - 360.0 * floor( $x * $INV360 + 0.5 ) );
568             }
569              
570             sub equal {
571 266     266 1 682 my ($A, $B, $dp) = @_;
572              
573 266         1626 return sprintf("%.${dp}g", $A) eq sprintf("%.${dp}g", $B);
574             }
575              
576             sub _fmt_hr {
577 0     0   0 my ($utc, $lon) = @_;
578 0         0 my $lmt = $utc + $lon / 15;
579 0         0 my $hr_utc = floor($utc);
580 0         0 $utc -= $hr_utc;
581 0         0 $utc *= 60;
582 0         0 my $mn_utc = floor($utc);
583 0         0 $utc -= $mn_utc;
584 0         0 $utc *= 60;
585 0         0 my $sc_utc = floor($utc);
586 0         0 my $hr_lmt = floor($lmt);
587 0         0 $lmt -= $hr_lmt;
588 0         0 $lmt *= 60;
589 0         0 my $mn_lmt = floor($lmt);
590 0         0 $lmt -= $mn_lmt;
591 0         0 $lmt *= 60;
592 0         0 my $sc_lmt = floor($lmt);
593 0         0 return sprintf("%02d:%02d:%02d UTC %02d:%02d:%02d LMT", $hr_utc, $mn_utc, $sc_utc, $hr_lmt, $mn_lmt, $sc_lmt);
594             }
595              
596             sub _fmt_dur {
597 0     0   0 my ($dur) = @_;
598 0         0 my $hr = floor($dur);
599 0         0 $dur -= $hr;
600 0         0 $dur *= 60;
601 0         0 my $mn = floor($dur);
602 0         0 $dur -= $mn;
603 0         0 $dur *= 60;
604 0         0 my $sc = floor($dur);
605 0         0 return sprintf("%02d h %02d mn %02d s", $hr, $mn, $sc);
606             }
607              
608              
609             sub DEFAULT () { -0.833 }
610             sub CIVIL () { - 6 }
611             sub NAUTICAL () { -12 }
612             sub AMATEUR () { -15 }
613             sub ASTRONOMICAL () { -18 }
614              
615             # Ending a module with whatever, which risks to be zero, is wrong.
616             # Ending a module with 1 is boring. So, let us end it with:
617             1950;
618             # Hint: directed by BW, with GS, WH and EVS
619              
620             __END__
621              
622             =encoding utf8
623              
624             =head1 NAME
625              
626             Astro::Sunrise - Perl extension for computing the sunrise/sunset on a given day
627              
628             =head1 VERSION
629              
630             This documentation refers to C<Astro::Sunrise> version 0.98.
631              
632             =head1 SYNOPSIS
633              
634             # When did the sun rise on YAPC::Europe 2015?
635             use Astro::Sunrise;
636             my ($sunrise, $sunset) = sunrise( { year => 2015, month => 9, day => 2, # YAPC::EU starts on 2nd September 2015
637             lon => -3.6, lat => 37.17, # Granada is 37°10'N, 3°36'W
638             tz => 1, isdst => 1 } ); # This is still summer, therefore DST
639              
640             # When does the sun rise today in Salt Lake City (home to YAPC::NA 2015)?
641             use Astro::Sunrise;
642             use DateTime;
643             $sunrise_today = sun_rise( { lon => -111.88, lat => 40.75 } ); # 40°45'N, 111°53'W
644              
645             # And when does it set tomorrow at Salt Lake City?
646             use Astro::Sunrise;
647             use DateTime;
648             $sunset_tomorrow = sun_set( { lat => 40.75, # 40°45'N,
649             lon => -111.88, # 111°53'W
650             alt => -0.833, # standard value for the sun altitude at sunset
651             offset => 1 } ); # day offset up to tomorrow
652              
653             =head1 DESCRIPTION
654              
655             This module will return the sunrise and sunset for a given day.
656              
657             Months are numbered 1 to 12, in the usual way, not 0 to 11 as in
658             C and in Perl's localtime.
659              
660             Eastern longitude is entered as a positive number
661             Western longitude is entered as a negative number
662             Northern latitude is entered as a positive number
663             Southern latitude is entered as a negative number
664              
665             Please note that, when given as positional parameters, the longitude is specified before the
666             latitude.
667              
668             The time zone is given as the numeric value of the offset from UTC.
669              
670             The C<precise> parameter is set to either 0 or 1.
671             If set to 0 no Iteration will occur.
672             If set to 1 Iteration will occur, which will give a more precise result.
673             Default is 0.
674              
675             There are a number of sun altitudes to chose from. The default is
676             -0.833 because this is what most countries use. Feel free to
677             specify it if you need to. Here is the list of values to specify
678             altitude (ALT) with, including symbolic constants for each.
679              
680             =over
681              
682             =item B<0> degrees
683              
684             Center of Sun's disk touches a mathematical horizon
685              
686             =item B<-0.25> degrees
687              
688             Sun's upper limb touches a mathematical horizon
689              
690             =item B<-0.583> degrees
691              
692             Center of Sun's disk touches the horizon; atmospheric refraction accounted for
693              
694             =item B<-0.833> degrees, DEFAULT
695              
696             Sun's upper limb touches the horizon; atmospheric refraction accounted for
697              
698             =item B<-6> degrees, CIVIL
699              
700             Civil twilight (one can no longer read outside without artificial illumination)
701              
702             =item B<-12> degrees, NAUTICAL
703              
704             Nautical twilight (navigation using a sea horizon no longer possible)
705              
706             =item B<-15> degrees, AMATEUR
707              
708             Amateur astronomical twilight (the sky is dark enough for most astronomical observations)
709              
710             =item B<-18> degrees, ASTRONOMICAL
711              
712             Astronomical twilight (the sky is completely dark)
713              
714             =back
715              
716             =head1 USAGE
717              
718             =head2 B<sunrise>
719              
720             ($sunrise, $sunset) = sunrise( { year => $year, month => $month,
721             day => $day,
722             lon => $longitude, lat => $latitude,
723             tz => $tz_offset, isdst => $is_dst,
724             alt => $altitude, upper_limb => $upper_limb,
725             precise => $precise, polar => $action,
726             trace => $fh } );
727              
728             ($sunrise, $sunset) = sunrise(YYYY,MM,DD,longitude,latitude,Time Zone,DST);
729              
730             ($sunrise, $sunset) = sunrise(YYYY,MM,DD,longitude,latitude,Time Zone,DST,ALT);
731              
732             ($sunrise, $sunset) = sunrise(YYYY,MM,DD,longitude,latitude,Time Zone,DST,ALT,precise);
733              
734             Returns the sunrise and sunset times, in HH:MM format.
735              
736             The first form uses a hash reference to pass arguments by name.
737             The other forms are kept for backward compatibility. The arguments are:
738              
739             =over 4
740              
741             =item year, month, day
742              
743             The three elements of the date for which you want to compute the sunrise and sunset.
744             Months are numbered 1 to 12, in the usual way, not 0 to 11 as in C and in Perl's localtime.
745              
746             Mandatory, can be positional (#1, #2 and #3).
747              
748             =item lon, lat
749              
750             The longitude and latitude of the place for which you want to compute the sunrise and sunset.
751             They are given in decimal degrees. For example:
752              
753             lon => -3.6, # 3° 36' W
754             lat => 37.17, # 37° 10' N
755              
756             Eastern longitude is entered as a positive number
757             Western longitude is entered as a negative number
758             Northern latitude is entered as a positive number
759             Southern latitude is entered as a negative number
760              
761             Mandatory, can be positional (#4 and #5).
762              
763             =item tz
764              
765             Time Zone is the offset from GMT
766              
767             Mandatory, can be positional (#6).
768              
769             =item isdst
770              
771             1 if daylight saving time is in effect, 0 if not.
772              
773             Mandatory, can be positional (#7).
774              
775             =item alt
776              
777             Altitude of the sun, in decimal degrees. Usually a negative number,
778             because the sun should be I<under> the mathematical horizon.
779             But if there is a high mountain range sunward (that is, southward if you
780             live in the Northern hemisphere), you may need to enter a positive altitude.
781              
782             This parameter is optional. Its default value is -0.833. It can be positional (#8).
783              
784             =item upper_limb
785              
786             If this parameter set to a true value (usually 1), the algorithm computes
787             the sun apparent radius and takes it into account when computing the sun
788             altitude. This parameter is useful only when the C<alt> parameter is set
789             to C<0> or C<-0.583> degrees. When using C<-0.25> or C<-0.833> degrees,
790             the sun radius is already taken into account. When computing twilights
791             (C<-6> to C<-18>), the sun radius is irrelevant.
792              
793             Since the default value for the C<alt> parameter is -0.833, the
794             default value for C<upper_limb> is 0.
795              
796             This parameter is optional and it can be specified only by keyword.
797              
798             =item polar
799              
800             When dealing with a polar location, there may be dates where there is
801             a polar night (sun never rises) or a polar day. The default behaviour of
802             the module is to emit a warning in these cases ("Sun never rises!!"
803             or "Sun never sets!!"). But some programmers may find this inconvenient.
804             An alternate behaviour is to return special values reflecting the
805             situation.
806              
807             So, if the C<polar> parameter is set to C<'warn'>, the module emits
808             a warning. If the C<polar> parameter is set to C<'retval'>, the
809             module emits no warning, but it returns either C<'day'> or C<'night'>.
810              
811             Example:
812              
813             # Loosely based on Alex Gough's activities: scientist and Perl programmer, who spent a year
814             # in Halley Base in 2006. Let us suppose he arrived there on 15th January 2006.
815             my ($sunrise, $sunset) = sunrise( { year => 2006, month => 1, day => 15,
816             lon => -26.65, lat => -75.58, # Halley Base: 75°35'S 26°39'W
817             tz => 3, polar => 'retval' } );
818             if ($sunrise eq 'day') {
819             say "Alex Gough saw the midnight sun the first day he arrived at Halley Base";
820             }
821             elsif ($sunrise eq 'night') {
822             say "It would be days, maybe weeks, before the sun would rise.";
823             }
824             else {
825             say "Alex saw his first antarctic sunset at $sunset";
826             }
827              
828             This parameter is optional and it can be specified only by keyword.
829              
830             =item trace
831              
832             This parameter should either be a false value or a filehandle opened for output.
833             In the latter case, a few messages are printed to the filehandle, which allows
834             the programmer to see step by step how the sunrise and the sunset are computed.
835              
836             Used for analysis and debugging purposes. You need to read the text
837             F<doc/astronomical-notes.pod> to understand what the traced values
838             represent.
839              
840             This parameter is optional and it can be specified only by keyword.
841              
842             =item precise
843              
844             Choice between a precise algorithm and a simpler algorithm.
845             The default value is 0, that is, the simpler algorithm.
846             Any true value switches to the precise algorithm.
847              
848             The original method only gives an approximate value of the Sun's rise/set times.
849             The error rarely exceeds one or two minutes, but at high latitudes, when the Midnight Sun
850             soon will start or just has ended, the errors may be much larger. If you want higher accuracy,
851             you must then use the precise algorithm. This feature is new as of version 0.7. Here is
852             what I have tried to accomplish with this.
853              
854             a) Compute sunrise or sunset as always, with one exception: to convert LHA from degrees to hours,
855             divide by 15.04107 instead of 15.0 (this accounts for the difference between the solar day
856             and the sidereal day).
857              
858             b) Re-do the computation but compute the Sun's RA and Decl, and also GMST0, for the moment
859             of sunrise or sunset last computed.
860              
861             c) Iterate b) until the computed sunrise or sunset no longer changes significantly.
862             Usually 2 iterations are enough, in rare cases 3 or 4 iterations may be needed.
863              
864             This parameter is optional. It can be positional (#9).
865              
866             =back
867              
868             =head3 I<For Example>
869              
870             ($sunrise, $sunset) = sunrise( 2001, 3, 10, 17.384, 98.625, -5, 0 );
871             ($sunrise, $sunset) = sunrise( 2002, 10, 14, -105.181, 41.324, -7, 1, -18);
872             ($sunrise, $sunset) = sunrise( 2002, 10, 14, -105.181, 41.324, -7, 1, -18, 1);
873              
874             =head2 B<sun_rise>, B<sun_set>
875              
876             $sun_rise = sun_rise( { lon => $longitude, lat => $latitude,
877             alt => $altitude, upper_limb => $bool,
878             offset => $day_offset,
879             precise => $bool_precise, polar => $action } );
880             $sun_set = sun_set ( { lon => $longitude, lat => $latitude,
881             alt => $altitude, upper_limb => $bool,
882             offset => $day_offset,
883             precise => $bool_precise, polar => $action } );
884             $sun_rise = sun_rise( $longitude, $latitude );
885             $sun_rise = sun_rise( $longitude, $latitude, $alt );
886             $sun_rise = sun_rise( $longitude, $latitude, $alt, $day_offset );
887              
888             Returns the sun rise time (resp. the sun set time) for the given location
889             and for today's date (as given by DateTime), plus or minus some offset in days.
890             The first form use all parameters and transmit them by name. The second form
891             uses today's date (from DateTime) and the default altitude. The third
892             form adds specifying a custom altitude. The fourth form allows for specifying
893             an integer day offset from today, either positive or negative.
894              
895             The parameters are the same as the parameters for C<sunrise>. There is an additional
896             parameter, C<offset>, which allows using a date other than today: C<+1> for
897             to-morrow, C<-7> for one week ago, etc.
898              
899             The arguments are:
900              
901             =over 4
902              
903             =item lon, lat
904              
905             The longitude and latitude of the place for which you want to compute the sunrise and sunset.
906             They are given in decimal degrees. For example:
907              
908             lon => -3.6, # 3° 36' W
909             lat => 37.17, # 37° 10' N
910              
911             Eastern longitude is entered as a positive number
912             Western longitude is entered as a negative number
913             Northern latitude is entered as a positive number
914             Southern latitude is entered as a negative number
915              
916             Mandatory, can be positional (#1 and #2).
917              
918             =item alt
919              
920             Altitude of the sun, in decimal degrees. Usually a negative number,
921             because the sun should be I<under> the mathematical horizon.
922             But if there is a high mountain range sunward (that is, southward if you
923             live in the Northern hemisphere), you may need to enter a positive altitude.
924              
925             This parameter is optional. Its default value is -0.833. It can be positional (#3).
926              
927             =item offset
928              
929             By default, C<sun_rise> and C<sun_set> use the current day. If you need another
930             day, you give an offset relative to the current day. For example, C<+7> means
931             next week, while C<-365> means last year.
932              
933             This parameter has nothing to do with timezones.
934              
935             Optional, 0 by default, can be positional (#4).
936              
937             =item time_zone
938              
939             Time Zone is the Olson name for a timezone. By default, the functions
940             C<sun_rise> and C<sun_set> will try to use the C<local> timezone.
941              
942             This parameter is optional and it can be specified only by keyword.
943              
944             =item upper_limb
945              
946             If this parameter set to a true value (usually 1), the algorithm computes
947             the sun apparent radius and takes it into account when computing the sun
948             altitude. This parameter is useful only when the C<alt> parameter is set
949             to C<0> or C<-0.583> degrees. When using C<-0.25> or C<-0.833> degrees,
950             the sun radius is already taken into account. When computing twilights
951             (C<-6> to C<-18>), the sun radius is irrelevant.
952              
953             Since the default value for the C<alt> parameter is -0.833, the
954             default value for C<upper_limb> is 0.
955              
956             This parameter is optional and it can be specified only by keyword.
957              
958             =item polar
959              
960             When dealing with a polar location, there may be dates where there is
961             a polar night (sun never rises) or a polar day. The default behaviour of
962             the module is to emit a warning in these cases ("Sun never rises!!"
963             or "Sun never sets!!"). But some programmers may find this inconvenient.
964             An alternate behaviour is to return special values reflecting the
965             situation.
966              
967             So, if the C<polar> parameter is set to C<'warn'>, the module emits
968             a warning. If the C<polar> parameter is set to C<'retval'>, the
969             module emits no warning, but it returns either C<'day'> or C<'night'>.
970              
971             This parameter is optional and it can be specified only by keyword.
972              
973             =item trace
974              
975             This parameter should either be a false value or a filehandle opened for output.
976             In the latter case, a few messages are printed to the filehandle, which allows
977             the programmer to see step by step how the sunrise and the sunset are computed.
978              
979             Used for analysis and debugging purposes.
980              
981             This parameter is optional and it can be specified only by keyword.
982              
983             =item precise
984              
985             Choice between a precise algorithm and a simpler algorithm.
986             The default value is 0, that is, the simpler algorithm.
987             Any true value switches to the precise algorithm.
988              
989             For more documentation, see the corresponding parameter
990             for the C<sunrise> function.
991              
992             This parameter is optional and it can be specified only by keyword.
993              
994             =back
995              
996             =head3 For Example
997              
998             $sunrise = sun_rise( -105.181, 41.324 );
999             $sunrise = sun_rise( -105.181, 41.324, -15 );
1000             $sunrise = sun_rise( -105.181, 41.324, -12, +3 );
1001             $sunrise = sun_rise( -105.181, 41.324, undef, -12);
1002              
1003             =head2 Trigonometric functions using degrees
1004              
1005             Since the module use trigonometry with degrees, the corresponding functions
1006             are available to the module user, free of charge. Just mention the
1007             tag C<:trig> in the C<use> statement. These functions are:
1008              
1009             =over 4
1010              
1011             =item sind, cosd, tand
1012              
1013             The direct functions, that is, sine, cosine and tangent functions, respectively.
1014             Each one receives one parameter, in degrees, and returns the trigonometric value.
1015              
1016             =item asind, acosd, atand
1017              
1018             The reverse functions, that is, arc-sine, arc-cosine, and arc-tangent.
1019             Each one receives one parameter, the trigonometric value, and returns the corresponding
1020             angle in degrees.
1021              
1022             =item atan2d
1023              
1024             Arc-tangent. This function receives two parameters: the numerator and the denominator
1025             of a fraction equal to the tangent. Use this function instead of C<atand> when you
1026             are not sure the denominator is not zero. E.g.:
1027              
1028             use Astro::Sunrise qw(:trig);
1029             say atan2d(1, 2) # prints 26,5
1030             say atan2d(1, 0) # prints 90, without triggering a "division by zero" error
1031              
1032             =item equal
1033              
1034             Not really a trigonometrical function, but still useful at some times. This function
1035             receives two floating values and an integer value. It compares the floating numbers,
1036             and returns "1" if their most significant digits are equal. The integer value
1037             specifies how many digits are kept. E.g.
1038              
1039             say equal(22/7, 355/113, 3) # prints 1, because : 22/7 = 3.14285715286 rounded to 3.14
1040             # 355/113 = 3.14159292035 rounded to 3.14
1041             say equal(22/7, 355/113, 4) # prints 0, because : 22/7 = 3.14285715286 rounded to 3.143
1042             # 355/113 = 3.14159292035 rounded to 3.142
1043              
1044             =back
1045              
1046             =head1 EXPORTS
1047              
1048             By default, the functions C<sunrise>, C<sun_rise> and C<sun_set> are exported.
1049              
1050             The constants C<DEFAULT>, C<CIVIL>, C<NAUTICAL>, C<AMATEUR> and C<ASTRONOMICAL> are
1051             exported on request with the tag C<:constants>.
1052              
1053             The functions C<sind>, C<cosd>, C<tand>, C<asind>, C<acosd>, C<atand>, C<atan2d> and C<equal>
1054             exported on request with the tag C<:trig>.
1055              
1056             =head1 DEPENDENCIES
1057              
1058             This module requires only core modules: L<POSIX>, L<Math::Trig> and L<Carp>.
1059              
1060             If you use the C<sun_rise> and C<sun_set> functions, you will need also L<DateTime>.
1061              
1062             =head1 AUTHOR
1063              
1064             Ron Hill
1065             rkhill@firstlight.net
1066              
1067             Co-maintainer: Jean Forget (JFORGET at cpan dot org)
1068              
1069             =head1 SPECIAL THANKS
1070              
1071             Robert Creager [Astro-Sunrise@LogicalChaos.org]
1072             for providing help with converting Paul's C code to Perl,
1073             for providing code for sun_rise, sun_set subs.
1074             Also adding options for different altitudes.
1075              
1076             Joshua Hoblitt [jhoblitt@ifa.hawaii.edu]
1077             for providing the patch to convert to DateTime.
1078              
1079             Chris Phillips for providing patch for conversion to
1080             local time.
1081              
1082             Brian D Foy for providing patch for constants :)
1083              
1084             Gabor Szabo for pointing POD mistakes.
1085              
1086             People at L<https://geocoder.opencagedata.com/> for noticing an endless
1087             loop condition and for fixing it.
1088              
1089             =head1 CREDITS
1090              
1091             =over 4
1092              
1093             =item Paul Schlyter, Stockholm, Sweden
1094              
1095             for his excellent web page on the subject.
1096              
1097             =item Rich Bowen (rbowen@rbowen.com)
1098              
1099             for suggestions.
1100              
1101             =item Adrian Blockley [adrian.blockley@environ.wa.gov.au]
1102              
1103             for finding a bug in the conversion to local time.
1104              
1105             =item Slaven Rezić
1106              
1107             for finding and fixing a bug with DST.
1108              
1109             =back
1110              
1111             Lightly verified against L<http://aa.usno.navy.mil/data/docs/RS_OneYear.html>
1112              
1113             In addition, checked to be compatible with a C implementation of Paul Schlyter's algorithm.
1114              
1115             =head1 COPYRIGHT and LICENSE
1116              
1117             =head2 Perl Module
1118              
1119             This program is distributed under the same terms as Perl 5.16.3:
1120             GNU Public License version 1 or later and Perl Artistic License
1121              
1122             You can find the text of the licenses in the F<LICENSE> file or at
1123             L<https://dev.perl.org/licenses/artistic.html>
1124             and L<https://www.gnu.org/licenses/gpl-1.0.html>.
1125              
1126             Here is the summary of GPL:
1127              
1128             This program is free software; you can redistribute it and/or modify
1129             it under the terms of the GNU General Public License as published by
1130             the Free Software Foundation; either version 1, or (at your option)
1131             any later version.
1132              
1133             This program is distributed in the hope that it will be useful,
1134             but WITHOUT ANY WARRANTY; without even the implied warranty of
1135             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1136             GNU General Public License for more details.
1137              
1138             You should have received a copy of the GNU General Public License
1139             along with this program; if not, write to the Free Software Foundation,
1140             Inc., L<https://www.fsf.org/>.
1141              
1142             =head2 Original C program
1143              
1144             Here is the copyright information provided by Paul Schlyter:
1145              
1146             Written as DAYLEN.C, 1989-08-16
1147              
1148             Modified to SUNRISET.C, 1992-12-01
1149              
1150             (c) Paul Schlyter, 1989, 1992
1151              
1152             Released to the public domain by Paul Schlyter, December 1992
1153              
1154             Permission is hereby granted, free of charge, to any person obtaining a
1155             copy of this software and associated documentation files (the "Software"),
1156             to deal in the Software without restriction, including without limitation
1157             the rights to use, copy, modify, merge, publish, distribute, sublicense,
1158             and/or sell copies of the Software, and to permit persons to whom the
1159             Software is furnished to do so, subject to the following conditions:
1160              
1161             The above copyright notice and this permission notice shall be included
1162             in all copies or substantial portions of the Software.
1163              
1164             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1165             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1166             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1167             THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
1168             WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
1169             OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
1170             THE SOFTWARE.
1171              
1172             =head1 BUGS
1173              
1174             Before reporting a bug, please read the text
1175             F<doc/astronomical-notes.pod> because the strange behavior you observed
1176             may be a correct one, or it may be a corner case already known and
1177             already mentioned in the text.
1178              
1179             Nevertheless, patches and (justified) bug reports are welcome.
1180              
1181             See L<https://rt.cpan.org/Public/Dist/Display.html?Name=Astro-Sunrise>.
1182              
1183             =head2 Astro::Sunrise Bug
1184              
1185             Ticket #109992 has not been solved properly. For some combinations
1186             of longitude and date, the precise algorithm does not converge.
1187             As a stopgap measure, the loop is exited after 10 iterations, so
1188             your program will not run amok. But the bug will be considered as fixed
1189             only when we find a way to converge toward a single value.
1190              
1191             =head2 Kwalitee
1192              
1193             The CPANTS tools do not recognize the LICENSE POD paragraph. But any
1194             human reader will admit that this LICENSE paragraph exists and is valid.
1195              
1196             =head2 Haiku-OS CPAN Tester
1197              
1198             The built-in test F<t/06datetime.t> fails on Haiku-OS because there is no
1199             way to extract the timezone name from the system parameters. This failure does
1200             not affect the core functions of L<Astro::Sunrise>.
1201              
1202             =head1 SEE ALSO
1203              
1204             perl(1).
1205              
1206             L<DateTime::Event::Sunrise>
1207              
1208             L<DateTime::Event::Jewish::Sunrise>
1209              
1210             The text F<doc/astronomical-notes.pod> (or its original French version
1211             F<doc/notes-astronomiques>) in this distribution.
1212              
1213             L<https://stjarnhimlen.se/comp/riset.html>
1214              
1215             =cut