from __future__ import print_function

def foo(n):
    return sum(filter(lambda x: x % 3 == 0 or x % 5 == 0, range(n)))

n=10
foo(n)

n=1000
%timeit foo(n)
foo(n)

def foo(n):
    total = 0
    for i in range(n):
        if i % 3 == 0 or i % 5 == 0:
            total += i
    return total

n=1000
%timeit foo(n)
foo(n)

"""
Explanation: If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3, 5, 6 and 9.
The sum of these multiples is 23.
Find the sum of all the multiples of 3 or 5 below 1000.
End of explanation
"""


def foo(n):
    a = []
    for i in range(n):
        if i % 3 == 0 or i % 5 == 0:
            a.append(i)
    return sum(a)

n=1000
%timeit foo(n)
foo(n)

def foo(n):
    return sum([i for i in range(n) if i % 3 == 0 or i % 5 == 0])

n=1000
%timeit foo(n)
foo(n)

def foo(n):
    return sum((i for i in range(n) if i % 3 == 0 or i % 5 == 0))

n=1000
%timeit foo(n)
foo(n)

def foo(n):
    return sum((j for j in (i for i in range(n) if i % 3 == 0) if j % 5 == 0))

n=1000
%timeit foo(n)
foo(n)

"""
Explanation: It is surprising that the naive code immediately above is faster than the functional programming version.
End of explanation
"""


def foo(n):
    return sum([j for j in [i for i in range(n) if i % 3 == 0] if j % 5 == 0])

n=1000
%timeit foo(n)
foo(n)

"""
Explanation: Wow, the nested generator expressions above were fastest yet, and also ugliest.
The unnested list comprehension was faster than the unnested generator expression,
so let's try nested list comprehensions.
End of explanation
"""


def threes_or_fives(gen):
    for i in gen:
        if i % 3 == 0:
            yield i
        elif i % 5 == 0:
            yield i

def foo(n):
    return sum(threes_or_fives(range(n)))

n=1000
%timeit foo(n)
foo(n)

def threes_or_fives(gen):
    for i in gen:
        if i % 3 == 0 or i % 5 == 0:
            yield i

def foo(n):
    return sum(threes_or_fives(range(n)))

n=1000
%timeit foo(n)
foo(n)

"""
Explanation: I expected the nested list comprehensions to be a little bit faster than the nested generator expressions, 
so I was surprised by the big speed increase.
Let's play with generators more.
End of explanation
"""


def foo(n):
    return sum(set(range(0, n, 3)) | set(range(0, n, 5)))

n=1000
%timeit foo(n)
foo(n)

"""
Explanation: It is surprising that the naive verbose generator function with two if statements
is faster than the generator function with the combined if statement.
I thought of one more way later, using sets, that should be much more elegant.
End of explanation
"""


def foo(n, divisors):
    all_multiples = set([])
    for multiples in (set(range(0, n, divisor)) for divisor in divisors):
        all_multiples |= multiples
    return sum(multiples)

n=1000
divisors = (3, 5)
%timeit foo(n, divisors)
foo(n, divisors)

"""
Explanation: Holy smokes! It is easier to read, but I did not expect it to be so fast.
I can not resist the temptation to generalize.
End of explanation
"""


