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