File Coverage

blib/lib/Lingua/EN/Tokenizer/Offsets.pm
Criterion Covered Total %
statement 135 138 97.8
branch 29 42 69.0
condition 16 24 66.6
subroutine 14 15 93.3
pod 5 6 83.3
total 199 225 88.4


line stmt bran cond sub pod time code
1 1     1   14922 use strict;
  1         1  
  1         26  
2 1     1   3 use warnings;
  1         1  
  1         44  
3             package Lingua::EN::Tokenizer::Offsets;
4             {
5             $Lingua::EN::Tokenizer::Offsets::VERSION = '0.01_03';
6             }
7 1     1   406 use utf8::all;
  1         40954  
  1         4  
8 1     1   1747 use Data::Dump qw/dump/;
  1         5289  
  1         65  
9 1     1   6 use feature qw/say/;
  1         1  
  1         32  
10              
11 1     1   3 use base 'Exporter';
  1         2  
  1         687  
12             our @EXPORT_OK = qw/
13             initial_offsets
14             token_offsets
15             adjust_offsets
16             get_tokens
17             tokenize
18             /;
19              
20              
21             # ABSTRACT: Finds word (token) boundaries, and returns their offsets.
22              
23              
24             sub tokenize {
25 0     0 1 0 my ($text) = @_;
26 0         0 my $tokens = get_tokens($text);
27 0         0 return join ' ',@$tokens;
28             }
29              
30              
31              
32             sub token_offsets {
33 1     1 0 2 my ($text) = @_;
34 1 50       3 return [] unless defined $text;
35 1         5 my $offsets = initial_offsets($text);
36 1         5 $offsets = adjust_offsets($text,$offsets);
37 1         9 return $offsets;
38             }
39              
40              
41              
42             sub get_tokens {
43 1     1 1 14 my ($text) = @_;
44 1         4 my $offsets = token_offsets($text);
45 1         5 my $tokens = offsets2tokens($text,$offsets);
46 1         33 return $tokens;
47             }
48              
49              
50              
51              
52             sub adjust_offsets {
53 2     2 1 3 my ($text,$offsets) = @_;
54 2 50       6 $text = $$text if ref($text);
55 2         3 my $size = @$offsets;
56 2         5 for(my $i=0; $i<$size; $i++){
57 927         631 my $start = $offsets->[$i][0];
58 927         561 my $end = $offsets->[$i][1];
59 927         525 my $length = $end - $start;
60 927 100       952 if ($length <= 0){
61 29         21 delete $offsets->[$i];
62 29         37 next;
63             }
64 898         1224 my $s = substr($text,$start,$length);
65 898 100       1295 if ($s =~ /^\s*$/){
66 67         44 delete $offsets->[$i];
67 67         92 next;
68             }
69 831         1220 $s =~ /^(\s*).*?(\s*)$/s;
70 831 50       1021 if(defined($1)){ $start += length($1); }
  831         626  
71 831 50       941 if(defined($2)){ $end -= length($2); }
  831         607  
72 831         1393 $offsets->[$i] = [$start, $end];
73             }
74 2         6 my $new_offsets = [ grep { defined } @$offsets ];
  926         703  
75 2         5 return $new_offsets;
76             }
77              
78              
79             sub initial_offsets {
80 1     1 1 2 my ($text) = @_;
81 1 50       3 $text = $$text if ref($text);
82 1         2 my $end;
83 1         12 my $text_end = length($text);
84 1         3 my $offsets = [[0,$text_end]];
85              
86             # token patterns
87 1         20 my @patterns = (
88             qr{([^\p{IsAlnum}\s\.\'\`\,\-’])},
89             qr{(?
90             qr{(?<=\p{IsN})(,)(?!\d)},
91             qr{(?
92             qr{(?
93             qr{(?
94             qr{(?<=\p{isAlpha})(['`’])(?!\p{isAlpha})},
95             qr{(?<=\p{isAlpha})()['`’](?=\p{isAlpha})},
96             qr{(?:^|\s)(\S+)(?:$|\s)},
97             qr{(?:^|[^\.])(\.\.+)(?:$|[^\.])},
98              
99             qr{(?<=\p{isAlpha})['`]()(?=\p{isAlpha})},
100              
101             );
102              
103 1         3 for my $pat (@patterns){
104 11         12 my $size = @$offsets;
105 11         15 for(my $i=0; $i<$size; $i++){
106 2023         1357 my $start = $offsets->[$i][0];
107 2023         1294 my $length = $offsets->[$i][1]-$start;
108 2023         2167 my $s = substr($text,$start,$length);
109              
110 2023         1384 my $split_points = [];
111              
112 2023 100       4571 if($s =~ /^$pat(?!$)/g){
113 37         44 my $first = $-[1];
114 37         50 push @$split_points,[$start+$first,$start+$first];
115 37         44 my $second = $+[1];
116 37 50       64 push @$split_points,[$start+$second,$start+$second] if $first != $second;
117             }
118 2023         5215 while($s =~ /(?
119 179         1202 my $first = $-[1];
120 179         226 push @$split_points,[$start+$first,$start+$first];
121 179         923 my $second = $+[1];
122 179 100       878 push @$split_points,[$start+$second,$start+$second] if $first != $second;
123             }
124 2023 100       4773 if($s =~ /(?
125 37         58 my $first = $-[1];
126 37         45 push @$split_points,[$start+$first,$start+$first];
127 37         49 my $second = $+[1];
128 37 50       71 push @$split_points,[$start+$second,$start+$second] if $first != $second;
129             }
130              
131 2023 100       4763 _split_tokens($offsets,$i,[ sort { $a->[0] <=> $b->[0] } @$split_points ]) if @$split_points;
  610         461  
132             }
133             }
134 1         5 return _nonbp($text,$offsets);
135             }
136              
137             sub _split_tokens {
138 47     47   46 my ($offsets,$i,$split_points) = @_;
139 47         23 my ($end,$start) = @{shift @$split_points};
  47         48  
140 47         38 my $last = $offsets->[$i][1];
141 47         35 $offsets->[$i][1] = $end;
142 47         62 while(my $p = shift @$split_points){
143 457 50       655 push @$offsets, [$start,$p->[0]] unless $start == $p->[0];
144 457         608 $start = $p->[1];
145             }
146 47         157 push @$offsets, [$start, $last];
147             }
148              
149              
150              
151             sub offsets2tokens {
152 1     1 1 2 my ($text, $offsets) = @_;
153 1 50       3 $text = $$text if ref($text);
154 1         2 my $tokens = [];
155 1         7 foreach my $o ( sort {$a->[0] <=> $b->[0]} @$offsets) {
  421         247  
156 422         261 my $start = $o->[0];
157 422         257 my $length = $o->[1]-$o->[0];
158 422         468 push @$tokens, substr($text,$start,$length);
159             }
160 1         3 return $tokens;
161             }
162              
163              
164             sub _load_prefixes {
165 1     1   3 my ($prefixref) = @_;
166 1         4 $INC{'Lingua/EN/Tokenizer/Offsets.pm'} =~ m{\.pm$};
167 1         5 my $prefixfile = "$`/nonbreaking_prefix.en";
168            
169 1 50       82 open my $prefix, '<', $prefixfile or die "Could not open file '$prefixfile'!";
170 1         16 while (<$prefix>) {
171 107 100 100     314 next if /^#/ or /^\s*$/;
172 93         62 my $item = $_;
173 93         59 chomp($item);
174 93 100       95 if ($item =~ /(.*)[\s]+(\#NUMERIC_ONLY\#)/) { $prefixref->{$1} = 2; }
  3         16  
175 90         190 else { $prefixref->{$item} = 1; }
176             }
177 1         16 close($prefix);
178             }
179              
180             sub _nonbp {
181 1     1   4 my ($text,$offsets) = @_;
182 1 50       3 $text = $$text if ref($text);
183 1         3 my $nonbpref = {};
184 1         21 _load_prefixes($nonbpref);
185 1         5 my $new_offsets = adjust_offsets($text,$offsets);
186 1         9 $new_offsets = [ sort { $a->[0] <=> $b->[0] } @$new_offsets ];
  950         573  
187 1         7 my $size = @$new_offsets;
188 1         3 my $extra = [];
189 1         6 for(my $i=0; $i<$size-1; $i++){
190 408         241 my $start = $new_offsets->[$i][0];
191 408         241 my $end = $new_offsets->[$i][1];
192 408         237 my $length = $end-$start;
193 408         343 my $s = substr($text,$start,$length);
194 408         255 my $j=$i+1;
195 408         375 my $t = substr($text,$new_offsets->[$j][0], $new_offsets->[$j][1]-$new_offsets->[$j][0]);
196              
197 408 100       751 if($s =~ /^(\S+)\.\s?$/){
198 22         22 my $pre = $1;
199 22 0 66     147 unless (
      66        
      66        
      100        
      33        
      33        
      66        
200             ($pre =~ /\./ and $pre =~ /\p{IsAlpha}/)
201             or ($nonbpref->{$pre} and $nonbpref->{$pre}==1)
202             or ($t =~ /^[\p{IsLower}]/)
203             or (
204             $nonbpref->{$pre}
205             and $nonbpref->{$pre}==2
206             and $t =~ /^\d+/)
207             ){
208 13         18 $s =~ /^(.*[^\s\.])\.\s*?$/;
209 13         28 push @$extra, [$start+$+[1],$end];
210 13         33 $new_offsets->[$i][1] = $start+$+[1];
211             }
212             }
213             }
214 1         11 return [ sort { $a->[0] <=> $b->[0] } (@$new_offsets,@$extra) ];
  621         443  
215             }
216            
217             1;
218              
219              
220              
221             =pod
222              
223             =encoding utf-8
224              
225             =head1 NAME
226              
227             Lingua::EN::Tokenizer::Offsets - Finds word (token) boundaries, and returns their offsets.
228              
229             =head1 VERSION
230              
231             version 0.01_03
232              
233             =head1 SYNOPSIS
234              
235             use Lingua::EN::Tokenizer::Offsets qw/token_offsets get_tokens/;
236            
237             my $str <
238             Hey! Mr. Tambourine Man, play a song for me.
239             I'm not sleepy and there is no place I’m going to.
240             END
241              
242             my $offsets = token_offsets($str); ## Get the offsets.
243             foreach my $o (@$offsets) {
244             my $start = $o->[0];
245             my $length = $o->[1]-$o->[0];
246              
247             my $token = substr($text,$start,$length) ## Get a token.
248             # ...
249             }
250              
251             ### or
252              
253             my $tokens = get_tokens($str);
254             foreach my $token (@$tokens) {
255             ## do something with $token
256             }
257              
258             =head1 METHODS
259              
260             =head2 tokenize($text)
261              
262             Returns a tokenized version of $text (space-separated tokens).
263              
264             $text can be a scalar or a scalar reference.
265              
266             =head2 get_offsets($text)
267              
268             Returns a reference to an array containin pairs of character
269             offsets, corresponding to the start and end positions of tokens
270             from $text.
271              
272             $text can be a scalar or a scalar reference.
273              
274             =head2 get_tokens($text)
275              
276             Splits $text it into tokens, returning an array reference.
277              
278             $text can be a scalar or a scalar reference.
279              
280             =head2 adjust_offsets($text,$offsets)
281              
282             Minor adjusts to offsets (leading/trailing whitespace, etc)
283              
284             $text can be a scalar or a scalar reference.
285              
286             =head2 initial_offsets($text)
287              
288             First naive delimitation of tokens.
289              
290             $text can be a scalar or a scalar reference.
291              
292             =head2 offsets2tokens($text,$offsets)
293              
294             Given a list of token boundaries offsets and a text, returns an array with the text split into tokens.
295              
296             $text can be a scalar or a scalar reference.
297              
298             =head1 ACKNOWLEDGEMENTS
299              
300             Based on the original tokenizer written by Josh Schroeder and provided by Europarl L.
301              
302             =head1 SEE ALSO
303              
304             L, L
305              
306             =head1 AUTHOR
307              
308             André Santos
309              
310             =head1 COPYRIGHT AND LICENSE
311              
312             This software is copyright (c) 2012 by Andre Santos.
313              
314             This is free software; you can redistribute it and/or modify it under
315             the same terms as the Perl 5 programming language system itself.
316              
317             =cut
318              
319              
320             __END__