File Coverage

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


line stmt bran cond sub pod time code
1 4     4   209230 use strict; use warnings;
  4     4   10  
  4         252  
  4         25  
  4         8  
  4         333  
2             package Lingua::EN::Sentence::Offsets;
3             {
4             $Lingua::EN::Sentence::Offsets::VERSION = '0.01_05';
5             }
6             require Exporter;
7              
8             #ABSTRACT: Finds sentence boundaries, and returns their offsets.
9              
10             my ($EOS,$AP,$P,$PAP,@ABBREVIATIONS);
11 4     4   24 use Carp qw/cluck/;
  4         6  
  4         238  
12 4     4   26 use feature qw/say/;
  4         5  
  4         639  
13 4     4   3648 use utf8::all;
  4         292714  
  4         32  
14 4     4   19762 use Data::Dump qw/dump/;
  4         44547  
  4         781  
15              
16 4     4   48 use base 'Exporter';
  4         7  
  4         18392  
17             our @EXPORT_OK = qw/
18             get_sentences
19             get_offsets
20             add_acronyms
21             get_acronyms
22             set_acronyms
23             initial_offsets
24             offsets2sentences
25             remove_false_eos
26             adjust_offsets
27             split_unsplit_stuff
28             /;
29              
30              
31             $EOS="\001";$P = q/[\.!?]/;$AP = q/(?:'|"|»|\)|\]|\})?/;$PAP = $P.$AP;
32              
33             my @PEOPLE = ( 'jr', 'mr', 'mrs', 'ms', 'dr', 'prof', 'sr', "sens?", "reps?", 'gov',
34             "attys?", 'supt', 'det', 'rev' );
35              
36              
37             my @ARMY = ( 'col','gen', 'lt', 'cmdr', 'adm', 'capt', 'sgt', 'cpl', 'maj' );
38             my @INSTITUTES = ( 'dept', 'univ', 'assn', 'bros' );
39             my @COMPANIES = ( 'inc', 'ltd', 'co', 'corp' );
40             my @PLACES = ( 'arc', 'al', 'ave', "blv?d", 'cl', 'ct', 'cres', 'dr', "expy?",
41             'dist', 'mt', 'ft',
42             "fw?y", "hwa?y", 'la', "pde?", 'pl', 'plz', 'rd', 'st', 'tce',
43             'Ala' , 'Ariz', 'Ark', 'Cal', 'Calif', 'Col', 'Colo', 'Conn',
44             'Del', 'Fed' , 'Fla', 'Ga', 'Ida', 'Id', 'Ill', 'Ind', 'Ia',
45             'Kan', 'Kans', 'Ken', 'Ky' , 'La', 'Me', 'Md', 'Is', 'Mass',
46             'Mich', 'Minn', 'Miss', 'Mo', 'Mont', 'Neb', 'Nebr' , 'Nev',
47             'Mex', 'Okla', 'Ok', 'Ore', 'Penna', 'Penn', 'Pa' , 'Dak',
48             'Tenn', 'Tex', 'Ut', 'Vt', 'Va', 'Wash', 'Wis', 'Wisc', 'Wy',
49             'Wyo', 'USAFA', 'Alta' , 'Man', 'Ont', 'Qué', 'Sask', 'Yuk');
50             my @MONTHS = ('jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec','sept');
51             my @MISC = ( 'vs', 'etc', 'no', 'esp' );
52              
53             @ABBREVIATIONS = (@PEOPLE, @ARMY, @INSTITUTES, @COMPANIES, @PLACES, @MONTHS, @MISC );
54              
55              
56             sub get_offsets {
57 4     4 1 13 my ($text) = @_;
58 4 50       27 return [] unless defined $text;
59 4         24 my $offsets = initial_offsets($text);
60 4         20 $offsets = remove_false_eos($text,$offsets);
61 4         44 $offsets = split_unsplit_stuff($text,$offsets);
62 4         33 $offsets = adjust_offsets($text,$offsets);
63 4         18 return $offsets;
64             }
65              
66              
67              
68             sub get_sentences {
69 4     4 1 66756 my ($text) = @_;
70 4         24 my $offsets = get_offsets($text);
71 4         61 my $sentences = offsets2sentences($text,$offsets);
72 4         38 return $sentences;
73             }
74              
75              
76             sub add_acronyms {
77 0     0 1 0 push @ABBREVIATIONS, @_;
78             }
79              
80              
81              
82             sub get_acronyms {
83 0     0 1 0 return @ABBREVIATIONS;
84             }
85              
86              
87             sub set_acronyms {
88 0     0 1 0 @ABBREVIATIONS=@_;
89             }
90              
91              
92             sub remove_false_eos {
93 4     4 1 13 my ($text,$offsets) = @_;
94 4         10 my $size = @$offsets;
95 4         19 my $new_offsets = [ sort { $a->[0] <=> $b->[0] } @$offsets ];
  202         266  
96 4         27 for(my $i=0; $i<$size-1; $i++){
97 92         219 my $start = $new_offsets->[$i][0];
98 92         155 my $end = $new_offsets->[$i][1];
99 92         148 my $length = $end-$start;
100 92         272 my $s = substr($text,$start,$length);
101 92         144 my $j=$i+1;
102              
103 92         123 my $unsplit = 0;
104 92 100       9252 $unsplit = 1 if $s =~ /(?:[^-\w]|^)\w$PAP\s$/s;
105 92 100       675 $unsplit = 1 if $s =~ /[^-\w]\w$P$/s;
106              
107             # don't split after a white-space followed by a single letter followed
108             # by a dot followed by another whitespace.
109 92 100       331 $unsplit = 1 if $s =~ /\s\w\.\s+$/;
110              
111             # fix: bla bla... yada yada
112 92         479 my $t = substr($text,$offsets->[$j][0], $offsets->[$j][1]-$offsets->[$j][0]);
113 92 50 33     349 $unsplit = 1 if $s =~ /\.\.\.\s*$/s and $t =~ /^\s*[[:lower:]]/s;
114              
115             # fix "." "?" "!"
116 92 50       2628 $unsplit = 1 if $s =~ m{['"]$P['"]\s+$}s;
117              
118             ## fix where abbreviations exist
119 92 50       189 foreach (@ABBREVIATIONS){ $unsplit = $1 if $s =~ /\b$_$PAP\s$/is; }
  11776         375132  
120              
121             # don't break after quote unless its a capital letter.
122 92 50 33     464 $unsplit = 1 if $s =~ /["']\s*$/s and $t =~ /^\s*[[:lower:]]/s;
123              
124             # don't break: text . . some more text.
125 92 50 33     356 $unsplit = 1 if $s =~ /\s\.\s$/s and $t =~ /^\s*/s;
126              
127 92 50       693 $unsplit = 1 if $s =~ /\s$PAP\s$/s;
128              
129 92 100       589 _merge_forward($new_offsets,$i) if $unsplit;
130             }
131 4         23 $new_offsets = [ grep { defined } @$new_offsets ];
  96         162  
132 4         26 return $new_offsets;
133              
134             #for(my $i=$size-1; $i>=0; $i--){ splice @$offsets, $i,1 unless defined($offsets->[$i]); }
135             }
136              
137             sub _merge_forward {
138 18     18   30 my ($offsets,$i) = @_;
139 18         39 my $j = $i+1;
140 18 50       53 return $offsets unless defined($offsets->[$j]);
141              
142 18         36 $offsets->[$j][0] = $offsets->[$i][0];
143 18         118 delete $offsets->[$i];
144              
145             #splice @$offsets, $i, 1;
146             }
147              
148              
149             sub split_unsplit_stuff {
150 4     4 1 15 my ($text,$offsets) = @_;
151 4         15 my $size = @$offsets;
152 4         24 for(my $i=0; $i<$size; $i++){
153 78         118 my $start = $offsets->[$i][0];
154 78         111 my $length = $offsets->[$i][1]-$start;
155 78         139 my $s = substr($text,$start,$length);
156              
157 78         101 my $split_points = [];
158 78         1816 while($s =~ /((?:\D|^)\d+$P)(\s+)/g){
159 7         22 my $end = $+[1];
160 7         17 my $begin = $-[2];
161 7         160 push @$split_points,[$start+$end,$start+$begin];
162             }
163 78         496 while($s =~ /($PAP\s)(\s*\()/g){
164 0         0 my $end = $+[1];
165 0         0 my $begin = $-[2];
166 0         0 push @$split_points,[$start+$end,$start+$begin];
167             }
168 78         332 while($s =~ /('\w$P)(\s)/g){
169 0         0 my $end = $+[1];
170 0         0 my $begin = $-[2];
171 0         0 push @$split_points,[$start+$end,$start+$begin];
172             }
173 78         196 while($s =~ /(\sno\.)(\s+)(?!\d)/g){
174 0         0 my $end = $+[1];
175 0         0 my $begin = $-[2];
176 0         0 push @$split_points,[$start+$end,$start+$begin];
177             }
178 78         182 while($s =~ /([ap]\.m\.\s+)([[:upper:]])/g){
179 0         0 my $end = $+[1];
180 0         0 my $begin = $-[2];
181 0         0 push @$split_points,[$start+$end,$start+$begin];
182             }
183              
184 78 100       296 _split_sentence($offsets,$i, [ sort { $a->[0] <=> $b->[0] } @$split_points ]) if @$split_points;
  2         18  
185             }
186 4         18 return $offsets;
187             }
188              
189              
190              
191             #sub _split_sentence {
192             # my ($offsets,$i,$end1,$start2) = @_;
193             # my $end2 = $offsets->[$i][1];
194             # $offsets->[$i][1] = $end1;
195             # $start2 //= $end1;
196             # push $offsets, [$start2, $end2];
197             #}
198              
199              
200             sub _split_sentence {
201 26     26   45 my ($offsets,$i,$split_points) = @_;
202 26         39 my ($end,$start) = @{shift @$split_points};
  26         51  
203 26         44 my $last = $offsets->[$i][1];
204 26         43 $offsets->[$i][1] = $end;
205 26         80 while(my $p = shift @$split_points){
206 73         192 push @$offsets, [$start,$p->[0]];
207 73         198 $start = $p->[1];
208             }
209 26         167 push @$offsets, [$start, $last];
210             }
211              
212              
213             sub adjust_offsets {
214 4     4 1 13 my ($text,$offsets) = @_;
215 4         12 my $size = @$offsets;
216 4         23 for(my $i=0; $i<$size; $i++){
217 85         117 my $start = $offsets->[$i][0];
218 85         100 my $end = $offsets->[$i][1];
219 85         87 my $length = $end - $start;
220 85         147 my $s = substr($text,$start,$length);
221 85 100       248 if ($s !~ /\w+/){
222 1         3 delete $offsets->[$i];
223 1         4 next;
224             }
225 84         1266 $s =~ /^(\s*).*?(\s*)$/s;
226 84 50       195 if(defined($1)){ $start += length($1); }
  84         115  
227 84 50       158 if(defined($2)){ $end -= length($2); }
  84         134  
228 84         320 $offsets->[$i] = [$start, $end];
229             }
230 4         18 my $new_offsets = [ grep { defined } @$offsets ];
  84         137  
231 4         14 return $new_offsets;
232             #for(my $i=$size-1; $i>=0; $i--){ splice @$offsets, $i,1 unless defined($offsets->[$i]); }
233             }
234              
235              
236             sub initial_offsets {
237 4     4 1 13 my ($text) = @_;
238 4         8 my $end;
239 4         14 my $text_end = length($text);
240 4         18 my $offsets = [[0,$text_end]];
241              
242 4         232 my @patterns = (
243             qr{(\n\s*\n)},
244             qr{$PAP\s()},
245             qr{\s\w$P()}
246             );
247              
248 4         12 my $split = 1;
249 4         16 while($split){
250 8         16 $split = 0;
251 8         19 foreach my $pat (@patterns){
252 24         37 my $size = @$offsets;
253 24         78 for(my $i=0; $i<$size; $i++){
254 388         692 my $start = $offsets->[$i][0];
255 388         548 my $length = $offsets->[$i][1]-$start;
256 388         639 my $s = substr($text,$start,$length);
257              
258 388         622 my $split_points = [];
259 388         3207 while($s =~ /(?
260 92         190 my $end = $-[1];
261 92         181 my $begin = $+[1];
262 92         244 push @$split_points,[$start+$end,$start+$begin];
263 92         594 $split = 1;
264             }
265              
266 388 100       1343 _split_sentence($offsets,$i,[ sort { $a->[0] <=> $b->[0] } @$split_points ]) if @$split_points;
  109         209  
267             }
268             }
269             }
270 4         32 return $offsets;
271             }
272              
273              
274             sub offsets2sentences {
275 4     4 1 11 my ($text, $offsets) = @_;
276 4         12 my $sentences = [];
277 4         28 foreach my $o ( sort {$a->[0] <=> $b->[0]} @$offsets) {
  132         173  
278 84         92 my $start = $o->[0];
279 84         103 my $length = $o->[1]-$o->[0];
280 84         193 push @$sentences, substr($text,$start,$length);
281             }
282 4         15 return $sentences;
283             }
284              
285             1;
286              
287              
288              
289             =pod
290              
291             =head1 NAME
292              
293             Lingua::EN::Sentence::Offsets - Finds sentence boundaries, and returns their offsets.
294              
295             =head1 VERSION
296              
297             version 0.01_05
298              
299             =head1 SYNOPSIS
300              
301             use Lingua::EN::Sentence::Offsets qw/get_offsets get_sentences/;
302            
303             my $offsets = get_offsets($text); ## Get the offsets.
304             foreach my $o (@$offsets) {
305             my $start = $o->[0];
306             my $length = $o->[1]-$o->[0];
307              
308             my $sentence = substr($text,$start,$length) ## Get a sentence.
309             # ...
310             }
311              
312             ### or
313              
314             my $sentences = get_sentences($text);
315             foreach my $sentence (@$sentences) {
316             ## do something with $sentence
317             }
318              
319             =head1 METHODS
320              
321             =head2 get_offsets
322              
323             Takes text input and returns reference to array containin pairs of character
324             offsets, corresponding to the sentences start and end positions.
325              
326             =head2 get_sentences
327              
328             Takes text input and splits it into sentences.
329              
330             =head2 add_acronyms
331              
332             user can add a list of acronyms/abbreviations.
333              
334             =head2 get_acronyms
335              
336             get defined list of acronyms.
337              
338             =head2 set_acronyms
339              
340             run over the predefined acronyms list with your own list.
341              
342             =head2 remove_false_eos
343              
344             =head2 split_unsplit_stuff
345              
346             Finds additional split points in the middle of previously defined sentences.
347              
348             =head2 adjust_offsets
349              
350             Minor adjusts to offsets (leading/trailing whitespace, etc)
351              
352             =head2 initial_offsets
353              
354             First naive delimitation of sentences
355              
356             =head2 offsets2sentences
357              
358             Given a list of sentence boundaries offsets and a text, returns an array with the text split into sentences.
359              
360             =head1 ACKNOWLEDGEMENTS
361              
362             Based on the original module L, from Shlomo Yona (SHLOMOY)
363              
364             =head1 SEE ALSO
365              
366             L, L
367              
368             =head1 AUTHOR
369              
370             Andre Santos
371              
372             =head1 COPYRIGHT AND LICENSE
373              
374             This software is copyright (c) 2012 by Andre Santos.
375              
376             This is free software; you can redistribute it and/or modify it under
377             the same terms as the Perl 5 programming language system itself.
378              
379             =cut
380              
381              
382             __END__