File Coverage

blib/lib/Text/Math/NumExp.pm
Criterion Covered Total %
statement 97 99 97.9
branch 23 38 60.5
condition 5 9 55.5
subroutine 13 14 92.8
pod 4 4 100.0
total 142 164 86.5


line stmt bran cond sub pod time code
1 4     4   199734 use 5.006;
  4         14  
  4         159  
2 4     4   23 use strict;
  4         9  
  4         133  
3 4     4   20 use warnings;
  4         8  
  4         261  
4             package Text::Math::NumExp;
5             {
6             $Text::Math::NumExp::VERSION = '0.01_12';
7             }
8              
9             #ABSTRACT: Text::Math::NumExp - Find numeric expressions in text.
10              
11 4     4   3908 use utf8::all;
  4         295845  
  4         32  
12 4     4   14979 use base 'Exporter';
  4         11  
  4         605  
13             our @EXPORT = (qw/ norm_numexp
14             find_numexp
15             find_numwords
16             solve
17             /);
18 4     4   3754 use Lingua::EN::FindNumber;
  4         36108  
  4         584  
19 4     4   47 use Scalar::Util qw/looks_like_number/;
  4         9  
  4         346  
20 4     4   5366 use Safe;
  4         76970  
  4         7776  
21              
22              
23              
24             sub find_numexp {
25 1     1 1 12 my ($text_or_ref,$options) = @_;
26 1 50       6 my $text = (ref($text_or_ref) ? $$text_or_ref : $text_or_ref);
27              
28              
29 1         2 my $str_offset = 0;
30 1         2 my $numexps = [];
31              
32 1         5 my $w = qr{[A-Za-z\-]}; # letter
33 1         4 my $s = qr{[ \t]}; # space excluding \n
34 1         4 my $x = qr{[^\s\d]}; # not a space nor a digit
35 1         3 my $break = qr{\-fold|%|°C|,\s}; # common number-ending patterns
36 1         37 my $end = qr{$break|,$}; #
37 1         20 my $wgap = qr{$w+$s+$w+}; # gap between words
38 1         4 my $punct = qr{[:,\.!?\/]}; # punctuation
39              
40 1         88 while ($text =~ /
41             (?:
42             $wgap # word gap
43             $x* # remaning characters before numexp
44             \s+ # space
45             |
46             ^ # or begining of line
47             )
48             (.*?) # numexp
49             (?= # do not consume
50             \s+ # space
51             $wgap # word gap
52             |
53             $ # or end of line
54             )
55             /mgxp) {
56 9         24 my $str = $1;
57 9         24 $str_offset = $-[1];
58 9 50       35 next unless $str =~ /\d/;
59 9         12 my $offset = $str_offset;
60              
61 9         86 foreach my $ne (split /\s*$break\s*/,$str){
62 9         152 (substr $text, $str_offset) =~ /\Q$ne\E/;
63 9         26 my $ne_offset = $str_offset + $-[0];
64              
65             # Remove (partial) word, punctuation or space at the begining
66 9 100       154 $ne_offset+= $+[0]
67             if $ne =~ s/^(?:[A-Za-z\s\-]|$punct)*\s+//;
68              
69             # Remove punctuation at the end
70 9         113 $ne =~ s/$punct*$//;
71              
72             # Remove space followed by word chars or punctuation at the end
73 9         68 $ne =~ s/\s+(?:[A-Za-z\s]|$punct)*$//;
74              
75              
76             # Remove single '(' at the begining if there is no closing ')'
77 9 50 66     50 $ne_offset+= $+[0]
78             if $ne !~ /\)/ and $ne =~ s/^\(//;
79              
80             # Remove single ')' at the begining if there is no opening '('
81 9 100       23 $ne =~ s/\)$// if $ne !~ /\(/;
82              
83 9 100       20 next if _ignore($ne,$options);
84              
85 7         9 $offset = $ne_offset;
86 7         14 my $length = length($ne);
87 7         13 my $value = solve($ne);
88 7         37 push @$numexps, {
89             text => $ne,
90             offset => $offset,
91             length => $length,
92             value => $value,
93             };
94 7         113 $offset+= length $ne;
95             }
96             }
97 1 50       12 return wantarray ? @$numexps : $numexps;
98             }
99              
100              
101             sub find_numwords {
102 1     1 1 36 my ($text_or_ref,$options) = @_;
103 1 50       5 my $text = (ref($text_or_ref) ? $$text_or_ref : $text_or_ref);
104 1         2 my $numbers = [];
105              
106 1         771 while($text =~ /($number_re)/g){
107 6         12 my $text = $1;
108 6         30 my $start = $-[0];
109 6         28 my $end = $+[0];
110 6 50       42 $end = ($start + $-[0]) if($text =~ s/\s+$//);
111 6         18 my $value = numify($text);
112 6 50       1042 next unless looks_like_number($value);
113 6         120 push @$numbers, {
114             text => $text,
115             offset => $start,
116             length => $end-$start,
117             value => $value,
118             };
119             }
120 1 50       6 return wantarray ? @$numbers : $numbers;
121             }
122              
123             sub _ignore {
124 9     9   17 my ($ne, $options) = @_;
125             # Ignore if string is empty or blank
126 9 50       27 return 1 if $ne =~ /^\s*$/;
127              
128             # Ignore if string doesn't have a digit
129 9 50       26 return 1 if $ne !~ /\d/;
130              
131 9 100 66     105 return 1 if $options->{ipat} and $ne =~ /$options->{ipat}/;
132 7 50 33     18 return 1 if $options->{ifunc} and $options->{ifunc}->($ne);
133              
134 7         16 return;
135             }
136              
137              
138             sub solve {
139 12     12 1 3012 my ($ne,$options) = @_;
140 12         36 $ne =~ s/\^/**/g;
141 12         16 my $value;
142             {
143 12     0   13 local $SIG{__WARN__} = sub {};
  12         105  
  0         0  
144 12         73 my ($cpt) = new Safe;
145 12         12593 $cpt->permit(qw(lt i_lt gt i_gt le i_le ge i_ge eq i_eq ne i_ne ncmp i_ncmp slt sgt sle sge seq sne scmp));
146 12         309 $cpt->permit(qw(atan2 sin cos exp log sqrt rand srand));
147 12         128 $value = $cpt->reval($ne);
148             }
149 12 50       7953 return $value if looks_like_number($value);
150 12         30 return;
151             }
152              
153              
154             sub norm_numexp {
155 2     2 1 69 my ($text_or_ref,$options) = @_;
156 2 50       11 my $text = (ref($text_or_ref) ? $$text_or_ref : $text_or_ref);
157              
158             # 10 x 5 -> 10*5
159 2         9 my $mult = qr{[x×*✖✕✱∗﹡*]};
160 2         166 $text =~ s/(\d)\s{1,2}?$mult\s{1,2}?(\d)/$1*$2/g;
161              
162             # 10 ^ 5 -> 10^5
163 2         32 $text =~ s/(\d)\s{1,2}?\^\s{1,2}?(\d)/$1^$2/g;
164              
165             # 10(5)/10[5] -> 10^5
166 2         24 $text =~ s/(\d)\((\d+)\)/$1^$2/g;
167 2         14 $text =~ s/(\d)\[(\d+)\]/$1^$2/g;
168              
169             # Extreme options
170 2 50       11 if ($options->{x}){
171             # *1011 -> *10^11
172 2         18 $text =~ s/(\d)[*]10(\d{2})/$1*10^$2/g;
173             }
174              
175 2         9 $text =~ s/’/'/g;
176              
177 2 50       8 if(ref($text_or_ref)) { $$text_or_ref = $text; }
  2         6  
178 0         0 else { return $text; }
179 2         10 return;
180             }
181              
182              
183              
184             1; # End of Text::Math::NumExp
185              
186             __END__