
# for loop for iterating a list
print("iterate a list")
for i in [1, 2, 3, 4]:
    print(i)

# for loop for iterating a string
print("iterate a string")
for c in "iteratable":
    print(c)

# for loop for iterating a dictionary
print("iterate a dictionary")
for k in {"a": 1, "b": 2}:
    print(k)

# for loop for iterating a file line by line
print("iterate a file")
for line in open("input.txt"):
    print(line)

# list, string, dict, file stream are called iterable objects in python.
# built-in functions use these iterables
s = ",".join(["a", "b", "c"])
print(s)

d = ",".join({"x": 1, "y": 2})
print(d)

print(list("iterable"))
print(list({"x": 1, "y": 2}))

x = iter([1, 2, 3])
print(x.next())
print(x.next())
print(x.next())
# print(x.next())  # raises StopIteration

# good interview question
# implement your own xrange function
class yrange:
    def __init__(self, n):
        self.n = n
        self.i = 0

    def __iter__(self):
        # __iter__ method makes an object iterable
        # iter function calls __iter__ method behind the scenes
        # the return value of __iter__ is an iterator
        # it should implement next() method and raise StopIteration
        # when there are no more elements.
        return self

    def next(self):
        if self.i < self.n:
            i = self.i
            self.i += 1
            return i
        else:
            raise StopIteration()

# lets try yrange
y = yrange(3)
print(y.next())
print(y.next())
print(y.next())
# print(y.next())  # raises StopIteration

print(list(yrange(5)))
print(sum(yrange(5)))

# good interview question
# implement reverse iterator
class rev_iter:
    def __init__(self, iterable):
        self.i = len(iterable) - 1
        self.iterable = iterable
    
    def __iter__(self):
        return self

    def next(self):
        if self.i >= 0:
            i = self.iterable[self.i]
            self.i -= 1
            return i
        else:
            raise StopIteration()

r = rev_iter([1, 2, 3, 4])
print(r.next())
print(r.next())
print(r.next())
print(r.next())
# print(r.next())  # StopIteration

# THE ITERATION PROTOCOL
# in python, the iterator objects are required to implement
# these two methods:
# __iter__ returns the iterator object itself.
# __next__ returns next value from the iterator. if there is no
# more items, it should raise StopIteration exception.

# lets implement a Counter iteratable class
class Counter(object):
    def __init__(self, low, high):
        self.current = low
        self.high = high

    def __iter__(self):
        return self

    def next(self):
        if self.current <= self.high:
            i = self.current
            self.current += 1
            return i
        else:
            raise StopIteration()

c = Counter(5, 10)
for i in c:
    print(i)
