Blob Blame History Raw
#!/usr/bin/python
"""
################################################################
# clac version 004  2009-10-16      (c)  Mark Borgerding

Usage: clac [options] [expr1 [expr2 ...] ]

clac (Command Line Advanced Calculator) evaluates mathematical 
expressions given as arguments or as stdin and writes the answer(s) to stdout.

Unlike other command line calculators, clac:
    * has infix (natural order) expression syntax 
    * handles complex numbers.
    * defines a great many functions and constants by default
    * allows easy definition of new user functions and constants using python

"But I don't know python". 

You don't need to know python to use clac.  Expressions like 
    "1+2*3" 
    "sin(pi/4)" 
    "exp(j*2*pi/100)"
    "round( degrees(phase( e**(2j))))"
... act pretty much as you would expect.  Run the selftest (-T) for more examples.

Knowing python will help you extend clac's functionality.  Relax, it's not that hard.

Everything in the python math, and cmath modules is available...
    cos     cosh    acos    acosh   sin     sinh    asin    asinh
    tan     tanh    atan    atan2   atanh
    floor   ceil    fabs    abs     fmod modf degrees radians 
    exp     frexp   ldexp   hypot   pow     sqrt    log     log10

  ... plus a variety of other functions created to make your life easier
    phase angle mag2 cpx conj nfft gcd lcm sign log2 mag real imag

For more info about a given func, run: clac "help(func)"

"Oh but I really want the conversion from meters per second 
        to furlongs per fortnight"

New data, functions,and modules can be made via the user's .clacrc file.

e.g.
    #sample .clacrc
    from random import *
    c=3e8
    verbose=True          
    binary_prefix=True   # equivalent to using -k
    complex_tuple=True   # equivalent to using -p
    def myfunc(x):
        'a user defined function'
        return x+1


Options:
    -k : use k,m,g as binary prefix (kibibyte,mebibyte,gibibyte)
    -p : print complex numbers as (re,im) pairs,rather than re+imj
    -T : run a self-diagnostic test (which makes a nice syntax demo)
    -v : verbose
    -h : help message
    -c file: reads a config file other than $HOME/.clacrc
    -C : display copyright
"""

# __future__ division allows division of two integers to produce a float result
from __future__ import division


COPYRIGHT = """
                    GNU GENERAL PUBLIC LICENSE
                       Version 3, 29 June 2007

 Copyright (C) 2009 Mark Borgerding    
   email: mark at borgerding dot net

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

    This program has an explicit linking exception for the extensions 
    via the rc file interface.
"""

selftests="""
1+2 == 3
sqrt(-1) == j
-2*asin(-1) == pi
abs(sin(pi)) < 1e-9
abs(1-cos(0)) < 1e-9
round( 3.1 + -4.8j) == (3-5j)
ceil( 3.1 + -4.8j) == (4-4j)
abs( 3-4j) == 5
degrees(pi) == 180 
radians(180) == pi 
abs( exp(j*pi) + 1 ) < 1e-9
pow(1.2,3.4) == 1.2**3.4
ldexp(1.2,3) == 1.2 * 2 ** 3
modf(1.2)[1] == 1
log(81,3) == 4
gcd(6,8) == 2
lcm(6,8) == 24
angle( exp( j*pi ) ) == pi
log(-1)**2 == -1*pow(pi,2)
round( degrees(phase( e**(2j)))) == 115
sum( [ round(42 * exp(j*2*x*pi/4)) for x in range(4)] ) == 0
oct(8) == '010'
0x42-042-42 == -10
1k == 1024
1m == 2**20
1g == 2**30
2**10-1 == 1023
"""

import math
from math import e,pi,atan2,fmod,frexp,hypot,ldexp,modf

def degrees(x):
    return x*180/pi

def radians(x):
    return x*pi/180

import cmath
from cmath import acosh,asinh,atanh

# other nice-to-have constants
j = i = cmath.sqrt(-1)

# some applications might want (re,im) instead of re+imj
__print_cpx_as_pairs__ = False

def dip(x):
    'demote, if possible, a complex to scalar'
    if type(x) == complex and x.imag == 0:
        return x.real
    else:
        return x

def which_call( x , mathfunc,cmathfunc , allowNegative=True):
    x=dip(x)
    if type(x) == complex or (allowNegative == False and x<0):
        return cmathfunc(x)
    else:
        return mathfunc(x)

