diff --git a/.gitignore b/.gitignore index 68ca39690..0126fbf13 100644 --- a/.gitignore +++ b/.gitignore @@ -11,23 +11,25 @@ tex2pdf* .coverage .idea .vscode -02_crowsnest/crowsnest.py -03_picnic/picnic.py -04_jump_the_five/jump.py -05_howler/howler.py -06_wc/wc.py -07_gashlycrumb/gashlycrumb.py -08_apples_and_bananas/apples.py -09_abuse/abuse.py -10_telephone/telephone.py -11_bottles_of_beer/bottles.py -12_ransom/ransom.py -13_twelve_days/twelve_days.py -14_rhymer/rhymer.py -15_kentucky_friar/friar.py -16_scrambler/scrambler.py -17_mad_libs/mad.py -18_gematria/gematria.py -19_wod/wod.py -20_password/password.py -21_tictactoe/tictactoe.py +venv/ +# 02_crowsnest/crowsnest.py +# 03_picnic/picnic.py +# 04_jump_the_five/jump.py +# 05_howler/howler.py +# 06_wc/wc.py +# 07_gashlycrumb/gashlycrumb.py +# 08_apples_and_bananas/apples.py +# 09_abuse/abuse.py +# 10_telephone/telephone.py +# 11_bottles_of_beer/bottles.py +# 12_ransom/ransom.py +# 13_twelve_days/twelve_days.py +# 14_rhymer/rhymer.py +# 15_kentucky_friar/friar.py +# 16_scrambler/scrambler.py +# 17_mad_libs/mad.py +# 18_gematria/gematria.py +# 19_wod/wod.py +# 20_password/password.py +# 21_tictactoe/tictactoe.py +inputs/word* diff --git a/01_hello/hello.py b/01_hello/hello.py new file mode 100755 index 000000000..f1aa9f6fd --- /dev/null +++ b/01_hello/hello.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +""" +Author: the infamous herjazz +Purpose: Say hello +""" +import argparse + + +def get_args(): + """ Get command-line arguments """ + parser = argparse.ArgumentParser(description='Say hello') + parser.add_argument('-n', '--name', metavar='name', + default='World', help='Name to greet') + return parser.parse_args() + + +def main(): + """ It comes from Africa, ca, ca, ca """ + args = get_args() + print(f"Hello, {args.name}!") + + +if __name__ == '__main__': + main() diff --git a/02_crowsnest/crowsnest.py b/02_crowsnest/crowsnest.py new file mode 100755 index 000000000..fda936452 --- /dev/null +++ b/02_crowsnest/crowsnest.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +""" +Author : wrjt +Date : 2021-08-25 +Purpose: Print a sentence with a word taken from command line +""" + +import argparse + + +# -------------------------------------------------- +def get_args(): + """Get command-line arguments""" + + parser = argparse.ArgumentParser( + description="Crow's Nest -- choose the correct article", + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('word', metavar='word', help='A word') + + parser.add_argument('--starboard', default=False, action='store_true') + + return parser.parse_args() + + +# -------------------------------------------------- +def validate_word(supplied_word: str) -> str: + """Check first character of supplied word is a letter""" + + if not supplied_word[0].isalpha(): + raise ValueError + return supplied_word + + +# -------------------------------------------------- +def main(): + """ Running with it """ + + args = get_args() + word = validate_word(args.word) + side = 'starboard' if args.starboard else 'larboard' + char = word[0] + article = 'an' if char.lower() in 'aeiou' else 'a' + if char.isupper(): + article = article.title() + print(f"Ahoy, Captain, {article} {word} off the {side} bow!") + + +# -------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/02_crowsnest/test.py b/02_crowsnest/test.py index d4e25c6c7..57e34bc25 100755 --- a/02_crowsnest/test.py +++ b/02_crowsnest/test.py @@ -2,7 +2,7 @@ """tests for crowsnest.py""" import os -from subprocess import getstatusoutput, getoutput +from subprocess import getstatusoutput, getoutput, CalledProcessError prg = './crowsnest.py' consonant_words = [ @@ -43,11 +43,11 @@ def test_consonant(): # -------------------------------------------------- def test_consonant_upper(): - """brigantine -> a Brigatine""" + """brigantine -> A Brigantine""" for word in consonant_words: out = getoutput(f'{prg} {word.title()}') - assert out.strip() == template.format('a', word.title()) + assert out.strip() == template.format('A', word.title()) # -------------------------------------------------- @@ -61,8 +61,27 @@ def test_vowel(): # -------------------------------------------------- def test_vowel_upper(): - """octopus -> an Octopus""" + """octopus -> An Octopus""" for word in vowel_words: out = getoutput(f'{prg} {word.upper()}') - assert out.strip() == template.format('an', word.upper()) + assert out.strip() == template.format('An', word.upper()) + + +# -------------------------------------------------- +def test_starboard(): + """starboard""" + + flag = '--starboard' + out = getoutput(f'{prg} octopus {flag}') + assert out.strip() == 'Ahoy, Captain, an octopus off the starboard bow!' + + +# -------------------------------------------------- +def test_first_char_numeric(): + """e.g. 02test - throw exception""" + + word = '02test' + out = getoutput(f'{prg} {word}') + + assert out.endswith("ValueError") diff --git a/03_picnic/picnic.py b/03_picnic/picnic.py new file mode 100755 index 000000000..e7e7f11e2 --- /dev/null +++ b/03_picnic/picnic.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +""" +Author : wrjt +Date : 2021-08-26 +Purpose: Print a list (possibly sorted) of items to bring to a picnic +""" + +import argparse + + +# -------------------------------------------------- +def get_args(): + """Get command-line arguments""" + + parser = argparse.ArgumentParser( + description='Picnic game', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('item', + metavar='str', + nargs='+', + type=str, + help='Item(s) to bring') + + parser.add_argument('-s', + '--sorted', + help='Sort the items', + action='store_true') + + parser.add_argument('--no_oxford', + help='Don\'t use an Oxford comma', + action='store_true') + + return parser.parse_args() + + +# -------------------------------------------------- +def main(): + """ Main part of the prog """ + + args = get_args() + items = args.item + num_items = len(items) + + if args.sorted: + items.sort() + + if num_items == 1: + picnic_items = items[0] + elif num_items == 2: + picnic_items = ' and '.join(items) + else: + items_except_last = ', '.join(items[:-1]) + final_item = f" and {items[-1]}" if args.no_oxford else f", and {items[-1]}" + picnic_items = items_except_last + final_item + print(f"You are bringing {picnic_items}.") + + +# -------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/03_picnic/test.py b/03_picnic/test.py index 281fab552..fd49b0f49 100755 --- a/03_picnic/test.py +++ b/03_picnic/test.py @@ -66,3 +66,13 @@ def test_more_than_two_sorted(): out = getoutput(f'{prg} {arg} --sorted') expected = ('You are bringing apples, bananas, cherries, and dates.') assert out.strip() == expected + + +# -------------------------------------------------- +def test_no_oxford_comma(): + """more than two items no oxford comma""" + + arg = 'bananas apples dates cherries' + out = getoutput(f'{prg} {arg} --no_oxford') + expected = ('You are bringing bananas, apples, dates and cherries.') + assert out.strip() == expected diff --git a/04_jump_the_five/jump.py b/04_jump_the_five/jump.py new file mode 100755 index 000000000..4b3e86129 --- /dev/null +++ b/04_jump_the_five/jump.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +""" +Author : wrjt +Date : 2021-08-26 +Purpose: Use 'jump-the-five' encoding to unscramble phone numbers +""" + +import argparse + + +# -------------------------------------------------- +def get_args(): + """Get command-line arguments""" + + parser = argparse.ArgumentParser( + description='Jump the Five', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('text', metavar='str', help='Input text') + + return parser.parse_args() + + +# -------------------------------------------------- +def main(): + """Main prog""" + + args = get_args() + encoded_text = args.text + + jump_dict = { + "1": "9", + "2": "8", + "3": "7", + "4": "6", + "5": "0", + "6": "4", + "7": "3", + "8": "2", + "9": "1", + "0": "5" + } + + decoded_text = '' + + for char in encoded_text: + decoded_text += jump_dict.get(char, char) + + print(decoded_text) + + # Other possible solution - use str.translate + # print(encoded_text.translate(str.maketrans(jump_dict))) + + +# -------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/05_howler/howler.py b/05_howler/howler.py new file mode 100755 index 000000000..c859dd12f --- /dev/null +++ b/05_howler/howler.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +""" +Author : wrjt +Date : 2021-08-26 +Purpose: Return a string in UPPERCASE, either from an arg or a file and output + to stdout or a file. +""" + +import argparse +import os + + +# -------------------------------------------------- +def get_args(): + """Get command-line arguments""" + + parser = argparse.ArgumentParser( + description='Howler (upper-cases input)', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('text', metavar='str', help='Input string or file') + + parser.add_argument('-o', + '--outfile', + help='Output filename', + metavar='str', + type=str, + default='') + + args = parser.parse_args() + + # Handle already existing file + if os.path.isfile(args.outfile): + parser.error( + f'--outfile "{args.outfile}" file already exists: please choose another name.' + ) + + return args + + +# -------------------------------------------------- +def get_file_contents(filename: str) -> str: + """ Receive a filename and return its contents """ + + with open(filename, "rt", encoding="utf-8") as infile: + text = infile.read().rstrip() + + return text + + +# -------------------------------------------------- +def write_message(filename: str, contents: str) -> None: + """ Write contents of a message to a file """ + + with open(filename, "wt", encoding="utf-8") as outfile: + outfile.write(contents) + + +# -------------------------------------------------- +def main(): + """ Main prog """ + + args = get_args() + message = args.text + + if os.path.isfile(message): + message = get_file_contents(message) + + howled_message = message.upper() + + if args.outfile: + write_message(args.outfile, howled_message) + else: + print(howled_message) + + +# -------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/05_howler/howler_lowmem.py b/05_howler/howler_lowmem.py new file mode 100755 index 000000000..4e8a4b4b5 --- /dev/null +++ b/05_howler/howler_lowmem.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +""" +Author : wrjt +Date : 2021-08-26 +Purpose: Return a string in UPPERCASE, either from an arg or a file and output + to stdout or a file. +""" + +import argparse +import io +import os +import sys + + +# -------------------------------------------------- +def get_args(): + """Get command-line arguments""" + + parser = argparse.ArgumentParser( + description='Howler (upper-cases input)', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('text', metavar='str', help='Input string or file') + + parser.add_argument('-o', + '--outfile', + help='Output filename', + metavar='str', + type=str, + default='') + + args = parser.parse_args() + + if os.path.isfile(args.text): + # QUERY: Don't we need to close this file? + args.text = open(args.text, 'r', encoding='utf-8') + else: + # using io when unsure if arg is filename or string + # (using \n to look like lines of output from a file + args.text = io.StringIO(args.text + '\n') + + return args + + +# -------------------------------------------------- +def main(): + """ Main prog """ + + args = get_args() + out_fh = open(args.outfile, 'wt', + encoding='utf-8') if args.outfile else sys.stdout + for line in args.text: + out_fh.write(line.upper()) + out_fh.close() + + +# -------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/05_howler/test.py b/05_howler/test.py index 1c04934c8..bcd2f0b48 100755 --- a/05_howler/test.py +++ b/05_howler/test.py @@ -7,7 +7,7 @@ import string from subprocess import getstatusoutput, getoutput -prg = './howler.py' +prg = './howler_lowmem.py' # -------------------------------------------------- diff --git a/06_wc/cat.py b/06_wc/cat.py new file mode 100755 index 000000000..000957103 --- /dev/null +++ b/06_wc/cat.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +""" +Title : Cat emulation +Author : wrjt +Date : 2021-08-26 +Purpose: Make a python version of the command 'cat' +""" + +import argparse +import io +import os +import sys + + +# -------------------------------------------------- +def get_args(): + """Get command-line arguments""" + + parser = argparse.ArgumentParser( + description='Emulate the cat command', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('input', + metavar='str', + nargs='*', + type=str, + default=[], + help='Input file(s)') + + # parser.add_argument('-c', + # '--chars', + # help='Show number of bytes', + # action='store_true') + + # parser.add_argument('-w', + # '--words', + # help='Show number of words', + # action='store_true') + + # parser.add_argument('-l', + # '--lines', + # help='Show number of lines', + # action='store_true') + + return parser.parse_args() + + +# -------------------------------------------------- +# def select_data(first_data: int, second_data: int, third_data: int, +# flags: str) -> None: +# """ Choose what to print, depending on flags """ +# if not flags: +# return f"{first_data:8}{second_data:8}{third_data:8}" +# information = '' +# if 'l' in flags: +# information += f"{first_data:8}" +# if 'w' in flags: +# information += f"{second_data:8}" +# if 'c' in flags: +# information += f"{third_data:8}" +# return information + + +# -------------------------------------------------- +def main() -> None: + """ Main prog here """ + + args = get_args() + + # print(args.input) + + for entry in args.input: + # Check if input is a stream of text (rather than a file) + if entry in (None, '-'): + print(sys.stdin.readline().strip()) + elif os.path.isfile(entry): + with open(entry, "rt", encoding="utf-8") as fh: + print(fh.read()) + else: + print("Unknown input") + + +# -------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/06_wc/test.py b/06_wc/test.py index f9ebc9dea..fb13c6854 100755 --- a/06_wc/test.py +++ b/06_wc/test.py @@ -105,3 +105,79 @@ def test_stdin(): rv, out = getstatusoutput(f'{prg} < {fox}') assert rv == 0 assert out.rstrip() == ' 1 9 45 ' + + +# -------------------------------------------------- +def test_oneline_show_lines_only(): + """Test on fox""" + + rv, out = getstatusoutput(f'{prg} {fox} -l') + assert rv == 0 + assert out.rstrip() == ' 1 ../inputs/fox.txt' + + +# -------------------------------------------------- +def test_oneline_show_words_only(): + """Test on fox""" + + rv, out = getstatusoutput(f'{prg} {fox} -w') + assert rv == 0 + assert out.rstrip() == ' 9 ../inputs/fox.txt' + + +# -------------------------------------------------- +def test_oneline_show_bytes_only(): + """Test on fox""" + + rv, out = getstatusoutput(f'{prg} {fox} -c') + assert rv == 0 + assert out.rstrip() == ' 45 ../inputs/fox.txt' + + +# -------------------------------------------------- +def test_oneline_show_lines_words(): + """Test on fox""" + + rv, out = getstatusoutput(f'{prg} {fox} -wl') + assert rv == 0 + assert out.rstrip() == ' 1 9 ../inputs/fox.txt' + + +# -------------------------------------------------- +def test_oneline_show_lines_chars(): + """Test on fox""" + + rv, out = getstatusoutput(f'{prg} {fox} -lc') + assert rv == 0 + assert out.rstrip() == ' 1 45 ../inputs/fox.txt' + + +# -------------------------------------------------- +def test_oneline_show_words_chars(): + """Test on fox""" + + rv, out = getstatusoutput(f'{prg} {fox} -wc') + assert rv == 0 + assert out.rstrip() == ' 9 45 ../inputs/fox.txt' + + +# -------------------------------------------------- +def test_oneline_show_lines_words_chars(): + """Test on fox""" + + rv, out = getstatusoutput(f'{prg} {fox} -wlc') + assert rv == 0 + assert out.rstrip() == ' 1 9 45 ../inputs/fox.txt' + + +# -------------------------------------------------- +def test_more_lines_chars_only(): + """Test on more than one file""" + + rv, out = getstatusoutput(f'{prg} {fox} {sonnet} -lc') + expected = (' 1 45 ../inputs/fox.txt\n' + ' 17 661 ../inputs/sonnet-29.txt\n' + ' 18 706 total') + assert rv == 0 + assert out.rstrip() == expected + diff --git a/06_wc/test_cat.py b/06_wc/test_cat.py new file mode 100644 index 000000000..a8ef28c4e --- /dev/null +++ b/06_wc/test_cat.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +"""tests for wc.py""" + +import os +import random +import re +import string +from subprocess import getstatusoutput + +prg = './cat.py' +empty = './inputs/empty.txt' +one_line = './inputs/one.txt' +two_lines = './inputs/two.txt' +fox = '../inputs/fox.txt' +sonnet = '../inputs/sonnet-29.txt' + + +# -------------------------------------------------- +def test_exists(): + """exists""" + + assert os.path.isfile(prg) + + +# -------------------------------------------------- +def test_usage(): + """usage""" + + for flag in ['-h', '--help']: + rv, out = getstatusoutput(f'{prg} {flag}') + assert rv == 0 + assert re.match("usage", out, re.IGNORECASE) + + +# -------------------------------------------------- +def random_string(): + """generate a random string""" + + k = random.randint(5, 10) + return ''.join(random.choices(string.ascii_letters + string.digits, k=k)) + + +# -------------------------------------------------- +def test_bad_file(): + """bad_file""" + + bad = random_string() + rv, out = getstatusoutput(f'{prg} {bad}') + assert rv != 0 + assert re.search(f"No such file or directory: '{bad}'", out) + + +# # -------------------------------------------------- +# def test_empty(): +# """Test on empty""" + +# rv, out = getstatusoutput(f'{prg} {empty}') +# assert rv == 0 +# assert out.rstrip() == ' 0 0 0 ./inputs/empty.txt' + + +# # -------------------------------------------------- +# def test_one(): +# """Test on one""" + +# rv, out = getstatusoutput(f'{prg} {one_line}') +# assert rv == 0 +# assert out.rstrip() == ' 1 1 2 ./inputs/one.txt' + + +# # -------------------------------------------------- +# def test_two(): +# """Test on two""" + +# rv, out = getstatusoutput(f'{prg} {two_lines}') +# assert rv == 0 +# assert out.rstrip() == ' 2 2 4 ./inputs/two.txt' + + +# # -------------------------------------------------- +# def test_fox(): +# """Test on fox""" + +# rv, out = getstatusoutput(f'{prg} {fox}') +# assert rv == 0 +# assert out.rstrip() == ' 1 9 45 ../inputs/fox.txt' + + +# # -------------------------------------------------- +# def test_more(): +# """Test on more than one file""" + +# rv, out = getstatusoutput(f'{prg} {fox} {sonnet}') +# expected = (' 1 9 45 ../inputs/fox.txt\n' +# ' 17 118 661 ../inputs/sonnet-29.txt\n' +# ' 18 127 706 total') +# assert rv == 0 +# assert out.rstrip() == expected + + +# # -------------------------------------------------- +# def test_stdin(): +# """Test on stdin""" + +# rv, out = getstatusoutput(f'{prg} < {fox}') +# assert rv == 0 +# assert out.rstrip() == ' 1 9 45 ' + + +# # -------------------------------------------------- +# def test_oneline_show_lines_only(): +# """Test on fox""" + +# rv, out = getstatusoutput(f'{prg} {fox} -l') +# assert rv == 0 +# assert out.rstrip() == ' 1 ../inputs/fox.txt' + + +# # -------------------------------------------------- +# def test_oneline_show_words_only(): +# """Test on fox""" + +# rv, out = getstatusoutput(f'{prg} {fox} -w') +# assert rv == 0 +# assert out.rstrip() == ' 9 ../inputs/fox.txt' + + +# # -------------------------------------------------- +# def test_oneline_show_bytes_only(): +# """Test on fox""" + +# rv, out = getstatusoutput(f'{prg} {fox} -c') +# assert rv == 0 +# assert out.rstrip() == ' 45 ../inputs/fox.txt' + + +# # -------------------------------------------------- +# def test_oneline_show_lines_words(): +# """Test on fox""" + +# rv, out = getstatusoutput(f'{prg} {fox} -wl') +# assert rv == 0 +# assert out.rstrip() == ' 1 9 ../inputs/fox.txt' + + +# # -------------------------------------------------- +# def test_oneline_show_lines_chars(): +# """Test on fox""" + +# rv, out = getstatusoutput(f'{prg} {fox} -lc') +# assert rv == 0 +# assert out.rstrip() == ' 1 45 ../inputs/fox.txt' + + +# # -------------------------------------------------- +# def test_oneline_show_words_chars(): +# """Test on fox""" + +# rv, out = getstatusoutput(f'{prg} {fox} -wc') +# assert rv == 0 +# assert out.rstrip() == ' 9 45 ../inputs/fox.txt' + + +# # -------------------------------------------------- +# def test_oneline_show_lines_words_chars(): +# """Test on fox""" + +# rv, out = getstatusoutput(f'{prg} {fox} -wlc') +# assert rv == 0 +# assert out.rstrip() == ' 1 9 45 ../inputs/fox.txt' + + +# # -------------------------------------------------- +# def test_more_lines_chars_only(): +# """Test on more than one file""" + +# rv, out = getstatusoutput(f'{prg} {fox} {sonnet} -lc') +# expected = (' 1 45 ../inputs/fox.txt\n' +# ' 17 661 ../inputs/sonnet-29.txt\n' +# ' 18 706 total') +# assert rv == 0 +# assert out.rstrip() == expected + diff --git a/06_wc/wc.py b/06_wc/wc.py new file mode 100755 index 000000000..743f93e34 --- /dev/null +++ b/06_wc/wc.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +""" +Author : wrjt +Date : 2021-08-26 +Purpose: Make a python version of the command 'wc' +""" + +import argparse +# import io +# import os +import sys + + +# -------------------------------------------------- +def get_args(): + """Get command-line arguments""" + + parser = argparse.ArgumentParser( + description='Emulate wc (word count)', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('file', + metavar='FILE', + nargs='*', + type=argparse.FileType('rt', encoding='utf-8'), + default=[sys.stdin], + help='Input file(s)') + + parser.add_argument('-c', + '--chars', + help='Show number of bytes', + action='store_true') + + parser.add_argument('-w', + '--words', + help='Show number of words', + action='store_true') + + parser.add_argument('-l', + '--lines', + help='Show number of lines', + action='store_true') + + return parser.parse_args() + + +# -------------------------------------------------- +def select_data(first_data: int, second_data: int, third_data: int, + flags: str) -> None: + """ Choose what to print, depending on flags """ + if not flags: + return f"{first_data:8}{second_data:8}{third_data:8}" + information = '' + if 'l' in flags: + information += f"{first_data:8}" + if 'w' in flags: + information += f"{second_data:8}" + if 'c' in flags: + information += f"{third_data:8}" + return information + + +# -------------------------------------------------- +def main(): + """ Main prog here """ + + args = get_args() + flags = '' + if args.lines: + flags += "l" + if args.words: + flags += "w" + if args.chars: + flags += "c" + + total_lines, total_words, total_bytes = 0, 0, 0 + + for fh in args.file: + num_lines, num_words, num_bytes = 0, 0, 0 + for line in fh: + num_lines += 1 + num_words += len(line.split()) + num_bytes += len(line) + total_lines += num_lines + total_words += num_words + total_bytes += num_bytes + # Check for presence of flags, and print accordingly + print(select_data(num_lines, num_words, num_bytes, flags), fh.name) + fh.close() + + if len(args.file) > 1: + print( + select_data(total_lines, total_words, total_bytes, flags) + + ' total') + + +# -------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/07_gashlycrumb/gashly_interactive.py b/07_gashlycrumb/gashly_interactive.py new file mode 100755 index 000000000..2f4bb5fca --- /dev/null +++ b/07_gashlycrumb/gashly_interactive.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +""" +Author : wrjt +Date : 2021-08-30 +Purpose: Extract a line of text from Gashlycrumb that matches letter(s) +supplied on command line. Takes an optional argument to use a different source +file. +""" + +import argparse +import os + +# import sys +# import string + + +# -------------------------------------------------- +def get_args(): + """Get command-line arguments""" + + parser = argparse.ArgumentParser( + description='Gashlycrumb', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('-f', + '--file', + help='Input file', + metavar='FILE', + type=str, + default='./gashlycrumb.txt') + + return parser.parse_args() + + +# -------------------------------------------------- +def main(): + """ Main program """ + + args = get_args() + + # Extract text line-by-line from file + try: + with open(args.file, 'r', encoding='utf-8') as src_file: + text = src_file.readlines() + except FileNotFoundError: + print("No file matches") + else: + letter_dict = {line[0].upper(): line.rstrip() for line in text} + + while True: + user_choice = input("Please provide a letter [! to quit]: ") + + if user_choice == "!": + print("Bye") + break + + print(letter_dict.get(user_choice.upper(), f'I do not know "{user_choice}".')) + + +# -------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/07_gashlycrumb/gashlycrumb.py b/07_gashlycrumb/gashlycrumb.py new file mode 100755 index 000000000..167a4c4ca --- /dev/null +++ b/07_gashlycrumb/gashlycrumb.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +""" +Author : wrjt +Date : 2021-08-30 +Purpose: Extract a line of text from Gashlycrumb that matches letter(s) +supplied on command line. Takes an optional argument to use a different source +file. +""" + +import argparse +import os +# import sys +# import string + + +# -------------------------------------------------- +def get_args(): + """Get command-line arguments""" + + parser = argparse.ArgumentParser( + description='Gashlycrumb', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('letter', + metavar='letter', + nargs='+', + help='Letter(s)') + + parser.add_argument('-f', + '--file', + help='Input file', + metavar='FILE', + type=str, + default='./gashlycrumb.txt') + + return parser.parse_args() + + +# -------------------------------------------------- +def main(): + """ Main program """ + + args = get_args() + letter_choices = args.letter + + # Extract text line-by-line from file + try: + with open(args.file, 'r', encoding='utf-8') as src_file: + text = src_file.readlines() + # text = [line.rstrip() for line in text] + except FileNotFoundError: + print("No file matches") + # sys.exit() + else: + # Make dict that alphabetizes a dict extracting line for each value + # keys = string.ascii_lowercase + # letter_dict = {k: v for k, v in zip(keys, text)} + letter_dict = {line[0].upper(): line.rstrip() for line in text} + + for letter in letter_choices: + print(letter_dict.get(letter.upper(), f'I do not know "{letter}".')) + + +# -------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/08_apples_and_bananas/apples.py b/08_apples_and_bananas/apples.py new file mode 100755 index 000000000..a784daca8 --- /dev/null +++ b/08_apples_and_bananas/apples.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +""" +Author : wrjt +Date : 2021-08-31 +Purpose: A program which substitutes vowels in text with a supplied vowel +(default: 'a') +""" + +import argparse +import os + + +# -------------------------------------------------- +def get_args(): + """Get command-line arguments""" + + parser = argparse.ArgumentParser( + description='Apples and bananas', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('text', metavar='text', help='Input text or file') + + parser.add_argument('-v', + '--vowel', + help='The vowel to substitute', + metavar='vowel', + type=str, + choices=['a', 'e', 'i', 'o', 'u'], + default='a') + + args = parser.parse_args() + + if os.path.isfile(args.text): + with open(args.text, 'rt', encoding='utf-8') as stream: + args.text = stream.read().rstrip() + + return args + + +# -------------------------------------------------- +def main(): + """ Main prog """ + + args = get_args() + + vowels = 'AEIOUaeiou' + changed_text = '' + + for letter in args.text: + if letter in vowels: + if letter.isupper(): + args.vowel = args.vowel.upper() + letter = letter.replace(letter, args.vowel) + changed_text += letter + + print(changed_text) + + +# -------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/08_apples_and_bananas/apples_list_internal_function.py b/08_apples_and_bananas/apples_list_internal_function.py new file mode 100755 index 000000000..f323a3cb1 --- /dev/null +++ b/08_apples_and_bananas/apples_list_internal_function.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +""" +Author : wrjt +Date : 2021-08-31 +Purpose: A program which substitutes vowels in text with a supplied vowel +(default: 'a') +""" + +import argparse +import os + + +# -------------------------------------------------- +def get_args(): + """Get command-line arguments""" + + parser = argparse.ArgumentParser( + description='Apples and bananas', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('text', metavar='text', help='Input text or file') + + parser.add_argument('-v', + '--vowel', + help='The vowel to substitute', + metavar='vowel', + type=str, + choices=list('aeiou'), + default='a') + + args = parser.parse_args() + + if os.path.isfile(args.text): + with open(args.text, 'rt', encoding='utf-8') as stream: + args.text = stream.read().rstrip() + + return args + + +# -------------------------------------------------- +def main(): + """ Main prog """ + + args = get_args() + text = args.text + vowel = args.vowel + + def new_char(c): + """ Returns character depending on conditions """ + # NOTE: This funcation *can* access variables inside main()!!! (e.g. vowel) + return vowel if c in 'aeiou' else vowel.upper() if c in 'AEIOU' else c + + text = ''.join([new_char(letter) for letter in text]) + print(text) + + +# -------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/08_apples_and_bananas/apples_map.py b/08_apples_and_bananas/apples_map.py new file mode 100755 index 000000000..ed56afc1d --- /dev/null +++ b/08_apples_and_bananas/apples_map.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +""" +Author : wrjt +Date : 2021-08-31 +Purpose: A program which substitutes vowels in text with a supplied vowel +(default: 'a') +""" + +import argparse +import os + + +# -------------------------------------------------- +def get_args(): + """Get command-line arguments""" + + parser = argparse.ArgumentParser( + description='Apples and bananas', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('text', metavar='text', help='Input text or file') + + parser.add_argument('-v', + '--vowel', + help='The vowel to substitute', + metavar='vowel', + type=str, + choices=list('aeiou'), + default='a') + + args = parser.parse_args() + + if os.path.isfile(args.text): + with open(args.text, 'rt', encoding='utf-8') as stream: + args.text = stream.read().rstrip() + + return args + + +# -------------------------------------------------- +def main(): + """ Main prog """ + + args = get_args() + vowel = args.vowel + # text = map( + # lambda c: vowel if c in 'aeiou' else vowel.upper() + # if c in 'AEIOU' else c, args.text) + # print(''.join(text)) + + # Alternatively can use with the new_char function + def new_char(c): + """ Returns character depending on conditions """ + return vowel if c in 'aeiou' else vowel.upper() if c in 'AEIOU' else c + + print(''.join(map(new_char, args.text))) + + +# -------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/08_apples_and_bananas/apples_regex.py b/08_apples_and_bananas/apples_regex.py new file mode 100755 index 000000000..cd0465f36 --- /dev/null +++ b/08_apples_and_bananas/apples_regex.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +""" +Author : wrjt +Date : 2021-08-31 +Purpose: A program which substitutes vowels in text with a supplied vowel +(default: 'a') +""" + +import argparse +import re +import os + + +# -------------------------------------------------- +def get_args(): + """Get command-line arguments""" + + parser = argparse.ArgumentParser( + description='Apples and bananas', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('text', metavar='text', help='Input text or file') + + parser.add_argument('-v', + '--vowel', + help='The vowel to substitute', + metavar='vowel', + type=str, + choices=list('aeiou'), + default='a') + + args = parser.parse_args() + + if os.path.isfile(args.text): + with open(args.text, 'rt', encoding='utf-8') as stream: + args.text = stream.read().rstrip() + + return args + + +# -------------------------------------------------- +def main(): + """ Main prog """ + + args = get_args() + text = args.text + vowel = args.vowel + + # Added '+' to match one or more vowels and then collapse matches longer than one + text = re.sub('[aeiou]+', vowel, text) + text = re.sub('[AEIOU]+', vowel.upper(), text) + + print(text) + + +# -------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/08_apples_and_bananas/apples_translate.py b/08_apples_and_bananas/apples_translate.py new file mode 100755 index 000000000..7fb2374e5 --- /dev/null +++ b/08_apples_and_bananas/apples_translate.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +""" +Author : wrjt +Date : 2021-08-31 +Purpose: A program which substitutes vowels in text with a supplied vowel +(default: 'a') +""" + +import argparse +import os + + +# -------------------------------------------------- +def get_args(): + """Get command-line arguments""" + + parser = argparse.ArgumentParser( + description='Apples and bananas', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('text', metavar='text', help='Input text or file') + + parser.add_argument('-v', + '--vowel', + help='The vowel to substitute', + metavar='vowel', + type=str, + choices=list('aeiou'), + default='a') + + args = parser.parse_args() + + if os.path.isfile(args.text): + with open(args.text, 'rt', encoding='utf-8') as stream: + args.text = stream.read() + + return args + + +# -------------------------------------------------- +def main(): + """ Main prog """ + + args = get_args() + + # vowel_trans_table = {'A': args.vowel.upper(), + # 'E': args.vowel.upper(), + # 'I': args.vowel.upper(), + # 'O': args.vowel.upper(), + # 'U': args.vowel.upper(), + # 'a': args.vowel, + # 'e': args.vowel, + # 'i': args.vowel, + # 'o': args.vowel, + # 'u': args.vowel} + + # One line version below (won't need maketrans call below) + trans = str.maketrans('aeiouAEIOU', + args.vowel * 5 + args.vowel.upper() * 5) + + # changed_text = args.text.translate(str.maketrans(vowel_trans_table)) + + print(args.text.translate(trans)) + + +# -------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/09_abuse/abuse.py b/09_abuse/abuse.py new file mode 100755 index 000000000..3e3675e32 --- /dev/null +++ b/09_abuse/abuse.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +""" +Author : wrjt +Date : 2021-09-01 +Purpose: A file that fires out random insults taken from a corpus of nouns and +adjectives +""" + +import argparse +import random +import os + + +# -------------------------------------------------- +def get_args(): + """Get command-line arguments""" + + parser = argparse.ArgumentParser( + description='Heap abuse', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('-a', + '--adjectives', + help='Number of adjectives', + metavar='adjectives', + type=int, + default=2) + + parser.add_argument('-n', + '--number', + help='Number of insults', + metavar='insults', + type=int, + default=1) + + parser.add_argument('-s', + '--seed', + help='Random seed', + metavar='seed', + type=int, + default=None) + + args = parser.parse_args() + + if args.adjectives < 1: + parser.error(f'--adjectives "{args.adjectives}" must be > 0') + + if args.number < 1: + parser.error(f'--number "{args.number}" must be > 0') + + return args + + +def read_file(filename: str) -> str: + """ Read a file and return its contents in a list, raise FileNotFoundError + if there's an issue """ + if os.path.isfile(filename): + with open(filename, 'rt', encoding='utf-8') as f: + return f.read().split() + else: + raise FileNotFoundError(f"{filename} does not exist.") + + +# -------------------------------------------------- +def main(): + """ Main prog """ + + args = get_args() + random.seed(args.seed) + + adjectives = read_file('./adjectives.txt') + + nouns = read_file('./nouns.txt') + + for _ in range(args.number): + # Using random.sample as the choices are unique + chosen_adjs = ', '.join(random.sample(adjectives, args.adjectives)) + print(f"You {chosen_adjs} {random.choice(nouns)}!") + + +# -------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/09_abuse/adjectives.txt b/09_abuse/adjectives.txt new file mode 100644 index 000000000..637d9944d --- /dev/null +++ b/09_abuse/adjectives.txt @@ -0,0 +1 @@ +bankrupt base caterwauling corrupt cullionly detestable dishonest false filthsome filthy foolish foul gross heedless indistinguishable infected insatiate irksome lascivious lecherous loathsome lubbery old peevish rascaly rotten ruinous scurilous scurvy slanderous sodden-witted thin-faced toad-spotted unmannered vile wall-eyed diff --git a/09_abuse/nouns.txt b/09_abuse/nouns.txt new file mode 100644 index 000000000..8bf7d6884 --- /dev/null +++ b/09_abuse/nouns.txt @@ -0,0 +1 @@ +Judas Satan ape ass barbermonger beggar block boy braggart butt carbuncle coward coxcomb cur dandy degenerate fiend fishmonger fool gull harpy jack jolthead knave liar lunatic maw milksop minion ratcatcher recreant rogue scold slave swine traitor varlet villain worm diff --git a/09_abuse/test.py b/09_abuse/test.py index ff31fc90a..f325ad87e 100755 --- a/09_abuse/test.py +++ b/09_abuse/test.py @@ -90,11 +90,10 @@ def test_01(): def test_02(): """test""" - out = getoutput(f'{prg} --seed 2') + out = getoutput(f'{prg} --seed 2 --number 2') expected = """ You corrupt, detestable beggar! You peevish, foolish gull! -You insatiate, heedless worm! """.strip() assert out.strip() == expected diff --git a/10_telephone/telephone.py b/10_telephone/telephone.py new file mode 100755 index 000000000..3fe42f67d --- /dev/null +++ b/10_telephone/telephone.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +""" +Author : wrjt +Date : 2021-09-02 +Purpose: A game of telephone that randomly mutates text +""" + +import argparse +import os +import random +import string +# import sys + + +# -------------------------------------------------- +def get_args(): + """Get command-line arguments""" + + parser = argparse.ArgumentParser( + description='Telephone', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('text', + metavar='text', + help='Input text or file') + + parser.add_argument('-s', + '--seed', + help='Random Seed', + metavar='seed', + type=int, + default=None) + + parser.add_argument('-m', + '--mutations', + help='Percent mutations', + metavar='mutations', + type=float, + default=0.1) + + parser.add_argument('-o', + '--output', + help='Output filename', + metavar='str', + type=str, + default='') + + parser.add_argument('-c', + '--characters', + help='Only use characters to replace', + action='store_true') + + args = parser.parse_args() + + # Handle out of bounds error for mutations + if not 0 <= args.mutations <= 1: + parser.error(f'--mutations "{args.mutations}" must be between 0 and 1') + + # Handle if text is a filename + if os.path.isfile(args.text): + with open(args.text, "rt", encoding="utf-8") as f: + args.text = f.read().rstrip() + + # Handle already existing file + if os.path.isfile(args.output): + parser.error( + f'--output "{args.output}" file already exists: please choose another name.' + ) + + return args + + +# -------------------------------------------------- +def main(): + """ Main prog """ + + args = get_args() + text = args.text + random.seed(args.seed) + num_mutations = round(len(text) * args.mutations) + + # Get all letters (and punctuation) for mutation + if args.characters: + alpha = string.ascii_letters + else: + alpha = ''.join(sorted(string.ascii_letters + string.punctuation)) + + # Choose indices to change in text + indexes = random.sample(range(len(text)), num_mutations) + + # new_text = '' + # # Below works, but has different results from book version - why? + # for idx, char in enumerate(text): + # if idx in indexes: + # # Remove char from alpha so it cannot be put back into new string + # new_text += random.choice(alpha.replace(char, '')) + # else: + # new_text += char + + # # # BOOK ANSWER + # # does the process far quicker than my one (10 times!) + # new_text = text + # for i in indexes: + # # Remove char from alpha so it cannot be put back into new string + # new_char = random.choice(alpha.replace(new_text[i], '')) + # new_text = new_text[:i] + new_char + new_text[i + 1:] + + # # BOOK ANSWER - LIST APPROACH + # + new_text = list(text) + for i in indexes: + new_text[i] = random.choice(alpha.replace(new_text[i], '')) + + new_text = ''.join(new_text) + + # Extra space before 2nd ':' to line up with first one. + message = f'You said: "{text}"\nI heard : "{new_text}"' + + if args.output: + with open(args.output, 'wt', encoding='utf-8') as outfile: + outfile.write(message + '\n') + else: + print(message) + + +# -------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/10_telephone/test.py b/10_telephone/test.py index ea263af1b..47925382a 100755 --- a/10_telephone/test.py +++ b/10_telephone/test.py @@ -116,8 +116,46 @@ def test_fox_file_s2_m6(): assert out.rstrip() == f'You said: "{txt}"\nI heard : "{expected}"' +# -------------------------------------------------- +def test_text_outfile(): + """Test STDIN/outfile""" + + out_file = random_string() + if os.path.isfile(out_file): + os.remove(out_file) + + try: + rv, out = getstatusoutput(f'{prg} {out_flag()} {out_file} "foo bar baz"') + assert rv == 0 + # assert out.strip() == '' + assert os.path.isfile(out_file) + finally: + if os.path.isfile(out_file): + os.remove(out_file) + + +# -------------------------------------------------- +def test_for_char_flag(): + """test""" + + txt = open(now).read().rstrip() + rv, out = getstatusoutput(f'{prg} -s 2 -m .4 -c "{txt}"') + assert rv == 0 + expected = """ + Nob ic HheDriqe KorkallbgHojJmenxtS clleGtCutheuaidypyHthe laHty. + """.strip() + assert out.rstrip() == f'You said: "{txt}"\nI heard : "{expected}"' + + # -------------------------------------------------- def random_string(): """generate a random filename""" return ''.join(random.choices(string.ascii_lowercase + string.digits, k=5)) + + +# -------------------------------------------------- +def out_flag(): + """Either -o or --output""" + + return '-o' if random.randint(0, 1) else '--output' diff --git a/11_bottles_of_beer/bottles.py b/11_bottles_of_beer/bottles.py new file mode 100755 index 000000000..f26a0de34 --- /dev/null +++ b/11_bottles_of_beer/bottles.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +""" +Author : wrjt +Date : 2021-09-03 +Purpose: To generate "bottle of beer" song depending on the value of n +(default: 10) +""" + +import argparse +# import textwrap as tw + + +# -------------------------------------------------- +def get_args(): + """Get command-line arguments""" + + parser = argparse.ArgumentParser( + description='Bottles of beer song', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('-n', + '--num', + help='How many bottles', + metavar='number', + type=int, + default=10) + + args = parser.parse_args() + + # Handle out of bounds error for number of bottles + if args.num < 1: + parser.error(f'--num "{args.num}" must be greater than 0') + + return args + + +# -------------------------------------------------- +# def verse(bottle: int) -> str: +# """ Sing a verse """ +# if bottle == 2: +# verse = tw.dedent("""\ +# 2 bottles of beer on the wall, +# 2 bottles of beer, +# Take one down, pass it around, +# 1 bottle of beer on the wall! +# """) +# elif bottle == 1: +# verse = tw.dedent("""\ +# 1 bottle of beer on the wall, +# 1 bottle of beer, +# Take one down, pass it around, +# No more bottles of beer on the wall!""") +# else: +# verse = tw.dedent(f"""\ +# {bottle} bottles of beer on the wall, +# {bottle} bottles of beer, +# Take one down, pass it around, +# {bottle - 1} bottles of beer on the wall! +# """) + +# return verse + + +def verse(bottle): + """ Sing a verse """ + + current_bottle = '1 bottle' if bottle == 1 else f'{bottle} bottles' + next_bottle = 'No more bottles' if bottle == 1 else f'{bottle - 1} bottle' if bottle == 2 else f'{bottle - 1} bottles' + + return '\n'.join([ + f'{current_bottle} of beer on the wall,', + f'{current_bottle} of beer,', + f'Take one down, pass it around,', + f'{next_bottle} of beer on the wall!' + ]) + + +# -------------------------------------------------- +def main(): + """ Main Prog """ + + args = get_args() + + # for i in range(args.num, 0, -1): + # print(verse(i)) + + print('\n\n'.join(map(verse, range(args.num, 0, -1)))) + + # # Alternative way + # for n in range(args.num, 0, -1): + # print(verse(n), end='\n' * (2 if n > 1 else 1)) + + +def test_verse(): + """ Test verse """ + + last_verse = verse(1) + assert last_verse == '\n'.join([ + '1 bottle of beer on the wall,', '1 bottle of beer,', + 'Take one down, pass it around,', + 'No more bottles of beer on the wall!' + ]) + + two_bottles = verse(2) + assert two_bottles == '\n'.join([ + '2 bottles of beer on the wall,', '2 bottles of beer,', + 'Take one down, pass it around,', '1 bottle of beer on the wall!' + ]) + + +# -------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/12_ransom/ransom.py b/12_ransom/ransom.py new file mode 100755 index 000000000..6a871f5a2 --- /dev/null +++ b/12_ransom/ransom.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +""" +Title : Ransom Note +Author : wrjt +Date : 2021-09-04 +Purpose: Encode some text with random capitaization +""" + +import argparse +import os +import random + + +def get_args(): + """Get command-line arguments""" + + parser = argparse.ArgumentParser( + description='Ransom Note', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('text', + metavar='text', + help='Input text or file') + + parser.add_argument('-s', + '--seed', + help='Random Seed', + metavar='seed', + type=int, + default=None) + + args = parser.parse_args() + + # Handle if text is a filename + if os.path.isfile(args.text): + with open(args.text, "rt", encoding="utf-8") as f: + args.text = f.read().rstrip() + + return args + + +def main(): + """ Main Prog """ + + args = get_args() + random.seed(args.seed) + + ransom_note = [choose(char) for char in args.text] + # ransom_note = map(choose, args.text) + + print(''.join(ransom_note)) + + +def choose(char: str) -> str: + """ Change case of a character randomly """ + + # # My version returned different results than his - why? + # return random.choice([char.upper(), char.lower()]) + return char.upper() if random.choice([0, 1]) else char.lower() + + +def test_choose(): + """ Test choose() """ + + state = random.getstate() + random.seed(1) + assert choose('a') == 'a' + assert choose('b') == 'b' + assert choose('c') == 'C' + assert choose('d') == 'd' + random.setstate(state) + + +# -------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/13_twelve_days/twelve_days.py b/13_twelve_days/twelve_days.py new file mode 100755 index 000000000..91da23977 --- /dev/null +++ b/13_twelve_days/twelve_days.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +""" +Title : Twelve Days Of Christmas +Author : wrjt +Date : 2021-09-04 +Purpose: Rock the Casbah +""" + +import argparse +import os + + +# -------------------------------------------------- +def get_args(): + """Get command-line arguments""" + + parser = argparse.ArgumentParser( + description='Twelve Days Of Christmas', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('-n', + '--num', + help='Number of days to sing', + metavar='days', + type=int, + default=12) + + parser.add_argument('-o', + '--output', + help='Output filename', + metavar='str', + type=str, + default='') + + args = parser.parse_args() + + # Handle out of bounds error for number of days + if args.num not in range(1, 13): + parser.error(f'--num "{args.num}" must be between 1 and 12') + + # Handle already existing file + if os.path.isfile(args.output): + parser.error( + f'--output "{args.output}" file already exists: please choose another name.' + ) + + return args + + +# -------------------------------------------------- +def main(): + """ Main Prog """ + + args = get_args() + + # verses = '\n\n'.join(map(verse, range(1, args.num + 1))) + verses = '\n\n'.join([verse(n) for n in range(1, args.num + 1)]) + + if args.output: + with open(args.output, 'wt', encoding='utf-8') as outfile: + outfile.write(verses + '\n') + else: + print(verses) + + +def verse(day: int) -> str: + """ Return a verse of 12 Days of Xmas """ + + ordinals = [ + 'first', 'second', 'third', 'fourth', 'fifth', 'sixth', 'seventh', + 'eighth', 'ninth', 'tenth', 'eleventh', 'twelfth' + ] + + gifts = [ + 'A partridge in a pear tree.', 'Two turtle doves,', + 'Three French hens,', 'Four calling birds,', 'Five gold rings,', + 'Six geese a laying,', 'Seven swans a swimming,', + 'Eight maids a milking,', 'Nine ladies dancing,', + 'Ten lords a leaping,', 'Eleven pipers piping,', + 'Twelve drummers drumming,' + ] + + lines = [ + f"On the {ordinals[day - 1]} day of Christmas,", + "My true love gave to me," + ] + + for gift in reversed(gifts[1:day]): + lines.append(gift) + + if day > 1: + lines.append("And " + gifts[0].lower()) + else: + lines.append(gifts[0]) + + # # Book version + # lines.extend(reversed(gifts[:day])) + + # if day > 1: + # lines[-1] = 'And '+ lines[-1].lower() + + return '\n'.join(lines) + + +def test_verse(): + """ Test verse() """ + + assert verse(1) == '\n'.join([ + 'On the first day of Christmas,', 'My true love gave to me,', + 'A partridge in a pear tree.' + ]) + + assert verse(2) == '\n'.join([ + 'On the second day of Christmas,', 'My true love gave to me,', + 'Two turtle doves,', 'And a partridge in a pear tree.' + ]) + + assert verse(3) == '\n'.join([ + 'On the third day of Christmas,', 'My true love gave to me,', + 'Three French hens,', 'Two turtle doves,', + 'And a partridge in a pear tree.' + ]) + + +# -------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/14_rhymer/rhymer.py b/14_rhymer/rhymer.py new file mode 100755 index 000000000..3ba4bf8a1 --- /dev/null +++ b/14_rhymer/rhymer.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +""" +Title : Rhymer +Author : wrjt +Date : 2021-09-04 +Purpose: Find rhyming words using regexes +""" + +import argparse +import re +import string + + +# -------------------------------------------------- +def get_args(): + """Get command-line arguments""" + + parser = argparse.ArgumentParser( + description='Make rhyming "words"', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('word', metavar='word', help='A word to rhyme') + + args = parser.parse_args() + + # if len(args.word) != 1: + # parser.error("Please only enter one word.") + + return args + + +# -------------------------------------------------- +def main(): + """ Main prog """ + + args = get_args() + + consonants = list('bcdfghjklmnpqrstvwxyz') + clusters = """\ + bl br ch cl cr dr fl fr gl gr pl pr sc sh sk sl sm sn sp st sw th + tr tw thw wh wr sch scr shr sph spl spr squ str thr""".split() + prefixes = sorted(consonants + clusters) + + remove_me, stem = stemmer(args.word) + + if stem: + output = '\n'.join([p + stem for p in prefixes if p != remove_me]) + else: + output = f'Cannot rhyme "{args.word}"' + + print(output) + + +def stemmer(word: str) -> tuple: + """ Return leading consonants (if any), and 'stem' of the word """ + + letters, vowels = string.ascii_lowercase, 'aeiou' + consonants = ''.join([c for c in letters if c not in vowels]) + # consonants = ''.join(filter(lambda c: c not in vowels, letters)) + word = word.lower() + + # # Alternative using re.compile and findall (which returns a list) + # pattern_regex = re.compile( + # rf'''( + # ([{consonants}]+)? # Capture one or more (optional) + # ([{vowels}]+) # Capture at least one vowel + # (.*) # Capture zero or more of anything else + # )''', re.VERBOSE) + + # match = pattern_regex.findall(word) + + # if match: + # p1 = match[0][1] or '' + # p2 = match[0][2] or '' + # p3 = match[0][3] or '' + # return (p1, p2 + p3) + # else: + # return (word, '') + + pattern = ( + f'([{consonants}]+)?' # Capture one or more (optional) + f'([{vowels}])' # Capture at least one vowel + '(.*)' # Capture zero of more of anything else + ) + match = re.match(pattern, word) + + if match: + p1 = match.group(1) or '' + p2 = match.group(2) or '' + p3 = match.group(3) or '' + return (p1, p2 + p3) + else: + return (word, '') + + +def test_stemmer(): + """ Test stemmer() """ + assert stemmer('') == ('', '') + assert stemmer('cake') == ('c', 'ake') + assert stemmer('chair') == ('ch', 'air') + assert stemmer('APPLE') == ('', 'apple') + assert stemmer('RDNZL') == ('rdnzl', '') + assert stemmer('123') == ('123', '') + + +# -------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/15_kentucky_friar/friar.py b/15_kentucky_friar/friar.py new file mode 100755 index 000000000..ea38c76d2 --- /dev/null +++ b/15_kentucky_friar/friar.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +""" +Title : Kentucky Friar Accent +Author : wrjt +Date : 2021-09-05 +Purpose: Rock the Casbah +""" + +import argparse +import os +import re + + +# -------------------------------------------------- +def get_args(): + """Get command-line arguments""" + + parser = argparse.ArgumentParser( + description='Southern fry text', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('text', metavar='text', help='Input text or file') + + args = parser.parse_args() + + # Handle if text is a filename + if os.path.isfile(args.text): + with open(args.text, "rt", encoding="utf-8") as f: + args.text = f.read().rstrip() + + return args + + +# -------------------------------------------------- +def main(): + """ Main Prog """ + + args = get_args() + + # Saves a compile every loop and is more readable + splitter = re.compile(r'(\W+)') + for line in args.text.splitlines(): + words = [fry(word) for word in re.split(splitter, line.rstrip())] + print(''.join(words)) + + +def fry(word: str) -> str: + """ 'Fry' a word """ + + # re.match starts at the beginning of supplied string + you_match = re.match(r'([yY])ou$', word) + # re.search looks anywhere in the supplied string + ing_match = re.search(r'(.+)ing$', word) + if you_match: + return you_match.group(1) + "'all" + elif ing_match: + # Check for vowels before 'ing', if present 'fry' it + prefix = ing_match.group(1) + if re.search(r'[aeoiuy]', prefix, re.IGNORECASE): + return prefix + "in'" + + # # Non-regex version (passes all unit tests) + # if word.lower() == "you": + # return word[0] + "'all" + # elif word.lower().endswith('ing'): + # # Can replace for loop with: + # # if any(map(lambda c: c.lower() in 'aeiouy', word[:-3])): + # # return word[:-1] + "'" + # for c in word[:-3]: + # if c.lower() in 'aeiouy': + # return word[:-1] + "'" + + # Return word if not 'you' or ending in 'ing' (or one syllable 'ing') + return word + + +def test_fry(): + """ Test fry() """ + assert fry('you') == "y'all" + assert fry('You') == "Y'all" + assert fry('your') == "your" + assert fry('fishing') == "fishin'" + assert fry('Aching') == "Achin'" + assert fry('swing') == "swing" + assert fry('trying') == "tryin'" + assert fry('and') == "and" + + +# -------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/16_scrambler/scrambler.py b/16_scrambler/scrambler.py new file mode 100755 index 000000000..779875303 --- /dev/null +++ b/16_scrambler/scrambler.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +""" +Title : Scrambler +Author : wrjt +Date : 2021-09-06 +Purpose: Scrambles the middle letters in words +""" + +import argparse +import os +import random +import re + + +# -------------------------------------------------- +def get_args(): + """Get command-line arguments""" + + parser = argparse.ArgumentParser( + description='Scramble the letters or words', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('text', metavar='text', help='Input text or file') + + parser.add_argument('-s', + '--seed', + help='Random seed', + metavar='seed', + type=int, + default=None) + + args = parser.parse_args() + + # Handle if text is a filename + if os.path.isfile(args.text): + with open(args.text, "rt", encoding="utf-8") as f: + args.text = f.read().rstrip() + + return args + + +# -------------------------------------------------- +def main(): + """ Main prog """ + + args = get_args() + random.seed(args.seed) + + # Regex to capture words with apostrophes (e.g. "don't") + splitter = re.compile( + r""" + ([a-zA-Z] # Capture any letter + (?:[a-zA-Z']* # Account for any letter plus apostrophe + [a-zA-Z])?) # Followed by any letter (optional group) + """, re.VERBOSE) + + for line in args.text.splitlines(): + print(''.join([scramble(word) for word in splitter.split(line)])) + + +def scramble(word: str) -> str: + """ For words longer than 3 chars, scramble middle letters """ + + if len(word) > 3 and re.match(r'\w+', word): + middle = list(word[1:-1]) + random.shuffle(middle) + word = word[0] + ''.join(middle) + word[-1] + + return word + + +def test_scramble(): + """ Test scramble() """ + state = random.getstate() + random.seed(1) + assert scramble("a") == "a" + assert scramble("ab") == "ab" + assert scramble("abc") == "abc" + assert scramble("abcd") == "acbd" + assert scramble("abcde") == "acbde" + assert scramble("abcdef") == "aecbdf" + assert scramble("abcde'f") == "abcd'ef" + random.setstate(state) + + +# -------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/17_mad_libs/mad.py b/17_mad_libs/mad.py new file mode 100755 index 000000000..17f6f1af6 --- /dev/null +++ b/17_mad_libs/mad.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +""" +Title : Mad Libs +Author : wrjt +Date : 2021-09-07 +Purpose: Replace parts of speech in a string with user input +""" + +import argparse +import os +import re +import sys + + +# -------------------------------------------------- +def get_args(): + """Get command-line arguments""" + + parser = argparse.ArgumentParser( + description='Mad Libs', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('file', metavar='file', help='Input file') + + parser.add_argument('-i', + '--inputs', + help='Inputs (for testing)', + metavar='input', + type=str, + default='None', + nargs='*') + + args = parser.parse_args() + + # Handle file not present + if not os.path.isfile(args.file): + parser.error(f"No such file or directory: '{args.file}'") + + return args + + +# -------------------------------------------------- +def main(): + """ Main Prog """ + + args = get_args() + inputs = args.inputs + with open(args.file, 'rt', encoding='utf-8') as f: + text = f.read().rstrip() + + # Find part of speech (denoted by <>), return a tuple (placeholder, part) + part_of_speech_regex = re.compile(r'(<([^<>]+)>)') + + # Assume one line of text + matches = re.findall(part_of_speech_regex, text) + if not matches: + # Spit out an error and exit if no placeholders found in text + sys.exit(f'"{args.file}" has no placeholders.') + # Get user inputs + tmpl = 'Give me {} {}: ' + for placeholder, part in matches: + article = 'an' if part[0].lower() in 'aeiou' else 'a' + answer = inputs.pop(0) if inputs else input(tmpl.format(article, part)) + text = re.sub(placeholder, answer, text, count=1) + + print(text) + + +# -------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/17_mad_libs/mad_noregex.py b/17_mad_libs/mad_noregex.py new file mode 100755 index 000000000..2d0c49cf4 --- /dev/null +++ b/17_mad_libs/mad_noregex.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +""" +Title : Mad Libs +Author : wrjt +Date : 2021-09-07 +Purpose: Replace parts of speech in a string with user input +""" + +import argparse +import os +import sys + + +# -------------------------------------------------- +def get_args(): + """Get command-line arguments""" + + parser = argparse.ArgumentParser( + description='Mad Libs', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('file', metavar='file', help='Input file') + + parser.add_argument('-i', + '--inputs', + help='Inputs (for testing)', + metavar='input', + type=str, + default='None', + nargs='*') + + args = parser.parse_args() + + # Handle file not present + if not os.path.isfile(args.file): + parser.error(f"No such file or directory: '{args.file}'") + + return args + + +# -------------------------------------------------- +def main(): + """ Main Prog """ + + args = get_args() + inputs = args.inputs + with open(args.file, 'rt', encoding='utf-8') as f: + text = f.read().rstrip() + + tmpl = 'Give me {} {}: ' + placeholders = False + + # Get user inputs + while find_brackets(text) is not None: + left, right = find_brackets(text) + article = 'an' if text[left + 1].lower() in 'aeiou' else 'a' + if inputs: + word = inputs.pop(0) + else: + word = input(tmpl.format(article, text[left + 1:right])) + text = text[:left] + word + text[right + 1:] + placeholders = True + + if placeholders: + print(text) + else: + # Spit out an error and exit if no placeholders found in text + sys.exit(f'"{args.file}" has no placeholders.') + + +def find_brackets(text: str) -> tuple: + """ + Find position of angled brackets in a string and return as a tuple (start, end) + + ISSUES: + - Will return a value for "" which isn't what we want + (Maybe can resolve this by checking returned str matches certain + values - done outside of this function though) + - Will let mismatched brackets through (e.g. "<") + (again, resolvable outside of function) + """ + start, end = text.find('<'), text.find('>') + gap = end - start + # str.find will return -1 if substr not found + if any([gap == 1, start == -1, end == -1]): + return None + return (start, end) + + +def test_find_brackets(): + """ Test find_brackets() """ + assert find_brackets('') is None + assert find_brackets('<>') is None + assert find_brackets('') == (0, 2) + assert find_brackets('foo baz') == (4, 8) + assert find_brackets('foo bar> baz') is None + assert find_brackets('foo baz') == None + + +# -------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/18_gematria/gematria.py b/18_gematria/gematria.py new file mode 100755 index 000000000..f6895e489 --- /dev/null +++ b/18_gematria/gematria.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +""" +Title : gematria.py +Author : wrjt +Date : 2021-09-09 +Purpose: numerically encode a word using values for characters + (using ASCII values) +""" + +import argparse +import os +import re + + +# -------------------------------------------------- +def get_args(): + """Get command-line arguments""" + + parser = argparse.ArgumentParser( + description='Gematria', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('text', + metavar='text', + help='Input text or file') + + args = parser.parse_args() + + # Handle if text is a filename + if os.path.isfile(args.text): + with open(args.text, "rt", encoding="utf-8") as f: + args.text = f.read().rstrip() + + return args + + +# -------------------------------------------------- +def main(): + """Make a jazz noise here""" + + args = get_args() + + # Book version + # for line in args.text.splitlines(): + # print(' '.join(map(word2num, line.split()))) + + for line in args.text.splitlines(): + print(' '.join([word2num(word) for word in line.split()])) + + +def word2num(word: str) -> str: + """ Convert a word to the sum of its ascii values """ + + # # Book version: + # return str(sum(map(ord, re.sub(r'[^A-Za-z0-9]', '', word)))) + + # Remove non-letters and numbers + word = re.sub(re.compile(r'[^A-Za-z0-9]'), '', word) + return str(sum(ord(c) for c in word)) + + +def test_word2num(): + """ Test word2num() """ + assert word2num("a") == "97" + assert word2num("abc") == "294" + assert word2num("ab'c") == "294" + assert word2num("4a-b'c,") == "346" + + +# -------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/19_wod/unit2.py b/19_wod/unit2.py new file mode 100644 index 000000000..89c71656b --- /dev/null +++ b/19_wod/unit2.py @@ -0,0 +1,28 @@ +import io +from wod import read_csv + + +# -------------------------------------------------- +def test_read_csv(): + """Test read_csv""" + + good = io.StringIO('exercise,reps\nBurpees,20-50\nSitups,40-100') + assert read_csv(good, ',') == [('Burpees', 20, 50), ('Situps', 40, 100)] + + no_data = io.StringIO('') + assert read_csv(no_data, ',') == [] + + headers_only = io.StringIO('exercise,reps\n') + assert read_csv(headers_only, ',') == [] + + # bad_headers = io.StringIO('Exercise,Reps\nBurpees,20-50\nSitups,40-100') + # assert read_csv(bad_headers) == [] + + bad_numbers = io.StringIO('exercise,reps\nBurpees,20-50\nSitups,forty-100') + assert read_csv(bad_numbers, ',') == [('Burpees', 20, 50)] + + no_dash = io.StringIO('exercise,reps\nBurpees,20\nSitups,40-100') + assert read_csv(no_dash, ',') == [('Situps', 40, 100)] + + tabs = io.StringIO('exercise\treps\nBurpees\t20-50\nSitups\t40-100') + assert read_csv(tabs, '\t') == [('Burpees', 20, 50), ('Situps', 40, 100)] diff --git a/19_wod/wod.py b/19_wod/wod.py new file mode 100755 index 000000000..366e5da96 --- /dev/null +++ b/19_wod/wod.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +""" +Title : Workout of the Day +Author : wrjt +Date : 2021-09-10 +Purpose: Create a random daily workout from choices in a csv file +""" + +import argparse +import csv +import os +import random +import re +import sys + +from tabulate import tabulate + + +# -------------------------------------------------- +def get_args(): + """Get command-line arguments""" + + parser = argparse.ArgumentParser( + description='Create Workout Of (the) Day (WOD)', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('-f', + '--file', + help='CSV input file of exercises', + metavar='FILE', + type=str, + default='./inputs/exercises.csv') + + parser.add_argument('-s', + '--seed', + help='Random seed', + metavar='seed', + type=int, + default=None) + + parser.add_argument('-n', + '--num', + help='Number of exercises', + metavar='exercises', + type=int, + default=4) + + parser.add_argument('-d', + '--display', + help='Choose format of how to display output', + metavar='display', + choices=['plain', 'simple', 'grid', 'pipe', 'orgtbl', 'rst', + 'mediawiki', 'latex', 'latex_raw', 'latex_booktabs'], + default='plain') + + parser.add_argument('-t', + '--tab', + help='Specify tab delimited file', + action='store_true') + + parser.add_argument('-e', + '--easy', + help='Halve the reps', + action='store_true') + + args = parser.parse_args() + + # Handle out of bounds error for number of exercises + if args.num < 1: + parser.error(f'--num "{args.num}" must be greater than 0') + + # Handle empty file + if os.path.getsize(args.file) == 0: + parser.error(f"File: '{args.file}' appears to be empty") + + # Handle file not present + if not os.path.isfile(args.file): + parser.error(f"No such file or directory: '{args.file}'") + + return args + + +# -------------------------------------------------- +def main(): + """ Main prog """ + + args = get_args() + random.seed(args.seed) + with open(args.file, 'rt', encoding='utf-8') as f: + exercises = read_csv(f, tab=True) if args.tab else read_csv(f) + + # Deal with any badly formed data (read_csv returns None) + if not exercises: + sys.exit(f'No usable data in --file "{args.file}"') + + # Check if trying to sample more exercises than are actually in the file + num_exercises = len(exercises) + if args.num > num_exercises: + sys.exit(f'--num "{args.num}" > exercises "{num_exercises}"') + + wod = [] + for name, low, high in random.sample(exercises, k=args.num): + suggested_reps = random.randint(low, high) + if args.easy: + # Round down any fractional results + suggested_reps = suggested_reps // 2 + wod.append((name, suggested_reps)) + + print(tabulate(wod, headers=('Exercise', 'Reps'), tablefmt=args.display)) + + +def read_csv(fh, tab=False) -> list: + """ Read CSV formatted input, returns a list of tuples """ + exercises = [] + if fh.seek(0, os.SEEK_END) == 0: + return exercises + fh.seek(0) + delimit = '\t' if tab else ',' + reader = csv.reader(fh, delimiter=delimit) + # Skip past headers + _ = next(reader) + for row in reader: + name, reps = row + # Check each record has both items + if name and reps: + # Check data is as expected for reps (eg. number-number) + match = re.match(r'(\d+)-(\d+)', reps) + if match: + low, high = [int(value) for value in reps.split('-')] + exercises.append((name, low, high)) + return exercises + +# Removed unit test to separate file. + + +# -------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/20_password/password.py b/20_password/password.py new file mode 100755 index 000000000..0fa623673 --- /dev/null +++ b/20_password/password.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +""" +Title : XKCD Password generator +Author : wrjt +Date : 2021-09-14 +Purpose: To generate passphrases based on dictionary words +""" + +import argparse +import os +import random +import re +import string + + +# -------------------------------------------------- +def get_args(): + """Get command-line arguments""" + + parser = argparse.ArgumentParser( + description='Password maker', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('file', + metavar='file', + type=str, + nargs="+", + help='Input file(s)') + + parser.add_argument('-n', + '--num', + help='Number of passwords to generate', + metavar='num_passwords', + type=int, + default=3) + + parser.add_argument('-w', + '--num_words', + help='Number of words to use for password', + metavar='num_words', + type=int, + default=4) + + parser.add_argument('-m', + '--min_word_len', + help='Minimum word length', + metavar='minimum', + type=int, + default=3) + + parser.add_argument('-x', + '--max_word_len', + help='Maximum word length', + metavar='maximum', + type=int, + default=6) + + parser.add_argument('-s', + '--seed', + help='Random seed', + metavar='seed', + type=int, + default=None) + + parser.add_argument('-l', + '--l33t', + help='Obsfucate letters', + action='store_true') + + args = parser.parse_args() + + # Handle file not present (nargs makes it a list) + for f in args.file: + if not os.path.isfile(f): + parser.error(f"No such file or directory: '{f}'") + + return args + + +# -------------------------------------------------- +def main(): + """ Main Prog """ + + args = get_args() + random.seed(args.seed) + words = set() + short, long = args.min_word_len, args.max_word_len + + for f in args.file: + with open(f, 'rt', encoding='utf-8') as fh: + for line in fh: + for word in map(clean, line.lower().split()): + if short <= len(word) <= long: + words.add(word.title()) + + words = sorted(words) + + passwords = [ + ''.join(random.sample(words, args.num_words)) for _ in range(args.num) + ] + + if args.l33t: + passwords = [l33t(word) for word in passwords] + + print('\n'.join(passwords)) + + +def clean(text: str) -> str: + """ Clean inputted text of non-alphabet characters """ + return re.sub(re.compile(r'[^A-Za-z]'), '', text) + + +def ransom(text: str) -> str: + """ Randomly CApITalIzE letters in a word """ + ransomed = [ + c.upper() if random.choice([0, 1]) else c.lower() for c in text + ] + return ''.join(ransomed) + + +def l33t(text: str) -> str: + """ Make text l33t like """ + leet_me = { + 'a': '@', + 'A': '4', + 'O': '0', + 't': '+', + 'E': '3', + 'I': '1', + 'S': '5' + } + leeted = ransom(text).translate(str.maketrans(leet_me)) + return leeted + random.choice(string.punctuation) + + +# -------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/21_tictactoe/tictactoe.py b/21_tictactoe/tictactoe.py new file mode 100755 index 000000000..22ce8fd04 --- /dev/null +++ b/21_tictactoe/tictactoe.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +""" +Title : Tictactoe +Author : wrjt +Date : 2021-09-15 +Purpose: Emulate a game of tictactoe +""" + +import argparse +import re + + +# -------------------------------------------------- +def get_args(): + """Get command-line arguments""" + + parser = argparse.ArgumentParser( + description='Tictactoe', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('-b', + '--board', + help='Current state of the board', + metavar='board', + type=str, + default='.' * 9) + + parser.add_argument('-p', + '--player', + help='Player to move', + metavar='player', + type=str, + choices="XO", + default=None) + + parser.add_argument('-c', + '--cell', + help='Number of the cell to be taken', + metavar='cell', + type=int, + choices=range(1, 10), + default=None) + + args = parser.parse_args() + + if not re.search(r'^[.XO]{9}$', args.board): + parser.error(f'--board "{args.board}" must be 9 characters of ., X, O') + + if (args.player and not args.cell) or (args.cell and not args.player): + parser.error("Must provide both --player and --cell") + + if args.cell and args.player and args.board[args.cell - 1] in "XO": + parser.error(f'--cell "{args.cell}" already taken') + + return args + + +# -------------------------------------------------- +def main(): + """ Main Prog""" + + args = get_args() + board = args.board + + if args.cell: + pos = args.cell + board = board[:pos - 1] + args.player + board[pos:] + + winner = find_winner(board, 'O') or find_winner(board, 'X') or "No winner." + + print(format_board(board) + '\n' + winner) + + +def format_board(board: str) -> str: + """ Print out current state of board """ + cells = [str(i) if c == '.' else c for i, c in enumerate(board, start=1)] + divider = "-------------" + cells_tmpl = "| {} | {} | {} |" + return '\n'.join([ + divider, + cells_tmpl.format(*cells[:3]), divider, + cells_tmpl.format(*cells[3:6]), divider, + cells_tmpl.format(*cells[6:]), divider + ]) + + +def find_winner(board: str, player: str) -> str: + """ Return the winner """ + wins = [('PPP......'), ('...PPP...'), ('......PPP'), ('P..P..P..'), + ('.P..P..P.'), ('..P..P..P'), ('P...P...P'), ('..P.P.P..')] + board = ''.join([c.replace(c, '.') if c != player else c for c in board]) + for win in wins: + matches = 0 + win = win.replace('P', player) + for w, b in zip(win, board): + if w == player and b == player: + matches += 1 + if matches == 3: + return f"{player} has won!" + + +# -------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/22_itictactoe/itictactoe.py b/22_itictactoe/itictactoe.py new file mode 100755 index 000000000..e11c337e2 --- /dev/null +++ b/22_itictactoe/itictactoe.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +""" +Title : ITictactoe +Author : wrjt +Date : 2021-09-15 +Purpose: Play an interactive game of tictactoe +""" + +import os +import sys + + +def main() -> None: + """ Main Prog""" + + board = '.' * 9 + player = 'X' + print('\n' + format_board(board)) + + while True: + move = input(f"Player {player}, what is your move? [q to quit]: ") + if move == 'q': + sys.exit("You lose, loser!") + try: + move = int(move) + if move not in range(1, 10): + raise ValueError + except ValueError: + print(f'Invalid cell "{move}", please use 1-9') + continue + else: + if board[move - 1] in "XO": + print(f'Cell "{move}" already taken.') + else: + board = board[:move - 1] + player + board[move:] + print('\n' + format_board(board)) + if find_winner(board, player): + sys.exit(f"{player} has won!") + if "." not in board: + sys.exit("All right, we'll call it a draw.") + player = 'O' if player == 'X' else 'X' + + +def clear() -> None: + """ Clear screen (depending on OS) """ + if os.name == 'nt': + _ = os.system('cls') + else: + _ = os.system('clear') + + +def format_board(board: str) -> str: + """ Print out current state of board """ + clear() + cells = [str(i) if c == '.' else c for i, c in enumerate(board, start=1)] + divider = "-------------" + cells_tmpl = "| {} | {} | {} |" + return '\n'.join([ + divider, + cells_tmpl.format(*cells[:3]), divider, + cells_tmpl.format(*cells[3:6]), divider, + cells_tmpl.format(*cells[6:]), divider + ]) + + +def find_winner(board: str, player: str) -> bool: + """ Return the winner """ + wins = [('PPP......'), ('...PPP...'), ('......PPP'), ('P..P..P..'), + ('.P..P..P.'), ('..P..P..P'), ('P...P...P'), ('..P.P.P..')] + board = ''.join([c.replace(c, '.') if c != player else c for c in board]) + for win in wins: + matches = 0 + win = win.replace('P', player) + for w, b in zip(win, board): + if w == player and b == player: + matches += 1 + if matches == 3: + return True + + +# -------------------------------------------------- +if __name__ == '__main__': + main()