package DDG::Goodie::Calculator; # ABSTRACT: do simple arthimetical calculations use strict; use DDG::Goodie; with 'DDG::GoodieRole::NumberStyler'; use List::Util qw( max ); use Math::Trig; zci answer_type => "calc"; zci is_cached => 1; primary_example_queries '$3.43+$34.45'; secondary_example_queries '64*343'; description 'Basic calculations'; name 'Calculator'; code_url 'https://github.com/duckduckgo/zeroclickinfo-goodies/blob/master/lib/DDG/Goodie/Calculator.pm'; category 'calculations'; topics 'math'; attribution github => ['https://github.com/duckduckgo', 'duckduckgo']; triggers query_nowhitespace => qr< ^ ( what is | calculate | solve | math )? [\( \) x X * % + / \^ \$ -]* (?: [0-9 \. ,]* ) (?: gross | dozen | pi | e | c | squared | score |) [\( \) x X * % + / \^ 0-9 \. , _ \$ -]* (?(1) (?: -? [0-9 \. , _ ]+ |) |) (?: [\( \) x X * % + / \^ \$ -] | times | divided by | plus | minus | cos | sin | tan | cotan | log | ln | log[_]?\d{1,3} | exp | tanh | sec | csc | squared | sqrt | pi | e )+ (?: [0-9 \. ,]* ) (?: gross | dozen | pi | e | c | squared | score |) [\( \) x X * % + / \^ 0-9 \. , _ \$ -]* =? $ >xi; my $number_re = number_style_regex(); my $funcy = qr/[[a-z]+\(|log[_]?\d{1,3}\(|\^|\*|\/|squared|divided/; # Stuff that looks like functions. my %named_operations = ( '\^' => '**', 'x' => '*', 'times' => '*', 'minus' => '-', 'plus' => '+', 'divided\sby' => '/', 'ln' => 'log', # perl log() is natural log. 'squared' => '**2', ); my %named_constants = ( dozen => 12, e => 2.71828182845904523536028747135266249, # This should be computed. pi => pi, # pi constant from Math::Trig gross => 144, score => 20, ); my $ored_constants = join('|', keys %named_constants); # For later substitutions 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 handle query_nowhitespace => sub { my $query = $_; return if ($query =~ /\b0x/); # Probable attempt to express a hexadecimal number, query_nowhitespace makes this overreach a bit. return if ($query =~ $network); # Probably want to talk about addresses, not calculations. return if ($query =~ qr/(?:(?\d+)%(?(\+|\-|\*|\/))(?\d+)) | (?:(?\d+)(?(\+|\-|\*|\/))(?\d+)%)/); # Probably want to calculate a percent ( will be used PercentOf ) $query =~ s/^(?:whatis|calculate|solve|math)//; # Grab expression. my $tmp_expr = spacing($query, 1); return if $tmp_expr eq $query; # If it didn't get spaced out, there are no operations to be done. # First replace named operations with their computable equivalents. while (my ($name, $operation) = each %named_operations) { $tmp_expr =~ s# $name # $operation #xig; $query =~ s#$name#$operation#xig; # We want these ones to show later. } $tmp_expr =~ s#log[_]?(\d{1,3})#(1/log($1))*log#xg; # Arbitrary base logs. $tmp_expr =~ s/ (\d+?)E(-?\d+)([^\d]|\b) /\($1 * 10**$2\)$3/xg; # E == *10^n $tmp_expr =~ s/\$//g; # Remove $s. $tmp_expr =~ s/=$//; # Drop =. $tmp_expr =~ s/([0-9])\s*([a-zA-Z])([^0-9])/$1*$2$3/g; # Support 0.5e or 0.5pi; but don't break 1e8 # Now sub in constants while (my ($name, $constant) = each %named_constants) { $tmp_expr =~ s# (\d+?)\s+$name # $1 * $constant #xig; $tmp_expr =~ s#\b$name\b# $constant #ig; $query =~ s#\b$name\b#($name)#ig; } my @numbers = grep { $_ =~ /^$number_re$/ } (split /\s+/, $tmp_expr); my $style = number_style_for(@numbers); return unless $style; $tmp_expr = $style->for_computation($tmp_expr); # Using functions makes us want answers with more precision than our inputs indicate. my $precision = ($query =~ $funcy) ? undef : ($query =~ /^\$/) ? 2 : max(map { $style->precision_of($_) } @numbers); my $tmp_result; eval { # e.g. sin(100000)/100000 completely makes this go haywire. alarm(1); $tmp_result = eval($tmp_expr); alarm(0); # Assume the string processing will be "fast enough" }; # Guard against non-result results return unless (defined $tmp_result && $tmp_result ne 'inf' && $tmp_result ne ''); # Try to determine if the result is supposed to be 0, but isn't because of FP issues. # If there's a defined precision, let sprintf worry about it. # Otherwise, we'll say that smaller than 1e-14 was supposed to be zero. # -14 selected to account for the result of sin(pi) $tmp_result = 0 if (not defined $precision and ($tmp_result =~ /e\-(?\d+)$/ and $+{exp} > 14)); $tmp_result = sprintf('%0.' . $precision . 'f', $tmp_result) if ($precision); # Dollars. $tmp_result = '$' . $tmp_result if ($query =~ /^\$/); my $results = prepare_for_display($query, $tmp_result, $style); return if $results->{text} =~ /^\s/; return $results->{text}, structured_answer => $results->{structured}, heading => "Calculator"; }; sub prepare_for_display { my ($query, $result, $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+?|\s))E(-?\d+)/\($1 * 10^$2\)/i; $query =~ s/\s*\*{2}\s*/^/g; # Use prettier exponentiation. $result = $style->for_display($result); foreach my $name (keys %named_constants) { $query =~ s#\($name\)#$name#xig; } return +{ text => spacing($query) . ' = ' . $result, structured => { input => [spacing($query)], operation => 'Calculate', result => "" . $style->with_html($result) . "" }, }; } #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*(?