File Coverage

blib/lib/Geo/Postcodes.pm
Criterion Covered Total %
statement 108 299 36.1
branch 50 234 21.3
condition 19 39 48.7
subroutine 21 39 53.8
pod 31 31 100.0
total 229 642 35.6


line stmt bran cond sub pod time code
1             package Geo::Postcodes;
2              
3             #################################################################################
4             # #
5             # This file is written by Arne Sommer - perl@bbop.org #
6             # #
7             #################################################################################
8              
9 3     3   79176 use strict;
  3         9  
  3         139  
10 3     3   18 use warnings;
  3         6  
  3         14797  
11              
12             our $VERSION = '0.32';
13              
14             ## Which methods are available ##################################################
15              
16             my @valid_fields = qw(postcode location borough county type type_verbose owner
17             address); # Used by the 'get_fields' procedure.
18              
19             my %valid_fields;
20              
21             foreach (@valid_fields)
22             {
23             $valid_fields{$_} = 1; # Used by 'is_field' for easy lookup.
24             }
25              
26             ## Type Description #############################################################
27              
28             my %typedesc;
29              
30             $typedesc{BX} = "Post Office box";
31             $typedesc{ST} = "Street address";
32             $typedesc{SX} = "Service box";
33             $typedesc{IO} = "Individual owner";
34             $typedesc{STBX} = "Street Address and Post Office box";
35             $typedesc{MU} = "Multiple usage";
36             $typedesc{PP} = "Porto Paye receiver";
37              
38             $typedesc{PN} = "Place name";
39              
40             ## OO Methods ###################################################################
41              
42             our %postcode_of;
43             our %location_of;
44             our %borough_of;
45             our %county_of;
46             our %type_of;
47             our %owner_of;
48             our %address_of;
49              
50             sub new
51             {
52 0     0 1 0 my $class = shift;
53 0         0 my $postcode = shift;
54 0         0 my $self = shift; # Allow for subclassing.
55              
56 0 0       0 return unless valid($postcode);
57              
58 0 0       0 unless ($self)
59             {
60 0         0 $self = bless \(my $dummy), $class;
61             }
62              
63 0         0 $postcode_of {$self} = $postcode;
64 0         0 $location_of {$self} = location_of ($postcode);
65 0         0 $borough_of {$self} = borough_of ($postcode);
66 0         0 $county_of {$self} = county_of ($postcode);
67 0         0 $type_of {$self} = type_of ($postcode);
68 0         0 $owner_of {$self} = owner_of ($postcode);
69 0         0 $address_of {$self} = address_of ($postcode);
70 0         0 return $self;
71             }
72              
73             sub DESTROY
74             {
75 0     0   0 my $object_id = $_[0];
76              
77 0         0 delete $postcode_of {$object_id};
78 0         0 delete $location_of {$object_id};
79 0         0 delete $borough_of {$object_id};
80 0         0 delete $county_of {$object_id};
81 0         0 delete $type_of {$object_id};
82 0         0 delete $owner_of {$object_id};
83 0         0 delete $address_of {$object_id};
84             }
85              
86             sub postcode
87             {
88 0     0 1 0 my $self = shift;
89 0 0       0 return unless defined $self;
90 0 0       0 return $postcode_of{$self} if exists $postcode_of{$self};
91 0         0 return;
92             }
93              
94             sub location
95             {
96 0     0 1 0 my $self = shift;
97 0 0       0 return unless defined $self;
98 0 0       0 return $location_of{$self} if exists $location_of{$self};
99 0         0 return;
100             }
101              
102             sub borough
103             {
104 0     0 1 0 my $self = shift;
105 0 0       0 return unless defined $self;
106 0 0       0 return $borough_of{$self} if exists $borough_of{$self};
107 0         0 return;
108             }
109              
110             sub county
111             {
112 0     0 1 0 my $self = shift;
113 0 0       0 return unless defined $self;
114 0 0       0 return $county_of{$self} if exists $county_of{$self};
115 0         0 return;
116             }
117              
118             sub type
119             {
120 0     0 1 0 my $self = shift;
121 0 0       0 return unless defined $self;
122 0 0       0 return $type_of{$self} if exists $type_of{$self};
123 0         0 return;
124             }
125              
126             sub type_verbose
127             {
128 0     0 1 0 my $self = shift;
129 0 0       0 return unless defined $self;
130 0 0       0 return unless exists $type_of{$self};
131 0 0       0 return unless exists $typedesc{$type_of{$self}};
132 0         0 return $typedesc{$type_of{$self}};
133             }
134              
135             sub owner
136             {
137 0     0 1 0 my $self = shift;
138 0 0       0 return unless defined $self;
139 0 0       0 return $owner_of{$self} if exists $owner_of{$self};
140 0         0 return;
141             }
142              
143             sub address
144             {
145 0     0 1 0 my $self = shift;
146 0 0       0 return unless defined $self;
147 0 0       0 return $address_of{$self} if exists $address_of{$self};
148 0         0 return;
149             }
150              
151             #################################################################################
152              
153             sub get_postcodes ## Return all the postcodes, unsorted.
154             {
155 1     1 1 2 return;
156             }
157              
158             sub get_fields ## Get a list of legal fields for the class/object.
159             {
160 1     1 1 1554 return @valid_fields;
161             }
162              
163             sub is_field ## Is the specified field legal? Can be called as
164             { ## a procedure, or as a method.
165 46     46 1 74 my $field = shift;
166 46 100       152 $field = shift if $field =~ /Geo::Postcodes/; # Called on an object.
167              
168 46 100       157 return 1 if $valid_fields{$field};
169 11         33 return 0;
170             }
171              
172             ## Global Procedures - Stub Version, Override in your subclass #################
173              
174             sub legal # Is it a legal code, i.e. something that follows the syntax rule.
175             {
176 4     4 1 26 return 0;
177             }
178              
179             sub valid # Is the code in actual use.
180             {
181 4     4 1 16 return 0;
182             }
183              
184             sub postcode_of
185             {
186 0     0 1 0 return;
187             }
188              
189             sub location_of
190             {
191 2     2 1 9 return;
192             }
193              
194             sub borough_of
195             {
196 2     2 1 9 return;
197             }
198              
199             sub county_of
200             {
201 2     2 1 8 return;
202             }
203              
204             sub type_of
205             {
206 2     2 1 6 return;
207             }
208              
209             sub type_verbose_of
210             {
211 0     0 1 0 return;
212             }
213              
214             sub owner_of
215             {
216 2     2 1 9 return;
217             }
218              
219             sub address_of
220             {
221 2     2 1 11 return;
222             }
223              
224             sub get_types
225             {
226 0     0 1 0 return keys %typedesc;
227             }
228              
229             sub type2verbose
230             {
231 0     0 1 0 my $type = shift;
232 0 0       0 return unless $type;
233 0 0       0 return unless exists $typedesc{$type};
234 0         0 return $typedesc{$type};
235             }
236              
237             my %legal_mode;
238             $legal_mode{'and'} = $legal_mode{'and not'} = 1;
239             $legal_mode{'nand'} = $legal_mode{'nand not'} = 1;
240             $legal_mode{'nor'} = $legal_mode{'nor not'} = 1;
241             $legal_mode{'or'} = $legal_mode{'or not'} = 1;
242             $legal_mode{'xnor'} = $legal_mode{'xnor not'} = 1;
243             $legal_mode{'xor'} = $legal_mode{'xor not'} = 1;
244              
245             my %legal_initial_mode;
246             $legal_initial_mode{'all'} = $legal_initial_mode{'none'} = 1;
247             $legal_initial_mode{'not'} = $legal_initial_mode{'one'} = 1;
248              
249             sub is_legal_selectionmode
250             {
251 12     12 1 15 my $mode = shift;
252 12 100       271 return 1 if $legal_mode{$mode};
253 6         21 return 0;
254             }
255              
256             sub is_legal_initial_selectionmode
257             {
258 60     60 1 74 my $mode = shift;
259 60 100 100     359 return 1 if $legal_initial_mode{$mode} or $legal_mode{$mode};
260 12         31 return 0;
261             }
262              
263             sub get_selectionmodes
264             {
265 0     0 1 0 return sort keys %legal_mode;
266             }
267              
268             sub get_initial_selectionmodes
269             {
270 0     0 1 0 return sort (keys %legal_mode, keys %legal_initial_mode);
271             }
272              
273             sub verify_selectionlist
274             {
275 53     53 1 28922 return Geo::Postcodes::_verify_selectionlist('Geo::Postcodes', @_);
276             # Black magic.
277             }
278              
279             sub _verify_selectionlist
280             {
281 53     53   74 my $caller_class = shift;
282 53         96 my @args = @_; # A list of selection arguments to verify
283              
284 53         124 my $status = 1; # Return value
285 53         69 my @out = ();
286 53         59 my @verbose = ();
287              
288 53 50       110 return (0, "No arguments") unless @args;
289              
290 53 100       103 if (is_legal_initial_selectionmode($args[0]))
291             {
292 41         49 my $mode = shift @args;
293              
294 41 100 100     182 if (@args and $args[0] eq "not" and is_legal_initial_selectionmode("$mode $args[0]"))
      66        
295             {
296 5         21 $mode = "$mode $args[0]";
297 5         7 shift @args;
298             }
299              
300 41         56 push @out, $mode;
301 41         73 push @verbose, "Mode: '$mode' - ok";
302              
303 41 100 100     168 return (1, @out) if $mode eq "all" or $mode eq "none";
304 37 100 100     91 return (1, @out) if $mode eq "one" and @args == 0;
305             # This one can both be used alone, or followed by more.
306              
307 36 100       122 return (0, @verbose, "Missing method/value pair - not ok") unless @args >= 2;
308             # Missing method/value pair.
309             }
310              
311             ## Done with the first one
312              
313 34         72 while (@args)
314             {
315 45         58 my $argument = shift(@args);
316              
317 45 100       136 if ($caller_class->is_field($argument))
    100          
    100          
318             {
319 34         40 push @out, $argument;
320 34         66 push @verbose, "Field: '$argument' - ok";
321              
322 34 50       61 if (@args)
323             {
324 34         42 $argument = shift(@args);
325 34         41 push @out, $argument;
326 34         115 push @verbose, "String: '$argument' - ok";
327             }
328             else
329             {
330 0         0 push @verbose, "Missing string - not ok"; # The last element was a method.
331 0         0 $status = 0;
332 0         0 @args = (); # Terminate the loop
333             }
334             }
335             elsif (is_legal_selectionmode($argument))
336             {
337 5 100 66     39 if (@args and $args[0] eq "not" and is_legal_selectionmode("$argument $args[0]"))
      66        
338             {
339 1         2 $argument = "$argument $args[0]";
340 1         2 shift @args;
341             }
342 5         10 push @out, $argument;
343 5         13 push @verbose, "Mode: '$argument' - ok";
344              
345 5 50       45 unless (@args >= 2) # Missing method/value pair
346             {
347 0         0 push @verbose, "Missing method/value pair - not ok";
348 0         0 $status = 0;
349 0         0 @args = (); # Terminate the loop
350             }
351             }
352             elsif ($argument eq 'procedure')
353             {
354 4         8 push @out, $argument;
355 4         6 push @verbose, "Field: 'procedure' - ok";
356              
357 4         7 my $procedure = shift(@args);
358 4 50       10 if (ref $procedure eq "CODE")
359             {
360 4 100       19 if (_valid_procedure_pointer($procedure))
361             {
362 2         4 push @out, $procedure;
363 2         12 push @verbose, "Procedure pointer: '$procedure' - ok";
364             }
365             else
366             {
367 2         7 push @verbose, "No such procedure: '$procedure' - not ok";
368 2         4 $status = 0;
369 2         7 @args = (); # Terminate the loop
370             }
371             }
372             else
373             {
374 0         0 push @verbose, "Not a procedure pointer: '$procedure' - not ok";
375 0         0 $status = 0;
376 0         0 @args = (); # Terminate the loop
377             }
378             }
379             else
380             {
381 2         7 push @verbose, "Illegal argument: '$argument' - not ok";
382 2         3 $status = 0;
383 2         8 @args = (); # Terminate the loop
384             }
385             }
386              
387 34 100       152 return (1, @out) if $status; # Return a modified argument list on success.
388              
389 4         14 return (0, @verbose); # Return a list of diagnostic meddages on failure.
390             }
391              
392             sub selection_loop
393             {
394 0     0 1 0 return Geo::Postcodes::_selection_loop('Geo::Postcodes', @_);
395             # Black magic.
396             }
397              
398             sub _selection_loop
399             {
400 0     0   0 my $caller_class = shift;
401              
402 0         0 my $objects_requested = 0; # Not object oriented.
403              
404 0 0       0 if ($_[0] eq $caller_class)
405             {
406 0         0 $objects_requested = 1;
407 0         0 shift;
408             }
409              
410 0         0 my $procedure_pointer = shift;
411              
412 0 0       0 return 0 unless $procedure_pointer;
413              
414 0         0 my @selection_clauses = @_;
415 0         0 my @postcodes = _selection($caller_class, @selection_clauses);
416              
417 0 0       0 return 0 unless @postcodes;
418              
419 0         0 foreach (@postcodes)
420             {
421 0 0       0 &$procedure_pointer($objects_requested ? $caller_class->new($_) : $_);
422             }
423 0         0 return 1;
424             }
425              
426              
427             #################################################################################
428             # #
429             # Returns a list of postcodes if called as a procedure; #
430             # Geo::Postcodes::XX::selection(...) #
431             # Returns a list of objects if called as a method; #
432             # Geo::Postcodes::XX->selection(...) #
433             # #
434             # Note that 'or' and 'not' are not written efficient, as they recompile the #
435             # regular expression(s) for every postcode. #
436             # #
437             #################################################################################
438              
439             sub selection
440             {
441 1     1 1 6 return Geo::Postcodes::_selection('Geo::Postcodes', @_);
442             # Black magic.
443             }
444              
445             sub _selection
446             {
447 1     1   3 my $caller_class = shift;
448              
449 1         3 my $objects_requested = 0; # Not object oriented.
450              
451 1 50       5 if ($_[0] eq $caller_class)
452             {
453 0         0 $objects_requested = 1;
454 0         0 shift;
455             }
456              
457 1 50       22 if ($_[0] eq 'all')
    50          
458             {
459 0         0 my @all = sort &{&_proc_pointer($caller_class . '::get_postcodes')}();
  0         0  
460             # Get all the postcodes.
461              
462 0 0       0 return @all unless $objects_requested;
463              
464 0         0 my @out_objects;
465              
466 0         0 foreach my $postcode (@all)
467             {
468 0         0 push(@out_objects, $caller_class->new($postcode));
469             }
470              
471 0         0 return @out_objects;
472             }
473              
474             elsif ($_[0] eq 'none')
475             {
476 0         0 return; # Absolutely nothing.
477             }
478              
479 1         3 my $limit = 0; # Set to one if we have requested only one postcode.
480 1 50       4 if ($_[0] eq "one")
481             {
482 0         0 $limit = 1;
483 0         0 shift; # Get rid of the mode.
484             }
485              
486 1         1 my $mode = "and";
487             # The mode defaults to 'and' unless specified.
488              
489 1         4 my %out = ();
490              
491             ## The first set of method/value ##############################################
492              
493 1         1 my @all = &{&_proc_pointer($caller_class . '::get_postcodes')}();
  1         6  
494             # Get all the postcodes.
495              
496 1         65 my($field, $current_field, $value, $current_value);
497              
498 1 50       4 if (@_) # As 'one' can be without additional arguments.
    0          
499             {
500 1 50       4 if (is_legal_initial_selectionmode($_[0]))
501             {
502 1 50 33     5 if ($_[1] eq "not" and is_legal_initial_selectionmode("$_[0] $_[1]"))
503             {
504 0         0 $mode = shift; $mode .= " "; $mode .= shift;
  0         0  
  0         0  
505             }
506             else
507             {
508 1 50       4 $mode = shift if is_legal_initial_selectionmode($_[0]);
509             }
510             }
511              
512 1         2 $field = shift;
513              
514 1 50       5 if ($field eq 'procedure')
515             {
516 0         0 my $procedure = shift;
517 0 0       0 return unless _valid_procedure_pointer($procedure);
518              
519 0         0 my $match;
520              
521 0         0 foreach my $postcode (@all)
522             {
523 0         0 eval { $match = $procedure->($_); };
  0         0  
524 0 0       0 return if $@; # Return if the procedure was uncallable.
525              
526 0 0       0 if ($mode =~ /not/) { $out{$postcode}++ unless $match; }
  0 0       0  
527 0 0       0 else { $out{$postcode}++ if $match; }
528             }
529             }
530             else
531             {
532 1 50       1 return unless &{&_proc_pointer($caller_class . '::is_field')}($field);
  1         5  
533             # Return if the specified method is undefined for the class.
534             # As and 'and' with a list with one undefined item gives an empty list.
535              
536 1         6 my $current_field = &_proc_pointer($caller_class . '::' . $field .'_of');
537              
538 1         2 $value = shift; $value =~ s/%/\.\*/g;
  1         4  
539 1 50       3 return unless $value;
540             # A validity check is impossible, so this is the next best thing.
541              
542 1         4 foreach my $postcode (@all)
543             {
544 0         0 $current_value = $current_field->($postcode);
545             # Call the procedure with the current postcode as argument
546              
547 0 0       0 next unless $current_value;
548             # Skip postcodes without this field.
549              
550 0         0 my $match = $current_value =~ m{^$value$}i; ## Case insensitive
551              
552 0 0       0 if ($mode =~ /not/) { $out{$postcode}++ unless $match; }
  0 0       0  
553 0 0       0 else { $out{$postcode}++ if $match; }
554             }
555             }
556              
557 1 50       6 $mode = 'and' if $mode eq 'not';
558             }
559              
560             elsif ($limit) # just one argument; 'one'.
561             {
562 0         0 map { $out{$_} = 1 } @all
  0         0  
563             }
564              
565 1         4 while (@_)
566             {
567 0 0       0 if (is_legal_selectionmode($_[0]))
568             {
569 0 0 0     0 if ($_[1] eq "not" and is_legal_selectionmode("$_[0] $_[1]"))
570             {
571 0         0 $mode = shift; $mode .= " "; $mode .= shift;
  0         0  
  0         0  
572             }
573             else
574             {
575 0 0       0 $mode = shift if is_legal_selectionmode($_[0]);
576             }
577             }
578              
579             # Use the one already on hand, if none is given.
580              
581 0         0 my $is_procedure = 0;
582 0         0 my $procedure;
583              
584 0         0 $field = shift;
585              
586 0 0       0 if ($field eq 'procedure')
587             {
588 0         0 $is_procedure = 1;
589 0         0 $procedure = shift;
590 0 0       0 return unless _valid_procedure_pointer($procedure);
591             }
592             else
593             {
594 0 0       0 return unless &{&_proc_pointer($caller_class . '::is_field')}($field);
  0         0  
595             # Return if the specified method is undefined for the class.
596             # As an 'and' with a list with one undefined item gives an empty list.
597              
598 0         0 $current_field = &_proc_pointer($caller_class . '::' . $field .'_of');
599              
600 0         0 $value = shift;
601 0         0 $value =~ s/%/\.\*/g;
602 0 0       0 return unless $value;
603             # A validity check is impossible, so this is the next best thing.
604             }
605              
606 0 0       0 foreach my $postcode ($mode =~ /and/ ? (keys %out) : @all)
607             {
608             # We start with the result from the previous iteration if the mode
609             # is one of the 'and'-family. Otherwise it is one of the 'or'-family,
610             # and we have to start from scratch (@all).
611              
612 0         0 my $match;
613              
614 0 0       0 if ($procedure)
615             {
616 0         0 eval { $match = $procedure->($postcode); };
  0         0  
617 0 0       0 return if $@; # Return if the procedure was uncallable.
618             }
619             else
620             {
621 0         0 $current_value = $current_field->($postcode);
622             # Call the procedure with the current postcode as argument
623              
624 0 0       0 next unless $current_value;
625             # Skip postcodes without this field.
626              
627 0         0 $match = $current_value =~ m{^$value$}i; ## Case insensitive
628             }
629              
630 0 0       0 if ($mode eq "and")
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
631             {
632 0 0       0 delete $out{$postcode} unless $match;
633             }
634             elsif ($mode eq "and not")
635             {
636 0 0       0 delete $out{$postcode} if $match;
637             }
638              
639             elsif ($mode eq "nand")
640             {
641 0 0 0     0 if ($match and $out{$postcode}) { delete $out{$postcode} if $out{$postcode}; }
  0 0       0  
642 0         0 else { $out{$postcode}++; }
643             }
644             elsif ($mode eq "nand not")
645             {
646 0 0 0     0 if (!$match and $out{$postcode}) { delete $out{$postcode} if $out{$postcode}; }
  0 0       0  
647 0         0 else { $out{$postcode}++; }
648             }
649              
650             elsif ($mode eq "or")
651             {
652 0 0       0 $out{$postcode}++ if $match;
653             }
654             elsif ($mode eq "or not")
655             {
656 0 0       0 $out{$postcode}++ unless $match;
657             }
658             elsif ($mode eq "nor")
659             {
660 0 0 0     0 if (!$match and !$out{$postcode}) { $out{$postcode}++; }
  0         0  
661 0 0       0 else { delete $out{$postcode} if $out{$postcode}; }
662             }
663             elsif ($mode eq "nor not")
664             {
665 0 0 0     0 if ($match and !$out{$postcode}) { $out{$postcode}++; }
  0         0  
666 0 0       0 else { delete $out{$postcode} if $out{$postcode}; }
667             }
668             elsif ($mode eq "xor")
669             {
670 0 0       0 if ($match)
671             {
672 0 0       0 if ($out{$postcode}) { delete $out{$postcode}; }
  0         0  
673 0         0 else { $out{$postcode}++; }
674             }
675             }
676             elsif ($mode eq "xor not")
677             {
678 0 0       0 unless ($match)
679             {
680 0 0       0 if ($out{$postcode}) { delete $out{$postcode}; }
  0         0  
681 0         0 else { $out{$postcode}++; }
682             }
683             }
684              
685             elsif ($mode eq "xnor")
686             {
687 0 0       0 my $boolean = $out{$postcode} ? 1 : 0;
688 0 0       0 if ($match == $boolean)
689             {
690 0         0 $out{$postcode}++;
691             }
692             else
693             {
694 0 0       0 delete $out{$postcode} if $out{$postcode};
695             }
696             }
697             elsif ($mode eq "xnor not")
698             {
699 0 0       0 my $boolean = $out{$postcode} ? 1 : 0;
700 0 0       0 if ($match != $boolean)
701             {
702 0         0 $out{$postcode}++;
703             }
704             else
705             {
706 0 0       0 delete $out{$postcode} if $out{$postcode};
707             }
708             }
709             }
710             }
711              
712             ###############################################################################
713              
714 1 50       8 return unless %out;
715             # Return nothing if we have an empty list (or rather, hash).
716              
717 0         0 my @out;
718              
719 0 0       0 if ($limit) # The caller has requested just one postcode, #
720             { # and will get exactly that if any matches #
721 0         0 my @list = keys %out; # were found. The returned postcode is chosen #
722 0         0 @out = $list[rand(@list)]; # by random. #
723             }
724             else
725             {
726 0         0 @out = sort keys %out;
727             # This will give an ordered list, as opposed to a semi random order. This #
728             # is essential when comparing lists of postcodes, as the test scripts do. #
729             }
730              
731             ###############################################################################
732              
733 0 0       0 return @out unless $objects_requested;
734              
735 0         0 my @out_objects;
736              
737 0         0 foreach my $postcode (@out)
738             {
739 0         0 push(@out_objects, $caller_class->new($postcode));
740             }
741              
742 0         0 return @out_objects;
743             }
744              
745              
746             sub _proc_pointer
747             {
748 3     3   4 my $procedure_name = shift;
749 3         4 return \&{$procedure_name};
  3         20  
750             }
751              
752             sub _valid_procedure_pointer
753             {
754 4     4   5 my $ptr = shift;
755 4 50       10 return 0 if ref $ptr ne "CODE";
756 4 100       15 return 1 if defined(&$ptr);
757 2         5 return 0;
758             }
759              
760             1;
761             __END__