Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.

Channels ▼

Web Development

An Almanac in Perl

February, 2004: An Almanac in Perl

brian has been a Perl user since 1994. He is founder of the first Perl Users Group, NY.pm, and Perl Mongers, the Perl advocacy organization. He has been teaching Perl through Stonehenge Consulting for the past five years, and has been a featured speaker at The Perl Conference, Perl University, YAPC, COMDEX, and Builder.com.

brian is currently on active duty with the United States Army as a military policeman in Iraq. You can e-mail him at com[email protected] panix.com, if you don't mind waiting a couple months for a reply.

I have been living in the middle of the desert for several months while on active duty with the U.S. Army and, occasionally, we take advantage of our night vision technology. In any conflict, the ability to see at night has been important. Night vision can be limited by weather and climate effects such as cloud cover or fog, but over here it is mostly affected by the phase of the moon. We consider that full or new moons give off 100 percent illumination, a half-moon 50 percent, and so on; and whenever we plan any night operation, we try to schedule it for the lowest illumination possible to make maximum use of our night vision devices. This is nothing new—soldiers in any war realize that the darker it is, the easier it is to sneak around unseen. All of this information is available in almanacs, but I am going to create my own since Perl makes it so easy.

The lunar cycle is 28 days, so the same phase comes at different days of each month. If I marked a known phase on my calendar, I could count the right number of days to the next phase, and through that process, create my own almanac. Indeed, you can buy such books. That way, however, does not let me play with Perl and, eventually, I want to create a program to give me a summary of the day's information.

In Listing 1, lines 3 to 6 pull in the modules I need. The Astro::MoonPhase module does most of the hard work for me, and some of the Time and Date modules handle the rest. I want to make a graph of luminosity as a function of the date. The GD::Graph module takes care of visuals as long as I come up with the data.

Lines 8 and 9 look at the command-line arguments to get the time zone offset and year for which I want to create the graph; otherwise, I assume I am in Greenwich during the current year. On line 12, I do a little trickery to get the first Sunday in the year because I want that to be my first data point. When I label the dates on the graph, I will make a mark for each Sunday. The seventh element that localtime() returns gives me the day of the week, starting at 0 for Sunday, so I keep looping through the days of the year until localtime() gives me a false value, taking into account the time zone difference.

On line 22, I get the latest time in the year for which I want to calculate the luminosity, and I will use that as a limit when I start generating data points. The for() loop on line 25 is the interesting work. I start at the first Sunday time, stored in $now, and go to the end of the year, stored in $then, and I create a data point every three hours. I keep the list of times in @times and the luminosity in @illum. I could have put these in a hash, but I am going to take them right back out to plot them, so I just use named arrays.

On line 35, I create the @data array to pass off to GD::Graph. The GD::Graph modules expect the data points to be in an array of arrays. The first anonymous array element is the horizontal axis value and the rest are the vertical axis values.

On line 37, I create a new graph object by telling GD::Graph that I want a line graph that is 800 pixels wide and 300 pixels long. On line 39, I set the options for the graph. The interesting options include the x_number_format, which I give an anonymous subroutine to translate the time data to an English date, and the x_tick_number, which tells GD::Graph to make 52 x ticks, one for each week (remember that I started at the first Sunday). The rest of the options specify various colors and labels.

Once I specify what I want, on line 58, I tell GD::Graph to make it happen by plotting the data. GD::Graph creates the graph and puts everything where it should be (if I have set my options right—sometimes I have to tweak it a bit). Although GD::Graph created the graph, it did not actually give me an image. On line 61, I tell the module that I want the graph as a PNG image and I save it to a file.

Once I run the program, I open the image to make sure it is what I want, but it usually is not, so I change a few GD::Graph options and try again. You can play with the options yourself to make the image pleasing to you if you do not like my choices. The GD::Graph module can do quite a bit more than I have shown.

I want to know all sorts of other things, too. I want to know the various levels of sunrises and sunsets. The sun's relationship to the horizon defines the different types. The civil twilight is when it becomes too dark to read outside, and that is when we like to start moving around at night.

For my almanac, I want to use Perl's formats, which saves me a lot of work in displaying the data and making it suitable for printing. Formats fell out of favor a while ago, largely because they require package variables, but they are meant for what I want to do—create multiple pages of columnar data. Formats handle pagination, which I would have to do myself if I used printf() or something similar.

On line 11 of Listing 2, I read in my personal configuration settings found in the file .almanacrc in my home directory. The configuration file is very simple. When I move around, I can change the configuration and generate a new almanac:

longitude 38.5
latitude 45.5
time_zone 2

On line 19, I start a for() loop that goes from the current time until one month later (there are 3.15e7 seconds in one year, so that many divided by 12 in one month. Remember that number in case you run out of things to talk about at a party—there are approximately times 107 seconds in a year). I advance one day for each iteration.

On lines 21 to 25, I get the current date from localtime() and format it with Date::Format to look nice for me. You may want to change the format to suit yourself. I put the formatted date in the global variable $date because formats work with global variables.

In the foreach() loop that starts on line 29, I get the sunrise and sunset times for the various altitudes of the sun using the values from the Astro::Sunrise documentation. I add each pair of values, the sunrise and sunset, to the global array @sunrises. I get the moon illumination from Astro::MoonPhase's phase() and store that in the global $moon_phase.

