Skip to content
46 changes: 46 additions & 0 deletions parenthetic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#Parenthetics
Parenthetics includes an eponymous function which takes a string
argument and determines whether or not it has broken, open, or well-formed
parentheses.

##Illustrative Examples of functionality
These are all simplified examples. Any unicode characters can be
intersperced into any of the following.

###Case One: Broken Parentheses

All of the following arguments will return -1:
```
>>> parenthetical("))))(((")
>>> parenthetical(")")
>>> parenthetical("()()()()()))(()))()(((()")
```
###Case Two: Open Parentheses

All of the following will return 1:
```
>>> parenthetical("()(")
>>> parenthetical("()()()(")
>>> parenthetical("()()(((()()()")
```

###Case Three: Okay Parentheses
All of the following will return 0:
```
>>> parenthetical("()()()()")
>>> parenthetical("(())")
>>> parenthetical("((()())()()())")
```

##Helpful Resources
All of the following were helpful in constructing this code:
* [Filtering using a set constructor]
(http://stackoverflow.com/questions/3013449/list-filtering-list-comprehension-vs-lambda-filter)
* [Why isn't there a sign function in Python?]
(http://stackoverflow.com/questions/1986152/why-python-doesnt-have-a-sign-function)
* [Goose-typing is intended in Python]
(https://docs.python.org/2/glossary.html#term-abstract-base-class)

The "goose typing" coin was termed in an amusing little article by
Alex Martelli included as part of Luciano Rahmalho's Fluent Python.
I'm tempted to do a lightining talk on it.
56 changes: 56 additions & 0 deletions parenthetic/parenthetics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from __future__ import unicode_literals
from random import choice as choice
from abc import types
import math

def generate_parenthetical_iterable(string):
"""
Take a string and return an ordered iterable with only the "(" and ")"
characters remaining
"""
# Using an abstract base class to "goose-type" check;
# this is an intentional part of the Python language. See README.md
if not isinstance(string, types.StringTypes):
raise TypeError

set_to_find = ["(", ")"] #Defining a filter
characters = tuple(string) #Turning characters into an iterable

for character in characters:
if character in set_to_find:
yield character



def parenthetical(string):
"""
Examine a string for closed, open, and well-formed parentheses;
return a -1, 1, and 0 respectively.

It might be helpful to recall that parenthesis is of greek etymology;
parenthesis is singular, parentheses plural.
"""

parentheses = generate_parenthetical_iterable(string)

# Score will help us keep track of parentheses state as we iterate;
# also will allow us to short-circuit out of for loop for open parenthesis
score = 0

for parenthesis in parentheses:
if parenthesis == ")":
score -= 1
if score < 0:
# An open parenthesis exists. No need to check further.
break
else:
# Parenthesis is "(" here
score += 1

if score in set([1, 0, -1]):
# Score can be directly returned in some cases
return score

else:
# Else use copysign to transfer sign of score to 1
return math.copysign(1, score)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wow. This is the most complex implementation of a solution to this problem I've seen yet. You've used a lot of tools. And used them correctly. However, it could be a lot simpler with the use of a data structure you learned this week.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed the comment you made about using Stacks in lecture... oh well, it was actually kind of fun doing it this way.

74 changes: 74 additions & 0 deletions parenthetic/test_parenthetics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from __future__ import unicode_literals
import pytest
from parenthetics import parenthetical


# Case that should return -1
broken_case = [
"))sdf)as((43215("
")tqw()3",
"345a)))",
"eq()()q()hq()(hqre[][][]{()))",
")dfh",
"ehu{()()())()()()()",
"({})()()()()))asdg(())asgwq)()321t5(((()12fds"]

# Case that should return 1
open_case = [
"!@#^$#&(324643)(",
"()asdgw()(@!#&^$#&)(",
"13246((()asd()oigo",
"$@*-=()()(",
"(qy1235)()(qwet)((",
"()()((3461(()25()153()145"
]

# Case that should return 0
okay_case = [
"(1266)()32164()!$#^%@&()|||",
"((qwet)qwet)",
"(52135(()1231())()q143265123()())"
]

# Types that should fail with TypeError
bad_types = [
0,
None,
5.32324,
{1: 123123, "a": 45243},
[1, 2, 3, 4, 5]
]

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not turn all these into fixtures?


def test_broken_case_via_assert(broken_case=broken_case):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This syntax is not really all that helpful to you. The broken_case symbol is bound in the scope of this function, but only because it's bound in the module as well. This does not operate the way you are thinking it does.

I believe what you were looking for is the "fixture" approach we used in the learning journal tests. Do you remember how that worked? Can you apply the same approach here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I started out with fixtures, but then opted to do lists because it was a tad simpler looking (momentarily opting for kiss). But the scope control is a good point, I wasn't thinking about that. Thanks!

"""
Testing a set of arguments that should return -1
"""
for case in broken_case:
assert parenthetical(case) == -1


def test_open_case_via_assert(open_case=open_case):
"""
Testing a set of arguments that should return 1
"""
for case in open_case:
assert parenthetical(case) == 1


def test_okay_case_via_assert(okay_case=okay_case):
"""
Testing a set of arguments that should return 1
"""
for case in okay_case:
assert parenthetical(case) == 0


def test_bad_types(bad_types=bad_types):
"""
Testing types that are not currently supported by parenthetical;
these will raise TypeError
"""
for bad_type in bad_types:
with pytest.raises(TypeError):
parenthetical(bad_type)