File Coverage

blib/lib/Text/Difference.pm
Criterion Covered Total %
statement 75 87 86.2
branch 24 30 80.0
condition 3 3 100.0
subroutine 7 7 100.0
pod 1 1 100.0
total 110 128 85.9


line stmt bran cond sub pod time code
1             package Text::Difference;
2              
3 6     6   147589 use strict;
  6         13  
  6         149  
4 6     6   30 use warnings;
  6         14  
  6         156  
5              
6 6     6   6010 use Data::Dumper;
  6         61698  
  6         388  
7 6     6   5135 use Moose;
  6         2938197  
  6         40  
8 6     6   48352 use namespace::autoclean;
  6         51223  
  6         30  
9              
10             $Data::Dumper::Sortkeys = 1;
11              
12             =head1 NAME
13              
14             Text::Difference - Compare two strings and find which tokens are actually different, with optional stopwords.
15              
16             =head1 VERSION
17              
18             Version 0.01
19              
20             =cut
21              
22             our $VERSION = '0.01';
23              
24             $VERSION = eval $VERSION;
25              
26             =head1 SYNOPSIS
27              
28             Compare two strings to check what tokens (words) are actually different, if any.
29              
30             use Text::Difference;
31              
32             my $diff = Text::Difference->new(
33             a => 'big blue car',
34             b => 'yellow car in small',
35             stopwords => [ 'in' ],
36             tokens => {
37             colour => [ 'blue', 'yellow' ],
38             size => [ 'big', 'small' ],
39             }
40             );
41              
42             $diff->check;
43              
44             $diff->match; # true
45              
46             $diff->a_tokens_remaining; # {}
47              
48             $diff->a_tokens_matched; # {}
49            
50             =cut
51              
52             has debug => ( is => 'rw', isa => 'Bool', default => 0 );
53              
54             has a => ( is => 'rw', isa => 'Str' );
55             has b => ( is => 'rw', isa => 'Str' );
56              
57             has _stopwords => (
58             is => 'rw',
59             isa => 'ArrayRef[Str]',
60             init_arg => 'stopwords',
61             traits => [ 'Array' ],
62             default => sub { [] },
63             handles => {
64             stopwords => 'elements',
65             },
66             );
67              
68             has _tokens => (
69             is => 'rw',
70             isa => 'HashRef',
71             init_arg => 'tokens',
72             traits => [ 'Hash' ],
73             default => sub { {} },
74             handles => {
75             tokens => 'keys',
76             get_token => 'get',
77             },
78             );
79              
80             has match => ( is => 'rw', isa => 'Bool', default => 0 );
81              
82             has a_tokens_remaining => ( is => 'rw', isa => 'HashRef', clearer => '_clear_a_tokens_remaining' );
83             has b_tokens_remaining => ( is => 'rw', isa => 'HashRef', clearer => '_clear_b_tokens_remaining' );
84              
85             has a_tokens_matched => ( is => 'rw', isa => 'HashRef', clearer => '_clear_a_tokens_matched' );
86             has b_tokens_matched => ( is => 'rw', isa => 'HashRef', clearer => '_clear_b_tokens_matched' );
87              
88             =head1 DESCRIPTION
89              
90             =cut
91              
92             =head1 METHODS
93              
94             =head2 Instance Methods
95              
96             =head3 check
97              
98             $diff->check
99              
100             =cut
101              
102             sub check
103             {
104 16     16 1 3549 my $self = shift;
105              
106             #########
107             # reset #
108             #########
109              
110 16         761 $self->match( 0 );
111              
112 16         834 $self->_clear_a_tokens_remaining;
113 16         757 $self->_clear_b_tokens_remaining;
114              
115 16         797 $self->_clear_a_tokens_matched;
116 16         737 $self->_clear_b_tokens_matched;
117              
118             ##################
119             # perform checks #
120             ##################
121              
122 16         49 $self->_has_all_same_words;
123              
124 16         77 return $self;
125             }
126              
127             sub _has_all_same_words
128             {
129 16     16   26 my $self = shift;
130              
131             #####################
132             # take a local copy #
133             #####################
134              
135 16         548 my $string_a = lc( $self->a );
136 16         556 my $string_b = lc( $self->b );
137              
138             ############################################
139             # strip out/replace stopwords (or symbols) #
140             ############################################
141              
142 16         769 foreach my $word ( map { quotemeta $_ } $self->stopwords )
  5         22  
143             {
144 5         78 $string_a =~ s/$word/ /g;
145 5         51 $string_b =~ s/$word/ /g;
146             }
147              
148             ###################
149             # condense spaces #
150             ###################
151              
152 16         84 $string_a =~ s/\s+/ /g;
153 16         65 $string_b =~ s/\s+/ /g;
154              
155             ###############
156             # trim spaces #
157             ###############
158              
159 16         38 $string_a =~ s/^\s+//;
160 16         46 $string_a =~ s/\s+$//;
161              
162 16         36 $string_b =~ s/^\s+//;
163 16         39 $string_b =~ s/\s+$//;
164              
165 16 50       584 print "ORIGINAL A : " . $string_a . "\n" if $self->debug;
166 16 50       561 print "ORIGINAL B : " . $string_b . "\n" if $self->debug;
167              
168             ###############################
169             # extract any supplied tokens #
170             ###############################
171            
172 16 100       720 if ( $self->tokens )
173             {
174 3         6 my %a_tokens_matched = ();
175 3         4 my %b_tokens_matched = ();
176              
177 3         122 foreach my $category ( $self->tokens )
178             {
179 4 50       165 if ( ref $self->get_token( $category ) eq 'ARRAY' )
180             {
181             # order by the tokens with more spaces in
182              
183 4         5 foreach my $token ( sort { scalar( () = $b =~ /\s/g ) <=> scalar( () = $a =~ /\s/g ) } @{ $self->get_token( $category ) } )
  22         74  
  4         164  
184             {
185 16         26 my $quoted_token = quotemeta $token;
186              
187 16         99 my $a_substitutions = $string_a =~ s/$quoted_token//ig;
188              
189 16 100       36 $a_tokens_matched{ $category }->{ $token } = $a_substitutions if $a_substitutions;
190              
191 16         87 my $b_substitutions = $string_b =~ s/$quoted_token//ig;
192              
193 16 100       59 $b_tokens_matched{ $category }->{ $token } = $b_substitutions if $b_substitutions;
194             }
195             }
196             }
197              
198 3         124 $self->a_tokens_matched( \%a_tokens_matched );
199 3         108 $self->b_tokens_matched( \%b_tokens_matched );
200              
201 3 50       98 if ( $self->debug )
202             {
203 0         0 print "A TOKENS MATCHED:\n";
204            
205 0         0 foreach my $category ( sort keys %a_tokens_matched )
206             {
207 0         0 print "\t" . $category . ":\n";
208              
209 0         0 foreach my $token ( sort keys %{ $a_tokens_matched{ $category } } )
  0         0  
210             {
211 0         0 print "\t\t" . $token . " = " . $a_tokens_matched{ $category }->{ $token } . "\n";
212             }
213             }
214              
215 0         0 print "B TOKENS MATCHED:\n";
216            
217 0         0 foreach my $category ( sort keys %b_tokens_matched )
218             {
219 0         0 print "\t" . $category . ":\n";
220              
221 0         0 foreach my $token ( sort keys %{ $b_tokens_matched{ $category } } )
  0         0  
222             {
223 0         0 print "\t\t" . $token . " = " . $b_tokens_matched{ $category }->{ $token } . "\n";
224             }
225             }
226             }
227             }
228              
229             ####################
230             # get the a tokens #
231             ####################
232              
233 16         40 my %a_tokens = ();
234              
235 16         56 foreach my $token ( split(' ', $string_a ) )
236             {
237 34 100       109 $a_tokens{ $token } = 0 if ! exists $a_tokens{ $token };
238 34         61 $a_tokens{ $token } ++;
239             }
240              
241             ########################
242             # get the right tokens #
243             ########################
244              
245 16         34 my %b_tokens = ();
246              
247 16         49 foreach my $token ( split(' ', $string_b ) )
248             {
249 32 100       106 $b_tokens{ $token } = 0 if ! exists $b_tokens{ $token };
250 32         66 $b_tokens{ $token } ++;
251             }
252              
253             ############################
254             # filer out the duplicates #
255             ############################
256              
257 16         54 foreach my $a_token ( keys %a_tokens )
258             {
259 30 100       76 if ( exists $b_tokens{ $a_token } )
260             {
261 27         38 $a_tokens{ $a_token } --;
262 27         33 $b_tokens{ $a_token } --;
263              
264 27 100       75 delete $a_tokens{ $a_token } if $a_tokens{ $a_token } == 0;
265 27 100       84 delete $b_tokens{ $a_token } if $b_tokens{ $a_token } == 0;
266             }
267             }
268              
269 16 50       647 print "REMAINING A : " . join( ' ', keys %a_tokens ) . "\n" if $self->debug;
270 16 50       637 print "REMAINING B : " . join( ' ', keys %b_tokens ) . "\n" if $self->debug;
271              
272             #############################
273             # evaluate what's remaining #
274             #############################
275              
276 16 100 100     100 if ( keys %a_tokens == 0 && keys %b_tokens == 0 )
277             {
278 10         348 $self->match( 1 );
279             }
280              
281 16         641 $self->a_tokens_remaining( \%a_tokens );
282              
283 16         612 $self->b_tokens_remaining( \%b_tokens );
284              
285 16         50 return $self;
286             }
287              
288             __PACKAGE__->meta->make_immutable;
289              
290             1;