File Coverage

blib/lib/Lingua/Word/Parser.pm
Criterion Covered Total %
statement 64 80 80.0
branch 8 22 36.3
condition 5 9 55.5
subroutine 12 13 92.3
pod 2 2 100.0
total 91 126 72.2


line stmt bran cond sub pod time code
1             package Lingua::Word::Parser;
2             our $AUTHORITY = 'cpan:GENE';
3              
4             # ABSTRACT: Parse a word into scored known and unknown parts
5              
6 2     2   826611 use strict;
  2         3  
  2         87  
7 2     2   9 use warnings;
  2         4  
  2         181  
8              
9             our $VERSION = '0.0809';
10              
11 2     2   1172 use Bit::Vector ();
  2         2652  
  2         73  
12 2     2   4214 use DBI ();
  2         112989  
  2         124  
13 2     2   1494 use List::PowerSet qw(powerset_lazy);
  2         1241  
  2         242  
14 2     2   1063 use IO::File ();
  2         34773  
  2         88  
15              
16 2     2   8816 use Memoize qw(memoize);
  2         5795  
  2         9249  
17             memoize('_does_not_overlap');
18             memoize('power');
19             memoize('_reconstruct');
20             memoize('_grouping');
21             memoize('score');
22             memoize('score_parts');
23             memoize('_rle');
24             memoize('_or_together');
25              
26              
27              
28             sub new {
29 2     2 1 1310 my $class = shift;
30 2         5 my %args = @_;
31             my $self = {
32             file => $args{file},
33             dbhost => $args{dbhost} || 'localhost',
34             dbtype => $args{dbtype} || 'mysql',
35             dbname => $args{dbname},
36             dbuser => $args{dbuser},
37             dbpass => $args{dbpass},
38             lex => $args{lex},
39             word => $args{word},
40 2   50     26 known => {},
      50        
41             masks => {},
42             combos => [],
43             score => {},
44             };
45 2         4 bless $self, $class;
46 2         7 $self->_init(%args);
47 2         6 return $self;
48             }
49             sub _init {
50 2     2   4 my ($self, %args) = @_;
51              
52             # Set the length of our word.
53 2         7 $self->{wlen} = length $self->{word};
54              
55             # Set lex if given data.
56 2 100 66     46 if ( $self->{file} && -e $self->{file} ) {
    50          
57 1         4 $self->_fetch_lex;
58             }
59             elsif( $self->{dbname} )
60             {
61 0         0 $self->_db_fetch;
62             }
63             }
64              
65             sub _fetch_lex {
66 1     1   7 my $self = shift;
67              
68 1         2 my $i = 0;
69              
70             # Open the given file for reading...
71 1         8 my $fh = IO::File->new;
72 1 50       48 $fh->open( "< $self->{file}" ) or die "Can't read file: '$self->{file}'";
73 1         123 for ( <$fh> ) {
74 9         11 $i++;
75             # Split space-separated entries.
76 9         24 chomp;
77 9         17 my ($re, $defn) = split /\s+/, $_, 2;
78             # Add the entry to the lexicon.
79 9         267 $self->{lex}{$i} = { defn => $defn, re => qr/$re/ };
80             }
81 1         11 $fh->close;
82              
83 1         23 return $self->{lex};
84             }
85              
86             sub _db_fetch {
87 0     0   0 my $self = shift;
88              
89 0         0 my $dsn;
90 0 0       0 if (lc($self->{dbtype}) eq 'sqlite') {
91 0         0 $dsn = "DBI:$self->{dbtype}:dbname=$self->{dbname};$self->{dbhost}";
92             }
93             else {
94 0         0 $dsn = "DBI:$self->{dbtype}:$self->{dbname};$self->{dbhost}";
95             }
96              
97 0 0       0 my $dbh = DBI->connect( $dsn, $self->{dbuser}, $self->{dbpass}, { RaiseError => 1, AutoCommit => 1 } )
98             or die "Unable to connect to $self->{dbname}: $DBI::errstr\n";
99              
100 0         0 my $sql = 'SELECT id, affix, definition FROM fragment';
101              
102 0         0 my $sth = $dbh->prepare($sql);
103 0 0       0 $sth->execute or die "Unable to execute '$sql': $DBI::errstr\n";
104              
105 0         0 while( my @row = $sth->fetchrow_array ) {
106 0         0 my ($id, $part, $defn) = @row;
107 0         0 $self->{lex}{$id} = { re => qr/$part/, defn => $defn };
108             }
109 0 0       0 die "Fetch terminated early: $DBI::errstr\n" if $DBI::errstr;
110              
111 0 0       0 $sth->finish or die "Unable to finish '$sql': $DBI::errstr\n";
112              
113 0 0       0 $dbh->disconnect or die "Unable to disconnect from $self->{dbname}: $DBI::errstr\n";
114             }
115              
116              
117             sub knowns {
118 1     1 1 873 my $self = shift;
119              
120             # The identifier for the known and masks lists.
121 1         2 my $id = 0;
122              
123 1         2 for my $i (values %{ $self->{lex} }) {
  1         4  
124 9         35 while ($self->{word} =~ /$i->{re}/g) {
125             # Match positions.
126 10         22 my ($m, $n) = ($-[0], $+[0]);
127             # Get matched word-part.
128 10         18 my $part = substr $self->{word}, $m, $n - $m;
129              
130             # Create the part-of-word bitmask.
131 10         14 my $mask = 0 x $m; # Before known
132 10   50     15 $mask .= 1 x (($n - $m) || 1); # Known part
133 10         15 $mask .= 0 x ($self->{wlen} - $n); # After known
134              
135             # Output our progress.
136             # warn sprintf "%s %s - %s, %s (%d %d), %s\n",
137             # $mask,
138             # $i->{re},
139             # substr($self->{word}, 0, $m),
140             # $part,
141             # $m,
142             # $n - 1,
143             # substr($self->{word}, $n),
144             # ;
145              
146             # Save the known as a member of a list keyed by starting position.
147             $self->{known}{$id} = {
148             part => $part,
149             span => [$m, $n - 1],
150             defn => $i->{defn},
151 10         29 mask => $mask,
152             };
153              
154             # Save the relationship between mask and id.
155 10         40 $self->{masks}{$mask} = $id++;
156             }
157             }
158              
159 1         3 return $self->{known};
160             }
161              
162              
163             sub power {
164             my $self = shift;
165              
166             # Get a new powerset generator.
167             my $power = powerset_lazy(sort keys %{ $self->{masks} });
168              
169             # Consider each member of the powerset.. to save or skip?
170             while (my $collection = $power->()) {
171             # warn "C: @$collection\n";
172              
173             # Save this collection if it has only one item.
174             if (1 == @$collection) {
175             # warn "\t\tE: only 1 mask\n";
176             push @{ $self->{combos} }, $collection;
177             next;
178             }
179              
180             # Compare each mask against the others.
181             LOOP: for my $i (0 .. @$collection - 1) {
182              
183             # Set the comparison mask.
184             my $compare = $collection->[$i];
185              
186             for my $j ($i + 1 .. @$collection - 1) {
187              
188             # Set the current mask.
189             my $mask = $collection->[$j];
190             # warn "\tP:$compare v $mask\n";
191              
192             # Skip this collection if an overlap is found.
193             if (not $self->_does_not_overlap($compare, $mask)) {
194             # warn "\t\tO:$compare v $mask\n";
195             last LOOP;
196             }
197              
198             # Save this collection if we made it to the last pair.
199             if ($i == @$collection - 2 && $j == @$collection - 1) {
200             # warn "\t\tE:$compare v $mask\n";
201             push @{ $self->{combos} }, $collection;
202             }
203             }
204             }
205             }
206              
207             # Hand back the "non-overlapping powerset."
208             return $self->{combos};
209             }
210              
211              
212             sub score {
213             my $self = shift;
214             my ( $open_separator, $close_separator ) = @_;
215              
216             my $parts = $self->score_parts( $open_separator, $close_separator );
217              
218             for my $mask ( keys %$parts ) {
219             my $familiarity = sprintf "%.2f chunks / %.2f chars", @{ $self->_familiarity($mask) };
220              
221             for my $element ( @{ $parts->{$mask} } ) {
222             my $score = sprintf "%d:%d chunks / %d:%d chars",
223             $element->{score}{knowns}, $element->{score}{unknowns},
224             $element->{score}{knownc}, $element->{score}{unknownc};
225              
226             my $part = join ', ', @{ $element->{partition} };
227              
228             my $defn = join ', ', @{ $element->{definition} };
229              
230             push @{ $self->{score}{$mask} }, {
231             score => $score,
232             familiarity => $familiarity,
233             partition => $part,
234             definition => $defn,
235             };
236             }
237             }
238              
239             return $self->{score};
240             }
241              
242             sub _familiarity {
243 609     609   1062 my ( $self, $mask ) = @_;
244              
245 609         3348 my @chunks = grep { $_ ne "" } split /(0+)/, $mask;
  2826         5243  
246              
247             # Figure out how many chars are only 1s and
248             # Figure out how many chunks are made up of 1s:
249 609         1062 my $char_1s = 0;
250 609         677 my $chunk_1s = 0;
251 609         936 for my $chunk (@chunks) {
252 2577 100       4346 $char_1s += $chunk =~ /0/ ? 0 : length($chunk);
253 2577 100       4430 $chunk_1s += $chunk =~ /0/ ? 0 : 1;
254             }
255              
256 609         4165 return [ $chunk_1s / @chunks, $char_1s / length($mask) ];
257             }
258              
259              
260             sub score_parts {
261             my $self = shift;
262             my ( $open_separator, $close_separator, $line_terminator ) = @_;
263              
264             $line_terminator = '' unless defined $line_terminator;
265              
266             # Visit each combination...
267             my $i = 0;
268             for my $c (@{ $self->{combos} }) {
269             $i++;
270             my $together = $self->_or_together(@$c);
271              
272             # Breakdown knowns vs unknowns and knowncharacters vs unknowncharacters.
273             my %count = (
274             knowns => 0,
275             unknowns => 0,
276             knownc => 0,
277             unknownc => 0,
278             );
279              
280             for my $x ( reverse sort @$c ) {
281             # Run-length encode an "un-digitized" string.
282             my $y = _rle($x);
283             my ( $knowns, $unknowns, $knownc, $unknownc ) = _grouping($y);
284             # Accumulate the counters!
285             $count{knowns} += $knowns;
286             $count{unknowns} += $unknowns;
287             $count{knownc} += $knownc;
288             $count{unknownc} += $unknownc;
289             }
290              
291             my ( $s, $m ) = _reconstruct( $self->{word}, $c, $open_separator, $close_separator );
292              
293             my $defn = [];
294             for my $i ( @$m )
295             {
296             for my $j ( keys %{ $self->{known} } )
297             {
298             push @$defn, $self->{known}{$j}{defn} if $self->{known}{$j}{mask} eq $i;
299             }
300             }
301              
302             push @{ $self->{score_parts}{$together} }, {
303             score => \%count,
304             partition => $s,
305             definition => $defn,
306             familiarity => $self->_familiarity($together),
307             };
308             }
309              
310             return $self->{score_parts};
311             }
312              
313             sub _grouping {
314             my $scored = shift;
315             my @groups = $scored =~ /([ku]\d+)/g;
316             my ( $knowns, $unknowns ) = ( 0, 0 );
317             my ( $knownc, $unknownc ) = ( 0, 0 );
318             for ( @groups ) {
319             if ( /k(\d+)/ ) {
320             $knowns++;
321             $knownc += $1;
322             }
323             if ( /u(\d+)/ ) {
324             $unknowns++;
325             $unknownc += $1;
326             }
327             }
328             return $knowns, $unknowns, $knownc, $unknownc;
329             }
330              
331             sub _rle {
332             my $scored = shift;
333             # Run-length encode an "un-digitized" string.
334             $scored =~ s/1/k/g; # Undigitize
335             $scored =~ s/0/u/g; # "
336             # Count contiguous chars.
337             $scored =~ s/(.)\1*/$1 . length(substr($scored, $-[0], $+[0]-$-[0]))/ge;
338             return $scored;
339             }
340              
341             sub _does_not_overlap {
342             my $self = shift;
343              
344             # Get our masks to check.
345             my ($mask, $check) = @_;
346              
347             # Create the bitstrings to compare.
348             my $bitmask = Bit::Vector->new_Bin($self->{wlen}, $mask);
349             my $orclone = Bit::Vector->new_Bin($self->{wlen}, $check);
350             my $xorclone = Bit::Vector->new_Bin($self->{wlen}, $check);
351              
352             # Compute or and xor for the strings.
353             $orclone->Or($bitmask, $orclone);
354             $xorclone->Xor($bitmask, $xorclone);
355              
356             # Return the "or & xor equivalent sibling."
357             return $xorclone->equal($orclone) ? $orclone->to_Bin : 0;
358             }
359              
360             sub _or_together {
361             my $self = shift;
362              
363             # Get our masks to score.
364             my @masks = @_;
365              
366             # Initialize the bitmask to return, to zero.
367             my $result = Bit::Vector->new_Bin($self->{wlen}, (0 x $self->{wlen}));
368              
369             for my $mask (@masks) {
370             # Create the bitstrings to compare.
371             my $bitmask = Bit::Vector->new_Bin($self->{wlen}, $mask);
372              
373             # Get the union of the bit strings.
374             $result->Or($result, $bitmask);
375             }
376              
377             # Return the "or sum."
378             return $result->to_Bin;
379             }
380              
381             sub _reconstruct {
382             my ( $word, $masks, $open_separator, $close_separator ) = @_;
383              
384             $open_separator = '<' unless defined $open_separator;
385             $close_separator = '>' unless defined $close_separator;
386              
387             my $strings = [];
388             my $my_masks = [];
389              
390             for my $mask (reverse sort @$masks) {
391             my $i = 0;
392             my $last = 0;
393             my $string = '';
394             for my $m ( split //, $mask ) {
395             if ( $m ) {
396             $string .= $open_separator unless $last;
397             $string .= substr( $word, $i, 1 );
398             $last = 1;
399             }
400             else {
401             $string .= $close_separator if $last;
402             $string .= substr( $word, $i, 1 );
403             $last = 0;
404             }
405             $i++;
406             }
407             $string .= $close_separator if $last;
408             push @$strings, $string;
409             push @$my_masks, $mask;
410             }
411              
412             return $strings, $my_masks;
413             }
414              
415             1;
416              
417             __END__