File Coverage

blib/lib/Data/CompactReadonly/V0/Dictionary.pm
Criterion Covered Total %
statement 109 109 100.0
branch 41 42 97.6
condition 13 15 86.6
subroutine 16 16 100.0
pod 0 3 0.0
total 179 185 96.7


line stmt bran cond sub pod time code
1             package Data::CompactReadonly::V0::Dictionary;
2             our $VERSION = '0.1.0';
3              
4 5     5   44 use warnings;
  5         11  
  5         171  
5 5     5   28 use strict;
  5         10  
  5         122  
6 5     5   26 use base qw(Data::CompactReadonly::V0::Collection Data::CompactReadonly::Dictionary);
  5         10  
  5         2942  
7              
8 5     5   2496 use Data::CompactReadonly::V0::TiedDictionary;
  5         19  
  5         182  
9 5     5   35 use Scalar::Util qw(blessed);
  5         9  
  5         341  
10 5     5   39 use Devel::StackTrace;
  5         13  
  5         7111  
11              
12             sub _init {
13 53     53   189 my($class, %args) = @_;
14 53         153 my($root, $offset) = @args{qw(root offset)};
15              
16 53 100       171 my $object = bless({
17             root => $root,
18             offset => $offset,
19             cache => ($root->_fast_collections() ? {} : undef),
20             }, $class);
21              
22 53 100       151 if($root->_tied()) {
23 27         135 tie my %dict, 'Data::CompactReadonly::V0::TiedDictionary', $object;
24 27         183 return \%dict;
25             } else {
26 26         206 return $object;
27             }
28             }
29              
30             # write a Dictionary to the file at the current offset
31             sub _create {
32 19     19   104 my($class, %args) = @_;
33 19         61 my $fh = $args{fh};
34 19         143 $class->_stash_already_seen(%args);
35 19         1510377 (my $scalar_type = $class) =~ s/Dictionary/Scalar/;
36              
37             # node header
38             print $fh $class->_type_byte_from_class().
39 19         158 $scalar_type->_get_bytes_from_word(scalar(keys %{$args{data}}));
  19         170  
40              
41             # empty pointer table
42 19         184 my $table_start_ptr = tell($fh);
43 19         118 print $fh "\x00" x $args{ptr_size} x 2 x scalar(keys %{$args{data}});
  19         1237  
44 19         212 $class->_set_next_free_ptr(%args);
45              
46 19         66 my @sorted_keys = sort keys %{$args{data}};
  19         371123  
47 19         21324 foreach my $index (0 .. $#sorted_keys) {
48 65626         277271 my $this_key = $sorted_keys[$index];
49 65626         180836 my $this_value = $args{data}->{$this_key};
50              
51             # write the pointer to the key, and the key if needed. Then write the
52             # pointer to the value, and the value if needed. The value can be any
53             # type. Keys are coerced Text to avoid floating point problems.
54 65626         297308 foreach my $item (
55             { data => $this_key, ptr_offset => 0, coerce_to_text => 1 },
56             { data => $this_value, ptr_offset => $args{ptr_size} }
57             ) {
58 131249         793154 $class->_seek(%args, pointer => $item->{ptr_offset} + $table_start_ptr + 2 * $index * $args{ptr_size});
59 131249 100       711230 if(my $ptr = $class->_get_already_seen(%args, data => $item->{data})) {
60 65574         843932 print $fh $class->_encode_ptr(%args, pointer => $ptr);
61             } else {
62 65675         270938 print $fh $class->_encode_ptr(%args, pointer => $class->_get_next_free_ptr(%args));
63 65675         508029 $class->_seek(%args, pointer => $class->_get_next_free_ptr(%args));
64              
65 65672         223496 my $node_class = 'Data::CompactReadonly::V0::Node';
66 65672 100       173594 if($item->{coerce_to_text}) {
67 65619         212315 $node_class = 'Data::CompactReadonly::V0::'.$class->_text_type_for_data($item->{data});
68 65619 100       521724 unless($node_class->VERSION()) {
69 2     2   1232 eval "use $node_class";
  2         8  
  2         68  
  2         155  
70 2 50       11 die($@) if($@);
71             }
72             }
73 65672         334148 $node_class->_create(%args, data => $item->{data});
74             }
75             }
76             }
77             }
78              
79             # Efficient binary search. Relies on elements' being ASCIIbetically sorted by key.
80             # 1 <= iterations to find key (or find that there is no key) <= ceil(log2(N))
81             # so no more than 4 iterations for a ten element list, no more than 20 for
82             # a million element list. Each iteration takes two seeks and two reads there
83             # are then two more seeks and reads to get the value
84             sub element {
85 111     111 0 10316 my($self, $element) = @_;
86              
87 111 100 100     621 die(
    100          
88             "$self: Invalid element: ".
89             (!defined($element) ? '[undef]' : $element).
90             " isn't Text or numeric\n"
91             ) unless(defined($element) && !ref($element));
92              
93             # first we need to find that key
94 108         393 my $max_candidate = $self->count() - 1;
95 108         230 my $min_candidate = 0;
96 108         351 my $cur_candidate = int($max_candidate / 2);
97 108         197 my $prev_candidate = -1;
98              
99 108         187 while(1) {
100 474         1186 my $key = $self->_nth_key($cur_candidate);
101 474         857 $prev_candidate = $cur_candidate;
102 474 100       1364 if($key eq $element) {
    100          
103 103         347 return $self->_nth_value($cur_candidate);
104             } elsif($key lt $element) { # our target is futher down the list
105 220         783 ($min_candidate, $cur_candidate, $max_candidate) = (
106             $cur_candidate + 1,
107             int(($cur_candidate + $max_candidate + 1) / 2),
108             $max_candidate
109             );
110             } else { # our target is further up the list
111 151         559 ($min_candidate, $cur_candidate, $max_candidate) = (
112             $min_candidate,
113             int(($min_candidate + $cur_candidate) / 2),
114             $cur_candidate - 1
115             );
116             }
117 371 100       995 last if($prev_candidate == $cur_candidate);
118             }
119 5         77 die("$self: Invalid element: $element: doesn't exist\n");
120             }
121              
122             sub exists {
123 16     16 0 469 my($self, $element) = @_;
124 16 100       44 return 0 if($self->count() == 0);
125 15         35 eval { $self->element($element) };
  15         42  
126 15 100       57 if($@ =~ /doesn't exist/) {
    100          
127 2         18 return 0;
128             } elsif($@) {
129 1         7 die($@);
130             } else {
131 12         60 return 1;
132             }
133             }
134              
135             sub _nth_key {
136 506     506   965 my($self, $n) = @_;
137 506 100 100     1246 if($self->{cache} && exists($self->{cache}->{keys}->{$n})) {
138 19         51 return $self->{cache}->{keys}->{$n}
139             }
140            
141 487         1210 $self->_seek($self->_nth_key_ptr_location($n));
142 487         2019 $self->_seek($self->_ptr_at_current_offset());
143              
144             # for performance, cache the filehandle in this object
145 487   33     2099 $self->{_fh} ||= $self->_fh();
146 487         1030 my $offset = tell($self->{_fh});
147 487         1452 my $key = $self->_node_at_current_offset();
148 486 100 100     2107 if(!defined($key) || ref($key)) {
149 2 100       22 die("$self: Invalid type: ".
150             (!defined($key) ? 'Null' : $key).
151             ": Dictionary keys must be Text at ".
152             sprintf("0x%08x", $offset).
153             "\n".
154             Devel::StackTrace->new()->as_string()
155             );
156             }
157 484 100       1138 if($self->{cache}) {
158 16         62 return $self->{cache}->{keys}->{$n} = $key;
159             }
160 468         1097 return $key;
161             }
162              
163             sub _nth_value {
164 103     103   225 my($self, $n) = @_;
165 103 100 100     312 if($self->{cache} && exists($self->{cache}->{values}->{$n})) {
166 1         5 return $self->{cache}->{values}->{$n}
167             }
168              
169 102         276 $self->_seek($self->_nth_key_ptr_location($n) + $self->_ptr_size());
170 102         417 $self->_seek($self->_ptr_at_current_offset());
171              
172 102         433 my $val = $self->_node_at_current_offset();
173              
174 102 100       358 if($self->{cache}) {
175 15         131 return $self->{cache}->{values}->{$n} = $val;
176             }
177 87         707 return $val;
178             }
179              
180             sub _nth_key_ptr_location {
181 589     589   1091 my($self, $n) = @_;
182 589         1577 return $self->_offset() + $self->_scalar_type_bytes() +
183             2 * $n * $self->_ptr_size();
184             }
185              
186             sub _ptr_at_current_offset {
187 589     589   1063 my $self = shift;
188 589         1404 return $self->_decode_ptr(
189             $self->_bytes_at_current_offset($self->_ptr_size())
190             );
191             }
192              
193             sub indices {
194 7     7 0 134 my $self = shift;
195 7 100       22 return [] if($self->count() == 0);
196 6         20 return [ map { $self->_nth_key($_) } (0 .. $self->count() - 1) ];
  17         43  
197             }
198              
199             1;