# marshall between the math and cmath functions automatically
def acos( x ): return which_call(x,math.acos,cmath.acos)
def asin( x ): return which_call(x,math.asin,cmath.asin)
def atan( x ): return which_call(x,math.atan,cmath.atan)
def cos( x ): return which_call(x,math.cos,cmath.cos)
def cosh( x ): return which_call(x,math.cosh,cmath.cosh)
def sin( x ): return which_call(x,math.sin,cmath.sin)
def sinh( x ): return which_call(x,math.sinh,cmath.sinh)
def tan( x ): return which_call(x,math.tan,cmath.tan)
def tanh( x ): return which_call(x,math.tanh,cmath.tanh)
def exp( x ): return which_call(x,math.exp,cmath.exp)
def log10( x ): return which_call(x,math.log10,cmath.log10,False)
def sqrt( x ): return which_call(x,math.sqrt,cmath.sqrt,False)

#steal the help strings from the cmath functions
acos.__doc__ = cmath.acos.__doc__
asin.__doc__ = cmath.asin.__doc__
atan.__doc__ = cmath.atan.__doc__
cos.__doc__ = cmath.cos.__doc__
cosh.__doc__ = cmath.cosh.__doc__
sin.__doc__ = cmath.sin.__doc__
sinh.__doc__ = cmath.sinh.__doc__
tan.__doc__ = cmath.tan.__doc__
tanh.__doc__ = cmath.tanh.__doc__
exp.__doc__ = cmath.exp.__doc__
log10.__doc__ = cmath.log10.__doc__
sqrt.__doc__ = cmath.sqrt.__doc__

def log(x,b=e):
    'log(x[, base]) -> the logarithm of x to the given base.\nIf the base not specified, returns the natural logarithm (base e) of x.'
    if type(x) == complex or x<0:
        return dip( cmath.log(x) / cmath.log(b) )
    else:
        return math.log(x)/math.log(b)

def real(x):
    'return just the real portion'
    if type(x) == complex:
        return x.real
    else:
        return x

def imag(x):
    'return just the imaginary portion'
    if type(x) == complex:
        return x.imag
    else:
        return 0

def sign(x):
    'returns -1,0,1 for negative,zero,positive numbers'
    if x == 0:
        return 0
    elif x > 0:
        return 1
    else:
        return -1

def log2(x):
    'logarithm base 2'
    return log(x,2)

def nfft( insize,direction=1, musthave=2,factors=(2,3,5) ):
    """find a 'good' fft size close to the desired size
        sign(direction)     search directions
        -1                  lower
        1                   higher
        0                   closest
    """
    insize=round(insize)
    direction = sign(direction)
    offset=0
    if type(factors) not in (tuple,list): 
        factors=(factors,)
    while True:
        ntmp = insize+offset
        if musthave > 1 and (ntmp%musthave)==0:
            for f in factors:
                while (ntmp%f)==0:
                    ntmp = ntmp / f
                if ntmp==1:
                    return insize+offset
        if direction == 0:
            offset = (offset<=0) - offset 
        else:
            offset += direction

def gcd(x,y):
    'greatest common denominator'
    while x>0:
        (x,y) = (y%x,x) # Guido showed me this one on the geek cruise
    return y

def lcm(x,y):
    'least common multiple'
    return x*y/gcd(x,y)

def phase(z):
    'phase of a complex in radians' 
    z=cpx(z)
    return atan2( z.imag , z.real )

def cpx(x):
    'convert a number or tuple to a complex'
    if type(x) == tuple:
        return complex( x[0] , x[1] )
    else:
        return complex(x)

def mag2(x):
    'magnitude,squared'
    return abs(x*x)

def conj( x ):
    'complex conjugate'
    x = cpx( x )
    return complex( x.real , -x.imag )

def complexify(x,func ):
    'call func on the real and imaginary portions, creating a complex from the respective results'
    if type(x) == complex and x.imag != 0:
        return dip( complex( func(x.real) , func(x.imag) ) )
    else:
        return func(x)

# overwrite the builtin math functions that don't handle complex
def round(x):
    'nearest integer' 
    if type(x) == complex:
        return complexify( x , round )
    else:
        return math.floor(x+.5)

def floor(x):
    'round towards negative infinity' 
    return complexify( x , math.floor )
def ceil(x):
    'round towards positive infinity' 
    return complexify( x , math.ceil )
