From 9f3eda50a2eec737f4006f7a77d987f35d4865b0 Mon Sep 17 00:00:00 2001 From: Adrien-LUDWIG Date: Sat, 28 Oct 2023 11:19:48 +0200 Subject: [PATCH 01/14] Add binary-search synced and completed files --- config.json | 98 +++++-------------- .../binary-search/.docs/instructions.md | 29 ++++++ .../binary-search/.docs/introduction.md | 13 +++ .../practice/binary-search/.meta/config.json | 11 +++ .../practice/binary-search/.meta/tests.toml | 43 ++++++++ 5 files changed, 122 insertions(+), 72 deletions(-) create mode 100644 exercises/practice/binary-search/.docs/instructions.md create mode 100644 exercises/practice/binary-search/.docs/introduction.md create mode 100644 exercises/practice/binary-search/.meta/config.json create mode 100644 exercises/practice/binary-search/.meta/tests.toml diff --git a/config.json b/config.json index b9514112..b005b326 100644 --- a/config.json +++ b/config.json @@ -19,18 +19,10 @@ "average_run_time": 2 }, "files": { - "solution": [ - "%{kebab_slug}.rkt" - ], - "test": [ - "%{kebab_slug}-test.rkt" - ], - "example": [ - ".meta/example.rkt" - ], - "exemplar": [ - ".meta/exemplar.rkt" - ] + "solution": ["%{kebab_slug}.rkt"], + "test": ["%{kebab_slug}-test.rkt"], + "example": [".meta/example.rkt"], + "exemplar": [".meta/exemplar.rkt"] }, "exercises": { "practice": [ @@ -38,9 +30,7 @@ "slug": "hello-world", "name": "Hello World", "uuid": "4fb471fc-4e6d-486d-abf5-939e89f028fc", - "practices": [ - "strings" - ], + "practices": ["strings"], "prerequisites": [], "difficulty": 1 }, @@ -56,10 +46,7 @@ "slug": "two-fer", "name": "Two Fer", "uuid": "27ffc2d2-e950-40a1-90fa-a1f3eec4fd36", - "practices": [ - "optional-values", - "text-formatting" - ], + "practices": ["optional-values", "text-formatting"], "prerequisites": [], "difficulty": 1 }, @@ -75,9 +62,7 @@ "slug": "difference-of-squares", "name": "Difference of Squares", "uuid": "a3d9a2bb-a80a-487f-b529-64e20f7cf9b5", - "practices": [ - "math" - ], + "practices": ["math"], "prerequisites": [], "difficulty": 1 }, @@ -93,9 +78,7 @@ "slug": "perfect-numbers", "name": "Perfect Numbers", "uuid": "72b2c36b-fcd5-4c8c-89e7-98bf24faaa3e", - "practices": [ - "math" - ], + "practices": ["math"], "prerequisites": [], "difficulty": 1 }, @@ -119,9 +102,7 @@ "slug": "collatz-conjecture", "name": "Collatz Conjecture", "uuid": "28102e69-dad0-4f3c-8cdf-5a18a73178a4", - "practices": [ - "math" - ], + "practices": ["math"], "prerequisites": [], "difficulty": 1 }, @@ -145,11 +126,7 @@ "slug": "twelve-days", "name": "Twelve Days", "uuid": "5961220f-5d33-4e97-a4bb-8b375714a8fc", - "practices": [ - "enumerations", - "reduce", - "strings" - ], + "practices": ["enumerations", "reduce", "strings"], "prerequisites": [], "difficulty": 2 }, @@ -157,12 +134,7 @@ "slug": "isogram", "name": "Isogram", "uuid": "2fa21cb9-5469-4b95-9b78-c6f4dec6f67f", - "practices": [ - "algorithms", - "conditionals", - "loops", - "strings" - ], + "practices": ["algorithms", "conditionals", "loops", "strings"], "prerequisites": [], "difficulty": 3 }, @@ -185,11 +157,7 @@ "slug": "armstrong-numbers", "name": "Armstrong Numbers", "uuid": "0fa9d2c6-f170-43b4-a28a-eb4994b4140e", - "practices": [ - "algorithms", - "loops", - "math" - ], + "practices": ["algorithms", "loops", "math"], "prerequisites": [], "difficulty": 1 }, @@ -197,11 +165,7 @@ "slug": "affine-cipher", "name": "Affine Cipher", "uuid": "529c3ced-eefa-402c-b901-ea39ae2fb24e", - "practices": [ - "algorithms", - "cryptography", - "strings" - ], + "practices": ["algorithms", "cryptography", "strings"], "prerequisites": [], "difficulty": 4 }, @@ -226,12 +190,7 @@ "slug": "all-your-base", "name": "All Your Base", "uuid": "f6617861-da32-4735-8c7f-5ae57a8cf44a", - "practices": [ - "integers", - "map", - "math", - "transforming" - ], + "practices": ["integers", "map", "math", "transforming"], "prerequisites": [], "difficulty": 1 }, @@ -259,6 +218,14 @@ "prerequisites": [], "difficulty": 1 }, + { + "slug": "binary-search", + "name": "Binary Search", + "uuid": "033094e7-fd15-42ee-bbc8-0fb12c2cdb65", + "practices": [], + "prerequisites": [], + "difficulty": 2 + }, { "slug": "clock", "name": "Clock", @@ -341,9 +308,7 @@ "slug": "reverse-string", "name": "Reverse String", "uuid": "135c7e52-04ac-4cde-9617-e16870dacb3f", - "practices": [ - "strings" - ], + "practices": ["strings"], "prerequisites": [], "difficulty": 1 }, @@ -399,11 +364,7 @@ "slug": "atbash-cipher", "name": "Atbash Cipher", "uuid": "92d97f99-04f9-4a52-942d-d1a9fa96375f", - "practices": [ - "algorithms", - "cryptography", - "strings" - ], + "practices": ["algorithms", "cryptography", "strings"], "prerequisites": [], "difficulty": 3 }, @@ -411,10 +372,7 @@ "slug": "variable-length-quantity", "name": "Variable Length Quantity", "uuid": "a6f90be2-1360-479e-8a6f-4f2d8b492c71", - "practices": [ - "algorithms", - "bitwise-operations" - ], + "practices": ["algorithms", "bitwise-operations"], "prerequisites": [], "difficulty": 7 }, @@ -455,11 +413,7 @@ "slug": "house", "name": "House", "uuid": "79861b42-3bf8-4fe9-91f0-72b4c0a164f9", - "practices": [ - "algorithms", - "recursion", - "strings" - ], + "practices": ["algorithms", "recursion", "strings"], "prerequisites": [], "difficulty": 4 }, diff --git a/exercises/practice/binary-search/.docs/instructions.md b/exercises/practice/binary-search/.docs/instructions.md new file mode 100644 index 00000000..12f4358e --- /dev/null +++ b/exercises/practice/binary-search/.docs/instructions.md @@ -0,0 +1,29 @@ +# Instructions + +Your task is to implement a binary search algorithm. + +A binary search algorithm finds an item in a list by repeatedly splitting it in half, only keeping the half which contains the item we're looking for. +It allows us to quickly narrow down the possible locations of our item until we find it, or until we've eliminated all possible locations. + +~~~~exercism/caution +Binary search only works when a list has been sorted. +~~~~ + +The algorithm looks like this: + +- Find the middle element of a _sorted_ list and compare it with the item we're looking for. +- If the middle element is our item, then we're done! +- If the middle element is greater than our item, we can eliminate that element and all the elements **after** it. +- If the middle element is less than our item, we can eliminate that element and all the elements **before** it. +- If every element of the list has been eliminated then the item is not in the list. +- Otherwise, repeat the process on the part of the list that has not been eliminated. + +Here's an example: + +Let's say we're looking for the number 23 in the following sorted list: `[4, 8, 12, 16, 23, 28, 32]`. + +- We start by comparing 23 with the middle element, 16. +- Since 23 is greater than 16, we can eliminate the left half of the list, leaving us with `[23, 28, 32]`. +- We then compare 23 with the new middle element, 28. +- Since 23 is less than 28, we can eliminate the right half of the list: `[23]`. +- We've found our item. diff --git a/exercises/practice/binary-search/.docs/introduction.md b/exercises/practice/binary-search/.docs/introduction.md new file mode 100644 index 00000000..03496599 --- /dev/null +++ b/exercises/practice/binary-search/.docs/introduction.md @@ -0,0 +1,13 @@ +# Introduction + +You have stumbled upon a group of mathematicians who are also singer-songwriters. +They have written a song for each of their favorite numbers, and, as you can imagine, they have a lot of favorite numbers (like [0][zero] or [73][seventy-three] or [6174][kaprekars-constant]). + +You are curious to hear the song for your favorite number, but with so many songs to wade through, finding the right song could take a while. +Fortunately, they have organized their songs in a playlist sorted by the title — which is simply the number that the song is about. + +You realize that you can use a binary search algorithm to quickly find a song given the title. + +[zero]: https://en.wikipedia.org/wiki/0 +[seventy-three]: https://en.wikipedia.org/wiki/73_(number) +[kaprekars-constant]: https://en.wikipedia.org/wiki/6174_(number) diff --git a/exercises/practice/binary-search/.meta/config.json b/exercises/practice/binary-search/.meta/config.json new file mode 100644 index 00000000..0918b937 --- /dev/null +++ b/exercises/practice/binary-search/.meta/config.json @@ -0,0 +1,11 @@ +{ + "authors": ["Adrien-LUDWIG"], + "files": { + "solution": ["binary-search.rkt"], + "test": ["binary-search-test.rkt"], + "example": [".meta/example.rkt"] + }, + "blurb": "Implement a binary search algorithm.", + "source": "Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/Binary_search_algorithm" +} diff --git a/exercises/practice/binary-search/.meta/tests.toml b/exercises/practice/binary-search/.meta/tests.toml new file mode 100644 index 00000000..61e2b068 --- /dev/null +++ b/exercises/practice/binary-search/.meta/tests.toml @@ -0,0 +1,43 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[b55c24a9-a98d-4379-a08c-2adcf8ebeee8] +description = "finds a value in an array with one element" + +[73469346-b0a0-4011-89bf-989e443d503d] +description = "finds a value in the middle of an array" + +[327bc482-ab85-424e-a724-fb4658e66ddb] +description = "finds a value at the beginning of an array" + +[f9f94b16-fe5e-472c-85ea-c513804c7d59] +description = "finds a value at the end of an array" + +[f0068905-26e3-4342-856d-ad153cadb338] +description = "finds a value in an array of odd length" + +[fc316b12-c8b3-4f5e-9e89-532b3389de8c] +description = "finds a value in an array of even length" + +[da7db20a-354f-49f7-a6a1-650a54998aa6] +description = "identifies that a value is not included in the array" + +[95d869ff-3daf-4c79-b622-6e805c675f97] +description = "a value smaller than the array's smallest value is not found" + +[8b24ef45-6e51-4a94-9eac-c2bf38fdb0ba] +description = "a value larger than the array's largest value is not found" + +[f439a0fa-cf42-4262-8ad1-64bf41ce566a] +description = "nothing is found in an empty array" + +[2c353967-b56d-40b8-acff-ce43115eed64] +description = "nothing is found when the left and right bounds cross" From c27776357c7475d002d8cfb2b25bf06ed05a0aea Mon Sep 17 00:00:00 2001 From: Adrien-LUDWIG Date: Sat, 28 Oct 2023 13:14:18 +0200 Subject: [PATCH 02/14] Add binary-search tests, example and slug --- .../practice/binary-search/.meta/example.rkt | 14 ++++++ .../binary-search/binary-search-test.rkt | 47 +++++++++++++++++++ .../practice/binary-search/binary-search.rkt | 6 +++ 3 files changed, 67 insertions(+) create mode 100644 exercises/practice/binary-search/.meta/example.rkt create mode 100644 exercises/practice/binary-search/binary-search-test.rkt create mode 100644 exercises/practice/binary-search/binary-search.rkt diff --git a/exercises/practice/binary-search/.meta/example.rkt b/exercises/practice/binary-search/.meta/example.rkt new file mode 100644 index 00000000..a0a4663a --- /dev/null +++ b/exercises/practice/binary-search/.meta/example.rkt @@ -0,0 +1,14 @@ +#lang racket + +(provide binary-search) + +(define (binary-search array value) + (define (rec left right) + (cond + [(> left right) (error "Value not in array")] + [else (define mid (+ left (quotient (- right left) 2))) + (cond + [(< value (list-ref array mid)) (rec left (sub1 mid))] + [(< (list-ref array mid) value) (rec (add1 mid) right)] + [else mid])])) + (rec 0 (sub1 (length array)))) diff --git a/exercises/practice/binary-search/binary-search-test.rkt b/exercises/practice/binary-search/binary-search-test.rkt new file mode 100644 index 00000000..af570a6e --- /dev/null +++ b/exercises/practice/binary-search/binary-search-test.rkt @@ -0,0 +1,47 @@ +#lang racket/base + +(require "binary-search.rkt") + +(module+ test + (require rackunit rackunit/text-ui) + + (define suite + (test-suite + "binary-search tests" + + (test-eqv? "finds a value in an array with one element" + (binary-search (list 6) 6) + 0) + (test-eqv? "finds a value in the middle of an array" + (binary-search (list 1 3 4 6 8 9 11) 6) + 3) + (test-eqv? "finds a value at the beginning of an array" + (binary-search (list 1 3 4 6 8 9 11) 1) + 0) + (test-eqv? "finds a value at the end of an array" + (binary-search (list 1 3 4 6 8 9 11) 11) + 6) + (test-eqv? "finds a value in an array of odd length" + (binary-search (list 1 3 5 8 13 21 34 55 89 144 233 377 634) 144) + 9) + (test-eqv? "finds a value in an array of even length" + (binary-search (list 1 3 5 8 13 21 34 55 89 144 233 377) 21) + 5) + (test-exn "identifies that a value is not included in the array" + exn:fail? + (lambda () (binary-search (list 1 3 4 6 8 9 11) 7))) + (test-exn "a value smaller than the array's smallest value is not found" + exn:fail? + (lambda () (binary-search (list 1 3 4 6 8 9 11) 0))) + (test-exn "a value larger than the array's smallest value is not found" + exn:fail? + (lambda () (binary-search (list 1 3 4 6 8 9 11) 13))) + (test-exn "nothing is found in an empty array" + exn:fail? + (lambda () (binary-search '() 1))) + (test-exn "nothing is found when the left and right bounds cross" + exn:fail? + (lambda () (binary-search (list 1 2) 0))) + )) + + (run-tests suite)) diff --git a/exercises/practice/binary-search/binary-search.rkt b/exercises/practice/binary-search/binary-search.rkt new file mode 100644 index 00000000..04e81bbf --- /dev/null +++ b/exercises/practice/binary-search/binary-search.rkt @@ -0,0 +1,6 @@ +#lang racket + +(provide binary-search) + +(define (binary-search array value) + (error "Not implemented yet")) From 4cb097cdbaca2c0b53cd4cef95ef1c4033a4021a Mon Sep 17 00:00:00 2001 From: Adrien-LUDWIG Date: Mon, 30 Oct 2023 00:26:41 +0100 Subject: [PATCH 03/14] Copy the exercise generator of common lisp --- bin/custom_json_encoder.py | 239 +++++++++++++++++ bin/lisp_exercise_generator.py | 473 +++++++++++++++++++++++++++++++++ 2 files changed, 712 insertions(+) create mode 100644 bin/custom_json_encoder.py create mode 100644 bin/lisp_exercise_generator.py diff --git a/bin/custom_json_encoder.py b/bin/custom_json_encoder.py new file mode 100644 index 00000000..f6bb93d8 --- /dev/null +++ b/bin/custom_json_encoder.py @@ -0,0 +1,239 @@ +#### This code is pretty much just copied from json.encoder with +#### minor differences in _iterencode_list function embedded within +#### _make_iterencode function + +import json +from json.encoder import ( + encode_basestring, + encode_basestring_ascii, + INFINITY, + c_make_encoder +) + +class CustomJSONEncoder(json.JSONEncoder): + ## Same as the iterencode method that it is overriding in parent + ## json.JSONEncoder class, but all to call the customized + ## _make_iterencode function + def iterencode(self, o, _one_shot=False): + if self.check_circular: + markers = {} + else: + markers = None + if self.ensure_ascii: + _encoder = encode_basestring_ascii + else: + _encoder = encode_basestring + + def floatstr(o, allow_nan=self.allow_nan, + _repr=float.__repr__, _inf=INFINITY, _neginf=-INFINITY): + # Check for specials. Note that this type of test is processor + # and/or platform-specific, so do tests which don't depend on the + # internals. + + if o != o: + text = 'NaN' + elif o == _inf: + text = 'Infinity' + elif o == _neginf: + text = '-Infinity' + else: + return _repr(o) + + if not allow_nan: + raise ValueError( + "Out of range float values are not JSON compliant: " + + repr(o)) + + return text + + + if (_one_shot and c_make_encoder is not None + and self.indent is None): + _iterencode = c_make_encoder( + markers, self.default, _encoder, self.indent, + self.key_separator, self.item_separator, self.sort_keys, + self.skipkeys, self.allow_nan) + else: + _iterencode = _make_iterencode( + markers, self.default, _encoder, self.indent, floatstr, + self.key_separator, self.item_separator, self.sort_keys, + self.skipkeys, _one_shot) + return _iterencode(o, 0) + + +## Same function as original, except for _list_iterencode function +def _make_iterencode(markers, _default, _encoder, _indent, _floatstr, + _key_separator, _item_separator, _sort_keys, _skipkeys, _one_shot, + ## HACK: hand-optimized bytecode; turn globals into locals + ValueError=ValueError, + dict=dict, + float=float, + id=id, + int=int, + isinstance=isinstance, + list=list, + str=str, + tuple=tuple, + _intstr=int.__repr__, + ): + + if _indent is not None and not isinstance(_indent, str): + _indent = ' ' * _indent + + ## Customized function now creates inline arrays/lists instead of + ## newlining + indenting all elements + def _iterencode_list(lst, _current_indent_level): + if not lst: + yield '[]' + return + if markers is not None: + markerid = id(lst) + if markerid in markers: + raise ValueError("Circular reference detected") + markers[markerid] = lst + buf = '[' + first = True + for value in lst: + if first: + first = False + else: + buf = _item_separator + if isinstance(value, str): + yield buf + _encoder(value) + elif value is None: + yield buf + 'null' + elif value is True: + yield buf + 'true' + elif value is False: + yield buf + 'false' + elif isinstance(value, int): + # Subclasses of int/float may override __repr__, but we still + # want to encode them as integers/floats in JSON. One example + # within the standard library is IntEnum. + yield buf + _intstr(value) + elif isinstance(value, float): + # see comment above for int + yield buf + _floatstr(value) + else: + yield buf + if isinstance(value, (list, tuple)): + chunks = _iterencode_list(value, _current_indent_level) + elif isinstance(value, dict): + chunks = _iterencode_dict(value, _current_indent_level) + else: + chunks = _iterencode(value, _current_indent_level) + yield from chunks + yield ']' + if markers is not None: + del markers[markerid] + + def _iterencode_dict(dct, _current_indent_level): + if not dct: + yield '{}' + return + if markers is not None: + markerid = id(dct) + if markerid in markers: + raise ValueError("Circular reference detected") + markers[markerid] = dct + yield '{' + if _indent is not None: + _current_indent_level += 1 + newline_indent = '\n' + _indent * _current_indent_level + item_separator = _item_separator + newline_indent + yield newline_indent + else: + newline_indent = None + item_separator = _item_separator + first = True + if _sort_keys: + items = sorted(dct.items()) + else: + items = dct.items() + for key, value in items: + if isinstance(key, str): + pass + # JavaScript is weakly typed for these, so it makes sense to + # also allow them. Many encoders seem to do something like this. + elif isinstance(key, float): + # see comment for int/float in _make_iterencode + key = _floatstr(key) + elif key is True: + key = 'true' + elif key is False: + key = 'false' + elif key is None: + key = 'null' + elif isinstance(key, int): + # see comment for int/float in _make_iterencode + key = _intstr(key) + elif _skipkeys: + continue + else: + raise TypeError(f'keys must be str, int, float, bool or None, ' + f'not {key.__class__.__name__}') + if first: + first = False + else: + yield item_separator + yield _encoder(key) + yield _key_separator + if isinstance(value, str): + yield _encoder(value) + elif value is None: + yield 'null' + elif value is True: + yield 'true' + elif value is False: + yield 'false' + elif isinstance(value, int): + # see comment for int/float in _make_iterencode + yield _intstr(value) + elif isinstance(value, float): + # see comment for int/float in _make_iterencode + yield _floatstr(value) + else: + if isinstance(value, (list, tuple)): + chunks = _iterencode_list(value, _current_indent_level) + elif isinstance(value, dict): + chunks = _iterencode_dict(value, _current_indent_level) + else: + chunks = _iterencode(value, _current_indent_level) + yield from chunks + if newline_indent is not None: + _current_indent_level -= 1 + yield '\n' + _indent * _current_indent_level + yield '}' + if markers is not None: + del markers[markerid] + + def _iterencode(o, _current_indent_level): + if isinstance(o, str): + yield _encoder(o) + elif o is None: + yield 'null' + elif o is True: + yield 'true' + elif o is False: + yield 'false' + elif isinstance(o, int): + # see comment for int/float in _make_iterencode + yield _intstr(o) + elif isinstance(o, float): + # see comment for int/float in _make_iterencode + yield _floatstr(o) + elif isinstance(o, (list, tuple)): + yield from _iterencode_list(o, _current_indent_level) + elif isinstance(o, dict): + yield from _iterencode_dict(o, _current_indent_level) + else: + if markers is not None: + markerid = id(o) + if markerid in markers: + raise ValueError("Circular reference detected") + markers[markerid] = o + o = _default(o) + yield from _iterencode(o, _current_indent_level) + if markers is not None: + del markers[markerid] + return _iterencode diff --git a/bin/lisp_exercise_generator.py b/bin/lisp_exercise_generator.py new file mode 100644 index 00000000..df0d696f --- /dev/null +++ b/bin/lisp_exercise_generator.py @@ -0,0 +1,473 @@ +import json, toml, os, argparse, sys, string +from custom_json_encoder import CustomJSONEncoder + +LEGITIMATE_CHARS = string.ascii_lowercase + string.digits + " -" + +def find_common_lisp_main(): + up_one = os.path.split(os.getcwd())[0] + return ".." if os.path.split(up_one)[1] == "common-lisp" else "." + +TARGET = os.path.abspath(find_common_lisp_main() + "/exercises/practice") + + +def create_directory_structure(exercise_name): + """ + Creates the exercise, .meta, and .docs directories. + + Parameter exercise_name: Name of the exercise to be generated. + Precondition: exercise_name is a string of a valid exercise. + """ + if not os.path.exists(exercise := f"{TARGET}/{exercise_name}"): + os.makedirs(exercise) + if not os.path.exists(meta := f"{exercise}/.meta"): + os.makedirs(meta) + if not os.path.exists(docs := f"{exercise}/.docs"): + os.makedirs(docs) + + +def create_meta_config(exercise_name, prob_spec_exercise, author): + """ + Auto-generates the .meta/config.json file. + + Parameter exercise_name: Name of the exercise to be generated. + Precondition: exercise_name is a string of a valid exercise. + + Parameter prob_spec_exercise: A filepath to the location of the exercise folder + in the problem-specifications repository. + Precondition: prob_spec_exercise is a string of a valid filepath. + + Parameter author: The Github handle of the author. + Precondition: author is a string. + """ + config_data = None + with open(f"{prob_spec_exercise}/metadata.toml") as file: + config_data = toml.load(file) # Get the blurb, source, and source_url + + # Add the files, authors, and contributors to the config_data + config_data["files"] = {} + config_data["files"]["test"] = [f"{exercise_name}-test.lisp"] + config_data["files"]["solution"] = [f"{exercise_name}.lisp"] + config_data["files"]["example"] = [".meta/example.lisp"] + config_data["authors"] = [author] + config_data["contributors"] = [] + + with open(f"{TARGET}/{exercise_name}/.meta/config.json", 'w') as file: + # Encode into a string in json format and write to file + file.write(json.dumps(config_data, cls = CustomJSONEncoder, indent = 3)) + file.write("\n") + + +def create_instructions(exercise_name, prob_spec_exercise): + """ + Auto-generates the .docs/instructions.md file + + Parameter exercise_name: Name of the exercise to be generated. + Precondition: exercise_name is a string of a valid exercise. + + Parameter prob_spec_exercise: A filepath to the location of the exercise folder + in the problem-specifications repository. + Precondition: prob_spec_exercise is a string of a valid filepath. + """ + input_file = f"{prob_spec_exercise}/description.md" + output_file = f"{TARGET}/{exercise_name}/.docs/instructions.md" + with open(input_file, 'r', encoding = "utf-8") as read_from: + with open(output_file, 'w', encoding = "utf-8") as write_to: + # Replace first line with "# Instructions\n" during copy process + write_to.write("# Instructions\n" + "\n".join(read_from.read().split("\n")[1:])) + + +def create_test_example_solution_files(exercise_name, prob_spec_exercise): + """ + Auto-generates the test file. + + Function creates the test file in its own right, but also calls the + create_example_and_solution_files function. This function also creates + the parameters to feed into the create_example_and_solution_files function. + + Parameter exercise_name: Name of the exercise to be generated. + Precondition: exercise_name is a string of a valid exercise. + + Parameter prob_spec_exercise: A filepath to the location of the exercise folder + in the problem-specifications repository. + Precondition: prob_spec_exercise is a string of a valid filepath. + """ + data = None + with open(f"{prob_spec_exercise}/canonical-data.json") as file: + data = json.load(file) + + # Boilerplate test code. Multiline docstring format used to maintain + # correct indentation and to increase readability. + exercise_string = """;; Ensures that {0}.lisp and the testing library are always loaded +(eval-when (:compile-toplevel :load-toplevel :execute) + (load "{0}") + (quicklisp-client:quickload :fiveam)) + +;; Defines the testing package with symbols from {0} and FiveAM in scope +;; The `run-tests` function is exported for use by both the user and test-runner +(defpackage :{0}-test + (:use :cl :fiveam) + (:export :run-tests)) + +;; Enter the testing package +(in-package :{0}-test) + +;; Define and enter a new FiveAM test-suite +(def-suite* {0}-suite) +""".format(exercise_name) + + # func_name_dict is a dictionary of all function names and their + # expected input argument names. + func_name_dict, tests_string = create_test(data["cases"], exercise_name) + + # tests_string is sandwiched between exercise_string and more boilerplate + # code at the end of the file. + exercise_string += tests_string + """ +(defun run-tests (&optional (test-or-suite '{0}-suite)) + "Provides human readable results of test run. Default to entire suite." + (run! test-or-suite)) +""".format(exercise_name) + + with open(f"{TARGET}/{exercise_name}/{exercise_name}-test.lisp", 'w') as file: + file.write(exercise_string) + + create_example_and_solution_files(exercise_name, func_name_dict) + + +def create_test(cases, exercise_name, fnd = dict()): + """ + Auto-generates tests for the test file. + + Parameter cases: A list of test cases to be Lispified. + + Parameter exercise_name: Name of the exercise to be generated. + Precondition: exercise_name is a string of a valid exercise. + + Parameter fnd: A dictionary of all function names and their expected + input argument names. + Precondition: fnd is a dictionary. + + Returns a tuple of fnd and the test string + """ + # Helper functions only used in create_test function + def to_kebab_case(string): + from_snake = string.replace("_", "-") + from_camel = "".join([f"-{c.lower()}" if c.isupper() else c for c in from_snake]) + return from_camel + + def to_predicate(string, expected_result): + if not isinstance(expected_result, bool): + return string + elif (partitioned := string.partition("-"))[2]: + if '-' in partitioned[2] or partitioned[2][-1] == 'p': + return partitioned[2] + "-p" + else: + return partitioned[2] + "p" + else: + return partitioned[0] + ("-p" if 'p' == partitioned[0][-1] else "p") + + # Normal code begins here + output = "" + for case in cases: + try: + # Add function_name and func_params to fnd + kebab_func_name = to_kebab_case(case["property"]) + function_name = to_predicate(kebab_func_name, case["expected"]) + func_params = [to_kebab_case(param) for param in list(case["input"])] + fnd[function_name] = func_params + + # Prepare the variables and their associated values for + # implementation inside a "let" + arg_pairs = [] + for var, value in case["input"].items(): + arg = "({0} {1})".format(to_kebab_case(var), clean_lispification(lispify(value))) + arg_pairs.append(arg) + let_args = ("\n" + " " * 10).join(arg_pairs) + + # Create the test name + cleaned = [c for c in case["description"].lower() if c in LEGITIMATE_CHARS] + description = "".join(cleaned).replace(" ", "-") + + output += create_test_string(description, let_args, case["expected"], + exercise_name, function_name, func_params) + except KeyError: + # Recursively dig further into the data structure + fnd, string = create_test(case["cases"], exercise_name, fnd) + output += string + + return fnd, output + +def create_test_string(desc, args, expected, exercise, func_name, func_params): + result, let_result, close_paren = "", "", ")" + if isinstance(expected, bool): + result = f"is-{str(expected).lower()}" + close_paren = "" + else: + equality = "" + if isinstance(expected, int) or isinstance(expected, float): + equality = "=" + elif isinstance(expected, str) and len(expected) == 1: + equality = "char=" + elif isinstance(expected, str): + equality = "string=" + else: + equality = "equal" + + cleaned = clean_lispification(lispify(expected)) + if len(cleaned) < 35: + result = f"is ({equality} {cleaned}" + else: + result = f"is ({equality} result" + let_result = """ + (result {0})""".format(cleaned) + + # Multiline docstring format used to maintain correct indentation + # and to increase readability. + return """ +(test {0} + (let ({1}{2}) + ({3} ({4}:{5} {6})))){7} +""".format(desc, args, let_result, result, exercise, func_name, " ".join(func_params), close_paren) + + +def create_example_and_solution_files(exercise_name, func_name_dict): + """ + Auto-generates the .meta/example.lisp and the 'exercise'.lisp files. + + Parameter exercise_name: Name of the exercise to be generated. + Precondition: exercise_name is a string of a valid exercise. + + Parameter func_name_dict: A dictionary of all function names and their + expected input argument names. + Precondition: func_name_dict is a dictionary. + """ + # Create keywords of function names to be exported (vertically aligned) + exports_string = ("\n" + " " * 11).join([":" + k for k in func_name_dict]) + + # Boilerplate code. Multiline docstring format used to maintain + # correct indentation and to increase readability. + file_string = """(defpackage :{0} + (:use :cl) + (:export {1})) + +(in-package :{0}) +""".format(exercise_name, exports_string) + + # For each function-parameters pairing, add the requisite function + # definition to the file. + for func, params in func_name_dict.items(): + file_string += "\n(defun {0} ({1}))\n".format(func, " ".join(params)) + + with open(f"{TARGET}/{exercise_name}/{exercise_name}.lisp", 'w') as file: + file.write(file_string) + + with open(f"{TARGET}/{exercise_name}/.meta/example.lisp", 'w') as file: + file.write(file_string) + + +def create_test_toml(exercise_name, prob_spec_exercise): + """ + Auto-generates the .meta/tests.toml file. + + Parameter exercise_name: Name of the exercise to be generated. + Precondition: exercise_name is a string of a valid exercise. + + Parameter prob_spec_exercise: A filepath to the location of the exercise folder + in the problem-specifications repository. + Precondition: prob_spec_exercise is a string of a valid filepath. + """ + # Nested helper function that will either build a string in a toml + # style, or recursively dig until it finds the data and then build + # a string. + def find_uuids_and_descriptions(cases): + output = "" + for case in cases: + try: + # Add lines in toml style + output += "\n[{0}]\ndescription = \"{1}\"\n".format(case["uuid"], case["description"]) + except KeyError: + # Recursively dig further into the data structure + output += find_uuids_and_descriptions(case["cases"]) + + return output + + # Non-nested code starts here + data = None + with open(f"{prob_spec_exercise}/canonical-data.json") as file: + data = json.load(file) + + # Boilerplate comment at top of test.toml + toml_string = """# This is an auto-generated file. Regular comments will be removed when this +# file is regenerated. Regenerating will not touch any manually added keys, +# so comments can be added in a "comment" key. +""" + toml_string += find_uuids_and_descriptions(data["cases"]) + + with open(f"{TARGET}/{exercise_name}/.meta/tests.toml", 'w') as file: + file.write(toml_string) + + +def brand_new_exercise(exercise_name :str, prob_spec_exercise :str, author :str = ""): + """ + A delegation function. + + Call this function to build the exercise. + + Parameter exercise_name: Name of the exercise to be generated. + Precondition: exercise_name is a string of a valid exercise. + + Parameter prob_spec_exercise: A filepath to the location of the exercise folder + in the problem-specifications repository. + Precondition: prob_spec_exercise is a string of a valid filepath. + + Parameter author: The Github handle of the author. + Precondition: author is a string. + """ + # This MUST be called first + create_directory_structure(exercise_name) + + # The order that these functions are called is unimportant + create_meta_config(exercise_name, prob_spec_exercise, author) + create_instructions(exercise_name, prob_spec_exercise) + create_test_example_solution_files(exercise_name, prob_spec_exercise) + create_test_toml(exercise_name, prob_spec_exercise) + + +def lispify(value, string_to_keyword = False): + """ + Converts a given value from a Python data type into its Lisp counterpart. + + Returns the following conversions: + lists/arrays -> lists + bools -> T or NIL + ints -> ints + floats -> floats + strings -> strings (or chars if string len == 1) or keywords + dicts -> acons lists (or NIL if key == "error") + + Parameter value: The value which needs to be converted (i.e. Lispified). + Parameter string_to_keyword: Boolean to signal that strings should be + converted into keywords. + """ + if isinstance(value, list): + return "(" + " ".join(["list"] + [lispify(v) for v in value]) + ")" + elif isinstance(value, bool): + return "T" if value else "NIL" + elif isinstance(value, int) or isinstance(value, float): + return str(value) + elif isinstance(value, str): + if len(value) == 1: + return f"#\\{value}" + else: + return f":{value.lower()}" if string_to_keyword else f"\"{value}\"" + elif isinstance(value, dict): + acons_list = [] + for k, v in value.items(): + if k == "error": + return "NIL" + acons_list += ["'({0} . {1})".format(lispify(k, True), lispify(v))] + return "(" + " ".join(["list"] + acons_list) + ")" + elif value is None: + return "NIL" + else: + raise TypeError("lispify function does not know how to handle value of type: " + str(type(value))) + + +def clean_lispification(lispified): + listless = lispified.replace(" '(", " (").replace("(list ", "(").replace("(list", "(") + if listless[0] == '(': + listless = "'" + listless + return listless + + +def no_arguments(): + exercise_name = None + prob_spec = None + + while prob_spec == None or not os.path.exists(f"{prob_spec}/exercises"): + fp = input("Enter the path (relative or absolute) to the problem-specifications repository: ") + prob_spec = os.path.abspath(fp) + + top_loop = True + while top_loop: + while exercise_name == None or not os.path.exists(f"{prob_spec}/exercises/{exercise_name}"): + exercise_name = input("Enter the name of the exercise you wish to generate: ") + if os.path.exists(f"{TARGET}/{exercise_name}"): + while True: + confirmation = input("You are about to overwrite an existing exercise! Confirm (Y/N): ") + confirmation = confirmation.upper() + if confirmation == "Y" or confirmation == "YES": + top_loop = False + break + elif confirmation == "N" or confirmation == "NO": + exercise_name = None + break + else: + break + + author = input("Author's Github handle: ") + + prob_spec_exercise = f"{prob_spec}/exercises/{exercise_name}" + brand_new_exercise(exercise_name, prob_spec_exercise, author) + + +def execute_via_cli(args): + prob_spec = os.path.abspath(args.Path) + if not os.path.exists(f"{prob_spec}/exercises"): + print("lisp_exercise_generator: error: problem-specifications repository not found") + sys.exit() + + exercise_name = args.Exercise + if not os.path.exists(f"{prob_spec}/exercises/{exercise_name}"): + print("lisp_exercise_generator: error: exercise does not exist in problem-specifications repository") + sys.exit() + + if os.path.exists(f"{TARGET}/{exercise_name}") and not args.f: + print("lisp_exercise_generator: error: exercise already exists in common-lisp repository") + sys.exit() + + author = args.Author + prob_spec_exercise = f"{prob_spec}/exercises/{exercise_name}" + brand_new_exercise(exercise_name, prob_spec_exercise, author) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(prog = "lisp_exercise_generator", + description = "Practice exercise generator for Common Lisp", + usage = "%(prog)s [-f] [path exercise author]") + + parser.add_argument("Path", + metavar = "path", + action = "store", + type = str, + help = "relative or absolute path to problem-specifications repository", + nargs = "?") + parser.add_argument("Exercise", + metavar = "exercise", + action = "store", + type = str, + help = "name of the exercise to be generated", + nargs = "?") + parser.add_argument("Author", + metavar = "author", + action = "store", + type = str, + help = "author's Github handle", + nargs = "?") + parser.add_argument("-f", + action = "store_true", + help = "force overwrite of existing exercise folder") + + args = parser.parse_args() + + arg_states = [args.Path == None, args.Exercise == None, args.Author == None] + + if all(arg_states): + no_arguments() + elif any(arg_states): + arg_num = arg_states.count(False) + print(f"lisp_exercise_generator: error: expected 0 or 3 arguments - received {arg_num}") + sys.exit() + else: + execute_via_cli(args) + + print("\nDone!") From 289ef1035c6eca17ae2de449fc723deeb59fa1a9 Mon Sep 17 00:00:00 2001 From: Adrien-LUDWIG Date: Mon, 30 Oct 2023 00:28:42 +0100 Subject: [PATCH 04/14] Rename exercise generator --- bin/{lisp_exercise_generator.py => racket_exercise_generator.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename bin/{lisp_exercise_generator.py => racket_exercise_generator.py} (100%) diff --git a/bin/lisp_exercise_generator.py b/bin/racket_exercise_generator.py similarity index 100% rename from bin/lisp_exercise_generator.py rename to bin/racket_exercise_generator.py From 6396a5ce4771fa360d063c4d9587bc457709d5e9 Mon Sep 17 00:00:00 2001 From: Adrien-LUDWIG Date: Mon, 30 Oct 2023 00:29:44 +0100 Subject: [PATCH 05/14] Adapt exercise generator to Racket (in progress) --- bin/racket_exercise_generator.py | 343 +++++++++++++++++-------------- 1 file changed, 194 insertions(+), 149 deletions(-) diff --git a/bin/racket_exercise_generator.py b/bin/racket_exercise_generator.py index df0d696f..d7d266bf 100644 --- a/bin/racket_exercise_generator.py +++ b/bin/racket_exercise_generator.py @@ -1,13 +1,13 @@ import json, toml, os, argparse, sys, string from custom_json_encoder import CustomJSONEncoder -LEGITIMATE_CHARS = string.ascii_lowercase + string.digits + " -" -def find_common_lisp_main(): +def find_racket_main(): up_one = os.path.split(os.getcwd())[0] - return ".." if os.path.split(up_one)[1] == "common-lisp" else "." + return ".." if os.path.split(up_one)[1] == "racket" else "." -TARGET = os.path.abspath(find_common_lisp_main() + "/exercises/practice") + +TARGET = os.path.abspath(find_racket_main() + "/exercises/practice") def create_directory_structure(exercise_name): @@ -41,19 +41,19 @@ def create_meta_config(exercise_name, prob_spec_exercise, author): """ config_data = None with open(f"{prob_spec_exercise}/metadata.toml") as file: - config_data = toml.load(file) # Get the blurb, source, and source_url + config_data = toml.load(file) # Get the blurb, source, and source_url # Add the files, authors, and contributors to the config_data config_data["files"] = {} - config_data["files"]["test"] = [f"{exercise_name}-test.lisp"] - config_data["files"]["solution"] = [f"{exercise_name}.lisp"] - config_data["files"]["example"] = [".meta/example.lisp"] + config_data["files"]["test"] = [f"{exercise_name}-test.rkt"] + config_data["files"]["solution"] = [f"{exercise_name}.rkt"] + config_data["files"]["example"] = [".meta/example.rkt"] config_data["authors"] = [author] config_data["contributors"] = [] - with open(f"{TARGET}/{exercise_name}/.meta/config.json", 'w') as file: + with open(f"{TARGET}/{exercise_name}/.meta/config.json", "w") as file: # Encode into a string in json format and write to file - file.write(json.dumps(config_data, cls = CustomJSONEncoder, indent = 3)) + file.write(json.dumps(config_data, cls=CustomJSONEncoder, indent=3)) file.write("\n") @@ -70,10 +70,12 @@ def create_instructions(exercise_name, prob_spec_exercise): """ input_file = f"{prob_spec_exercise}/description.md" output_file = f"{TARGET}/{exercise_name}/.docs/instructions.md" - with open(input_file, 'r', encoding = "utf-8") as read_from: - with open(output_file, 'w', encoding = "utf-8") as write_to: + with open(input_file, "r", encoding="utf-8") as read_from: + with open(output_file, "w", encoding="utf-8") as write_to: # Replace first line with "# Instructions\n" during copy process - write_to.write("# Instructions\n" + "\n".join(read_from.read().split("\n")[1:])) + write_to.write( + "# Instructions\n" + "\n".join(read_from.read().split("\n")[1:]) + ) def create_test_example_solution_files(exercise_name, prob_spec_exercise): @@ -97,23 +99,24 @@ def create_test_example_solution_files(exercise_name, prob_spec_exercise): # Boilerplate test code. Multiline docstring format used to maintain # correct indentation and to increase readability. - exercise_string = """;; Ensures that {0}.lisp and the testing library are always loaded -(eval-when (:compile-toplevel :load-toplevel :execute) - (load "{0}") - (quicklisp-client:quickload :fiveam)) + # TODO: Define exn-msg-matches? only if errors should be tested + exercise_string = """#lang racket/base -;; Defines the testing package with symbols from {0} and FiveAM in scope -;; The `run-tests` function is exported for use by both the user and test-runner -(defpackage :{0}-test - (:use :cl :fiveam) - (:export :run-tests)) +(require "{0}.rkt") -;; Enter the testing package -(in-package :{0}-test) +(module+ test + (require rackunit rackunit/text-ui) + + (define (exn-msg-matches? msg f) + (with-handlers ([exn:fail? (lambda (exn) + (string=? (exn-message exn) msg))]) + (f))) -;; Define and enter a new FiveAM test-suite -(def-suite* {0}-suite) -""".format(exercise_name) + (define suite + (test-suite + \"{0} tests\"""".format( + exercise_name + ) # func_name_dict is a dictionary of all function names and their # expected input argument names. @@ -121,23 +124,25 @@ def create_test_example_solution_files(exercise_name, prob_spec_exercise): # tests_string is sandwiched between exercise_string and more boilerplate # code at the end of the file. - exercise_string += tests_string + """ -(defun run-tests (&optional (test-or-suite '{0}-suite)) - "Provides human readable results of test run. Default to entire suite." - (run! test-or-suite)) -""".format(exercise_name) + exercise_string += ( + tests_string + + """)) + + (run-tests suite)) +""" + ) - with open(f"{TARGET}/{exercise_name}/{exercise_name}-test.lisp", 'w') as file: + with open(f"{TARGET}/{exercise_name}/{exercise_name}-test.rkt", "w") as file: file.write(exercise_string) create_example_and_solution_files(exercise_name, func_name_dict) -def create_test(cases, exercise_name, fnd = dict()): +def create_test(cases, exercise_name, fnd=dict()): """ Auto-generates tests for the test file. - Parameter cases: A list of test cases to be Lispified. + Parameter cases: A list of test cases to be Racketified. Parameter exercise_name: Name of the exercise to be generated. Precondition: exercise_name is a string of a valid exercise. @@ -148,22 +153,26 @@ def create_test(cases, exercise_name, fnd = dict()): Returns a tuple of fnd and the test string """ + # Helper functions only used in create_test function def to_kebab_case(string): from_snake = string.replace("_", "-") - from_camel = "".join([f"-{c.lower()}" if c.isupper() else c for c in from_snake]) + from_camel = "".join( + [f"-{c.lower()}" if c.isupper() else c for c in from_snake] + ) return from_camel def to_predicate(string, expected_result): + # TODO: Understand, adapt and document this function if not isinstance(expected_result, bool): return string elif (partitioned := string.partition("-"))[2]: - if '-' in partitioned[2] or partitioned[2][-1] == 'p': + if "-" in partitioned[2] or partitioned[2][-1] == "p": return partitioned[2] + "-p" else: return partitioned[2] + "p" else: - return partitioned[0] + ("-p" if 'p' == partitioned[0][-1] else "p") + return partitioned[0] + ("-p" if "p" == partitioned[0][-1] else "p") # Normal code begins here output = "" @@ -175,20 +184,21 @@ def to_predicate(string, expected_result): func_params = [to_kebab_case(param) for param in list(case["input"])] fnd[function_name] = func_params - # Prepare the variables and their associated values for - # implementation inside a "let" - arg_pairs = [] - for var, value in case["input"].items(): - arg = "({0} {1})".format(to_kebab_case(var), clean_lispification(lispify(value))) - arg_pairs.append(arg) - let_args = ("\n" + " " * 10).join(arg_pairs) - - # Create the test name - cleaned = [c for c in case["description"].lower() if c in LEGITIMATE_CHARS] - description = "".join(cleaned).replace(" ", "-") - - output += create_test_string(description, let_args, case["expected"], - exercise_name, function_name, func_params) + # Prepare the arguments to pass to the tested function + args = [] + for value in case["input"].values(): + arg = racketify(value) + args.append(arg) + joined_args = " ".join(args) + + output += create_test_string( + case["description"], + joined_args, + case["expected"], + exercise_name, + function_name, + func_params, + ) except KeyError: # Recursively dig further into the data structure fnd, string = create_test(case["cases"], exercise_name, fnd) @@ -196,42 +206,40 @@ def to_predicate(string, expected_result): return fnd, output + def create_test_string(desc, args, expected, exercise, func_name, func_params): - result, let_result, close_paren = "", "", ")" - if isinstance(expected, bool): - result = f"is-{str(expected).lower()}" - close_paren = "" + # TODO: Check the better way to test equality depending on the type + equality = "" + if isinstance(expected, int) or isinstance(expected, float): + equality = "test-eqv?" + elif isinstance(expected, str) and len(expected) == 1: + equality = "test-equal?" + elif isinstance(expected, str): + equality = "test-equal?" else: - equality = "" - if isinstance(expected, int) or isinstance(expected, float): - equality = "=" - elif isinstance(expected, str) and len(expected) == 1: - equality = "char=" - elif isinstance(expected, str): - equality = "string=" - else: - equality = "equal" + equality = "test-equal?" - cleaned = clean_lispification(lispify(expected)) - if len(cleaned) < 35: - result = f"is ({equality} {cleaned}" - else: - result = f"is ({equality} result" - let_result = """ - (result {0})""".format(cleaned) + expected_result = racketify(expected) # Multiline docstring format used to maintain correct indentation # and to increase readability. + # TODO: Handle errors differently (exn-msg-matches? and lambda) return """ -(test {0} - (let ({1}{2}) - ({3} ({4}:{5} {6})))){7} -""".format(desc, args, let_result, result, exercise, func_name, " ".join(func_params), close_paren) + + ({0} "{1}" + ({2} {3}) + {4})""".format( + equality, + desc, + func_name, + args, + expected_result, + ) def create_example_and_solution_files(exercise_name, func_name_dict): """ - Auto-generates the .meta/example.lisp and the 'exercise'.lisp files. + Auto-generates the .meta/example.rkt and the 'exercise'.rkt files. Parameter exercise_name: Name of the exercise to be generated. Precondition: exercise_name is a string of a valid exercise. @@ -240,27 +248,30 @@ def create_example_and_solution_files(exercise_name, func_name_dict): expected input argument names. Precondition: func_name_dict is a dictionary. """ - # Create keywords of function names to be exported (vertically aligned) - exports_string = ("\n" + " " * 11).join([":" + k for k in func_name_dict]) # Boilerplate code. Multiline docstring format used to maintain # correct indentation and to increase readability. - file_string = """(defpackage :{0} - (:use :cl) - (:export {1})) + file_string = """#lang racket -(in-package :{0}) -""".format(exercise_name, exports_string) +(provide {0}) +""".format( + func_name_dict.keys() + ) # For each function-parameters pairing, add the requisite function # definition to the file. for func, params in func_name_dict.items(): - file_string += "\n(defun {0} ({1}))\n".format(func, " ".join(params)) - - with open(f"{TARGET}/{exercise_name}/{exercise_name}.lisp", 'w') as file: + file_string += """ +(define ({0} {1}) + (error "Not implemented yet")) +""".format( + func, " ".join(params) + ) + + with open(f"{TARGET}/{exercise_name}/{exercise_name}.rkt", "w") as file: file.write(file_string) - with open(f"{TARGET}/{exercise_name}/.meta/example.lisp", 'w') as file: + with open(f"{TARGET}/{exercise_name}/.meta/example.rkt", "w") as file: file.write(file_string) @@ -275,6 +286,7 @@ def create_test_toml(exercise_name, prob_spec_exercise): in the problem-specifications repository. Precondition: prob_spec_exercise is a string of a valid filepath. """ + # Nested helper function that will either build a string in a toml # style, or recursively dig until it finds the data and then build # a string. @@ -283,7 +295,9 @@ def find_uuids_and_descriptions(cases): for case in cases: try: # Add lines in toml style - output += "\n[{0}]\ndescription = \"{1}\"\n".format(case["uuid"], case["description"]) + output += '\n[{0}]\ndescription = "{1}"\n'.format( + case["uuid"], case["description"] + ) except KeyError: # Recursively dig further into the data structure output += find_uuids_and_descriptions(case["cases"]) @@ -296,17 +310,24 @@ def find_uuids_and_descriptions(cases): data = json.load(file) # Boilerplate comment at top of test.toml - toml_string = """# This is an auto-generated file. Regular comments will be removed when this -# file is regenerated. Regenerating will not touch any manually added keys, -# so comments can be added in a "comment" key. + toml_string = """# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. """ toml_string += find_uuids_and_descriptions(data["cases"]) - with open(f"{TARGET}/{exercise_name}/.meta/tests.toml", 'w') as file: + with open(f"{TARGET}/{exercise_name}/.meta/tests.toml", "w") as file: file.write(toml_string) -def brand_new_exercise(exercise_name :str, prob_spec_exercise :str, author :str = ""): +def brand_new_exercise(exercise_name: str, prob_spec_exercise: str, author: str = ""): """ A delegation function. @@ -332,51 +353,51 @@ def brand_new_exercise(exercise_name :str, prob_spec_exercise :str, author :str create_test_toml(exercise_name, prob_spec_exercise) -def lispify(value, string_to_keyword = False): +def racketify(value, string_to_keyword=False): """ - Converts a given value from a Python data type into its Lisp counterpart. + Converts a given value from a Python data type into its Racket counterpart. Returns the following conversions: lists/arrays -> lists - bools -> T or NIL + bools -> #t or #f ints -> ints floats -> floats strings -> strings (or chars if string len == 1) or keywords - dicts -> acons lists (or NIL if key == "error") + # TODO: see what to do with "error" + dicts -> hash tables (or NIL if key == "error") - Parameter value: The value which needs to be converted (i.e. Lispified). + Parameter value: The value which needs to be converted (i.e. Racketified). Parameter string_to_keyword: Boolean to signal that strings should be converted into keywords. """ if isinstance(value, list): - return "(" + " ".join(["list"] + [lispify(v) for v in value]) + ")" + return "'(" + " ".join([racketify(v) for v in value]) + ")" elif isinstance(value, bool): - return "T" if value else "NIL" + return "#t" if value else "#f" elif isinstance(value, int) or isinstance(value, float): return str(value) elif isinstance(value, str): if len(value) == 1: + # TODO: Handle special chars (space, tab, ...) + # see: https://docs.racket-lang.org/reference/reader.html#%28part._parse-character%29 return f"#\\{value}" else: - return f":{value.lower()}" if string_to_keyword else f"\"{value}\"" + return f":{value.lower()}" if string_to_keyword else f'"{value}"' elif isinstance(value, dict): - acons_list = [] + key_value_pairs = [] for k, v in value.items(): + # TODO: see what to do with "error" if k == "error": - return "NIL" - acons_list += ["'({0} . {1})".format(lispify(k, True), lispify(v))] - return "(" + " ".join(["list"] + acons_list) + ")" + return "null" + key_value_pairs += ["({0} . {1})".format(racketify(k, True), racketify(v))] + return "'#hash(" + " ".join(key_value_pairs) + ")" elif value is None: - return "NIL" + return "null" else: - raise TypeError("lispify function does not know how to handle value of type: " + str(type(value))) - - -def clean_lispification(lispified): - listless = lispified.replace(" '(", " (").replace("(list ", "(").replace("(list", "(") - if listless[0] == '(': - listless = "'" + listless - return listless + raise TypeError( + "racketify function does not know how to handle value of type: " + + str(type(value)) + ) def no_arguments(): @@ -384,16 +405,24 @@ def no_arguments(): prob_spec = None while prob_spec == None or not os.path.exists(f"{prob_spec}/exercises"): - fp = input("Enter the path (relative or absolute) to the problem-specifications repository: ") + fp = input( + "Enter the path (relative or absolute) to the problem-specifications repository: " + ) prob_spec = os.path.abspath(fp) top_loop = True while top_loop: - while exercise_name == None or not os.path.exists(f"{prob_spec}/exercises/{exercise_name}"): - exercise_name = input("Enter the name of the exercise you wish to generate: ") + while exercise_name == None or not os.path.exists( + f"{prob_spec}/exercises/{exercise_name}" + ): + exercise_name = input( + "Enter the name of the exercise you wish to generate: " + ) if os.path.exists(f"{TARGET}/{exercise_name}"): while True: - confirmation = input("You are about to overwrite an existing exercise! Confirm (Y/N): ") + confirmation = input( + "You are about to overwrite an existing exercise! Confirm (Y/N): " + ) confirmation = confirmation.upper() if confirmation == "Y" or confirmation == "YES": top_loop = False @@ -413,16 +442,22 @@ def no_arguments(): def execute_via_cli(args): prob_spec = os.path.abspath(args.Path) if not os.path.exists(f"{prob_spec}/exercises"): - print("lisp_exercise_generator: error: problem-specifications repository not found") + print( + "racket_exercise_generator: error: problem-specifications repository not found" + ) sys.exit() exercise_name = args.Exercise if not os.path.exists(f"{prob_spec}/exercises/{exercise_name}"): - print("lisp_exercise_generator: error: exercise does not exist in problem-specifications repository") + print( + "racket_exercise_generator: error: exercise does not exist in problem-specifications repository" + ) sys.exit() if os.path.exists(f"{TARGET}/{exercise_name}") and not args.f: - print("lisp_exercise_generator: error: exercise already exists in common-lisp repository") + print( + "racket_exercise_generator: error: exercise already exists in Racket repository" + ) sys.exit() author = args.Author @@ -430,32 +465,40 @@ def execute_via_cli(args): brand_new_exercise(exercise_name, prob_spec_exercise, author) -if __name__ == '__main__': - parser = argparse.ArgumentParser(prog = "lisp_exercise_generator", - description = "Practice exercise generator for Common Lisp", - usage = "%(prog)s [-f] [path exercise author]") - - parser.add_argument("Path", - metavar = "path", - action = "store", - type = str, - help = "relative or absolute path to problem-specifications repository", - nargs = "?") - parser.add_argument("Exercise", - metavar = "exercise", - action = "store", - type = str, - help = "name of the exercise to be generated", - nargs = "?") - parser.add_argument("Author", - metavar = "author", - action = "store", - type = str, - help = "author's Github handle", - nargs = "?") - parser.add_argument("-f", - action = "store_true", - help = "force overwrite of existing exercise folder") +if __name__ == "__main__": + parser = argparse.ArgumentParser( + prog="racket_exercise_generator", + description="Practice exercise generator for Racket", + usage="%(prog)s [-f] [path exercise author]", + ) + + parser.add_argument( + "Path", + metavar="path", + action="store", + type=str, + help="relative or absolute path to problem-specifications repository", + nargs="?", + ) + parser.add_argument( + "Exercise", + metavar="exercise", + action="store", + type=str, + help="name of the exercise to be generated", + nargs="?", + ) + parser.add_argument( + "Author", + metavar="author", + action="store", + type=str, + help="author's Github handle", + nargs="?", + ) + parser.add_argument( + "-f", action="store_true", help="force overwrite of existing exercise folder" + ) args = parser.parse_args() @@ -465,7 +508,9 @@ def execute_via_cli(args): no_arguments() elif any(arg_states): arg_num = arg_states.count(False) - print(f"lisp_exercise_generator: error: expected 0 or 3 arguments - received {arg_num}") + print( + f"racket_exercise_generator: error: expected 0 or 3 arguments - received {arg_num}" + ) sys.exit() else: execute_via_cli(args) From 765ee0a5ed8aae5a25b9c3e1f7fa799cefaffac7 Mon Sep 17 00:00:00 2001 From: Adrien-LUDWIG Date: Mon, 30 Oct 2023 00:34:22 +0100 Subject: [PATCH 06/14] Add TODOs to the exercise generator --- bin/racket_exercise_generator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bin/racket_exercise_generator.py b/bin/racket_exercise_generator.py index d7d266bf..1e94c0b9 100644 --- a/bin/racket_exercise_generator.py +++ b/bin/racket_exercise_generator.py @@ -53,6 +53,7 @@ def create_meta_config(exercise_name, prob_spec_exercise, author): with open(f"{TARGET}/{exercise_name}/.meta/config.json", "w") as file: # Encode into a string in json format and write to file + # TODO: Use configlet instead? What about the author? file.write(json.dumps(config_data, cls=CustomJSONEncoder, indent=3)) file.write("\n") @@ -347,9 +348,10 @@ def brand_new_exercise(exercise_name: str, prob_spec_exercise: str, author: str create_directory_structure(exercise_name) # The order that these functions are called is unimportant + create_test_example_solution_files(exercise_name, prob_spec_exercise) + # TODO: Use configlet instead? create_meta_config(exercise_name, prob_spec_exercise, author) create_instructions(exercise_name, prob_spec_exercise) - create_test_example_solution_files(exercise_name, prob_spec_exercise) create_test_toml(exercise_name, prob_spec_exercise) From 3fb6fe2cacd7d66ee254eac476a99f7ab5778de2 Mon Sep 17 00:00:00 2001 From: Adrien-LUDWIG Date: Mon, 30 Oct 2023 00:36:55 +0100 Subject: [PATCH 07/14] Ignore files generated by the exercise generator --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index d9e829bf..0a3b0c41 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ compiled/ .#* *.bak TAGS + +# Exercise generator +__pycache__ From 2e0fa994f95a4e5ba999f64215c0fbeb3f78e949 Mon Sep 17 00:00:00 2001 From: Adrien-LUDWIG Date: Mon, 30 Oct 2023 01:05:14 +0100 Subject: [PATCH 08/14] Handle errors in exercise generation --- bin/racket_exercise_generator.py | 53 ++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/bin/racket_exercise_generator.py b/bin/racket_exercise_generator.py index 1e94c0b9..0b388171 100644 --- a/bin/racket_exercise_generator.py +++ b/bin/racket_exercise_generator.py @@ -100,19 +100,35 @@ def create_test_example_solution_files(exercise_name, prob_spec_exercise): # Boilerplate test code. Multiline docstring format used to maintain # correct indentation and to increase readability. - # TODO: Define exn-msg-matches? only if errors should be tested exercise_string = """#lang racket/base (require "{0}.rkt") (module+ test (require rackunit rackunit/text-ui) - +""".format( + exercise_name + ) + + # Check if some tests expect errors + expect_errors = any( + [ + "error" in case["expected"].keys() + for case in data["cases"] + if isinstance(case["expected"], dict) + ] + ) + + # If so, add the definition of the helper function to test returned errors + if expect_errors: + exercise_string += """ (define (exn-msg-matches? msg f) (with-handlers ([exn:fail? (lambda (exn) (string=? (exn-message exn) msg))]) (f))) +""" + exercise_string += """ (define suite (test-suite \"{0} tests\"""".format( @@ -196,9 +212,7 @@ def to_predicate(string, expected_result): case["description"], joined_args, case["expected"], - exercise_name, function_name, - func_params, ) except KeyError: # Recursively dig further into the data structure @@ -208,7 +222,11 @@ def to_predicate(string, expected_result): return fnd, output -def create_test_string(desc, args, expected, exercise, func_name, func_params): +def create_test_string(desc, args, expected, func_name): + # Handle errors differently + if isinstance(expected, dict) and "error" in expected.keys(): + return create_error_test_string(desc, args, expected, func_name) + # TODO: Check the better way to test equality depending on the type equality = "" if isinstance(expected, int) or isinstance(expected, float): @@ -224,7 +242,6 @@ def create_test_string(desc, args, expected, exercise, func_name, func_params): # Multiline docstring format used to maintain correct indentation # and to increase readability. - # TODO: Handle errors differently (exn-msg-matches? and lambda) return """ ({0} "{1}" @@ -238,6 +255,24 @@ def create_test_string(desc, args, expected, exercise, func_name, func_params): ) +def create_error_test_string(desc, args, expected, func_name): + error_message = racketify(expected) + + # Multiline docstring format used to maintain correct indentation + # and to increase readability. + return """ + + (test-true "{0}" + (exn-msg-matches? + {1} + (lambda () ({2} {3}))))""".format( + desc, + error_message, + func_name, + args, + ) + + def create_example_and_solution_files(exercise_name, func_name_dict): """ Auto-generates the .meta/example.rkt and the 'exercise'.rkt files. @@ -365,8 +400,7 @@ def racketify(value, string_to_keyword=False): ints -> ints floats -> floats strings -> strings (or chars if string len == 1) or keywords - # TODO: see what to do with "error" - dicts -> hash tables (or NIL if key == "error") + dicts -> hash tables (or the expected error message if key == "error") Parameter value: The value which needs to be converted (i.e. Racketified). Parameter string_to_keyword: Boolean to signal that strings should be @@ -388,9 +422,8 @@ def racketify(value, string_to_keyword=False): elif isinstance(value, dict): key_value_pairs = [] for k, v in value.items(): - # TODO: see what to do with "error" if k == "error": - return "null" + return f'"{v}"' key_value_pairs += ["({0} . {1})".format(racketify(k, True), racketify(v))] return "'#hash(" + " ".join(key_value_pairs) + ")" elif value is None: From 2e7fdc2004a87033c8e04132bf718795f947e0fa Mon Sep 17 00:00:00 2001 From: Adrien-LUDWIG Date: Mon, 30 Oct 2023 01:08:47 +0100 Subject: [PATCH 09/14] Fix provided functions in example and slug files --- bin/racket_exercise_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/racket_exercise_generator.py b/bin/racket_exercise_generator.py index 0b388171..2f5e3448 100644 --- a/bin/racket_exercise_generator.py +++ b/bin/racket_exercise_generator.py @@ -291,7 +291,7 @@ def create_example_and_solution_files(exercise_name, func_name_dict): (provide {0}) """.format( - func_name_dict.keys() + "".join(func_name_dict.keys()) ) # For each function-parameters pairing, add the requisite function From 1ad70864a08deae70049a66a567b28c7b96dd94e Mon Sep 17 00:00:00 2001 From: Adrien-LUDWIG Date: Mon, 30 Oct 2023 01:18:18 +0100 Subject: [PATCH 10/14] Revert adding unrelated files --- .../binary-search/.docs/instructions.md | 29 ------------ .../binary-search/.docs/introduction.md | 13 ----- .../practice/binary-search/.meta/config.json | 11 ----- .../practice/binary-search/.meta/example.rkt | 14 ------ .../practice/binary-search/.meta/tests.toml | 43 ----------------- .../binary-search/binary-search-test.rkt | 47 ------------------- .../practice/binary-search/binary-search.rkt | 6 --- 7 files changed, 163 deletions(-) delete mode 100644 exercises/practice/binary-search/.docs/instructions.md delete mode 100644 exercises/practice/binary-search/.docs/introduction.md delete mode 100644 exercises/practice/binary-search/.meta/config.json delete mode 100644 exercises/practice/binary-search/.meta/example.rkt delete mode 100644 exercises/practice/binary-search/.meta/tests.toml delete mode 100644 exercises/practice/binary-search/binary-search-test.rkt delete mode 100644 exercises/practice/binary-search/binary-search.rkt diff --git a/exercises/practice/binary-search/.docs/instructions.md b/exercises/practice/binary-search/.docs/instructions.md deleted file mode 100644 index 12f4358e..00000000 --- a/exercises/practice/binary-search/.docs/instructions.md +++ /dev/null @@ -1,29 +0,0 @@ -# Instructions - -Your task is to implement a binary search algorithm. - -A binary search algorithm finds an item in a list by repeatedly splitting it in half, only keeping the half which contains the item we're looking for. -It allows us to quickly narrow down the possible locations of our item until we find it, or until we've eliminated all possible locations. - -~~~~exercism/caution -Binary search only works when a list has been sorted. -~~~~ - -The algorithm looks like this: - -- Find the middle element of a _sorted_ list and compare it with the item we're looking for. -- If the middle element is our item, then we're done! -- If the middle element is greater than our item, we can eliminate that element and all the elements **after** it. -- If the middle element is less than our item, we can eliminate that element and all the elements **before** it. -- If every element of the list has been eliminated then the item is not in the list. -- Otherwise, repeat the process on the part of the list that has not been eliminated. - -Here's an example: - -Let's say we're looking for the number 23 in the following sorted list: `[4, 8, 12, 16, 23, 28, 32]`. - -- We start by comparing 23 with the middle element, 16. -- Since 23 is greater than 16, we can eliminate the left half of the list, leaving us with `[23, 28, 32]`. -- We then compare 23 with the new middle element, 28. -- Since 23 is less than 28, we can eliminate the right half of the list: `[23]`. -- We've found our item. diff --git a/exercises/practice/binary-search/.docs/introduction.md b/exercises/practice/binary-search/.docs/introduction.md deleted file mode 100644 index 03496599..00000000 --- a/exercises/practice/binary-search/.docs/introduction.md +++ /dev/null @@ -1,13 +0,0 @@ -# Introduction - -You have stumbled upon a group of mathematicians who are also singer-songwriters. -They have written a song for each of their favorite numbers, and, as you can imagine, they have a lot of favorite numbers (like [0][zero] or [73][seventy-three] or [6174][kaprekars-constant]). - -You are curious to hear the song for your favorite number, but with so many songs to wade through, finding the right song could take a while. -Fortunately, they have organized their songs in a playlist sorted by the title — which is simply the number that the song is about. - -You realize that you can use a binary search algorithm to quickly find a song given the title. - -[zero]: https://en.wikipedia.org/wiki/0 -[seventy-three]: https://en.wikipedia.org/wiki/73_(number) -[kaprekars-constant]: https://en.wikipedia.org/wiki/6174_(number) diff --git a/exercises/practice/binary-search/.meta/config.json b/exercises/practice/binary-search/.meta/config.json deleted file mode 100644 index 0918b937..00000000 --- a/exercises/practice/binary-search/.meta/config.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "authors": ["Adrien-LUDWIG"], - "files": { - "solution": ["binary-search.rkt"], - "test": ["binary-search-test.rkt"], - "example": [".meta/example.rkt"] - }, - "blurb": "Implement a binary search algorithm.", - "source": "Wikipedia", - "source_url": "https://en.wikipedia.org/wiki/Binary_search_algorithm" -} diff --git a/exercises/practice/binary-search/.meta/example.rkt b/exercises/practice/binary-search/.meta/example.rkt deleted file mode 100644 index a0a4663a..00000000 --- a/exercises/practice/binary-search/.meta/example.rkt +++ /dev/null @@ -1,14 +0,0 @@ -#lang racket - -(provide binary-search) - -(define (binary-search array value) - (define (rec left right) - (cond - [(> left right) (error "Value not in array")] - [else (define mid (+ left (quotient (- right left) 2))) - (cond - [(< value (list-ref array mid)) (rec left (sub1 mid))] - [(< (list-ref array mid) value) (rec (add1 mid) right)] - [else mid])])) - (rec 0 (sub1 (length array)))) diff --git a/exercises/practice/binary-search/.meta/tests.toml b/exercises/practice/binary-search/.meta/tests.toml deleted file mode 100644 index 61e2b068..00000000 --- a/exercises/practice/binary-search/.meta/tests.toml +++ /dev/null @@ -1,43 +0,0 @@ -# This is an auto-generated file. -# -# Regenerating this file via `configlet sync` will: -# - Recreate every `description` key/value pair -# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications -# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) -# - Preserve any other key/value pair -# -# As user-added comments (using the # character) will be removed when this file -# is regenerated, comments can be added via a `comment` key. - -[b55c24a9-a98d-4379-a08c-2adcf8ebeee8] -description = "finds a value in an array with one element" - -[73469346-b0a0-4011-89bf-989e443d503d] -description = "finds a value in the middle of an array" - -[327bc482-ab85-424e-a724-fb4658e66ddb] -description = "finds a value at the beginning of an array" - -[f9f94b16-fe5e-472c-85ea-c513804c7d59] -description = "finds a value at the end of an array" - -[f0068905-26e3-4342-856d-ad153cadb338] -description = "finds a value in an array of odd length" - -[fc316b12-c8b3-4f5e-9e89-532b3389de8c] -description = "finds a value in an array of even length" - -[da7db20a-354f-49f7-a6a1-650a54998aa6] -description = "identifies that a value is not included in the array" - -[95d869ff-3daf-4c79-b622-6e805c675f97] -description = "a value smaller than the array's smallest value is not found" - -[8b24ef45-6e51-4a94-9eac-c2bf38fdb0ba] -description = "a value larger than the array's largest value is not found" - -[f439a0fa-cf42-4262-8ad1-64bf41ce566a] -description = "nothing is found in an empty array" - -[2c353967-b56d-40b8-acff-ce43115eed64] -description = "nothing is found when the left and right bounds cross" diff --git a/exercises/practice/binary-search/binary-search-test.rkt b/exercises/practice/binary-search/binary-search-test.rkt deleted file mode 100644 index af570a6e..00000000 --- a/exercises/practice/binary-search/binary-search-test.rkt +++ /dev/null @@ -1,47 +0,0 @@ -#lang racket/base - -(require "binary-search.rkt") - -(module+ test - (require rackunit rackunit/text-ui) - - (define suite - (test-suite - "binary-search tests" - - (test-eqv? "finds a value in an array with one element" - (binary-search (list 6) 6) - 0) - (test-eqv? "finds a value in the middle of an array" - (binary-search (list 1 3 4 6 8 9 11) 6) - 3) - (test-eqv? "finds a value at the beginning of an array" - (binary-search (list 1 3 4 6 8 9 11) 1) - 0) - (test-eqv? "finds a value at the end of an array" - (binary-search (list 1 3 4 6 8 9 11) 11) - 6) - (test-eqv? "finds a value in an array of odd length" - (binary-search (list 1 3 5 8 13 21 34 55 89 144 233 377 634) 144) - 9) - (test-eqv? "finds a value in an array of even length" - (binary-search (list 1 3 5 8 13 21 34 55 89 144 233 377) 21) - 5) - (test-exn "identifies that a value is not included in the array" - exn:fail? - (lambda () (binary-search (list 1 3 4 6 8 9 11) 7))) - (test-exn "a value smaller than the array's smallest value is not found" - exn:fail? - (lambda () (binary-search (list 1 3 4 6 8 9 11) 0))) - (test-exn "a value larger than the array's smallest value is not found" - exn:fail? - (lambda () (binary-search (list 1 3 4 6 8 9 11) 13))) - (test-exn "nothing is found in an empty array" - exn:fail? - (lambda () (binary-search '() 1))) - (test-exn "nothing is found when the left and right bounds cross" - exn:fail? - (lambda () (binary-search (list 1 2) 0))) - )) - - (run-tests suite)) diff --git a/exercises/practice/binary-search/binary-search.rkt b/exercises/practice/binary-search/binary-search.rkt deleted file mode 100644 index 04e81bbf..00000000 --- a/exercises/practice/binary-search/binary-search.rkt +++ /dev/null @@ -1,6 +0,0 @@ -#lang racket - -(provide binary-search) - -(define (binary-search array value) - (error "Not implemented yet")) From 6200dcc168daa9c3c37ffc33b8d5fbb0af71d31d Mon Sep 17 00:00:00 2001 From: Adrien-LUDWIG Date: Mon, 30 Oct 2023 13:15:09 +0100 Subject: [PATCH 11/14] Do not check error message in generated tests --- bin/racket_exercise_generator.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/bin/racket_exercise_generator.py b/bin/racket_exercise_generator.py index 2f5e3448..3d1e29c3 100644 --- a/bin/racket_exercise_generator.py +++ b/bin/racket_exercise_generator.py @@ -225,7 +225,7 @@ def to_predicate(string, expected_result): def create_test_string(desc, args, expected, func_name): # Handle errors differently if isinstance(expected, dict) and "error" in expected.keys(): - return create_error_test_string(desc, args, expected, func_name) + return create_error_test_string(desc, args, func_name) # TODO: Check the better way to test equality depending on the type equality = "" @@ -255,19 +255,15 @@ def create_test_string(desc, args, expected, func_name): ) -def create_error_test_string(desc, args, expected, func_name): - error_message = racketify(expected) - +def create_error_test_string(desc, args, func_name): # Multiline docstring format used to maintain correct indentation # and to increase readability. return """ (test-true "{0}" - (exn-msg-matches? - {1} - (lambda () ({2} {3}))))""".format( + (exn:fail? + (lambda () ({1} {2}))))""".format( desc, - error_message, func_name, args, ) From 980ed1b8983c94c5fe547af8aece1afd7f681024 Mon Sep 17 00:00:00 2001 From: Adrien-LUDWIG Date: Mon, 30 Oct 2023 13:19:34 +0100 Subject: [PATCH 12/14] Revert breaking config.json formatting --- config.json | 98 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 72 insertions(+), 26 deletions(-) diff --git a/config.json b/config.json index b005b326..b9514112 100644 --- a/config.json +++ b/config.json @@ -19,10 +19,18 @@ "average_run_time": 2 }, "files": { - "solution": ["%{kebab_slug}.rkt"], - "test": ["%{kebab_slug}-test.rkt"], - "example": [".meta/example.rkt"], - "exemplar": [".meta/exemplar.rkt"] + "solution": [ + "%{kebab_slug}.rkt" + ], + "test": [ + "%{kebab_slug}-test.rkt" + ], + "example": [ + ".meta/example.rkt" + ], + "exemplar": [ + ".meta/exemplar.rkt" + ] }, "exercises": { "practice": [ @@ -30,7 +38,9 @@ "slug": "hello-world", "name": "Hello World", "uuid": "4fb471fc-4e6d-486d-abf5-939e89f028fc", - "practices": ["strings"], + "practices": [ + "strings" + ], "prerequisites": [], "difficulty": 1 }, @@ -46,7 +56,10 @@ "slug": "two-fer", "name": "Two Fer", "uuid": "27ffc2d2-e950-40a1-90fa-a1f3eec4fd36", - "practices": ["optional-values", "text-formatting"], + "practices": [ + "optional-values", + "text-formatting" + ], "prerequisites": [], "difficulty": 1 }, @@ -62,7 +75,9 @@ "slug": "difference-of-squares", "name": "Difference of Squares", "uuid": "a3d9a2bb-a80a-487f-b529-64e20f7cf9b5", - "practices": ["math"], + "practices": [ + "math" + ], "prerequisites": [], "difficulty": 1 }, @@ -78,7 +93,9 @@ "slug": "perfect-numbers", "name": "Perfect Numbers", "uuid": "72b2c36b-fcd5-4c8c-89e7-98bf24faaa3e", - "practices": ["math"], + "practices": [ + "math" + ], "prerequisites": [], "difficulty": 1 }, @@ -102,7 +119,9 @@ "slug": "collatz-conjecture", "name": "Collatz Conjecture", "uuid": "28102e69-dad0-4f3c-8cdf-5a18a73178a4", - "practices": ["math"], + "practices": [ + "math" + ], "prerequisites": [], "difficulty": 1 }, @@ -126,7 +145,11 @@ "slug": "twelve-days", "name": "Twelve Days", "uuid": "5961220f-5d33-4e97-a4bb-8b375714a8fc", - "practices": ["enumerations", "reduce", "strings"], + "practices": [ + "enumerations", + "reduce", + "strings" + ], "prerequisites": [], "difficulty": 2 }, @@ -134,7 +157,12 @@ "slug": "isogram", "name": "Isogram", "uuid": "2fa21cb9-5469-4b95-9b78-c6f4dec6f67f", - "practices": ["algorithms", "conditionals", "loops", "strings"], + "practices": [ + "algorithms", + "conditionals", + "loops", + "strings" + ], "prerequisites": [], "difficulty": 3 }, @@ -157,7 +185,11 @@ "slug": "armstrong-numbers", "name": "Armstrong Numbers", "uuid": "0fa9d2c6-f170-43b4-a28a-eb4994b4140e", - "practices": ["algorithms", "loops", "math"], + "practices": [ + "algorithms", + "loops", + "math" + ], "prerequisites": [], "difficulty": 1 }, @@ -165,7 +197,11 @@ "slug": "affine-cipher", "name": "Affine Cipher", "uuid": "529c3ced-eefa-402c-b901-ea39ae2fb24e", - "practices": ["algorithms", "cryptography", "strings"], + "practices": [ + "algorithms", + "cryptography", + "strings" + ], "prerequisites": [], "difficulty": 4 }, @@ -190,7 +226,12 @@ "slug": "all-your-base", "name": "All Your Base", "uuid": "f6617861-da32-4735-8c7f-5ae57a8cf44a", - "practices": ["integers", "map", "math", "transforming"], + "practices": [ + "integers", + "map", + "math", + "transforming" + ], "prerequisites": [], "difficulty": 1 }, @@ -218,14 +259,6 @@ "prerequisites": [], "difficulty": 1 }, - { - "slug": "binary-search", - "name": "Binary Search", - "uuid": "033094e7-fd15-42ee-bbc8-0fb12c2cdb65", - "practices": [], - "prerequisites": [], - "difficulty": 2 - }, { "slug": "clock", "name": "Clock", @@ -308,7 +341,9 @@ "slug": "reverse-string", "name": "Reverse String", "uuid": "135c7e52-04ac-4cde-9617-e16870dacb3f", - "practices": ["strings"], + "practices": [ + "strings" + ], "prerequisites": [], "difficulty": 1 }, @@ -364,7 +399,11 @@ "slug": "atbash-cipher", "name": "Atbash Cipher", "uuid": "92d97f99-04f9-4a52-942d-d1a9fa96375f", - "practices": ["algorithms", "cryptography", "strings"], + "practices": [ + "algorithms", + "cryptography", + "strings" + ], "prerequisites": [], "difficulty": 3 }, @@ -372,7 +411,10 @@ "slug": "variable-length-quantity", "name": "Variable Length Quantity", "uuid": "a6f90be2-1360-479e-8a6f-4f2d8b492c71", - "practices": ["algorithms", "bitwise-operations"], + "practices": [ + "algorithms", + "bitwise-operations" + ], "prerequisites": [], "difficulty": 7 }, @@ -413,7 +455,11 @@ "slug": "house", "name": "House", "uuid": "79861b42-3bf8-4fe9-91f0-72b4c0a164f9", - "practices": ["algorithms", "recursion", "strings"], + "practices": [ + "algorithms", + "recursion", + "strings" + ], "prerequisites": [], "difficulty": 4 }, From f0493412dedbd6214107702ee2ae710eb9e8e343 Mon Sep 17 00:00:00 2001 From: Adrien-LUDWIG Date: Mon, 30 Oct 2023 16:48:35 +0100 Subject: [PATCH 13/14] Fix generation for tests expecting errors --- bin/racket_exercise_generator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/racket_exercise_generator.py b/bin/racket_exercise_generator.py index 3d1e29c3..7c2f50b5 100644 --- a/bin/racket_exercise_generator.py +++ b/bin/racket_exercise_generator.py @@ -260,9 +260,9 @@ def create_error_test_string(desc, args, func_name): # and to increase readability. return """ - (test-true "{0}" - (exn:fail? - (lambda () ({1} {2}))))""".format( + (test-exn "{0}" + exn:fail? + (lambda () ({1} {2})))""".format( desc, func_name, args, From d5de0b7389d168e1459cbdad9e7bf3f2761d89cd Mon Sep 17 00:00:00 2001 From: Adrien-LUDWIG Date: Mon, 30 Oct 2023 17:15:14 +0100 Subject: [PATCH 14/14] Fix generation of provided functions --- bin/racket_exercise_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/racket_exercise_generator.py b/bin/racket_exercise_generator.py index 7c2f50b5..c20f7a53 100644 --- a/bin/racket_exercise_generator.py +++ b/bin/racket_exercise_generator.py @@ -287,7 +287,7 @@ def create_example_and_solution_files(exercise_name, func_name_dict): (provide {0}) """.format( - "".join(func_name_dict.keys()) + " ".join(func_name_dict.keys()) ) # For each function-parameters pairing, add the requisite function