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