File Coverage

blib/lib/Hash/Type.pm
Criterion Covered Total %
statement 119 121 98.3
branch 41 54 75.9
condition 4 6 66.6
subroutine 21 21 100.0
pod 6 6 100.0
total 191 208 91.8


line stmt bran cond sub pod time code
1             package Hash::Type;
2              
3 4     4   56708 use strict;
  4         7  
  4         99  
4 4     4   14 use warnings;
  4         3  
  4         98  
5 4     4   12 use Carp;
  4         8  
  4         274  
6 4     4   15 use Scalar::Util qw/blessed/;
  4         3  
  4         340  
7 4     4   22 use List::Util qw/max/;
  4         5  
  4         5054  
8              
9             our $VERSION = "2.00";
10              
11             our $reserved_keys_field = "\0HTkeys\0"; # special reserved hash entry
12              
13             #----------------------------------------------------------------------
14             # constructor
15             #----------------------------------------------------------------------
16             sub new { # this is a polymorphic 'new', creating either Hash::Type instances
17             # from this class, or tied hashes from one of those instance
18 29     29 1 3950 my $obj_or_class = shift;
19              
20 29 100       48 if (ref($obj_or_class)) {
21             # create a new tied hash from the Hash::Type instance
22 23         16 my %h;
23 23         49 tie %h, $obj_or_class , @_;
24 23         44 return \%h;
25             }
26             else {
27             # create a new Hash::Type instance
28 6         16 my $self = {$reserved_keys_field => []};
29 6         10 bless $self, $obj_or_class;
30 6         21 $self->add(@_); # add indices for fields given in @_
31 6         12 return $self;
32             }
33             }
34              
35              
36              
37             #----------------------------------------------------------------------
38             # tied hash implementation
39             #----------------------------------------------------------------------
40 27     27   628 sub TIEHASH { bless [@_], __PACKAGE__ }
41 8 100   8   1279 sub STORE { my $index = $_[0]->[0]{$_[1]} or
42             croak "can't STORE, key '$_[1]' was never added "
43             . "to this Hash::Type";
44 6         11 $_[0]->[$index] = $_[2]; }
45              
46             # FETCH : must be an lvalue because it may be used in $h{field} =~ s/.../../;
47             # And since lvalues cannot use "return" (cf. L), we
48             # must write it with nested ternary ifs -- not nice to read :-(
49             sub FETCH : lvalue {
50 304     304   2536 my $index = $_[0]->[0]{$_[1]};
51 304 50       632 $index ? $_[0]->[$index]
    100          
52             : $_[1] eq 'Hash::Type' ? $_[0]->[0]
53             : undef;
54             }
55              
56 14     14   1550 sub FIRSTKEY { $_[0]->[0]{$reserved_keys_field}[0]; }
57 65     65   166 sub NEXTKEY { my ($h, $last_key) = @_;
58 65         46 my $index_last = $h->[0]{$last_key}; # index on base 1..
59 65         117 $h->[0]{$reserved_keys_field}[$index_last]; # .. used on base 0!
60             }
61 8     8   637 sub EXISTS { exists $_[0]->[0]{$_[1]} }
62 1     1   328 sub DELETE { croak "DELETE is forbidden on hash tied to " . __PACKAGE__ }
63 1     1   381 sub CLEAR { delete @{$_[0]}[1 .. $#{$_[0]}] }
  1         5  
  1         3  
64              
65             #----------------------------------------------------------------------
66             # Object-oriented methods for dealing with names and values
67             #----------------------------------------------------------------------
68             sub add {
69 7     7 1 284 my $self = shift;
70 7         6 my $max = @{$self->{$reserved_keys_field}};
  7         24  
71 7         7 my $ix = $max;
72             NAME:
73 7         14 foreach my $name (@_) {
74 30 100       45 next NAME if exists $self->{$name};
75 28         27 $self->{$name} = ++$ix;
76 28         20 push @{$self->{$reserved_keys_field}}, $name;
  28         43  
77             }
78              
79             # return the number of added names
80 7         15 return $ix - $max;
81             }
82              
83             sub names {
84 1     1 1 2 my ($self) = @_;
85 1         2 return @{$self->{$reserved_keys_field}};
  1         7  
86             }
87              
88             sub values {
89 1     1 1 400 my ($self, $tied_hash) = @_;
90 1         3 my $tied = tied %$tied_hash;
91 1         1 return @{$tied}[1 .. @{$self->{$reserved_keys_field}}];
  1         6  
  1         5  
92             }
93              
94             sub each {
95 1     1 1 373 my ($self, $tied_hash) = @_;
96 1         2 my $tied = tied %$tied_hash;
97 1         1 my $index = 0;
98 1         1 my $max = @{$self->{$reserved_keys_field}};
  1         2  
99             return sub {
100 6     6   16 $index += 1;
101 6 100       14 return $index <= $max ? ($self->{$reserved_keys_field}[$index-1],
102             $tied->[$index])
103             : ();
104 1         4 };
105             }
106              
107             #----------------------------------------------------------------------
108             # compiling comparison functions
109             #----------------------------------------------------------------------
110             sub cmp {
111 11     11 1 4079 my $self = shift;
112              
113 11 50       24 @_ or croak "cmp : no cmp args";
114              
115 11 100       26 if (@_ == 1) {
116             # parse first syntax, where all comparison fiels are in one string
117 10         30 my @fields = split /,/, shift @_;
118 10         23 foreach (@fields) {
119 18 50       85 m[^\s*(\S.*?)\s*(?::([^:]+))?$] or croak "bad cmp op : $_";
120 18         52 push @_, $1, $2; # feed back to @_ as arguments to second syntax
121             }
122             }
123              
124             # parse second syntax (pairs of field_name => comparison_instruction)
125              
126             # $a and $b are different in each package, so we must refer to the caller's
127 11         17 my $caller = caller;
128 11         28 my ($a, $b) = ("\$${caller}::a", "\$${caller}::b");
129              
130 11         7 my @cmp; # holds code for each comparison to perform
131             my @caller_sub; # references to comparison subs given by caller
132             # (must copy them from @_ into a lexical variable
133             # in order to build a proper closure)
134 0         0 my $regex; # used only for date comparisons, see below
135              
136 11         25 for (my $i = 0; $i < @_; $i += 2) {
137 21 50       44 my $ix = $self->{$_[$i]} or croak "can't do cmp on absent field : $_[$i]";
138              
139 21 100       44 if (ref $_[$i+1] eq 'CODE') { # ref. to cmp function supplied by caller
140 1         2 push @caller_sub, $_[$i+1];
141 1         7 push @cmp, "do {local ($a, $b) = (tied(%$a)->[$ix], tied(%$b)->[$ix]);".
142             "&{\$caller_sub[$#caller_sub]}}";
143             }
144             else { # builtin comparison operator
145 20         24 my ($sign, $op) = ("", "cmp");
146 20         15 my $str;
147 20 100       31 if (defined $_[$i+1]) {
148 15         60 ($sign, $op) = ($_[$i+1] =~ /^\s*([-+]?)\s*(.+)/);
149             }
150              
151 20         30 for ($op) {
152 20 100       51 /^(alpha|cmp)\s*$/ and do {$str = "%s cmp %s"; last};
  7         7  
  7         7  
153 13 100       35 /^(num|<=>)\s*$/ and do {$str = "%s <=> %s"; last};
  9         11  
  9         8  
154 4 100       26 /^d(\W+)m(\W+)y\s*$/ and do {$regex=qr{(\d+)\Q$1\E(\d+)\Q$2\E(\d+)};
  2         21  
155 2         4 $str = "_date_cmp(\$regex, 0, 1, 2, %s, %s)";
156 2         3 last};
157 2 100       8 /^m(\W+)d(\W+)y\s*$/ and do {$regex=qr{(\d+)\Q$1\E(\d+)\Q$2\E(\d+)};
  1         15  
158 1         1 $str = "_date_cmp(\$regex, 1, 0, 2, %s, %s)";
159 1         2 last};
160 1 50       4 /^y(\W+)m(\W+)d\s*$/ and do {$regex=qr{(\d+)\Q$1\E(\d+)\Q$2\E(\d+)};
  1         16  
161 1         2 $str = "_date_cmp(\$regex, 2, 1, 0, %s, %s)";
162 1         1 last};
163 0         0 croak "bad operator for Hash::Type::cmp : $_[$i+1]";
164             }
165 20         99 $str = sprintf("$sign($str)", "tied(%$a)->[$ix]", "tied(%$b)->[$ix]");
166 20         44 push @cmp, $str;
167             }
168             }
169              
170 11         13 local $@;
171 11 50       1013 my $sub = eval "sub {" . join(" || ", @cmp) . "}"
172             or croak $@;
173 11         37 return $sub;
174             }
175              
176              
177             sub _date_cmp {
178 55     55   59 my ($regex, $d, $m, $y, $date1, $date2) = @_;
179              
180 55 0 33     68 return 0 if not $date1 and not $date2;
181 55 50       54 return 1 if not $date1; # null date is treated as bigger than any other
182 55 50       53 return -1 if not $date2;
183              
184 55         43 for my $date ($date1, $date2) {
185 110         75 $date =~ s[<.*?>][]g; # remove any markup
186 110         110 $date =~ tr/{}[]()//d; # remove any {}[]() chars
187             };
188              
189 55 50       226 my @d1 = ($date1 =~ $regex) or croak "invalid date '$date1' for regex $regex";
190 55 50       183 my @d2 = ($date2 =~ $regex) or croak "invalid date '$date2' for regex $regex";
191              
192 55 50       120 $d1[$y] += ($d1[$y] < 33) ? 2000 : 1900 if $d1[$y] < 100;
    100          
193 55 100       90 $d2[$y] += ($d2[$y] < 33) ? 2000 : 1900 if $d2[$y] < 100;
    100          
194              
195 55   100     990 return ($d1[$y]<=>$d2[$y]) || ($d1[$m]<=>$d2[$m]) || ($d1[$d]<=>$d2[$d]);
196             }
197              
198              
199             1;
200              
201             __END__