File Coverage

blib/lib/Tree/Ternary.pm
Criterion Covered Total %
statement 154 158 97.4
branch 89 96 92.7
condition 26 30 86.6
subroutine 18 18 100.0
pod 10 10 100.0
total 297 312 95.1


line stmt bran cond sub pod time code
1             ###########################################################################
2             #
3             # Tree::Ternary
4             #
5             # Copyright (C) 1999, Mark Rogaski; all rights reserved.
6             #
7             # This module is free software. You can redistribute it and/or
8             # modify it under the terms of the Artistic License 2.0.
9             #
10             # This program is distributed in the hope that it will be useful,
11             # but without any warranty; without even the implied warranty of
12             # merchantability or fitness for a particular purpose.
13             #
14             ###########################################################################
15              
16             package Tree::Ternary;
17              
18 11     11   1131372 use 5;
  11         41  
  11         488  
19 11     11   62 use strict;
  11         20  
  11         617  
20 11         2882 use vars qw(
21             @ISA
22             @EXPORT_OK
23             %EXPORT_TAGS
24             @ATTRIBUTES
25 11     11   60 );
  11         19  
26              
27             require Exporter;
28              
29             our $VERSION = '0.04';
30             $VERSION = eval $VERSION;
31              
32             @ISA = qw(Exporter);
33              
34             # Export the attribute names
35             @EXPORT_OK = @ATTRIBUTES;
36             %EXPORT_TAGS = (attrib => [ @ATTRIBUTES ]);
37              
38             BEGIN {
39              
40             #
41             # I'm using Greg Bacon's design for array-based objects.
42             # SPLIT_CHAR, LO_KID, EQ_KID, and HI_KID are the only ones that
43             # will be used in every node, the others will only be defined
44             # in the root.
45             #
46 11     11   63 @ATTRIBUTES = qw(
47             SPLIT_CHAR
48             LO_KID
49             EQ_KID
50             HI_KID
51             PAYLOAD
52             NODE_COUNT
53             TERMINAL_COUNT
54             );
55              
56             #
57             # Construct the code to declare our constants, execute, and check for
58             # errors (this was so much simpler in Pascal!)
59             #
60 11         182 my $attrcode = join "\n",
61             map qq[ sub $ATTRIBUTES[$_] () { $_ } ],
62             0..$#ATTRIBUTES;
63              
64 11         1974 eval $attrcode;
65              
66 11 50       26778 if ($@) {
67 0         0 require Carp;
68 0         0 Carp::croak("Failed to initialize module index: $@\n");
69             }
70             };
71              
72             #
73             # Here is the terminal character. '00' was chosen since it is not equal to
74             # any 8 bit character. This is actually an improvement over the original
75             # C code, in that it permits the methods to be 8 bit clean. If I include
76             # Unicode support, this may be replaced with some Ultra Mega meta-character.
77             #
78             sub TERM_CHAR () { '00'; }
79              
80             #
81             # Public Methods
82             #
83              
84             sub new {
85             #
86             # Create a new Tree::Ternary object
87             #
88 10     10 1 120 my $class = shift;
89 10         28 my $self = [];
90              
91 10         31 bless $self, $class;
92              
93             # initialize the counters
94 10         85 $self->[NODE_COUNT] = 0;
95 10         29 $self->[TERMINAL_COUNT] = 0;
96              
97 10         49 $self;
98             }
99              
100              
101             sub nodes {
102             #
103             # Returns the total number of nodes
104             #
105 27     27 1 60 my $self = shift;
106 27         128 $self->[NODE_COUNT];
107             }
108              
109              
110             sub terminals {
111             #
112             # Returns the total number of terminal nodes
113             #
114 27     27 1 51 my $self = shift;
115 27         118 $self->[TERMINAL_COUNT];
116             }
117              
118             sub insert {
119             #
120             # Iterative implementation of string insertion.
121             #
122             # Arguments:
123             # a string to be inserted into the array
124             #
125             # Return value:
126             # Returns a reference to a scalar on successful insert,
127             # returns undef if the string is already in the tree.
128             #
129 55     55 1 214 my($self, $str) = @_;
130              
131             #
132             # We can keep this implementation relatively simple and still
133             # be 8 bit clean if we split the string into an array and use
134             # TERM_CHAR as a terminator.
135             #
136 55         494 my(@char) = (split(//, $str), TERM_CHAR);
137              
138 55         100 my $ref = $self;
139 55         64 my $retval = undef;
140              
141 55         122 while (@char) {
142              
143 1790         8520 my $char = $char[0];
144              
145 1790 100       2862 if (! defined $ref->[SPLIT_CHAR]) { # We use defined() to avoid
146             # auto-vivification.
147              
148             # create a new node
149 642         1113 $ref->[LO_KID] = [];
150 642         976 $ref->[EQ_KID] = [];
151 642         1045 $ref->[HI_KID] = [];
152 642 100       1294 if (($ref->[SPLIT_CHAR] = $char) eq TERM_CHAR) {
153 50         63 $self->[TERMINAL_COUNT]++;
154 50         82 $ref->[PAYLOAD] = '';
155 50         132 $retval = \$ref->[PAYLOAD];
156             } else {
157 592         1327 $self->[NODE_COUNT]++;
158             }
159              
160             } else {
161              
162             # here be the guts
163 1148 100       2463 if ($char lt $ref->[SPLIT_CHAR]) {
    100          
164 55         122 $ref = $ref->[LO_KID];
165             } elsif ($char gt $ref->[SPLIT_CHAR]) {
166 69         206 $ref = $ref->[HI_KID];
167             } else {
168 1024         1159 $ref = $ref->[EQ_KID];
169 1024         2096 shift @char;
170             }
171              
172             }
173              
174             }
175              
176 55         192 $retval;
177             }
178              
179             sub search {
180             #
181             # Iterative implementation of the string search.
182             #
183             # Arguments:
184             # string - string to search for in the tree
185             #
186             # Return value:
187             # Returns a reference to the scalar payload if the string is found,
188             # returns undef if the string is not found
189             #
190 17     17 1 47 my($self, $str) = @_;
191 17         93 my(@char) = (split(//, $str), TERM_CHAR);
192 17         32 my $ref = $self;
193              
194 17         45 while (defined $ref->[SPLIT_CHAR]) {
195              
196 189         197 my $char = $char[0];
197              
198 189 100       452 if ($char lt $ref->[SPLIT_CHAR]) {
    100          
199 16         38 $ref = $ref->[LO_KID];
200             } elsif ($char gt $ref->[SPLIT_CHAR]) {
201 6         15 $ref = $ref->[HI_KID];
202             } else {
203 167 100       254 if ($char eq TERM_CHAR) {
204 11         68 return \$ref->[PAYLOAD];
205             }
206 156         159 $ref = $ref->[EQ_KID];
207 156         308 shift @char;
208             }
209            
210             }
211              
212 6         32 undef;
213             }
214              
215             sub rinsert {
216             #
217             # Recursive implementation of string insertion.
218             #
219             # Arguments:
220             # a string to be inserted into the array
221             #
222             # Return value:
223             # Returns a reference to a scalar on successful insert,
224             # returns undef if the string is already in the tree.
225             #
226 19     19 1 49 my($self, $str) = @_;
227 19         160 my(@char) = (split(//, $str), TERM_CHAR);
228              
229 19         69 return ($self->_rinsert_core($self, @char))[1];
230              
231             }
232              
233             sub _rinsert_core {
234             #
235             # Core of the rinsert() function. This allows us to do some
236             # "clean" recursion without clubbing the user over the head
237             # with the gory details.
238             #
239 416     416   5744 my($self, $ref, @char) = @_;
240 416         823 my $retval = undef;
241 416         514 my $char = $char[0];
242              
243 416 100       1448 if (! defined($ref->[SPLIT_CHAR])) {
244            
245             # create a new node
246 251         425 $ref->[LO_KID] = [];
247 251         344 $ref->[EQ_KID] = [];
248 251         369 $ref->[HI_KID] = [];
249 251 100       1732 if (($ref->[SPLIT_CHAR] = $char) eq TERM_CHAR) {
250 14         18 $self->[TERMINAL_COUNT]++;
251 14         23 $ref->[PAYLOAD] = '';
252 14         32 $retval = \$ref->[PAYLOAD];
253             } else {
254 237         423 $self->[NODE_COUNT]++;
255             }
256              
257             }
258              
259 416 100       1100 if ($char lt $ref->[SPLIT_CHAR]) {
    100          
260 16         40 ($ref->[LO_KID], $retval) =
261             $self->_rinsert_core($ref->[LO_KID], @char);
262             } elsif ($char eq $ref->[SPLIT_CHAR]) {
263 390 100       1591 if ($char ne TERM_CHAR) {
264 371         3351 ($ref->[EQ_KID], $retval) =
265             $self->_rinsert_core($ref->[EQ_KID], @char[1..$#char]);
266             }
267             } else {
268 10         50 ($ref->[HI_KID], $retval) =
269             $self->_rinsert_core($ref->[HI_KID], @char);
270             }
271              
272 416         2148 ($ref, $retval);
273              
274             }
275              
276             sub rsearch {
277             #
278             # Recursive implementation of the string search.
279             #
280             # Arguments:
281             # string - string to search for in the tree
282             #
283             # Return value:
284             # Returns a reference to the scalar payload if the string is found,
285             # returns undef if the string is not found
286             #
287 17     17 1 44 my($self, $str) = @_;
288 17         78 my(@char) = (split(//, $str), TERM_CHAR);
289              
290 17 50       41 if (defined $self->[SPLIT_CHAR]) {
291 17         39 return $self->_rsearch_core($self, @char);
292             } else {
293 0         0 return undef;
294             }
295              
296             }
297              
298             sub _rsearch_core {
299             #
300             # Core recursive function for research().
301             #
302 186     186   1279 my($self, $ref, @char) = @_;
303 186         205 my $char = $char[0];
304              
305 186 100       444 if ($char lt $ref->[SPLIT_CHAR]) {
    100          
306 17 100       49 if (defined $ref->[LO_KID]->[SPLIT_CHAR]) {
307 13         27 return $self->_rsearch_core($ref->[LO_KID], @char);
308             } else {
309 4         36 return undef;
310             }
311             } elsif ($char eq $ref->[SPLIT_CHAR]) {
312 166 100       331 if ($char eq TERM_CHAR) {
313 11         409 return \$ref->[PAYLOAD];
314             }
315 155 50       227 if (defined $ref->[EQ_KID]->[SPLIT_CHAR]) {
316 155         1204 return $self->_rsearch_core($ref->[EQ_KID], @char[1..$#char]);
317             } else {
318 0         0 return undef;
319             }
320             } else {
321 3 100       19 if (defined $ref->[HI_KID]->[SPLIT_CHAR]) {
322 1         4 return $self->_rsearch_core($ref->[HI_KID], @char);
323             } else {
324 2         18 return undef;
325             }
326             }
327             }
328              
329             sub pmsearch {
330             #
331             # Pattern match function
332             #
333             # Arguments:
334             # wildcard - the character that is used as the wildcard
335             # in the search string
336             # string - string to search for in the tree, including
337             # wildcard replacements
338             #
339             # Return value:
340             # scalar context: returns a count of strings that match
341             # array context: returns a list of the matched strings
342             #
343 16     16 1 4511 my($self, $wildcard, $str) = @_;
344 16         74 my(@char) = (split(//, $str), TERM_CHAR);
345 16         23 my(@result);
346              
347 16 50       40 if (defined $self->[SPLIT_CHAR]) {
348 16         55 @result = $self->_pmsearch_core($self, $wildcard, '', @char);
349             }
350              
351 16 100       93 wantarray ? @result : scalar(@result);
352             }
353              
354             sub _pmsearch_core {
355             #
356             # Core recursive function for pmsearch().
357             #
358 400     400   1089 my($self, $ref, $wildcard, $candidate, @char) = @_;
359 400         434 my $char = $char[0];
360 400         472 my(@hitlist) = ();
361              
362 400 100 100     1391 if ($char eq $wildcard or $char lt $ref->[SPLIT_CHAR]) {
363 152 100       354 if (defined $ref->[LO_KID]->[SPLIT_CHAR]) {
364 52         114 push(@hitlist, $self->_pmsearch_core( $ref->[LO_KID],
365             $wildcard,
366             $candidate,
367             @char));
368             }
369             }
370              
371 400 100 100     1324 if ($char eq $wildcard or $char eq $ref->[SPLIT_CHAR]) {
372 324 100 66     1128 if ($ref->[SPLIT_CHAR] ne TERM_CHAR and $char ne TERM_CHAR) {
373 292 50       554 if (defined $ref->[EQ_KID]->[SPLIT_CHAR]) {
374 292         1081 push(@hitlist,
375             $self->_pmsearch_core( $ref->[EQ_KID],
376             $wildcard,
377             $candidate . $ref->[SPLIT_CHAR],
378             @char[1..$#char]));
379             }
380             }
381             }
382              
383 400 100 100     1280 if ($char eq TERM_CHAR and $ref->[SPLIT_CHAR] eq TERM_CHAR) {
384 30         57 push(@hitlist, $candidate);
385             }
386              
387 400 100 100     2638 if ($char eq $wildcard or $char gt $ref->[SPLIT_CHAR]) {
388 148 100       307 if (defined $ref->[HI_KID]->[SPLIT_CHAR]) {
389 40         88 push(@hitlist, $self->_pmsearch_core( $ref->[HI_KID],
390             $wildcard,
391             $candidate,
392             @char));
393             }
394             }
395              
396 400         5416 @hitlist;
397              
398             }
399              
400             sub nearsearch {
401             #
402             # Function to find member strings within a difference-distance from
403             # a specified string.
404             #
405             # Arguments:
406             # max_distance - the maximum number of differences between the
407             # source string and the matched string
408             # string - string to search for in the tree
409             #
410             # Return value:
411             # scalar context: returns a count of strings that match
412             # array context: returns a list of the matched strings
413             #
414 16     16 1 6767 my($self, $dist, $str) = @_;
415 16         114 my(@char) = (split(//, $str), TERM_CHAR);
416 16         24 my(@result);
417              
418 16 50       48 if (defined $self->[SPLIT_CHAR]) {
419 16         64 @result = $self->_nearsearch_core($self, $dist, '', @char);
420             }
421              
422 16 100       345 wantarray ? @result : scalar(@result);
423             }
424              
425             sub _nearsearch_core {
426 592     592   1658 my($self, $ref, $dist, $candidate, @char) = @_;
427 592         610 my $char = $char[0];
428 592         649 my(@hitlist) = ();
429              
430             #
431             # Still need this, as explained below.
432             #
433 592 100 66     2111 if (! defined($ref->[SPLIT_CHAR]) or $dist < 0) {
434 85         255 return;
435             }
436              
437 507 100 100     1453 if ($dist > 0 or $char lt $ref->[SPLIT_CHAR]) {
438 348 100 66     1039 unless (! defined($ref->[LO_KID]->[SPLIT_CHAR]) or $dist < 0) {
439 60         139 push(@hitlist, $self->_nearsearch_core( $ref->[LO_KID],
440             $dist,
441             $candidate,
442             @char));
443             }
444             }
445              
446 507 100       859 if ($ref->[SPLIT_CHAR] eq TERM_CHAR) {
447 35 100       64 if ($#char <= $dist) {
448 33         46 push(@hitlist, $candidate);
449             }
450             } else {
451             #
452             # I'm allowing this one to perform some unecessary recursion,
453             # to save some recursion overhead would seriously hurt any
454             # semblance of readability. This may change in the future
455             # if there is a need for this method to be a speed demon.
456             #
457 472 100       2708 push(@hitlist,
    100          
    100          
458             $self->_nearsearch_core($ref->[EQ_KID],
459             (($char eq $ref->[SPLIT_CHAR]) ? $dist : $dist - 1),
460             $candidate . (($char[0] eq TERM_CHAR) ? ''
461             : $ref->[SPLIT_CHAR]),
462             @char[(($char eq TERM_CHAR) ? 0 : 1)..$#char]));
463             }
464              
465 507 100 100     1664 if ($dist > 0 or $char gt $ref->[SPLIT_CHAR]) {
466 375 100 66     829 unless (! defined($ref->[HI_KID]->[SPLIT_CHAR]) or $dist < 0) {
467 44         100 push(@hitlist, $self->_nearsearch_core( $ref->[HI_KID],
468             $dist,
469             $candidate,
470             @char));
471             }
472             }
473              
474 507         1114 @hitlist;
475              
476             }
477              
478             sub traverse {
479             #
480             # Pattern match function
481             #
482             # Arguments:
483             # none
484             #
485             # Return value:
486             # returns a sorted list of the contents of the tree
487             #
488 74     74 1 129 my($self, $ref, $candidate) = @_;
489 74         91 my(@hitlist) = ();
490              
491 74 100       119 unless (defined $ref) {
492 1         2 $ref = $self; # keep the method compact
493 1         13 $candidate = '';
494             }
495              
496 74 100       144 if (defined $ref->[LO_KID]->[SPLIT_CHAR]) {
497 6         11 push(@hitlist, $self->traverse($ref->[LO_KID], $candidate));
498             }
499              
500 74 50       130 if (defined $ref->[SPLIT_CHAR]) {
501 74 100       136 if ($ref->[SPLIT_CHAR] eq TERM_CHAR) {
502 12         44 push(@hitlist, $candidate);
503             }
504             }
505              
506 74 100       126 if (defined $ref->[EQ_KID]->[SPLIT_CHAR]) {
507 62         195 push(@hitlist, $self->traverse( $ref->[EQ_KID],
508             $candidate . $ref->[SPLIT_CHAR]));
509             }
510              
511 74 100       142 if (defined $ref->[HI_KID]->[SPLIT_CHAR]) {
512 5         13 push(@hitlist, $self->traverse($ref->[HI_KID], $candidate));
513             }
514              
515 74         145 @hitlist;
516              
517             }
518              
519             1;
520              
521             __END__