From 8745daaaea28f99c4874b9644675223283c5a661 Mon Sep 17 00:00:00 2001 From: Matt Miller Date: Sat, 21 Jun 2014 07:56:50 +0800 Subject: [PATCH 1/3] Calculator: use proper quoting of regex chars. The old version clearly worked, but it was clunky and somewhat inexplciable. While reading up on something else, I discovered `perldoc quotemeta` which resolves this in a much more predictable and correct way. --- lib/DDG/Goodie/Calculator.pm | 39 ++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/lib/DDG/Goodie/Calculator.pm b/lib/DDG/Goodie/Calculator.pm index 05dc17b65..b9356dca9 100644 --- a/lib/DDG/Goodie/Calculator.pm +++ b/lib/DDG/Goodie/Calculator.pm @@ -49,18 +49,14 @@ triggers query_nowhitespace => qr< # 'thousands' assumption) I am going to pretend that I do need it. # If it could fit more than one the first in order gets preference. my @known_styles = ({ - id => 'perl', - decimal => '\.', - sub_decimal => '.', - thousands => ',', - sub_thousands => ',', + id => 'perl', + decimal => '.', + thousands => ',', }, { - id => 'euro', - decimal => ',', - sub_decimal => ',', - thousands => '\.', - sub_thousands => '.', + id => 'euro', + decimal => ',', + thousands => '.', }, ); @@ -155,7 +151,7 @@ handle query_nowhitespace => sub { return unless (defined $precision || ($tmp_result =~ /^(?:\-|)[0-9\.]+$/)); # Ok, this might be overkill on flexibility. - $tmp_result = sprintf('%0' . $perl_style->{sub_decimal} . $precision . 'f', $tmp_result) if ($precision); + $tmp_result = sprintf('%0' . $perl_style->{decimal} . $precision . 'f', $tmp_result) if ($precision); # Dollars. $tmp_result = '$' . $tmp_result if ($query =~ /^\$/); @@ -244,13 +240,13 @@ sub _well_formed_for_style_func { return sub { my $number = shift; return ( - $number =~ /^[\d$thousands$decimal]+$/ + $number =~ /^(\d|\Q$thousands\E|\Q$decimal\E)+$/ # Only contains things we understand. - && ($number !~ /$thousands/ || ($number !~ /$thousands\d{1,2}\b/ && $number !~ /$thousands\d{4,}/ && $number !~ /^0\Q$thousands\E/)) + && ($number !~ /\Q$thousands\E/ || ($number !~ /\Q$thousands\E\d{1,2}\b/ && $number !~ /\Q$thousands\E\d{4,}/ && $number !~ /^0\Q$thousands\E/)) # You can leave out thousands breaks, but the ones you put in must be in the right place # which does not include following an initial 0. # Note that this does not confirm that they put all the 'required' ones in. - && ($number !~ /$decimal/ || $number !~ /$decimal(?:.*)?(?:$decimal|$thousands)/) + && ($number !~ /\Q$decimal\E/ || $number !~ /\Q$decimal\E(?:.*)?(?:\Q$decimal\E|\Q$thousands\E)/) # You can omit the decimal but you cannot have another decimal or thousands after: ) ? 1 : 0; }; @@ -259,14 +255,13 @@ sub _well_formed_for_style_func { # Returns function which given a number in a certain style, makes it nice for human eyes. sub _display_style_func { my ($style, $perl_style) = @_; - my ($decimal, $sub_decimal, $sub_thousands, $perl_dec) = - (@{$style}{qw(decimal sub_decimal sub_thousands)}, $perl_style->{decimal}); # Unpacked for easier regex-building + my ($decimal, $thousands, $perl_dec) = (@{$style}{qw(decimal thousands)}, $perl_style->{decimal}); # Unpacked for easier regex-building return sub { my $text = shift; $text = reverse $text; - $text =~ s/$perl_dec/$sub_decimal/g; - $text =~ s/(\d\d\d)(?=\d)(?!\d*$decimal)/$1$sub_thousands/g; + $text =~ s/\Q$perl_dec\E/$decimal/g; + $text =~ s/(\d\d\d)(?=\d)(?!\d*\Q$decimal\E)/$1$thousands/g; return scalar reverse $text; }; @@ -275,13 +270,13 @@ sub _display_style_func { # Returns function which given a number in a certain style, makes it safe for perl eval. sub _prepare_for_computation_func { my ($style, $perl_style) = @_; - my ($decimal, $thousands, $perl_dec) = (@{$style}{qw(decimal thousands)}, $perl_style->{sub_decimal}); + my ($decimal, $thousands, $perl_dec) = (@{$style}{qw(decimal thousands)}, $perl_style->{decimal}); return sub { my $number_text = shift; - $number_text =~ s/$thousands//g; # Remove thousands seps, since they are just visual. - $number_text =~ s/$decimal/$perl_dec/g; # Make sure decimal mark is something perl knows how to use. + $number_text =~ s/\Q$thousands\E//g; # Remove thousands seps, since they are just visual. + $number_text =~ s/\Q$decimal\E/$perl_dec/g; # Make sure decimal mark is something perl knows how to use. return $number_text; }; @@ -295,7 +290,7 @@ sub _precision_for_style_func { return sub { my $number_text = shift; - return ($number_text =~ /$decimal(\d+)/) ? length($1) : 0; + return ($number_text =~ /\Q$decimal\E(\d+)/) ? length($1) : 0; }; } From 31b344376401abc700f1563fc08e71cdf2f55fac Mon Sep 17 00:00:00 2001 From: Matt Miller Date: Sat, 21 Jun 2014 08:22:54 +0800 Subject: [PATCH 2/3] Calculator: refactor the output preprocessing. I think this makes it somewhat easier to follow the intent here. We're just trying to convert these two known strings into different output formats, so wrapping that up seems like a good idea. --- lib/DDG/Goodie/Calculator.pm | 71 +++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/lib/DDG/Goodie/Calculator.pm b/lib/DDG/Goodie/Calculator.pm index b9356dca9..58b396acb 100644 --- a/lib/DDG/Goodie/Calculator.pm +++ b/lib/DDG/Goodie/Calculator.pm @@ -155,48 +155,59 @@ handle query_nowhitespace => sub { # Dollars. $tmp_result = '$' . $tmp_result if ($query =~ /^\$/); - # Query for display. - my $tmp_q = $query; - - # Drop equals. - $tmp_q =~ s/\=$//; - $tmp_q =~ s/((?:\d+?|\s))E(-?\d+)/\($1 * 10^$2\)/; - - # Copy - $results_no_html = $results_html = $tmp_q; - - # Superscript (before spacing). - $results_html =~ s/\^($numbery|\b$ored_constants\b)/$1<\/sup>/g; - - ($results_no_html, $results_html) = map { spacing($_) } ($results_no_html, $results_html); - return if $results_no_html =~ /^\s/; - # Add proper separators. $tmp_result = $style->{make_pretty}->($tmp_result); - # Now add = back. - $results_no_html .= ' = '; + my $results = prepare_for_display($query, $tmp_result); - return $results_no_html . $tmp_result, - html => wrap_html($results_html, $tmp_result), + return if $results->{text} =~ /^\s/; + return $results->{text}, + html => $results->{html}, heading => "Calculator"; } return; }; -# Add some HTML and styling to our output -# so that we can make it prettier (unabashedly stolen from -# the ReverseComplement goodie.) -sub append_css { - my $html = shift; - state $css = share("style.css")->slurp; - return "$html"; +sub prepare_for_display { + my ($query, $result) = @_; + + # Equals varies by output type. + $query =~ s/\=$//; + # Show them how 'E' was interpreted. + $query =~ s/((?:\d+?|\s))E(-?\d+)/\($1 * 10^$2\)/; + + return { + text => format_text($query, $result), + html => format_html($query, $result), + }; } -sub wrap_html { - my ($entered, $result) = @_; - return append_css("
$entered = $result
"); +# Format query for HTML +sub format_html { + my ($query, $result) = @_; + + state $css = ''; + + if ($query =~ /\^/) { + $query =~ s/\^($numbery|\b$ored_constants\b)/$1<\/sup>/g; + $query =~ s/\^\(([^\)]+)\)/($1)<\/sup>/g; + } + + return + $css + . "
" + . spacing($query) + . " = " + . $result + . "
"; +} + +# Format query for text +sub format_text { + my ($query, $result) = @_; + + return spacing($query) . ' = ' . $result; } #separates symbols with a space From 13c9ea7f554863a63070867cc18c2775f031a060 Mon Sep 17 00:00:00 2001 From: Matt Miller Date: Sat, 21 Jun 2014 08:32:50 +0800 Subject: [PATCH 3/3] Calculator: superscripting for arbitrary terms. This would be better with some proper parsing, but this resolves some issues with the current HTML output, making it raise things to arbitrarily high superscripting levels based on what it seems to look like. --- lib/DDG/Goodie/Calculator.pm | 60 +++++++++++++++++++++++++++++++++--- t/Calculator.t | 40 ++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 4 deletions(-) diff --git a/lib/DDG/Goodie/Calculator.pm b/lib/DDG/Goodie/Calculator.pm index 58b396acb..eee5c6b42 100644 --- a/lib/DDG/Goodie/Calculator.pm +++ b/lib/DDG/Goodie/Calculator.pm @@ -189,10 +189,7 @@ sub format_html { state $css = ''; - if ($query =~ /\^/) { - $query =~ s/\^($numbery|\b$ored_constants\b)/$1<\/sup>/g; - $query =~ s/\^\(([^\)]+)\)/($1)<\/sup>/g; - } + $query = _add_html_exponents($query); return $css @@ -203,6 +200,61 @@ sub format_html { . ""; } +sub _add_html_exponents { + + my $string = shift; + + return $string if ($string !~ /\^/ or $string =~ /^\^|\^$/); # Give back the same thing if we won't deal with it properly. + + my @chars = split //, $string; + my ($start_tag, $end_tag) = ('', ''); + my ($newly_up, $in_exp_number, $in_exp_parens, %power_parens); + my ($parens_count, $number_up) = (0, 0); + + # because of associativity and power-to-power, we need to scan nearly the whole thing + for my $index (1 .. $#chars - 1) { + my $this_char = $chars[$index]; + if ($this_char =~ $numbery) { + if ($newly_up) { + $in_exp_number = 1; + $newly_up = 0; + } + } elsif ($this_char eq '(') { + $parens_count += 1; + $in_exp_number = 0; + if ($newly_up) { + $in_exp_parens += 1; + $power_parens{$parens_count} = 1; + $newly_up = 0; + } + } elsif ($this_char eq '^') { + $chars[$index - 1] =~ s/$end_tag$//; # Added too soon! + $number_up += 1; + $newly_up = 1; + $chars[$index] = $start_tag; # Replace ^ with the tag. + } elsif ($in_exp_number) { + $in_exp_number = 0; + $number_up -= 1; + $chars[$index] = $end_tag . $chars[$index]; + } elsif ($number_up && !$in_exp_parens) { + # Must have ended another term or more + $chars[$index] = ($end_tag x ($number_up - 1)) . $chars[$index]; + $number_up = 0; + } elsif ($this_char eq ')') { + # We just closed a set of parens, see if it closes one of our things + if ($in_exp_parens && $power_parens{$parens_count}) { + $chars[$index] .= $end_tag; + delete $power_parens{$parens_count}; + $in_exp_parens -= 1; + } + $parens_count -= 1; + } + } + $chars[-1] .= $end_tag x $number_up if ($number_up); + + return join('', @chars); +} + # Format query for text sub format_text { my ($query, $result) = @_; diff --git a/t/Calculator.t b/t/Calculator.t index ef366b256..b9b9f2a32 100644 --- a/t/Calculator.t +++ b/t/Calculator.t @@ -305,6 +305,11 @@ ddg_goodie_test( heading => 'Calculator', html => qr#2 squared3 = #, ), + '2 squared ^ 3.06' => test_zci( + '2 squared ^ 3.06 = 323.972172143725', + heading => 'Calculator', + html => qr#2 squared3\.06 = #, + ), '2^3 squared' => test_zci( '2 ^ 3 squared = 512', heading => 'Calculator', @@ -325,6 +330,41 @@ ddg_goodie_test( heading => 'Calculator', html => qr/./, ), + '(pi^4+pi^5)^(1/6)' => test_zci( + '(pi ^ 4 + pi ^ 5) ^ (1 / 6) = 2.71828180861191', + heading => 'Calculator', + html => qr#\(pi4 \+ pi5\)\(1 / 6\) =#, + ), + '(pi^4+pi^5)^(1/6)+1' => test_zci( + '(pi ^ 4 + pi ^ 5) ^ (1 / 6) + 1 = 3.71828180861191', + heading => 'Calculator', + html => qr#\(pi4 \+ pi5\)\(1 / 6\) \+ 1 =#, + ), + '(pi^4.1^(5-4)+pi^(5-(4^2 -8)))^(1/6)+1' => test_zci( + '(pi ^ 4.1 ^ (5 - 4) + pi ^ (5 - (4 ^ 2 - 8))) ^ (1 / 6) + 1 = 3.18645452799383', + heading => 'Calculator', + html => qr#\(pi4.1\(5 - 4\) \+ pi\(5 - \(42 - 8\)\)\)\(1 / 6\) \+ 1 =#, + ), + '5^4^(3-2)^1' => test_zci( + '5 ^ 4 ^ (3 - 2) ^ 1 = 625', + heading => 'Calculator', + html => qr#54\(3 - 2\)1 =#, + ), + '(5-4)^(3-2)^1' => test_zci( + '(5 - 4) ^ (3 - 2) ^ 1 = 1', + heading => 'Calculator', + html => qr#\(5 - 4\)\(3 - 2\)1 =#, + ), + '(5+4-3)^(2-1)' => test_zci( + '(5 + 4 - 3) ^ (2 - 1) = 6', + heading => 'Calculator', + html => qr#\(5 \+ 4 - 3\)\(2 - 1\) =#, + ), + '5^((4-3)*(2+1))+6' => test_zci( + '5 ^ ((4 - 3) * (2 + 1)) + 6 = 131', + heading => 'Calculator', + html => qr#5\(\(4 - 3\) \* \(2 \+ 1\)\) \+ 6 =#, + ), 'sin(1.0) + 1,05' => undef, '4,24,334+22,53,828' => undef, '5234534.34.54+1' => undef,