"""
This module demonstrates various patterns
for ITERATING through SEQUENCES, including:
  -- Beginning to end
  -- Other ranges (e.g., backwards and every-3rd-item)
  -- The COUNT/SUM/etc pattern
  -- The FIND pattern (via LINEAR SEARCH)
  -- The MAX/MIN pattern
  -- Looking two places in the sequence at once
  -- Looking at two sequences in parallel

Of course, these are not the only patterns, and some problems require
combining these patterns, but this is a good base upon which to build.

Authors: David Mutchler, Vibha Alangar, Matt Boutell, Dave Fisher,
         Mark Hays, Amanda Stouder, Derek Whitley, their colleagues,
         and PUT_YOUR_NAME_HERE.
"""  # TODO: 1. PUT YOUR NAME IN THE ABOVE LINE.


# -----------------------------------------------------------------------------
# TODO: 2. SKIM the program below and RUN it.
#   ___
#   Then look more closely at the CODE for:
#     -- find_example1
#     -- find_example2
#     -- find_example3
#     -- min_index
#     -- min_item
#     -- two_places_at_once
#     -- two_sequences_in_parallel
#   ___
#   When you have read them, asking questions as needed,
#   and you feel that you understand (at least mostly)
#   the patterns for:
#     -- FIND
#     -- MIN/MAX
#     -- TWO-PLACES-AT-ONCE, and
#     -- TWO-SEQUENCES-IN-PARALLEL
#   then:
#      change the above _TODO_ to DONE.
# -----------------------------------------------------------------------------

import math