Once I have all the variables in place, I write() the format. Perl prints the STDOUT_TOP if I am at the top of the page (and when I get to the next page, it will do it again, so my columns always have headings). The STDOUT format puts the values of the $date, @sunrises, and $moon_phase data in the right places.

On line 35, I get the moon phase data by passing the date-time in $i, adjusted for the time zone, to Astro::MoonPhase's phase(). The number I get back is a fractional value, and I turn it into a percentage by multiplying by 100. To make sure I end up with an integer, I use sprintf() to format the number as a decimal integer. I do not mind losing precision after the third decimal place—atmospheric effects make that meaningless anyway.

Now, I have all of the information I want to put into my almanac, and all of the values are in package variables. I call write(), which invokes the current format, which is the same name as the current default output filehandle name. Since I have not messed with any of that, both names are still STDOUT. I do not need to pass any arguments to write() because the format already knows which package variables to use.

The output (Example 1) looks kind of dry, but it is perfect for my needs. I can refer quickly to any of the columns to get the information I want. When is the morning going to be light enough that it wakes me up? I look in the first column of the "Upper" set of times. When will I be able to see most of the stars? I look at the second column under the "Astro" heading. In the army, we consider the day over (or the night beginning) at nautical twilight, which is the second column of the "Nautical" column.

Pages and pages of this sort of data may be too long for me to carry around, so I can make a graph, like I did before. All I have to do is put the right values in the @data array I used in my GD::Graph program.

With a couple of Perl modules from CPAN, I was able to quickly churn out as much astrological data as I wanted. If I want to calculate data that does not already have a module, I only need to turn to the instructions in Practical Astronomy With Your Calculator, by Peter Duffett-Smith (Cambridge University Press, 1989; ISBN 0521356997), into Perl programs. Some of the Astro:: modules recommend other sources as well. Good luck with your own astrological projects.


Listing 1

1   #!/usr/bin/perl
3   use Astro::MoonPhase;
4   use Date::Format;
5   use GD::Graph::lines;
6   use Time::Local;
8   my $tz_offset = $ARGV[1] || 0;
9   my $year      = $ARGV[0] || ( (localtime)[5] + 1900 );
11  # get the first Sunday in the year
12  my $now = do {
13      for( $day = 1; ; $day++ )
14          {
15          $time = timegm( 0, 0, 0, $day, 0, $year );
16          last unless (localtime($time + $tz_offset))[6];
17          }
19      $time;
20      };
22  my $then = timegm(59, 59, 23, 31, 11, $year);
24  # calculate the moon phase for every three hours
25  for( my $i = $now; $i < $then; $i += 60 * 60 * 3 )
26      {   
27      my @data = phase( $i + $tz_offset );
29      push @time, $i;
30      push @illum, $data[1];
31      }
33  my @data = ( \@time, \@illum,);
35  my $my_graph = new GD::Graph::lines( 800, 300 );
37  $my_graph->set(
38      x_label             => 'Day',
39      y_label             => 'Moon Illumination',
40      title               => 'Moon Illumination',
41 .    x_number_format     => sub { time2str( "%h %e", $_[0] ) },
42      line_width          => 1,
43      x_label_position    => 1/2,
44      r_margin            => 15,
45      x_min_value         => $time[0],
46      x_max_value         => $time[-1],
47      x_tick_number       => 52,
48      transparent         => 0,
49      x_labels_vertical   => 1,
50      tick_clr            => 'gray',
51      border_clr          => 'dgray',
52      x_long_ticks        => 1,
53      y_long_ticks        => 0,
54      border              => 1,
55      border_clr          => 'black',
56  );
58  my $gd = $my_graph->plot(\@data);
60  open IMG, "> moon.png" or die "$!\n";
61  print IMG $gd->png;
62  close IMG;
Back to article

Listing 2
1   #!/usr/bin/perl
2   use strict;
4   use Astro::MoonPhase;
5   use Astro::Sunrise;
6   use ConfigReader::Simple;
7   use Date::Format;
9   use vars qw( $date @sunrises $moon_phase );
11  my $config = ConfigReader::Simple->new( ".almanacrc" );
13  my $long = $config->longitude;
14  my $lat  = $config->latitude;
15  my $tz   = $config->time_zone;
17  my $now = time;
19  for( my $i = $now; $i < $now + 3.15e7/12 ; $i += 24 * 60 * 60 )
20      {   
21      my @date = localtime( $i );
23      print "\n" unless $date[6];
25      $date = time2str( "%a %b %d", $i );
27      @sunrises = ();
29      foreach my $altitude ( -0.833, -6, -12, -18 )
30          {
31          push @sunrises, sunrise( @date[5,4,3], 
32              $long, $lat, $tz, $date[8], $altitude );
33          }
35      $moon_phase = sprintf "%3d", 100 * ( phase( $i + $tz ) )[1];
37      write;  
38      }
40  format STDOUT = 
41  @<<<<<<<<<  @>>>> @>>>>  @>>>> @>>>>  @>>>> @>>>>  @>>>> @>>>>  @>>
42  $date,      @sunrises,            $moon_phase
43  .
45  format STDOUT_TOP =
46  Date           Upper        Civil       Nautical      Astro    Illum
47  .
Back to article

Related Reading

More Insights

Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.