Perl Weekly Challenge: Should five percent appear too small…

This week, George Harrison gives us a song about both incrementing and taxes: “One, two, three, four, one, two…

Now that we’ve been counted in song, let’s count in code for Perl Weekly Challenge 323.

Task 1: Increment Decrement

You are given a list of operations.

Write a script to return the final value after performing the given operations in order. The initial value is always 0.

Possible Operations:
++x or x++: increment by 1
--x or x--: decrement by 1

Example 1

Input: @operations = ("--x", "x++", "x++")
Output: 1

Operation "--x" =>  0 - 1 => -1
Operation "x++" => -1 + 1 =>  0
Operation "x++" =>  0 + 1 =>  1

Example 2

Input: @operations = ("x++", "++x", "x++")
Output: 3

Example 3

Input: @operations = ("x++", "++x", "--x", "x--")
Output: 0

Operation "x++" => 0 + 1 => 1
Operation "++x" => 1 + 1 => 2
Operation "--x" => 2 - 1 => 1
Operation "x--" => 1 - 1 => 0

Approach

I’m sure there’s some clever way to do this, but I always go for straightforward over clever. One thing to note is the x in the operation doesn’t really matter, since it’s always the same, so we could remove the x from the operations, which would convert ++x and x++ to just ++, and --x and x-- to just --, making the logic way easier. If we’re going to show the operations at work, I need to maintain a variable for the last value as well as the current value, and… that’s pretty much it.

Raku

Though the moment I started writing the code, I said… nah, let’s use a regular expression.

sub incDec(@operations) {
  my ($x, $old_x, $o) = (0, 0, q{});
  my @explain;
  for @operations -> $op {
    $x = ($op ~~ /\+\+/) ?? $x + 1 !! $x - 1;
    $o = ($op ~~ /\+\+/) ??   '+'  !!   '-';
    @explain.push(
      sprintf 'Operation "%s" => %2d %s 1 => %2d',
                         $op, $old_x, $o, $x
    );
    $old_x = $x;
  }
  return ($x, @explain.join("\n"));
}

View the entire Raku script for this task on GitHub.

$ raku/ch-1.raku 
Example 1:
Input: @operations = ("--x", "x++", "x++")
Output: 1

Operation "--x" =>  0 - 1 => -1
Operation "x++" => -1 + 1 =>  0
Operation "x++" =>  0 + 1 =>  1

Example 2:
Input: @operations = ("x++", "++x", "x++")
Output: 3

Operation "x++" =>  0 + 1 =>  1
Operation "++x" =>  1 + 1 =>  2
Operation "x++" =>  2 + 1 =>  3

Example 3:
Input: @operations = ("x++", "++x", "--x", "x--")
Output: 0

Operation "x++" =>  0 + 1 =>  1
Operation "++x" =>  1 + 1 =>  2
Operation "--x" =>  2 - 1 =>  1
Operation "x--" =>  1 - 1 =>  0

If I just wanted to produce the output and not show the operations, the code could be much shorter:

sub incDec(@operations) {
  my $x = 0;
  for @operations -> $op {
    $x = ($op ~~ /\+\+/) ?? $x + 1 !! $x - 1;
  }
  return $x;
}

Perl

Because my Raku is still very Perlish, converting it to Perl is trivial.

sub incDec(@operations) {
  my ($x, $old_x, $o) = (0, 0, q{});
  my @explain;
  for my $op ( @operations ) {
    $x = ($op =~ /\+\+/) ? $x + 1 : $x - 1;
    $o = ($op =~ /\+\+/) ?   '+'  :   '-';
    push @explain,
      sprintf 'Operation "%s" => %2d %s 1 => %2d',
                         $op, $old_x, $o, $x
    ;
    $old_x = $x;
  }
  return ($x, join("\n", @explain));
}

View the entire Perl script for this task on GitHub.

Python

The cool thing in Python is that we don’t need a regular expression; we can just say if substring in string to check whether or not a substring is part of a string.

def incDec(operations):
  x = 0
  old_x = 0
  o = ''
  explain = []
  for op in operations:
    x = x + 1 if "++" in op else x - 1
    o =  '+'  if "++" in op else  '-'
    explain.append(
      'Operation "{}" => {:2d} {} 1 => {:2d}'.format(
                  op,    old_x, o,       x
      )
    )
    old_x = x
  return x, "\n".join(explain)

View the entire Python script for this task on GitHub.

Elixir

The Elixir solution was fun, because the recursion handles all the looping. We don’t need to pass the value of old_x in the recursive call because we can just set the value from x before we modify it. Also, I’m assigning the new value of x and o in a single conditional statement, because I’m using a tuple. I could have done the same thing in Raku by saying ($x, $o) = ($op ~~ /\+\+/) ?? ($x + 1, '+') !! ($x - 1, '-'); but it didn’t occur to me at the time.

The termination clause for incDec/3 is on lines 8-9; if the list of operations is empty, return the value of x and join the explain list into a single string.

