192 lines
6.7 KiB
Perl
192 lines
6.7 KiB
Perl
package DDG::Goodie::Calculator;
|
|
# ABSTRACT: do simple arthimetical calculations
|
|
|
|
use feature 'state';
|
|
|
|
use DDG::Goodie;
|
|
with 'DDG::GoodieRole::NumberStyler';
|
|
|
|
use List::Util qw( max );
|
|
use Math::Trig;
|
|
|
|
zci is_cached => 1;
|
|
zci answer_type => "calc";
|
|
|
|
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
|
|
web => ['https://www.duckduckgo.com', 'DuckDuckGo'],
|
|
github => ['https://github.com/duckduckgo', 'duckduckgo'],
|
|
twitter => ['http://twitter.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 )+
|
|
|
|
(?: [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}\(|\^|\*|\//; # 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 $results_html;
|
|
my $results_no_html;
|
|
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.
|
|
|
|
$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;
|
|
}
|
|
|
|
$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 =.
|
|
|
|
# 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;
|
|
}
|
|
|
|
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 : 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\-(?<exp>\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},
|
|
html => $results->{html},
|
|
heading => "Calculator";
|
|
};
|
|
|
|
sub prepare_for_display {
|
|
my ($query, $result, $style) = @_;
|
|
|
|
# Equals varies by output type.
|
|
$query =~ s/\=$//;
|
|
# Show them how 'E' was interpreted. This should use the number styler, too.
|
|
$query =~ s/((?:\d+?|\s))E(-?\d+)/\($1 * 10^$2\)/i;
|
|
|
|
return {
|
|
text => format_text($query, $result, $style),
|
|
html => format_html($query, $result, $style),
|
|
};
|
|
}
|
|
|
|
# Format query for HTML
|
|
sub format_html {
|
|
my ($query, $result, $style) = @_;
|
|
|
|
$query = spacing($style->with_html($query));
|
|
$result = $style->with_html($style->for_display($result));
|
|
|
|
return "<div class='zci--calculator text--primary'>"
|
|
. $query
|
|
. "<span class='text--secondary'> = </span><a href='javascript:;' onclick='document.x.q.value=\"$result\";document.x.q.focus();' class='text--primary'>"
|
|
. $result
|
|
. "</a></div>";
|
|
}
|
|
|
|
# Format query for text
|
|
sub format_text {
|
|
my ($query, $result, $style) = @_;
|
|
|
|
return spacing($query) . ' = ' . $style->for_display($result);
|
|
}
|
|
|
|
#separates symbols with a space
|
|
#spacing '1+1' -> '1 + 1'
|
|
sub spacing {
|
|
my ($text, $space_for_parse) = @_;
|
|
|
|
$text =~ s/(\s*(?<!<)(?:[\+\-\^xX\*\/\%]|times|plus|minus|dividedby)+\s*)/ $1 /ig;
|
|
$text =~ s/\s*dividedby\s*/ divided by /ig;
|
|
$text =~ s/(\d+?)((?:dozen|pi|gross|squared|score))/$1 $2/ig;
|
|
$text =~ s/([\(\)])/ $1 /g if ($space_for_parse);
|
|
|
|
return $text;
|
|
}
|
|
|
|
1;
|