File Coverage

lib/Range/Merge.pm
Criterion Covered Total %
statement 139 145 95.8
branch 29 36 80.5
condition 2 3 66.6
subroutine 15 16 93.7
pod 3 3 100.0
total 188 203 92.6


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3             #
4             # Copyright (C) 2016-2019 Joelle Maslak
5             # All Rights Reserved - See License
6             #
7              
8             package Range::Merge;
9             $Range::Merge::VERSION = '2.191190';
10 6     6   6348 use strict;
  6         16  
  6         184  
11 6     6   31 use warnings;
  6         20  
  6         171  
12              
13 6     6   32 use Range::Merge::Boilerplate 'script';
  6         13  
  6         44  
14              
15             require Exporter;
16             our @ISA = qw(Exporter);
17             our @EXPORT_OK = qw(merge merge_discrete merge_ipv4);
18              
19 6     6   9361 use List::Util qw(max);
  6         48  
  6         377  
20 6     6   3538 use Net::CIDR;
  6         34242  
  6         309  
21 6     6   3450 use Socket;
  6         22877  
  6         12335  
22              
23             # ABSTRACT: Merges ranges of data including subset/superset ranges
24              
25              
26              
27 14     14 1 243 sub merge($ranges) {
  14         27  
  14         18  
28 14         40 my $sorted = _sort($ranges);
29 14         29 my $split = [];
30 14         45 _split( $sorted, $split );
31 14         35 return _combine($split);
32             }
33              
34              
35 4     4 1 8418 sub merge_discrete($values) {
  4         8  
  4         5  
36 4         8 my $ranges = [];
37              
38 4         6 my $run;
39              
40 4         18 for my $num ( sort { $a <=> $b } @$values ) {
  9         16  
41 9 100       18 if ( !defined $run ) {
42 3         7 $run = [ $num, $num ];
43 3         7 push @$ranges, $run;
44             } else {
45 6 100       19 if ( $run->[1] == $num ) {
    100          
46             # Do nothing
47             } elsif ( $run->[1] == $num - 1 ) {
48 2         5 $run->[1] = $num;
49             } else {
50 3         5 $run = [ $num, $num ];
51 3         8 push @$ranges, $run;
52             }
53             }
54             }
55              
56 4         10 return $ranges;
57             }
58              
59              
60 12     12 1 193784 sub merge_ipv4($cidr) {
  12         28  
  12         16  
61 12         27 my $ranges = [];
62 12         135 @$ranges = map { _cidr2range($_) } @$cidr;
  66123         107110  
63 12         476 my $combined = merge($ranges);
64 12         39 return _range2cidr($combined);
65             }
66              
67 66123     66123   79271 sub _cidr2range($cidr) {
  66123         87742  
  66123         86094  
68 66123         138883 my ( $ip, @a ) = @$cidr;
69 66123         116222 my ($range) = Net::CIDR::cidr2range($ip);
70 66123         7649904 my (@parts) = map { unpack( 'N', inet_aton($_) ) } split( /-/, $range );
  132246         396048  
71              
72 66123         202225 return [ @parts, @a ];
73             }
74              
75 12     12   20 sub _range2cidr($ranges) {
  12         20  
  12         24  
76 12         18 my @output;
77 12         27 foreach my $range (@$ranges) {
78 24         60 my ( $start, $end, @other ) = @$range;
79 24         178 $start = inet_ntoa( pack( 'N', $start ) );
80 24         92 $end = inet_ntoa( pack( 'N', $end ) );
81 24         105 foreach my $cidr ( Net::CIDR::range2cidr("$start-$end") ) {
82 32         5069 push @output, [ $cidr, @other ];
83             }
84             }
85 12         6005 return \@output;
86             }
87              
88             # Sorts by starting address and then by reverse (less specific to more
89             # specific)
90 14     14   23 sub _sort($ranges) {
  14         21  
  14         16  
91 14 50       599 my (@output) = sort { ( $a->[0] <=> $b->[0] ) || ( $b->[1] <=> $a->[0] ) } @$ranges;
  66139         110658  
92 14         49 return \@output;
93             }
94              
95 0     0   0 sub _merge($ranges) {
  0         0  
  0         0  
96 0         0 my $split = [];
97 0         0 _split( $ranges, $split );
98 0         0 return _combine($split);
99             }
100              
101 14     14   23 sub _combine($ranges) {
  14         22  
  14         17  
102 14         29 my @output;
103              
104             my $last;
105 14         33 foreach my $range (@$ranges) {
106 66133 100       98839 if ( !defined($last) ) {
107 14         36 $last = [@$range];
108 14         28 next;
109             }
110 66119 100 66     174576 if ( ( $last->[1] == $range->[0] - 1 ) && ( scalar(@$last) == scalar(@$range) ) ) {
111 66105         78806 my $nomatch;
112 66105         110376 for ( my $i = 2; $i < scalar(@$range); $i++ ) {
113 3 100       8 if ( $last->[$i] ne $range->[$i] ) {
114 2         3 $nomatch = 1;
115 2         5 last;
116             }
117             }
118 66105 100       88359 if ($nomatch) {
119 2         6 push @output, $last;
120 2         4 $last = [@$range];
121             } else {
122 66103         99132 $last->[1] = $range->[1];
123             }
124             } else {
125 14         34 push @output, $last;
126 14         31 $last = [@$range];
127             }
128             }
129 14 50       35 if ( defined($last) ) { push @output, $last }
  14         31  
130              
131 14         3019 return \@output;
132             }
133              
134 14     14   28 sub _split ( $ranges, $output, $stack = [] ) {
  14         22  
  14         21  
  14         26  
  14         20  
135             # Termination condition
136 14 50       51 return if scalar( $ranges->@* ) == 0;
137              
138             # We just repeatedly call _add_to_stack
139 14         38 foreach my $range ( $ranges->@* ) {
140 66131         96818 _add_to_stack( $range, $stack, $output );
141             }
142              
143             # Return stack
144 14 50       39 if ( scalar( $stack->@* ) ) {
145 14         28 push $output->@*, $stack->@*;
146             }
147              
148 14         27 return;
149             }
150              
151 66131     66131   76375 sub _add_to_stack ( $range, $stack, $output ) {
  66131         77573  
  66131         76061  
  66131         75847  
  66131         75321  
152 66131 100       101188 if ( !scalar( $stack->@* ) ) {
153             # Empty stack
154 14         30 push $stack->@*, $range;
155 14         35 return;
156             }
157              
158             # We know the following:
159             #
160             # 1. The stack is sorted
161             # 2. There are no overlapping elements
162             # 2a. Thus we only have to split 1 element max
163             # 3. The stack has at least one element
164              
165 66117         89946 my (@lstack) = grep { $_->[1] < $range->[0] } @$stack;
  66121         143546  
166 66117         94317 my (@rstack) = grep { $_->[0] > $range->[1] } @$stack;
  66121         116238  
167 66117 100       92150 my (@mid) = grep { ( $_->[0] <= $range->[1] ) && ( $_->[1] >= $range->[0] ) } @$stack;
  66121         193010  
168              
169             # Clear stack
170 66117         93210 @$stack = ();
171              
172             # Output the stuff completely to the left of the new range
173 66117         89781 push @$output, @lstack;
174              
175             # Option 1 -> No middle element, so just add the range (and the
176             # right stack) to the stack
177 66117 100       106857 if ( !scalar(@mid) ) {
178 66112         84050 push @$stack, $range, @rstack;
179 66112         111766 return;
180             }
181              
182             # We start with the left and right parts of the element that might
183             # need to be split.
184 5         14 my (@left) = $mid[0]->@*;
185 5         12 my (@right) = $mid[0]->@*;
186              
187             # Does the ele needing split start before the range? If so, add the piece
188             # needed to the output
189 5 50       15 if ( $left[0] < $range->[0] ) {
190 5         19 @left[1] = $range->[0] - 1;
191 5 50       14 if ( $left[0] <= $left[1] ) {
192 5         30 push @$output, \@left;
193             }
194             }
195              
196             # We need to add the range to the stack
197 5         13 push @$stack, $range;
198              
199             # Does the ele needing split end after the range? If so, add the
200             # piece to the stack
201 5 100       17 if ( $right[1] > $range->[1] ) {
202 2         7 @right[0] = $range->[1] + 1;
203 2 50       5 if ( $right[0] <= $right[1] ) {
204 2         5 push @$stack, \@right;
205             }
206             }
207              
208 5         11 push @$stack, @rstack;
209              
210 5         12 return;
211             }
212              
213             1;
214              
215             __END__