Chapter 12: Construction (Solutions to Even-Numbered Exercises)

Question 2

Because -1 is a valid index in Python. Where possible, defaults should be chosen to avoid any possibility of confusion or mis-interpretation.

Question 4

Six informative tests are:

Line 1 Line 2 Expected Explanation
"hello" "world" exception Make sure the function is sanity testing its inputs.
[[0, 0], [0, 1]] [[1, 0], [1, 1]] None Lines do not overlap.
[[0, 0], [1, 1]] [[0, 1], [1, 0]] [0.5, 0.5] Lines cross at one point.
[[0, 0], [1, 1]] [[1, 1], [2, 2]] [1, 1] Lines cross at an endpoint.
[[0, 0], [1, 0]] [[0, 0], [1, 0]] [[0, 0], [1, 0]] Lines lie exactly on top of each other.
[[0, 0], [2, 0]] [[1, 0], [3, 0]] [[1, 0], [2, 0]] Lines partially overlap.

Question 6

I would want to test both single-letter and double-letter symbols for all 26 languages, which would be 52 tests. I would also want to test illegal symbols (non-string, empty string, more than two characters, and one or two letters that aren't a symbol) for at least one language (at least four more tests), and at least one valid symbol with the same kinds of illega country codes (another four tests), bringing the total to 60.

Question 8

The first problem is that these tests are now tied to an implementation detail of the function is_salt. If whoever wrote that function decides to change their implementation, the tests could all break or (even worse) succeed for the wrong reasons.

The second problem is that if the sets Acids and Bases are not being initialized correctly, then re-setting them for testing purposes will result in tests passing when the function would fail in real-world use.

Question 10

def test_non_list():
    try:
        is_sorted("string")
        assert False
    except ValueError:
        pass
    except:
        assert False

def test_non_integer():
    try:
        is_sorted([1, []])
        assert False
    except ValueError:
        pass
    except:
        assert False

def test_empty():
    assert is_sorted([])

def test_single():
    assert is_sorted([33])

def test_pair_sorted():
    assert is_sorted([22, 44])

def test_pair_reversed():
    assert not is_sorted([44, 22])

def test_equal_values():
    assert is_sorted([5, 5, 5, 5, 5])

def test_middle_mislocated():
    assert not is_sorted([1, 2, 5, 3, 4])

def test_last_mislocated():
    assert not is_sorted([1, 2, 3, 4, 0])

def test_long_ordered():
    assert is_sorted([-100, -50, 2, 99, 173, 450, 172389])

Question 12

The StringIO and cStringIO modules allow programs to read from strings as if they were files. Programs can also write to these objects as if they were files; if asked, the objects will return the concatenation of all the data that has been written to them as a string.

import cStringIO
from file_util import count_lines

def test_five_lines():
    data = '''one
two
three
four
five
'''
    mock_file = cStringIO.StringIO(data)
    assert count_lines(mock_files) == 5

Question 14

a) If limit is negative, the function will count down until current becomes the most negative value that Python can represent, at which point the program will fail.

b) The function "hangs", i.e., appears to be doing nothing.

c) One solution would be to break the loop into two pieces, i.e., to say:

if limit > 0:
    ...count down to zero as the function presently does...
else:
    ...count up to zero instead...

Another possibility is to make the increment a variable:

if limit < 0:
    increment = 1
else:
    increment = -1

while current != 0:
    total += current
    current += increment

Question 16

find_last:

VariableRoleBetter name
v0steppercurrent_index
v1most-wanted holderlast_so_far
v2fixed valuethe_file
v3stepperline

standard_deviation:

v0accumulatorsum
v1steppervalue
v2temporaryaverage
v3accumulatorstd_sum
v4steppervalue
v5temporarystd_avg

Question 18

def process_file(filename):
    '''Read and print the contents of a file.'''
    
    try:
        f = open(filename, 'r')
        for line in f:
            line = line.strip()
            print line
    except:
        pass

    f.close()

