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.0.5';
3              
4 5     5   42 use warnings;
  5         13  
  5         166  
5 5     5   25 use strict;
  5         11  
  5         133  
6 5     5   23 use base qw(Data::CompactReadonly::V0::Collection Data::CompactReadonly::Dictionary);
  5         13  
  5         3089  
7              
8 5     5   2745 use Data::CompactReadonly::V0::TiedDictionary;
  5         16  
  5         188  
9 5     5   34 use Scalar::Util qw(blessed);
  5         9  
  5         319  
10 5     5   31 use Devel::StackTrace;
  5         9  
  5         6795  
11              
12             sub _init {
13 53     53   209 my($class, %args) = @_;
14 53         145 my($root, $offset) = @args{qw(root offset)};
15              
16 53 100       167 my $object = bless({
17             root => $root,
18             offset => $offset,
19             cache => ($root->_fast_collections() ? {} : undef),
20             }, $class);
21              
22 53 100       170 if($root->_tied()) {
23 27         119 tie my %dict, 'Data::CompactReadonly::V0::TiedDictionary', $object;
24 27         161 return \%dict;
25             } else {
26 26         199 return $object;
27             }
28             }
29              
30             # write a Dictionary to the file at the current offset
31             sub _create {
32 19     19   89 my($class, %args) = @_;
33 19         56 my $fh = $args{fh};
34 19         123 $class->_stash_already_seen(%args);
35 19         1406088 (my $scalar_type = $class) =~ s/Dictionary/Scalar/;
36              
37             # node header
38             print $fh $class->_type_byte_from_class().
39 19         159 $scalar_type->_get_bytes_from_word(scalar(keys %{$args{data}}));
  19         180  
40              
41             # empty pointer table
42 19         192 my $table_start_ptr = tell($fh);
43 19         109 print $fh "\x00" x $args{ptr_size} x 2 x scalar(keys %{$args{data}});
  19         1251  
44 19         178 $class->_set_next_free_ptr(%args);
45              
46 19         64 my @sorted_keys = sort keys %{$args{data}};
  19         342409  
47 19         22263 foreach my $index (0 .. $#sorted_keys) {
48 65617         243359 my $this_key = $sorted_keys[$index];
49 65617         158854 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 65617         278648 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 131231         813639 $class->_seek(%args, pointer => $item->{ptr_offset} + $table_start_ptr + 2 * $index * $args{ptr_size});
59 131231 100       683320 if(my $ptr = $class->_get_already_seen(%args, data => $item->{data})) {
60 65568         730455 print $fh $class->_encode_ptr(%args, pointer => $ptr);
61             } else {
62 65663         266416 print $fh $class->_encode_ptr(%args, pointer => $class->_get_next_free_ptr(%args));
63 65663         499336 $class->_seek(%args, pointer => $class->_get_next_free_ptr(%args));
64              
65 65660         224460 my $node_class = 'Data::CompactReadonly::V0::Node';
66 65660 100       164282 if($item->{coerce_to_text}) {
67 65610         219303 $node_class = 'Data::CompactReadonly::V0::'.$class->_text_type_for_data($item->{data});
68 65610 100       499290 unless($node_class->VERSION()) {
69 2     2   1017 eval "use $node_class";
  2         6  
  2         39  
  2         122  
70 2 50       11 die($@) if($@);
71             }
72             }
73 65660         318952 $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 108     108 0 10491 my($self, $element) = @_;
86              
87 108 100 100     546 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 105         333 my $max_candidate = $self->count() - 1;
95 105         196 my $min_candidate = 0;
96 105         287 my $cur_candidate = int($max_candidate / 2);
97 105         169 my $prev_candidate = -1;
98              
99 105         209 while(1) {
100 457         1107 my $key = $self->_nth_key($cur_candidate);
101 457         741 $prev_candidate = $cur_candidate;
102 457 100       1400 if($key eq $element) {
    100          
103 100         276 return $self->_nth_value($cur_candidate);
104             } elsif($key lt $element) { # our target is futher down the list
105 215         721 ($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 142         500 ($min_candidate, $cur_candidate, $max_candidate) = (
112             $min_candidate,
113             int(($min_candidate + $cur_candidate) / 2),
114             $cur_candidate - 1
115             );
116             }
117 357 100       825 last if($prev_candidate == $cur_candidate);
118             }
119 5         52 die("$self: Invalid element: $element: doesn't exist\n");
120             }
121              
122             sub exists {
123 16     16 0 476 my($self, $element) = @_;
124 16 100       49 return 0 if($self->count() == 0);
125 15         41 eval { $self->element($element) };
  15         43  
126 15 100       81 if($@ =~ /doesn't exist/) {
    100          
127 2         12 return 0;
128             } elsif($@) {
129 1         7 die($@);
130             } else {
131 12         48 return 1;
132             }
133             }
134              
135             sub _nth_key {
136 489     489   919 my($self, $n) = @_;
137 489 100 100     1167 if($self->{cache} && exists($self->{cache}->{keys}->{$n})) {
138 19         49 return $self->{cache}->{keys}->{$n}
139             }
140            
141 470         1087 $self->_seek($self->_nth_key_ptr_location($n));
142 470         2029 $self->_seek($self->_ptr_at_current_offset());
143              
144             # for performance, cache the filehandle in this object
145 470   33     1879 $self->{_fh} ||= $self->_fh();
146 470         972 my $offset = tell($self->{_fh});
147 470         1415 my $key = $self->_node_at_current_offset();
148 469 100 100     1956 if(!defined($key) || ref($key)) {
149 2 100       25 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 467 100       1098 if($self->{cache}) {
158 16         59 return $self->{cache}->{keys}->{$n} = $key;
159             }
160 451         1066 return $key;
161             }
162              
163             sub _nth_value {
164 100     100   219 my($self, $n) = @_;
165 100 100 100     274 if($self->{cache} && exists($self->{cache}->{values}->{$n})) {
166 1         5 return $self->{cache}->{values}->{$n}
167             }
168              
169 99         253 $self->_seek($self->_nth_key_ptr_location($n) + $self->_ptr_size());
170 99         366 $self->_seek($self->_ptr_at_current_offset());
171              
172 99         380 my $val = $self->_node_at_current_offset();
173              
174 99 100       293 if($self->{cache}) {
175 15         113 return $self->{cache}->{values}->{$n} = $val;
176             }
177 84         580 return $val;
178             }
179              
180             sub _nth_key_ptr_location {
181 569     569   1087 my($self, $n) = @_;
182 569         1407 return $self->_offset() + $self->_scalar_type_bytes() +
183             2 * $n * $self->_ptr_size();
184             }
185              
186             sub _ptr_at_current_offset {
187 569     569   982 my $self = shift;
188 569         1392 return $self->_decode_ptr(
189             $self->_bytes_at_current_offset($self->_ptr_size())
190             );
191             }
192              
193             sub indices {
194 7     7 0 148 my $self = shift;
195 7 100       20 return [] if($self->count() == 0);
196 6         19 return [ map { $self->_nth_key($_) } (0 .. $self->count() - 1) ];
  17         57  
197             }
198              
199             1;