File Coverage

blib/lib/Business/UDC/Parser.pm
Criterion Covered Total %
statement 166 179 92.7
branch 66 82 80.4
condition 21 24 87.5
subroutine 20 20 100.0
pod 0 1 0.0
total 273 306 89.2


line stmt bran cond sub pod time code
1             package Business::UDC::Parser;
2              
3 9     9   106167 use base qw(Exporter);
  9         17  
  9         1153  
4 9     9   49 use strict;
  9         23  
  9         238  
5 9     9   32 use warnings;
  9         16  
  9         533  
6              
7 9         237 use Business::UDC::Grammar qw(can_follow_operator can_follow_primary can_precede_number
8 9     9   3894 is_operator_token is_primary_token is_valid_operator);
  9         29  
9 9     9   5568 use Business::UDC::Tokenizer qw(tokenize);
  9         24  
  9         203  
10 9     9   568 use Error::Pure qw(err);
  9         15  
  9         411  
11 9     9   44 use List::Util 1.33 qw(any);
  9         187  
  9         469  
12 9     9   77 use Readonly;
  9         16  
  9         17731  
13              
14             Readonly::Array our @EXPORT_OK => qw(parse);
15              
16             our $VERSION = 0.06;
17              
18             sub parse {
19 64     64 0 219540 my $input = shift;
20              
21 64 100       209 if (! defined $input) {
22 3         14 err 'No input provided.';
23             }
24 61 100       299 if ($input !~ /\S/) {
25 2         21 err 'Empty input.';
26             }
27              
28 59         245 my $tokens = tokenize($input);
29 55         222 my $state = {
30             'tokens' => $tokens,
31             'pos' => 0,
32             };
33              
34 55         140 my $ast = _parse_expression($state);
35              
36 40 100       70 if ($state->{'pos'} < @{$state->{'tokens'}}) {
  40         109  
37 2         5 my $tok = $state->{'tokens'}[$state->{'pos'}];
38             err "Unexpected token '$tok->{'value'}'.",
39 2         11 'position' => $tok->{'pos'},
40             ;
41             }
42              
43             return {
44 38         238 'tokens' => $tokens,
45             'ast' => $ast,
46             };
47             }
48              
49             sub _consume {
50 143     143   255 my $state = shift;
51              
52 143         367 return $state->{'tokens'}[$state->{'pos'}++];
53             }
54              
55             sub _expect {
56 4     4   7 my ($state, $type) = @_;
57              
58 4         5 my $tok = _peek($state);
59 4 50       7 if (! $tok) {
60 0         0 err "Expected '$type' but reached end of input.";
61             }
62 4 50       7 if ($tok->{'type'} ne $type) {
63             err "Expected $type but got $tok->{'type'} ('$tok->{'value'}').",
64 0         0 'position' => $tok->{'pos'},
65             ;
66             }
67              
68 4         55 return _consume($state);
69             }
70              
71             sub _parse_expression {
72 59     59   107 my $state = shift;
73              
74 59         142 my $left = _parse_term($state);
75 49         111 while (my $tok = _peek($state)) {
76 30 100       91 if (! is_operator_token($tok->{'type'})) {
77 6         9 last;
78             }
79 24 50       67 if (! is_valid_operator($tok->{'value'})) {
80 0         0 last;
81             }
82              
83 24         251 my $op = _consume($state);
84 24 100       44 my $next = _peek($state)
85             or err "Expected term after operator '$op->{'value'}'.";
86             err "Token '$next->{'value'}' is not allowed after operator '$op->{'value'}'."
87             if $next->{'type'} ne 'LBRACK'
88 23 100 100     97 && ! can_follow_operator($op->{'value'}, $next->{'type'});
89 21         68 my $right = _parse_term_after_operator($state, $op->{'value'});
90              
91 21 100 100     98 if ($op->{'value'} eq '/' && $right->{'type'} eq 'APOS_AUX') {
92 4         10 my ($base, $from) = _split_trailing_apos_aux($left);
93 4 100       10 if (! $from) {
94             err "Apostrophe auxiliary range shorthand '$op->{'value'}$right->{'value'}' ".
95             "requires apostrophe auxiliary on the left side.",
96              
97 2         12 'position' => $op->{'pos'},
98             ;
99             }
100              
101             $left = {
102             type => 'APOS_RANGE',
103             base => $base,
104             from => $from->{'value'},
105 2         9 to => $right->{'value'},
106             };
107              
108 2         5 next;
109             }
110              
111             $left = {
112             type => 'BINARY_OP',
113 17         78 operator => $tok->{'value'},
114             left => $left,
115             right => $right,
116             };
117              
118 17 100       51 if ($op->{'value'} eq '/') {
119 7         14 my @modifiers;
120 7         12 my $current_type = 'NUMBER';
121 7         15 my $current_value = undef;
122              
123 7         17 while (my $next_tok = _peek($state)) {
124 1 50       6 if (! can_follow_primary(
125             $next_tok->{'type'},
126             $next_tok->{'value'},
127             $current_type,
128             $current_value,
129             )) {
130 0         0 last;
131             }
132              
133             push @modifiers, {
134             type => $next_tok->{'type'},
135 1         10 value => $next_tok->{'value'},
136             };
137              
138 1         4 $current_type = $next_tok->{'type'};
139 1         2 $current_value = $next_tok->{'value'};
140              
141 1         5 _consume($state);
142             }
143 7 100       29 if (@modifiers) {
144 1         7 $left = {
145             type => 'TERM',
146             primary => $left,
147             modifiers => \@modifiers,
148             };
149             }
150             }
151             }
152              
153 44         95 return $left;
154             }
155              
156             sub _parse_primary {
157 74     74   123 my $state = shift;
158              
159 74 50       160 my $tok = _peek($state)
160             or err 'Expected term but reached end of input.';
161              
162 74 100       176 if ($tok->{'type'} eq 'LBRACK') {
163 4         7 return _parse_subgroup($state);
164             }
165              
166 70 100       222 if (is_primary_token($tok->{'type'})) {
167 61         1473 _consume($state);
168             return {
169             'type' => $tok->{'type'},
170 61         353 'value' => $tok->{'value'},
171             };
172             }
173              
174 9 100       177 if ($tok->{'type'} eq 'ALPHA_SPEC') {
175             err "Alphabetical specification cannot appear standalone.",
176             'position' => $tok->{'pos'},
177 3         18 'value' => $tok->{'value'},
178             ;
179             }
180              
181 6 100       13 if ($tok->{'type'} eq 'APOS_AUX') {
182 1         7 err "Apostrophe auxiliary '$tok->{'value'}' must follow a valid UDC notation.";
183             }
184              
185             err "Expected NUMBER, subgroup, or standalone auxiliary but got $tok->{'type'} ('$tok->{'value'}').",
186 5         17 'position' => $tok->{'pos'},
187             ;
188             }
189              
190             sub _parse_subgroup {
191 4     4   5 my $state = shift;
192              
193 4         12 my $lbrack = _expect($state, 'LBRACK');
194 4         8 my $expr = _parse_expression($state);
195              
196 4         5 my $end = _peek($state);
197 4 50       7 if (! $end) {
198             err "Unclosed subgroup '['.",
199 0         0 'position' => $lbrack->{'pos'},
200             ;
201             }
202 4 50       15 if ($end->{'type'} ne 'RBRACK') {
203             err "Expected closing ']' for subgroup but got '$end->{'value'}'.",
204 0         0 'position' => $end->{'pos'},
205             ;
206             }
207 4         17 _consume($state);
208              
209             return {
210 4         9 'type' => 'SUBGROUP',
211             'expression' => $expr,
212             };
213             }
214              
215             sub _parse_term {
216 73     73   111 my $state = shift;
217              
218 73         187 my $primary = _parse_primary($state);
219 64 100       172 if ($primary->{'type'} ne 'NUMBER') {
220 13         25 my $next = _peek($state);
221 13 100 100     52 if ($next && $next->{'type'} eq 'NUMBER') {
222 4 100       15 if (! can_precede_number($primary->{'type'}, $primary->{'value'})) {
223 1 50       3 my $what = defined $primary->{'value'} ? $primary->{'value'} : $primary->{'type'};
224 1         4 err "NUMBER cannot follow '$what'.";
225             }
226              
227 3         8 my $number = _consume($state);
228              
229 3         7 my @modifiers;
230 3         60 my $current_type = 'NUMBER';
231 3         9 my $current_value = $number->{'value'};
232 3         6 my $has_main_number = 1;
233 3         7 while (my $tok = _peek($state)) {
234 1 50   2   3 if (any { $tok->{'type'} eq $_ } qw(APOS_AUX AUX_DOT)) {
  2 50       6  
235 0 0       0 if (! $has_main_number) {
236 0         0 last;
237             }
238             } elsif (! can_follow_primary(
239             $tok->{'type'},
240             $tok->{'value'},
241             $current_type,
242             $current_value,
243             )) {
244 0         0 last;
245             }
246              
247             push @modifiers, {
248             'type' => $tok->{'type'},
249 1         5 'value' => $tok->{'value'},
250             };
251              
252 1         2 $current_type = $tok->{'type'};
253 1         1 $current_value = $tok->{'value'};
254              
255 1         17 _consume($state);
256             }
257              
258             return {
259             'type' => 'TERM',
260             'prefixes' => [
261             {
262             'type' => $primary->{'type'},
263             'value' => $primary->{'value'},
264             },
265             ],
266             'primary' => {
267             'type' => 'NUMBER',
268 3         34 'value' => $number->{'value'},
269             },
270             'modifiers' => \@modifiers,
271             };
272             }
273             }
274              
275 60         105 my @modifiers;
276 60         98 my $current_type = $primary->{'type'};
277 60         109 my $current_value = $primary->{'value'};
278 60     69   418 my $allow_dot_aux = any { $primary->{'type'} eq $_ } qw(NUMBER SUBGROUP);
  69         178  
279 60         237 while (my $tok = _peek($state)) {
280 69 100       172 if ($tok->{'type'} eq 'LBRACK') {
281 1         2 my $subgroup = _parse_primary($state);
282              
283 1         2 push @modifiers, $subgroup;
284              
285 1         2 $current_type = $subgroup->{'type'};
286 1         2 $current_value = undef;
287              
288 1         2 next;
289             }
290              
291 68 100       131 if ($tok->{'type'} eq 'AUX_DOT') {
292 4 100       14 if (! $allow_dot_aux) {
293 1         3 last;
294             }
295             }
296 67 100       197 if (! can_follow_primary(
297             $tok->{'type'},
298             $tok->{'value'},
299             $current_type,
300             $current_value,
301             )) {
302 29         66 last;
303             }
304              
305             push @modifiers, {
306             'type' => $tok->{'type'},
307 38         195 'value' => $tok->{'value'},
308             };
309              
310 38         79 $current_type = $tok->{'type'};
311 38         67 $current_value = $tok->{'value'};
312              
313 38         86 _consume($state);
314             }
315              
316             return {
317 60         305 'type' => 'TERM',
318             'primary' => $primary,
319             'modifiers' => \@modifiers,
320             };
321             }
322              
323             sub _parse_term_after_operator {
324 21     21   42 my ($state, $op) = @_;
325              
326 21 50       38 my $tok = _peek($state)
327             or err "Expected term after operator '$op'.";
328              
329 21 100 100     79 if ($op eq '/' && $tok->{'type'} eq 'AUX_DOT') {
330 2         8 _consume($state);
331             return {
332             type => 'PARTIAL_NUMBER',
333 2         10 value => $tok->{'value'},
334             };
335             }
336              
337 19 100 100     66 if ($op eq '/' && $tok->{'type'} eq 'FORM') {
338 1         5 _consume($state);
339             return {
340             type => 'PARTIAL_FORM',
341 1         6 value => $tok->{'value'},
342             };
343             }
344              
345 18 100 100     97 if ($op eq '/' && $tok->{'type'} eq 'APOS_AUX') {
346 4         11 _consume($state);
347             return {
348             type => 'APOS_AUX',
349 4         15 value => $tok->{'value'},
350             };
351             }
352              
353 14         56 return _parse_term($state);
354             }
355              
356             sub _peek {
357 319     319   445 my $state = shift;
358              
359 319         995 return $state->{'tokens'}[$state->{'pos'}];
360             }
361              
362             sub _split_trailing_apos_aux {
363 4     4   7 my $node = shift;
364              
365 4 50       9 if (! defined $node) {
366 0         0 return;
367             }
368 4 100       11 if ($node->{'type'} ne 'TERM') {
369 1         4 return;
370             }
371 3 100 66     28 if (! $node->{'modifiers'} || ! @{$node->{'modifiers'}}) {
  3         11  
372 1         31 return;
373             }
374              
375 2         3 my @modifiers = @{$node->{'modifiers'}};
  2         4  
376 2         3 my $last = $modifiers[-1];
377 2 50 33     10 if (! defined $last || $last->{'type'} ne 'APOS_AUX') {
378 0         0 return;
379             }
380              
381 2         2 pop @modifiers;
382              
383             my $base = {
384             type => 'TERM',
385 2         5 primary => $node->{'primary'},
386             modifiers => \@modifiers,
387             };
388              
389 2 50       3 if (exists $node->{'prefixes'}) {
390 0         0 $base->{'prefixes'} = [ @{$node->{'prefixes'}} ];
  0         0  
391             }
392              
393 2         5 return ($base, $last);
394             }
395              
396             1;
397              
398             __END__