5

My prof wants me to create a function that return the sum of numbers in a string but without using any lists or list methods.

The function should look like this when operating:

>>> sum_numbers('34 3 542 11')
    590

Usually a function like this would be easy to create when using lists and list methods. But trying to do so without using them is a nightmare.

I tried the following code but they don't work:

 >>> def sum_numbers(s):
    for i in range(len(s)):
        int(i)
        total = s[i] + s[i]
        return total


>>> sum_numbers('1 2 3')
'11'

Instead of getting 1, 2, and 3 all converted into integers and added together, I instead get the string '11'. In other words, the numbers in the string still have not been converted to integers.

I also tried using a map() function but I just got the same results:

>>> def sum_numbers(s):
    for i in range(len(s)):
        map(int, s[i])
        total = s[i] + s[i]
        return total


>>> sum_numbers('1 2 3')
'11'
juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172
MountainSlayer
  • 291
  • 1
  • 5
  • 14
  • When you say "without using any lists or list methods", does this include things like `"1 2 3".split()`? (Technically `str.split()` creates a list, but you don't have to store it explicitly.) – Pierce Darragh Oct 09 '16 at 16:56
  • Pretty much. I used split() for a different question that allowed it. On the assignment sheet it says explicitly "Requirement: Do not use lists". – MountainSlayer Oct 09 '16 at 16:59
  • I provided an answer (which I think addresses all your issues!). But I wanted to comment about your use of `map`. `map` takes a function and an iterable (like a list) and returns a list where each element of the list had the function applied to it. Imagine a function `def add_one(x): return x + 1`, then doing `map(add_one, [1, 2, 3])` would return `[2, 3, 4]`. – Pierce Darragh Oct 09 '16 at 17:31
  • @PierceDarragh, in Python3, `map()` doesn't return a list any more than `range()` does -- why pick on it? I agree that using `map()` is not in the spirit of the exercise, but then again, I feel that way about `range()`, `sum()`, and `re.finditer()` too! – cdlane Oct 09 '16 at 19:03
  • @cdlane I'm confused... I wasn't talking about whether `map` is list-like. OP's example tries to use `map` on a single character... which won't work. So I was explaining how `map` works, since it seemed to me that OP didn't really know how to use it. – Pierce Darragh Oct 09 '16 at 19:05
  • Also, @T-Dot1992, my revised solution will handle all types of numbers, including negatives, scientific notation, and floating-points. I know that's not what you were specifically looking for, but maybe it'll be helpful for you? – Pierce Darragh Oct 09 '16 at 19:06
  • @PierceDarragh, understood. But your comment can also be read as saying you can't use `map()` because it 'returns a list'. – cdlane Oct 09 '16 at 19:10
  • @cdlane with regard to that, you will see above in these comments that OP did not want to use things like `str.split()`. If `.split()` is considered too list-like, then I decided that no list-returning or iterable-returning functions should be used in my solution. You will find no `map()`, `range()`, `sum()`, or `re.finditer()` because I deliberately avoided these due to my interpretation of OP's specifications. The final one-liner I provide is my personal preference, though I think it does not conform to OP's specifications. – Pierce Darragh Oct 09 '16 at 19:13

8 Answers8

6

Totally silly of course, but for fun:

s = '34 3 542 11'

n = ""; total = 0
for c in s:
    if c == " ":
        total = total + int(n)
        n = ""
    else:
        n = n + c
# add the last number
total = total + int(n)

print(total)
> 590

This assumes all characters (apart from whitespaces) are figures.

Jacob Vlijm
  • 3,099
  • 1
  • 21
  • 37
  • We have similar solutions, but I think mine is better because I check for alternate types of space characters via `str.isspace()` and I provide an opportunity to handle errors raised by any non-space non-digit character (whereas your code will result in an unhandled `ValueError`). – Pierce Darragh Oct 09 '16 at 17:20
  • 1
    @PierceDarragh you make use of `split()`, which returns a list. This means it doesn't meet the objective of the question. – roganjosh Oct 09 '16 at 17:21
  • 1
    @roganjosh I don't think you read my actual solution... the little bit at the end was a one-liner to show what *I* would do. – Pierce Darragh Oct 09 '16 at 17:22
  • 1
    @roganjosh no worries! I misread things all the time, haha. :) – Pierce Darragh Oct 09 '16 at 17:28
  • 1
    This code will fail if the input is empty or has leading or trailing whitespace. It's better to test with `isdigit()`. – ekhumoro Oct 09 '16 at 17:33
  • @ekhumoro if there is a risk of the first issue, it would be easily solved by `for c in s.strip()`, if either one of the non-white chars would *not* be a digit, the script probably *should* break. Then obviously there was a typo in the input. – Jacob Vlijm Oct 09 '16 at 17:48
  • 1
    @JacobVlijm. If you use `isdigit()`, all the whitespace issues go away. – ekhumoro Oct 09 '16 at 17:53
  • @ekhumoro again, the question is if they should. For now, I'd stick with the example. – Jacob Vlijm Oct 09 '16 at 17:55
