bc revisited

Here we explore bc again, but now more as a language in which to get more involved calculations. Just type bc -lq and you will get a prompt (the q flag is to avoid the initial message). There we can define any variable, for example:

$ bc -lq
a=sqrt(2)
b=sqrt(3)
a+b
3.14626436994197234232
last
3.14626436994197234232

We don't need to end lines with ;, although it is always a good idea to do so. We can now write an if statement:

if(1>0) print "hello\n"
hello
if(1<0) print "hello\n" else print "bye\n"
bye
if(1>0) {print "hello 1\n"; print "hello 2\n"}
hello 1
hello 2

We now write a for statement:

for(i=0;i<5;i++) print i," ";print "\n"
0 1 2 3 4
quit
$

The built-in mathematical functions are power, square root, sine, cosine, arctangent, natural logarithm, exponential and Bessel function of order n. They are called as ^, sqrt(), s(x), c(x), a(x), l(x), e(x), j(n,x). The trigonometric functions work with radians. By default, they operate with 20 decimal places, which is enough for most practical cases.

What if we want to use other trigonometric functions? We can just apply some trigonometric identities:

x=0.1;
# tan(x)
s(x)/c(x)
.10033467208545054505
# arcsin(x)
a(x/sqrt(1-x^2))
.10016742116155979633
# arccos(x)
a(sqrt(1-x^2)/x)
1.47062890563333682288

An important quantity we may want to use is, of course, π. But we know that tan(π/4)=1, so that

pi=4*a(1);
print pi,"\n"
3.14159265358979323844

A weird finding regarding powers of negative numbers:

$ bc -lq
-1^2
1
-(1)^2
1
(-1)^2
1
-(1^2)
-1
quit

$ python3 -c "print(-1**2)"
-1
$ python3 -c "print(-(1)**2)"
-1
$ python3 -c "print((-1)**2)"
1
$ python3 -c "print(-(1**2))"
-1

$ octave-cli --eval "-1^2"
ans = -1
$ octave-cli --eval "-(1)^2"
ans = -1
$ octave-cli --eval "(-1)^2"
ans = 1
$ octave-cli --eval "-(1^2)"
ans = -1

All calculators I know behave like Pyton or Octave in this operation. I find bc's behaviour quite unsettling here.

We can write all commands to a file, and then execute them all at once. For example:

$ cat snd.bc
# standard normal distribution (snd)
scale=16 
pi=4*a(1);
n=100000;
dx=10/n;
snd=0;
for(i=-n;i<n+1;i++) {
x=i*dx;
snd+=e(-(x^2)/2)*dx;
}
snd*=1/sqrt(2*pi);
print snd,"\n";

We execute the script as

$ time bc -l < snd.bc
.9999999999969449

real	0m18.382s
user	0m18.377s
sys	0m0.004s

We can compare the performance of bc against the same calculation with NumPy:

$ cat snd.py
import numpy as np
n=100000
dx=10/n
snd=0
for i in range(-n,n+1):
    x=dx*i;
    snd+=np.exp(-x**2/2)*dx;
snd*=1/np.sqrt(2*np.pi)
print(snd)
$ python3 snd.py 
0.9999999999998942

real	0m0.629s
user	0m0.698s
sys	0m0.240s

In the previous case, for n=1000 or even 10000, the differences are not so dramatic. But for n=10000 the time differences become significant. The results are slightly different and I am not sure which one has more accuracy. Python is using here double precision numbers, which means 16 digits, and we have set the bc scale to 16 as well.

Another potential issue is the lack of fractional exponents, but this is easily overcome:

2^3
8
2^0.5
Runtime warning (func=(main), adr=8): non-zero scale in exponent
1
e(0.5*l(2))
1.41421356237309504878

We can define functions and store them at ~/.bcrc. In this repository you can find a nice example. You can summon the functions library with the command:

bc ~/.bcrc -lq

Despite not being the ideal tool for complex and long calculations, this extremely lightweight tool is very capable for most practical situations.