File Coverage

blib/lib/Weather/API/Base.pm
Criterion Covered Total %
statement 121 121 100.0
branch 75 76 98.6
condition 28 32 87.5
subroutine 25 25 100.0
pod 7 7 100.0
total 256 261 98.0


line stmt bran cond sub pod time code
1             package Weather::API::Base;
2              
3 3     3   694335 use 5.008;
  3         21  
4 3     3   17 use strict;
  3         15  
  3         99  
5 3     3   15 use warnings;
  3         6  
  3         154  
6              
7 3     3   16 use Carp;
  3         8  
  3         262  
8 3     3   1647 use LWP::UserAgent;
  3         136827  
  3         129  
9 3     3   23 use Time::Local;
  3         7  
  3         232  
10              
11 3     3   23 use Exporter 'import';
  3         7  
  3         7157  
12              
13             our @EXPORT_OK = qw(ts_to_date ts_to_iso_date datetime_to_ts convert_units mon_to_num num_to_mon);
14             our %EXPORT_TAGS = (all => \@EXPORT_OK);
15             our @months = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
16              
17             =head1 NAME
18              
19             Weather::API::Base - Base/util module for Weather API clients
20              
21             =cut
22              
23             our $VERSION = '0.4';
24              
25             =head1 SYNOPSIS
26              
27             ### Using Helper Functions
28              
29             use Weather::API::Base qw(:all);
30              
31             # Get time in YYYY-MM-DD HH:mm:ss format, local time zone
32             my $datetime = ts_to_date(time());
33              
34             # Convert a date to unix timestamp
35             my $ts = datetime_to_ts('2024-01-12 13:46:40');
36              
37             # Convert 30 degrees Celsius to Fahrenheit
38             my $result = convert_units('C', 'F', 30);
39              
40              
41             ### Building a Weather API client
42              
43             use parent 'Weather::API::Base';
44             use Weather::API::Base qw(:all);
45              
46             # Constructor
47             sub new {
48             my ($class, %args) = @_;
49             return $class->SUPER::new(%args);
50             }
51              
52             # Getting an HTTP::Response
53             sub get_response {
54             my $self = shift;
55             my $url = shift;
56              
57             return $self->_get_ua($url);
58             }
59              
60             # Getting the response contents as a scalar or decoded to a data structure
61             sub get {
62             my $self = shift;
63             my $resp = shift;
64              
65             return $self->_get_output($resp, wantarray);
66             }
67              
68             =head1 DESCRIPTION
69              
70             L is a base class for simple Perl Weather API clients. Apart
71             from handling JSON and XML API responses (L and L required respectivelly),
72             it offers utility functions for time and unit conversions, specifically useful for
73             weather-related APIs.
74              
75             This module was mainly created to streamline maintenance of the L,
76             L and L modules by factoring out shared
77             code. In the unlikely event that you'd like to base your own weather or similar
78             API wrapper module on it, look at the implementation of those modules for guidance.
79              
80             =head1 CONSTRUCTOR
81              
82             =head2 C
83              
84             my $base = Weather::API::Base->new(
85             timeout => $timeout_sec?,
86             agent => $user_agent_string?,
87             ua => $lwp_ua?,
88             error => $die_or_return?,
89             debug => $debug?,
90             output => $output,
91             scheme => $url_scheme?
92             );
93              
94             Creates a Weather::API::Base object. As explained, you'd normally use a module that
95             inherits from this, but the base class sets these defaults:
96              
97             (
98             timeout => 30,
99             agent => "libwww-perl $package/$version",
100             error => 'return',
101             output => 'json',
102             scheme => 'https',
103             );
104              
105             Parameters:
106              
107             =over 4
108              
109             =item * C : Timeout for requests in secs. Default: C<30>.
110              
111             =item * C : Customize the user agent string. Default: C
112              
113             =item * C : Pass your own L to customize further. Will override C.
114              
115             =item * C : If there is an error response with the main methods, you have the options to C or C it. Default: C.
116              
117             =item * C : If debug mode is enabled, API URLs accessed are printed in STDERR when calling C<_get_ua>. Default: C.
118              
119             =item * C : You can use C as an option if it is supported by the API and you have trouble building https support for LWP in your system. Default: C.
120              
121             =item * C : Output format/mode. C are automatically supported for decoding. Default: C.
122              
123             =back
124              
125             =head1 PRIVATE METHODS
126              
127             These are to be used when subclassing.
128              
129             =head2 C<_get_output>
130              
131             $self->_get_output($response, wantarray);
132              
133             C<$response> should be an L object, unless C<$self-E{curl}> is true
134             in which case it should be a string. On C a Perl hash (or array) will be
135             returned by decoding a JSON/XML response (if C<$self-E{output}> is C) or
136             just the decoded content as a value for the C key otherwise.
137              
138             =head2 C<_get_ua>
139              
140             my $resp = $self->_get_ua($url);
141              
142             Will either use C<$self-E{ua}> or create a new one and fetch the C<$url> with it.
143             If the URL does not contain the scheme, it will be applied from C<$self-E{scheme}>.
144              
145              
146             =head1 HELPER FUNCTIONS
147              
148             Exportable helper/utility functions:
149              
150             =head2 C
151              
152             my $result = convert_units($from, $to, $value);
153              
154             Can convert from/to various units that are used in weather:
155              
156             =over 4
157              
158             =item * B km/h, mph, m/s, Bft, kt
159              
160             =item * B K, F, C
161              
162             =item * B mm, in, m, km, mi
163              
164             =item * B atm, mbar, mmHg, kPa, hPa
165              
166             =back
167              
168             Use the above units as string parameters. Example:
169              
170             $result = convert_units('atm', 'mmHg', 1); # Will return 760 (mmHg per 1 atm)
171              
172             If you try to convert between non convertible units, the croak message will list
173             the valid conversions from the 'from' units. For example C
174             will croak with the speed units (km/h, mph, m/s, Bft, kt) that are available to
175             convert from km/h.
176              
177             Note that the Beaufort scale (C) is an empirical scale commonly used in whole
178             numbers (converting to a range of +/- 0.5 Bft in other units), but the convert
179             function will actually give you the approximate floating point value based on an
180             accepted empirical function.
181              
182             =head2 C
183              
184             my $datetime = ts_to_date($timestamp, $utc?);
185              
186             There are many ways to convert unix timestamps to human readable dates, but for
187             convenience you can use C, which is a very fast function that will
188             return the format C in your local time zone, or
189             C in UTC if the second argument is true.
190              
191             =head2 C
192              
193             my $datetime = ts_to_iso_date($timestamp, $utc?);
194              
195             Same as C but returns a strict ISO date with the C date/time
196             separator.
197              
198             =head2 C
199              
200             my $ts = datetime_to_ts($datetime, $utc?);
201              
202             Fast function that accepts C or C and converts
203             to a timestamp (for midnight in the former case). Will use local timezone unless
204             you either pass a true second argument or use datetime with the C (Zulu time)
205             suffix. Accepts any date/time divider, so strict ISO with C will work as well.
206              
207             =head2 C
208              
209             my $month_no = mon_to_num($month_abbrev, $pad_zero?);
210              
211             Takes a 3-letter English month abbreviation and returns the month number (1-12,
212             zero-padded if second argument is true). Case insensitive.
213              
214             =head2 C
215              
216             my $month_abbr = num_to_mon($month_no);
217              
218             Takes the month number (1-12) and returns the 3-letter English month abbreviation
219             (capital first letter).
220              
221             =cut
222              
223             my $geocache;
224              
225             sub new {
226 6     6 1 494746 my $class = shift;
227              
228 6         11 my $self = {};
229 6         15 bless($self, $class);
230              
231 6         21 my %args = @_;
232 6         20 my ($package) = caller;
233 6 100       53 $package = __PACKAGE__ if $package eq 'main';
234 6         69 my $version = $package->VERSION;
235              
236 6         98 my %defaults = (
237             scheme => 'https',
238             timeout => 30,
239             agent => "libwww-perl $package/$version",
240             output => 'json',
241             units => 'metric',
242             error => 'return',
243             );
244 6 100       26 $args{agent} = $args{ua}->agent() if $args{ua};
245 6   66     160 $self->{$_} = $args{$_} || $defaults{$_} for keys %defaults;
246 6         55 $self->{$_} = $args{$_} for qw/ua debug curl language lang/;
247              
248             croak("http or https scheme expected")
249 6 100 100     178 if $self->{scheme} ne 'http' && $self->{scheme} ne 'https';
250              
251 5         22 return $self;
252             }
253              
254             sub ts_to_date {
255 2     2 1 3740 return _ts_to_date(@_);
256             }
257              
258             sub ts_to_iso_date {
259 2     2 1 2303 return _ts_to_date($_[0], $_[1], 'T');
260             }
261              
262             sub _ts_to_date {
263 4     4   10 my $ts = shift;
264 4         8 my $gm = shift;
265 4   100     20 my $iso = shift || ' ';
266 4 100       44 $gm = $gm ? 'Z' : '';
267 4 100       72 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) =
268             $gm ? gmtime($ts) : localtime($ts);
269 4         9 $mon++;
270 4         9 $year += 1900;
271 4         51 return sprintf "%04d-%02d-%02d%s%02d:%02d:%02d%s", $year, $mon, $mday, $iso,
272             $hour, $min, $sec, $gm;
273             }
274              
275             sub datetime_to_ts {
276 5     5 1 6531 my $date = shift;
277 5         23 my $gm = shift;
278 5 100 100     87 return ($7 || $gm)
    100          
279             ? timegm($6, $5, $4, $3, $2 - 1, $1)
280             : timelocal($6, $5, $4, $3, $2 - 1, $1)
281             if $date =~
282             /(\d{4})-(\d{2})-(\d{2})(?:[ _Tt](\d{2}):(\d{2}):(\d{2})([Zz])?)?/;
283              
284 1         120 croak("Unrecognized date format (try 'YYYY-MM-DD' or 'YYYY-MM-DD HH:mm:ss')");
285             }
286              
287             sub mon_to_num {
288 5     5 1 2484 my $month = shift;
289 5         9 my $pad = shift;
290              
291 5 100       20 return unless $month;
292              
293 4         7 my %map;
294 4         56 $map{lc($months[$_-1])} = $_ for 1..12;
295              
296 4         9 my $num = $map{lc($month)};
297              
298 4 100 100     28 return "0$num" if $pad && length($num) < 2;
299              
300 3         16 return $num;
301             }
302              
303             sub num_to_mon {
304 3     3 1 4367 my $num = shift;
305 3 100 66     55 return unless $num && $num > 0 && $num < 13;
      100        
306              
307 1         5 return $months[$num-1];
308             }
309              
310             sub _verify_lat_lon {
311 5     5   3119 my $args = shift;
312              
313             croak("lat between -90 and 90 expected")
314 5 100 100     171 unless defined $args->{lat} && abs($args->{lat}) <= 90;
315              
316             croak("lon between -180 and 180 expected")
317 3 100 100     158 unless defined $args->{lon} && abs($args->{lon}) <= 180;
318             }
319              
320             sub _get_output {
321 9     9   13418 my $self = shift;
322 9         34 my $resp = shift;
323 9         15 my $wantarr = shift;
324 9 100       30 my $output = $wantarr ? $self->{output} : '';
325              
326 9 100       34 return _output($resp, $output) if $self->{curl};
327            
328 8 100       36 if ($resp->is_success) {
329 5         71 return _output($resp->decoded_content, $output);
330             } else {
331 3 100 66     41 if ($self->{error} && $self->{error} eq 'die') {
332 1         4 die $resp->status_line;
333             } else {
334 2 100       17 return $wantarr ? (error => $resp) : "ERROR: ".$resp->status_line;
335             }
336             }
337             }
338              
339             sub _get_ua {
340 3     3   1629 my $self = shift;
341 3         32 my $url = shift;
342 3 100       16 $url = $self->{scheme}.'://'.$url unless $url =~ /^https?:/;
343              
344 3 100       12 warn "$url\n" if $self->{debug};
345              
346 3 100       12 $self->_ua unless $self->{ua};
347              
348 3         48 return $self->{ua}->get($url);
349             }
350              
351             sub _ua {
352 1     1   2 my $self = shift;
353              
354 1         26 $self->{ua} = LWP::UserAgent->new();
355 1         252 $self->{ua}->agent($self->{agent});
356 1         46 $self->{ua}->timeout($self->{timeout});
357             }
358              
359             sub _output {
360 6     6   954 my $str = shift;
361 6         12 my $format = shift;
362              
363 6 100       32 return $str unless $format;
364              
365 4 100       22 if ($format eq 'json') {
    100          
366 1         1512 require JSON;
367 1         14141 return _deref(JSON::decode_json($str));
368             } elsif ($format eq 'xml') {
369 2         16 require XML::Simple;
370 2         9 return _deref(XML::Simple::XMLin($str));
371             }
372 1         11 return (data => $str);
373             }
374              
375             sub _deref {
376 7     7   99810 my $ref = shift;
377 7 100       33 die "Could not decode response body" unless $ref;
378 6 100       19 return $ref unless ref($ref);
379 5 100       68 return %$ref if ref($ref) eq 'HASH';
380 1         4 return @$ref;
381             }
382              
383             my %units = (
384             'km/h' => [1000 / 3600, 'm/s'],
385             mph => [1609.344 / 3600, 'm/s'],
386             Bft => [\&_beaufort, 'm/s'],
387             kt => [0.514444, 'm/s'],
388             'm/s' => [1, 'm/s'],
389             in => [0.0254, 'm'],
390             mm => [0.001, 'm'],
391             mi => [1609.344, 'm'],
392             m => [1, 'm'],
393             km => [1000, 'm'],
394             atm => [1, 'atm'],
395             mbar => [1 / 1013.25, 'atm'],
396             mmHg => [1 / 760, 'atm'],
397             hPa => [1 / 1013.25, 'atm'],
398             kPa => [1 / 101.325, 'atm'],
399             K => [\&_kelvin, 'C'],
400             F => [\&_fahr, 'C'],
401             C => [1, 'C'],
402             );
403              
404             sub _units {
405 2     2   26 my $conv = shift;
406 2 50       17 my @list = sort {$units{$b} cmp $units{$a} || $a cmp $b} keys %units;
  110         222  
407 2 100       92 return join(', ', @list) unless $conv;
408 1 100 66     4 my @ok = map {($units{$_}->[1] && $units{$_}->[1] ne $_) ? $_ : ()} @list;
  18         70  
409 1         143 return join(', ', @ok);
410             }
411              
412             sub convert_units {
413 19     19 1 13248 my ($from, $to, $val) = @_;
414              
415 19 100       129 croak "Value not defined." unless defined $val;
416              
417 18         45 foreach ($from, $to) {
418 35 100       117 croak "$_ not recognized. Supported units: "._units unless $units{$_};
419             }
420              
421             croak "Cannot convert to $to. Can only convert $from to: "._units($from)
422 17 100       68 unless $units{$from}->[1] eq $units{$to}->[1];
423              
424             $val =
425             ref($units{$from}->[0])
426             ? $units{$from}->[0]->($val)
427 16 100       63 : $val * $units{$from}->[0];
428              
429 16 100       60 return $val if $units{$from}->[1] eq $to;
430              
431             return
432             ref($units{$to}->[0])
433             ? $units{$to}->[0]->($val, 1)
434 11 100       57 : $val / $units{$to}->[0];
435             }
436              
437             sub _kelvin {
438 2     2   5 my $val = shift;
439 2 100       7 my $mult = shift() ? 1 : -1;
440              
441 2         12 return $val + $mult * 273.15;
442             }
443              
444             sub _fahr {
445 2     2   5 my $val = shift;
446 2         6 my $rev = shift;
447              
448 2 100       12 return $val * 9 / 5 + 32 if $rev;
449 1         6 return ($val - 32) * 5 / 9;
450             }
451              
452             sub _beaufort {
453 2     2   6 my $val = shift;
454 2         5 my $rev = shift;
455              
456 2 100       12 return ($val / 0.836)**(2 / 3) if $rev;
457 1         7 return 0.836 * ($val**1.5);
458             }
459              
460             =head1 RELATED WEATHER MODULES
461              
462             A quick listing of Perl modules that are based on L:
463              
464             =head2 L
465              
466             If you are interested in astronomy/stargazing the 7Timer! weather forecast might be
467             very useful. It uses the standard NOAA forecast, but calculates astronomical seeing
468             and transparency. It is completely free, no API key needed.
469              
470             =head2 L
471              
472             OpenWeatherMap uses various weather sources combined with their own ML and offers
473             a couple of free endpoints (the v2.5 current weather and 5d/3h forecast) with generous
474             request limits. Their newer One Call 3.0 API also offers some free usage (1000 calls/day)
475             and the cost is per call above that. If you want access to history APIs, extended
476             hourly forecasts etc, there are monthly subscriptions.
477              
478             =head2 L
479              
480             An alternative source for multi-source forecasts is Apple's WeatherKit (based on
481             the old Dark Sky weather API). It offers 500k calls/day for free, but requires a
482             paid Apple developer account.
483              
484             =head2 L
485              
486             Simple client for NOAA's Aurora Forecast Service.
487              
488             =head1 AUTHOR
489              
490             Dimitrios Kechagias, C<< >>
491              
492             =head1 BUGS
493              
494             Please report any bugs or feature requests on L.
495              
496             =head1 GIT
497              
498             L
499              
500             =head1 LICENSE AND COPYRIGHT
501              
502             This software is copyright (c) 2024 by Dimitrios Kechagias.
503              
504             This is free software; you can redistribute it and/or modify it under
505             the same terms as the Perl 5 programming language system itself.
506              
507             =cut
508              
509             1;