2

You've definitely put some effort in here, but one part of your approach definitely won't work as-is: you're iterating over the characters in the string, but you keep trying to treat each character as its own number. I've written a (very commented) method that accomplishes what you want without using any lists or list methods:

def sum_numbers(s):
    """
    Convert a string of numbers into a sum of those numbers.

    :param s: A string of numbers, e.g. '1 -2 3.3 4e10'.
    :return: The floating-point sum of the numbers in the string.
    """
    def convert_s_to_val(s):
        """
        Convert a string into a number. Will handle anything that
        Python could convert to a float.

        :param s: A number as a string, e.g. '123' or '8.3e-18'.
        :return: The float value of the string.
        """
        if s:
            return float(s)
        else:
            return 0
    # These will serve as placeholders.
    sum = 0
    current = ''
    # Iterate over the string character by character.
    for c in s:
        # If the character is a space, we convert the current `current`
        # into its numeric representation.
        if c.isspace():
            sum += convert_s_to_val(current)
            current = ''
        # For anything else, we accumulate into `current`.
        else:
            current = current + c
    # Add `current`'s last value to the sum and return.
    sum += convert_s_to_val(current)
    return sum

Personally, I would use this one-liner, but it uses str.split():

def sum_numbers(s):
    return sum(map(float, s.split()))
Pierce Darragh
  • 2,072
  • 2
  • 16
  • 29
1

No lists were used (nor harmed) in the production of this answer:

def sum_string(string):
    total = 0

    if len(string):
        j = string.find(" ") % len(string) + 1
        total += int(string[:j]) + sum_string(string[j:])

    return total

If the string is noisier than the OP indicates, then this should be more robust:

import re

def sum_string(string):
    pattern = re.compile(r"[-+]?\d+")

    total = 0

    match = pattern.search(string)

    while match:

        total += int(match.group())

        match = pattern.search(string, match.end())

    return total

EXAMPLES

>>> sum_string('34 3 542 11')
590
>>> sum_string('   34    4   ')
38
>>> sum_string('lksdjfa34adslkfja4adklfja')
38
>>> # and I threw in signs for fun
... 
>>> sum_string('34 -2 45 -8 13')
82
>>> 
cdlane
  • 40,441
  • 5
  • 32
  • 81
  • 2
    Wouldn't list slicing be considered a list method? – Pierce Darragh Oct 09 '16 at 17:27
  • 2
    @PierceDarragh, a slice is an operation on a sequence, including strings. The string slice returns a substring, not a list. – cdlane Oct 09 '16 at 17:31
  • That's fair! I guess I chose to avoid anything list-like at all in my solution, since I wasn't sure how far OP's prof wanted them to take the "no lists and no list methods" thing. I like that your solution took a different approach to the problem though! – Pierce Darragh Oct 09 '16 at 17:33
  • This code will fail if the input has leading whitespace, or if there are runs of multiple whitespace characters. – ekhumoro Oct 09 '16 at 17:41
  • @ekhumoro, I've added an alternative solution that handles lots of such things the OP didn't mention. – cdlane Oct 09 '16 at 18:02
  • @PierceDarragh, avoiding things list-like could invalidate posted answers. `range()` returns a list in Python 2, but an iterator in Python 3 -- is its result list-like? `re.find` returns a list so it's not allowed but `re.finditer` returns an iterator so it is? There was [discussion whether Python 3 `split()` should return an iterator.](http://bugs.python.org/msg183782) Would it now be non list-like if did? Does `sums()` operate on list-like things. Perhaps the instructor should have specified ***nothing iterable*** beyond the input string. – cdlane Oct 09 '16 at 18:44
1

If you want to be able to handle floats and negative numbers:

def sum_numbers(s):
    sm = i = 0
    while i < len(s):
        t = ""
        while  i < len(s) and not s[i].isspace():
            t += s[i]
            i += 1
        if t:
            sm += float(t)
        else:
            i += 1
    return sm

Which will work for all cases:

In [9]: sum_numbers('34 3 542 11')
Out[9]: 590.0

In [10]: sum_numbers('1.93 -1 23.12 11')
Out[10]: 35.05

