# string_recursion.py
# Walker M. White (wmw2), Lillian Lee (LJL2)
# Mar 5, 2014
"""A module of recursive functions on strings."""

# The functions in this module use assert statements, in this case, to
# check that the types of the argument values are expected.  Assert statements
# can be used in any kind of code, not just recursive functions.
# They halt execution of Python, printing out the message you supply, if the
# boolean expression is false.
#
# In this code, assert statements are being used to verify that the types of
# the argument values are correct, as a way of explicitly enforcing preconditions.
# Their error messages involve calls to the function "repr", which returns a
# string that represents *unambiguously* the object given as argument. As such,
# its output can differ from that of str().  The book is a little light on this
# topic; a dense but perhaps useful post is this:
# http://stackoverflow.com/questions/1436703/difference-between-str-and-repr-in-python




import string # constants to help with string processing

def length(s):
    """Returns: number of characters in s

    Precondition s is a string"""
    assert type(s) == str, repr(s) + ' is not a string' # get in the habit

    # {s is empty} - base case
    if s == '':
        return 0

    # {s has at least one character} - recursive case
    # return 1 + length of the rest of s
    return 1 + length(s[1:])


def length2(s):
    """Returns: number of characters in s

    Precondition s is a string"""
    assert type(s) == str, repr(s) + ' is not a string' # get in the habit

    # This implementation uses if-else, rather than using the fact that "return"
    # instantly exits the function call.

    if s == '':
        return 0
    else:
        # {s has at least one character} - recursive case
        # return 1 + length of the rest of s
        return 1 + length2(s[1:])

def num_e(s):
    """Returns: number of 'e's in s

    Precondition s is a string"""
    assert type(s) == str, repr(s) + ' is not a string' # get in the habit

    # {s is empty} - base case
    if s == '':
        return 0
    else:
        # { s has at least one character}  - recursive case
        # return (number of 'e's in s[0]) + number of 'e's in the rest of s
        #
        # this implementation uses a trick: if len(s) ==1, s[1:] == ''
        return (1 if s[0] == 'e' else 0) + num_e(s[1:])

def num_e_2(s):
    """Returns: number of 'e's in s

    Precondition s is a string"""
    # More straightforward implementation in terms of cases;
    # but more verbose
    if s =='':
        return 0
    elif len(s) == 1:
        if s == 'e':
            return 1
        else:
            return 0
    else:
        return (1 if s[0] == 'e' else 0) + num_e_2(s[1:])

def num_e_3(s):
    """Returns: number of 'e's in s

    Precondition s is a string"""
    # Non-"else-ist" version
    if s =='':
        return 0

    # this implementation uses a trick: if len(s)==1, s[1:] == ''
    return (1 if s[0] == 'e' else 0) + num_e_3(s[1:])


def deblank(s):
    """Returns: copy of s but with blanks removed

    Precondition s is a string"""
    assert type(s) == str, repr(s) + ' is not a string' # get in the habit

    # {s is empty} - base case
    if s == '':
        return s
    else:
        # {s has at least one character} - recursive cases
        if s[0] in string.whitespace:
            # {There's a blank to be deleted in the first character}
            return deblank(s[1:])
        else:
            return s[0] + deblank(s[1:])


def depunct(s):
    """Returns:  copy of s with everything that is not a letter removed

    Precondition s is a string"""
    assert type(s) == str, repr(s) + ' is not a string' # get in the habit

    # {s is empty}
    if s == '':
        return s
    else:
        # {s has at least one character} - recursive cases
        if not s[0] in string.letters:
            return depunct(s[1:])
        else:
            return s[0] + depunct(s[1:])

def reverse(s):
    """Returns: copy of s with its characters in reverse order

    Precondition s is a string"""
    assert type(s) == str, repr(s) + ' is not a string' # get in the habit

    if s == '': # base case
        return s
    else:
        # { s has at least 2 chars }
        return reverse(s[1:]) + s[0]

def equals_ignore_case(c, d):
    """Returns: true if strings c and d differ only in case, if at all

    Precondition c, d are strings"""
    # Not implemented recursively
    assert type(c) == str, repr(c) + ' is not a string' # get in the habit
    assert type(d) == str, repr(d) + ' is not a string' # get in the habit

    return c.upper() == d.upper()


def ispalindrome(s):
    """Returns: true if s is a palindrome

    There are two ways to define a palindrome:
      1. s is a palindrome if it reads the same backward and forward.
      2. s is a palindrome if either
         (1) its length is <= 1   OR
         (2) its first and last chars are the same and the string
             between them is a palindrome.
      Letters that differ only in case are considered to match.

    Precondition s is a string"""
    assert type(s) == str, repr(s) + ' is not a string' # get in the habit
    # This implementation explicitly checks front end against the back end
    # of the string.

    # This kind of print statement can help a lot in debugging,
    # but we've commented it out here.
    # print 's is: ' + repr(s)

    if len(s) < 2:
        return True
    else:
        # { s has at least 2 characters }
        # return (first and last chars match) and (inner substring is pal.)
        return (equals_ignore_case(s[0], s[-1]) and
                ispalindrome(s[1:len(s)-1]))


def ispalindrome2(s):
    """Returns: true if s is a palindrome

    There are two ways to define a palindrome:
      1. s is a palindrome if it reads the same backward and forward.
      2. s is a palindrome if either
         (1) its length is <= 1   OR
         (2) its first and last chars are the same and the string
             between them is a palindrome.
      Letters that differ only in case are considered to match.

    Precondition s is a string"""
    # Non-explicitly-recursive implementation of same spec as ispalindrome

    return equals_ignore_case(s,reverse(s))


def ispalindrome_loosely(s):
    """Returns: true if s is a palindrome paying attention only to the letters

    Case and any non-letter characters are ignored.

    Precondition s is a string"""
    # Not a recursive function
    assert type(s) == str, repr(s) + ' is not a string' # get in the habit
    return ispalindrome(depunct(s))