Just like Python, we don’t need a regular expression to test for the presence of a substring in a string; we can just use String.contains?/2. And since we’re basically concerned with leaving space for the sign when we’re printing x in the intermediate steps, we can use String.pad_leading/3.

  defp fmt(num) do
    String.pad_leading(to_string(num), 2)
  end

  def incDec([], x, explain), do:
    {x, Enum.join(explain, "\n")}

  def incDec([op | operations], x, explain) do
    old_x = x
    {x, o} = cond do
      String.contains?(op, "++") ->
        { x + 1, "+" }
      true ->
        { x - 1, "-" }
    end
    incDec(operations, x, explain ++ [
      "Operation \"#{op}\" => #{fmt(old_x)} #{o} 1 => #{fmt(x)}"
    ])
  end

  def incDec(operations) do
    incDec(operations, 0, [])
  end

View the entire Elixir script for this task on GitHub.


Task 2: Tax Amount

You are given an income amount and tax brackets.

Write a script to calculate the total tax amount.

Example 1

Input: $income = 10, @tax = ([3, 50], [7, 10], [12,25])
Output: 2.65

1st tax bracket upto  3, tax is 50%.
2nd tax bracket upto  7, tax is 10%.
3rd tax bracket upto 12, tax is 25%.

Total Tax => (3 * 50/100) + (4 * 10/100) + (3 * 25/100)
          => 1.50 + 0.40 + 0.75
          => 2.65

Example 2

Input: $income = 2, @tax = ([1, 0], [4, 25], [5,50])
Output: 0.25

Total Tax => (1 * 0/100) + (1 * 25/100)
          => 0 + 0.25
          => 0.25

Example 3

Input: $income = 0, @tax = ([2, 50])
Output: 0

Approach

Basically, this is another loop where we’re keeping track of what happened the last time through the loop. Looking at the first example, the rates are
0 < income <= 3 - 50%
3 < income <= 7 - 10%
7 < income <= 12 - 25%

so the first 3 are taxed at 50%, but when we look at the next income bracket, we’re not taxing 7, we’re only taxing the bit over 3 and up to 7… if $income is over 7, then that means we’re taxing 4 (7 – 3). Then in the last income bracket, we’re only taxing the bit over 7 and up to 12. Since $income is 10, that means we’re taxing (10 – 7) = 3.

Raku

I discovered while I was testing the code, that if I wanted to unpack my tax rate lists into individual variables and then be able to change them in the loop, I needed to use is copy (I tried for @tax <-> ($max, $rate), but got Cannot assign to a readonly variable or a value).

sub taxAmt($income is copy, @tax) {
  return (0, "") if $income == 0; # special case

  my $last_max = 0;
  my @operations;
  my @subtotals;
  for @tax -> ($max is copy, $rate) {
    # adjust the maximum amount at this rate
    # to be relative to the last rate
    $max -= $last_max;
    # the amount to be taxed at this rate
    my $amt = min($income, $max);
    my $tax = $amt * $rate/100;
    # make sure we display to 100ths place
    # if we hace a non-zero tax amount
    $tax = sprintf '%0.2f', $tax if $tax > 0;
    # save the steps so we can display them at the end
    @operations.push("($amt * $rate/100)");
    @subtotals.push($tax);
    # we've just taxed $amt, so remove it from $income
    $income -= $amt; 
    # adjust the last rate for the next loop
    $last_max += $max;
    # bail if there's no more income
    last if $income <= 0;
  }
  my $total = @subtotals.sum;
  $total = sprintf '%0.2f', $total if $total > 0;
  my $explain = "Total Tax => " ~ @operations.join(" + ") ~ "\n"
              ~ "          => " ~ @subtotals.join(" + ") ~ "\n"
              ~ "          => $total";

  return $total, $explain;
}

View the entire Raku script for this task on GitHub.

$ raku/ch-2.raku 
Example 1:
Input: $income = 10, @tax = ([3,50], [7,10], [12,25])
Output: 2.65

Total Tax => (3 * 50/100) + (4 * 10/100) + (3 * 25/100)
          => 1.50 + 0.40 + 0.75
          => 2.65

Example 2:
Input: $income = 2, @tax = ([1,0], [4,25], [5,50])
Output: 0.25

Total Tax => (1 * 0/100) + (1 * 25/100)
          => 0 + 0.25
          => 0.25

Example 3:
Input: $income = 0, @tax = ([2,50])
Output: 0

Perl

Again, because my Raku is written like Perl, it doesn’t change much when it becomes Perl.

use List::AllUtils qw( min sum );