In [11]: sum_numbers('')
Out[11]: 0

In [12]: sum_numbers('123456')
Out[12]: 123456.0

Or a variation taking slices:

def sum_numbers(s):
    prev = sm = i = 0
    while i < len(s):
        while i < len(s) and not s[i].isspace():
            i += 1
        if i > prev:
            sm += float(s[prev:i])
            prev = i
        i += 1
    return sm

You could also use itertools.groupby which uses no lists, using a set of allowed chars to group by:

from itertools import groupby


def sum_numbers(s):
    allowed = set("0123456789-.")
    return sum(float("".join(v)) for k,v in groupby(s, key=allowed.__contains__) if k)

which gives you the same output:

In [14]: sum_numbers('34 3 542 11')
Out[14]: 590.0

In [15]: sum_numbers('1.93 -1 23.12 11')
Out[15]: 35.05

In [16]: sum_numbers('')
Out[16]: 0

In [17]: sum_numbers('123456')
Out[17]: 123456.0

Which if you only have to consider positive ints could just use str.isdigit as the key:

def sum_numbers(s):
    return sum(int("".join(v)) for k,v in groupby(s, key=str.isdigit) if k)
Padraic Cunningham
  • 176,452
  • 29
  • 245
  • 321
0

Try this:

def sum_numbers(s):
    sum = 0
    #This string will represent each number
    number_str = ''
    for i in s:
        if i == ' ':
            #if it is a whitespace it means
            #that we have a number so we incease the sum
            sum += int(number_str)
            number_str = ''
            continue
        number_str += i
    else:
        #add the last number
        sum += int(number_str)
    return sum
theVoid
  • 743
  • 4
  • 14
  • This is similar to my solution, but yours doesn't handle non-space non-digit characters at all. – Pierce Darragh Oct 09 '16 at 17:20
  • @PierceDarragh It assumes that the input is correct, but i used for/else just to add the last number inside the function and present an other solution. – theVoid Oct 09 '16 at 17:22
  • the for/else is good, but I think a better solution would (1) check for multiple types of whitespace and (2) potentially handle errors in the input. – Pierce Darragh Oct 09 '16 at 17:26
  • @PierceDarragh I just wanted to show the op that this mechanism exists.I never said it is optimal, but it is beautiful.Also it is not bad for the op to learn something new and useful. – theVoid Oct 09 '16 at 17:27
0

You could write a generator:

def nums(s):
    idx=0
    while idx<len(s):
        ns=''
        while idx<len(s) and s[idx].isdigit():
            ns+=s[idx]
            idx+=1
        yield int(ns)
        while idx<len(s) and not s[idx].isdigit():
            idx+=1

>>> list(nums('34 3 542 11'))
[34, 3, 542, 11]

Then just sum that:

>>> sum(nums('34 3 542 11')) 
590

or, you could use re.finditer with a regular expression and a generator construction:

>>> sum(int(m.group(1)) for m in re.finditer(r'(\d+)', '34 3 542 11'))
590

No lists used...

dawg
  • 98,345
  • 23
  • 131
  • 206
0
def sum_numbers(s):
    total=0
    gt=0 #grand total
    l=len(s)
    for i in range(l):
        if(s[i]!=' '):#find each number
            total = int(s[i])+total*10
        if(s[i]==' ' or i==l-1):#adding to the grand total and also add the last number
            gt+=total
            total=0
    return gt

print(sum_numbers('1 2 3'))

Here each substring is converted to number and added to grant total

Smart Manoj
  • 5,230
  • 4
  • 34
  • 59
  • Please provide some commentry to explain how this code works and how it answers the question. – James K Oct 09 '16 at 18:55
  • While this answer is probably correct and useful, it is preferred if you [include some explanation along with it](http://meta.stackexchange.com/q/114762/159034) to explain how it helps to solve the problem. This becomes especially useful in the future, if there is a change (possibly unrelated) that causes it to stop working and readers need to understand how it once worked. – Kevin Brown-Silva Oct 09 '16 at 18:58
0

If we omit the fact eval is evil, we can solve that problem with it.

def sum_numbers(s):
    s = s.replace(' ', '+')
    return eval(s)

Yes, that simple. But i won't put that thing in production.

And sure we need to test that:

from hypothesis import given
import hypothesis.strategies as st


@given(list_num=st.lists(st.integers(), min_size=1))
def test_that_thing(list_num):
    assert sum_numbers(' '.join(str(i) for i in list_num)) == sum(list_num)

test_that_thing()

And it would raise nothing.

Community
  • 1
  • 1
vishes_shell
  • 22,409
  • 6
  • 71
  • 81