File Coverage

blib/lib/App/HL7/Compare.pm
Criterion Covered Total %
statement 145 145 100.0
branch 23 24 95.8
condition 9 9 100.0
subroutine 18 18 100.0
pod 2 2 100.0
total 197 198 99.4


line stmt bran cond sub pod time code
1             package App::HL7::Compare;
2             $App::HL7::Compare::VERSION = '0.004';
3 3     3   554523 use v5.10;
  3         12  
4 3     3   14 use strict;
  3         34  
  3         99  
5 3     3   12 use warnings;
  3         6  
  3         146  
6              
7 3     3   1899 use Moo;
  3         25826  
  3         19  
8 3     3   6492 use Mooish::AttributeBuilder -standard;
  3         6293  
  3         21  
9 3     3   1955 use App::HL7::Compare::Parser;
  3         10  
  3         148  
10 3     3   23 use Types::Standard qw(Tuple Str ScalarRef InstanceOf Bool HashRef);
  3         6  
  3         24  
11 3     3   11528 use List::Util qw(max);
  3         7  
  3         5268  
12              
13             has param 'files' => (
14             isa => Tuple [Str | ScalarRef, Str | ScalarRef],
15             );
16              
17             has param 'exclude_matching' => (
18             isa => Bool,
19             default => sub { 1 },
20             );
21              
22             has param 'message_opts' => (
23             isa => HashRef,
24             default => sub { {} },
25             );
26              
27             has field 'parser' => (
28             isa => InstanceOf ['App::HL7::Compare::Parser'],
29             default => sub { App::HL7::Compare::Parser->new },
30             );
31              
32             sub _compare_line
33             {
34 50     50   82 my ($self, $segment, $field, $component, $subcomponent, $message_num, $comps) = @_;
35              
36 50         133 my $name = sprintf "%s.%s", $segment->name, $segment->number;
37 50   100     114 my $order = $comps->{order}{$name} //= @{$comps->{segments}};
  14         37  
38 50         729 $comps->{segments}[$order]
39             [$field->number][$component->number][$subcomponent->number]
40             [$message_num] = $subcomponent->value;
41             }
42              
43             sub _gather_recursive
44             {
45 132     132   263 my ($self, $item, $levels_down) = @_;
46 132         172 $levels_down -= 1;
47              
48 132         167 my @results;
49 132         173 foreach my $subitem (@{$item->parts}) {
  132         2325  
50 170 100       35539 if ($levels_down == 0) {
51 50         134 push @results, [$subitem];
52             }
53             else {
54             push @results, map {
55 150         171 [$subitem, @{$_}]
  150         370  
56 120         166 } @{$self->_gather_recursive($subitem, $levels_down)};
  120         284  
57             }
58             }
59              
60 132         316 return \@results;
61             }
62              
63             sub _build_comparison_recursive
64             {
65 104     104   136 my ($self, $parts, $levels_down) = @_;
66 104         108 $levels_down -= 1;
67              
68 104 100       140 if ($levels_down == 0) {
69             return [
70             {
71             path => [],
72 33         42 value => [@{$parts}[0, 1]],
  33         154  
73             }
74             ];
75             }
76              
77 71         105 my @results;
78 71         73 foreach my $part_num (0 .. $#{$parts}) {
  71         117  
79 163         178 my $part = $parts->[$part_num];
80 163 100       240 next unless defined $part;
81              
82 90         153 my $deep_results = $self->_build_comparison_recursive($part, $levels_down);
83 90 100 100     95 if (@{$deep_results} == 1 && defined $deep_results->[0]{path}[0]) {
  90         234  
84             $deep_results->[0]{path}[0] = $part_num
85 51 100       90 if $deep_results->[0]{path}[0] == 1;
86 51         116 push @results, $deep_results->[0];
87             }
88             else {
89             push @results, map {
90 48         44 unshift @{$_->{path}}, $part_num;
  48         65  
91 48         102 $_
92 39         45 } @{$deep_results};
  39         49  
93             }
94             }
95              
96 71         115 return \@results;
97             }
98              
99             sub _build_comparison
100             {
101 6     6   13 my ($self, $comps) = @_;
102              
103 6         10 my %reverse_order = map { $comps->{order}{$_} => $_ } keys %{$comps->{order}};
  14         48  
  6         21  
104 6         12 my @results;
105              
106 6         9 foreach my $segment_num (0 .. $#{$comps->{segments}}) {
  6         14  
107 14         23 my $segment = $comps->{segments}[$segment_num];
108             push @results, {
109 14         38 segment => $reverse_order{$segment_num},
110             compared => $self->_build_comparison_recursive($segment, 4)
111             };
112             }
113              
114 6         25 return \@results;
115             }
116              
117             sub _compare_messages
118             {
119 6     6   176 my ($self, $message1, $message2) = @_;
120              
121 6         24 my %comps = (
122             order => {},
123             segments => [],
124             );
125              
126 6         10 my @parts;
127 6         17 foreach my $message ($message1, $message2) {
128 12         35 push @parts, $self->_gather_recursive($message, 4);
129             }
130              
131 6         11 while ('processing messages simultaneously') {
132 32         38 my $got_parts = 0;
133              
134 32         57 foreach my $message_num (0 .. $#parts) {
135 64         555 my $part = shift @{$parts[$message_num]};
  64         95  
136 64 100       105 next unless defined $part;
137              
138 50         56 $got_parts = 1;
139 50         49 $self->_compare_line(@{$part}, $message_num, \%comps);
  50         89  
140             }
141              
142 32 100       423 last unless $got_parts;
143             }
144              
145 6         21 return $self->_build_comparison(\%comps);
146             }
147              
148             sub _get_files
149             {
150 6     6   13 my ($self) = @_;
151              
152             my $slurp = sub {
153 4     4   5 my ($file) = @_;
154              
155 4 50       158 open my $fh, '<', $file
156             or die "couldn't open file $file: $!";
157              
158 4         14 local $/;
159 4         172 return readline $fh;
160 6         30 };
161              
162 6         45 my @files = @{$self->files};
  6         27  
163 6         57 foreach my $file (@files) {
164 12 100       37 if (ref $file eq 'SCALAR') {
165 8         11 $file = ${$file};
  8         20  
166             }
167             else {
168 4         6 $file = $slurp->($file);
169             }
170             }
171              
172 6         42 return @files;
173             }
174              
175             sub _remove_matching
176             {
177 6     6   13 my ($self, $compared) = @_;
178              
179 6 100       26 return unless $self->exclude_matching;
180              
181 5         7 foreach my $segment (@{$compared}) {
  5         28  
182 11         15 my @to_delete;
183              
184 11         17 foreach my $comp_num (0 .. $#{$segment->{compared}}) {
  11         23  
185 25         35 my $comp = $segment->{compared}[$comp_num];
186              
187 25         26 my @values = @{$comp->{value}};
  25         47  
188 25 100 100     31 if ((grep { defined } @values) == 2 && $values[0] eq $values[1]) {
  50         113  
189 8         19 push @to_delete, $comp_num;
190             }
191             }
192              
193 11         21 foreach my $comp_num (reverse @to_delete) {
194 8         10 splice @{$segment->{compared}}, $comp_num, 1;
  8         29  
195             }
196             }
197             }
198              
199             sub compare
200             {
201 6     6 1 167 my ($self) = @_;
202              
203             my $compared = $self->_compare_messages(
204             map {
205 6         31 $self->parser->parse($_, %{$self->message_opts})
  12         241  
  12         68  
206             } $self->_get_files
207             );
208 6         182 $self->_remove_matching($compared);
209              
210 6         15 return $compared;
211             }
212              
213             sub compare_stringify
214             {
215 4     4 1 229 my ($self) = @_;
216 4         12 my $compared = $self->compare;
217              
218 4         7 my @out;
219 4         8 my $longest = 0;
220 4         9 foreach my $segment (@{$compared}) {
  4         8  
221 12         15 my @stringified;
222 12         15 foreach my $comp (@{$segment->{compared}}) {
  12         32  
223             my $stringified = [
224 27         59 $segment->{segment} . join('', map { "[$_]" } @{$comp->{path}}) . ':',
  17         24  
225 17 100       24 map { defined $_ ? $_ : '(empty)' } @{$comp->{value}}
  34         64  
  17         25  
226             ];
227              
228 17         31 push @stringified, $stringified;
229             }
230              
231 12         25 $longest = max $longest, map { length $_->[0] } @stringified;
  17         33  
232 12         22 my $blueprint = "%-${longest}s %s => %s";
233 12         21 push @out, map { sprintf $blueprint, @{$_} } @stringified;
  17         21  
  17         50  
234             }
235              
236 4         40 return join "\n", @out;
237             }
238              
239             1;
240              
241             # ABSTRACT: compare two HL7 v2 messages against one another
242