Stealthy Calculator
with Raku and Perl

by Arne Sommer

Stealthy Calculator with Raku and Perl

[160] Published 19. December 2021.

This is my response to the Perl Weekly Challenge #143.

Challenge #143.1: Calculator

You are given a string, $s, containing mathematical expression.

Write a script to print the result of the mathematical expression. To keep it simple, please only accept + - * ().

Example 1:
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>  

Grammars

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/.

Perl

This is a straight forward translation of the Raku version.

File: calculator-eval-perl
#! /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

Challenge #143.2: Stealthy Number

You are given a positive number, $n.

Write a script to find out if the given number is Stealthy Number.

A positive integer N is stealthy, if there exist positive integers a, b, c, d such that a * b = c * d = N and a + b = c + d + 1.

Example 1:
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

Perl

This is a straight forward translation of the Raku version, so is presented without a running commentary.

File: stealthy-number-perl
#! /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.