File Coverage

blib/lib/Text/Difference.pm
Criterion Covered Total %
statement 73 85 85.8
branch 20 26 76.9
condition 3 3 100.0
subroutine 7 7 100.0
pod 1 1 100.0
total 104 122 85.2


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