def fabs(x):
    'absolute value of real and imaginary parts'
    return complexify( x , math.fabs )

def pow(x,y):
    'raise to a power'
    if type(x) == complex or type(y) == complex:
        return dip( exp( y * log( x ) ) ) # for some reason cmath does not define pow
    else:
        return math.pow(x,y)

# some people may prefer these names
mag=abs
angle=phase

def fmt_float(x):
    'convert a single float to a string'
    return '%.17g' % x

def fmt_cpx(x):
    'convert a complex value to a string'
    if __print_cpx_as_pairs__ == True:
        return '(%s,%s)' % ( fmt_float( x.real) , fmt_float( x.imag) )
    elif x.imag<0:
        return '%s %sj' % ( fmt_float( x.real) , fmt_float( x.imag) )
    else:
        return '%s+%sj' % ( fmt_float( x.real) , fmt_float( x.imag) )

def fmt(x):
    'convert the evaluated expression to a string'
    if type(x) == complex:
        if x.imag == 0:
            return fmt_float( x.real)
        else:
            return fmt_cpx(x)
    elif type(x) in (list,tuple):
        return ','.join(  [fmt(item) for item in x ])
    elif type(x) == float:
        return fmt_float(x)
    else:
        return '%s' % x


class Session:
    'clac sequence of commands'
    def __init__(self):
        import os
        self.rcfile = '%s/.clacrc' % os.getenv('HOME')
        self.env = {}
        self.env['verbose'] = False
        self.env['binary_prefix'] = False
        self.env['complex_tuple'] = False

    def read_cmdline( self):
        import sys
        import getopt
        try:
            opts,self.args = getopt.getopt(sys.argv[1:] , 'pvc:hCkT')
            opts = dict(opts)
        except getopt.GetoptError,e:
            opts={}
            opts['-h'] = True

        if opts.has_key('-C'):
            sys.stderr.write(COPYRIGHT)
            sys.exit(1)
        if opts.has_key('-h'):
            sys.stderr.write(__doc__)
            sys.exit(1)
        self.env['verbose'] = opts.has_key('-v')
        self.env['binary_prefix'] = opts.has_key('-k')
        self.env['complex_tuple'] = opts.has_key('-p')
        self.rcfile = opts.get('-c' , self.rcfile )
        if opts.has_key('-T'):
            self.env['binary_prefix'] = True # needed for some of the selftests
            self.run_self_test()
            sys.exit(0)

    def eval_text( self,text,name="<text>" ):
        'evaluate a set of commands in a file' 
        global __print_cpx_as_pairs__
        __print_cpx_as_pairs__ = self.env['complex_tuple']
        eval( compile( text ,name,'exec') ,globals(),self.env )

    def eval_file( self,file ):
        'evaluate a set of commands in a file' 
        self.eval_text( ''.join( open(file).readlines() ) , '<%s>' % file)

    def eval_to_string( self,s  ):
        'evaluate an expression and print the string'
        if s is None: return
        if s in ('','\n'): return
        if self.env['binary_prefix']:
            import re
            s = re.sub( r'(\b[\d+])[kK]\b' , '\\1*1024' , s )
            s = re.sub( r'(\b[\d+])[mM]\b' , '\\1*1048576' , s )
            s = re.sub( r'(\b[\d+])[gG]\b' , '\\1*1073741824' , s )

        global __print_cpx_as_pairs__
        __print_cpx_as_pairs__ = self.env['complex_tuple']

        eval_rc = eval( s ,globals(),self.env )
        if  eval_rc is None: return
        return fmt( eval_rc )

    def eval_expr( self,s  ):
        s2 = self.eval_to_string(s)
        if s2:
            print s2

    def run_self_test(self):
        for expr in selftests.split('\n'):
            if expr == "": continue
            result = self.eval_to_string(expr)
            if result == "True":
                print " %s" % expr
            else:
                raise AssertionError( "FAILED:%s" % expr )

    def main(self):
        self.read_cmdline()
        import os
        import sys
        # read and evaluate the rc file in the current context
        if os.access( self.rcfile,os.F_OK):
            self.eval_file( self.rcfile )

        if len( self.args ) < 1:
            #interactive mode
            while 1:
                line = sys.stdin.readline()
                if line == '':
                    break
                self.eval_expr( line )
        else:
            # arguments are expressions
            for expr in self.args:
                self.eval_expr( expr )

if __name__ == "__main__":
    ses=Session()
    ses.main()