File Coverage

blib/lib/Vigil/Calendar.pm
Criterion Covered Total %
statement 88 153 57.5
branch 20 52 38.4
condition 25 59 42.3
subroutine 26 30 86.6
pod 17 21 80.9
total 176 315 55.8


line stmt bran cond sub pod time code
1             package Vigil::Calendar;
2            
3             require 5.010;
4 2     2   387465 use Carp;
  2         4  
  2         170  
5 2     2   19 use strict;
  2         4  
  2         69  
6 2     2   9 use warnings;
  2         6  
  2         169  
7 2     2   1139 use Time::Local qw(timegm);
  2         4572  
  2         200  
8             our $VERSION = '2.1.4';
9            
10 2     2   13 use constant MONTHS => { 1 => 'January', 2 => 'February', 3 => 'March', 4 => 'April', 5 => 'May', 6 => 'June', 7 => 'July', 8 => 'August', 9 => 'September', 10 => 'October', 11 => 'November', 12 => 'December' };
  2         3  
  2         178  
11            
12 2     2   11 use constant BASE_DAYS_IN_MONTH => { 1 => 31, 2 => 28, 3 => 31, 4 => 30, 5 => 31, 6 => 30, 7 => 31, 8 => 31, 9 => 30, 10 => 31, 11 => 30, 12 => 31 };
  2         3  
  2         93  
13            
14 2     2   9 use constant DAYS_IN_A_WEEK => 7;
  2         2  
  2         3933  
