diff --git a/parenthetic/README.md b/parenthetic/README.md new file mode 100644 index 0000000..c591c56 --- /dev/null +++ b/parenthetic/README.md @@ -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. \ No newline at end of file diff --git a/parenthetic/parenthetics.py b/parenthetic/parenthetics.py new file mode 100644 index 0000000..84a1f3d --- /dev/null +++ b/parenthetic/parenthetics.py @@ -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) diff --git a/parenthetic/test_parenthetics.py b/parenthetic/test_parenthetics.py new file mode 100644 index 0000000..5e9b696 --- /dev/null +++ b/parenthetic/test_parenthetics.py @@ -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] +] + + +def test_broken_case_via_assert(broken_case=broken_case): + """ + 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)