File Coverage

blib/lib/Complete/Sequence.pm
Criterion Covered Total %
statement 63 71 88.7
branch 20 26 76.9
condition 6 11 54.5
subroutine 8 8 100.0
pod 1 1 100.0
total 98 117 83.7


line stmt bran cond sub pod time code
1             package Complete::Sequence;
2              
3             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
4             our $DATE = '2019-12-15'; # DATE
5             our $DIST = 'Complete-Sequence'; # DIST
6             our $VERSION = '0.001'; # VERSION
7              
8 1     1   58092 use 5.010001;
  1         11  
9 1     1   6 use strict;
  1         1  
  1         19  
10 1     1   4 use warnings;
  1         2  
  1         36  
11 1     1   1371 use Log::ger;
  1         43  
  1         4  
12              
13 1     1   619 use Complete::Common qw(:all);
  1         332  
  1         125  
14              
15 1     1   6 use Exporter qw(import);
  1         2  
  1         624  
16             our @EXPORT_OK = qw(
17             complete_sequence
18             );
19              
20             our %SPEC;
21              
22             sub _get_strings_from_item {
23 20     20   33 my ($item) = @_;
24              
25 20         23 my @array;
26 20         24 my $ref = ref $item;
27 20 100       61 if (!$ref) {
    100          
    100          
    50          
28 1         3 push @array, $item;
29             } elsif ($ref eq 'ARRAY') {
30 15         24 push @array, @$item;
31             } elsif ($ref eq 'CODE') {
32 1         3 push @array, _get_strings_from_item( $item->() );
33             } elsif ($ref eq 'HASH') {
34 3 100 33     9 if (defined $item->{alternative}) {
    50          
35 2         4 push @array, map { _get_strings_from_item($_) }
36 1         2 @{ $item->{alternative} };
  1         3  
37 2         6 } elsif (defined $item->{sequence} && @{ $item->{sequence} }) {
38 3         7 my @set = map { [_get_strings_from_item($_)] }
39 2         3 @{ $item->{sequence} };
  2         4  
40             #use DD; dd \@set;
41             # sigh, this module is quite fussy. it won't accept
42 2 100       7 if (@set > 1) {
    50          
43 1         457 require Set::CrossProduct;
44 1         1922 my $scp = Set::CrossProduct->new(\@set);
45 1         40 while (my $tuple = $scp->get) {
46 4         178 push @array, join("", @$tuple);
47             }
48             } elsif (@set == 1) {
49 1         2 push @array, @{ $set[0] };
  1         2  
50             }
51             } else {
52 0         0 die "Need alternative or sequence";
53             }
54             } else {
55 0         0 die "Invalid item: $item";
56             }
57 20         52 @array;
58             }
59              
60             $SPEC{complete_sequence} = {
61             v => 1.1,
62             summary => 'Complete string from a sequence of choices',
63             description => <<'_',
64              
65             Sometime you want to complete a string where its parts (sequence items) are
66             formed from various pieces. For example, suppose your program "delete-user-data"
67             accepts an argument that is in the form of:
68              
69             USERNAME
70             UID "(" "current" ")"
71             UID "(" "historical" ")"
72              
73             "EVERYONE"
74              
75             Supposed existing users include `budi`, `ujang`, and `wati` with UID 101, 102,
76             103.
77              
78             This can be written as:
79              
80             [
81             {
82             alternative => [
83             [qw/budi ujang wati/],
84             {sequence => [
85             [qw/101 102 103/],
86             ["(current)", "(historical)"],
87             ]},
88             "EVERYONE",
89             ],
90             }
91             ]
92              
93             When word is empty (`''`), the offered completion is:
94              
95             budi
96             ujang
97             wati
98              
99             101
100             102
101             103
102              
103             EVERYONE
104              
105             When word is `101`, the offered completion is:
106              
107             101
108             101(current)
109             101(historical)
110              
111             When word is `101(h`, the offered completion is:
112              
113             101(historical)
114              
115             _
116             args => {
117             %arg_word,
118             sequence => {
119             schema => 'array*',
120             req => 1,
121             description => <<'_',
122              
123             A sequence structure is an array of items. An item can be:
124              
125             * a scalar/string (a single string to choose from)
126              
127             * an array of strings (multiple strings to choose from)
128              
129             * a coderef (will be called to extract an item)
130              
131             * a hash (another sequence or alternative of items)
132              
133             If you want to specify another sub-sequence of items:
134              
135             {sequence => [ ... ]} # put items in here
136              
137             If you want to specify an alternative of sub-sequences or sub-alternative:
138              
139             {alternative => [ ... ]} # put items in here
140              
141             _
142             },
143             },
144             result_naked => 1,
145             result => {
146             schema => 'array',
147             },
148             };
149             sub complete_sequence {
150 9     9 1 607 require Complete::Util;
151              
152 9         3705 my %args = @_;
153              
154 9   50     25 my $word = $args{word} // "";
155 9         13 my $sequence = $args{sequence};
156              
157 9         9 my $orig_word = $word;
158 9         13 my @prefixes_from_completed_items;
159              
160 9         16 for my $item (@$sequence) {
161 14         23 my @array = _get_strings_from_item($item);
162 14         32 my $res = Complete::Util::complete_array_elem(
163             word => $word,
164             array => \@array,
165             );
166 14 100 66     2236 if ($res && @$res == 1) {
    100 66        
167             # the word can be completed directly (unambiguously) with this item.
168             # move on to get more words from the next item.
169 5         8 push @prefixes_from_completed_items, $res->[0];
170 5         10 substr($word, 0, length $res->[0]) = "";
171 5         10 next;
172             } elsif ($res && @$res > 1) {
173             # the word can be completed with several choices from this item.
174             # present the choices as the final answer.
175 8         13 return [map { join("", @prefixes_from_completed_items, $_) } @$res];
  22         91  
176             } else {
177             # the word cannot be completed with this item. it can be that the
178             # word already contains this item and the next.
179 1         2 my $num_matches = 0;
180 1         2 my $matching_str;
181 1         2 for my $str (@array) {
182             # XXX perhaps we want to be case-insensitive?
183 2 50       6 if (index($word, $str) == 0) {
184 0         0 $num_matches++;
185 0         0 $matching_str = $str;
186             }
187             }
188 1 50       4 if ($num_matches == 1) {
189 0         0 substr($word, 0, length($matching_str)) = "";
190 0         0 push @prefixes_from_completed_items, $matching_str;
191 0         0 next;
192             }
193              
194             # nope, this word simply doesn't match
195 1         11 goto RETURN;
196             }
197             }
198              
199             RETURN:
200 1 50       2 if (@prefixes_from_completed_items) {
201 0         0 return [join("", @prefixes_from_completed_items)];
202             } else {
203 1         5 return [];
204             }
205              
206             }
207              
208             1;
209             # ABSTRACT: Complete string from a sequence of choices
210              
211             __END__