Using bc, Part 2
Programming the bc calculator tool
Summary
Last month Mo gave you a rundown on bc and how to perform calculations with
it. This month he covers how to turn all of those great bc functions into
programs. (2,200 words)
The
first order of business is to cover a few corrections from last month's
issue. It was brought to my attention by a reader, that the Solaris version
of bc does not like the void keyword as with void ++x or void --m.
If your version of bc has a problem with void, then replace an increment or
decrement as follows: void ++x and void x++ become x=x+1 . Similarly, void --m and void m-- become m=m-1 .
Our first example of a program based on the bc calculator will use the
square function. Using the vi editor, create the
following script (which includes line numbers for explanation). The first
things
you will notice are comments, at lines 2 and 7. Comments can be any text
between an initial /* and a closing */ and can span multiple lines. Some
versions of bc support the pound sign (#) as a comment delimiter. The semicolon at the
end of line 4
is optional, but the semicolons inside lines 9 and 11 are not. The closing
semicolon is because bc treats a semicolon or a new line as the end of a
statement. Aside from the fancy text formatting, there are no surprises in
the
script. Save this script with a name such as sqr.
-----------------------
1
2 /* s() returns the square of a number */
3 define s(x){
4 return(x*x);
5 }
6
7 /* use s on 2 and 8 */
8 x=2
9 "The area of a square with ";x;" ft sides is ";s(x);" sq ft";
10 x=8
11 "The area of a square with ";x;" ft sides is ";s(x);" sq ft";
12
13 quit
14
Execute the script by typing bc sqr . If bc is followed by a
file name, the file name is used as the input to bc.
$ bc sqr
The area of a square with 2
ft sides is 4
sq ft
The area of a square with 8
ft sides is 64
sq ft
This is fine if you want to know the squares of 2 and 8, but we need something more flexible, something that will take an argument and
calculate a square from it.
The first step in doing this is using something called a "here" document.
The
following listing is sqr converted into a here document. A line has been
added at the top of the program that reads "bc <<END-OF-INPUT>"
and a line at
the bottom reads "END-OF-INPUT". The sqr program is no longer a bc program;
it
is now a shell script, and you must change its mode to allow execution using
chmod a+x sqr. The instruction at line 1 translates as "run bc -- use the rest of
this
file as input to bc until you encounter a line containing END-OF-INPUT." When
sqr is run, the shell starts bc, reads line 2 of the script, pumps it
into bc, and then it reads line 3 and so on up to line 16. In fact bc stops
running at line 15, but the shell stops feeding information to bc on the
very
next line. So far we have another method of showing the squares of 2 and 8,
and
not much has changed.
1 bc <<<END-OF-INPUT>
2 /* s() returns the square of a number */
3 define s(x){
4 return(x*x);
5 }
6
7 /* use s on 2 and 8 */
8 x=2
9 "The area of a square with ";x;" ft sides is ";s(x);" sq ft";
10 x=8
11 "The area of a square with ";x;" ft sides is ";s(x);" sq ft";
12
15 quit
16 END-OF-INPUT
In the next listing, sqr has been turned into a script that will accept
arguments and process them correctly. At lines 3 through 7, the number
of arguments on the command line is tested. This program must be
started with the command sqr followed by a number. If a number argument
is missing, an error results. Note that the error checking does not
test whether the value is a number, only that an argument exists. The
call to start bc with a here document begins at line 9, and is the same
down to line 15. At line 16, the argument from the command line is
assigned to x, and then the s function is called as before.
1 # sqr - takes a number argument and outputs the square
2
3 if [ $# != 1 ]
4 then
5 echo "A number argument is required"
6 exit
7 fi
8
9 bc <<END-OF-INPUT
10 /* s() returns the square of a number */
11 define s(x){
12 return(x*x);
13 }
14
15 /* use s on 2 and 8 */
16 x=$1
17 "The area of a square with ";x;" ft sides is ";s(x);" sq ft";
18
19 quit
20 END-OF-INPUT
The output of sqr 9 and sqr 15 is shown below.
$ sqr 9
The area of a square with 9
ft sides is 81
$ sqr 15
The area of a square with 15
ft sides is 225
sq ft
$
Using bc you can open up a world of complex numeric evaluations without
having
to go through the clunky syntax of expr. It also means that there is another
way of skinning the cat (or reinventing the wheel).
The next listing, addgrg, is a bc version of the procedure to add days to a
Gregorian date. The algorithm is exactly the same one used in the preceding
series, and I refer you to them for an explanation of the logic in date
calculations (see Resources at the end of this article). This uses the old
bc
convention of only using single-character names for variables and functions,
so the functions have cryptic names such as y() and j(). If your bc supports
it, use better names such as days_in_year() or cvt_grg_to_jul(). The script
starts out with a test to ensure that two arguments exist on the command
line,
which should be a year in yyyymmdd format and a number of days, positive or
negative, to add to the date. Again, I only check for the existence of the
arguments, and do not verify their correctness. Most of the bc script
defines
functions, and the arguments aren't used until line 101.
There are some additional features of a bc program that you will see here.
Look at the function y() defines at lines 12 through 27. The first oddity is
the use of
y as the name of a function and a variable. The designer of bc realized the
limitation of one-character names and made sure that although two functions
could not have the same name, a function could have the same name as a
variable. Without that feature, bc would run out of names quickly. Another
new
feature appears at line 13. The variable x is declared as auto. By default
all
variables in bc are global and available to all parts of the program. The
auto
declaration causes a temporary variable named x to be created that is only
valid inside the function in which it is declared. Inside the function y(),
x
is created, used, and then thrown away when y() is finished. If there
were
a global variable named x, it would not be affected by the use of a local
version of x inside the y() function.
The third new feature first appears at
lines 15 through 17. I have made mention of if tests earlier, but this is
the
first example of the syntax of an if test. Everything within the curly
braces
is executed if x==0 is true.
A brief description of the functions in the script is as follows:
- The function y(), at lines 11 through 27, tests a year for even
divisibility by 400, 100, and 4 and returns 366 or 365 accordingly as the
number of days in that year.
- The function m(), at lines 28 though 41, receives a year and a month
and uses
the "30 days hath September" rule to return 30 or 31. If the month is February
then the days in the year are tested and returns 29 or 28 depending on the
number of days in the year.
- The function g(), at lines 42 through 55, is passed a Julian format date
of the
form yyyyddd and converts it to a Gregorian format, yyyymmdd, by calculating
the days in each month and reducing the days portion of the date until a
month
is found.
- The function j(), at lines 56 though 61, is the inverse function and
converts
a Gregorian format date to Julian format by adding up the days in each month
and the leftover days to obtain the ddd portion of the date.
- The function b(), at lines 70 through 90, should have had a better name as
it
adds positive or negative days to a Julian format date. Julian date addition
is a fairly simple matter of adding the days and then checking if
you
need to wrap forward or backward to another year.
- Finally, the function a(), at lines 91 through 98, accepts a Gregorian
format
date and a days argument, converts the date to Julian format, calls b(), and
then converts the result back to Gregorian format.
At line 101, the a() function is called using the two arguments that arrived
on the command line.
1 # addgrg yyyymmdd days - adds days to yyyymmdd and outputs new yyyymmdd
2
3 if [ $# != 2 ]
4 then
5 echo "Not enough arguments"
6 echo "Syntax addgrg yyyymmdd [-]days"
7 exit
8 fi
9
10 bc < 0 ; ++m){
48 n = m(y,m);
49 d -= n;
50 }
51 void --m;
52 d+=n;
53 g= (y*10000)+(m*100)+d;
54 return(g);
55 }
56 /* convert gregorian (yyyymmdd) to julian(yyyyddd)*/
57 define j(g){
58 auto y,m,d,x,i,j;
59 y=g/10000;
60 m=(g/100) % 100;
61 d=g%100;
62 for(x=1;xi){
78 y+=1;
79 d-=i;
80 i=y(y);
81 }
82 while(d<1){
83 y-=1;
84 i=y(y);
85 d+=i;
86 }
87 r= (y*1000)+d;
88 return(r);
89
90 }
91 /* add positive or negative days to a greg date - returns new date */
92 define a(g,a){
93 auto j,r;
94 j=j(g);
95 j=b(j,a);
96 r=g(j)
97 return(r);
98 }
99
100 /* call add to greg using the two command line arguments*/
101 a($1,$2)
102
103 quit
104 END-OF-INPUT
You can see from reading the bc script that it is much more legible than
the equivalent shell scripts even though it uses the identical logic in a
more
compact form.
The sample output from addgrg tests in and around leap years and boundary
conditions.
$ addgrg 19980101 -1
19971231
addgrg 20000101 60
20000301
addgrg 20000101 59
20000229
addgrg 19000101 365
19010101
addgrg 20001231 -365
20000101
$
I learned a lot about bc and some useful things about here documents by
reinventing this wheel, and I hope you did too.
Contact
us for a free consultation. |