File Coverage

blib/lib/Date/Darian/Mars.pm
Criterion Covered Total %
statement 103 103 100.0
branch 45 46 97.8
condition 24 27 88.8
subroutine 23 23 100.0
pod 9 9 100.0
total 204 208 98.0


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Date::Darian::Mars - the Darian calendar for Mars
4              
5             =head1 SYNOPSIS
6              
7             use Date::Darian::Mars qw(present_y);
8              
9             print present_y($y);
10              
11             use Date::Darian::Mars
12             qw(month_days cmsdn_to_ymd ymd_to_cmsdn present_ymd);
13              
14             $md = month_days(209, 23);
15             ($y, $m, $d) = cmsdn_to_ymd(546236);
16             $cmsdn = ymd_to_cmsdn(209, 23, 18);
17             print present_ymd(546236);
18             print present_ymd(209, 23, 18);
19              
20             use Date::Darian::Mars
21             qw(year_days cmsdn_to_yd yd_to_cmsdn present_yd);
22              
23             $yd = year_days(209);
24             ($y, $d) = cmsdn_to_yd(546236);
25             $cmsdn = yd_to_cmsdn(209, 631);
26             print present_yd(546236);
27             print present_yd(209, 631);
28              
29             =head1 DESCRIPTION
30              
31             The Darian calendar for Mars is a mechanism by which Martian solar days
32             (also known as "sols") can be labelled in a manner useful to inhabitants
33             of Mars. This module provides functions to convert dates between the
34             Darian calendar and Chronological Mars Solar Day Numbers, which is a
35             suitable format to do arithmetic with. It also supplies functions that
36             describe the shape of the Darian calendar, to assist in calendrical
37             calculations. It also supplies functions to represent Darian dates
38             textually in a conventional format.
39              
40             The Darian calendar divides time up into years, months, and days.
41             This module also supports dividing the Darian year directly into days,
42             with no months.
43              
44             The Chronological Mars Solar Day Number is an integral number labelling
45             each Martian day, where the day extends from midnight to midnight in
46             whatever time zone is of interest. It is a linear count of days, where
47             each day's number is one greater than the previous day's number.
48              
49             This module places no limit on the range of dates to which it may be
50             applied. All function arguments are permitted to be C or
51             C objects in order to achieve arbitrary range. Native Perl
52             integers are also permitted, as a convenience when the range of dates
53             being handled is known to be sufficiently small.
54              
55             =head1 DARIAN CALENDAR FOR MARS
56              
57             The main cycle in the Darian calendar is the year. It approximates the
58             length of a Martian tropical year (specifically, the northward equinoctal
59             year), and the year starts approximately on the northward equinox.
60             Years are either 668 or 669 Martian solar days long. 669-day years are
61             referred to as "leap years".
62              
63             Each year is divided into 24 months, of nearly equal length. The months
64             are purely nominal: they do not correspond to any astronomical cycle.
65             Each quarter of the year consists of five months of 28 days followed by
66             one month of 27 days, except that the last month of a leap year contains
67             28 days instead of 27.
68              
69             All odd-numbered years are leap years. Even-numbered years are not leap
70             years, except for years divisible by ten which are leap years, except
71             for years divisible by 100 which are not, except for years divisible by
72             500 which are.
73              
74             Days within each month are numbered sequentially, starting at 1.
75             The months have names (in fact several competing sets of names), but this
76             module does not deal with the names. In this module, months within each
77             year are numbered sequentially from 1.
78              
79             Years are numbered sequentially. Year 0 is the year in which the first
80             known telescopic observations of Mars occurred. Specifically, year 0
81             started at the midnight that occurred on the Airy meridian (the Martian
82             prime meridian) at approximately MJD -91195.22 in Terrestrial Time.
83              
84             The calendar is described canonically, and in more detail, at
85             L.
86              
87             The day when Mars Exploration Rover "Opportunity" landed in Meridiani
88             Planum was 0209-23-18 or 0209-631 in the Darian calendar, and CMSDN
89             546236.
90              
91             =cut
92              
93             package Date::Darian::Mars;
94              
95 3     3   92886 { use 5.006; }
  3         13  
  3         219  
