File Coverage

blib/lib/Business/UDC/Parser.pm
Criterion Covered Total %
statement 227 241 94.1
branch 90 106 84.9
condition 21 24 87.5
subroutine 28 28 100.0
pod 0 1 0.0
total 366 400 91.5


line stmt bran cond sub pod time code
1             package Business::UDC::Parser;
2              
3 10     10   145086 use base qw(Exporter);
  10         22  
  10         1630  
4 10     10   68 use strict;
  10         31  
  10         305  
5 10     10   87 use warnings;
  10         19  
  10         856  
6              
7 10         351 use Business::UDC::Grammar qw(can_follow_operator can_follow_primary can_precede_number
8 10     10   5171 is_operator_token is_primary_token is_valid_operator);
  10         42  
9 10     10   7385 use Business::UDC::Tokenizer qw(tokenize);
  10         70  
  10         347  
10 10     10   787 use Error::Pure qw(err);
  10         24  
  10         562  
11 10     10   62 use List::Util 1.33 qw(any);
  10         196  
  10         618  
12 10     10   58 use Readonly;
  10         22  
  10         36905  
13              
14             Readonly::Array our @EXPORT_OK => qw(parse);
15              
16             our $VERSION = 0.08;
17              
18             sub parse {
19 84     84 0 404838 my $input = shift;
20              
21 84 100       277 if (! defined $input) {
22 3         19 err 'No input provided.';
23             }
24 81 100       415 if ($input !~ /\S/) {
25 2         8 err 'Empty input.';
26             }
27              
28 79         321 my $tokens = tokenize($input);
29 76         156 my $normalized_tokens = [];
30 76         125 foreach my $tok_hr (@{$tokens}) {
  76         185  
31 213         326 my %parse_tok = %{$tok_hr};
  213         1062  
32 213         633 _check_whitespace_token(\%parse_tok);
33 208         512 _check_apos_aux_tokens(\%parse_tok);
34 203         497 _check_aux_time_tokens(\%parse_tok);
35 200         530 _check_number_token(\%parse_tok);
36 198         524 _normalize_alpha_spec_token(\%parse_tok);
37 198         286 push @{$normalized_tokens}, \%parse_tok;
  198         602  
38             }
39 61         220 my $state = {
40             'tokens' => $normalized_tokens,
41             'pos' => 0,
42             };
43              
44 61         201 my $ast = _parse_expression($state);
45              
46 46 100       88 if ($state->{'pos'} < @{$state->{'tokens'}}) {
  46         132  
47 2         7 my $tok = $state->{'tokens'}[$state->{'pos'}];
48             err "Unexpected token '$tok->{'value'}'.",
49 2         11 'position' => $tok->{'pos'},
50             ;
51             }
52              
53             return {
54 44         440 'tokens' => $tokens,
55             'ast' => $ast,
56             };
57             }
58              
59             sub _check_whitespace_token {
60 213     213   397 my $tok = shift;
61              
62 213 100       521 if ($tok->{'type'} ne 'WHITESPACE') {
63 208         364 return;
64             }
65              
66             err "Whitespace is not allowed in UDC string.",
67             'position' => $tok->{'pos'},
68 5         31 'character' => substr($tok->{'value'}, 0, 1),
69             ;
70             }
71              
72             sub _check_apos_aux_tokens {
73 208     208   336 my $tok = shift;
74              
75 208 100       456 if ($tok->{'type'} ne 'APOS_AUX') {
76 190         344 return;
77             }
78 18 100       63 if (substr($tok->{'value'}, 0, 1) eq "'") {
79 13         23 return;
80             }
81              
82 5         22 my ($character) = $tok->{'value'} =~ /^('|.)/us;
83             err 'Bad apostrophe character.',
84             'character' => $character,
85 5         20 'position' => $tok->{'pos'},
86             ;
87              
88 0         0 return;
89             }
90              
91             sub _check_aux_time_tokens {
92 203     203   305 my $tok = shift;
93              
94 203 100       469 if ($tok->{'type'} ne 'AUX_TIME') {
95 198         332 return;
96             }
97              
98 5         17 my ($left, $left_length) = _time_quote_at_start($tok->{'value'});
99 5 100       16 if ($left ne '"') {
100             err 'Bad quotation mark character.',
101             'character' => $left,
102 2         7 'position' => $tok->{'pos'},
103             ;
104             }
105              
106 3         9 my ($right, $right_length) = _time_quote_at_end($tok->{'value'});
107 3 100       11 if ($right ne '"') {
108             err 'Bad quotation mark character.',
109             'character' => $right,
110 1         6 'position' => $tok->{'pos'} + length($tok->{'value'}) - $right_length,
111             ;
112             }
113              
114 2         5 return;
115             }
116              
117             sub _check_number_token {
118 200     200   318 my $tok = shift;
119              
120 200 100       498 if ($tok->{'type'} ne 'NUMBER') {
121 120         210 return;
122             }
123 80 100       378 if ($tok->{'value'} !~ /,/) {
124 78         137 return;
125             }
126              
127             err 'Bad dot character in number.',
128 2         15 'position' => $tok->{'pos'} + index($tok->{'value'}, ','),
129             'character' => ',',
130             ;
131             }
132              
133             sub _time_quote_at_start {
134 5     5   9 my $value = shift;
135              
136 5 100       21 if (substr($value, 0, 2) eq "''") {
137 1         6 return ("''", 2);
138             }
139              
140 4         13 return (substr($value, 0, 1), 1);
141             }
142              
143             sub _time_quote_at_end {
144 3     3   10 my $value = shift;
145              
146 3 100       13 if (substr($value, -2) eq "''") {
147 1         4 return ("''", 2);
148             }
149              
150 2         6 return (substr($value, -1), 1);
151             }
152              
153             sub _consume {
154 164     164   290 my $state = shift;
155              
156 164         460 return $state->{'tokens'}[$state->{'pos'}++];
157             }
158              
159             sub _expect {
160 4     4   10 my ($state, $type) = @_;
161              
162 4         9 my $tok = _peek($state);
163 4 50       12 if (! $tok) {
164 0         0 err "Expected '$type' but reached end of input.";
165             }
166 4 50       13 if ($tok->{'type'} ne $type) {
167             err "Expected $type but got $tok->{'type'} ('$tok->{'value'}').",
168 0         0 'position' => $tok->{'pos'},
169             ;
170             }
171              
172 4         9 return _consume($state);
173             }
174              
175             sub _parse_expression {
176 65     65   136 my $state = shift;
177              
178 65         182 my $left = _parse_term($state);
179 55         147 while (my $tok = _peek($state)) {
180 33 100       125 if (! is_operator_token($tok->{'type'})) {
181 6         14 last;
182             }
183 27 50       99 if (! is_valid_operator($tok->{'value'})) {
184 0         0 last;
185             }
186              
187 27         349 my $op = _consume($state);
188 27 100       88 my $next = _peek($state)
189             or err "Expected term after operator '$op->{'value'}'.";
190             err "Token '$next->{'value'}' is not allowed after operator '$op->{'value'}'."
191             if $next->{'type'} ne 'LBRACK'
192 26 100 100     133 && ! can_follow_operator($op->{'value'}, $next->{'type'});
193 24         92 my $right = _parse_term_after_operator($state, $op->{'value'});
194              
195 24 100 100     121 if ($op->{'value'} eq '/' && $right->{'type'} eq 'APOS_AUX') {
196 4         16 my ($base, $from) = _split_trailing_apos_aux($left);
197 4 100       11 if (! $from) {
198             err "Apostrophe auxiliary range shorthand '$op->{'value'}$right->{'value'}' ".
199             "requires apostrophe auxiliary on the left side.",
200              
201 2         13 'position' => $op->{'pos'},
202             ;
203             }
204              
205             $left = {
206             type => 'APOS_RANGE',
207             base => $base,
208             from => $from->{'value'},
209 2         16 to => $right->{'value'},
210             };
211              
212 2         11 next;
213             }
214              
215             $left = {
216             type => 'BINARY_OP',
217 20         101 operator => $tok->{'value'},
218             left => $left,
219             right => $right,
220             };
221              
222 20 100       88 if ($op->{'value'} eq '/') {
223 8         15 my @modifiers;
224 8         32 my $current_type = 'NUMBER';
225 8         19 my $current_value = undef;
226              
227 8         29 while (my $next_tok = _peek($state)) {
228 1 50       38 if (! can_follow_primary(
229             $next_tok->{'type'},
230             $next_tok->{'value'},
231             $current_type,
232             $current_value,
233             )) {
234 0         0 last;
235             }
236              
237             push @modifiers, {
238             type => $next_tok->{'type'},
239 1         8 value => $next_tok->{'value'},
240             };
241              
242 1         4 $current_type = $next_tok->{'type'};
243 1         2 $current_value = $next_tok->{'value'};
244              
245 1         3 _consume($state);
246             }
247 8 100       33 if (@modifiers) {
248 1         6 $left = {
249             type => 'TERM',
250             primary => $left,
251             modifiers => \@modifiers,
252             };
253             }
254             }
255             }
256              
257 50         112 return $left;
258             }
259              
260             sub _parse_primary {
261 83     83   159 my $state = shift;
262              
263 83 50       180 my $tok = _peek($state)
264             or err 'Expected term but reached end of input.';
265              
266 83 100       247 if ($tok->{'type'} eq 'LBRACK') {
267 4         12 return _parse_subgroup($state);
268             }
269              
270 79 100       296 if (is_primary_token($tok->{'type'})) {
271 70         1909 _consume($state);
272             return {
273             'type' => $tok->{'type'},
274 70         463 'value' => $tok->{'value'},
275             };
276             }
277              
278 9 100       277 if ($tok->{'type'} eq 'ALPHA_SPEC') {
279             err "Alphabetical specification cannot appear standalone.",
280             'position' => $tok->{'pos'},
281 3         22 'value' => $tok->{'value'},
282             ;
283             }
284              
285 6 100       18 if ($tok->{'type'} eq 'APOS_AUX') {
286 1         6 err "Apostrophe auxiliary '$tok->{'value'}' must follow a valid UDC notation.";
287             }
288              
289             err "Expected NUMBER, subgroup, or standalone auxiliary but got $tok->{'type'} ('$tok->{'value'}').",
290 5         28 'position' => $tok->{'pos'},
291             ;
292             }
293              
294             sub _parse_subgroup {
295 4     4   8 my $state = shift;
296              
297 4         9 my $lbrack = _expect($state, 'LBRACK');
298 4         14 my $expr = _parse_expression($state);
299              
300 4         11 my $end = _peek($state);
301 4 50       43 if (! $end) {
302             err "Unclosed subgroup '['.",
303 0         0 'position' => $lbrack->{'pos'},
304             ;
305             }
306 4 50       12 if ($end->{'type'} ne 'RBRACK') {
307             err "Expected closing ']' for subgroup but got '$end->{'value'}'.",
308 0         0 'position' => $end->{'pos'},
309             ;
310             }
311 4         13 _consume($state);
312              
313             return {
314 4         19 'type' => 'SUBGROUP',
315             'expression' => $expr,
316             };
317             }
318              
319             sub _parse_term {
320 82     82   130 my $state = shift;
321              
322 82         249 my $primary = _parse_primary($state);
323 73 100       261 if ($primary->{'type'} ne 'NUMBER') {
324 13         31 my $next = _peek($state);
325 13 100 100     68 if ($next && $next->{'type'} eq 'NUMBER') {
326 4 100       19 if (! can_precede_number($primary->{'type'}, $primary->{'value'})) {
327 1 50       6 my $what = defined $primary->{'value'} ? $primary->{'value'} : $primary->{'type'};
328 1         6 err "NUMBER cannot follow '$what'.";
329             }
330              
331 3         8 my $number = _consume($state);
332              
333 3         6 my @modifiers;
334 3         7 my $current_type = 'NUMBER';
335 3         6 my $current_value = $number->{'value'};
336 3         6 my $has_main_number = 1;
337 3         9 while (my $tok = _peek($state)) {
338 1 50   2   9 if (any { $tok->{'type'} eq $_ } qw(APOS_AUX AUX_DOT)) {
  2 50       9  
339 0 0       0 if (! $has_main_number) {
340 0         0 last;
341             }
342             } elsif (! can_follow_primary(
343             $tok->{'type'},
344             $tok->{'value'},
345             $current_type,
346             $current_value,
347             )) {
348 0         0 last;
349             }
350              
351             push @modifiers, {
352             'type' => $tok->{'type'},
353 1         10 'value' => $tok->{'value'},
354             };
355              
356 1         3 $current_type = $tok->{'type'};
357 1         2 $current_value = $tok->{'value'};
358              
359 1         5 _consume($state);
360             }
361              
362             return {
363             'type' => 'TERM',
364             'prefixes' => [
365             {
366             'type' => $primary->{'type'},
367             'value' => $primary->{'value'},
368             },
369             ],
370             'primary' => {
371             'type' => 'NUMBER',
372 3         39 'value' => $number->{'value'},
373             },
374             'modifiers' => \@modifiers,
375             };
376             }
377             }
378              
379 69         169 my @modifiers;
380 69         204 my $current_type = $primary->{'type'};
381 69         147 my $current_value = $primary->{'value'};
382 69     78   479 my $allow_dot_aux = any { $primary->{'type'} eq $_ } qw(NUMBER SUBGROUP);
  78         199  
383 69         304 while (my $tok = _peek($state)) {
384 81 100       217 if ($tok->{'type'} eq 'LBRACK') {
385 1         3 my $subgroup = _parse_primary($state);
386              
387 1         6 push @modifiers, $subgroup;
388              
389 1         3 $current_type = $subgroup->{'type'};
390 1         3 $current_value = undef;
391              
392 1         3 next;
393             }
394              
395 80 100       188 if ($tok->{'type'} eq 'AUX_DOT') {
396 4 100       14 if (! $allow_dot_aux) {
397 1         3 last;
398             }
399             }
400 79 100       270 if (! can_follow_primary(
401             $tok->{'type'},
402             $tok->{'value'},
403             $current_type,
404             $current_value,
405             )) {
406 32         95 last;
407             }
408              
409             push @modifiers, {
410             'type' => $tok->{'type'},
411 47         284 'value' => $tok->{'value'},
412             };
413              
414 47         96 $current_type = $tok->{'type'};
415 47         115 $current_value = $tok->{'value'};
416              
417 47         102 _consume($state);
418             }
419              
420             return {
421 69         420 'type' => 'TERM',
422             'primary' => $primary,
423             'modifiers' => \@modifiers,
424             };
425             }
426              
427             sub _parse_term_after_operator {
428 24     24   74 my ($state, $op) = @_;
429              
430 24 50       73 my $tok = _peek($state)
431             or err "Expected term after operator '$op'.";
432              
433 24 100 100     121 if ($op eq '/' && $tok->{'type'} eq 'AUX_DOT') {
434 2         7 _consume($state);
435             return {
436             type => 'PARTIAL_NUMBER',
437 2         9 value => $tok->{'value'},
438             };
439             }
440              
441 22 100 100     83 if ($op eq '/' && $tok->{'type'} eq 'FORM') {
442 1         4 _consume($state);
443             return {
444             type => 'PARTIAL_FORM',
445 1         5 value => $tok->{'value'},
446             };
447             }
448              
449 21 100 100     169 if ($op eq '/' && $tok->{'type'} eq 'APOS_AUX') {
450 4         14 _consume($state);
451             return {
452             type => 'APOS_AUX',
453 4         19 value => $tok->{'value'},
454             };
455             }
456              
457 17         73 return _parse_term($state);
458             }
459              
460             sub _peek {
461 362     362   598 my $state = shift;
462              
463 362         1223 return $state->{'tokens'}[$state->{'pos'}];
464             }
465              
466             sub _normalize_alpha_spec_token {
467 198     198   308 my $tok = shift;
468              
469 198 100       547 if ($tok->{'type'} ne 'ALPHA_SPEC') {
470 185         321 return;
471             }
472              
473 13         38 my ($value, $pos) = _trim_alpha_spec_value($tok);
474 13         28 $tok->{'pos'} = $pos;
475 13         76 $tok->{'value'} = $value;
476              
477 13         27 return;
478             }
479              
480             sub _trim_alpha_spec_value {
481 13     13   25 my $tok = shift;
482 13         30 my $value = $tok->{'value'};
483 13         30 my $pos = $tok->{'pos'};
484              
485 13 100       85 if ($value =~ s/^(\s+)//) {
486 3         12 $pos += length($1);
487             }
488 13         46 $value =~ s/\s+\z//;
489              
490 13         46 return ($value, $pos);
491             }
492              
493             sub _split_trailing_apos_aux {
494 4     4   7 my $node = shift;
495              
496 4 50       13 if (! defined $node) {
497 0         0 return;
498             }
499 4 100       37 if ($node->{'type'} ne 'TERM') {
500 1         4 return;
501             }
502 3 100 66     12 if (! $node->{'modifiers'} || ! @{$node->{'modifiers'}}) {
  3         13  
503 1         4 return;
504             }
505              
506 2         4 my @modifiers = @{$node->{'modifiers'}};
  2         7  
507 2         3 my $last = $modifiers[-1];
508 2 50 33     49 if (! defined $last || $last->{'type'} ne 'APOS_AUX') {
509 0         0 return;
510             }
511              
512 2         7 pop @modifiers;
513              
514             my $base = {
515             type => 'TERM',
516 2         12 primary => $node->{'primary'},
517             modifiers => \@modifiers,
518             };
519              
520 2 50       7 if (exists $node->{'prefixes'}) {
521 0         0 $base->{'prefixes'} = [ @{$node->{'prefixes'}} ];
  0         0  
522             }
523              
524 2         6 return ($base, $last);
525             }
526              
527             1;
528              
529             __END__