File Coverage

blib/lib/Geo/Hashing.pm
Criterion Covered Total %
statement 124 135 91.8
branch 54 68 79.4
condition 17 33 51.5
subroutine 16 16 100.0
pod 8 8 100.0
total 219 260 84.2


line stmt bran cond sub pod time code
1             #!/usr/bin/perl -w
2             #
3             # $Id: Hashing.pm 257 2008-06-25 02:02:19Z dan $
4             #
5              
6             package Geo::Hashing;
7              
8 1     1   25312 use strict;
  1         2  
  1         38  
9 1     1   6 use warnings;
  1         2  
  1         31  
10 1     1   6 use Carp;
  1         6  
  1         116  
11 1     1   5 use Digest::MD5 qw/md5_hex/;
  1         2  
  1         168  
12              
13             our $VERSION = '0.06';
14              
15             =head1 NAME
16              
17             Geo::Hashing - Perl library to calculate Geohashing points
18              
19             =head1 SYNOPSIS
20              
21             use Geo::Hashing;
22             my $g = new Geo::Hashing(lat => 37, lon => -122, date => "2008-05-24");
23             printf "Today's location is at %.6f, %.6f.\n", $g->lat, $g->lon;
24              
25             =head1 DESCRIPTION
26              
27             This module allows calculating the locaiton of Geohashes as described
28             in http://wiki.xkcd.com/geohashing/Main_Page.
29              
30             =head1 METHODS
31              
32             =cut
33              
34             =head2 new
35              
36             Create a new Geo::Hashing object.
37              
38             =cut
39              
40             sub new {
41 3     3 1 620 my $class = shift;
42 3         17 my %p = @_;
43              
44 3         25 my $self = {_lat => 0, _lon => 0, _dlat => 0, _dlon => 0, _debug => 0};
45 3         9 bless $self, $class;
46              
47 3         408 $self->{_date} = sprintf("%04d-%02d-%02d", (localtime)[5]+1900, (localtime)[4]+1, (localtime)[3]);
48              
49             {
50 1     1   5 no strict 'subs';
  1         2  
  1         1782  
  3         8  
51 3         8 foreach (qw/debug source lat lon date use_30w_rule djia/) {
52 21 100       54 if (exists $p{$_}) {
53 7         35 $self->_log("Setting $_ to $p{$_}");
54 7         30 $self->$_($p{$_});
55             }
56             }
57             }
58              
59 3 100       11 unless ($p{source}) {
60 2         7 $self->source('peeron');
61             }
62              
63 3         13 $self->_log("Using", $self->source, "as the DJIA source");
64              
65 3         20 return $self;
66             }
67              
68             =head2 lat
69              
70             Set or get the points latitude. When settings, only the integer portion is
71             considered. Set to undef to just get the offset.
72              
73             =cut
74              
75             sub lat {
76 7     7 1 397 my $self = shift;
77 7         15 my $lat = shift;
78              
79 7 100       31 if (defined $lat) {
80 2 50 33     34 if ($lat =~ /^-?\d+(?:\.\d+)?$/ and $lat > -180 and $lat < 180) {
      33        
81 2 100       11 $self->{_lat} = $lat ne "-0" ? int($lat) : "-0";
82 2         8 $self->_update();
83             } else {
84 0         0 croak "Invalid latitude ($lat)!";
85             }
86             }
87              
88 7 100 66     64 return undef unless defined $self->{_dlat} and defined $self->{_dlon};
89              
90 5 100 66     71 return $self->{_lat} eq "-0" || $self->{_lat} < 0 ?
91             $self->{_lat} - $self->{_dlat} :
92             $self->{_lat} + $self->{_dlat};
93             }
94              
95             =head2 lon
96              
97             Set or get the points longitude. When settings, only the integer portion is
98             considered. Set to undef to just get the offset.
99             =cut
100              
101             sub lon {
102 45     45 1 2597 my $self = shift;
103 45         62 my $lon = shift;
104              
105 45 100       377 if (defined $lon) {
106 3 50 33     76 if ($lon =~ /^-?\d+(?:\.\d+)?$/ and $lon > -180 and $lon < 180) {
      33        
107 3 100       13 $self->{_lon} = $lon ne "-0" ? int($lon) : "-0";
108 3         9 $self->_update();
109             } else {
110 0         0 croak "Invalid longitude ($lon)!";
111             }
112             }
113              
114 45 100 66     281 return undef unless defined $self->{_dlat} and defined $self->{_dlon};
115              
116 41 100 100     605 return $self->{_lon} eq "-0" || $self->{_lon} < 0 ?
117             $self->{_lon} - $self->{_dlon} :
118             $self->{_lon} + $self->{_dlon};
119             }
120              
121             =head2 date
122              
123             Set or get the date used for the calculation. Note that this is the actual
124             date of the meetup in question, even when the 30w rule is in effect.
125             =cut
126              
127             sub date {
128 92     92 1 1859 my $self = shift;
129 92         107 my $date = shift;
130              
131 92 100       207 if (defined $date) {
132 5 50       26 if ($date =~ /^\d\d\d\d-\d\d-\d\d$/) {
133 5         12 $self->{_date} = $date;
134 5         16 $self->djia(undef);
135 5         25 $self->_update();
136             } else {
137 0         0 croak "Invalid date ($date)!";
138             }
139             }
140              
141 92         617 return $self->{_date};
142             }
143              
144             =head2 djia
145              
146             Set or get the Dow Jones Industrial Average used for the calculation. If not
147             set, it will be automatically retrieved depending on the value of
148             $self->source. If the data cannot be retrieved, undef will be returned.
149             =cut
150              
151             sub djia {
152 21     21 1 499 my $self = shift;
153 21         37 my $djia = shift;
154              
155 21 50       87 if ($djia) {
    100          
156 0 0       0 if ($djia =~ /^\d+(?:\.\d+)?$/) {
157 0         0 $self->{_djia} = $djia;
158             } else {
159 0         0 croak "Invalid DJIA ($djia)!";
160             }
161             } elsif ($self->source) {
162 17         63 my $date = $self->date;
163 17 100       58 if ($self->use_30w_rule) {
164 10         127 require Time::Local;
165 10         29 my ($y, $m, $d) = split /-/, $self->date;
166 10         79 my $time = Time::Local::timelocal(0, 0, 0, $d, $m-1, $y);
167 10         992 ($d, $m, $y) = (localtime($time - 24*60*60))[3,4,5];
168 10         18 $m++; $y += 1900;
  10         20  
169 10         45 $date = sprintf("%04d-%02d-%02d", $y, $m, $d);
170             }
171 17         46 $self->_log("Requesting", $self->source, "->DJIA($date)");
172 17         65 $self->{_djia} = $self->_get_djia($date);
173             } else {
174 4         7 $self->_log("No source set, can't automatically get DJIA");
175 4         13 return undef;
176             }
177              
178 17         81 return $self->{_djia};
179             }
180              
181             =head2 source
182              
183             Set the source of the DJIA opening data. Will load Geo::Hashing::Source::Name
184             and call get_djia($date). See Geo::Hashing::Source::Random for a sample.
185             =cut
186              
187             sub source {
188 80     80 1 923 my $self = shift;
189 80         98 my $source = shift;
190              
191 80 100       223 if (defined $source) {
192 3         15 $self->_log("Loading source Geo::Hashing::Source::\u$source");
193 3         264 eval " require Geo::Hashing::Source::\u$source";
194              
195 3 50       20 if ($@) {
196 0         0 croak "Failed to load Geo::Hashing::Source::\u$source: $@";
197             }
198              
199 3         11 $self->{_source} = $source;
200 3         12 $self->_update();
201             }
202              
203 80 100       200 if ($self->{_source}) {
204 76         566 return "Geo::Hashing::Source::" . ucfirst $self->{_source};
205             } else {
206 4         13 return undef;
207             }
208             }
209              
210             =head2 use_30w_rule
211              
212             Set or get the 30w flag. Will be set automatically if lon is set and is
213             greater than -30.
214             =cut
215              
216             sub use_30w_rule {
217 20     20 1 1621 my $self = shift;
218 20         33 my $w30 = shift;
219              
220 20 50       86 if (defined $w30) {
    100          
221 0 0       0 $self->{_30w} = $w30 ? 1 : 0;
222 0         0 $self->_update();
223             } elsif (defined $self->lon) {
224 18 100       41 if ($self->lon > -30) {
225 15 50       5884 if (not $self->date) {
226 0         0 $self->{_30w} = 1;
227             } else {
228 15         35 my ($y, $m, $d) = split /-/, $self->date;
229 15 100 33     139 if ($y > 2008) {
    50 33        
    100 66        
230 6         16 $self->{_30w} = 1;
231             } elsif ($y == 2008 and $m > 5) {
232 0         0 $self->{_30w} = 1;
233             } elsif ($y == 2008 and $m == 5 and $d >= 27) {
234 4         13 $self->{_30w} = 1;
235             } else {
236 5         17 $self->{_30w} = 0;
237             }
238             }
239             } else {
240 3         7 $self->{_30w} = 0;
241             }
242             }
243              
244 20         87 return $self->{_30w};
245             }
246              
247             =head2 debug
248              
249             Enable or disable diagnostic logging
250             =cut
251              
252             sub debug {
253 106     106 1 126 my $self = shift;
254 106         131 my $debug = shift;
255              
256 106 100       253 if (defined $debug) {
257 3 50       10 $self->{_debug} = $debug ? 1 : 0;
258 3 50       12 $self->_log("Debug", $self->{_debug} ? "enabled" : "disabled");
259             }
260              
261 106         514 return $self->{_debug};
262             }
263              
264             # private methods
265             # _update - given all the information in the object, calculate the day's
266             # offsets
267             sub _update {
268 13     13   29 my $self = shift;
269              
270 13         43 my $djia = $self->djia;
271 13 100       57 unless (defined $djia) {
272 4         13 $self->_log("Failed to get DJIA");
273 4         14 $self->{_dlat} = $self->{_dlon} = undef;
274 4         12 return undef;
275             }
276              
277 9         40 $self->_log("DJIA for", $self->date, "is $djia");
278              
279 9         35 my $md5 = md5_hex($self->date . "-" . $djia);
280 9         29 $self->_log(" - md5(". $self->date ."-$djia)");
281 9         47 $self->_log(" - md5 = $md5");
282              
283 9         50 my ($md5lat, $md5lon) = (substr($md5, 0, 16), substr($md5, 16, 16));
284 9         40 $self->_log(" - = $md5lat, $md5lon");
285 9         44 ($self->{_dlat}, $self->{_dlon}) = (0, 0);
286              
287 9         47 while (length $md5lat) {
288 144         414 $self->{_dlat} += hex substr($md5lat, -1, 1, "");
289 144         196 $self->{_dlon} += hex substr($md5lon, -1, 1, "");
290 144         165 $self->{_dlat} /= 16;
291 144         348 $self->{_dlon} /= 16;
292             }
293              
294 9         152 $self->_log(" dlat, dlon => $self->{_dlat}, $self->{_dlon}");
295             }
296              
297             # _log - print out a timestampped log entry
298             sub _log {
299 103     103   158 my $self = shift;
300              
301 103 100       215 return unless $self->debug;
302              
303 100         11399 print scalar localtime, " - @_\n";
304             }
305              
306             # _get_djia - call get_djia on from the current source
307             sub _get_djia {
308 17     17   28 my $self = shift;
309              
310 17         42 $self->_log("getting DJIA from", $self->source);
311 17         49 return $self->source->get_djia(@_);
312             }
313              
314             =head1 SEE ALSO
315              
316             Original comic: http://www.xkcd.com/426/
317              
318             Wiki: http://wiki.xkcd.com/geohashing/Main_Page
319              
320             IRC: irc://irc.xkcd.com/geohashing
321              
322             =head1 AUTHOR
323              
324             Dan Boger, Ezigdon@gmail.comE
325              
326             =head1 COPYRIGHT AND LICENSE
327              
328             Copyright (C) 2008 by Dan Boger
329              
330             This library is free software; you can redistribute it and/or modify
331             it under the same terms as Perl itself, either Perl version 5.10.0 or,
332             at your option, any later version of Perl 5 you may have available.
333              
334              
335             =cut
336              
337             1;
338