File Coverage

blib/lib/Lingua/EN/Sentence/Offsets.pm
Criterion Covered Total %
statement 140 155 90.3
branch 25 34 73.5
condition 3 9 33.3
subroutine 16 19 84.2
pod 10 10 100.0
total 194 227 85.4


line stmt bran cond sub pod time code
1 5     5   241399 use strict; use warnings;
  5     5   10  
  5         249  
  5         31  
  5         8  
  5         483  
2             package Lingua::EN::Sentence::Offsets;
3             $Lingua::EN::Sentence::Offsets::VERSION = '0.03';
4             require Exporter;
5              
6             #ABSTRACT: Finds sentence boundaries, and returns their offsets.
7              
8             my ($EOS,$AP,$P,$PAP,@ABBREVIATIONS);
9 5     5   28 use Carp qw/cluck/;
  5         11  
  5         797  
10 5     5   29 use feature qw/say/;
  5         10  
  5         787  
11 5     5   5689 use utf8::all;
  5         394712  
  5         40  
12 5     5   30969 use Data::Dump qw/dump/;
  5         57665  
  5         510  
13              
14 5     5   119 use base 'Exporter';
  5         11  
  5         21714  
15             our @EXPORT_OK = qw/
16             get_sentences
17             get_offsets
18             add_acronyms
19             get_acronyms
20             set_acronyms
21             initial_offsets
22             offsets2sentences
23             remove_false_eos
24             adjust_offsets
25             split_unsplit_stuff
26             /;
27              
28              
29             $EOS="\001";$P = q/[\.!?]/;$AP = q/(?:'|"|»|\)|\]|\})?/;$PAP = $P.$AP;
30              
31             my @PEOPLE = ( 'jr', 'mr', 'mrs', 'ms', 'dr', 'prof', 'sr', "sens?", "reps?", 'gov',
32             "attys?", 'supt', 'det', 'rev' );
33              
34              
35             my @ARMY = ( 'col','gen', 'lt', 'cmdr', 'adm', 'capt', 'sgt', 'cpl', 'maj' );
36             my @INSTITUTES = ( 'dept', 'univ', 'assn', 'bros' );
37             my @COMPANIES = ( 'inc', 'ltd', 'co', 'corp' );
38             my @PLACES = ( 'arc', 'al', 'ave', "blv?d", 'cl', 'ct', 'cres', 'dr', "expy?",
39             'dist', 'mt', 'ft',
40             "fw?y", "hwa?y", 'la', "pde?", 'pl', 'plz', 'rd', 'st', 'tce',
41             'Ala' , 'Ariz', 'Ark', 'Cal', 'Calif', 'Col', 'Colo', 'Conn',
42             'Del', 'Fed' , 'Fla', 'Ga', 'Ida', 'Id', 'Ill', 'Ind', 'Ia',
43             'Kan', 'Kans', 'Ken', 'Ky' , 'La', 'Me', 'Md', 'Is', 'Mass',
44             'Mich', 'Minn', 'Miss', 'Mo', 'Mont', 'Neb', 'Nebr' , 'Nev',
45             'Mex', 'Okla', 'Ok', 'Ore', 'Penna', 'Penn', 'Pa' , 'Dak',
46             'Tenn', 'Tex', 'Ut', 'Vt', 'Va', 'Wash', 'Wis', 'Wisc', 'Wy',
47             'Wyo', 'USAFA', 'Alta' , 'Man', 'Ont', 'Qué', 'Sask', 'Yuk');
48             my @MONTHS = ('jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec','sept');
49             my @MISC = ( 'vs', 'etc', 'no', 'esp' );
50              
51             @ABBREVIATIONS = (@PEOPLE, @ARMY, @INSTITUTES, @COMPANIES, @PLACES, @MONTHS, @MISC );
52              
53              
54             sub get_offsets {
55 5     5 1 14 my ($text) = @_;
56 5 50       32 return [] unless defined $text;
57 5         36 my $offsets = initial_offsets($text);
58 5         26 $offsets = remove_false_eos($text,$offsets);
59 5         49 $offsets = split_unsplit_stuff($text,$offsets);
60 5         27 $offsets = adjust_offsets($text,$offsets);
61 5         20 return $offsets;
62             }
63              
64              
65              
66             sub get_sentences {
67 5     5 1 68519 my ($text) = @_;
68 5         29 my $offsets = get_offsets($text);
69 5         56 my $sentences = offsets2sentences($text,$offsets);
70 5         44 return $sentences;
71             }
72              
73              
74             sub add_acronyms {
75 0     0 1 0 push @ABBREVIATIONS, @_;
76             }
77              
78              
79              
80             sub get_acronyms {
81 0     0 1 0 return @ABBREVIATIONS;
82             }
83              
84              
85             sub set_acronyms {
86 0     0 1 0 @ABBREVIATIONS=@_;
87             }
88              
89              
90             sub remove_false_eos {
91 5     5 1 74 my ($text,$offsets) = @_;
92 5         12 my $size = @$offsets;
93 5         24 my $new_offsets = [ sort { $a->[0] <=> $b->[0] } @$offsets ];
  203         337  
94 5         26 for(my $i=0; $i<$size-1; $i++){
95 93         226 my $start = $new_offsets->[$i][0];
96 93         145 my $end = $new_offsets->[$i][1];
97 93         125 my $length = $end-$start;
98 93         230 my $s = substr($text,$start,$length);
99 93         152 my $j=$i+1;
100              
101 93         119 my $unsplit = 0;
102 93 100       15477 $unsplit = 1 if $s =~ /(?:[^-\w]|^)\w$PAP\s$/s;
103 93 100       692 $unsplit = 1 if $s =~ /[^-\w]\w$P$/s;
104              
105             # don't split after a white-space followed by a single letter followed
106             # by a dot followed by another whitespace.
107 93 100       326 $unsplit = 1 if $s =~ /\s\w\.\s+$/;
108              
109             # fix: bla bla... yada yada
110 93         311 my $t = substr($text,$offsets->[$j][0], $offsets->[$j][1]-$offsets->[$j][0]);
111 93 50 33     338 $unsplit = 1 if $s =~ /\.\.\.\s*$/s and $t =~ /^\s*[[:lower:]]/s;
112              
113             # fix "." "?" "!"
114 93 50       472 $unsplit = 1 if $s =~ m{['"]$P['"]\s+$}s;
115              
116             ## fix where abbreviations exist
117 93 100       197 foreach (@ABBREVIATIONS){ $unsplit = 1 if $s =~ /\b$_$PAP\s$/is; }
  11904         359855  
118              
119             # don't break after quote unless its a capital letter.
120 93 50 33     418 $unsplit = 1 if $s =~ /["']\s*$/s and $t =~ /^\s*[[:lower:]]/s;
121              
122             # don't break: text . . some more text.
123 93 50 33     327 $unsplit = 1 if $s =~ /\s\.\s$/s and $t =~ /^\s*/s;
124              
125 93 50       634 $unsplit = 1 if $s =~ /\s$PAP\s$/s;
126              
127 93 100       574 _merge_forward($new_offsets,$i) if $unsplit;
128             }
129 5         79 $new_offsets = [ grep { defined } @$new_offsets ];
  98         157  
130 5         28 return $new_offsets;
131              
132             #for(my $i=$size-1; $i>=0; $i--){ splice @$offsets, $i,1 unless defined($offsets->[$i]); }
133             }
134              
135             sub _merge_forward {
136 19     19   89 my ($offsets,$i) = @_;
137 19         72 my $j = $i+1;
138 19 50       76 return $offsets unless defined($offsets->[$j]);
139              
140 19         49 $offsets->[$j][0] = $offsets->[$i][0];
141 19         156 delete $offsets->[$i];
142              
143             #splice @$offsets, $i, 1;
144             }
145              
146              
147             sub split_unsplit_stuff {
148 5     5 1 16 my ($text,$offsets) = @_;
149 5         14 my $size = @$offsets;
150 5         28 for(my $i=0; $i<$size; $i++){
151 79         123 my $start = $offsets->[$i][0];
152 79         120 my $length = $offsets->[$i][1]-$start;
153 79         153 my $s = substr($text,$start,$length);
154              
155 79         110 my $split_points = [];
156 79         1864 while($s =~ /((?:\D|^)\d+$P)(\s+)/g){
157 7         25 my $end = $+[1];
158 7         19 my $begin = $-[2];
159 7         179 push @$split_points,[$start+$end,$start+$begin];
160             }
161 79         565 while($s =~ /($PAP\s)(\s*\()/g){
162 0         0 my $end = $+[1];
163 0         0 my $begin = $-[2];
164 0         0 push @$split_points,[$start+$end,$start+$begin];
165             }
166 79         351 while($s =~ /('\w$P)(\s)/g){
167 0         0 my $end = $+[1];
168 0         0 my $begin = $-[2];
169 0         0 push @$split_points,[$start+$end,$start+$begin];
170             }
171 79         202 while($s =~ /(\sno\.)(\s+)(?!\d)/g){
172 0         0 my $end = $+[1];
173 0         0 my $begin = $-[2];
174 0         0 push @$split_points,[$start+$end,$start+$begin];
175             }
176 79         189 while($s =~ /([ap]\.m\.\s+)([[:upper:]])/g){
177 0         0 my $end = $+[1];
178 0         0 my $begin = $-[2];
179 0         0 push @$split_points,[$start+$end,$start+$begin];
180             }
181              
182 79 100       283 _split_sentence($offsets,$i, [ sort { $a->[0] <=> $b->[0] } @$split_points ]) if @$split_points;
  2         17  
183             }
184 5         19 return $offsets;
185             }
186              
187              
188              
189             #sub _split_sentence {
190             # my ($offsets,$i,$end1,$start2) = @_;
191             # my $end2 = $offsets->[$i][1];
192             # $offsets->[$i][1] = $end1;
193             # $start2 //= $end1;
194             # push $offsets, [$start2, $end2];
195             #}
196              
197              
198             sub _split_sentence {
199 27     27   50 my ($offsets,$i,$split_points) = @_;
200 27         34 my ($end,$start) = @{shift @$split_points};
  27         60  
201 27         56 my $last = $offsets->[$i][1];
202 27         38 $offsets->[$i][1] = $end;
203 27         323 while(my $p = shift @$split_points){
204 73         416 push @$offsets, [$start,$p->[0]];
205 73         170 $start = $p->[1];
206             }
207 27         167 push @$offsets, [$start, $last];
208             }
209              
210              
211             sub adjust_offsets {
212 5     5 1 14 my ($text,$offsets) = @_;
213 5         12 my $size = @$offsets;
214 5         26 for(my $i=0; $i<$size; $i++){
215 86         125 my $start = $offsets->[$i][0];
216 86         102 my $end = $offsets->[$i][1];
217 86         100 my $length = $end - $start;
218 86         152 my $s = substr($text,$start,$length);
219 86 100       261 if ($s !~ /\w+/){
220 1         4 delete $offsets->[$i];
221 1         6 next;
222             }
223 85         1269 $s =~ /^(\s*).*?(\s*)$/s;
224 85 50       199 if(defined($1)){ $start += length($1); }
  85         127  
225 85 50       171 if(defined($2)){ $end -= length($2); }
  85         122  
226 85         331 $offsets->[$i] = [$start, $end];
227             }
228 5         15 my $new_offsets = [ grep { defined } @$offsets ];
  85         133  
229 5         19 return $new_offsets;
230             #for(my $i=$size-1; $i>=0; $i--){ splice @$offsets, $i,1 unless defined($offsets->[$i]); }
231             }
232              
233              
234             sub initial_offsets {
235 5     5 1 17 my ($text) = @_;
236 5         10 my $end;
237 5         16 my $text_end = length($text);
238 5         22 my $offsets = [[0,$text_end]];
239              
240 5         293 my @patterns = (
241             qr{(\n\s*\n)},
242             qr{$PAP\s()},
243             qr{\s\w$P()}
244             );
245              
246 5         19 my $split = 1;
247 5         24 while($split){
248 10         96 $split = 0;
249 10         19 foreach my $pat (@patterns){
250 30         41 my $size = @$offsets;
251 30         93 for(my $i=0; $i<$size; $i++){
252 398         866 my $start = $offsets->[$i][0];
253 398         619 my $length = $offsets->[$i][1]-$start;
254 398         931 my $s = substr($text,$start,$length);
255              
256 398         902 my $split_points = [];
257 398         8018 while($s =~ /(?
258 93         242 my $end = $-[1];
259 93         177 my $begin = $+[1];
260 93         7556 push @$split_points,[$start+$end,$start+$begin];
261 93         548 $split = 1;
262             }
263              
264 398 100       2608 _split_sentence($offsets,$i,[ sort { $a->[0] <=> $b->[0] } @$split_points ]) if @$split_points;
  109         183  
265             }
266             }
267             }
268 5         43 return $offsets;
269             }
270              
271              
272             sub offsets2sentences {
273 5     5 1 12 my ($text, $offsets) = @_;
274 5         12 my $sentences = [];
275 5         39 foreach my $o ( sort {$a->[0] <=> $b->[0]} @$offsets) {
  132         158  
276 85         92 my $start = $o->[0];
277 85         101 my $length = $o->[1]-$o->[0];
278 85         189 push @$sentences, substr($text,$start,$length);
279             }
280 5         16 return $sentences;
281             }
282              
283             1;
284              
285             __END__