File Coverage

blib/lib/Temperature/Calculate/DegreeDays.pm
Criterion Covered Total %
statement 76 82 92.6
branch 43 54 79.6
condition 49 93 52.6
subroutine 9 9 100.0
pod 4 4 100.0
total 181 242 74.7


line stmt bran cond sub pod time code
1             package Temperature::Calculate::DegreeDays;
2              
3 2     2   181478 use 5.006;
  2         6  
4 2     2   11 use strict;
  2         3  
  2         58  
5 2     2   5 use warnings;
  2         4  
  2         92  
6 2     2   36 use Carp qw(carp croak);
  2         3  
  2         114  
7 2     2   10 use Scalar::Util qw(looks_like_number reftype);
  2         2  
  2         1761  
8              
9             =head1 NAME
10              
11             Temperature::Calculate::DegreeDays - Perl package to compute cooling, heating, and growing degree days
12              
13             =head1 VERSION
14              
15             Version 0.50
16              
17             =cut
18              
19             our $VERSION = '0.50';
20              
21             =head1 SYNOPSIS
22              
23             use Temperature::Calculate::DegreeDays;
24            
25             # Default params
26             my $dd_degF = Temperature::Calculate::DegreeDays->new();
27             # Try cooling and heating degree days in degC, and wheat growing degree days instead of corn!
28             my $dd_degC = Temperature::Calculate::DegreeDays->new({
29             BASE => 18,
30             GBASE => 0,
31             GCEILING => 27,
32             MISSING => -9999
33             });
34            
35             my $daily_high_temperature = 80;
36             my $daily_low_temperature = 60;
37             my $cdd = $dd_degF->cooling($daily_high_temperature,$daily_low_temperature); # Result is 5
38             my $hdd = $dd_degF->heating($daily_high_temperature,$daily_low_temperature); # Result is 0
39             my $gdd = $dd_degF->growing($daily_high_temperature,$daily_low_temperature); # Result is 20
40              
41             =head1 DESCRIPTION
42              
43             A degree day is a measure of sensible temperature compared to a baseline
44             temperature, typically integrated over time. Degree days have numerous
45             applications in agriculture, as temperatures during the growing season have a
46             direct impact on crop growth progress. Degree days also support energy usage
47             monitoring, as the costs to heat and cool climate controlled structures is
48             directly related to outdoor temperatures. The simplest method to calculate degree
49             days is to compare the daily mean temperature (the average of the daily high and low
50             observed temperatures) to a baseline temperature. This is how degree days are
51             L.
52              
53             The Temperature::Calculate::DegreeDays package provides methods to calculate
54             the following types of degree days:
55              
56             =over 4
57              
58             =item * Cooling degree days - zero if the baseline temperature exceeds the mean, otherwise the difference between the mean and baseline rounded to the nearest integer
59              
60             =item * Heating degree days - zero if the mean temperature exceeds the baseline, otherwise the difference between the baseline and mean rounded to the nearest integer
61              
62             =item * Growing degree days - same as cooling degree days, but when calculating the daily mean, temperatures exceeding a maximum "ceiling" value are set to the ceiling value
63              
64             =back
65              
66             This package was designed using an object-oriented framework, with a constructor
67             method (new) that returns a blessed reference. The object stores the baseline
68             temperature for heating and cooling degree days, a separate baseline temperature
69             for growing degree days, a growing degree days ceiling temperature, and a value to
70             be interpreted as missing data. The various degree days methods are designed to
71             handle missing or invalid input data gracefully by returning the missing data value,
72             but will fail if the caller does not supply the required number of arguments.
73              
74             =head1 METHODS
75              
76             =head2 new
77              
78             my $dd_degF = Temperature::Calculate::DegreeDays->new();
79             my $dd_degC = Temperature::Calculate::DegreeDays->new({BASE => 18});
80             my $dd_degC_2 = Temperature::Calculate::DegreeDays->new({
81             BASE => 18,
82             GBASE => 0,
83             GCEIL => 27,
84             MISSING => -9999
85             });
86              
87             Constructs a Temperature::Calculate::DegreeDays object (L)
88             and returns it to the calling program. The default object data can be changed by
89             passing a L argument with some or all
90             of the following parameters:
91              
92             $params = {
93             BASE => [baseline temperature for cooling and heating degree days],
94             GBASE => [baseline temperature for growing degree days],
95             GCEILING => [ceiling/heat threshold temperature for growing degree days]
96             MISSING => [missing value]
97             }
98              
99             If supplied, these parameters must have defined and numeric values or the constructor will
100             L. If not supplied, the default values are C,
101             C and C, which is how the US National Weather Service defines cooling,
102             heating, and corn (maize) growing degree days in degrees Fahrenheit. The default missing value
103             is C.
104              
105             =cut
106              
107             sub new {
108 6     6 1 140457 my $class = shift;
109 6         12 my $self = {};
110             # Set defaults
111 6         15 $self->{BASE} = 65;
112 6         8 $self->{GBASE} = 50;
113 6         15 $self->{GCEILING} = 86;
114 6         11 my $inf = exp(~0 >> 1);
115 6         9 my $nan = $inf / $inf;
116 6         11 $self->{MISSING} = $nan;
117              
118 6 100       16 if(@_) {
119 5         6 my $arg = shift;
120             # Assume the caller did something wrong and croak if the arg is not a hash ref
121 5 50       12 unless(reftype($arg) eq 'HASH') { croak "Argument must be a hash reference"; }
  0         0  
122              
123             # Check for each allowed key in the hash ref, croak if the value is undef or non-numeric
124              
125 5         7 foreach my $param (qw(BASE GBASE GCEILING MISSING)) {
126              
127 14 100       24 if(exists($arg->{$param})) {
128 8 100 66     359 croak "Invalid $param param" unless(defined($arg->{$param}) and looks_like_number($arg->{$param}));
129 4         5 $self->{$param} = $arg->{$param};
130             }
131              
132             }
133              
134             }
135              
136 2         5 bless($self,$class);
137 2         6 return $self;
138             }
139              
140             =head2 cooling
141              
142             my $maxT = 88;
143             my $minT = 60;
144             my $mean = ($maxT + $minT) / 2;
145             my $cdd = $dd->cooling($maxT,$minT); # Order of the args does not matter
146             $cdd = $dd->cooling($mean); # Same result
147              
148             Given one temperature argument taken to be the daily mean temperature, or two
149             arguments taken to be the daily maximum and minimum temperatures, returns the
150             cooling degree days accumulated to the nearest integer. If the argument value(s)
151             are undefined, non-numeric, NaN, or equal to the missing value, the missing value
152             is returned. The method will croak if no argument is supplied.
153              
154             =cut
155              
156             sub cooling {
157 10     10 1 1396 my $self = shift;
158 10         17 my $tmean;
159              
160             # In case the caller stupidly set the base param to missing or NaN...
161 10 50 33     62 return $self->{MISSING} if($self->{BASE} == $self->{MISSING} or not defined($self->{BASE} <=> 0));
162              
163 10 100       20 if(not @_) {
164 1         152 croak "No argument supplied";
165             }
166 9 100       26 if(@_ == 1) {
    50          
167 8         13 $tmean = shift;
168 8 100 66     62 if(not defined($tmean) or not looks_like_number($tmean) or $tmean == $self->{MISSING} or not defined($tmean <=> 0)) { return $self->{MISSING}; }
  5   100     23  
      100        
169             }
170             elsif(@_ >= 2) {
171 1         3 my $tmax = shift;
172 1         2 my $tmin = shift;
173 1 50 33     34 if(not defined($tmax) or not looks_like_number($tmax) or $tmax == $self->{MISSING} or not defined($tmax <=> 0)) { return $self->{MISSING}; }
  0   33     0  
      33        
174 1 50 33     15 if(not defined($tmin) or not looks_like_number($tmin) or $tmin == $self->{MISSING} or not defined($tmin <=> 0)) { return $self->{MISSING}; }
  0   33     0  
      33        
175 1         7 $tmean = ($tmax + $tmin) / 2;
176             }
177              
178 4 100       30 return $tmean > $self->{BASE} ? int($tmean - $self->{BASE} + 0.5) : 0;
179             }
180              
181             =head2 heating
182              
183             my $maxT = 50;
184             my $minT = 35;
185             my $mean = ($maxT + $minT) / 2;
186             my $hdd = $dd->heating($maxT,$minT); # Order of the args does not matter
187             $hdd = $dd->heating($mean); # Same result
188              
189             Given one temperature argument taken to be the daily mean temperature, or two
190             arguments taken to be the daily maximum and minimum temperatures, returns the
191             heating degree days accumulated to the nearest integer. If the argument value(s)
192             are undefined, non-numeric, NaN, or equal to the missing value, the missing value
193             is returned. The method will croak if no argument is supplied.
194              
195             =cut
196              
197             sub heating {
198 10     10 1 17 my $self = shift;
199 10         14 my $tmean;
200              
201             # In case the caller stupidly set the BASE param to missing or NaN...
202 10 50 33     77 return $self->{MISSING} if($self->{BASE} == $self->{MISSING} or not defined($self->{BASE} <=> 0));
203              
204 10 100       18 if(not @_) {
205 1         92 croak "No argument supplied";
206             }
207 9 100       19 if(@_ == 1) {
    50          
208 8         11 $tmean = shift;
209 8 100 66     49 if(not defined($tmean) or not looks_like_number($tmean) or $tmean == $self->{MISSING} or not defined($tmean <=> 0)) { return $self->{MISSING}; }
  5   100     20  
      100        
210             }
211             elsif(@_ >= 2) {
212 1         2 my $tmax = shift;
213 1         1 my $tmin = shift;
214 1 50 33     11 if(not defined($tmax) or not looks_like_number($tmax) or $tmax == $self->{MISSING} or not defined($tmax <=> 0)) { return $self->{MISSING}; }
  0   33     0  
      33        
215 1 50 33     11 if(not defined($tmin) or not looks_like_number($tmin) or $tmin == $self->{MISSING} or not defined($tmin <=> 0)) { return $self->{MISSING}; }
  0   33     0  
      33        
216 1         4 $tmean = ($tmax + $tmin) / 2;
217             }
218              
219 4 100       22 print return $tmean < $self->{BASE} ? int($self->{BASE} - $tmean + 0.5) : 0;
220             }
221              
222             =head2 growing
223              
224             my $maxT = 90;
225             my $minT = 70;
226             my $gdd = $dd->growing($maxT,$minT); # Order of args does not matter
227              
228             Given two arguments taken to be the daily maximum and minimum temperatures, returns
229             the growing degree days accumulated to the nearest integer. If the argument values
230             are undefined, non-numeric, NaN, or equal to the missing value, the missing value is
231             returned. If fewer than two arguments are supplied, the method will croak.
232              
233             =cut
234              
235             sub growing {
236 9     9 1 15 my $self = shift;
237              
238             # In case the caller stupidly set the GBASE or GCEILING params to missing or NaN...
239 9 50 33     64 return $self->{MISSING} if($self->{GBASE} == $self->{MISSING} or $self->{GCEILING} == $self->{MISSING} or not defined($self->{GBASE} <=> 0) or not defined($self->{GCEILING} <=> 0));
      33        
      33        
240              
241 9 100 100     25 if(not @_ or @_ < 2) {
242 2         171 croak "Two arguments were not supplied";
243             }
244              
245 7         7 my $tmax = shift;
246 7         11 my $tmin = shift;
247 7 100 66     73 if(not defined($tmax) or not looks_like_number($tmax) or $tmax == $self->{MISSING} or not defined($tmax <=> 0)) { return $self->{MISSING}; }
  3   100     13  
      100        
248 4 50 33     26 if(not defined($tmin) or not looks_like_number($tmin) or $tmin == $self->{MISSING} or not defined($tmin <=> 0)) { return $self->{MISSING}; }
  0   33     0  
      33        
249 4 100       7 if($tmax > $self->{GCEILING}) { $tmax = $self->{GCEILING}; }
  1         2  
250 4 100       8 if($tmin > $self->{GCEILING}) { $tmin = $self->{GCEILING}; }
  2         4  
251 4         7 my $tmean = ($tmax + $tmin) / 2;
252 4 100       21 return $tmean > $self->{GBASE} ? int($tmean - $self->{GBASE} + 0.5) : 0;
253             }
254              
255             =head1 INSTALLATION
256              
257             The best way to install this module is with a CPAN client, which will resolve and
258             install the dependencies:
259              
260             cpan Temperature::Calculate::DegreeDays
261             cpanm Temperature::Calculate::DegreeDays
262              
263             You can also install the module directly from the distribution directory after
264             downloading it and extracting the files, which will also install the dependencies:
265              
266             cpan .
267             cpanm .
268              
269             If you want to install the module manually do the following in the distribution
270             directory:
271              
272             perl Makefile.PL
273             make
274             make test
275             make install
276              
277             =head1 SUPPORT AND DOCUMENTATION
278              
279             After installing, you can find documentation for this module with the
280             perldoc command.
281              
282             perldoc Temperature::Calculate::DegreeDays
283              
284             You can also look for information at:
285              
286             =over 4
287              
288             =item * RT: CPAN's request tracker (report bugs here)
289              
290             L
291              
292             =item * CPAN Ratings
293              
294             L
295              
296             =item * Search CPAN
297              
298             L
299              
300             =back
301              
302             =head1 BUGS
303              
304             Please report any bugs or feature requests to C, or through
305             the web interface at L. I will be notified, and then you'll
306             automatically be notified of progress on your bug as I make changes.
307              
308             =head1 AUTHOR
309              
310             Adam Allgood
311              
312             =head1 LICENSE AND COPYRIGHT
313              
314             This software is copyright (c) 2024 by Adam Allgood.
315              
316             This is free software; you can redistribute it and/or modify it under
317             the same terms as the Perl 5 programming language system itself.
318              
319             =cut
320              
321             1; # End of Temperature::Calculate::DegreeDays