File Coverage

blib/lib/Path/Ancestor.pm
Criterion Covered Total %
statement 40 40 100.0
branch 14 14 100.0
condition 11 12 91.6
subroutine 4 4 100.0
pod 0 1 0.0
total 69 71 97.1


line stmt bran cond sub pod time code
1             ###########################################
2             package Path::Ancestor;
3             ###########################################
4 1     1   20557 use strict;
  1         3  
  1         32  
5 1     1   4 use warnings;
  1         2  
  1         29  
6 1     1   6 use List::Util qw(min max);
  1         7  
  1         524  
7              
8             require Exporter;
9             our @ISA = qw(Exporter);
10             our @EXPORT_OK = qw(longest_common_ancestor);
11              
12             our $VERSION = "0.01";
13              
14             ###########################################
15             sub longest_common_ancestor {
16             ###########################################
17 33     33 0 65 my $paths = [];
18              
19             # Just one path? Simply return it.
20 33 100       126 return $_[0] if @_ == 1;
21              
22             # Transform all paths to arrays
23 31         55 for ( @_ ) {
24 71         379 push @$paths, [split //, $_];
25             }
26              
27 31         51 my $minlen = min map { scalar @$_ } @$paths;
  71         139  
28 31         41 my $maxlen = max map { scalar @$_ } @$paths;
  71         104  
29 31         31 my $last_match = -1;
30 31         35 my $last_slash_idx = -1;
31              
32             # Examine all characters left-to-right
33 31         66 MATCH: for my $i (0..$minlen-1) {
34              
35             # Get the Nth character of the first path
36 154         200 my $ref = $paths->[0]->[ $i ];
37 154 100       301 if( $ref eq "/" ){
38 40         48 $last_slash_idx = $i;
39             }
40              
41             # ... and compare what all other paths have at this location
42 154         230 for my $path_idx ( 1 .. $#$paths ) {
43 204 100       344 if( $paths->[ $path_idx ]->[ $i ] ne $ref ) {
44 9         20 last MATCH;
45             }
46 195         367 $last_match = $i;
47             }
48             }
49              
50             # Here's an edge case: If we have "/foo", "/foo/bar", "/foo/moo/moo",
51             # we need to verify that "/foo" is a *complete* path with all other
52             # paths.
53 31         33 my $is_complete_path = 1;
54 31         45 for ( @$paths ) {
55 50 100 100     193 if(exists $_->[ $last_match+1 ] and
56             $_->[ $last_match+1 ] ne "/") {
57 20         19 $is_complete_path = 0;
58 20         30 last;
59             }
60             }
61              
62             # Remove only trailing slashes
63 31   100     121 while($last_match > 0 and
64             $paths->[0]->[ $last_match ] eq "/") {
65 5         21 $last_match--;
66             }
67              
68             # What if we didn't match all the way to the end?
69 31 100 100     121 if( $last_match+1 ne $maxlen and !$is_complete_path) {
70             # Not a complete path, go back
71              
72 20 100 66     68 if($last_slash_idx < 0) {
    100          
73             # We don't have a slash to go back to => empty match
74 5         8 $last_match = -1;
75             } elsif($last_slash_idx == 0 and $paths->[0]->[0] eq "/") {
76             # leave the slash in if "/" is the longest common path
77 4         4 $last_match = 0;
78             } else {
79             # up until (excluding) the last matching slash
80 11         16 $last_match = $last_slash_idx - 1;
81             }
82             }
83              
84 31         205 return substr $_[0], 0, $last_match+1;
85             }
86              
87             1;
88              
89             __END__