zeroclickinfo-goodies/lib/DDG/Goodie/Calculator.pm

260 lines
9.9 KiB
Perl
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

package DDG::Goodie::Calculator;
# ABSTRACT: Handles the triggering and query preprocessing for the calculator
use strict;
use DDG::Goodie;
with 'DDG::GoodieRole::NumberStyler';
use utf8;
zci answer_type => 'calc';
zci is_cached => 1;
my $calc_regex = qr/^(free)?\s?(online)?\s?calc(ulator)?\s?(online)?\s?(free)?$/i;
triggers query => $calc_regex;
triggers query => qr'^
(?: [0-9 () τ π e √ x × ∙ ⋅ * + \- ÷ / \^ \$ £ € \. \, _ ! = % °]+ |
\d+\%=?$ |
what\sis| calculat(e|or) | solve | math | log\sof | fact(?:orial?)?(\s+of)? |
times | mult | multiply | divided\sby | plus | minus | cos | tau |
sin | sinh | tan | log | ln | exp | tanh | arctan | atan | arccos | acosh? | asinh? | arcsin | cosh |
deg(?:rees?)? | rad(?:ians?)? |
squared | sqrt | cbrt | \d+\s?mod(?:ulo)?\s?\d+ | dozen | pi |
(?:cub(?:ed?|ic)|sq(?:uare)?)\s?r(?:oo)?t(?:\sof)? | cubed? |
score){2,}$
'xi;
my $number_re = number_style_regex();
my %named_operations = (
'\^' => '**',
'×' => '*',
'∙' => '*',
'⋅' => '*', # Can be mistaken for dot operator
'times' => '*',
'mult' => '*',
'multiply' => '*',
'minus' => '-',
'plus' => '+',
'divided\sby' => '/',
'÷' => '/',
'mod' => 'mod',
'modulo' => 'modulo',
'squared' => '**2',
);
my %named_constants = (
dozen => 12,
score => 20,
);
my $ored_constants = join('|', keys %named_constants); # For later substitutions
# operators that are allowed and not allowed at start/end of expression
my $no_start_ops = qr{^(?:x|×|∙|⋅|\*|÷|/|\^|\,|_)};
my $no_end_ops = qr{(?:√|x|×|∙|⋅|\*|\+|\-|÷|/|\^|\$|£|€|\,|_)$};
my $word_ops = join "|", ("dividedby", "divided by", "times", "plus", "minus");
my $no_word_ops = qr{(^(?:$word_ops)|(?:$word_ops)$)}; # word based operators at start / end
my $ip4_octet = qr/([01]?\d\d?|2[0-4]\d|25[0-5])/; # Each octet should look like a number between 0 and 255.
my $ip4_regex = qr/(?:$ip4_octet\.){3}$ip4_octet/; # There should be 4 of them separated by 3 dots.
my $up_to_32 = qr/([1-2]?[0-9]{1}|3[1-2])/; # 0-32
my $network = qr#^$ip4_regex\s*/\s*(?:$up_to_32|$ip4_regex)\s*$#; # Looks like network notation, either CIDR or subnet mask
## prepares the query to interpreted by the calculator front-end
sub prepare_for_frontend {
my ($query, $style) = @_;
# Equals varies by output type.
$query =~ s/\=$//;
$query =~ s/(\d)[ _](\d)/$1$2/g; # Squeeze out spaces and underscores.
# Show them how 'E' was interpreted. This should use the number styler, too.
$query =~ s/([\d\.\-]+)E([\-\d\.]+)/\($1 * 10^$2\)/ig;
$query =~ s/\s*\*\*\s*/^/g; # Use prettier exponentiation.
$query = $style->for_computation($query); # Make sure period is used as decimal point
foreach my $name (keys %named_constants) {
$query =~ s#\($name\)#$name#xig;
}
my $spaced_query = rewriteQuery(spacing($query));
$spaced_query =~ s/^ - /-/;
return $spaced_query;
}
# separates symbols with a space
# spacing '1+1' -> '1 + 1'
sub spacing {
my ($text, $space_for_parse) = @_;
$text =~ s/\s{2,}/ /g;
$text =~ s/(\s*(?<!<)(?:[\+\^∙⋅\*\/÷]|(?<!\de)\-|times|plus|minus|divided\s*by)+\s*)/ $1 /ig;
$text =~ s/\s*dividedby\s*/ divided by /ig;
$text =~ s/(\d+?)((?:dozen|pi|squared|score))/$1 $2/ig;
$text =~ s/([\(\)])/ $1 /g if $space_for_parse;
$text =~ s/\|/,/g;
$text =~ s/\s{2,}/ /g;
return $text;
}
# rewrites the users original query to include operator
# rewriteQuery '2 plus 2' -> '2 + 2'
sub rewriteQuery {
my ($text) = @_;
$text =~ s/plus/+/g;
$text =~ s/minus/-/g;
$text =~ s/times|mult/×/g;
$text =~ s/divided\s?by/÷/g;
$text =~ s/(a?cos|tau|τ|a?sinh?|a?tan|log|ln|exp|tanh|π|squared|sqrt|dozen|pi|e|score)\s*\1/$1/g;
$text =~ s|([x × % + ÷ / \^ \$ £ \. \, _ =])\s*\1|$1|gx;
return $text;
}
# rewrites log/ln functions coming from the search bar
# log of 5 --> log(5), log2 8 --> log(8,2), log321 --> log(321)
sub rewriteFunctions {
my ($query) = @_;
# Preprocesses modulo operations
$query =~ s/(\d+)(?:\s+?mod(?:ulo)?\s+?|\s+%\s+?)(\d+)/mod($1|$2)/;
# Preprocesses factorial operations
$query =~ s/fact\(?(\d+)\)?/$1!/;
$query =~ s/fact(?:orial?)?(?:\s+of)?\s+?(\(?\d+\)?)?/$1!/;
$query =~ s/exp\s?\(?(\d+)\)?/exp($1)/i;
$query =~ s/cube\s?\((.+)\)/cube($1)/gi;
$query =~ s/cube\s?(\d+|e|pi|tau)/cube($1)/gi;
$query =~ s/(\d+|e|pi|tau)\scubed/cube($1)/gi;
# Preprocesses cbrt / sqrt
$query =~ s/(?:cu(?:be)?d?|cubic)\s?r(?:oo)?t(?:\sof)?/cbrt/ig;
$query =~ s/sq(?:uare)?\s?r(?:oo)?t(?:\sof)?/sqrt/ig;
$query =~ s/(cbrt|sqrt)\s?\((.+)\)/$1($2)/i;
$query =~ s/(cbrt|sqrt)\s?(\d+|e|pi|tau)/$1($2)/i;
# Preprocesses Log/Ln
$query =~ s/log\sof\s(\d+)/log($1)/i;
$query =~ s/log10\((\d+)\)/log($1)/i;
$query =~ s/log10\s(\d+)/log($1)/i;
$query =~ s/log(\d+)\s?\((.+)\)/log($2|$1)/i;
$query =~ s/log(\d+)\s(\d+)/log($2|$1)/i;
$query =~ s/log\s?(\d+)/log($1)/i;
$query =~ s/ln\s?(\d+)/ln($1)/i;
# Preprocesses Trig functions
$query =~ s/arctan/atan/ig;
$query =~ s/arccos/acos/ig;
$query =~ s/arcsin/asin/ig;
$query =~ s/(deg)rees?/$1/ig;
$query =~ s/rad(?:ians?)?//ig;
$query =~ s/(a?sinh?|a?cosh?|a?tanh?|tan)\s?(\d+(?: deg)?)/$1($2)/ig;
return $query;
}
handle query => sub {
my $query = $_;
if ($query =~ $calc_regex) {
return '', structured_answer => {
data => {
query => undef
},
templates => {
group => 'base',
options => {
content => 'DDH.calculator.content'
}
}
};
}
# Help support the query if there is degrees involed
$query =~ s/°/ deg/gi;
return if ( m/deg(rees?)?|°/i && m/rad(ians?)?/); # we don't support a mix of degrees and radians in the same query
$query = rewriteFunctions($query);
# throw out obvious non-calculations immediately
return if $query =~ qr/(\$(.+)?(?=£|€))|(£(.+)?(?=\$|€))|(€(.+)?(?=\$|£))/; # only let one currency type through
return if $req->query_lc =~ /^0x/i; # hex maybe?
return if $query =~ /\d+(?:X|x)\.?$/; # websites such as 1337X
return if $query =~ /score$/;
return if ( $query =~ /^(?:\.\w+)/ && $query !~ $number_re); # Probably looking at file extensions
return if $query =~ $network; # Probably want to talk about addresses, not calculations.
return if $query =~ m/^(\+?\d{1,2}(\s|-)?|\(\d{2})?\(?\d{3,4}\)?(\s|-)?\d{3}(\s|-)?\d{3,4}(\s?x\d+)?$/; # Probably are searching for a phone number, not making a calculation
return if $query =~ m/^\(\d+\)\s+\d+\d+/;
return if $query =~ m/^(?:19[8-9][0-9]|20[0-9]{2})\s(?:E|e)\d+$/; # Prevents overtriggering on car models
return if $query =~ m/(\d+)\s+(\d+)/; # if spaces between numbers then bail
return if $query =~ m/^\)|\($/; # shouldn't open with a closing brace or finish with an opening brace
return if $query =~ m/(a?cosh?|tau|a?sin|a?tan|log|ln|exp|tanh|cbrt|cubed?)e?$/i; # stops empty functions at end or with <func>e
return if $query =~ m#(?:x(\^|/)|(\^|/)x)#; # stops triggering on what is most likely algebra
return if $query =~ m#^\d{1,2}/\d{1,2}/\d{2,4}$#;
return if $query =~ m#^0\d+/\d+$#;
# some shallow preprocessing of the query
$query =~ s/^(?:what is|calculat(e|or)|solve|math)//i;
$query =~ s/\s//g;
# return based on the query type
return unless $query =~ m/[0-9τπe]|tau|pi/;
return if $query =~ $no_start_ops; # don't trigger with illegal operator at start
return if $query =~ $no_end_ops; # don't trigger with illegal operator at end
return if $query =~ $no_word_ops;
return if $query =~ m{[x × ∙ ⋅ * + \- ÷ / \^ \$ £ € \. ,]{3,}}ix;
return if $query =~ m/\$[^\d\.]/;
return if $query =~ m/\(\)/;
return if $query =~ m/^e\d+/;
return if $query =~ m#(\+|-|\*|/|x|\^)\)#xi; # can't have an operator before a closing paren
return if $query =~ m{//};
return if $query =~ m/(^|[^\d])!/g;
return if $query =~ m/0x[A-Za-z]{2,}/;
return if $query =~ m/X\d+/;
return if $query =~ m/\d+e\+\d+/;
return if $query =~ m{^(?:7|9)/11$}; # date edge case, US supermarket
return if $query =~ m/.+=.+/; # check there isn't something on both sides of the equals sign
return if $query =~ /^(?:minus|-|\+)\d+$/;
# Grab expression.
my $tmp_expr = spacing($query, 1);
# First replace named operations with their computable equivalents.
while (my ($name, $operation) = each %named_operations) {
$query =~ s#$name#$operation#xig; # We want these ones to show later.
}
$query =~ s/(?<!e)x(?!p)/\*/ig; # stops the x in exp being converted to *
return if ($tmp_expr eq $query) && ($query !~ /\de|fact|a?cos|a?tan|sin|cbrt|cubed?|log|ln|exp|mod|!/i); # If it didn't get spaced out, there are no operations to be done.
# Now sub in constants
while (my ($name, $constant) = each %named_constants) {
$query =~ s#\b$name\b#($name)#ig;
}
my @numbers = $tmp_expr =~ m/$number_re/g;
my $style = number_style_for(@numbers);
return unless $style;
my $spaced_query = prepare_for_frontend($query, $style);
return if scalar(split(" ", $spaced_query)) < 2 && ($spaced_query !~ /^(fact|cubed?|cbrt|sqrt|exp|a?cosh?|a?tan|a?sin|log|ln|mod|modulo|\d+\!$)/i);
return '', structured_answer => {
data => {
query => $spaced_query
},
templates => {
group => 'base',
options => {
content => 'DDH.calculator.content'
}
}
};
};
1;