This is my response to the Perl Weekly Challenge #143.
$s
, containing mathematical expression.
+ - * ()
.
Input: $s = "10 + 20 - 5"
Output: 25
Example 2:
Input: $s = "(10 + 20 - 5) * 2"
Output: 50
I do think we should allow digits, even if the challenge does not say so.
Note that division (/
) is not allowed. Too
divisive, perhaps?
This is easy, as Raku can do the whole shebang for us:
File: calculator-eval
#! /usr/bin/env raku
unit sub MAIN ($expression);
say $expression.EVAL; # [1]
[1] Calling EVAL
on an expression will (try to) evaluate it.
See
docs.raku.org/routine/EVAL for
more information about EVAL
.
Running it:
$ ./calculator-eval "10 + 20 - 5"
25
$ ./calculator-eval "(10 + 20 - 5) * 2"
50
The problem with EVAL (pronounced «evil») is that it will execute almost anything, as shown by this example:
$ ./calculator-eval "mkdir('as')"
"as".IO
Here is a safer version, where we only allow the characters specified in the challenge, as well as spaces and digits:
File: calculator-eval-safer
#! /usr/bin/env raku
unit sub MAIN ($expression where $expression
~~ /^<[0..9 \( \) \+ \- \* \s]>+$/); # [1]
say $expression.EVAL;
[1] Only allow digits, parens and the three basic operators addition, subtraction and multiplication. And spaces.
Running it gives the same result as the first version, given legal input:
$ ./calculator-eval-safer "10 + 20 -5"
25
$ ./calculator-eval-safer "(10 + 20 -5) * 2"
50
$ ./calculator-eval-safer "mkdir('as')"
Usage:
./calculator-eval-safer <expression>
It is possible to do it manually, as I think the challenge do want us to, by parsing the expression and doing the math. This is a task that is suited for the Grammar treatment, as that will do the heavy lifting free of charge.
Andrew Shitov (famous for several Raku books and conferences) did that three years ago; see andrewshitov.com/2018/10/31/creating-a-calculator-with-perl-6-grammars/.
#! /usr/bin/env perl
use strict;
use warnings;
use feature 'say';
my $s = $ARGV[0] // exit;
$s =~ /^[0-9\(\)\+\-\*\s]+$/
? say eval($s)
: say "Error";
It gives the expected results:
$ ./calculator-eval-perl "10 + 20 -5"
25
$ ./calculator-eval-perl "(10 + 20 -5) * 2"
50
$n
.
Stealthy Number
.
Input: $n = 36
Output: 1
Since 36 = 4 (a) * 9 (b) = 6 (c) * 6 (d) and 4 (a) + 9 (b)
= 6 (c) + 6 (d) + 1.
Example 2:
Input: $n = 12
Output: 1
Since 2 * 6 = 3 * 4 and 2 + 6 = 3 + 4 + 1
Example 3:
Input: $n = 6
Output: 0
Since 2 * 3 = 1 * 6 but 2 + 3 != 1 + 6 + 1
Note that the input is not a positive number, but a positive integer.
We need a list of pairwise divisors that we can divide the number into (or 2 divisors that we can multiply with each other to get the number, to look at it from the other side). Let us do that first. I'll show some examples, before the code.
$ ./divisor-pairs 36
Divisor pairs: [(1, 36), (2, 18), (3, 12), (4, 9), (6, 6)]
$ ./divisor-pairs 12
Divisor pairs: [(1, 12), (2, 6), (3, 4)]
$ ./divisor-pairs 6
Divisor pairs: [(1, 6), (2, 3)]
Note that we do not want duplicates; 6*1 is the same as 1*6.
Primes cannot be divided, so give only one pair:
$ ./divisor-pairs 13
Divisor pairs: [(1, 13),]
$ ./divisor-pairs 1
Divisor pairs: ((1, 1),)
Note that «1» is not a prime, but it behaves just like one (pun intended) here.
File: divisor-pairs
#! /usr/bin/env raku
subset PosInt of Int where * > 0; # [1]
unit sub MAIN (PosInt $n); # [1]
say "Divisor pairs: { divisor-pairs($n).raku }"; # [2]
sub divisor-pairs ($number)
{
my @divisors; # [3]
my %seen; # [4]
return ((1,1),) if $number == 1; # [5]
for (1 .. $number/2) -> $candidate # [6]
{
if $number %% $candidate # [7]
{
my $b = $number div $candidate; # [8]
next if %seen{$candidate}; # [9]
%seen{$b} = True; # [10]
@divisors.push: ($candidate, $b); # [11]
}
}
return @divisors; # [12]
}
[1] Ensure a positve integer.
[2] Print the pairs.
[3] The pairs will go here.
[4] Numbers already reported (the right hand value of the pair), to avoid duplicates.
[5] Special case the input value 1, as it is indeed special (only divisible by itself, and no other value)
[6] For each possible divisor,
[7] check if it is a divisor?
[8] • Get the second value.
[9] • Skip it if we have seen it already.
[10] • Mark it as seen.
[11] &bill; Add the pair to the list.
[12] Return the list.
Then we can do the real program:
File: stealthy-number
#! /usr/bin/env raku
subset PosInt of Int where * > 0;
unit sub MAIN (PosInt $n, :v(:$verbose));
my @pairs = divisor-pairs($n); # [1]
say ": Divisor pairs: { @pairs.raku }" if $verbose;
if @pairs.elems > 1 # [2]
{
for @pairs.combinations(2) -> $pair # [3]
{
my $a = $pair[0][0]; # [4a]
my $b = $pair[0][1]; # [4b]
my $c = $pair[1][0]; # [4c]
my $d = $pair[1][1]; # [4d]
if ($a * $b == $c * $d == $n && $a + $b == $c + $d + 1) # [5]
{
say ": a:$a b:$b c:$c d:$d" if $verbose;
say 1; # [6]
exit; # [7]
}
}
}
say 0; # [8]
sub divisor-pairs ($number)
{
my @divisors;
my %seen;
# return ((1,1),) if $number == 1; # [9]
for (1 .. $number/2) -> $candidate
{
if $number %% $candidate
{
my $b = $number div $candidate;
next if %seen{$candidate};
%seen{$b} = True;
@divisors.push: ($candidate, $b);
}
}
return @divisors;
}
[1] Get the pairs.
[2] Do we have more than one pair?
[3] Iterate over every possible combination of two and two pairs.
[4] Get the individual values.
[5] Do the equation hold?
[6] If so, print «1»
[7] and we are done.
[8] No match? print «0».
[9] We need at least two pairs, so this one (pun intended, yet again) simply will not help us. It does no harm, so feel free to uncomment it.
Running it:
$ ./stealthy-number 36
1
$ ./stealthy-number 12
1
$ ./stealthy-number 6
0
With verbose mode:
$ ./stealthy-number -v 36
: Divisor pairs: [(1, 36), (2, 18), (3, 12), (4, 9), (6, 6)]
: a:4 b:9 c:6 d:6
1
$ ./stealthy-number -v 12
: Divisor pairs: [(1, 12), (2, 6), (3, 4)]
: a:2 b:6 c:3 d:4
1
$ ./stealthy-number -v 6
: Divisor pairs: [(1, 6), (2, 3)]
0
#! /usr/bin/env perl
use strict;
use warnings;
use feature 'say';
use feature 'signatures';
use Getopt::Long;
use Algorithm::Combinatorics qw(combinations);
no warnings qw(experimental::signatures);
my $verbose = 0;
GetOptions("verbose" => \$verbose);
my $n = $ARGV[0] // exit;
die("Not a positive integere") unless $n =~ /^[1-9][0-9]*$/;
my @pairs = divisor_pairs($n);
if (@pairs > 1)
{
for my $pair (combinations(\@pairs, 2))
{
my $a = @$pair[0]->[0];
my $b = @$pair[0]->[1];
my $c = @$pair[1]->[0];
my $d = @$pair[1]->[1];
if ($a * $b == $c * $d && $a * $b == $n && $a + $b == $c + $d + 1)
{
say ": a:$a b:$b c:$c d:$d" if $verbose;
say 1;
exit;
}
}
}
say 0;
sub divisor_pairs ($number)
{
my @divisors;
my %seen;
for my $candidate (1 .. $number/2)
{
if ($number % $candidate == 0)
{
my $b = $number / $candidate;
next if $seen{$candidate};
$seen{$b} = 1;
push(@divisors, [$candidate, $b]);
}
}
return @divisors;
}
Running it gives the same result as the Raku version, except that verbose mode does not print the pairs. That is due to programmer laziness.
$ ./stealthy-number-perl -v 36
: a:4 b:9 c:6 d:6
1
$ ./stealthy-number-perl -v 12
: a:2 b:6 c:3 d:4
1
$ ./stealthy-number-perl -v 6
0
And that's it.