Convert a big integer to alphanumeric string in Perl - perl

How to encode a number in Perl to 8 character Alphanumeric string starting with an alphabet and ending with a digit where the ending digit is check digit.
So how to generate check digit and I planned to start counter from 21767823360 so that my resultant string starts with A000000 but perl is not taking such a big number for calculation.
Please suggest a solution.
$AppID=alphanumero($appid,8,1);
sub alphanumero{
my ($n,$length,$type)=#_;
my #to_b36 = (0 .. 9, 'A' .. 'Z');
use integer; # so that /= 36 is easy
my $u=$n%10;$n=21767823360+($n-$u)/10;
my $t = "";do { $t = $to_b36[$n % 36] . $t, $n /= 36 } while $n;
return "$t$u";
}

Perl has little problems with big numbers, and if your numbers are really huge, just use bignum. This transparently enables infinite-precision arithmetics.
Your number 21767823360 needs about 35 bits. My perl is built with 64-bit integers (see perl -v to check your support), so your number isn't "too large" for me.
The algorithm to convert a number to base-n is simple:
# pseudocode
let "digits" be the array containing all the digits of our representation.
# the size of digits is the base of our new representation
# the digits are sorted in ascending order.
#digits[0] is zero.
var "n" is the number we want to represent.
var "size" is the number of digits of the new representation.
# floor(ln(n)/ln(digits.size))
var "representation" is the empty string.
while size >= 0:
representation ← representation.concat(digits[n / digits.length ^ size]).
n ← n.modulo(digits.length ^ size).
size ← size - 1.
return representation.
Example Perl:
#!/usr/bin/perl
use strict; use warnings;
use Carp;
sub base_n {
my ($number, $base, $max_digits, $pad) = #_;
defined $number or croak "Undefined number for base_n";
$number == int $number and $number >= 0
or croak "The number has to be a natural number for base_n";
defined $base or croak "Undefined base for base_n";
$base == int $base and $base > 0
or croak "The base has to be a positive integer for base_n";
my #digits = (0 .. 9, "A" .. "Z");
$base <= #digits or croak "base_n can only convert to base-" . #digits . " max.";
#digits = #digits[0 .. $base - 1];
my $size = $number ? int(log($number) / log($base)) : 0; # avoid log(0)
if (defined $max_digits) {
$size < $max_digits
or croak "The number $number is too large for $max_digits digits in base $base.";
$size = $max_digits - 1 if $pad;
}
my $representation = "";
while ($size >= 0) {
$representation .= $digits[$number / #digits**$size];
$number %= #digits**$size;
$size--;
}
if (wantarray) {
my $checksum = substr $representation, -1;
return $representation, $checksum;
} else {
return $representation;
}
}
A corresponding (but incomplete) unit test:
use Test::More;
my $n = 21767823360;
ok "A000000" eq base_n($n => 36), "simple";
ok "A000000" eq base_n($n => 36, 8), "valid constraint";
ok "0A000000" eq base_n($n => 36, 8, 1), "padding";
ok ! eval { base_n($n => 36, 6); 1 }, "invalid constraint";
ok "0" eq (base_n($n => 36))[1], "checksum (1)";
ok "A" eq (base_n($n+10 => 36))[1], "checksum (2)";
ok "0" eq base_n(0 => 36), "zero: simple";
ok "0"x8 eq base_n(0 => 36, 8, 1), "zero: padding";
ok ! eval { base_n($n => 0.7); 1 }, "invalid base";
ok ! eval { base_n(0.7 => 36); 1 }, "invalid number";
ok $n == base_n($n => 10), "round-trip safety";
ok $n eq base_n($n => 10, length $n, 1), "round-trip safety: padding";
done_testing;

Related

Sum the odd and even indices of an array separately - Perl

I have an array of 11 elements. Where I want to sum the odd elements including the first and last elements as one scalar and the evens as another.
This is my code I am trying to use map adding 2 to each index to achieve the result but I think I have got it wrong.
use strict;
use warnings;
use Data::Dumper;
print 'Enter the 11 digiet serial number: ';
chomp( my #barcode = //, <STDIN> );
my #sum1 = map { 2 + $_ } $barcode[1] .. $barcode[11];
my $sum1 = sum Dumper( \#sum1 );
# sum2 = l2 + l4 + l6 + r8 + r10;
printf "{$sum1}";
What is a good way to achieve this?
Sum of even/odd indicies (what you asked for, but not what you want[1]):
use List::Util qw( sum ); # Or: sub sum { my $acc; $acc += $_ for #_; $acc }
my $sum_of_even_idxs = sum grep { $_ % 2 == 0 } 0..$#nums;
my $sum_of_odd_idxs = sum grep { $_ % 2 == 1 } 0..$#nums;
Sum of even/odd values (what you also asked for, but not what you want[1]):
use List::Util qw( sum ); # Or: sub sum { my $acc; $acc += $_ for #_; $acc }
my $sum_of_even_vals = sum grep { $_ % 2 == 0 } #nums;
my $sum_of_odd_vals = sum grep { $_ % 2 == 1 } #nums;
Sum of values at even/odd indexes (what you appear to want):
use List::Util qw( sum ); # Or: sub sum { my $acc; $acc += $_ for #_; $acc }
my $sum_of_vals_at_even_idxs = sum #nums[ grep { $_ % 2 == 0 } 0..$#nums ];
my $sum_of_vals_at_odd_idxs = sum #nums[ grep { $_ % 2 == 1 } 0..$#nums ];
Given that you know how many elements you have, you could use the following:
use List::Util qw( sum ); # Or: sub sum { my $acc; $acc += $_ for #_; $acc }
my $sum_of_vals_at_even_idxs = sum #nums[0,2,4,6,8,10];
my $sum_of_vals_at_odd_idxs = sum #nums[1,3,5,7,9];
I included these in case someone needing these lands on this Q&A.
Add up values at odd and at even indices
perl -wE'#ary = 1..6;
for (0..$#ary) { $_ & 1 ? $odds += $ary[$_] : $evens += $ary[$_] };
say "odds: $odds, evens: $evens"
'
Note for tests: with even indices (0,2,4) we have (odd!) values (1,3,5), in this (1..6) example
You can use the fact that the ?: operator is assignable
print 'Enter the 11 digiet serial number: ';
chomp( my #barcode = //, <STDIN> );
my $odd = 0;
my $even = 0;
for (my $index = 0; $index < #barcode; $index++) {
($index % 2 ? $even : $odd) += $barcode[$index];
}
This works by indexing over #barcode and taking the mod 2 of the index, ie dividing the index by 2 and taking the remainder, and if the remainder is 1 adding that element of #barcode to $even otherwise to $odd.
That looks strange until you remember that arrays are 0 based so your first number of the barcode is stored in $barcode[0] which is an even index.
chomp( my #barcode = //, <STDIN> ); presumably was supposed to have a split before the //?
#barcode will have all the characters in the line read, including the newline. The chomp will change the final element from a newline to an empty string.
Better to chomp first so you just have your digits in the array:
chomp(my $barcode = <STDIN>);
my #barcode = split //, $barcode;
Another Perl, if the string is of length 11 and contains only digits
$ perl -le ' $_="12345678911"; s/(.)(.)|(.)$/$odd+=$1+$3;$even+=$2/ge; print "odd=$odd; even=$even" '
odd=26; even=21
$
with different input
$ perl -le ' $_="12121212121"; s/(.)(.)|(.)$/$odd+=$1+$3;$even+=$2/ge; print "odd=$odd; even=$even" '
odd=6; even=10
$

Perl: Create a binary number and convert it into hex

I want to create a binary number from the given user input.
Input - Array of number
Output - Binary number
A binary number should be created such that it has one on all the places which has been given as input.
In the given case input is 1, 3, and 7 so my binary no should be 1000101, so it has 1's on 1, 3 and 7 places from left.
#x = [ 1, 3, 7 ];
$z = 0;
for( $i = 0; $i < 10; $i++ ){
foreach $elem ( #x ){
if( $elem == $i ){
join( "", $z, 1 );
}
else{
join( "", $z, 0 );
}
}
}
print "Value of z: $z";
After execution, I am getting the value of z as 0.
I need to convert this binary to hexadecimal.
Is there some function which converts binary to hexadecimal?
[ ] creates an array and returns a reference to that array, so you are assigning a single scalar to (poorly named) #x.
You are also misusing join. Always use use strict; use warnings qw( all );! It would have caught this error.
Fixed:
my #bits = ( 1, 3, 7 );
my $num = 0;
$num |= 1 << $_ for #bits;
# 76543210
printf("0b%b\n", $num); # 0b10001010
printf("0x%X\n", $num); # 0x8A
It seems that you want 0b1000101, so we need to correct the indexes.
my #bits_plus_1 = ( 1, 3, 7 );
my $num = 0;
$num |= 1 << ( $_ - 1 ) for #bits_plus_1;
# 6543210
printf("0b%b\n", $num); # 0b1000101
printf("0x%X\n", $num); # 0x45
A few problems:
#x = [ 1, 3, 7 ]; is not an array of three integers. It's an array containing a single array reference. What you want is round brackets, not square brackets: #x = ( 1, 3, 7 );
The string returned by join is not assigned to $z
But even then your code is buggy:
it appends a bit at the end of $z, not the beginning
there's a trailing zero that has no business being there.

How to convert a number to an arbitary base in perl?

I would like to do something like the following in perl:
#digits = ("1", "2", ..., "a", ... "z", ... ); ## a list of characters
$num = 1033;
convert_to_base($num, #digits);
Now, $num will be converted to a string with the digits being used are from digits (so the base is $#digits + 1).
It can be done by iterating on $num, taking the modulo of $num with respect to $#digits and then dividing until 0 is reached, but I was wondering if there is any built-in function that does that in perl (or alternatively a fast function that would do it in perl).
Using Math::Base::Convert as suggested in choroba's comment to the question:
#!/usr/bin/perl
use Math::Base::Convert "cnv";
my $num = 1033;
printf "%b", $num; # binary: 10000001001
printf "%o", $num; # octal: 2011
printf "%d", $num; # decimal: 1033
printf "%x", $num; # hexadecimal: 409
print cnv($num, 10, b64); # base64*: G9 (*: 0-9, A-Z, a-z, ., _)
print cnv($num, 10, b85); # base85*: CD (*: from RFC 1924)
print cnv($num, 10, ascii); # base96: *s
Note, if you need to interpret this as a string, you may have to do e.g.
printf "%s", "" . cnv($num, 10, b85);
As #Adam Katz mentioned in his answer, the way to do it is with Math::Base::Convert. However, your question is about using an arbitrary base. The CPAN pod isn't very clear on how to do it, but it's actually as easy as:
use strict;
use Math::Base::Convert; # https://metacpan.org/pod/Math::Base::Convert
my $arb_enc = ['0'..'9', 'B'..'D', 'F'..'H', 'j'..'n', 'p'..'t', 'v'..'z', '*', '~'] ;
# ^^^^ note this is a array ref, which you can build with whatever characters you want
my $d_arbenc = new Math::Base::Convert('10', $arb_enc); # from decimal
my $arbenc_d = new Math::Base::Convert( $arb_enc, '10'); # to decimal
# test it like this:
foreach ( "1", "123", 62, 64, 255, 65535, 100, 10000, 1000000 ) {
my $status = eval { $d_arbenc->cnv($_) }; # if error, $status will be empty, and error message will be in $#
print "d_arbenc [$_] = [$status]\n";
}
foreach ( "BD3F", "jjjnnnppp", "333", "bad string" ) {
my $status = eval { $arbenc_d->cnv($_) }; # if error, $status will be empty, and error message will be in $#
print "arbenc_d [$_] = [$status]\n";
}

input a number and output the Fibonacci number recursively Perl

For a given value N I am trying to output the corresponding Fibonacci number F(N). My script doesn't seem to enter the recursive stage. fibonnaci($number) is not calling the subroutine. It is simply outputing "fibonacci(whatever number is inputted)".
Here is my code:
#!/usr/bin/perl -w
use warnings;
use strict;
print "Please enter value of N: ";
my $number = <STDIN>;
chomp($number);
sub fibonacci
{
my $f;
if ( $number == 0 ) { # base case
$f = 0;
} elsif ( $number == 1 ) {
$f = 1;
} else { # recursive step
$f = fibonacci( $number - 1 ) + fibonacci( $number - 2 );
}
return $f;
}
print "\nf($number) = fibonacci($number)\n";
Sample Output:
Please enter value of N: 4
f(4) = fibonacci(4)
user1:~>recursiveFib.pl
Please enter value of N: 5
f(5) = fibonacci(5)
user1:~>recursiveFib.pl
Please enter value of N: 10
f(10) = fibonacci(10)
user1:~>
Not sure where I went wrong. Any help would be greatly appreciated.
You need to accept the function arguments properly and take the function call out of the quotes.
use warnings;
use strict;
sub fibonacci {
my ($number) = #_;
if ($number < 2) { # base case
return $number;
}
return fibonacci($number-1) + fibonacci($number-2);
}
print "Please enter value of N: ";
my $number = <STDIN>;
chomp($number);
print "\n$number: ", fibonacci($number), "\n";
A more efficient but still recursive version:
sub fib_r {
my ($n,$a,$b) = #_;
if ($n <= 0) { return $a; }
else { return fib_r($n-1, $b, $a+$b); }
}
sub fib { fib_r($_[0], 0, 1); } # pass initial values of a and b
print fib(10), "\n";
Other answers have already mentioned the lack of taking an argument correctly to the fibonacci function, and that you can't interpolate a function call in the print string like that. Lately my favourite way to interpolate function calls into print strings is to use the ${\ ... } notation for embedding arbitrary expressions into strings:
print "f($number) = ${\ fibonacci($number) }\n";
Other techniques include separate arguments:
print "f($number) = ", fibonacci($number), "\n";
or a helper variable:
my $result = fibonacci($number);
print "f($number) = $result\n";
or even printf:
printf "f(%d) = %d\n", $number, fibonacci($number);
Of all these techniques I tend to prefer either of the first two, because they lead to putting the expressions "in-line" with the rest of the text string, whereas in the latter two they sit elsewhere, making it harder to see at a glance what gets printed where. Especially with printf's positional arguments, it can be easy to be "off-by-one" with a large number of arguments, and put everything in the wrong place.
You are printing in wrong way. you just need to handle the return value. Also the way you are using Number in the sub is also not seems relevant. I have updated the and its working fine.
Also the values that you wanted to print is depend on the start up of the series. whether you want to start from 0 or 1.
The series example start with 1 is 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, so if you put 10 you will get 55.
#!/usr/bin/perl -w
use warnings;
use strict;
print "Please enter value of N: ";
my $number = <STDIN>;
chomp($number);
my $result=fibonacci($number);
sub fibonacci
{
my $f =0;
if ($_[0] == 1 ) { # base case
$f = 1;
} elsif ( $_[0] == 2 ) {
$f = 1;
} else { # recursive step
$f= fibonacci( $_[0] - 1 ) + fibonacci( $_[0] - 2 );
}
return $f;
}
print "\nf($number) = $result\n";

How to print range values with specific reference number are given?

I have a set of data file looks like below. I would like to get the interpolation final value (final,P) by referring to 2 set of number range (scoreA and scoreB). Let's say for "Eric", his scoreA is 35 (value between range 30.00 - 40.00) and scoreB is 48 (a value between range 45.00 - 50.00). He will get 2 set of final values range which are (22.88,40.90) & (26.99,38.99). And I would like to get the final value of "Eric" and "George" in the data file. "George"'s scoreA = 38 and scoreB = 26.
After formula calculation, I want to get the exact final value when his scoreA=35 & scoreB=45. Let's assume formula is P=X+Y (P is final value), so far I have been trying the code as shown below. However it cannot get the correct lines.
How to get the exactly final value range by referring to the data given?
data file
Student_name ("Eric")
/* This is a junk line */
scoreA ("10.00, 20.00, 30.00, 40.00")
scoreB ("15.00, 30.00, 45.00, 50.00, 55.00")
final (
"12.23,19.00,37.88,45.98,60.00",\
"07.00,20.11,24.56,45.66,57.88",\
"05.00,15.78,22.88,40.90,57.99",\
"10.00,16.87,26.99,38.99,40.66"\)
Student_name ("Liy")
/* This is a junk line */
scoreA ("5.00, 10.00, 20.00, 60.00")
scoreB ("25.00, 30.00, 40.00, 55.00, 60.00")
final (
"02.23,15.00,37.88,45.98,70.00",\
"10.00,28.11,34.56,45.66,57.88",\
"08.00,19.78,32.88,40.90,57.66",\
"10.00,27.87,39.99,59.99,78.66"\)
Student_name ("Frank")
/* This is a junk line */
scoreA ("2.00, 15.00, 25.00, 40.00")
scoreB ("15.00, 24.00, 38.00, 45.00, 80.00")
final (
"02.23,15.00,37.88,45.98,70.00",\
"10.00,28.11,34.56,45.66,57.88",\
"08.00,19.78,32.88,40.90,57.66",\
"10.00,27.87,39.99,59.99,78.66"\)
Student_name ("George")
/* This is a junk line */
scoreA ("10.00, 15.00, 20.00, 40.00")
scoreB ("25.00, 33.00, 46.00, 55.00, 60.00")
final (
"10.23,25.00,37.88,45.98,68.00",\
"09.00,28.11,34.56,45.66,60.88",\
"18.00,19.78,32.88,40.90,79.66",\
"17.00,27.87,40.99,59.99,66.66"\)
Coding
data();
sub data() {
my $cnt = 0;
while (my #array = <FILE>) {
foreach $line(#array) {
if ($line =~ /Student_name/) {
$a = $line;
if ($a =~ /Eric/ or $cnt > 0 ) {
$cnt++;
}
if ( $cnt > 1 and $cnt <= 3 ) {
print $a;
}
if ( $cnt > 2 and $cnt <= 4 ) {
print $a;
}
if ( $cnt == 5 ) {
$cnt = 0;
}
}
}
}
}
Result
Eric final=42.66
George final=24.30
In my comment I said that parsing is fairly easy. Here is how it could be done. As the question lacks a proper specification of the file format, I will assume the following:
The file consists of properties, which have values:
document ::= property*
property ::= word "(" value ("," value)* ")"
A value is a double-quoted string containing numbers seperated by commata, or a single word:
value ::= '"' ( word | number ("," number)* ) '"'
Spaces, backslashes, and comments are irrelevant.
Here is a possible implementation; I will not go into the details of explaining how to write a simple parser.
package Parser;
use strict; use warnings;
sub parse {
my ($data) = #_;
# perform tokenization
pos($data) = 0;
my $length = length $data;
my #tokens;
while(pos($data) < $length) {
next if $data =~ m{\G\s+}gc
or $data =~ m{\G\\}gc
or $data =~ m{\G/[*].*?[*]/}gc;
if ($data =~ m/\G([",()])/gc) {
push #tokens, [symbol => $1];
} elsif ($data =~ m/\G([0-9]+[.][0-9]+)/gc) {
push #tokens, [number => 0+$1];
} elsif ($data =~ m/\G(\w+)/gc) {
push #tokens, [word => $1];
} else {
die "unreckognized token at:\n", substr $data, pos($data), 10;
}
}
return parse_document(\#tokens);
}
sub token_error {
my ($token, $expected) = #_;
return "Wrong token [#$token] when expecting [#$expected]";
}
sub parse_document {
my ($tokens) = #_;
my #properties;
push #properties, parse_property($tokens) while #$tokens;
return #properties;
}
sub parse_property {
my ($tokens) = #_;
$tokens->[0][0] eq "word"
or die token_error $tokens->[0], ["word"];
my $name = (shift #$tokens)->[1];
$tokens->[0][0] eq "symbol" and $tokens->[0][1] eq '('
or die token_error $tokens->[0], [symbol => '('];
shift #$tokens;
my #vals;
VAL: {
push #vals, parse_value($tokens);
if ($tokens->[0][0] eq 'symbol' and $tokens->[0][1] eq ',') {
shift #$tokens;
redo VAL;
}
}
$tokens->[0][0] eq "symbol" and $tokens->[0][1] eq ')'
or die token_error $tokens->[0], [symbol => ')'];
shift #$tokens;
return [ $name => #vals ];
}
sub parse_value {
my ($tokens) = #_;
$tokens->[0][0] eq "symbol" and $tokens->[0][1] eq '"'
or die token_error $tokens->[0], [symbol => '"'];
shift #$tokens;
my $value;
if ($tokens->[0][0] eq "word") {
$value = (shift #$tokens)->[1];
} else {
my #nums;
NUM: {
$tokens->[0][0] eq 'number'
or die token_error $tokens->[0], ['number'];
push #nums, (shift #$tokens)->[1];
if ($tokens->[0][0] eq 'symbol' and $tokens->[0][1] eq ',') {
shift #$tokens;
redo NUM;
}
}
$value = \#nums;
}
$tokens->[0][0] eq "symbol" and $tokens->[0][1] eq '"'
or die token_error $tokens->[0], [symbol => '"'];
shift #$tokens;
return $value;
}
Now, we get the following data structure as output from Parser::parse:
(
["Student_name", "Eric"],
["scoreA", [10, 20, 30, 40]],
["scoreB", [15, 30, 45, 50, 55]],
[
"final",
[12.23, 19, 37.88, 45.98, 60],
[7, 20.11, 24.56, 45.66, 57.88],
[5, 15.78, 22.88, 40.9, 57.99],
[10, 16.87, 26.99, 38.99, 40.66],
],
["Student_name", "Liy"],
["scoreA", [5, 10, 20, 60]],
["scoreB", [25, 30, 40, 55, 60]],
[
"final",
[2.23, 15, 37.88, 45.98, 70],
[10, 28.11, 34.56, 45.66, 57.88],
[8, 19.78, 32.88, 40.9, 57.66],
[10, 27.87, 39.99, 59.99, 78.66],
],
...,
)
As a next step, we want to transform it into nested hashes, so that we have the structure
{
Eric => {
scoreA => [...],
scoreB => [...],
final => [[...], ...],
},
Liy => {...},
...,
}
So we simply run it through this small sub:
sub properties_to_hash {
my %hash;
while(my $name_prop = shift #_) {
$name_prop->[0] eq 'Student_name' or die "Expected Student_name property";
my $name = $name_prop->[1];
while( #_ and $_[0][0] ne 'Student_name') {
my ($prop, #vals) = #{ shift #_ };
if (#vals > 1) {
$hash{$name}{$prop} = \#vals;
} else {
$hash{$name}{$prop} = $vals[0];
}
}
}
return \%hash;
}
So we have the main code
my $data = properties_to_hash(Parser::parse( $file_contents ));
Now we can move on to Part 2 fo the problem: calculating your scores. That is, once you make clear what you need done.
Edit: Bilinear interpolation
Let f be the function that returns the value at a certain coordinate. If we have a value at those coordinates, we can return that. Else, we perform bilinear interpolation with the next known values.
The formula for bilinear interpolation[1] is:
f(x, y) = 1/( (x_2 - x_1) · (y_2 - y_1) ) · (
f(x_1, y_1) · (x_2 - x) · (y_2 - y)
+ f(x_2, y_1) · (x - x_1) · (y_2 - y)
+ f(x_1, y_2) · (x_2 - x) · (y - y_1)
+ f(x_2, y_2) · (x - x_1) · (y - y_1)
)
Now, scoreA denote the positions of the data points in the final table on the first axis, scoreA the positions on the second axis. We have to do the following:
assert that the requested values x, y are inside the bounds
fetch the next smaller and next larger positions
perform interpolation
.
sub f {
my ($data, $x, $y) = #_;
# do bounds check:
my ($x_min, $x_max, $y_min, $y_max) = (#{$data->{scoreA}}[0, -1], #{$data->{scoreB}}[0, -1]);
die "indices ($x, $y) out of range ([$x_min, $x_max], [$y_min, $y_max])"
unless $x_min <= $x && $x <= $x_max
&& $y_min <= $y && $y <= $y_max;
To fetch the boxing indices x_1, x_2, y_1, y_2 we need to iterate through all possible scores. We'll also remember the physical indices x_i1, x_i2, y_i1, y_i2 of the underlying arrays.
my ($x_i1, $x_i2, $y_i1, $y_i2);
for ([$data->{scoreA}, \$x_i1, \$x_i2], [$data->{scoreB}, \$y_i1, \$y_i2]) {
my ($scores, $a_i1, $a_i2) = #$_;
for my $i (0 .. $#$scores) {
if ($scores->[$i] <= $x) {
($$a_i1, $$a_i2) = $i == $#$scores ? ($i, $i+1) : ($i-1, $i);
last;
}
}
}
my ($x_1, $x_2) = #{$data->{scoreA}}[$x_i1, $x_i2];
my ($y_1, $y_2) = #{$data->{scoreB}}[$y_i1, $y_i2];
Now, interpolation according to above formula can be performed, but each access at a known index can be changed to an access via physical index, so f(x_1, y_2) would become
$final->[$x_i1][$y_i2]
Detailed Explanation of sub f
sub f { ... } declares a sub with name f, although that is probably a bad name. bilinear_interpolation might be a better name.
my ($data, $x, $y) = #_ states that our sub takes three arguments:
$data, a hash reference containing fields scoreA, scoreB and final, which are array references.
$x, the position along the scoreA-axis where interpolation is required.
$y, the position along the scoreB-axis where interpolation is required.
Next, we want to assert that the positions for $x and $y are valid aka inside bounds. The first value in $data->{scoreA} is the minimal value; the maximal value is in the last position (index -1). To get both at once, we use an array slice. A slice accesses multiple values at once and returns a list, like #array[1, 2]. Because we use complex data structures which use references, we have to dereference the array in $data->{scoreA}. This makes the slice look like #{$data->{scoreA}}[0, 1].
Now that we have the $x_min and $x_max values, we throw and error unless the requested value $x is inside the range defined by the min/max values. This is true when
$x_min <= $x && $x <= $x_max
Should either $x or $y be out of bounds, we throw an error which shows the actual bounds. So the code
die "indices ($x, $y) out of range ([$x_min, $x_max], [$y_min, $y_max])"
could, for example, throw an error like
indices (10, 500) out of range ([20, 30], [25, 57]) at script.pl line 42
Here we can see that the value for $x is too small, and $y is too large.
The next problem is to find neighbouring values. Assuming scoreA holds [1, 2, 3, 4, 5], and $x is 3.7, we want to select the values 3 and 4. But because we can pull some nifty tricks a bit later, we would rather remember the position of the neighbouring values, not the values themselves. So this would give 2 and 3 in above example (remember that arrows are zero-based).
We can do this by looping over all indices of our array. When we find a value that is ≤ $x, we remember the index. E.g. 3 is the first value that is ≤ $x, so we remember the index 2. For the next higher value, we have to be a bit carful: Obviously, we can just take the next index, so 2 + 1 = 3. But now assume that $x is 5. This passes the bounds check. The first value that is ≤ $x would be value 5, so we can remember position 4. However, there is no entry at position 5, so we could use the current index itself. Because this would lead to division by zero later on, we would be better off remembering positions 3 and 4 (values 4 and 5).
Expressed as code, that is
my ($x_i1, $x_i2);
my #scoreA = #{ $data->{scoreA} }; # shortcut to the scoreA entry
for my $i (0 .. $#scores) { # iterate over all indices: `$#arr` is the last idx of #arr
if ($scores[$i] <= $x) { # do this if the current value is ≤ $x
if ($i != $#scores) { # if this isn't the last index
($x_i1, $x_i2) = ($i, $i+1);
} else { # so this is the last index
($x_i1, $x_i2) = ($i-1, $i);
}
last; # break out of the loop
}
}
In my original code I choose a more complex solution to avoid copy-pasting the same code for finding the neighbours of $y.
Because we also need the values, we obtain them via a slice with the indices:
my ($x_1, $x_2) = #{$data->{scoreA}}[$x_i1, $x_i2];
Now we have all surrounding values $x1, $x_2, $y_1, $y_2 which define the rectangle in which we want to perform bilinear interpolation. The mathematical formula is easy to translate to Perl: just choose the correct operators (*, not · for multiplication), and the variables need dollar signs before them.
The formula I used is recursive: The definition of f refers to itself. This would imply an infinite loop, unless we do some thinking and break the recursion. f symbolizes the value at a certain position. In most cases, this means interpolating. However, if $x and $y are both equal to values in scoreA and scoreB respectively, we don't need bilinear interpolation, and can return the final entry directly.
This can be done by checking if both $x and $y are members of their arrays, and doing an early return. Or we can use the fact that $x_1, ..., $y_2 all are members of the arrays. Instead of recursing with values we know don't need interpolating, we just do an array access. This is what we have saved the indices $x_i1, ..., $y_i2 for. So wherever the original formula says f(x_1, y_1) or similar, we write the equivalent $data->{final}[$x_i1][$y_i2].