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