sub taxAmt($income, @tax) {
  return (0, "") if $income == 0; # special case

  my $last_max = 0;
  my @operations;
  my @subtotals;
  for my $bracket (@tax) {
    my($max, $rate) = @$bracket;
    # adjust the maximum amount at this rate
    # to be relative to the last rate
    $max -= $last_max;
    # the amount to be taxed at this rate
    my $amt = min($income, $max);
    my $tax = $amt * $rate/100;
    # make sure we display to 100ths place
    # if we hace a non-zero tax amount
    $tax = sprintf '%0.2f', $tax if $tax > 0;
    # save the steps so we can display them at the end
    push @operations, "($amt * $rate/100)";
    push @subtotals, $tax;
    # we've just taxed $amt, so remove it from $income
    $income -= $amt; 
    # adjust the last rate for the next loop
    $last_max += $max;
    # bail if there's no more income
    last if $income <= 0;
  }
  my $total = sum @subtotals;
  $total = sprintf '%0.2f', $total if $total > 0;
  my $explain = "Total Tax => " . join(' + ', @operations) . "\n"
              . "          => " . join(' + ', @subtotals) . "\n"
              . "          => $total";

  return $total, $explain;
}

View the entire Perl script for this task on GitHub.

Python

The thing I needed to contend with going from Raku to Python is that Python is typed, so you can’t just add number-like strings and append numbers to strings like you can in Raku and Perl. However, by maintaining a running total instead of adding it up at the end, and pushing the numbers through a fmt() function I wrote to either display the number to the 100ths place or return the string 0, the conversion was pretty easy.

def fmt(num):
  if num > 0:
    # display to 100ths place
    return '{:0.2f}'.format(num)
  else:
    # format it as a string
    return f"{int(num)}"

def taxAmt(income, tax):
  if income == 0:
    return 0, "" # special case

  last_max = 0
  total = 0
  operations = []
  subtotals = []
  for max_income, rate in tax:
    # adjust the maximum amount at this rate
    # to be relative to the last rate
    max_income -= last_max
    # the amount to be taxed at this rate
    amt = min(income, max_income)
    tax = amt * rate/100
    total += tax
    # make sure we display to 100ths place
    # if we hace a non-zero tax amount
    tax = fmt(tax)
    # save the steps so we can display them at the end
    operations.append(f"({amt} * {rate}/100)")
    subtotals.append(tax)
    # we've just taxed $amt, so remove it from $income
    income -= amt
    # adjust the last rate for the next loop
    last_max += max_income
    # bail if there's no more income
    if income <= 0: break

  return (
    total,
    "Total Tax => " + ' + '.join(operations) + "\n" +
    "          => " + ' + '.join(subtotals) + "\n" +
    "          => " + fmt(total)
  )

View the entire Python script for this task on GitHub.

Elixir

The big challenge wound up being formatting the numeric values out to two decimal places. Float.to_string/1 doesn’t have any provision for padding numbers out to a particular precision, but it does contain a link to the documentation for the underlying :erlang.float_to_binary/2 call, which is configurable! Like I did with Python, I put it in a private function so I could access the functionality with the name fmt/1.

Because I wanted to be able to interpolate it in strings, I defined plusJoin/1 on lines 13-14 so the quotation marks wouldn’t mess things up.

Lines 26-30 serves the function of jumping right to the termination clause (defined on lines 16-24) when we’ve run out of income.

And I wound up doing a lot of the list building and amount adjustments in the recursive call to the next iteration through the loop (lines 45-57), which from what I see seems to be a pretty Elixir-y way to do things.

  defp fmt(num) do
    cond do
      num == 0 ->
        0 # don't format a zero value
      true ->
        :erlang.float_to_binary(num, [decimals: 2])
    end
  end

  defp plusJoin(arr), do:
    Enum.join(arr, " + ")

  def taxAmt([], _, _, operations, subtotals, total) do
    # there's no more brackets, return the summary
    {
      total,
      "Total Tax => #{plusJoin(operations)}\n" <>
      "          => #{plusJoin(subtotals)}\n" <>
      "          => #{fmt(total)}"
    }
  end

  def taxAmt(_, income, _, operations, subtotals, total)
    when income == 0 do
    # there's no more income to tax, skip to the end
    taxAmt([], 0, 0, operations, subtotals, total)
  end

  def taxAmt([tax | remaining], income, last_max, operations,
             subtotals, total) do
    # the maximum amount to tax at this rate is the first
    # element of the tax bracket; but we need to adjust this
    # amount to be relative to the amount of last bracket
    max  = List.first(tax) - last_max
    # the tax rate is the second element of the bracket
    rate = List.last(tax)
    # the amount to be taxed at this rate
    amt = min(income, max)
    # calculate the tax
    tax = amt * rate/100

    taxAmt(
      remaining,
      # we've just taxed amt, so remove it from income
      income - amt,
      # adjust the last rate for the next loop
      max + last_max,
      # save the steps so we can display them at the end
      operations ++ ["(#{amt} * #{rate}/100)"],
      # make sure we display to 100ths place
      # if we hace a non-zero tax amount
      subtotals  ++ ["#{fmt(tax)}"],
      total + tax
    )
  end

  def taxAmt(income, tax) do
    if income == 0 do
      { 0, "" } # special case
    else
      taxAmt(tax, income, 0, [], [], 0)
    end
  end

View the entire Elixir script for this task on GitHub.


Here’s all my solutions in GItHub: https://github.com/packy/perlweeklychallenge-club/tree/master/challenge-323/packy-anderson

Leave a Reply