File Coverage

blib/lib/Complete/Sequence.pm
Criterion Covered Total %
statement 77 87 88.5
branch 26 40 65.0
condition 6 11 54.5
subroutine 8 8 100.0
pod 1 1 100.0
total 118 147 80.2


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