File Coverage

blib/lib/NumSeq/Iter.pm
Criterion Covered Total %
statement 91 100 91.0
branch 50 64 78.1
condition 23 26 88.4
subroutine 7 8 87.5
pod 2 2 100.0
total 173 200 86.5


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