File Coverage

blib/lib/NumSeq/Iter.pm
Criterion Covered Total %
statement 76 85 89.4
branch 42 56 75.0
condition 23 24 95.8
subroutine 7 8 87.5
pod 2 2 100.0
total 150 175 85.7


line stmt bran cond sub pod time code
1             package NumSeq::Iter;
2              
3 1     1   59046 use 5.010001;
  1         12  
4 1     1   4 use strict;
  1         2  
  1         29  
5 1     1   4 use warnings;
  1         1  
  1         22  
6              
7 1     1   3 use Exporter qw(import);
  1         2  
  1         704  
8              
9             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
10             our $DATE = '2022-01-21'; # DATE
11             our $DIST = 'NumSeq-Iter'; # DIST
12             our $VERSION = '0.005'; # VERSION
13              
14             our @EXPORT_OK = qw(numseq_iter numseq_parse);
15              
16             my $re_num = qr/(?:[+-]?[0-9]+(?:\.[0-9]+)?)/;
17              
18             sub _numseq_parse_or_iter {
19 27     27   37 my $which = shift;
20 27 50       55 my $opts = ref($_[0]) eq 'HASH' ? shift : {};
21 27         38 my $numseq = shift;
22              
23 27         33 my @nums;
24 27         219 while ($numseq =~ s/\A(\s*,\s*)?($re_num)//) {
25 66 100 100     204 die "Number sequence must not start with comma" if $1 && !@nums;
26 65         301 push @nums, $2;
27             }
28 26 100       67 die "Please specify one or more number in number sequence: '$numseq'" unless @nums;
29              
30 24         26 my $has_ellipsis = 0;
31 24 100       76 if ($numseq =~ s/\A\s*,\s*\.\.\.//) {
32 19 100       39 die "Please specify at least three number in number sequence before ellipsis" unless @nums >= 3;
33 18         24 $has_ellipsis++;
34             }
35              
36 23         26 my $last_num;
37 23 100       110 if ($numseq =~ s/\A\s*,\s*($re_num|[+-]?Inf)//) {
38 12         25 $last_num = $1;
39             }
40 23 100       70 die "Extraneous token in number sequence: $numseq, please only use 'a,b,c, ...' or 'a,b,c,...,z'" if length $numseq;
41              
42 19         27 my ($is_arithmetic, $is_geometric, $inc);
43             CHECK_SEQ_TYPE: {
44 19 100       32 last unless $has_ellipsis;
  19         25  
45              
46             CHECK_ARITHMETIC: {
47 16         17 my $inc0;
  16         15  
48 16         37 for (1..$#nums) {
49 32 100       65 if ($_ == 1) { $inc0 = $nums[1] - $nums[0] }
  16 100       30  
50             elsif ($inc0 != ($nums[$_] - $nums[$_-1])) {
51 7         13 last CHECK_ARITHMETIC;
52             }
53             }
54 9         14 $is_arithmetic++;
55 9         11 $inc = $inc0;
56 9         14 last CHECK_SEQ_TYPE;
57             }
58              
59             CHECK_GEOMETRIC: {
60 7 50       8 last if $nums[0] == 0;
  7         11  
61 7         8 my $inc0;
62 7         13 for (1..$#nums) {
63 14 100       52 if ($_ == 1) { $inc0 = $nums[1] / $nums[0] }
  7         13  
64             else {
65 7 50       15 last CHECK_GEOMETRIC if $nums[$_-1] == 0;
66 7 100       17 if ($inc0 != ($nums[$_] / $nums[$_-1])) {
67 1         3 last CHECK_GEOMETRIC;
68             }
69             }
70             }
71 6         11 $is_geometric++;
72 6         7 $inc = $inc0;
73 6         9 last CHECK_SEQ_TYPE;
74             }
75              
76 1         9 die "Can't determine the pattern from number sequence: ".join(", ", @nums);
77             }
78              
79 18 50       32 if ($which eq 'parse') {
80             return {
81 0 0       0 numbers => \@nums,
    0          
    0          
82             has_ellipsis => $has_ellipsis,
83             ($has_ellipsis ? (last_number => $last_num) : ()),
84             type => $is_arithmetic ? 'arithmetic' : ($is_geometric ? 'geometric' : 'itemized'),
85             inc => $inc,
86             };
87             }
88              
89 18         20 my $i = 0;
90 18         21 my $cur;
91             my $ends;
92             return sub {
93 99 50   99   332 return undef if $ends; ## no critic: Subroutines::ProhibitExplicitReturnUndef
94 99 100       248 return $nums[$i++] if $i <= $#nums;
95 99 100       66 if (!$has_ellipsis) { $ends++; return undef } ## no critic: Subroutines::ProhibitExplicitReturnUndef
  3         5  
  3         6  
96              
97 96   66     78 $cur //= $nums[-1];
98 96 100       64 if ($is_arithmetic) {
    50          
99 27         29 $cur += $inc;
100 27 100       39 if (defined $last_num) {
101 18 100 100     65 if ($inc >= 0 && $cur > $last_num || $inc < 0 && $cur < $last_num) {
      100        
      100        
102 5         7 $ends++;
103 5         9 return undef; ## no critic: Subroutines::ProhibitExplicitReturnUndef
104             }
105             }
106 22         34 return $cur;
107             } elsif ($is_geometric) {
108 17         21 $cur *= $inc;
109 17 100       22 if (defined $last_num) {
110 11 100 100     44 if ($inc >= 1 && $cur > $last_num || $inc < 1 && $cur < $last_num) {
      100        
      100        
111 4         5 $ends++;
112 4         7 return undef; ## no critic: Subroutines::ProhibitExplicitReturnUndef
113             }
114             }
115 13         19 return $cur;
116             }
117 18         112 };
118             }
119              
120             sub numseq_iter {
121 27     27 1 12252 _numseq_parse_or_iter('iter', @_);
122             }
123              
124             sub numseq_parse {
125 0     0 1   my $res;
126 0           eval {
127 0           $res = _numseq_parse_or_iter('parse', @_);
128             };
129 0 0         if ($@) {
130 0           my $errmsg = $@;
131 0           $errmsg =~ s/(.+) at .+/$1/s; # ux: remove file+line number information from error message
132 0           return [400, "Parse fail: $errmsg"];
133             }
134 0           [200, "OK", $res];
135             }
136              
137             1;
138             # ABSTRACT: Generate a coderef iterator from a number sequence specification (e.g. '1,3,5,...,101')
139              
140             __END__