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   111981 use strict; use warnings;
  4     4   6  
  4         102  
  4         13  
  4         5  
  4         231  
2             package Lingua::EN::Sentence::Offsets;
3             {
4             $Lingua::EN::Sentence::Offsets::VERSION = '0.01_06';
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   15 use Carp qw/cluck/;
  4         4  
  4         168  
12 4     4   13 use feature qw/say/;
  4         4  
  4         330  
13 4     4   1550 use utf8::all;
  4         169541  
  4         19  
14 4     4   6759 use Data::Dump qw/dump/;
  4         22184  
  4         286  
15              
16 4     4   28 use base 'Exporter';
  4         7  
  4         9790  
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 8 my ($text) = @_;
58 4 50       16 return [] unless defined $text;
59 4         18 my $offsets = initial_offsets($text);
60 4         16 $offsets = remove_false_eos($text,$offsets);
61 4         26 $offsets = split_unsplit_stuff($text,$offsets);
62 4         19 $offsets = adjust_offsets($text,$offsets);
63 4         11 return $offsets;
64             }
65              
66              
67              
68             sub get_sentences {
69 4     4 1 11353 my ($text) = @_;
70 4         16 my $offsets = get_offsets($text);
71 4         18 my $sentences = offsets2sentences($text,$offsets);
72 4         24 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 8 my ($text,$offsets) = @_;
94 4         9 my $size = @$offsets;
95 4         9 my $new_offsets = [ sort { $a->[0] <=> $b->[0] } @$offsets ];
  202         154  
96 4         22 for(my $i=0; $i<$size-1; $i++){
97 92         120 my $start = $new_offsets->[$i][0];
98 92         74 my $end = $new_offsets->[$i][1];
99 92         67 my $length = $end-$start;
100 92         158 my $s = substr($text,$start,$length);
101 92         68 my $j=$i+1;
102              
103 92         74 my $unsplit = 0;
104 92 100       681 $unsplit = 1 if $s =~ /(?:[^-\w]|^)\w$PAP\s$/s;
105 92 100       350 $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       193 $unsplit = 1 if $s =~ /\s\w\.\s+$/;
110              
111             # fix: bla bla... yada yada
112 92         167 my $t = substr($text,$offsets->[$j][0], $offsets->[$j][1]-$offsets->[$j][0]);
113 92 50 33     183 $unsplit = 1 if $s =~ /\.\.\.\s*$/s and $t =~ /^\s*[[:lower:]]/s;
114              
115             # fix "." "?" "!"
116 92 50       273 $unsplit = 1 if $s =~ m{['"]$P['"]\s+$}s;
117              
118             ## fix where abbreviations exist
119 92 50       118 foreach (@ABBREVIATIONS){ $unsplit = $1 if $s =~ /\b$_$PAP\s$/is; }
  11776         192319  
120              
121             # don't break after quote unless its a capital letter.
122 92 50 33     241 $unsplit = 1 if $s =~ /["']\s*$/s and $t =~ /^\s*[[:lower:]]/s;
123              
124             # don't break: text . . some more text.
125 92 50 33     206 $unsplit = 1 if $s =~ /\s\.\s$/s and $t =~ /^\s*/s;
126              
127 92 50       339 $unsplit = 1 if $s =~ /\s$PAP\s$/s;
128              
129 92 100       324 _merge_forward($new_offsets,$i) if $unsplit;
130             }
131 4         19 $new_offsets = [ grep { defined } @$new_offsets ];
  96         98  
132 4         17 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   37 my ($offsets,$i) = @_;
139 18         28 my $j = $i+1;
140 18 50       40 return $offsets unless defined($offsets->[$j]);
141              
142 18         17 $offsets->[$j][0] = $offsets->[$i][0];
143 18         92 delete $offsets->[$i];
144              
145             #splice @$offsets, $i, 1;
146             }
147              
148              
149             sub split_unsplit_stuff {
150 4     4 1 12 my ($text,$offsets) = @_;
151 4         11 my $size = @$offsets;
152 4         19 for(my $i=0; $i<$size; $i++){
153 78         68 my $start = $offsets->[$i][0];
154 78         58 my $length = $offsets->[$i][1]-$start;
155 78         93 my $s = substr($text,$start,$length);
156              
157 78         66 my $split_points = [];
158 78         1005 while($s =~ /((?:\D|^)\d+$P)(\s+)/g){
159 7         13 my $end = $+[1];
160 7         11 my $begin = $-[2];
161 7         93 push @$split_points,[$start+$end,$start+$begin];
162             }
163 78         262 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         172 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         115 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         101 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       167 _split_sentence($offsets,$i, [ sort { $a->[0] <=> $b->[0] } @$split_points ]) if @$split_points;
  2         10  
185             }
186 4         8 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   28 my ($offsets,$i,$split_points) = @_;
202 26         21 my ($end,$start) = @{shift @$split_points};
  26         37  
203 26         25 my $last = $offsets->[$i][1];
204 26         30 $offsets->[$i][1] = $end;
205 26         64 while(my $p = shift @$split_points){
206 73         85 push @$offsets, [$start,$p->[0]];
207 73         97 $start = $p->[1];
208             }
209 26         87 push @$offsets, [$start, $last];
210             }
211              
212              
213             sub adjust_offsets {
214 4     4 1 6 my ($text,$offsets) = @_;
215 4         8 my $size = @$offsets;
216 4         16 for(my $i=0; $i<$size; $i++){
217 85         61 my $start = $offsets->[$i][0];
218 85         55 my $end = $offsets->[$i][1];
219 85         56 my $length = $end - $start;
220 85         81 my $s = substr($text,$start,$length);
221 85 100       154 if ($s !~ /\w+/){
222 1         2 delete $offsets->[$i];
223 1         3 next;
224             }
225 84         607 $s =~ /^(\s*).*?(\s*)$/s;
226 84 50       118 if(defined($1)){ $start += length($1); }
  84         79  
227 84 50       112 if(defined($2)){ $end -= length($2); }
  84         63  
228 84         152 $offsets->[$i] = [$start, $end];
229             }
230 4         11 my $new_offsets = [ grep { defined } @$offsets ];
  84         71  
231 4         9 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 8 my ($text) = @_;
238 4         4 my $end;
239 4         10 my $text_end = length($text);
240 4         11 my $offsets = [[0,$text_end]];
241              
242 4         162 my @patterns = (
243             qr{(\n\s*\n)},
244             qr{$PAP\s()},
245             qr{\s\w$P()}
246             );
247              
248 4         10 my $split = 1;
249 4         11 while($split){
250 8         10 $split = 0;
251 8         13 foreach my $pat (@patterns){
252 24         26 my $size = @$offsets;
253 24         44 for(my $i=0; $i<$size; $i++){
254 388         283 my $start = $offsets->[$i][0];
255 388         251 my $length = $offsets->[$i][1]-$start;
256 388         375 my $s = substr($text,$start,$length);
257              
258 388         338 my $split_points = [];
259 388         1620 while($s =~ /(?
260 92         116 my $end = $-[1];
261 92         104 my $begin = $+[1];
262 92         120 push @$split_points,[$start+$end,$start+$begin];
263 92         313 $split = 1;
264             }
265              
266 388 100       757 _split_sentence($offsets,$i,[ sort { $a->[0] <=> $b->[0] } @$split_points ]) if @$split_points;
  109         110  
267             }
268             }
269             }
270 4         18 return $offsets;
271             }
272              
273              
274             sub offsets2sentences {
275 4     4 1 7 my ($text, $offsets) = @_;
276 4         8 my $sentences = [];
277 4         41 foreach my $o ( sort {$a->[0] <=> $b->[0]} @$offsets) {
  132         97  
278 84         51 my $start = $o->[0];
279 84         63 my $length = $o->[1]-$o->[0];
280 84         115 push @$sentences, substr($text,$start,$length);
281             }
282 4         9 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_06
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__