File Coverage

lib/Range/Merge.pm
Criterion Covered Total %
statement 141 147 95.9
branch 32 38 84.2
condition 2 3 66.6
subroutine 15 16 93.7
pod 3 3 100.0
total 193 207 93.2


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3             #
4             # Copyright (C) 2016-2025 Joelle Maslak
5             # All Rights Reserved - See License
6             #
7              
8             package Range::Merge;
9             $Range::Merge::VERSION = '2.253531';
10 8     8   561823 use strict;
  8         21  
  8         339  
11 8     8   43 use warnings;
  8         19  
  8         506  
12              
13 8     8   485 use Range::Merge::Boilerplate 'script';
  8         15  
  8         71  
14              
15             require Exporter;
16             our @ISA = qw(Exporter);
17             our @EXPORT_OK = qw(merge merge_discrete merge_ipv4);
18              
19 8     8   14965 use List::Util qw(max);
  8         18  
  8         638  
20 8     8   5663 use Net::CIDR;
  8         71649  
  8         668  
21 8     8   5413 use Socket;
  8         43365  
  8         24914  
22              
23             # ABSTRACT: Merges ranges of data including subset/superset ranges
24              
25              
26              
27 15     15 1 809093 sub merge($ranges) {
  15         34  
  15         25  
28 15         50 my $sorted = _sort($ranges);
29 15         35 my $split = [];
30 15         59 _split( $sorted, $split );
31 15         43 return _combine($split);
32             }
33              
34              
35 4     4 1 212694 sub merge_discrete($values) {
  4         6  
  4         15  
36 4         7 my $ranges = [];
37              
38 4         5 my $run;
39              
40 4         15 for my $num ( sort { $a <=> $b } @$values ) {
  9         12  
41 9 100       12 if ( !defined $run ) {
42 3         4 $run = [ $num, $num ];
43 3         6 push @$ranges, $run;
44             } else {
45 6 100       12 if ( $run->[1] == $num ) {
    100          
46             # Do nothing
47             } elsif ( $run->[1] == $num - 1 ) {
48 2         3 $run->[1] = $num;
49             } else {
50 3         3 $run = [ $num, $num ];
51 3         4 push @$ranges, $run;
52             }
53             }
54             }
55              
56 4         10 return $ranges;
57             }
58              
59              
60 13     13 1 728036 sub merge_ipv4($cidr) {
  13         31  
  13         22  
61 13         33 my $ranges = [];
62 13         258 @$ranges = map { _cidr2range($_) } @$cidr;
  66125         116779  
63 12         590 my $combined = merge($ranges);
64 12         40 return _range2cidr($combined);
65             }
66              
67 66125     66125   89732 sub _cidr2range($cidr) {
  66125         90061  
  66125         84184  
68 66125         122983 my ( $ip, @a ) = @$cidr;
69 66125 100       264385 die("Invalid IP address: $ip") if ( grep { /^0[0-9]/ } split( /[\/.]/, $ip ) );
  330625         605394  
70 66124         165045 my ($range) = Net::CIDR::cidr2range($ip);
71 66124         8123275 my (@parts) = map { unpack( 'N', inet_aton($_) ) } split( /-/, $range );
  132248         487170  
72              
73 66124         194326 return [ @parts, @a ];
74             }
75              
76 12     12   18 sub _range2cidr($ranges) {
  12         18  
  12         14  
77 12         19 my @output;
78 12         24 foreach my $range (@$ranges) {
79 24         61 my ( $start, $end, @other ) = @$range;
80 24         160 $start = inet_ntoa( pack( 'N', $start ) );
81 24         80 $end = inet_ntoa( pack( 'N', $end ) );
82 24         98 foreach my $cidr ( Net::CIDR::range2cidr("$start-$end") ) {
83 32         5669 push @output, [ $cidr, @other ];
84             }
85             }
86 12         10947 return \@output;
87             }
88              
89             # Sorts by starting address and then by reverse (less specific to more
90             # specific)
91 15     15   40 sub _sort($ranges) {
  15         24  
  15         23  
92 15 50       866 my (@output) = sort { ( $a->[0] <=> $b->[0] ) || ( $b->[1] <=> $a->[1] ) } @$ranges;
  66147         101557  
93 15         46 return \@output;
94             }
95              
96 0     0   0 sub _merge($ranges) {
  0         0  
  0         0  
97 0         0 my $split = [];
98 0         0 _split( $ranges, $split );
99 0         0 return _combine($split);
100             }
101              
102 15     15   22 sub _combine($ranges) {
  15         23  
  15         48  
103 15         33 my @output;
104              
105             my $last;
106 15         73 foreach my $range (@$ranges) {
107 66138 100       133264 if ( !defined($last) ) {
108 15         62 $last = [@$range];
109 15         31 next;
110             }
111 66123 100 66     227459 if ( ( $last->[1] == $range->[0] - 1 ) && ( scalar(@$last) == scalar(@$range) ) ) {
112 66108         98298 my $nomatch;
113 66108         145727 for ( my $i = 2; $i < scalar(@$range); $i++ ) {
114 6 100       26 if ( $last->[$i] ne $range->[$i] ) {
115 5         10 $nomatch = 1;
116 5         8 last;
117             }
118             }
119 66108 100       117220 if ($nomatch) {
120 5         11 push @output, $last;
121 5         17 $last = [@$range];
122             } else {
123 66103         130389 $last->[1] = $range->[1];
124             }
125             } else {
126 15         27 push @output, $last;
127 15         36 $last = [@$range];
128             }
129             }
130 15 50       39 if ( defined($last) ) { push @output, $last }
  15         33  
131              
132 15         3980 return \@output;
133             }
134              
135 15     15   27 sub _split ( $ranges, $output, $stack = [] ) {
  15         25  
  15         23  
  15         55  
  15         42  
136             # Termination condition
137 15 50       53 return if scalar( $ranges->@* ) == 0;
138              
139             # We just repeatedly call _add_to_stack
140 15         56 foreach my $range ( $ranges->@* ) {
141 66136         112736 _add_to_stack( $range, $stack, $output );
142             }
143              
144             # Return stack
145 15 50       37 if ( scalar( $stack->@* ) ) {
146 15         35 push $output->@*, $stack->@*;
147             }
148              
149 15         37 return;
150             }
151              
152 66136     66136   86745 sub _add_to_stack ( $range, $stack, $output ) {
  66136         90926  
  66136         88921  
  66136         88428  
  66136         85746  
153 66136 100       120318 if ( !scalar( $stack->@* ) ) {
154             # Empty stack
155 15         29 push $stack->@*, $range;
156 15         33 return;
157             }
158              
159             # We know the following:
160             #
161             # 1. The stack is sorted
162             # 2. There are no overlapping elements
163             # 2a. Thus we only have to split 1 element max
164             # 3. The stack has at least one element
165              
166 66121         105997 my (@lstack) = grep { $_->[1] < $range->[0] } @$stack;
  66128         143546  
167 66121         99713 my (@rstack) = grep { $_->[0] > $range->[1] } @$stack;
  66128         124798  
168 66121 100       102749 my (@mid) = grep { ( $_->[0] <= $range->[1] ) && ( $_->[1] >= $range->[0] ) } @$stack;
  66128         212719  
169              
170             # Clear stack
171 66121         99299 @$stack = ();
172              
173             # Output the stuff completely to the left of the new range
174 66121         103247 push @$output, @lstack;
175              
176             # Option 1 -> No middle element, so just add the range (and the
177             # right stack) to the stack
178 66121 100       119778 if ( !scalar(@mid) ) {
179 66113         98183 push @$stack, $range, @rstack;
180 66113         128311 return;
181             }
182              
183             # We start with the left and right parts of the element that might
184             # need to be split.
185 8         21 my (@left) = $mid[0]->@*;
186 8         18 my (@right) = $mid[0]->@*;
187              
188             # Does the ele needing split start before the range? If so, add the piece
189             # needed to the output
190 8 100       23 if ( $left[0] < $range->[0] ) {
191 6         21 @left[1] = $range->[0] - 1;
192 6 50       20 if ( $left[0] <= $left[1] ) {
193 6         14 push @$output, \@left;
194             }
195             }
196              
197             # We need to add the range to the stack
198 8         17 push @$stack, $range;
199              
200             # Does the ele needing split end after the range? If so, add the
201             # piece to the stack
202 8 100       27 if ( $right[1] > $range->[1] ) {
203 4         11 @right[0] = $range->[1] + 1;
204 4 50       17 if ( $right[0] <= $right[1] ) {
205 4         12 push @$stack, \@right;
206             }
207             }
208              
209 8         16 push @$stack, @rstack;
210              
211 8         70 return;
212             }
213              
214             1;
215              
216             __END__