File Coverage

blib/lib/Date/Darian/Mars.pm
Criterion Covered Total %
statement 102 102 100.0
branch 45 46 97.8
condition 24 27 88.8
subroutine 23 23 100.0
pod 9 9 100.0
total 203 207 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 qw(
12             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 qw(
21             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   221260 { use 5.006; }
  3         15  
96 3     3   26 use warnings;
  3         11  
  3         121  
97 3     3   22 use strict;
  3         6  
  3         109  
98              
99 3     3   21 use Carp qw(croak);
  3         7  
  3         242  
100              
101             our $VERSION = "0.004";
102              
103 3     3   1785 use parent "Exporter";
  3         1260  
  3         21  
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   40633 my($a) = @_;
114 248 100       874 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   364 my($a, $b) = @_;
124 188 100       467 if(ref($a) eq "Math::BigRat") {
125 36         138 return ($a / $b)->bfloor;
126             } else {
127 152 100       314 if($a < 0) {
128 3     3   2665 use integer;
  3         53  
  3         21  
129 8         547 return -(($b - 1 - $a) / $b);
130             } else {
131 3     3   172 use integer;
  3         11  
  3         14  
132 144         5746 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   123414 my($a, $b) = @_;
144 572 100       1159 if(ref($a) eq "Math::BigRat") {
145 74         264 return $a - $b * ($a / $b)->bfloor;
146             } else {
147 498         1914 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 126745 my($y) = @_;
182 56         361 my($sign, $digits) = ("$y" =~ /\A\+?(-?)0*([0-9]+?)\z/);
183 56 100       1217 $digits = ("0" x (4 - length($digits))).$digits
184             unless length($digits) >= 4;
185 56 100 100     256 $sign = "+" if $sign eq "" && length($digits) > 4;
186 56         222 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   367 my($y) = @_;
208 210   66     408 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 192564 my($y, $m) = @_;
216 142 50 33     663 croak "month number $m is out of the range [1, 24]"
217             unless $m >= 1 && $m <= 24;
218 142 100       355 if($m == 24) {
219 53 100       105 return _year_leap($y) ? 28 : 27;
220             } else {
221 89 100       203 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 34814 my($cmsdn) = @_;
237 29         76 my($y, $d) = cmsdn_to_yd($cmsdn);
238 29 100       15090 return ($y, 24, 28) if $d == 669;
239 23         40 $d--;
240 23         55 my $sixm = _fdiv($d, 28*6 - 1);
241 23         48 $d -= $sixm * (28*6 - 1);
242 23         49 my $m = _fdiv($d, 28);
243 23         50 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 29959 my($y, $m, $d) = @_;
258 33 100 100     555 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         85 my $md = month_days($y, $m);
262 31 100 100     7947 croak "day number $d is out of the range [1, $md]"
263             unless $d >= 1 && $d <= $md;
264 27         86 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 2974 my($y, $m, $d);
282 11 100       26 if(@_ == 1) {
283 2         6 ($y, $m, $d) = cmsdn_to_ymd($_[0]);
284             } else {
285 9         17 ($y, $m, $d) = @_;
286 9 100 100     258 croak "month number $m is out of the displayable range"
287             unless $m >= 0 && $m < 100;
288 7 100 100     176 croak "day number $d is out of the displayable range"
289             unless $d >= 0 && $d < 100;
290             }
291 7         14 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 149599 my($y) = @_;
314 157 100       339 return _year_leap($y) ? 669 : 668;
315             }
316              
317 3     3   3285 use constant DARIAN_ZERO_CMSDN => 405871; # 0000-001
  3         11  
  3         390  
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 44084 my($cmsdn) = @_;
328 3     3   27 use integer;
  3         11  
  3         24  
329 58         243 my $d = $cmsdn - DARIAN_ZERO_CMSDN;
330 58         20578 my $qcents = _fdiv($d, 668*500 + 296);
331 58         24610 $d = _numify($d - $qcents * (668*500 + 296));
332 58         1004 my $y = $d / 669;
333 58 100       201 my $leaps = ($y / 2) + ($y+9) / 10 - ($y+99) / 100 + ($y == 0 ? 0 : 1);
334 58         122 $d -= 668 * $y + $leaps;
335 58         143 my $yd = year_days($y);
336 58 100       161 if($d >= $yd) {
337 20         39 $d -= $yd;
338 20         40 $y++;
339             }
340 58         173 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 54103 my($y, $d) = @_;
353 3     3   725 use integer;
  3         11  
  3         15  
354 57         150 my $qcents = _fdiv($y, 500);
355 57         22027 $y = _numify($y - $qcents * 500);
356 57         932 my $yd = year_days($y);
357 57 100 100     587 croak "day number $d is out of the range [1, $yd]"
358             unless $d >= 1 && $d <= $yd;
359 54         115 $d = _numify($d);
360 54 100       203 my $leaps = ($y / 2) + ($y+9) / 10 - ($y+99) / 100 + ($y == 0 ? 0 : 1);
361 54         204 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 4180 my($y, $d);
381 9 100       35 if(@_ == 1) {
382 2         10 ($y, $d) = cmsdn_to_yd($_[0]);
383             } else {
384 7         24 ($y, $d) = @_;
385 7 100 100     377 croak "day number $d is out of the displayable range"
386             unless $d >= 0 && $d < 1000;
387             }
388 7         24 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, 2017
406             Andrew Main (Zefram)
407              
408             =head1 LICENSE
409              
410             This module is free software; you can redistribute it and/or modify it
411             under the same terms as Perl itself.
412              
413             =cut
414              
415             1;