Question 20

a) What happens if one or both of prefix and pattern do not occur in source? What if the only occurrence of pattern occurs immediately after prefix?

b)

'''Return the index of the first occurrence of 'pattern' in 'source' that is
    NOT immediately after an occurrence of 'prefix'. If 'pattern' not in
    'source', or only occurs immediately after an occurrence of 'prefix', raise
    a ValueError.  If 'prefix' is the empty string, also raise a ValueError.

    For example, findButNotAfter('abcdcd', 'ab', 'cd') returns 4, since the
    first occurrence of 'cd' comes immediately after an 'ab', but
    findButNotAfter('abxcdcd', 'ab', 'cd') returns 3.'''

c)

import nose
import find_but_not_after

def test_empty_error():
    try:
        res = find_but_not_after.findButNotAfter('', '', '')
        assert False, 'Empty prefix should cause an exception.'
    except ValueError:
        pass

def test_one_empty_error():
    try:
        res = find_but_not_after.findButNotAfter('a', '', 'a')
        assert False, 'Empty prefix should cause an exception.'
    except ValueError:
        pass

def test_none_error():
    try:
        res = find_but_not_after.findButNotAfter('ab', 'a', 'b')
        assert False, 'Should raise error: no occurrence without a prefix.'
    except ValueError:
        pass

def test_after_error():
    try:
        res = find_but_not_after.findButNotAfter('abcd', 'ab', 'cd')
        assert False, 'Should raise error: no occurrence without a prefix.'
    except ValueError:
        pass

def test_one_same_okay():
    res = find_but_not_after.findButNotAfter('a', 'a', 'a')
    assert res == 0, \
        'Prefix and pattern are the same, but this should be okay.'

def test_same_letter_okay():
    res = find_but_not_after.findButNotAfter('aa', 'a', 'a')
    assert res == 0, \
        'Prefix and pattern are the same, but this should be okay.'

def test_prefix_after_no_error():
    res = find_but_not_after.findButNotAfter('ab', 'b', 'a')
    assert res == 0, \
        'Prefix occurs after pattern, and this should be okay.'

def test_prefix_but_no_error():
    res = find_but_not_after.findButNotAfter('abxcd', 'ab', 'cd')
    assert res == 3, \
        'Prefix and pattern separated by another character.'
    
def test_overlap():
    res = find_but_not_after.findButNotAfter('abcd', 'ab', 'bc')
    assert res == 1, \
        'Prefix and pattern overlap, but that should be okay.'

def test_second_occurrence():
    res = find_but_not_after.findButNotAfter('xabab', 'x', 'ab')
    assert res == 3, \
        'Prefix and pattern appear together, but just the pattern afterwards.'

def test_twice_no_prefix():
    res = find_but_not_after.findButNotAfter('abab', 'x', 'ab')
    assert res == 0, \
        'Should have returned the first occurrence.'

d)

def findButNotAfter(source, prefix, pattern):
    '''Return the index of the first occurrence of 'pattern' in 'source' that is
    NOT immediately after an occurrence of 'prefix'. If 'pattern' not in
    'source', or only occurs immediately after an occurrence of 'prefix', raise
    a ValueError.
    
    For example, findButNotAfter('abcdcd', 'ab', 'cd') returns 4, since the
    first occurrence of 'cd' comes immediately after an 'ab', but
    findButNotAfter('abxcdcd', 'ab', 'cd') returns 3.'''

    start = 0
    result = -1 # Will be -1 until valid solution found.

    loc = source.find(pattern, start)
    while loc != -1 and result == -1:
        # Check whether prefix occurs immediately before.
        if source[loc - len(prefix) : loc] != prefix:
            result = loc
        else:
            start = loc + 1
            loc = source.find(pattern, start)

    if result == -1:
        raise ValueError, 'Pattern not found in source'

    return result