File Coverage

blib/lib/Lingua/IdSplitter.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Lingua::IdSplitter;
2             # ABSTRACT: split identifiers into words
3             $Lingua::IdSplitter::VERSION = '0.01_1';
4 4     4   92914 use strict;
  4         11  
  4         163  
5 4     4   22 use warnings;
  4         10  
  4         117  
6              
7 4     4   5113 use Text::Aspell;
  0            
  0            
8             use LWP::Simple;
9             use String::CamelCase qw/decamelize/;
10             use File::ShareDir ':ALL';
11             use Data::Dumper;
12              
13             sub new {
14             my ($class, @dicts) = @_;
15             my $self = bless({}, $class);
16              
17             $self->{dicts} = [];
18             foreach (@dicts) {
19             if (ref($_) eq 'HASH') {
20             push @{$self->{dicts}}, $_;
21             }
22             if (ref($_) eq '') {
23             my $d = $self->_load_dict($_);
24             push @{$self->{dicts}}, $d;
25             }
26             }
27              
28             return $self;
29             }
30              
31             sub _load_dict {
32             my ($self, $name) = @_;
33             $name .= '.csv' unless ($name =~ m/\.csv$/);
34              
35             my $file;
36             $file = $name if (-e $name);
37             unless ($file) {
38             $file = "share/dictionaries/$name" if (-e "share/dictionaries/$name");
39             }
40             eval "require Lingua::IdSplitter;"; # XXX - be nice
41             unless ($file) {
42             $file = dist_file('Lingua-IdSplitter', "dictionaries/$name") unless $@;
43             }
44             unless ($file) {
45             print "$name not found";
46             exit;
47             }
48              
49             my $words = {};
50             open F, '<', $file;
51             while () {
52             chomp;
53             my ($left, $right) = split /\s*,\s*/, $_;
54             $words->{lc $left} = lc $right;
55             }
56              
57             return { weight=>0.6, words=>$words };
58             }
59              
60             sub soft_split {
61             my ($self, $id) = @_;
62             $self->{speller} = Text::Aspell->new;
63             $id = lc $id;
64             return () unless ($self->{speller} and $id);
65              
66             # test if the id is a single word or abbreviation
67             my $test = $self->_valid_word($id);
68             if ($test and $test->{w} > 0) {
69             push @{$self->{explain_rank}}, "$test->{t}(<-$test->{s}) ---> $test->{w}\n" if ($test->{w} ne $test->{s});
70             return ($test);
71             }
72              
73             # set initial values
74             $self->{full} = $id;
75             $self->{max} = length($id);
76             $self->{found} = {};
77             $self->{cand} = [];
78              
79             # create possible words for each level
80             my @chars = split //, $id;
81             my $i = 0;
82             while ($i < length($id)) {
83             $self->{found}->{$i} = [$self->_find_words(join('', @chars[$i .. length($id)-1]))];
84             $i++;
85             }
86              
87             # crete list of possible solutions
88             foreach (@{$self->{found}->{0}}) {
89             $self->_find_next(length($_->{s}), $_);
90             }
91              
92             # compute rank for each solution and sort by rank
93             my @rank;
94             foreach (@{$self->{cand}}) {
95             my $expr = $self->_calc_score($_);
96             my $score = eval $expr;
97             push @rank, {terms=>$_, expr=>$expr, score=>$score};
98             }
99             @rank = sort {$b->{score}<=>$a->{score}} @rank;
100             $self->{rank} = [@rank];
101              
102             my $top = shift @rank;
103             push @{$self->{explain_rank}}, $self->_explain_rank();
104              
105             return $top ? @{$top->{terms}} : ({s=>$self->{full},t=>$self->{full}});
106             }
107              
108             sub _find_words {
109             my ($self, $term) = @_;
110             my @res;
111              
112             my @chars = split //, $term;
113             my $left = '';
114             while (@chars) {
115             $left .= shift @chars;
116             push @res, $self->_valid_word($left) if ($self->_valid_word($left));
117             }
118              
119             return @res;
120             }
121              
122             sub _find_next {
123             my ($self, $lvl, @curr) = @_;
124              
125             if ($lvl < $self->{max}) {
126             foreach (@{$self->{found}->{$lvl}}) {
127             $self->_find_next($lvl+length($_->{s}), @curr, $_);
128             }
129             }
130             else {
131             my @strs = map {$_->{s}} @curr;
132             push @{$self->{cand}}, [@curr] if (join('', @strs) eq $self->{full});
133             }
134             }
135              
136             sub _calc_score {
137             my ($self,$cand) = @_;
138              
139             my @mul = ();
140             my $max_len = 0;
141             foreach (@$cand) {
142             push @mul, '('.$_->{w}.'*'.($_->{s}?length($_->{s}):0).')';
143             $max_len = length($_->{t}) if length($_->{t})>$max_len;
144             }
145             my $expr = '('.join('*', @mul).') * '.$max_len.' / ('.scalar(@$cand).'*'.scalar(@$cand).')';
146             #my $expr = '('.join('*', @mul).') / ('.scalar(@$cand).'*'.scalar(@$cand).')';
147              
148             return $expr;
149             }
150              
151             sub _valid_word {
152             my ($self, $word) = @_;
153              
154             foreach my $d (@{$self->{dicts}}) {
155             foreach my $w (keys %{$d->{words}}) {
156             my $o = $w;
157             $w =~ s#/##g;
158              
159             return {s=>$o,t=>$d->{words}->{$o},w=>$d->{weight}} if ($w eq $word);
160             }
161             }
162              
163             if ($self->{speller}->check($word)) {
164             return {s=>$word,t=>$word,w=>0.3};
165             }
166             else {
167             return undef;
168             }
169             }
170              
171             sub hard_split {
172             my ($self, $id) = @_;
173              
174             my @first;
175             if ($id =~ m/_/) {
176             $id =~ s/^_+//g;
177             $id =~ s/_+$//g;
178              
179             @first = split /_+/, $id;
180             push @{$self->{hard}}, {tech=>"'_' separator", terms=>[@first]};
181             }
182             push @first, $id unless @first;
183              
184             my @res;
185             foreach my $i (@first) {
186             if ( ($i =~ m/[A-Z][a-z0-9]+(.*?)[A-Z][a-z0-9]+/) or ($i =~ m/[a-z0-9]+(.*?)[A-Z]/) ) { # FIXME CamelCase detection
187             my @snd = split /_/, decamelize($i);
188             @snd = map {lc} @snd;
189             push @res, @snd;
190             push @{$self->{hard}}, {tech=>'CamelCase', terms=>[@res]};
191             }
192             else {
193             push @res, $i;
194             }
195             }
196              
197             my @final;
198             if (@res) {
199             push @final, {s=>$_, t=>$_} foreach @res;
200             }
201             else {
202             push @final, {s=>$id, t=>$id};
203             }
204             return @final;
205             }
206              
207             sub split {
208             my ($self, $id) = @_;
209              
210             # hard splits first
211             my @res = $self->hard_split($id);
212              
213             # soft splits second
214             my @final;
215             foreach (@res) {
216             push @final, $self->soft_split($_->{s});
217             }
218              
219             return @final;
220             }
221              
222             sub explain {
223             my ($self) = @_;
224             my $str;
225              
226             if ($self->{hard}) {
227             $str .= "\n## hard split\n";
228             foreach (@{$self->{hard}}) {
229             $str .= "Technique: $_->{tech}\n";
230             $str .= "Terms: ".join(',',@{$_->{terms}});
231             $str .= "\n";
232             }
233             }
234              
235             if ( $self->{explain_rank}) {
236             $str .= "\n## soft split rank(s):\n";
237             $str .= join("\n", @{$self->{explain_rank}});
238             }
239              
240             return $str;
241             }
242              
243             sub _explain_rank {
244             my ($self) = @_;
245              
246             my $r;
247             foreach (@{$self->{rank}}) {
248             my @parts;
249             foreach (@{$_->{terms}}) {
250             if ($_->{t} eq $_->{s}) {
251             push @parts, $_->{t};
252             }
253             else {
254             push @parts, "$_->{t}(<-$_->{s})";
255             }
256             }
257             $r .= join(',',@parts) . " ---> $_->{expr} = $_->{score}\n";
258             }
259              
260             return $r;
261             }
262              
263             1;
264              
265             __END__