File Coverage

blib/lib/Number/Util/Range.pm
Criterion Covered Total %
statement 11 40 27.5
branch 0 16 0.0
condition 0 16 0.0
subroutine 4 6 66.6
pod 1 1 100.0
total 16 79 20.2


line stmt bran cond sub pod time code
1             package Number::Util::Range;
2              
3 1     1   448481 use 5.010001;
  1         4  
4 1     1   6 use strict;
  1         3  
  1         32  
5 1     1   5 use warnings;
  1         3  
  1         84  
6              
7 1     1   6 use Exporter qw(import);
  1         1  
  1         945  
8              
9             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
10             our $DATE = '2023-09-08'; # DATE
11             our $DIST = 'Number-Util-Range'; # DIST
12             our $VERSION = '0.008'; # VERSION
13              
14             our @EXPORT_OK = qw(convert_number_sequence_to_range);
15             our %SPEC;
16              
17             $SPEC{'convert_number_sequence_to_range'} = {
18             v => 1.1,
19             summary => 'Find sequences in number arrays & convert to range '.
20             '(e.g. 100,2,3,4,5,101 -> 100,"2..5",101)',
21             description => <<'MARKDOWN',
22              
23             This routine accepts an array, finds sequences of numbers in it (e.g. 1, 2, 3),
24             and converts each sequence into a range ("1..3"). So basically it "compresses" the
25             sequence (many elements) into a single element.
26              
27             MARKDOWN
28             args => {
29             array => {
30             schema => ['array*', of=>'str*'],
31             pos => 0,
32             slurpy => 1,
33             cmdline_src => 'stdin_or_args',
34             },
35             min_range_len => {
36             schema => ['posint*', min=>2],
37             default => 4,
38             description => <<'MARKDOWN',
39              
40             Minimum number of items in a sequence to convert to a range. Sequence that has
41             less than this number of items will not be converted.
42              
43             MARKDOWN
44             },
45             max_range_len => {
46             schema => ['posint*',min=>2],
47             description => <<'MARKDOWN',
48              
49             Maximum number of items in a sequence to convert to a range. Sequence that has
50             more than this number of items might be split into two or more ranges.
51              
52             MARKDOWN
53             },
54             separator => {
55             schema => 'str*',
56             default => '..',
57             },
58             ignore_duplicates => {
59             schema => 'true*',
60             },
61             },
62             result_naked => 1,
63             examples => [
64             {
65             summary => 'basic, non-numbers ignored',
66             args => {
67             array => [100, 2, 3, 4, 5, 101, 'foo'],
68             },
69             result => [100, "2..5", 101, 'foo'],
70             },
71             {
72             summary => 'option: separator',
73             args => {
74             array => [100, 2, 3, 4, 5, 101],
75             separator => '-',
76             },
77             result => [100, "2-5", 101],
78             },
79             {
80             summary => 'multiple ranges, negative number',
81             args => {
82             array => [100, 2, 3, 4, 5, 6, 101, 102, -5, -4, -3, -2, 103],
83             },
84             result => [100, "2..6", 101, 102, "-5..-2", 103],
85             },
86             {
87             summary => 'option: min_range_len (1)',
88             args => {
89             array => [100, 2, 3, 4, 5, 101],
90             min_range_len => 5,
91             },
92             result => [100, 2, 3, 4, 5, 101],
93             },
94             {
95             summary => 'option: min_range_len (2)',
96             args => {
97             array => [100, 2, 3, 4, 101, 'foo'],
98             min_range_len => 3,
99             },
100             result => [100, "2..4", 101, 'foo'],
101             },
102             {
103             summary => 'option: ignore_duplicates',
104             args => {
105             array => [1, 2, 3, 4, 2, 9, 9, 9],
106             ignore_duplicates => 1,
107             },
108             result => ["1..4", 9],
109             },
110             {
111             summary => 'option: max_range_len (1)',
112             args => {
113             array => [98, 100..110, 5, 101],
114             max_range_len => 4,
115             },
116             result => [98, "100..103","104..107", 108, 109, 110, 5, 101],
117             },
118             ],
119             };
120             sub convert_number_sequence_to_range {
121 0     0 1   my %args = @_;
122              
123 0           my $array = $args{array};
124             my $min_range_len = $args{min_range_len}
125             // $args{threshold} # old name, DEPRECATED
126 0   0       // 4;
      0        
127 0           my $max_range_len = $args{max_range_len};
128 0 0 0       die "max_range_len must be >= min_range_len"
129             if defined($max_range_len) && $max_range_len < $min_range_len;
130 0   0       my $separator = $args{separator} // '..';
131 0           my $ignore_duplicates = $args{ignore_duplicates};
132              
133 0           my @res;
134             my @buf; # to hold possible sequence
135              
136             my $code_empty_buffer = sub {
137 0 0   0     return unless @buf;
138 0 0         push @res, @buf >= $min_range_len ? ("$buf[0]$separator$buf[-1]") : @buf;
139 0           @buf = ();
140 0           };
141              
142 0           my %seen;
143 0           for my $i (0..$#{$array}) {
  0            
144 0           my $el = $array->[$i];
145              
146 0 0 0       next if $ignore_duplicates && $seen{$el}++;
147              
148 0 0         unless ($el =~ /\A-?[0-9]+\z/) { # not an integer
149 0           $code_empty_buffer->();
150 0           push @res, $el;
151 0           next;
152             }
153 0 0         if (@buf) {
154 0 0         if ($el != $buf[-1]+1) { # breaks current sequence
155 0           $code_empty_buffer->();
156             }
157 0 0 0       if ($max_range_len && @buf >= $max_range_len) {
158 0           $code_empty_buffer->();
159             }
160             }
161 0           push @buf, $el;
162             }
163 0           $code_empty_buffer->();
164              
165 0           \@res;
166             }
167              
168             1;
169              
170             # ABSTRACT: Find sequences in number arrays & convert to range (e.g. 100,2,3,4,5,101 -> 100,"2..5",101)
171              
172             __END__