1### 2# Copyright (c) 2002-2004, Jeremiah Fincher 3# Copyright (c) 2008-2009, James McCoy 4# All rights reserved. 5# 6# Redistribution and use in source and binary forms, with or without 7# modification, are permitted provided that the following conditions are met: 8# 9# * Redistributions of source code must retain the above copyright notice, 10# this list of conditions, and the following disclaimer. 11# * Redistributions in binary form must reproduce the above copyright notice, 12# this list of conditions, and the following disclaimer in the 13# documentation and/or other materials provided with the distribution. 14# * Neither the name of the author of this software nor the name of 15# contributors to this software may be used to endorse or promote products 16# derived from this software without specific prior written consent. 17# 18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 22# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 23# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 24# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 26# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 28# POSSIBILITY OF SUCH DAMAGE. 29### 30 31from __future__ import division 32 33import re 34import math 35import cmath 36import types 37import string 38 39import supybot.utils as utils 40from supybot.commands import * 41import supybot.utils.minisix as minisix 42import supybot.callbacks as callbacks 43from supybot.i18n import PluginInternationalization, internationalizeDocstring 44_ = PluginInternationalization('Math') 45 46from .local import convertcore 47 48baseArg = ('int', 'base', lambda i: i <= 36) 49 50class Math(callbacks.Plugin): 51 """Provides commands to work with math, such as a calculator and 52 a unit converter.""" 53 @internationalizeDocstring 54 def base(self, irc, msg, args, frm, to, number): 55 """<fromBase> [<toBase>] <number> 56 57 Converts <number> from base <fromBase> to base <toBase>. 58 If <toBase> is left out, it converts to decimal. 59 """ 60 if not number: 61 number = str(to) 62 to = 10 63 try: 64 irc.reply(self._convertBaseToBase(number, to, frm)) 65 except ValueError: 66 irc.error(_('Invalid <number> for base %s: %s') % (frm, number)) 67 base = wrap(base, [('int', 'base', lambda i: 2 <= i <= 36), 68 optional(('int', 'base', lambda i: 2 <= i <= 36), 10), 69 additional('something')]) 70 71 def _convertDecimalToBase(self, number, base): 72 """Convert a decimal number to another base; returns a string.""" 73 if number == 0: 74 return '0' 75 elif number < 0: 76 negative = True 77 number = -number 78 else: 79 negative = False 80 digits = [] 81 while number != 0: 82 digit = number % base 83 if digit >= 10: 84 digit = string.ascii_uppercase[digit - 10] 85 else: 86 digit = str(digit) 87 digits.append(digit) 88 number = number // base 89 digits.reverse() 90 return '-'*negative + ''.join(digits) 91 92 def _convertBaseToBase(self, number, toBase, fromBase): 93 """Convert a number from any base, 2 through 36, to any other 94 base, 2 through 36. Returns a string.""" 95 number = minisix.long(str(number), fromBase) 96 if toBase == 10: 97 return str(number) 98 return self._convertDecimalToBase(number, toBase) 99 100 _mathEnv = {'__builtins__': types.ModuleType('__builtins__'), 'i': 1j} 101 _mathEnv.update(math.__dict__) 102 _mathEnv.update(cmath.__dict__) 103 def _sqrt(x): 104 if isinstance(x, complex) or x < 0: 105 return cmath.sqrt(x) 106 else: 107 return math.sqrt(x) 108 def _cbrt(x): 109 return math.pow(x, 1.0/3) 110 def _factorial(x): 111 if x<=10000: 112 return float(math.factorial(x)) 113 else: 114 raise Exception('factorial argument too large') 115 _mathEnv['sqrt'] = _sqrt 116 _mathEnv['cbrt'] = _cbrt 117 _mathEnv['abs'] = abs 118 _mathEnv['max'] = max 119 _mathEnv['min'] = min 120 _mathEnv['round'] = lambda x, y=0: round(x, int(y)) 121 _mathSafeEnv = dict([(x,y) for x,y in _mathEnv.items()]) 122 _mathSafeEnv['factorial'] = _factorial 123 _mathRe = re.compile(r'((?:(?<![A-Fa-f\d)])-)?' 124 r'(?:0x[A-Fa-f\d]+|' 125 r'0[0-7]+|' 126 r'\d+\.\d+|' 127 r'\.\d+|' 128 r'\d+\.|' 129 r'\d+))') 130 def _floatToString(self, x): 131 if -1e-10 < x < 1e-10: 132 return '0' 133 elif -1e-10 < int(x) - x < 1e-10: 134 return str(int(x)) 135 else: 136 return str(x) 137 138 def _complexToString(self, x): 139 realS = self._floatToString(x.real) 140 imagS = self._floatToString(x.imag) 141 if imagS == '0': 142 return realS 143 elif imagS == '1': 144 imagS = '+i' 145 elif imagS == '-1': 146 imagS = '-i' 147 elif x.imag < 0: 148 imagS = '%si' % imagS 149 else: 150 imagS = '+%si' % imagS 151 if realS == '0' and imagS == '0': 152 return '0' 153 elif realS == '0': 154 return imagS.lstrip('+') 155 elif imagS == '0': 156 return realS 157 else: 158 return '%s%s' % (realS, imagS) 159 160 _calc_match_forbidden_chars = re.compile('[_\[\]]') 161 _calc_remover = utils.str.MultipleRemover('_[] \t') 162 ### 163 # So this is how the 'calc' command works: 164 # First, we make a nice little safe environment for evaluation; basically, 165 # the names in the 'math' and 'cmath' modules. Then, we remove the ability 166 # of a random user to get ints evaluated: this means we have to turn all 167 # int literals (even octal numbers and hexadecimal numbers) into floats. 168 # Then we delete all square brackets, underscores, and whitespace, so no 169 # one can do list comprehensions or call __...__ functions. 170 ### 171 @internationalizeDocstring 172 def calc(self, irc, msg, args, text): 173 """<math expression> 174 175 Returns the value of the evaluated <math expression>. The syntax is 176 Python syntax; the type of arithmetic is floating point. Floating 177 point arithmetic is used in order to prevent a user from being able to 178 crash to the bot with something like '10**10**10**10'. One consequence 179 is that large values such as '10**24' might not be exact. 180 """ 181 try: 182 text = str(text) 183 except UnicodeEncodeError: 184 irc.error(_("There's no reason you should have fancy non-ASCII " 185 "characters in your mathematical expression. " 186 "Please remove them.")) 187 return 188 if self._calc_match_forbidden_chars.match(text): 189 # Note: this is important to keep this to forbid usage of 190 # __builtins__ 191 irc.error(_('There\'s really no reason why you should have ' 192 'underscores or brackets in your mathematical ' 193 'expression. Please remove them.')) 194 return 195 text = self._calc_remover(text) 196 if 'lambda' in text: 197 irc.error(_('You can\'t use lambda in this command.')) 198 return 199 text = text.lower() 200 def handleMatch(m): 201 s = m.group(1) 202 if s.startswith('0x'): 203 i = int(s, 16) 204 elif s.startswith('0') and '.' not in s: 205 try: 206 i = int(s, 8) 207 except ValueError: 208 i = int(s) 209 else: 210 i = float(s) 211 x = complex(i) 212 if x.imag == 0: 213 x = x.real 214 # Need to use string-formatting here instead of str() because 215 # use of str() on large numbers loses information: 216 # str(float(33333333333333)) => '3.33333333333e+13' 217 # float('3.33333333333e+13') => 33333333333300.0 218 return '%.16f' % x 219 return str(x) 220 text = self._mathRe.sub(handleMatch, text) 221 try: 222 self.log.info('evaluating %q from %s', text, msg.prefix) 223 x = complex(eval(text, self._mathSafeEnv, self._mathSafeEnv)) 224 irc.reply(self._complexToString(x)) 225 except OverflowError: 226 maxFloat = math.ldexp(0.9999999999999999, 1024) 227 irc.error(_('The answer exceeded %s or so.') % maxFloat) 228 except TypeError: 229 irc.error(_('Something in there wasn\'t a valid number.')) 230 except NameError as e: 231 irc.error(_('%s is not a defined function.') % str(e).split()[1]) 232 except Exception as e: 233 irc.error(str(e)) 234 calc = wrap(calc, ['text']) 235 236 @internationalizeDocstring 237 def icalc(self, irc, msg, args, text): 238 """<math expression> 239 240 This is the same as the calc command except that it allows integer 241 math, and can thus cause the bot to suck up CPU. Hence it requires 242 the 'trusted' capability to use. 243 """ 244 if self._calc_match_forbidden_chars.match(text): 245 # Note: this is important to keep this to forbid usage of 246 # __builtins__ 247 irc.error(_('There\'s really no reason why you should have ' 248 'underscores or brackets in your mathematical ' 249 'expression. Please remove them.')) 250 return 251 # This removes spaces, too, but we'll leave the removal of _[] for 252 # safety's sake. 253 text = self._calc_remover(text) 254 if 'lambda' in text: 255 irc.error(_('You can\'t use lambda in this command.')) 256 return 257 text = text.replace('lambda', '') 258 try: 259 self.log.info('evaluating %q from %s', text, msg.prefix) 260 irc.reply(str(eval(text, self._mathEnv, self._mathEnv))) 261 except OverflowError: 262 maxFloat = math.ldexp(0.9999999999999999, 1024) 263 irc.error(_('The answer exceeded %s or so.') % maxFloat) 264 except TypeError: 265 irc.error(_('Something in there wasn\'t a valid number.')) 266 except NameError as e: 267 irc.error(_('%s is not a defined function.') % str(e).split()[1]) 268 except Exception as e: 269 irc.error(utils.exnToString(e)) 270 icalc = wrap(icalc, [('checkCapability', 'trusted'), 'text']) 271 272 _rpnEnv = { 273 'dup': lambda s: s.extend([s.pop()]*2), 274 'swap': lambda s: s.extend([s.pop(), s.pop()]) 275 } 276 def rpn(self, irc, msg, args): 277 """<rpn math expression> 278 279 Returns the value of an RPN expression. 280 """ 281 stack = [] 282 for arg in args: 283 try: 284 x = complex(arg) 285 if x == abs(x): 286 x = abs(x) 287 stack.append(x) 288 except ValueError: # Not a float. 289 if arg in self._mathSafeEnv: 290 f = self._mathSafeEnv[arg] 291 if callable(f): 292 called = False 293 arguments = [] 294 while not called and stack: 295 arguments.append(stack.pop()) 296 try: 297 stack.append(f(*arguments)) 298 called = True 299 except TypeError: 300 pass 301 if not called: 302 irc.error(_('Not enough arguments for %s') % arg) 303 return 304 else: 305 stack.append(f) 306 elif arg in self._rpnEnv: 307 self._rpnEnv[arg](stack) 308 else: 309 arg2 = stack.pop() 310 arg1 = stack.pop() 311 s = '%s%s%s' % (arg1, arg, arg2) 312 try: 313 stack.append(eval(s, self._mathSafeEnv, self._mathSafeEnv)) 314 except SyntaxError: 315 irc.error(format(_('%q is not a defined function.'), 316 arg)) 317 return 318 if len(stack) == 1: 319 irc.reply(str(self._complexToString(complex(stack[0])))) 320 else: 321 s = ', '.join(map(self._complexToString, list(map(complex, stack)))) 322 irc.reply(_('Stack: [%s]') % s) 323 324 @internationalizeDocstring 325 def convert(self, irc, msg, args, number, unit1, unit2): 326 """[<number>] <unit> to <other unit> 327 328 Converts from <unit> to <other unit>. If number isn't given, it 329 defaults to 1. For unit information, see 'units' command. 330 """ 331 try: 332 digits = len(str(number).split('.')[1]) 333 except IndexError: 334 digits = 0 335 try: 336 newNum = convertcore.convert(number, unit1, unit2) 337 if isinstance(newNum, float): 338 zeros = 0 339 for char in str(newNum).split('.')[1]: 340 if char != '0': 341 break 342 zeros += 1 343 # Let's add one signifiant digit. Physicists would not like 344 # that, but common people usually do not give extra zeros... 345 # (for example, with '32 C to F', an extra digit would be 346 # expected). 347 newNum = round(newNum, digits + 1 + zeros) 348 newNum = self._floatToString(newNum) 349 irc.reply(str(newNum)) 350 except convertcore.UnitDataError as ude: 351 irc.error(str(ude)) 352 convert = wrap(convert, [optional('float', 1.0),'something','to','text']) 353 354 @internationalizeDocstring 355 def units(self, irc, msg, args, type): 356 """ [<type>] 357 358 With no arguments, returns a list of measurement types, which can be 359 passed as arguments. When called with a type as an argument, returns 360 the units of that type. 361 """ 362 363 irc.reply(convertcore.units(type)) 364 units = wrap(units, [additional('text')]) 365 366Class = Math 367 368# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: 369