| 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__ |