def main():
    """
    Demonstrates some patterns for ITERATING through SEQUENCES.
    These are the CALLS to code that uses the patterns.
    See further below for the CODE ITSELF that uses the pattersn.
    """
    # -------------------------------------------------------------------------
    # Demonstrate the    BEGINNING-TO-END    and   COUNTING   patterns.
    # -------------------------------------------------------------------------
    print()
    print('-----------------------------------------------------------')
    print('Demonstrating the   BEGINNING-TO-END   pattern,')
    print('   along with the   COUNTING   pattern:')
    print('-----------------------------------------------------------')

    sequence = [88, 232, 8.5, -11, 'Grandmother', 22, 4.3, 18 / 2, 7 // 2]
    answer = beginning_to_end(sequence)
    print('The test sequence is:')
    print('  ', sequence)
    print('The number of integers in that sequence is', answer)

    # -------------------------------------------------------------------------
    # Demonstrate the    OTHER-RANGES    pattern.
    # -------------------------------------------------------------------------
    print()
    print('-----------------------------------------------------------')
    print('Demonstrating the   OTHER-RANGES   pattern')
    print('   (backwards, every 3rd item)   in each of THREE FORMS:')
    print('-----------------------------------------------------------')

    sequence = [88, 232, 8.5, -11, 'Grandmother', 22, 4.3, 9.0, 3]
    other_ranges(sequence)

    # -------------------------------------------------------------------------
    # Demonstrate the    FIND    pattern.
    # -------------------------------------------------------------------------
    print()
    print('-----------------------------------------------------------')
    print('Demonstrating the   FIND   pattern, in several variations:')
    print('-----------------------------------------------------------')

    print()
    print('Example: FINDING the first STRING in the sequence.')
    print()
    print('  Case 1:  The item  ** IS **  in the sequence:')
    find([88, 232, 8.5, -11, 'Grandmother', 22, 'mother', 9.0, 3])

    print()
    print('  Case 2:  The item  ** IS NOT **  in the sequence:')
    find([1, 2, 5])

    # -------------------------------------------------------------------------
    # Demonstrate the    MAX/MIN    pattern.
    # -------------------------------------------------------------------------
    print()
    print('-----------------------------------------------------------')
    print('Demonstrating the  MAX/MIN  pattern, in several variations:')
    print('-----------------------------------------------------------')

    sequence = [4, 66, 33, 90, 93, 2, 3, 3, 2, 15]
    max_min(sequence)

    # -------------------------------------------------------------------------
    # Demonstrate the    TWO-PLACES-IN-THE-SEQUENCE-AT-ONCE    pattern.
    # -------------------------------------------------------------------------
    print()
    print('-----------------------------------------------------------')
    print('Demonstrating  TWO-PLACES-IN-THE-SEQUENCE-AT-ONCE  pattern:')
    print('-----------------------------------------------------------')

    sequence = [4, 66, 33, 90, 93, 3, 3, 3, 2, 15]
    print('The sequence is:', sequence)
    answer = two_places_at_once(sequence)
    print('The sequence increments at', answer, 'places.')

    # -------------------------------------------------------------------------
    # Demonstrate the    TWO-SEQUENCES-IN-PARALLEL    pattern.
    # -------------------------------------------------------------------------
    print()
    print('-----------------------------------------------------------')
    print('Demonstrating the   TWO-SEQUENCES-IN-PARALLEL   pattern:')
    print('-----------------------------------------------------------')

    seq1 = [11, 22, 10, 44, 33, 12]
    seq2 = [55, 10, 30, 30, 30, 30]
    print('Sequence 1 is:', seq1)
    print('Sequence 2 is:', seq2)
    answer = two_sequences_in_parallel(seq1, seq2)
    print('The 2nd sequence exceeds the 1st at', answer, 'places.')


# -----------------------------------------------------------------------------
# The   BEGINNING-TO-END   pattern:
# -----------------------------------------------------------------------------
def beginning_to_end(sequence):
    """
    Demonstrates iterating (looping) through a sequence in the most
    typical way:  from BEGINNING TO END.

    This particular example returns the number of items in the sequence
    that are integers. It also prints the index during the looping.
    Type hints:
      :type sequence: list | tuple
    """
    # -------------------------------------------------------------------------
    # The   BEGINNING-TO-END   pattern is:
    #
    #    for k in range(len(sequence)):
    #        ... sequence[k] ...
    #
    # -------------------------------------------------------------------------
    count = 0
    for k in range(len(sequence)):
        if type(sequence[k]) == int:
            count = count + 1
        print(k)

    return count


# -----------------------------------------------------------------------------
# The   OTHER-RANGES   pattern:
# -----------------------------------------------------------------------------
def other_ranges(sequence):
    """
    Demonstrates iterating (looping) through a sequence in a pattern
    OTHER than from beginning to end.

    This particular example prints every 3rd item of the sequence,
    but starting at the END of the list (and going backwards).
    Type hints:
      :type sequence: list | tuple
    """
    # -------------------------------------------------------------------------
    # The   OTHER-RANGES   pattern can be thought of as having
    #   any of three equivalent forms.
    # Choose the form that makes the most sense to you.
    #
    # FORM 1:
    #    for k in range( NUMBER ):
    #        ... sequence[ BLAH_EXPRESSION ] ...
    #
    # where NUMBER is the number of items in the sequence to examine
    # and   BLAH_EXPRESSION   is some formula involving k that is carefully
    #                         crafted to produce exactly the right indices.
    #
    # FORM 2:
    #    for k in range( BLAH_RANGE ):
    #        ... sequence[k] ...
    #
    # where BLAH_RANGE is some range OTHER than one that generates
    #    0, 1, 2, 3, ...
    # and is carefully crafted to produce exactly the right indices.
    #
    # FORM 3:
    #    m = ...
    #    for k in range( NUMBER ):
    #        ... sequence[ m ] ...
    #        m = ...
    #
    # where NUMBER is the number of items in the sequence to examine
    # and     m    is an auxiliary variable that is carefully crafted and
    #                controlled to produce exactly the right indices.
    #
    # FORM 1: Puts all the heavy lifting into figuring out BLAH_EXPRESSION.
    # FORM 2: Puts all the heavy lifting into figure out BLAH_RANGE.
    # FORM 3: Like FORM 1,
    #    but uses an auxiliary variable to simplify figuring out the
    #    index of the sequence at each iteration of the loop.
    # -------------------------------------------------------------------------
    print('Printing backwards, every 3rd item, ONE WAY:')

    # -------------------------------------------------------------------------
    # Solution using FORM 1:
    # -------------------------------------------------------------------------
    last = len(sequence) - 1
    for k in range(len(sequence) // 3):
        print(sequence[last - (k * 3)])

    print('\nANOTHER way:')

    # -------------------------------------------------------------------------
    # Solution using FORM 2:
    # -------------------------------------------------------------------------
    last = len(sequence) - 1
    for k in range(last, -1, -3):
        print(sequence[k])

    print('\nYET ANOTHER way:')

    # -------------------------------------------------------------------------
    # Solution using FORM 3:
    # -------------------------------------------------------------------------
    last = len(sequence) - 1
    m = last
    for k in range(len(sequence) // 3):
        print(sequence[m])
        m = m - 3


# -----------------------------------------------------------------------------
# The   FIND   pattern, in its several variations:
# -----------------------------------------------------------------------------
def find(sequence):
    """
    Demonstrates FINDING an item in a sequence.  Its forms include:
      1. Finding WHETHER a given item is in the given sequence.
      2. Finding whether the sequence contains an item
         with a given property, and returning either of:
           2a. the INDEX of the first such item found
               (or -1 if none was found)
           2b. the ITEM that was found (or None if none was found)

    This particular example finds the first item in the sequence
    that is a string.  The three sub-examples return:
      -- True or False (per item 1 above)
      -- the INDEX of the found item (per 2a above)
      -- the ITEM that was found (per 2b above)
    Type hints:
      :type sequence: list | tuple
    """
    # -------------------------------------------------------------------------
    # The   FIND   pattern is:
    #
    #     for k in range(len(sequence)):
    #         if ... seq[k] ...:
    #             return k
    #
    #     return -1
    #
    # *** NOTE the placement of the TWO   return   statements! ***
    #
    # The above returns the INDEX where the item was found,
    #   or -1 if none was found.
    #
    # Other problems/variations might return  True  if the item
    # was found and  False  otherwise, or might return the item itself
    # that was found (and perhaps  None  if the item was not found).
    #
    # Also, some problems might require you to search through only part
    # of the sequence (not all of it) or to search backwards, or ...
    # -------------------------------------------------------------------------

    answer1 = find_example1(sequence)
    answer2 = find_example2(sequence)
    answer3 = find_example3(sequence)

    print('  Test sequence is:', sequence)
    print('    -- Found (True or False)?', answer1)
    print('    -- Index of found item:', answer2)
    print('    -- Item that was found:', answer3)


def find_example1(sequence):
    """
    Returns True if the given sequence contains a string,
    else returns False.
    Type hints:
      :type sequence: list | tuple
      :rtype: bool
    """
    # Returns True or False
    for k in range(len(sequence)):
        if type(sequence[k]) == str:
            return True

    return False


def find_example2(sequence):
    """
    Returns the INDEX of the first string in the given sequence,
    or -1 if the given sequence contains no strings.
    Type hints:
      :type sequence: list | tuple
      :rtype: int
    """
    # Returns the index (k) or -1
    for k in range(len(sequence)):
        if type(sequence[k]) == str:
            return k

    return -1


def find_example3(sequence):
    """
    Returns the FIRST STRING in the given sequence,
    or None if the sequence contains no strings.
    Type hints:
      :type sequence: list | tuple
      :rtype: str
    """
    # Returns the item ( sequence[k] )  or  None
    for k in range(len(sequence)):
        if type(sequence[k]) == str:
            return sequence[k]

    return None


# -----------------------------------------------------------------------------
# The   MAX/MIN   pattern, in several variations:
# -----------------------------------------------------------------------------
def max_min(sequence):
    """
    Demonstrates determining the item in a sequence whose BLAH
    is SMALLEST (or LARGEST), where BLAH varies from problem to problem.

    The sub-examples of this particular example find,
    for a sequence of numbers:
      -- The index of the smallest number in the sequence
      -- The number at that index
           (i.e., the smallest number in the sequence)
      -- The index of the number in the sequence
           whose cosine is smallest
    Type hints:
      :type sequence: list[int | float] | tuple[int | float]
    """
    # -------------------------------------------------------------------------
    # The   MIN   pattern is as follows (with  MAX  being similar):
    #
    #     index_of_min = 0
    #     for k in range(1, len(sequence)):
    #         if BLAH(sequence[k]) < BLAH(sequence[index_of_min]):
    #             index_of_min = k
    #
    #     return index_of_min
    #
    # The property  BLAH  varies from problem to problem.
    # NOTE the distinction between the SMALLEST item
    #   and the INDEX of that item.
    #
    # The above returns the INDEX where the item was found.
    # Some problems ask for the item itself.
    # -------------------------------------------------------------------------

    print('The sequence is:', sequence)
    answer1 = min_index(sequence)
    answer2 = min_item(sequence)
    answer3 = min_cosine(sequence)

    print()
    print('The index of the smallest item in the sequence is', answer1)

    print()
    print('The smallest item in the sequence is', answer2)

    print()
    print('The index of the item in the sequence')
    print('  whose cosine is smallest is', answer3)
    print('That item is', sequence[answer3])
    print('Its cosine is', math.cos(sequence[answer3]))


def min_index(sequence):
    """
    Returns the index of the smallest item in the given sequence.
    Breaks ties in favor of the smallest index among the ties.

    Type hints:
      :type sequence: list[int | float] | tuple[int | float]
      :rtype: int
    Precondition: the sequence is a non-empty sequence of numbers.
    """
    # -------------------------------------------------------------------------
    # Return the INDEX of the smallest item in the sequence.
    # -------------------------------------------------------------------------
    index_of_min = 0
    for k in range(1, len(sequence)):
        if sequence[k] < sequence[index_of_min]:
            index_of_min = k

    return index_of_min


def min_item(sequence):
    """
    Returns the smallest item in the given sequence.

    Type hints:
      :type sequence: list[int | float] | tuple[int | float]
      :rtype: int | float
    Precondition: the sequence is a non-empty sequence of numbers.
    """
    # -------------------------------------------------------------------------
    # Use the same code as above to find the INDEX of the smallest item.
    # But return the item itself, in this variation.
    # -------------------------------------------------------------------------
    index_of_min = min_index(sequence)
    return sequence[index_of_min]


def min_cosine(sequence):
    """
    Returns the index of the item in the sequence whose cosine
    is smallest.

    Type hints:
      :type sequence: list[int | float] | tuple[int | float]
      :rtype: int
    Precondition: the sequence is a non-empty sequence of numbers.
    """
    # -------------------------------------------------------------------------
    # Very similar to   min_index   above.  Just compare the COSINES
    #   of the numbers instead of the numbers themselves.
    # -------------------------------------------------------------------------
    index_of_min = 0
    for k in range(1, len(sequence)):
        if math.cos(sequence[k]) < math.cos(sequence[index_of_min]):
            index_of_min = k

    return index_of_min


# -----------------------------------------------------------------------------
# The   TWO-PLACES-AT-ONCE   pattern:
# -----------------------------------------------------------------------------
def two_places_at_once(sequence):
    """
    Demonstrates iterating (looping) through a sequence,
    but examining TWO places in the sequence on the SAME ITERATION.

    This particular example returns the number of items in the sequence
    that are bigger than the previous item in the sequence.
    For example, if the sequence is [4, 66, 33, 90, 93, 3, 3, 3, 2, 15],
    then the function returns   4
    since   66 > 4   and   90 > 33   and   93 > 90   and   15 > 2.

    Type hints:
      :type sequence: list[int | float] | tuple[int | float]
      :rtype: int
    """
    # -------------------------------------------------------------------------
    # The   TWO-PLACES-AT-ONCE   pattern is:
    #
    #    for k in range( BLAH ):
    #        ... sequence[ FOO_1 ] ... sequence[ FOO_2 ] ...
    #
    # where BLAH is some range appropriate to the problem,
    #       FOO_1 is some function of k that gives ONE
    #             of the "two places at once" to examine, and
    #       FOO_2 is another function of k that gives the OTHER
    #             of the "two places at once" to examine.
    # Typically, FOO_1 or FOO_2 (but not both) is simply k.
    # -------------------------------------------------------------------------
    count = 0
    for k in range(len(sequence) - 1):
        if sequence[k + 1] > sequence[k]:
            count = count + 1

    return count


# -----------------------------------------------------------------------------
# The   TWO-SEQUENCES-IN-PARALLEL   pattern:
# -----------------------------------------------------------------------------
def two_sequences_in_parallel(sequence1, sequence2):
    """
    Demonstrates iterating (looping) through TWO sequences in PARALLEL.

    This particular example assumes that the two sequences are of equal
    length and returns the number of items in sequence2 that are bigger
    than  their corresponding item in sequence1.  For example,
    if the sequences are:
        [11, 22, 10, 44, 33, 12]
        [55, 10, 30, 30, 30, 30]
    then this function returns 3, since 55 > 11 and 30 > 10 and 30 > 12.

    Type hints:
      :type sequence1: list[int | float] | tuple[int | float]
      :type sequence2: list[int | float] | tuple[int | float]
      :rtype: int
    Precondition:  the two sequences are of equal length.
    """
    # -------------------------------------------------------------------------
    # The TWO-SEQUENCES-IN-PARALLEL pattern is:
    #
    #    for k in range(len(sequence1)):
    #        ... sequence1[k] ... sequence2[k] ...
    #
    # The above assumes that the sequences are of equal length
    # (or that you just want to do the length of sequence1).
    # -------------------------------------------------------------------------
    count = 0
    for k in range(len(sequence1)):
        if sequence1[k] > sequence2[k]:
            count = count + 1

    return count


# -----------------------------------------------------------------------------
# Calls  main  to start the ball rolling.
# -----------------------------------------------------------------------------
main()