96 3     3   17 use warnings;
  3         6  
  3         131  
97 3     3   16 use strict;
  3         3  
  3         134  
98              
99 3     3   16 use Carp qw(croak);
  3         6  
  3         341  
100              
101             our $VERSION = "0.003";
102              
103 3     3   2532 use parent "Exporter";
  3         2367  
  3         16  
104             our @EXPORT_OK = qw(
105             present_y
106             month_days cmsdn_to_ymd ymd_to_cmsdn present_ymd
107             year_days cmsdn_to_yd yd_to_cmsdn present_yd
108             );
109              
110             # _numify(A): turn possibly-object number into native Perl integer
111              
112             sub _numify($) {
113 248     248   25258 my($a) = @_;
114 248 100       992 return ref($a) eq "" ? $a : $a->numify;
115             }
116              
117             # _fdiv(A, B): divide A by B, flooring remainder
118             #
119             # B must be a positive Perl integer. A may be a Perl integer, Math::BigInt,
120             # or Math::BigRat. The result has the same type as A.
121              
122             sub _fdiv($$) {
123 188     188   282 my($a, $b) = @_;
124 188 100       412 if(ref($a) eq "Math::BigRat") {
125 36         132 return ($a / $b)->bfloor;
126             } else {
127 152 100       334 if($a < 0) {
128 3     3   4056 use integer;
  3         32  
  3         15  
129 8         528 return -(($b - 1 - $a) / $b);
130             } else {
131 3     3   118 use integer;
  3         8  
  3         12  
132 144         4774 return $a / $b;
133             }
134             }
135             }
136              
137             # _fmod(A, B): A modulo B, flooring remainder
138             #
139             # B must be a positive Perl integer. A may be a Perl integer, Math::BigInt,
140             # or Math::BigRat. The result has the same type as A.
141              
142             sub _fmod($$) {
143 572     572   64116 my($a, $b) = @_;
144 572 100       1132 if(ref($a) eq "Math::BigRat") {
145 74         261 return $a - $b * ($a / $b)->bfloor;
146             } else {
147 498         2627 return $a % $b;
148             }
149             }
150              
151             =head1 FUNCTIONS
152              
153             Numbers in this API may be native Perl integers, C objects,
154             or integer-valued C objects. All three types are acceptable
155             for all parameters, in any combination. In all conversion functions,
156             the most-significant part of the result (which is the only part with
157             unlimited range) is of the same type as the most-significant part of
158             the input. Less-significant parts of results (which have a small range)
159             are consistently native Perl integers.
160              
161             All functions C if given invalid parameters.
162              
163             =head2 Years
164              
165             =over
166              
167             =item present_y(YEAR)
168              
169             Puts the given year number into the conventional textual presentation
170             format. For years [0, 9999] this is simply four digits. For years
171             outside that range it is a sign followed by at least four digits.
172              
173             This is the minimum-length presentation format. If it is desired
174             to use a form that is longer than necessary, such as to use at least
175             five digits for all year numbers, then the right tool is C
176             (see L).
177              
178             =cut
179              
180             sub present_y($) {
181 56     56 1 130726 my($y) = @_;
182 56         253 my($sign, $digits) = ("$y" =~ /\A\+?(-?)0*([0-9]+?)\z/);
183 56 100       896 $digits = ("0" x (4 - length($digits))).$digits
184             unless length($digits) >= 4;
185 56 100 100     240 $sign = "+" if $sign eq "" && length($digits) > 4;
186 56         174 return $sign.$digits;
187             }
188              
189             =back
190              
191             =head2 Darian calendar
192              
193             Each year is divided into 24 months, numbered [1, 24]. Each month is
194             divided into days, numbered sequentially from 1. The month lengths
195             are irregular. The year numbers have unlimited range.
196              
197             =over
198              
199             =item month_days(YEAR, MONTH)
200              
201             The parameters identify a month, and the function returns the number of
202             days in that month as a native Perl integer.
203              
204             =cut
205              
206             sub _year_leap($) {
207 210     210   296 my($y) = @_;
208 210   66     441 return _fmod($y, 2) == 1 ||
209             (_fmod($y, 10) == 0 &&
210             (_fmod($y, 100) != 0 || _fmod($y, 500) == 0));
211             }
212              
213             {
214             sub month_days($$) {
215 142     142 1 210499 my($y, $m) = @_;
216 142 50 33     953 croak "month number $m is out of the range [1, 24]"
217             unless $m >= 1 && $m <= 24;
218 142 100       331 if($m == 24) {
219 53 100       131 return _year_leap($y) ? 28 : 27;
220             } else {
221 89 100       232 return _fmod($m, 6) == 0 ? 27 : 28;
222             }
223             }
224             }
225              
226             =item cmsdn_to_ymd(CMSDN)
227              
228             This function takes a Chronological Mars Solar Day Number and returns
229             a list of a year, month, and day.
230              
231             =cut
232              
233             sub cmsdn_to_yd($);
234              
235             sub cmsdn_to_ymd($) {
236 29     29 1 26955 my($cmsdn) = @_;
237 29         74 my($y, $d) = cmsdn_to_yd($cmsdn);
238 29 100       7305 return ($y, 24, 28) if $d == 669;
239 23         30 $d--;
240 23         54 my $sixm = _fdiv($d, 28*6 - 1);
241 23         50 $d -= $sixm * (28*6 - 1);
242 23         37 my $m = _fdiv($d, 28);
243 23         55 return ($y, 1 + 6*$sixm + $m, 1 + _fmod($d, 28));
244             }
245              
246             =item ymd_to_cmsdn(YEAR, MONTH, DAY)
247              
248             This performs the reverse of the translation that C does.
249             It takes year, month, and day numbers, and returns the corresponding
250             CMSDN.
251              
252             =cut
253              
254             sub yd_to_cmsdn($$);
255              
256             sub ymd_to_cmsdn($$$) {
257 33     33 1 32324 my($y, $m, $d) = @_;
258 33 100 100     560 croak "month number $m is out of the range [1, 24]"
259             unless $m >= 1 && $m <= 24;
260 31         77 $m = _numify($m);
261 31         78 my $md = month_days($y, $m);
262 31 100 100     5247 croak "day number $d is out of the range [1, $md]"
263             unless $d >= 1 && $d <= $md;
264 27         93 return yd_to_cmsdn($y, ($m - 1) * 28 - _fdiv($m - 1, 6) + _numify($d));
265             }
266              
267             =item present_ymd(CMSDN)
268              
269             =item present_ymd(YEAR, MONTH, DAY)
270              
271             Puts the given date into conventional Darian textual presentation format.
272              
273             If the date is given as a (YEAR, MONTH, DAY) triplet then these are not
274             checked for consistency. The MONTH and DAY values are only checked to
275             ensure that they fit into the fixed number of digits. This allows the
276             use of this function on data other than actual Darian dates.
277              
278             =cut
279              
280             sub present_ymd($;$$) {
281 11     11 1 4244 my($y, $m, $d);
282 11 100       30 if(@_ == 1) {
283 2         8 ($y, $m, $d) = cmsdn_to_ymd($_[0]);
284             } else {
285 9         19 ($y, $m, $d) = @_;
286 9 100 100     524 croak "month number $m is out of the displayable range"
287             unless $m >= 0 && $m < 100;
288 7 100 100     310 croak "day number $d is out of the displayable range"
289             unless $d >= 0 && $d < 100;
290             }
291 7         16 return sprintf("%s-%02d-%02d", present_y($y),
292             _numify($m), _numify($d));
293             }
294              
295             =back
296              
297             =head2 Ordinal dates
298              
299             Each year is divided into days, numbered sequentially from 1. The year
300             lengths are irregular. The years correspond exactly to those of the
301             Darian calendar.
302              
303             =over
304              
305             =item year_days(YEAR)
306              
307             The parameter identifies a year, and the function returns the number of
308             days in that year as a native Perl integer.
309              
310             =cut
311              
312             sub year_days($) {
313 157     157 1 124132 my($y) = @_;
314 157 100       636 return _year_leap($y) ? 669 : 668;
315             }
316              
317 3     3   3227 use constant DARIAN_ZERO_CMSDN => 405871; # 0000-001
  3         10  
  3         394  
