├── .gitignore ├── README.rst ├── dictionary.txt ├── lookahead.py ├── lookahead_test.py ├── search.py ├── search_test.py ├── substitution.py ├── substitution_test.py ├── test.py ├── validation.py └── validation_test.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__ 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Regular Expression Exercises 2 | ============================ 3 | 4 | These exercises accompany the Regular Expressions tutorial presented by `Trey Hunner`_ at PyCon 2017. 5 | 6 | 7 | Testing an exercise by name 8 | --------------------------- 9 | 10 | .. code-block:: bash 11 | 12 | $ python test.py has_vowel 13 | Testing function has_vowel 14 | 15 | ....... 16 | ---------------------------------------------------------------------- 17 | Ran 7 tests in 0.000s 18 | 19 | OK 20 | 21 | You will see ``OK`` if your code passes the tests and ``FAILED`` if your code fails one or more tests. 22 | 23 | 24 | Forgot the exercise name? 25 | ------------------------- 26 | 27 | If you're not sure of the function name for your exercise, just run the ``test.py`` file without any arguments to see a list of all exercise names: 28 | 29 | .. code-block:: bash 30 | 31 | $ python test.py 32 | Please select a function to test 33 | 34 | Functions that may be tested: 35 | [01] has_vowel: Return True iff the string contains one or more vowels. 36 | [02] is_integer: Return True iff the string represents a valid integer. 37 | [03] is_fraction: Return True iff the string represents a valid fraction. 38 | [04] is_valid_time: Return True iff the string represents a valid 24 hour time. 39 | [05] is_valid_date: Return True iff the string represents a valid YYYY-MM-DD date. 40 | [06] tetravocalic: Return a list of all words that have four consecutive vowels. 41 | [07] hexadecimal: Return a list of all words consisting solely of the letters A to F. 42 | [08] hexaconsonantal: Return a list of all words with six consecutive consonants. 43 | [09] possible_words: Return possible word matches from a partial word. 44 | [10] five_repeats: Return all words with at least five occurrences of the given letter. 45 | [11] is_number: Return True iff the string represents a decimal number. 46 | [12] is_hex_color: Return True iff the string represents an RGB hex color code. 47 | [13] abbreviate: Return an acronym for the given phrase. 48 | [14] palindrome5: Return a list of all five letter palindromes. 49 | [15] double_double: Return words with a double repeated letter with one letter between. 50 | [16] repeaters: Return words that consist of the same letters repeated two times. 51 | [17] get_extension: Return the file extension for a full file path. 52 | [18] normalize_jpeg: Return the filename with jpeg extensions normalized. 53 | [19] normalize_whitespace: Replace all runs of whitespace with a single space. 54 | [20] compress_blank_lines: Compress N or more empty lines into just N empty lines. 55 | [21] normalize_domain: Normalize all instances of treyhunner.com URLs. 56 | [22] convert_linebreaks: Convert linebreaks to HTML. 57 | [23] have_all_vowels: Return all words at most 9 letters long that contain all vowels. 58 | [24] no_repeats: Return all words with 10 or more letters and no repeating letters. 59 | [25] to_pig_latin: Convert English phrase to Pig Latin. 60 | [26] encode_ampersands: HTML-encode all & characters. 61 | [27] camel_to_underscore: Convert camelCase strings to under_score. 62 | [28] get_inline_links: Return a list of all inline links. 63 | [29] find_broken_links: Return a list of all broken reference-style links. 64 | [30] get_markdown_links: Return a list of all markdown links. 65 | 66 | What function would you like to test? 67 | 68 | You will see a list of all exercises. Note that each exercise has a name (e.g. ``has_vowel``) and a number (e.g. ``01``). 69 | 70 | You can type in the name or number of the exercise to run it. 71 | 72 | 73 | Solving an exercise 74 | ------------------- 75 | 76 | To solve an exercise, navigate to the appropriate exercise module and find the exercise function. For example: 77 | 78 | .. code-block:: python 79 | 80 | def has_vowel(string): 81 | """Return True iff the string contains one or more vowels.""" 82 | 83 | Modify this function to return the expected result for any input. Make sure to **use regular expressions** in your answer! 84 | 85 | 86 | .. _trey hunner: http://treyhunner.com/ 87 | -------------------------------------------------------------------------------- /lookahead.py: -------------------------------------------------------------------------------- 1 | """ 2 | Lookahead Exercises 3 | 4 | These functions could be made using lookaheads and/or lookbehinds. 5 | 6 | """ 7 | 8 | 9 | with open('dictionary.txt') as dict_file: 10 | dictionary = dict_file.read() 11 | 12 | 13 | def have_all_vowels(dictionary=dictionary): 14 | """Return all words at most 9 letters long that contain all vowels.""" 15 | 16 | 17 | def no_repeats(dictionary=dictionary): 18 | """Return all words with 10 or more letters and no repeating letters.""" 19 | 20 | 21 | def to_pig_latin(phrase): 22 | """Convert English phrase to Pig Latin.""" 23 | 24 | 25 | def encode_ampersands(phrase): 26 | """HTML-encode all & characters.""" 27 | 28 | 29 | def camel_to_underscore(camel_string): 30 | """Convert camelCase strings to under_score.""" 31 | 32 | 33 | def get_inline_links(markdown): 34 | """Return a list of all inline links.""" 35 | 36 | 37 | def find_broken_links(markdown): 38 | """Return a list of all broken reference-style links.""" 39 | 40 | 41 | def get_markdown_links(markdown): 42 | """Return a list of all markdown links.""" 43 | -------------------------------------------------------------------------------- /lookahead_test.py: -------------------------------------------------------------------------------- 1 | """Tests for lookahead exercises""" 2 | from textwrap import dedent 3 | import unittest 4 | 5 | 6 | from lookahead import (have_all_vowels, no_repeats, encode_ampersands, 7 | to_pig_latin, camel_to_underscore, 8 | find_broken_links, get_inline_links, get_markdown_links) 9 | 10 | 11 | class HaveAllVowelsTests(unittest.TestCase): 12 | 13 | def test_vowels_in_order(self): 14 | self.assertEqual(have_all_vowels("facetious"), ['facetious']) 15 | 16 | def test_vowels_out_of_order(self): 17 | self.assertEqual(have_all_vowels("euphoria"), ['euphoria']) 18 | 19 | def test_multiple_words(self): 20 | words = "Let's have a dialogue about education.".replace(" ", "\n") 21 | self.assertEqual(have_all_vowels(words), ['dialogue', 'education']) 22 | 23 | def test_too_long(self): 24 | self.assertEqual(have_all_vowels('educational'), []) 25 | 26 | 27 | class NoRepeatsTests(unittest.TestCase): 28 | 29 | def test_background(self): 30 | self.assertEqual(no_repeats("background"), ['background']) 31 | 32 | def test_multiple(self): 33 | words = "the\ntambourines\nnefariously\nclamouring\neverywhere" 34 | self.assertEqual( 35 | no_repeats(words), 36 | ['tambourines', 'nefariously', 'clamouring'] 37 | ) 38 | 39 | def test_too_short(self): 40 | self.assertEqual(no_repeats('authorize'), []) 41 | 42 | 43 | class EncodeAmpersandsTests(unittest.TestCase): 44 | 45 | def test_a_and_w(self): 46 | self.assertEqual(encode_ampersands("A&W"), "A&W") 47 | 48 | def test_encoded_ampersand(self): 49 | self.assertEqual(encode_ampersands("&"), "&") 50 | 51 | def test_copyright(self): 52 | self.assertEqual(encode_ampersands("©"), "©") 53 | 54 | def test_numeric_encoding(self): 55 | self.assertEqual(encode_ampersands("&"), "&") 56 | 57 | def test_whole_sentence(self): 58 | sentence = "This & that & that & this." 59 | encoded = "This & that & that & this." 60 | self.assertEqual(encode_ampersands(sentence), encoded) 61 | 62 | 63 | class ToPigLatinTests(unittest.TestCase): 64 | 65 | def test_apple(self): 66 | self.assertEqual(to_pig_latin("apple"), 'appleay') 67 | 68 | def test_eggs(self): 69 | self.assertEqual(to_pig_latin("eggs"), 'eggsay') 70 | 71 | def test_pig(self): 72 | self.assertEqual(to_pig_latin("pig"), 'igpay') 73 | 74 | def test_trust(self): 75 | self.assertEqual(to_pig_latin("trust"), 'usttray') 76 | 77 | def test_quack(self): 78 | self.assertEqual(to_pig_latin("quack"), 'ackquay') 79 | 80 | def test_squeak(self): 81 | self.assertEqual(to_pig_latin("squeak"), 'eaksquay') 82 | 83 | def test_enqeue(self): 84 | self.assertEqual(to_pig_latin("enqueue"), 'enqueueay') 85 | 86 | def test_sequoia(self): 87 | self.assertEqual(to_pig_latin("sequoia"), 'equoiasay') 88 | 89 | 90 | class CamelToUnderscoreTests(unittest.TestCase): 91 | 92 | def test_index_of(self): 93 | self.assertEqual(camel_to_underscore("indexOf"), "index_of") 94 | 95 | def test_to_lowercase(self): 96 | self.assertEqual(camel_to_underscore("toLowerCase"), "to_lower_case") 97 | 98 | def test_xml(self): 99 | self.assertEqual(camel_to_underscore("XML"), "xml") 100 | 101 | def test_http_client(self): 102 | self.assertEqual(camel_to_underscore("HTTPClient"), "http_client") 103 | 104 | def test_xml_http_request(self): 105 | self.assertEqual( 106 | camel_to_underscore("XMLHttpRequest"), 107 | "xml_http_request" 108 | ) 109 | 110 | 111 | class GetInlineLinksTests(unittest.TestCase): 112 | 113 | def test_multiple_links(self): 114 | markdown = dedent(""" 115 | [Python](https://www.python.org) 116 | [Google](https://www.google.com)""") 117 | self.assertEqual( 118 | get_inline_links(markdown), 119 | [('Python', 'https://www.python.org'), 120 | ('Google', 'https://www.google.com')] 121 | ) 122 | 123 | 124 | class FindBrokenLinksTests(unittest.TestCase): 125 | 126 | def test_multiple_links(self): 127 | markdown = dedent(""" 128 | [working link][Python] 129 | [broken link][Google] 130 | [python]: https://www.python.org/""") 131 | self.assertEqual( 132 | find_broken_links(markdown), 133 | [('broken link', 'Google')] 134 | ) 135 | 136 | def test_implicit_links(self): 137 | markdown = dedent(""" 138 | [Python][] 139 | [Google][] 140 | [python]: https://www.python.org/""") 141 | self.assertEqual( 142 | find_broken_links(markdown), 143 | [('Google', 'Google')] 144 | ) 145 | 146 | 147 | class GetMarkdownLinksTests(unittest.TestCase): 148 | 149 | def test_multiple_links(self): 150 | markdown = dedent(""" 151 | [Python](https://www.python.org) 152 | [Google][] 153 | [Another link][example] 154 | [google]: https://www.google.com 155 | [example]: http://example.com""") 156 | self.assertEqual( 157 | set(get_markdown_links(markdown)), 158 | {('Python', 'https://www.python.org'), 159 | ('Google', 'https://www.google.com'), 160 | ('Another link', 'http://example.com')} 161 | ) 162 | 163 | 164 | if __name__ == "__main__": 165 | raise SystemExit("No CLI for this file. Run test.py instead.") 166 | -------------------------------------------------------------------------------- /search.py: -------------------------------------------------------------------------------- 1 | """ 2 | Search Exercises 3 | 4 | These functions return a list of strings matching a condition. 5 | 6 | """ 7 | 8 | 9 | with open('dictionary.txt') as dict_file: 10 | dictionary = dict_file.read() 11 | 12 | 13 | def get_extension(filename): 14 | """Return the file extension for a full file path.""" 15 | 16 | 17 | def tetravocalic(dictionary=dictionary): 18 | """Return a list of all words that have four consecutive vowels.""" 19 | 20 | 21 | def hexadecimal(dictionary=dictionary): 22 | """Return a list of all words consisting solely of the letters A to F.""" 23 | 24 | 25 | def hexaconsonantal(dictionary=dictionary): 26 | """Return a list of all words with six consecutive consonants.""" 27 | 28 | 29 | def possible_words(partial_word, dictionary=dictionary): 30 | """ 31 | Return possible word matches from a partial word. 32 | 33 | Underscores in partial words represent missing letters. Examples: 34 | C_T (cat, cot, cut) 35 | _X_ (axe) 36 | """ 37 | 38 | 39 | def five_repeats(letter, dictionary=dictionary): 40 | """Return all words with at least five occurrences of the given letter.""" 41 | 42 | 43 | def abbreviate(phrase): 44 | """Return an acronym for the given phrase.""" 45 | 46 | 47 | def palindrome5(dictionary=dictionary): 48 | """Return a list of all five letter palindromes.""" 49 | 50 | 51 | def double_double(dictionary=dictionary): 52 | """ 53 | Return words with a double repeated letter with one letter between. 54 | 55 | Example double double words: 56 | - freebee 57 | - assessed 58 | - voodoo 59 | """ 60 | 61 | 62 | def repeaters(dictionary=dictionary): 63 | """ 64 | Return words that consist of the same letters repeated two times. 65 | 66 | Example double double words: 67 | - tutu 68 | - cancan 69 | - murmur 70 | """ 71 | -------------------------------------------------------------------------------- /search_test.py: -------------------------------------------------------------------------------- 1 | """Tests for search exercises""" 2 | import unittest 3 | 4 | 5 | from search import (get_extension, hexadecimal, tetravocalic, hexaconsonantal, 6 | possible_words, five_repeats, abbreviate, palindrome5, 7 | double_double, repeaters) 8 | 9 | 10 | class GetExtensionTests(unittest.TestCase): 11 | 12 | def test_zip(self): 13 | self.assertEqual(get_extension('archive.zip'), 'zip') 14 | 15 | def test_jpeg(self): 16 | self.assertEqual(get_extension('image.jpeg'), 'jpeg') 17 | 18 | def test_xhtml(self): 19 | self.assertEqual(get_extension('index.xhtml'), 'xhtml') 20 | 21 | def test_gzipped_tarball(self): 22 | self.assertEqual(get_extension('archive.tar.gz'), 'gz') 23 | 24 | 25 | class HexadecimalTests(unittest.TestCase): 26 | 27 | def test_cab(self): 28 | self.assertEqual(hexadecimal('cab'), ['cab']) 29 | 30 | def test_bed(self): 31 | self.assertEqual(hexadecimal('bed'), ['bed']) 32 | 33 | def test_cog(self): 34 | self.assertEqual(hexadecimal('cog'), []) 35 | 36 | def test_deed(self): 37 | self.assertEqual(hexadecimal('deed'), ['deed']) 38 | 39 | def test_sentence(self): 40 | sentence = "Hooligans defaced the cafe." 41 | self.assertEqual( 42 | hexadecimal(sentence.replace(' ', '\n')), 43 | ['defaced', 'cafe'] 44 | ) 45 | 46 | 47 | class TetravocalicTests(unittest.TestCase): 48 | 49 | def test_three_vowels(self): 50 | self.assertEqual(tetravocalic("aei"), []) 51 | 52 | def test_four_vowels(self): 53 | self.assertEqual(tetravocalic("aeio"), ['aeio']) 54 | 55 | def test_five_vowels(self): 56 | self.assertEqual(tetravocalic("aeiou"), ['aeiou']) 57 | 58 | def test_non_consecutive(self): 59 | self.assertEqual(tetravocalic("aeixou"), []) 60 | 61 | def test_onomatopoeia(self): 62 | self.assertEqual(tetravocalic("onomatopoeia"), ['onomatopoeia']) 63 | 64 | def test_gooey(self): 65 | sentence = "A is gooey but B is gooier and C is gooiest" 66 | self.assertEqual( 67 | tetravocalic(sentence.replace(' ', '\n')), 68 | ['gooier', 'gooiest'] 69 | ) 70 | 71 | 72 | class HexaconsonantalTests(unittest.TestCase): 73 | 74 | def test_borscht(self): 75 | self.assertEqual(hexaconsonantal("I\nlike\nborschts\n"), ['borschts']) 76 | 77 | def test_catchphrases(self): 78 | self.assertEqual( 79 | hexaconsonantal("favorite\ncatchphrases"), 80 | ['catchphrases'] 81 | ) 82 | 83 | def test_five_consonants(self): 84 | self.assertEqual(hexaconsonantal("touchscreen\nborscht"), []) 85 | 86 | 87 | class PossibleWordsTests(unittest.TestCase): 88 | 89 | def test_cistern(self): 90 | self.assertEqual(possible_words("CIS____"), ['cistern']) 91 | 92 | def test_axe(self): 93 | self.assertEqual(possible_words("_X_"), ['axe']) 94 | 95 | def test_c_t_(self): 96 | self.assertEqual(possible_words("c_t"), ['cat', 'cot', 'cut']) 97 | 98 | 99 | class FiveRepeatsTests(unittest.TestCase): 100 | 101 | def test_a(self): 102 | self.assertEqual(five_repeats('a'), ['abracadabra', 'abracadabras']) 103 | 104 | def test_b(self): 105 | self.assertEqual(five_repeats('b'), []) 106 | 107 | def test_c(self): 108 | self.assertEqual(five_repeats('c'), []) 109 | 110 | def test_d(self): 111 | self.assertEqual(five_repeats('d'), []) 112 | 113 | def test_e(self): 114 | words = five_repeats('e') 115 | self.assertIn('beekeeper', words) 116 | self.assertIn('interdependence', words) 117 | self.assertIn('peacekeeper', words) 118 | self.assertNotIn('peacekeeping', words) 119 | self.assertIn('reemergence', words) 120 | self.assertNotIn('speechlessness', words) 121 | 122 | 123 | class AbbreviateTests(unittest.TestCase): 124 | 125 | def test_gif(self): 126 | self.assertEqual(abbreviate('Graphics Interchange Format'), 'GIF') 127 | 128 | def test_faq(self): 129 | self.assertEqual(abbreviate('frequently asked questions'), 'FAQ') 130 | 131 | def test_css(self): 132 | self.assertEqual(abbreviate('cascading style sheets'), 'CSS') 133 | 134 | def test_cms(self): 135 | self.assertEqual(abbreviate('content management system'), 'CMS') 136 | 137 | def test_json(self): 138 | self.assertEqual(abbreviate('JavaScript Object Notation'), 'JSON') 139 | 140 | def test_html(self): 141 | self.assertEqual(abbreviate('HyperText Markup Language'), 'HTML') 142 | 143 | 144 | class PalindromeTests(unittest.TestCase): 145 | 146 | def test_kayak(self): 147 | self.assertEqual(palindrome5('kayak'), ['kayak']) 148 | 149 | def test_partial(self): 150 | self.assertEqual(palindrome5('what were you referring to?'), []) 151 | 152 | def test_multiple(self): 153 | self.assertEqual( 154 | palindrome5('which level are you and what are your stats?'), 155 | ['level', 'stats'] 156 | ) 157 | 158 | def test_wrong_length(self): 159 | self.assertEqual(palindrome5('racecar driver doing a pullup'), []) 160 | 161 | 162 | class DoubleDoubleTests(unittest.TestCase): 163 | 164 | def test_no_in_between(self): 165 | self.assertEqual(double_double('eeee'), []) 166 | 167 | def test_one_in_between(self): 168 | self.assertEqual(double_double('eexee'), ['eexee']) 169 | 170 | def test_two_in_between(self): 171 | self.assertEqual(double_double('eexxee'), []) 172 | 173 | def test_singles(self): 174 | self.assertEqual(double_double('aba'), []) 175 | 176 | def test_triples(self): 177 | self.assertEqual(double_double('aaabaaa'), ['aaabaaa']) 178 | 179 | def test_beekeeper(self): 180 | self.assertEqual(double_double('beekeeper'), ['beekeeper']) 181 | 182 | def test_two_words(self): 183 | words = 'Your freebees need more pizzazz.'.replace(' ', '\n') 184 | self.assertEqual(double_double(words), ['freebees', 'pizzazz']) 185 | 186 | def test_three_words(self): 187 | sentence = 'The granddaddy of all dispossessed squeegees.' 188 | self.assertEqual( 189 | double_double(sentence.replace(' ', '\n')), 190 | ['granddaddy', 'dispossessed', 'squeegees'] 191 | ) 192 | 193 | 194 | class RepeatersTests(unittest.TestCase): 195 | 196 | def test_tutu(self): 197 | self.assertEqual(repeaters('tutu'), ['tutu']) 198 | 199 | def test_cancan(self): 200 | self.assertEqual(repeaters('cancan'), ['cancan']) 201 | 202 | def test_murmur(self): 203 | self.assertEqual(repeaters('murmur'), ['murmur']) 204 | 205 | def test_multiple(self): 206 | sentence = 'look at those froufrou hotshots' 207 | self.assertEqual(repeaters(sentence), ['froufrou', 'hotshots']) 208 | 209 | def test_whole_word(self): 210 | sentence = "gaga for bonbons" 211 | self.assertEqual(repeaters(sentence), ['gaga']) 212 | 213 | 214 | if __name__ == "__main__": 215 | raise SystemExit("No CLI for this file. Run test.py instead.") 216 | -------------------------------------------------------------------------------- /substitution.py: -------------------------------------------------------------------------------- 1 | """ 2 | Substitution Exercises 3 | 4 | These functions return a new altered version of the given string. 5 | 6 | """ 7 | 8 | 9 | def get_extension(filename): 10 | """Return the file extension for a full file path.""" 11 | 12 | 13 | def normalize_jpeg(filename): 14 | """Return the filename with jpeg extensions normalized.""" 15 | 16 | 17 | def normalize_whitespace(string): 18 | """Replace all runs of whitespace with a single space.""" 19 | 20 | 21 | def compress_blank_lines(string, max_blanks): 22 | """Compress N or more empty lines into just N empty lines.""" 23 | 24 | 25 | def normalize_domain(string): 26 | """Normalize all instances of treyhunner.com URLs.""" 27 | 28 | 29 | def convert_linebreaks(string): 30 | """Convert linebreaks to HTML.""" 31 | -------------------------------------------------------------------------------- /substitution_test.py: -------------------------------------------------------------------------------- 1 | """Tests for substitution exercises""" 2 | from textwrap import dedent 3 | import unittest 4 | 5 | 6 | from substitution import (normalize_jpeg, normalize_whitespace, 7 | compress_blank_lines, normalize_domain, 8 | convert_linebreaks) 9 | 10 | 11 | class NormalizeJPEGTests(unittest.TestCase): 12 | 13 | def test_jpeg(self): 14 | self.assertEqual(normalize_jpeg('avatar.jpeg'), 'avatar.jpg') 15 | 16 | def test_capital(self): 17 | self.assertEqual(normalize_jpeg('Avatar.JPEG'), 'Avatar.jpg') 18 | 19 | def test_mixed_case(self): 20 | self.assertEqual(normalize_jpeg('AVATAR.Jpg'), 'AVATAR.jpg') 21 | 22 | def test_gif(self): 23 | self.assertEqual(normalize_jpeg('avatar.gif'), 'avatar.gif') 24 | 25 | 26 | class NormalizeWhitespaceTests(unittest.TestCase): 27 | 28 | def test_two_spaces(self): 29 | self.assertEqual(normalize_whitespace("hello there"), "hello there") 30 | 31 | def test_poem(self): 32 | poem = dedent(""" 33 | Hold fast to dreams 34 | For if dreams die 35 | Life is a broken-winged bird 36 | That cannot fly. 37 | 38 | Hold fast to dreams 39 | For when dreams go 40 | Life is a barren field 41 | Frozen with snow. 42 | """).strip() 43 | self.assertEqual( 44 | normalize_whitespace(poem), 45 | 'Hold fast to dreams For if dreams die ' 46 | 'Life is a broken-winged bird That cannot fly. ' 47 | 'Hold fast to dreams For when dreams go ' 48 | 'Life is a barren field Frozen with snow.' 49 | ) 50 | 51 | 52 | class CompressBlankLinesTests(unittest.TestCase): 53 | 54 | def test_one_blank(self): 55 | self.assertEqual( 56 | compress_blank_lines("a\n\nb", max_blanks=1), 57 | 'a\n\nb' 58 | ) 59 | 60 | def test_no_blanks(self): 61 | self.assertEqual( 62 | compress_blank_lines("a\n\nb", max_blanks=0), 63 | 'a\nb' 64 | ) 65 | 66 | def test_two_blanks(self): 67 | self.assertEqual( 68 | compress_blank_lines("a\n\nb", max_blanks=2), 69 | 'a\n\nb' 70 | ) 71 | 72 | def test_many_blanks(self): 73 | self.assertEqual( 74 | compress_blank_lines("a\n\n\n\nb\n\n\nc", max_blanks=2), 75 | 'a\n\n\nb\n\n\nc' 76 | ) 77 | 78 | 79 | class NormalizeDomainTests(unittest.TestCase): 80 | 81 | def test_http(self): 82 | self.assertEqual( 83 | normalize_domain("http://treyhunner.com/2015/12/"), 84 | 'https://treyhunner.com/2015/12/' 85 | ) 86 | 87 | def test_https(self): 88 | self.assertEqual( 89 | normalize_domain("https://treyhunner.com/2016/02/"), 90 | 'https://treyhunner.com/2016/02/' 91 | ) 92 | 93 | def test_www(self): 94 | self.assertEqual( 95 | normalize_domain("http://www.treyhunner.com/2015/11/"), 96 | 'https://treyhunner.com/2015/11/' 97 | ) 98 | 99 | def test_just_domain(self): 100 | self.assertEqual( 101 | normalize_domain("http://www.treyhunner.com"), 102 | 'https://treyhunner.com' 103 | ) 104 | 105 | def test_different_domain(self): 106 | self.assertEqual( 107 | normalize_domain("http://trey.in/give-a-talk"), 108 | 'http://trey.in/give-a-talk' 109 | ) 110 | 111 | 112 | class ConvertLinebreaksTests(unittest.TestCase): 113 | 114 | def test_one_paragraph(self): 115 | self.assertEqual( 116 | convert_linebreaks("hello"), 117 | '

hello

' 118 | ) 119 | 120 | def test_one_linebreak(self): 121 | self.assertEqual( 122 | convert_linebreaks("hello\nthere"), 123 | '

hello
there

' 124 | ) 125 | 126 | def test_two_paragraphs(self): 127 | self.assertEqual( 128 | convert_linebreaks("hello\n\nthere"), 129 | '

hello

there

' 130 | ) 131 | 132 | def test_two_paragraphs_with_break(self): 133 | self.assertEqual( 134 | convert_linebreaks("hello\nthere\n\nworld"), 135 | '

hello
there

world

' 136 | ) 137 | 138 | 139 | if __name__ == "__main__": 140 | raise SystemExit("No CLI for this file. Run test.py instead.") 141 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import print_function 3 | from collections import OrderedDict 4 | import sys 5 | import unittest 6 | 7 | # Python 2 8 | try: 9 | input = raw_input 10 | except NameError: 11 | pass 12 | 13 | 14 | TESTS = OrderedDict([ 15 | ('has_vowel', 'validation_test.HasVowelTests'), 16 | ('is_integer', 'validation_test.IsIntegerTests'), 17 | ('is_fraction', 'validation_test.IsFractionTests'), 18 | ('get_extension', 'search_test.GetExtensionTests'), 19 | ('hexadecimal', 'search_test.HexadecimalTests'), 20 | ('tetravocalic', 'search_test.TetravocalicTests'), 21 | ('hexaconsonantal', 'search_test.HexaconsonantalTests'), 22 | ('possible_words', 'search_test.PossibleWordsTests'), 23 | ('five_repeats', 'search_test.FiveRepeatsTests'), 24 | ('is_number', 'validation_test.IsNumberTests'), 25 | ('is_hex_color', 'validation_test.IsHexColorTests'), 26 | ('is_valid_date', 'validation_test.IsValidDateTests'), 27 | ('abbreviate', 'search_test.AbbreviateTests'), 28 | ('palindrome5', 'search_test.PalindromeTests'), 29 | ('double_double', 'search_test.DoubleDoubleTests'), 30 | ('repeaters', 'search_test.RepeatersTests'), 31 | ('normalize_jpeg', 'substitution_test.NormalizeJPEGTests'), 32 | ('normalize_whitespace', 'substitution_test.NormalizeWhitespaceTests'), 33 | ('compress_blank_lines', 'substitution_test.CompressBlankLinesTests'), 34 | ('normalize_domain', 'substitution_test.NormalizeDomainTests'), 35 | ('convert_linebreaks', 'substitution_test.ConvertLinebreaksTests'), 36 | ('have_all_vowels', 'lookahead_test.HaveAllVowelsTests'), 37 | ('no_repeats', 'lookahead_test.NoRepeatsTests'), 38 | ('to_pig_latin', 'lookahead_test.ToPigLatinTests'), 39 | ('encode_ampersands', 'lookahead_test.EncodeAmpersandsTests'), 40 | ('camel_to_underscore', 'lookahead_test.CamelToUnderscoreTests'), 41 | ('get_inline_links', 'lookahead_test.GetInlineLinksTests'), 42 | ('find_broken_links', 'lookahead_test.FindBrokenLinksTests'), 43 | ('get_markdown_links', 'lookahead_test.GetMarkdownLinksTests'), 44 | ]) 45 | 46 | 47 | def get_function(func_id): 48 | """Return function name given a name or number.""" 49 | try: 50 | n = int(func_id) - 1 51 | except ValueError: 52 | if func_id in TESTS: 53 | return func_id 54 | else: 55 | try: 56 | return list(TESTS.keys())[n] 57 | except IndexError: 58 | pass 59 | raise SystemExit("Function {} doesn't exist.".format(func_id)) 60 | 61 | 62 | def load_test(func_name): 63 | print("Testing function {}\n".format(func_name)) 64 | tests = [unittest.defaultTestLoader.loadTestsFromName(TESTS[func_name])] 65 | test_suite = unittest.TestSuite(tests) 66 | unittest.TextTestRunner().run(test_suite) 67 | 68 | 69 | def print_function_names(): 70 | print("Functions that may be tested:") 71 | for n, (function_name, test_class_name) in enumerate(TESTS.items(), 1): 72 | module_name = test_class_name.split('.', 1)[0] 73 | function = getattr(__import__(module_name), function_name) 74 | print("[{n:02d}] {name}: {doc}".format( 75 | n=n, 76 | name=function.__name__, 77 | doc=function.__doc__.strip().split('\n', 1)[0], 78 | )) 79 | 80 | 81 | def get_function_name(): 82 | print("Please select a function to test\n") 83 | print_function_names() 84 | print("") 85 | return input("What function would you like to test? ") 86 | 87 | 88 | def main(*arguments): 89 | if not arguments: 90 | arguments = [get_function_name()] 91 | for arg in arguments: 92 | load_test(get_function(arg)) 93 | 94 | 95 | if __name__ == "__main__": 96 | main(*sys.argv[1:]) 97 | -------------------------------------------------------------------------------- /validation.py: -------------------------------------------------------------------------------- 1 | """ 2 | Validation Exercises 3 | 4 | These functions return ``True`` or ``False`` depending on whether the 5 | string passes a condition. 6 | 7 | """ 8 | 9 | 10 | def has_vowel(string): 11 | """Return True iff the string contains one or more vowels.""" 12 | 13 | 14 | def is_integer(string): 15 | """Return True iff the string represents a valid integer.""" 16 | 17 | 18 | def is_fraction(string): 19 | """Return True iff the string represents a valid fraction.""" 20 | 21 | 22 | def is_valid_date(string): 23 | """Return True iff the string represents a valid YYYY-MM-DD date.""" 24 | 25 | 26 | def is_number(string): 27 | """Return True iff the string represents a decimal number.""" 28 | 29 | 30 | def is_hex_color(string): 31 | """Return True iff the string represents an RGB hex color code.""" 32 | -------------------------------------------------------------------------------- /validation_test.py: -------------------------------------------------------------------------------- 1 | """Tests for validation exercises""" 2 | import unittest 3 | 4 | 5 | from validation import (has_vowel, is_integer, is_fraction, is_number, 6 | is_hex_color, is_valid_date) 7 | 8 | 9 | class HasVowelTests(unittest.TestCase): 10 | 11 | def test_rhythm(self): 12 | self.assertFalse(has_vowel("rhythm")) 13 | 14 | def test_exit(self): 15 | self.assertTrue(has_vowel("exit")) 16 | 17 | def test_no(self): 18 | self.assertTrue(has_vowel("no")) 19 | 20 | def test_yes(self): 21 | self.assertTrue(has_vowel("yes")) 22 | 23 | def test_fly(self): 24 | self.assertFalse(has_vowel("fly")) 25 | 26 | def test_symbols(self): 27 | self.assertFalse(has_vowel("%^&")) 28 | 29 | def test_empty(self): 30 | self.assertFalse(has_vowel("")) 31 | 32 | 33 | class IsIntegerTests(unittest.TestCase): 34 | 35 | def test_single_digit(self): 36 | self.assertTrue(is_integer("5")) 37 | 38 | def test_leading_letter(self): 39 | self.assertFalse(is_integer("a5")) 40 | 41 | def test_trailing_letter(self): 42 | self.assertFalse(is_integer("5a")) 43 | 44 | def test_5000(self): 45 | self.assertTrue(is_integer("5000")) 46 | 47 | def test_leading_minus(self): 48 | self.assertTrue(is_integer("-999")) 49 | 50 | def test_leading_plus(self): 51 | self.assertFalse(is_integer("+999")) 52 | 53 | def test_leading_zero(self): 54 | self.assertTrue(is_integer("00")) 55 | 56 | def test_zero_decimal(self): 57 | self.assertFalse(is_integer("0.0")) 58 | 59 | def test_only_minus(self): 60 | self.assertFalse(is_integer("-")) 61 | 62 | def test_leading_space(self): 63 | self.assertFalse(is_integer(" 5")) 64 | 65 | def test_empty(self): 66 | self.assertFalse(is_integer("")) 67 | 68 | 69 | class IsFractionTests(unittest.TestCase): 70 | 71 | def test_5000(self): 72 | self.assertFalse(is_fraction("5000")) 73 | 74 | def test_leading_minus(self): 75 | self.assertTrue(is_fraction("-999/1")) 76 | 77 | def test_leading_plus(self): 78 | self.assertFalse(is_fraction("+999/1")) 79 | 80 | def test_leading_zero_in_numerator(self): 81 | self.assertTrue(is_fraction("00/1")) 82 | 83 | def test_no_numerator(self): 84 | self.assertFalse(is_fraction("/5")) 85 | 86 | def test_no_denominator(self): 87 | self.assertFalse(is_fraction("5/")) 88 | 89 | def test_divide_by_zero(self): 90 | self.assertFalse(is_fraction("5/0")) 91 | 92 | def test_leading_zero_in_denominator(self): 93 | self.assertTrue(is_fraction("5/010")) 94 | 95 | def test_simple_fraction(self): 96 | self.assertTrue(is_fraction("5/105")) 97 | 98 | def test_with_spaces(self): 99 | self.assertFalse(is_fraction("5 / 1")) 100 | 101 | def test_empty(self): 102 | self.assertFalse(is_fraction("")) 103 | 104 | def test_leading_letter(self): 105 | self.assertFalse(is_fraction("a5")) 106 | 107 | def test_trailing_letter(self): 108 | self.assertFalse(is_fraction("5a")) 109 | 110 | 111 | class IsNumberTests(unittest.TestCase): 112 | 113 | def test_5(self): 114 | self.assertTrue(is_number("5")) 115 | 116 | def test_5_point(self): 117 | self.assertTrue(is_number("5.")) 118 | 119 | def test_point_5_point(self): 120 | self.assertFalse(is_number(".5.")) 121 | 122 | def test_point_5(self): 123 | self.assertTrue(is_number(".5")) 124 | 125 | def test_leading_zero(self): 126 | self.assertTrue(is_number("01.5")) 127 | 128 | def test_negative(self): 129 | self.assertTrue(is_number("-123.859")) 130 | 131 | def test_two_decimals(self): 132 | self.assertFalse(is_number("-123.859.")) 133 | 134 | def test_just_a_decimal(self): 135 | self.assertFalse(is_number(".")) 136 | 137 | def test_leading_garbage(self): 138 | self.assertFalse(is_number("a5")) 139 | 140 | def test_trailing_garbage(self): 141 | self.assertFalse(is_number("5a")) 142 | 143 | 144 | class IsHexColorTests(unittest.TestCase): 145 | 146 | def test_purple_short(self): 147 | self.assertTrue(is_hex_color("#639")) 148 | 149 | def test_four_digits(self): 150 | self.assertFalse(is_hex_color("#6349")) 151 | 152 | def test_five_digits(self): 153 | self.assertFalse(is_hex_color("#63459")) 154 | 155 | def test_dark_purple(self): 156 | self.assertTrue(is_hex_color("#634569")) 157 | 158 | def test_purple_long(self): 159 | self.assertTrue(is_hex_color("#663399")) 160 | 161 | def test_black(self): 162 | self.assertTrue(is_hex_color("#000000")) 163 | 164 | def test_two_digits(self): 165 | self.assertFalse(is_hex_color("#00")) 166 | 167 | def test_mixed_case(self): 168 | self.assertTrue(is_hex_color("#FFffFF")) 169 | 170 | def test_hex(self): 171 | self.assertTrue(is_hex_color("#decaff")) 172 | 173 | def test_invalid_character(self): 174 | self.assertFalse(is_hex_color("#decafz")) 175 | 176 | def test_no_octothorpe(self): 177 | self.assertFalse(is_hex_color("639")) 178 | 179 | def test_misplaced_octothorpe(self): 180 | self.assertFalse(is_hex_color("639#")) 181 | 182 | def test_leading_garbage(self): 183 | self.assertFalse(is_hex_color("a#639")) 184 | 185 | 186 | class IsValidDateTests(unittest.TestCase): 187 | 188 | def test_this_year(self): 189 | self.assertTrue(is_valid_date("2016-01-02")) 190 | 191 | def test_1990(self): 192 | self.assertTrue(is_valid_date("1900-01-01")) 193 | 194 | def test_invalid_day(self): 195 | self.assertFalse(is_valid_date("2016-02-99")) 196 | 197 | def test_invalid_year(self): 198 | self.assertFalse(is_valid_date("20-02-20")) 199 | 200 | def test_invalid_month(self): 201 | self.assertFalse(is_valid_date("1980-30-05")) 202 | 203 | def test_leading_garbage(self): 204 | self.assertFalse(is_valid_date("12016-01-02")) 205 | 206 | def test_trailing_garbage(self): 207 | self.assertFalse(is_valid_date("2016-01-020")) 208 | 209 | 210 | if __name__ == "__main__": 211 | raise SystemExit("No CLI for this file. Run test.py instead.") 212 | --------------------------------------------------------------------------------