tags:

views:

413

answers:

5

Python loves raising exceptions, which is usually great. But I'm facing some strings I desperately want to convert to integers using C's atoi / atof semantics - e.g. atoi of "3 of 12", "3/12", "3 / 12", should all become 3; atof("3.14 seconds") should become 3.14; atoi(" -99 score") should become -99. Python of course has atoi and atof functions, which behave nothing like atoi and atof and exactly like Python's own int and float constructors.

The best I have so far, which is really ugly and hard to extend to the various float formats available:

value = 1
s = str(s).strip()
if s.startswith("-"):
    value = -1
    s = s[1:]
elif s.startswith("+"):
    s = s[1:]
try:
    mul = int("".join(itertools.takewhile(str.isdigit, s)))
except (TypeError, ValueError, AttributeError):
    mul = 0
return mul * value
+5  A: 

Perhaps use a quick regex to grab only the first part of the string that can be considered numeric? Something like...

-?[0-9]+(?:\.[0-9]+)?

for floats and for ints just,

-?[0-9]+
Amber
floats can have `e` or `E` in them also
gnibbler
+4  A: 

I think the iterative version is better than the recursive version

# Iterative
def atof(s):
    s,_,_=s.partition(' ') # eg. this helps by trimming off at the first space
    while s:
        try:
            return float(s)
        except:
            s=s[:-1]
    return 0.0

# Recursive
def atof(s):
    try:
        return float(s)
    except:
        if not s:
            return 0.0
        return atof(s[:-1])


print atof("3 of 12")
print atof("3/12")
print atof("3 / 12")
print atof("3.14 seconds")
print atof("314e-2 seconds")
print atof("-99 score")
print atof("hello world")
gnibbler
+1 for what I'm guessing is the simplest algorithm I'm going to see here!
Joe
Simple, perhaps, but not really efficient (especially if the textual part of the string is long compared to the numerical part).
Amber
If the string can have a lot of junk you'll have to use a loop instead of recursion. If you are doing lots of conversions there are faster ways to do it.
gnibbler
This also sort of rubs the wrong way in that it uses exceptions for flow control, not really an ideal situation.
Amber
atof on an empty string returns 0.0 so you should return 0.0 instead of raise in the if not s:
jmucchiello
+14  A: 

If you're so keen on getting exactly the functionality of c's atoi, why not use it directly? E.g., on my Mac,

>>> import ctypes, ctypes.util
>>> whereislib = ctypes.util.find_library('c')
>>> whereislib
'/usr/lib/libc.dylib'
>>> clib = ctypes.cdll.LoadLibrary(whereislib)
>>> clib.atoi('-99foobar')
-99

In Linux, Windows, etc, identical code should work except that you'll see a different path if you examine whereislib (only on really, really peculiar installations should this code ever fail to find the C runtime library).

If you're keen on avoiding direct C library usage, I guess you could grab the relevant prefix, e.g. with a RE such as r'\s*([+-]?\d+)', and try int on that.

Alex Martelli
+1 Great answer!
Andrew Hare
My guess would be the biggest argument against this is the platform dependence (not to mention that libraries could theoretically reside in different locations even on the same platform).
Amber
@Andrew, tx! @Dav, yes, you do have to locate libc's DLL (it may well have different names and paths), but `ctypes.util.find_library` helps -- I've just edited the answer to show how to use it.
Alex Martelli
A: 

I think I will do it char by char:

def myatof(s):
    try:
     return float(s);
    except:
     last_result = None
     for i in range(1, len(s)):
      try:
       last_result = float(s[:i])
      except:
       return last_result
    return last_result
Michał Niklas
That doesn't work properly for `314e-2`
gnibbler
+1  A: 

It's pretty straightforward to do this with regular expressions:

>>> import re
>>> p = re.compile(r'[^\d-]*(-?[\d]+(\.[\d]*)?([eE][+-]?[\d]+)?)')
>>> def test(seq):
        for s in seq:
            m = p.match(s)
            if m:
                result = m.groups()[0]
                if "." in result or "e" in result or "E" in result:
                    print "{0} -> {1}".format(s, float(result))
                else:
                    print '"{0}" -> {1}'.format(s, int(result))
            else:
                print s, "no match"

>>> test(s)
"1 0" -> 1
"3 of 12" -> 3
"3 1/2" -> 3
"3/12" -> 3
3.15 seconds -> 3.15
3.0E+102 -> 3e+102
"what about 2?" -> 2
"what about -2?" -> -2
2.10a -> 2.1
Robert Rossney