#
# calculator.py : A calculator module for the deskbar applet.
#
# Copyright (C) 2006 by Callum McKenzie
#
# 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 2 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, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# Authors: Callum McKenzie
#
# Enable this to see what goes wrong when evaluating the string.
# Since half-written strings get evaluated, this produces a lot
# of crap as well as the answer you want.
_debug = False
from gettext import gettext as _
from __future__ import division
import deskbar.Handler, deskbar.Match
import deskbar.Categories
import cgi
import gtk
import re
import math
if _debug:
import traceback
# This is evil since I strongly suspect this isn't meant to be public API.
# However, I can't see a better way.
deskbar.Categories.CATEGORIES["calculator"] = { "name" : _("Calculator") }
HANDLERS = {
"CalculatorHandler" : {
"name" : _("Calculator"),
"description" : _("Calculate simple equations"),
"version" : "1.5",
}
}
def bin (n):
"""A local binary equivalent of the hex and oct builtins."""
if (n == 0):
return "0b0"
s = ""
if (n < 0):
while n != -1:
s = str (n & 1) + s
n >>= 1
return "0b" + "...111" + s
else:
while n != 0:
s = str (n & 1) + s
n >>= 1
return "0b" + s
# These next three make sure {hex, oct, bin} can handle floating point,
# by rounding. This makes sure things like hex(255/2) behave as a
# programmer would expect while allowing 255/2 to equal 127.5 for normal
# people. Abstracting out the body of these into a single function which
# takes hex, oct or bin as an argument seems to run into problems with
# those functions not being defined correctly in the resticted eval (?).
def lenient_hex (c):
try:
return hex (c)
except TypeError:
return hex (int (c))
def lenient_oct (c):
try:
return oct (c)
except TypeError:
return oct (int (c))
def lenient_bin (c):
try:
return bin (c)
except TypeError:
return bin (int (c))
class CalculatorMatch (deskbar.Match.Match):
def get_name (self, text=None):
# I really think that deskbar should be doing the
# escaping for us.
escapedtext = cgi.escape (text)
return { "name" : self.name, "escapedtext" : escapedtext }
def get_verb (self):
"""We print out the entire equation."""
return "%(escapedtext)s = **%(name)s**"
def get_category (self):
return "calculator"
def action (self, text=None):
for clipboard in ('CLIPBOARD', 'PRIMARY'):
gtk.clipboard_get(clipboard).set_text(str(self.name))
class CalculatorHandler (deskbar.Handler.Handler):
def __init__ (self):
deskbar.Handler.Handler.__init__ (self, "gtk-add")
self.hexre = re.compile ("0[Xx][0-9a-fA-F_]*[0-9a-fA-F]")
self.binre = re.compile ("0[bB][01_]*[01]")
def _number_parser (self, match, base):
"""A generic number parser, regardless of base. It also ignores the
'_' character so it can be used as a separator. Note how we skip
the first two characters since we assume it is something like '0x'
or '0b' and identifies the base."""
table = { '0' : 0, '1' : 1, '2' : 2, '3' : 3, '4' : 4,
'5' : 5, '6' : 6, '7' : 7, '8' : 8, '9' : 9,
'a' : 10, 'b' : 11, 'c' : 12, 'd' : 13,
'e' : 14, 'f' : 15 }
d = 0
for c in match.group()[2:]:
if c != "_":
d = d*base + table[c]
return str(d)
def _binsub (self, match):
"""Because python doesn't handle binary literals, we parse it
ourselves and replace it with a decimal representation."""
return self._number_parser (match, 2)
def _hexsub (self, match):
"""Parse the hex literal ourselves. We could let python do it, but
since we have a generic parser we use that instead."""
return self._number_parser (match, 16)
def query (self, query):
"""We evaluate the equation by first replacing hex and binary literals
with their decimal representation. (We need to check hex, so we can
distinguish 0x10b1 as a hex number, not 0x1 followed by 0b1.) We
severely restrict the eval environment. Any errors are ignored."""
restricted_dictionary = { "__builtins__" : None, "abs" : abs,
"acos" : math.acos, "asin" : math.asin,
"atan" : math.atan, "atan2" : math.atan2,
"bin" : lenient_bin,"ceil" : math.ceil,
"cos" : math.cos, "cosh" : math.cosh,
"degrees" : math.degrees,
"exp" : math.exp, "floor" : math.floor,
"hex" : lenient_hex, "int" : int,
"log" : math.log,
"log10" : math.log10, "oct" : lenient_oct,
"pi" : math.pi, "radians" : math.radians,
"round": round, "sin" : math.sin,
"sinh" : math.sinh, "sqrt" : math.sqrt,
"tan" : math.tan, "tanh" : math.tanh}
try:
scrubbedquery = query.lower()
scrubbedquery = self.hexre.sub (self._hexsub, scrubbedquery)
scrubbedquery = self.binre.sub (self._binsub, scrubbedquery)
for (c1, c2) in (("[", "("), ("{", "("), ("]", ")"), ("}", ")")):
scrubbedquery = scrubbedquery.replace (c1, c2)
answer = eval (scrubbedquery, restricted_dictionary)
# Try and avoid echoing back simple numbers. Note that this
# doesn't work well for floating point, e.g. '3.' behaves badly.
if str (answer) == query:
return []
# We need this check because the eval can return function objects
# when we are halfway through typing the expression.
if isinstance (answer, (float, int, long, str)):
return [ CalculatorMatch (self, name=answer) ]
else:
return []
except:
if _debug:
traceback.print_exc ()
return []