sifukurt has asked for the wisdom of the Perl Monks concerning the following question:
Hello, fellow programmers of the cloth. I need some help. The wife of a good friend of mine is taking a trigonometry class. I thought that I'd whip up a quickie perl script that would perform the Law of Sines so she could quickly check her answers. After a bit of playing, I ended up with this:
use Math::NumberCruncher;
sub asin {
my $sine = shift;
return undef unless ( $sine >= 1 && $sine <= 1 );
return atan2( $sine, sqrt( 1$sine * $sine ) );
}
sub LawOfSines {
my ( $A, $a, $B, $b, $C, $c, $format ) = @_;
return undef unless ( $A && $a )  ( $B && $b )  ( $C && $c );
return undef unless defined $a or defined $b or defined $c;
return undef unless defined $A or defined $B or defined $C;
my %angles = ();
my %sides = ();
my @angles = undef;
my ( $var, $other, $sum ) = undef;
if ( $format eq "d" ) {
if ( $A ) {
$angles{A} = $A;
$A = sprintf( "%.10f", Math::NumberCruncher::deg2rad( $A )
+ );
}
if ( $B ) {
$angles{B} = $B;
$B = sprintf( "%.10f", Math::NumberCruncher::deg2rad( $B )
+ );
}
if ( $C ) {
$angles{C} = $C;
$C = sprintf( "%.10f", Math::NumberCruncher::deg2rad( $C )
+ );
}
} else {
if ( $A ) {
$angles{A} = sprintf( "%.10f", Math::NumberCruncher::rad2d
+eg( $A ) );
}
if ( $B ) {
$angles{B} = sprintf( "%.10f", Math::NumberCruncher::rad2d
+eg( $B ) );
}
if ( $C ) {
$angles{C} = sprintf( "%.10f", Math::NumberCruncher::rad2d
+eg( $C ) );
}
}
unless ( $A > 0 && $B > 0 && $C > 0 ) {
if ( $A > 0 && $B > 0 ) { $var = "C" }
if ( $A > 0 && $C > 0 ) { $var = "B" }
if ( $B > 0 && $C > 0 ) { $var = "A" }
if ( $var eq "C" ) {
$sum = $angles{B} + $angles{A};
$other = 180  $sum;
$angles{$var} = $other;
$C = sprintf( "%.10f", Math::NumberCruncher::deg2rad( $oth
+er ) );
} elsif ( $var eq "B" ) {
$sum = $angles{A} + $angles{C};
$other = 180  $sum;
$angles{$var} = $other;
$B = sprintf( "%.10f", Math::NumberCruncher::deg2rad( $oth
+er ) );
} elsif ( $var eq "A" ) {
$sum = $angles{B} + $angles{C};
$other = 180  $sum;
$angles{$var} = $other;
$A = sprintf( "%.10f", Math::NumberCruncher::deg2rad( $oth
+er ) );
}
undef $var;
}
if ( $a ) {
$sides{a} = $a;
}
if ( $b ) {
$sides{b} = $b;
}
if ( $c ) {
$sides{c} = $c;
}
if ( $A > 0 && $a ) {
my $x = $a / sin( $A );
if ( $b && $B == 0 ) {
my $sin_B = ( 1/$x ) * $b;
$B = asin( $sin_B );
$angles{B} = sprintf( "%.10f", Math::NumberCruncher::rad2d
+eg( $B ) );
} elsif ( !$b && $B > 0 ) {
$b = sprintf( "%.3f", ( $x * sin( $B ) ) );
$sides{b} = $b;
}
unless ( $A > 0 && $B > 0 && $C > 0 ) {
if ( $A > 0 && $B > 0 ) { $var = "C" }
if ( $A > 0 && $C > 0 ) { $var = "B" }
if ( $B > 0 && $C > 0 ) { $var = "A" }
if ( $var eq "C" ) {
$sum = $angles{B} + $angles{A};
$other = 180  $sum;
$angles{$var} = $other;
$C = sprintf( "%.10f", Math::NumberCruncher::deg2rad(
+$other ) );
} elsif ( $var eq "B" ) {
$sum = $angles{A} + $angles{C};
$other = 180  $sum;
$angles{$var} = $other;
$B = sprintf( "%.10f", Math::NumberCruncher::deg2rad(
+$other ) );
} elsif ( $var eq "A" ) {
$sum = $angles{B} + $angles{C};
$other = 180  $sum;
$angles{$var} = $other;
$A = sprintf( "%.10f", Math::NumberCruncher::deg2rad(
+$other ) );
}
undef $var;
}
if ( $c && $C == 0 ) {
my $sin_C = ( 1/$x ) * $c;
$C = asin( $sin_C );
$angles{C} = sprintf( "%.10f", Math::NumberCruncher::rad2d
+eg( $C ) );
} elsif ( !$c && $C > 0 ) {
$c = sprintf( "%.3f", ( $x * sin( $C ) ) );
$sides{c} = $c;
}
} elsif ( $B > 0 && $b ) {
my $x = $b / sin( $B );
if ( $a && $A == 0 ) {
my $sin_A = ( 1/$x ) * $a;
$A = asin( $sin_A );
$angles{A} = sprintf( "%.10f", Math::NumberCruncher::rad2d
+eg( $A ) );
} elsif ( !$a && $A > 0 ) {
$a = sprintf( "%.3f", ( $x * sin( $A ) ) );
$sides{a} = $a;
}
unless ( $A > 0 && $B > 0 && $C > 0 ) {
if ( $A > 0 && $B > 0 ) { $var = "C" }
if ( $A > 0 && $C > 0 ) { $var = "B" }
if ( $B > 0 && $C > 0 ) { $var = "A" }
if ( $var eq "C" ) {
$sum = $angles{B} + $angles{A};
$other = 180  $sum;
$angles{$var} = $other;
$C = sprintf( "%.10f", Math::NumberCruncher::deg2rad(
+$other ) );
} elsif ( $var eq "B" ) {
$sum = $angles{A} + $angles{C};
$other = 180  $sum;
$angles{$var} = $other;
$B = sprintf( "%.10f", Math::NumberCruncher::deg2rad(
+$other ) );
} elsif ( $var eq "A" ) {
$sum = $angles{B} + $angles{C};
$other = 180  $sum;
$angles{$var} = $other;
$A = sprintf( "%.10f", Math::NumberCruncher::deg2rad(
+$other ) );
}
undef $var;
}
if ( $c && $C == 0 ) {
my $sin_C = ( 1/$x ) * $c;
$C = asin( $sin_C );
$angles{C} = sprintf( "%.10f", Math::NumberCruncher::rad2d
+eg( $C ) );
} elsif ( !$c && $C > 0 ) {
$c = sprintf( "%.3f", ( $x * sin( $C ) ) );
$sides{c} = $c;
}
} elsif ( $C > 0 && $c ) {
my $x = $c / sin( $C );
if ( $a && $A == 0 ) {
my $sin_A = ( 1/$x ) * $a;
$A = asin( $sin_A );
$angles{A} = sprintf( "%.10f", Math::NumberCruncher::rad2d
+eg( $A ) );
} elsif ( !$a && $A > 0 ) {
$a = sprintf( "%.3f", ( $x * sin( $A ) ) );
$sides{a} = $a;
}
unless ( $A > 0 && $B > 0 && $C > 0 ) {
if ( $A > 0 && $B > 0 ) { $var = "C" }
if ( $A > 0 && $C > 0 ) { $var = "B" }
if ( $B > 0 && $C > 0 ) { $var = "A" }
if ( $var eq "C" ) {
$sum = $angles{B} + $angles{A};
$other = 180  $sum;
$angles{$var} = $other;
$C = sprintf( "%.10f", Math::NumberCruncher::deg2rad(
+$other ) );
} elsif ( $var eq "B" ) {
$sum = $angles{A} + $angles{C};
$other = 180  $sum;
$angles{$var} = $other;
$B = sprintf( "%.10f", Math::NumberCruncher::deg2rad(
+$other ) );
} elsif ( $var eq "A" ) {
$sum = $angles{B} + $angles{C};
print ("Sum: $sum\n");
$other = 180  $sum;
$angles{$var} = $other;
$A = sprintf( "%.10f", Math::NumberCruncher::deg2rad(
+$other ) );
}
undef $var;
}
if ( $b && $B == 0 ) {
my $sin_B = ( 1/$x ) * $b;
$B = asin( $sin_B );
$angles{B} = sprintf( "%.10f", Math::NumberCruncher::rad2d
+eg( $B ) );
} elsif ( !$b && $B > 0 ) {
$b = sprintf( "%.3f", ( $x * sin( $B ) ) );
$sides{b} = $b;
}
unless ( $A > 0 && $B > 0 && $C > 0 ) {
if ( $A > 0 && $B > 0 ) { $var = "C" }
if ( $A > 0 && $C > 0 ) { $var = "B" }
if ( $B > 0 && $C > 0 ) { $var = "A" }
if ( $var eq "C" ) {
$sum = $angles{B} + $angles{A};
$other = 180  $sum;
$angles{$var} = $other;
$C = sprintf( "%.10f", Math::NumberCruncher::deg2rad(
+$other ) );
} elsif ( $var eq "B" ) {
$sum = $angles{A} + $angles{C};
$other = 180  $sum;
$angles{$var} = $other;
$B = sprintf( "%.10f", Math::NumberCruncher::deg2rad(
+$other ) );
} elsif ( $var eq "A" ) {
$sum = $angles{B} + $angles{C};
$other = 180  $sum;
$angles{$var} = $other;
$A = sprintf( "%.10f", Math::NumberCruncher::deg2rad(
+$other ) );
}
undef $var;
}
if ( $a && $A == 0 ) {
my $sin_A = ( 1/$x ) * $a;
$A = asin( $sin_A );
$angles{A} = sprintf( "%.10f", Math::NumberCruncher::rad2d
+eg( $A ) );
} elsif ( !$a && $A > 0 ) {
$a = sprintf( "%.3f", ( $x * sin( $A ) ) );
$sides{a} = $a;
}
}
if ( $format eq "d" ) {
$A = sprintf( "%.3f", Math::NumberCruncher::rad2deg( $A ) );
$B = sprintf( "%.3f", Math::NumberCruncher::rad2deg( $B ) );
$C = sprintf( "%.3f", Math::NumberCruncher::rad2deg( $C ) );
}
$A = sprintf( "%.3f", $A );
$a = sprintf( "%.3f", $a );
$B = sprintf( "%.3f", $B );
$b = sprintf( "%.3f", $b );
$C = sprintf( "%.3f", $C );
$c = sprintf( "%.3f", $c );
return ( $A, $B, $C, $a, $b, $c );
}
print <<END;
Enter the triangle items you have available. You must enter at least
three items, including at least one side and at least one angle. In ad
+dition,
two of the three must be an angle and its opposing side. (i.e., B and
+b ).
END
print ("A: ");
chomp($A = <STDIN>);
print ("a: ");
chomp($a = <STDIN>);
print ("B: ");
chomp($B = <STDIN>);
print ("b: ");
chomp($b = <STDIN>);
print ("C: ");
chomp($C = <STDIN>);
print ("c: ");
chomp($c = <STDIN>);
( $A1, $B1, $C1, $a1, $b1, $c1 ) = LawOfSines( $A, $a, $B, $b, $C, $c,
+ "d" );
print <<END;
A: $A1
a: $a1
B: $B1
b: $b1
C: $C1
c: $c1
END
I was more interested in doing it quickly that I was in making it efficient or elegant. Now that I have the time, though, I want to clean it up. My problem is that after looking at it for as many hours as I have, I could really use someone else's input. I use Math::NumberCruncher to perform any degree/radian conversions. I'm really just doing this for fun (egad, did I just say that I'm doing the Law of Sines for fun? I need to get out more....), so there is no hurry, but if someone could take a look at my code and give me an assist in making it more efficient and/or elegant, I would appreciate it greatly.
Thanks a bunch.
___________________
Kurt
Re: Help w/ Law of Sines script
by dragonchild (Archbishop) on Aug 01, 2001 at 21:29 UTC

A few suggestions:
 Break the Law_Of_Sines() function up a bit more. For example, you have a (rather hefty) section of code cut'n'pasted three times. Make it a subroutine.
 Instead of running $A, $a, $B, $b, $C, and $c, why not do a list of hashes called angles? So, you could do something like $angles>[0]{B} to reference the second angle in the first triangle. The real benefit of that is to allow you to do for loops, reducing the number of lines while improving readability.
An example of the second suggestion would be:
unless ( $A > 0 && $B > 0 && $C > 0 ) {
if ( $A > 0 && $B > 0 ) { $var = "C" }
if ( $A > 0 && $C > 0 ) { $var = "B" }
if ( $B > 0 && $C > 0 ) { $var = "A" }
if ( $var eq "C" ) {
$sum = $angles{B} + $angles{A};
$other = 180  $sum;
$angles{$var} = $other;
$C = sprintf( "%.10f", Math::NumberCruncher::deg2rad( $oth
+er ) );
} elsif ( $var eq "B" ) {
$sum = $angles{A} + $angles{C};
$other = 180  $sum;
$angles{$var} = $other;
$B = sprintf( "%.10f", Math::NumberCruncher::deg2rad( $oth
+er ) );
} elsif ( $var eq "A" ) {
$sum = $angles{B} + $angles{C};
print ("Sum: $sum\n");
$other = 180  $sum;
$angles{$var} = $other;
$A = sprintf( "%.10f", Math::NumberCruncher::deg2rad( $oth
+er ) );
instead could be written as:
if (grep $_ <= 0, @{$angles>[0]}{('A' .. 'C')}) {
my $zero_angle = 'A';
if ($angles>[0]{C} <= 0) { $zero_angle = 'C' }
elsif ($angles>[0]{B} <= 0) { $zero_angle = 'B' }
my @sides = ('A' .. 'C');
@sides = grep $_ ne $zero_angle, @sides;
$sum = 0;
$sum += $_ for @{$angles>[0]}{@sides};
$other = 180  $sum;
$angles{$var} = $other;
$angles>[0]{$zero_angle} =
sprintf( "%.10f", Math::NumberCruncher::deg2rad( $other ) );
}
The primary difference between your version and mine is that I chose a data structure that allows me to have the interpreter associate pieces of data that should be associated together. Your version requires that you continually associate them yourself.
The second difference is that I'm taking advantage of the easy conversions between hashes and arrays through hashslices. Read up on them. They're SOOOO powerful. :)
Now, you don't have to use my data structure. I chose it because it was the closest to what you were doing. However, it can easily make sense to have the angles in an array and not a hash. So, triangle 1, angle 2 would be $angles>[0][1]. Simple enough.
Keep with it. It's a neat idea!
Update: Fixed a few square brackets. Thanx Albannach and ChemBoy!  [reply] [d/l] [select] 

Very nice. That's exactly the sort of thing I was looking for. Originally I had the repeated blocks of code as a subroutine, but for reasons I don't know, it didn't want to work correctly. It was calculating the correct values, but wasn't returning them correctly. Of course, I was working on this in the evening after a day of programming, so by that time I was probably suffering from that weird sort of programming blindness where the answer to the problem is hiding in plain sight.
I'm going to incorporate your suggestions (as well as the suggestions of others) and will post the modified version as soon as it is ready. Thanks much.
___________________
Kurt
 [reply] 
Re: Help w/ Law of Sines script
by lucs (Sexton) on Aug 02, 2001 at 07:28 UTC

#! /usr/bin/perl w
# Here is another way to do it, using a trigonometry 'toolkit'
# approach and a different interface. Sides are $a, $b, and $c, and
# corresponding opposing angles are $A, $B, and $C. Note that
# pathological cases will break the program (e.g., using two angles
# whose sum is greater than 180); more robust error checking is
# required.
use strict;
test();
main();
sub main {
print "Enter an angle (in degrees) and its opposing side:\n";
print 'Angle: ';
chomp(my $A = <STDIN>);
print 'Side : ';
chomp(my $a = <STDIN>);
print q/Enter 'a' if you have an angle, 's' if you have a side: /;
GET_WHAT:
chomp(my $what = lc <STDIN>);
if ($what eq 'a') {
print 'Other angle: ';
chomp(my $B = <STDIN>);
my $C = 180  $A  $B;
my $b = aAB_to_b($a, $A, $B);
my $c = aAB_to_b($a, $A, $C);
printf 'The other two sides are %.2f and %.2f, '
. "and the other angle is %.2f\n",
$b, $c, $C;
}
elsif ($what eq 's') {
print 'Other side: ';
chomp(my $b = <STDIN>);
my $B = aAb_to_B($a, $A, $b);
my $C = 180  $A  $B;
my $c = aAB_to_b($a, $A, $C);
printf 'The other side is %.2f, '
. "and the other two angles are %.2f and %.2f\n",
$c, $B, $C;
}
else {
print q/Please enter 'a' or 's': /;
goto GET_WHAT;
}
}
sub aAb_to_B {
my ($a, $A, $b) = @_;
return rad2deg(asin($b * sin(deg2rad($A)) / $a));
}
sub aAB_to_b {
my ($a, $A, $B) = @_;
return sin(deg2rad($B)) * $a / sin(deg2rad($A));
}
sub asin {
my $sine = shift;
return undef unless $sine >= 1 && $sine <= 1;
return atan2($sine, sqrt(1  $sine * $sine));
}
use constant PI => 4 * atan2(1, 1);
sub deg2rad { $_[0] / 180 * PI }
sub rad2deg { $_[0] / PI * 180 }
sub test {
# More tests would be welcome.
expect('aAB_to_b', 10, 5, 30, 90);
expect('aAb_to_B', 90, 5, 30, 10);
}
sub expect {
my ($sub, $exp, @args) = @_;
no strict 'refs';
my $got = $sub>(@args);
return if abs($got  $exp) < 0.0001;
local $" = ', ';
printf "Expected $sub(@args) to give $exp, but got %.2f\n", $got;
}
 [reply] [d/l] 
Re: Help w/ Law of Sines script
by John M. Dlugosz (Monsignor) on Aug 01, 2001 at 22:15 UTC

Urrrgh! PM bites again, losing the long reply I typed. Instead of "Preview" it took me to Dragonchild's node, and hitting the Back button just went to the same place ad infinitum. I wish they'd fix that already... I've taken to copying the text to the clipboard before submitting, just in case. Wish I'd done it this time (sigh)
So, DC mentioned reducing the redundancy in your code, so I won't retype it.
But, I do want to encourage you and give you some food for thought: When I wrote this same thing in high school, I incorporated the law of sines, law of cosines, and the fact that the sum of the angles is 180 into a single unified program. It would prompt you for 3 sides and angles. Enter what you know, or ? to solve for that, or blank for don't know/don't care. Then it tells you what you wanted to know.
—John
 [reply] 