318              
319             =item cmsdn_to_yd(CMSDN)
320              
321             This function takes a Chronological Mars Solar Day Number and returns
322             a list of a year and ordinal day.
323              
324             =cut
325              
326             sub cmsdn_to_yd($) {
327 58     58 1 21505 my($cmsdn) = @_;
328 3     3   17 use integer;
  3         6  
  3         24  
329 58         183 my $d = $cmsdn - DARIAN_ZERO_CMSDN;
330 58         9667 my $qcents = _fdiv($d, 668*500 + 296);
331 58         10770 $d = _numify($d - $qcents * (668*500 + 296));
332 58         1267 my $y = $d / 669;
333 58 100       179 my $leaps = ($y / 2) + ($y+9) / 10 - ($y+99) / 100 + ($y == 0 ? 0 : 1);
334 58         90 $d -= 668 * $y + $leaps;
335 58         129 my $yd = year_days($y);
336 58 100       348 if($d >= $yd) {
337 20         29 $d -= $yd;
338 20         31 $y++;
339             }
340 58         174 return ($qcents*500 + $y, 1 + $d);
341             }
342              
343             =item yd_to_cmsdn(YEAR, DAY)
344              
345             This performs the reverse of the translation that C does.
346             It takes year and ordinal day numbers, and returns the corresponding
347             CMSDN.
348              
349             =cut
350              
351             sub yd_to_cmsdn($$) {
352 57     57 1 25390 my($y, $d) = @_;
353 3     3   656 use integer;
  3         6  
  3         13  
354 57         129 my $qcents = _fdiv($y, 500);
355 57         10700 $y = _numify($y - $qcents * 500);
356 57         746 my $yd = year_days($y);
357 57 100 100     844 croak "day number $d is out of the range [1, $yd]"
358             unless $d >= 1 && $d <= $yd;
359 54         124 $d = _numify($d);
360 54 100       188 my $leaps = ($y / 2) + ($y+9) / 10 - ($y+99) / 100 + ($y == 0 ? 0 : 1);
361 54         205 return (DARIAN_ZERO_CMSDN + 668*$y + $leaps + ($d - 1)) +
362             $qcents * (668*500 + 296);
363             }
364              
365             =item present_yd(CMSDN)
366              
367             =item present_yd(YEAR, DAY)
368              
369             Puts the given date into the conventional ordinal textual presentation
370             format.
371              
372             If the date is given as a (YEAR, DAY) pair then these are not checked
373             for consistency. The DAY value is only checked to ensure that it fits
374             into the fixed number of digits. This allows the use of this function
375             on data other than actual ordinal dates.
376              
377             =cut
378              
379             sub present_yd($;$) {
380 9     9 1 1167 my($y, $d);
381 9 100       24 if(@_ == 1) {
382 2         7 ($y, $d) = cmsdn_to_yd($_[0]);
383             } else {
384 7         12 ($y, $d) = @_;
385 7 100 100     348 croak "day number $d is out of the displayable range"
386             unless $d >= 0 && $d < 1000;
387             }
388 7         14 return sprintf("%s-%03d", present_y($y), _numify($d));
389             }
390              
391             =back
392              
393             =head1 SEE ALSO
394              
395             L,
396             L,
397             L
398              
399             =head1 AUTHOR
400              
401             Andrew Main (Zefram)
402              
403             =head1 COPYRIGHT
404              
405             Copyright (C) 2007, 2009, 2011 Andrew Main (Zefram)
406              
407             =head1 LICENSE
408              
409             This module is free software; you can redistribute it and/or modify it
410             under the same terms as Perl itself.
411              
412             =cut
413              
414             1;