File Coverage

blib/lib/String/Util/Range.pm
Criterion Covered Total %
statement 11 37 29.7
branch 0 14 0.0
condition 0 16 0.0
subroutine 4 6 66.6
pod 1 1 100.0
total 16 74 21.6


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