Using bc, Part 2

Programming the bc calculator tool

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.


2 /* s() returns the square of a number */
3 define s(x){
4 return(x*x);
5 }
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";
13 quit

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 }
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";
15 quit

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
 3 if [ $# != 1 ]
 4 then
 5    echo "A number argument is required"
 6    exit
 7 fi
 9 bc <<END-OF-INPUT
10 /* s() returns the square of a number */
11 define s(x){
12    return(x*x);
13 }
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";
19 quit

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
 3 if [ $# != 2 ] 
 4 then
 5    echo "Not enough arguments"
 6    echo "Syntax addgrg yyyymmdd [-]days"
 7    exit
 8 fi
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);
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 }
100 /* call add to greg using the two command line arguments*/
101 a($1,$2)
103 quit

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
addgrg 20000101 60
addgrg 20000101 59
addgrg 19000101 365
addgrg 20001231 -365

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.






    • GEN-CODE
    • COMPILERS   



Search Now:
In Association with

Copyright©2001 King Computer Services Inc. All rights reserved.