File Coverage

blib/lib/Range/Merge.pm
Criterion Covered Total %
statement 118 125 94.4
branch 23 30 76.6
condition 2 3 66.6
subroutine 12 13 92.3
pod 2 2 100.0
total 157 173 90.7


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3             #
4             # Copyright (C) 2016 J. Maslak
5             # All Rights Reserved - See License
6             #
7              
8             package Range::Merge v0.01.00;
9             $Range::Merge::VERSION = '1.003';
10 4     4   2958 use Range::Merge::Boilerplate 'script';
  4         10  
  4         26  
11              
12             require Exporter;
13             our @ISA = qw(Exporter);
14             our @EXPORT_OK = qw(merge merge_ipv4);
15              
16 4     4   5726 use List::Util qw(max);
  4         10  
  4         232  
17 4     4   1446 use Net::CIDR;
  4         16815  
  4         172  
18 4     4   1638 use Socket;
  4         11278  
  4         6393  
19              
20             # ABSTRACT: Merges ranges of data including subset/superset ranges
21              
22              
23              
24 14     14 1 188 sub merge($ranges) {
  14         23  
  14         22  
25 14         37 my $sorted = _sort($ranges);
26 14         32 my $split = [];
27 14         45 _split($ranges, $split);
28 14         34 return _combine($split);
29             }
30              
31              
32 12     12 1 151616 sub merge_ipv4($cidr) {
  12         25  
  12         16  
33 12         20 my $ranges = [];
34 12         116 @$ranges = map { _cidr2range($_) } @$cidr;
  66123         92235  
35 12         423 my $combined = merge($ranges);
36 12         36 return _range2cidr($combined);
37             }
38              
39 66123     66123   67152 sub _cidr2range($cidr) {
  66123         71693  
  66123         63804  
40 66123         95263 my ($ip, @a) = @$cidr;
41 66123         95458 my ($range) = Net::CIDR::cidr2range($ip);
42 66123         6471243 my (@parts) = map { unpack('N', inet_aton($_)) } split(/-/, $range);
  132246         343058  
43              
44 66123         149835 return [ @parts, @a ];
45             }
46              
47 12     12   17 sub _range2cidr($ranges) {
  12         15  
  12         17  
48 12         13 my @output;
49 12         28 foreach my $range (@$ranges) {
50 24         49 my ($start, $end, @other) = @$range;
51 24         171 $start = inet_ntoa(pack('N', $start));
52 24         85 $end = inet_ntoa(pack('N', $end ));
53 24         93 foreach my $cidr (Net::CIDR::range2cidr("$start-$end")) {
54 32         4865 push @output, [ $cidr, @other ];
55             }
56             }
57 12         6110 return \@output;
58             }
59              
60             # Sorts by starting address and then by reverse (less specific to more
61             # specific)
62 14     14   23 sub _sort($ranges) {
  14         21  
  14         16  
63 14 50       503 my (@output) = sort { ($a->[0] <=> $b->[0]) || ($b->[1] <=> $a->[0]) } @$ranges;
  66139         90085  
64 14         42 return \@output;
65             }
66              
67 0     0   0 sub _merge($ranges) {
  0         0  
  0         0  
68 0         0 my $split = [];
69 0         0 _split($ranges, $split);
70 0         0 return _combine($split);
71             }
72              
73 14     14   16 sub _combine($ranges) {
  14         18  
  14         18  
74 14         20 my @output;
75              
76             my $last;
77 14         26 foreach my $range (@$ranges) {
78 66133 100       82772 if (!defined($last)) {
79 14         32 $last = [ @$range ];
80 14         24 next;
81             }
82 66119 100 66     137322 if (($last->[1] == $range->[0] - 1) && (scalar(@$last) == scalar(@$range))) {
83 66105         65927 my $nomatch;
84 66105         92019 for (my $i=2; $i
85 3 100       6 if ($last->[$i] ne $range->[$i]) {
86 2         2 $nomatch = 1;
87 2         3 last;
88             }
89             }
90 66105 100       73951 if ($nomatch) {
91 2         3 push @output, $last;
92 2         4 $last = [ @$range ];
93             } else {
94 66103         79907 $last->[1] = $range->[1];
95             }
96             } else {
97 14         23 push @output, $last;
98 14         33 $last = [ @$range ];
99             }
100             }
101 14 50       30 if (defined($last)) { push @output, $last }
  14         23  
102              
103 14         2355 return \@output;
104             }
105              
106 14     14   23 sub _split($ranges, $output, $stack = []) {
  14         21  
  14         24  
  14         23  
  14         22  
107             # Termination condition
108 14 50       45 if (scalar($ranges->@*) == 0) { return undef; }
  0         0  
109              
110             # We just repeatedly call _add_to_stack
111 14         30 foreach my $range ($ranges->@*) {
112 66131         83254 _add_to_stack($range, $stack, $output);
113             }
114              
115             # Return stack
116 14 50       27 if (scalar($stack->@*)) {
117 14         24 push $output->@*, $stack->@*;
118             }
119              
120 14         24 return undef;
121             }
122              
123 66131     66131   65451 sub _add_to_stack($range, $stack, $output) {
  66131         64491  
  66131         62114  
  66131         62430  
  66131         62784  
124 66131 100       82583 if (!scalar($stack->@*)) {
125             # Empty stack
126 14         27 push $stack->@*, $range;
127 14         30 return undef;
128             }
129              
130             # We know the following:
131             #
132             # 1. The stack is sorted
133             # 2. There are no overlapping elements
134             # 2a. Thus we only have to split 1 element max
135             # 3. The stack has at least one element
136            
137 66117         74797 my (@lstack) = grep { $_->[1] < $range->[0]} @$stack;
  66121         109712  
138 66117         77944 my (@rstack) = grep { $_->[0] > $range->[1]} @$stack;
  66121         94354  
139 66117 100       75530 my (@mid ) = grep { ($_->[0] <= $range->[1]) && ($_->[1] >= $range->[0])} @$stack;
  66121         156159  
140              
141             # Clear stack
142 66117         74884 @$stack = ();
143              
144             # Output the stuff completely to the left of the new range
145 66117         72060 push @$output, @lstack;
146              
147             # Option 1 -> No middle element, so just add the range (and the
148             # right stack) to the stack
149 66117 100       88580 if (!scalar(@mid)) {
150 66112         70271 push @$stack, $range, @rstack;
151 66112         93921 return undef;
152             }
153              
154             # We start with the left and right parts of the element that might
155             # need to be split.
156 5         11 my (@left) = $mid[0]->@*;
157 5         9 my (@right) = $mid[0]->@*;
158            
159             # Does the ele needing split start before the range? If so, add the piece
160             # needed to the output
161 5 50       14 if ($left[0] < $range->[0]) {
162 5         12 @left[1] = $range->[0] - 1;
163 5 50       12 if ($left[0] <= $left[1]) {
164 5         10 push @$output, \@left;
165             }
166             }
167              
168             # We need to add the range to the stack
169 5         8 push @$stack, $range;
170              
171             # Does the ele needing split end after the range? If so, add the
172             # piece to the stack
173 5 100       13 if ($right[1] > $range->[1]) {
174 2         5 @right[0] = $range->[1] + 1;
175 2 50       6 if ($right[0] <= $right[1]) {
176 2         5 push @$stack, \@right;
177             }
178             }
179              
180 5         7 push @$stack, @rstack;
181              
182 5         11 return undef;
183             }
184              
185             # Main element in the algorithm
186             # sub _merge($ranges, $output, $stack) {
187             # if (!scalar($stack->@*)) {
188             # # Stack is empty
189             #
190             # if (!scalar($ranges->@*)) {
191             # return undef; # No ranges, no stack
192             # }
193             #
194             # push $stack->@*, unshift($ranges->@*);
195             # }
196             #
197             # _output_stack($output, $stack);
198             # }
199              
200             1;
201              
202             __END__