# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---

# 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.

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)


# It is surprising that the naive code immediately above is faster than the functional programming version.

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)


# 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.

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)


# 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.

# +
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)


# 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.

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


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


# 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.

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)

# I am surprised that the generalized version is faster yet. It sure is ugly.

# Of course, this does not compare to what I did last year in
# http://nbviewer.ipython.org/url/colug.net/python/all-ipython-notebooks/euler-001-multiples-of-3-and-5-20150220.ipynb.

StarcoderdataJupyterScriptsDedupFiltered