15            
16             sub new {
17 1     1 1 214912 my ($pkg, $y, $m) = @_;
18             #Default to current date if missing
19 1 50 33     9 unless (defined $y && defined $m) {
20 0         0 my ($sec, $min, $hour, $mday, $mon, $year) = gmtime();
21 0         0 $y = $year + 1900;
22 0         0 $m = $mon + 1;
23             }
24             #Validate inputs
25 1 50 33     18 return unless $y =~ /^\d+$/ && $m =~ /^\d+$/ && $m >= 1 && $m <= 12;
      33        
      33        
26             #Calculate previous and next months/years
27 1 50       6 my ($pm, $py) = $m == 1 ? (12, $y - 1) : ($m - 1, $y);
28 1 50       5 my ($nm, $ny) = $m == 12 ? (1, $y + 1) : ($m + 1, $y);
29 1         12 bless {
30             _year => $y,
31             _month => $m,
32             _previous_month_number => $pm,
33             _previous_month_year => $py,
34             _next_month_number => $nm,
35             _next_month_year => $ny,
36             }, $pkg;
37             }
38            
39 1     1 1 15 sub month { return $_[0]->{_month}; }
40            
41 1     1 1 6 sub year { return $_[0]->{_year}; }
42            
43 1     1 1 6 sub previous_month_number{ return $_[0]->{_previous_month_number}; }
44            
45 1     1 1 7 sub previous_month_year{ return $_[0]->{_previous_month_year}; }
46            
47 1     1 1 7 sub days_in_previous_month { return $_[0]->days_in_month($_[0]->{_previous_month_year}, $_[0]->{_previous_month_number}); }
48            
49 1     1 1 6 sub next_month_number{ return $_[0]->{_next_month_number}; }
50            
51 1     1 1 6 sub next_month_year{ return $_[0]->{_next_month_year}; }
52            
53 1     1 1 7 sub days_in_next_month { return $_[0]->days_in_month($_[0]->{_next_month_year}, $_[0]->{_next_month_number}); }
54            
55             sub evaluate {
56             #Returns an evaluation of the _month in the following list form:
57             #($name_of_month, $days_in_previous_month, $days_in_this_month, $days_in_next_month, $is_a_leap_year,
58             # $name_of_day_of_first_day, $name_of_day_of_last_day, $num_of_sundays)
59 0     0 1 0 my $self = shift;
60 0         0 my @ret_array;
61 0         0 $ret_array[0] = $self->month_name($self->{_month});
62 0         0 $ret_array[1] = $self->days_in_previous_month;
63 0         0 $ret_array[2] = $self->days_in_month;
64 0         0 $ret_array[3] = $self->days_in_next_month;
65 0         0 $ret_array[4] = $self->is_a_leap_year;
66 0         0 $ret_array[5] = $self->dayname(1);
67 0         0 $ret_array[6] = $self->dayname($self->days_in_month);
68 0         0 my @count = $self->_get_sundays;
69 0         0 $ret_array[7] = $#count + 1;
70 0         0 return(@ret_array);
71             }
72            
73             sub is_a_leap_year {
74             #Returns true if the object year is a leap year, false if it isn't.
75 5     5 1 14 my ($self, $test_year) = @_;
76 5   33     15 $test_year ||= $self->{_year};
77 5 50 33     25 return 1 if $test_year % 4 == 0 && ($test_year % 100 != 0 || $test_year % 400 == 0);
      66        
78 3         11 return 0;
79             }
80            
81             sub dayname {
82 1     1 1 3 my ($self, $dom, $month, $year) = @_;
83 1   33     5 my $mo = $month // $self->{_month};
84 1   33     3 my $yr = $year // $self->{_year};
85 1 50       6 return unless length($yr) == 4;
86 1 50 33     7 return unless $mo >= 1 && $mo <= 12;
87             # Zeller's congruence: https://www.geeksforgeeks.org/dsa/zellers-congruence-find-day-date/
88             # Zeller's congruence: 0 = Saturday, 1 = Sunday ... 6 = Friday
89 1         3 my $y = $yr;
90 1         2 my $m = $mo;
91 1 50       8 if ($m == 1) { $m = 13; $y--; }
  0 50       0  
  0         0  
92 0         0 elsif ($m == 2) { $m = 14; $y--; }
  0         0  
93            
94 1         2 my $k = $y % 100;
95 1         4 my $j = int($y / 100);
96            
97 1         7 my $f = int($dom + int((13 * ($m + 1)) / 5) + $k + int($k / 4) + int($j / 4) + 5 * $j) % 7;
98            
99             # Convert Zeller's (0=Sat) to your system (0=Sun, 6=Sat)
100 1         2 my $dow = ($f + 6) % 7;
101             #This is not reliant on the one-based list of week days, so we use a zero-based list.
102 1         10 my %days = (
103             0 => 'Sunday',
104             1 => 'Monday',
105             2 => 'Tuesday',
106             3 => 'Wednesday',
107             4 => 'Thursday',
108             5 => 'Friday',
109             6 => 'Saturday',
110             );
111 1         6 return $days{$dow};
112             }
113            
114             sub weekday {
115 3     3 1 9 my ($self, $dom, $month, $year) = @_;
116 3   50     10 $dom //= 1;
117 3   66     13 $month //= $self->{_month};
118 3   66     10 $year //= $self->{_year};
119 3 50       19 return 0 unless $dom =~ /^\d+$/;
120 3 50       8 return unless $dom <= $self->days_in_month($year, $month);
121             #Tomohiko Sakamoto’s Algorithm - https://www.geeksforgeeks.org/dsa/tomohiko-sakamotos-algorithm-finding-day-week/
122 3         10 my @offset = (0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4);
123 3         8 $year -= $month < 3; # Jan & Feb are considered part of previous year
124             #We add 1 because this module calculates Sunday as 1, not 0.
125 3         28 return 1 + (($year + int($year / 4) - int($year / 100) + int($year / 400) + $offset[$month - 1] + $dom) % DAYS_IN_A_WEEK);
126             }
127            
128             sub calendar_week {
129             #This method takes a day of month as an argument and then returns
130             #the calendar week that day is in based on the objects current
131             #year and month. Max value for this is 6.
132 1     1 1 5 return int( ($_[1] + $_[0]->weekday(1) - 2) / 7 ) + 1;
133             }
134            
135             sub month_name {
136             #Convert month number to name
137 1     1 0 5 my ($self, $num, $short) = @_;
138 1   33     5 $num //= $self->{_month};
139 1 50 33     7 return 0 unless $num >= 1 && $num <= 12;
140 1 50       9 return $short ? substr(MONTHS->{$num}, 0, 3) : MONTHS->{$num};
141             }
142            
143             sub month_number {
144             #Convert month name to number
145 1     1 0 3 my $self = shift;
146 1         3 my $month = shift;
147 1         4 $month = lc(substr($month, 0, 3));
148 1         12 my %hash = (jan => '1', feb => '2', mar => '3', apr => '4', may => '5', jun => '6', jul => '7', aug => '8', sep => '9', oct => '10', nov => '11', dec => '12');
149 1 50       5 if($hash{$month}) {
150 1         8 return($hash{$month});
151             } else {
152 0         0 return(0);
153             }
154             }
155            
156 1     1 1 4 sub weeks_in_month { return 1 + int( ($_[0]->days_in_month($_[1], $_[2]) - (9 - $_[0]->weekday(1, $_[2], $_[1])) + 6) / 7 ); }
157            
158             sub days_in_month {
159 7     7 1 16 my $self = shift;
160 7   33     25 my $test_year = shift || $self->{_year};
161 7   33     18 my $test_month = shift || $self->{_month};
162 7         14 my %days = %{ BASE_DAYS_IN_MONTH() };
  7         57  
163 7 100 100     39 $days{2} = 29 if $test_month == 2 && $self->is_a_leap_year($test_year);
164 7         45 return($days{$test_month});
165             }
166            
167             sub week_definition {
168             # NOTE: This module uses 1-based weekdays (Sunday = 1, Monday = 2, …, Saturday = 7)
169             # instead of the more common 0-based system (Sunday = 0).
170             # This was originally done to simplify the calculations in week_definition,
171             # which constructs calendar weeks including spillover from previous/next months.
172             #
173             #No matter what year or month, the maximum number of possible calendar weeks
174             #in any give month is six. Therefore, this method will return the dates, in
175             #order, of any calendar week within a month where '1' is the first week and
176             #'6' is the last possible week. If the week is not a valid week for that
177             #month, then a null value is returned.
178 0     0 1 0 my $self = shift;
179 0         0 my $week = shift;
180 0 0       0 return if $week > $self->weeks_in_month;
181            
182 0         0 my @sunday = $self->_get_sundays;
183            
184             #Initialize and localize the array that will return the results.
185 0         0 my @evaluated_week;
186            
187             #The first week is a special week no day of the week except the last
188             #day of the week is guaranteed to be a valid date. Therefore we have
189             #to find the position in the week for the start of the month.
190 0         0 my $position = $self->weekday(1);
191            
192 0 0       0 if($week == 1) {
193             #if the position is equal to '1', then the first of the month is on
194             #a sunday and we can proceed as normal, otherwise, we have to muck
195             #about with the calculations and return the dates of the previous
196             #month for the calendar month preceeding the objects month.
197 0 0       0 if($position == 1) {
198 0         0 my $a;
199 0         0 for($a = 0; $a <= 6; $a++) {
200             #Please see this methods outer 'else' conditional for notes
201             #on what we are doing in this for-loop. No test for being
202             #over the number of days for the month is required as this
203             #is the first calendar week in the month.
204 0         0 my $this_day = 1 + $a;
205 0         0 push(@evaluated_week, $this_day);
206             }
207             } else {
208             #Since the first weeks sunday is not on the first calenday day of the week
209             #we have to determine what that first days date would be for the previous
210             #month that shows up in this months first calendar week. The formula for
211             # this is the date of the day of the month that is $a numbers subtracted
212             #from the maximum number of days in the month.
213 0         0 my $dipm = $self->days_in_previous_month;
214 0         0 my $a;
215 0         0 for($a = ($position - 2); $a >= 0; $a--) {
216 0         0 push(@evaluated_week, $dipm - $a);
217             }
218            
219             #Now that we have determined the dates for the portion of the week prior
220             #to the first of the month, we will determine the dates for the days of
221             #the calendar week that follow the first day of the month.
222 0         0 my $day = 1;
223 0         0 for($a = $position; $a <= DAYS_IN_A_WEEK; $a++) {
224 0         0 push(@evaluated_week, $day);
225 0         0 $day++;
226             }
227             }
228             } else {
229             #We are not in the first week, therefore everything can go along tickety-boo
230             #until we reach the final calendar week for the month.
231             #Since we are accepting a real number as the argument but the first sunday
232             #dates are stored as an array, we have to decrement the real number to match
233             #the position in the array of sundays.
234            
235 0 0       0 if($position == 1) {
236             #1st on a Sunday
237 0         0 $week -= 1;
238             } else {
239             #1st NOT on a Sunday
240 0         0 $week -= 2;
241             }
242            
243             #Now, step through seven days and build the array.
244 0         0 my $a;
245 0         0 my $next_month_day = 0;
246 0         0 for($a = 0; $a <= 6; $a++) {
247             #Date of the first day of the calendar week plus the day of the position
248             #in the week (minus one cause were in an array).
249 0         0 my $this_day = $sunday[$week] + $a;
250            
251             #If this day of the calendar week has a date that is greater than the
252             #number of days in the month, then we will instead add in the date of
253             #that calendar week position for the following month. The program calling
254             #this will have to test the output based on previous output if it wants
255             #to make any colouring differences based on the date of that day in the
256             #calendar week.
257 0 0       0 if($this_day > $self->days_in_month) {
258 0         0 $this_day = ++$next_month_day;
259             }
260 0         0 push(@evaluated_week, $this_day);
261             }
262             }
263 0         0 return(@evaluated_week);
264             }
265            
266             sub ordinal {
267 1     1 0 4 my ($self, $dom) = @_;
268 1 50       8 return 'th' if $dom =~ /11$|12$|13$/; # special case teens
269 1 50       4 return 'st' if $dom % 10 == 1;
270 1 50       7 return 'nd' if $dom % 10 == 2;
271 0 0       0 return 'rd' if $dom % 10 == 3;
272 0         0 return 'th';
273             }
274            
275             sub sse_from_ymd {
276 1     1 0 4 my ($self, $y, $m, $d) = @_;
277 1 50 33     10 return unless $y && $m && $d;
      33        
278 1         8 return timegm(0, 0, 0, $d, $m - 1, $y - 1900);
279             }
280            
281             sub _first_sunday {
282             # NOTE: This module uses 1-based weekdays (Sunday = 1, Monday = 2, …, Saturday = 7)
283             # instead of the more common 0-based system (Sunday = 0).
284             # This was originally done to simplify the calculations in week_definition,
285             # which constructs calendar weeks including spillover from previous/next months.
286 0     0     my $self = shift;
287             #return $self->{firstsunday} if exists $self->{firstsunday};
288 0           my $pos = $self->weekday(1);
289 0 0         $self->{firstsunday} = ($pos == 1 ? 1 : 9 - $pos);
290 0           return $self->{firstsunday};
291             }
292            
293             sub _get_sundays {
294 0     0     my $self = shift;
295 0           my $fs = $self->_first_sunday;
296 0           my $dim = $self->days_in_month;
297 0           my @sundays;
298 0           push @sundays, $fs + DAYS_IN_A_WEEK * $_ for 0..5;
299 0           @sundays = grep { $_ <= $dim } @sundays;
  0            
300 0           return @sundays;
301             }
302            
303             1;
304            
305            
306             __END__