From 3d3e2e57e6c6233dfbcf42ac7ab6214778396e46 Mon Sep 17 00:00:00 2001 From: Kah Goh Date: Mon, 29 Jan 2024 21:01:02 +0800 Subject: [PATCH] Tidy up List Ops exercise - Reduce difficulty (since most of the skeleton has been added) - Move template helper methods to generator_plugin - Add spacing between tests --- config.json | 2 +- .../practice/list-ops/.meta/src/example.cr | 18 ++-- .../practice/list-ops/.meta/test_template.ecr | 55 ++++-------- .../practice/list-ops/spec/list_ops_spec.cr | 87 +++++++++---------- exercises/practice/list-ops/src/list_ops.cr | 18 ++-- .../spec/generator_plugins_tests.cr | 75 ++++++++++++---- test-generator/src/generator_help.cr | 16 ---- test-generator/src/generator_plugins.cr | 38 +++++++- 8 files changed, 175 insertions(+), 134 deletions(-) diff --git a/config.json b/config.json index d563237e..85351788 100644 --- a/config.json +++ b/config.json @@ -1398,7 +1398,7 @@ "procs-blocks" ], "prerequisites": [], - "difficulty": 5 + "difficulty": 3 } ] }, diff --git a/exercises/practice/list-ops/.meta/src/example.cr b/exercises/practice/list-ops/.meta/src/example.cr index 3af51768..6f67197c 100644 --- a/exercises/practice/list-ops/.meta/src/example.cr +++ b/exercises/practice/list-ops/.meta/src/example.cr @@ -1,9 +1,9 @@ -class ListOps(T) - def append(list1 : Array(T), list2 : Array(T)) : Array(T) +module ListOps(T) + def self.append(list1 : Array(T), list2 : Array(T)) : Array(T) concat([list1, list2]) end - def concat(lists : Array(Array(T))) + def self.concat(lists : Array(Array(T))) result = [] of T lists.each { |sublist| sublist.each { |elem| result << elem } @@ -11,7 +11,7 @@ class ListOps(T) result end - def filter(list : Array(T), function : T -> Bool) : Array(T) + def self.filter(list : Array(T), function : T -> Bool) : Array(T) filtered = [] of T list.each { |elem| if function.call(elem) @@ -21,25 +21,25 @@ class ListOps(T) filtered end - def length(list : Array(T)) : Int + def self.length(list : Array(T)) : Int count = 0 list.each { |_| count = count + 1 } count end - def map(list : Array(T), function : T -> _) + def self.map(list : Array(T), function : T -> _) mapped = [] of T list.each { |elem| mapped << function.call(elem) } mapped end - def foldl(list : Array(T), initial : _, function : _, T -> _) + def self.foldl(list : Array(T), initial : _, function : _, T -> _) acc = initial list.each { |elem| acc = function.call(acc, elem) } acc end - def foldr(list : Array(T), initial : _, function : _, T -> _) + def self.foldr(list : Array(T), initial : _, function : _, T -> _) acc = initial copy = list.clone until copy.empty? @@ -48,7 +48,7 @@ class ListOps(T) acc end - def reverse(list : Array(T)) : Array(T) + def self.reverse(list : Array(T)) : Array(T) reversed = [] of T copy = list.clone until copy.empty? diff --git a/exercises/practice/list-ops/.meta/test_template.ecr b/exercises/practice/list-ops/.meta/test_template.ecr index e39e0a69..11b72172 100644 --- a/exercises/practice/list-ops/.meta/test_template.ecr +++ b/exercises/practice/list-ops/.meta/test_template.ecr @@ -2,74 +2,57 @@ require "spec" require "../src/*" <%- + # These are uuids of the test cases that use nested arrays. tests_with_nested = ["d6ecd72c-197f-40c3-89a4-aa1f45827e09", "40872990-b5b8-4cb8-9085-d91fc0d05d26"] - - emptiable = ->(input : JSON::Any) { - if input.as_a.empty? - "[] of Int32" - else - input.to_s - end - } - - nestable_emptiable = ->(input : JSON::Any) { - if input.as_a.empty? - "[] of Array(Int32)" - else - input.to_s.gsub("[]", "[] of Int32") - end - } -%> describe "<%-= to_capitalized(@json["exercise"].to_s) %>" do <%- @json["cases"].as_a.each do |describe| -%> describe "<%= describe["description"].to_s %>" do <%- describe["cases"].as_a.each do |cases| -%> <%= status() %> "<%= cases["description"] %>" do - <%- if tests_with_nested.any?(cases["uuid"]) -%> - list_ops = ListOps(Array(Int32)).new() - <%- else -%> - list_ops = ListOps(Int32).new() - <%- end -%> <%- if cases["property"].to_s == "append" -%> - list_ops.<%= cases["property"] %>(<%= emptiable.call(cases["input"]["list1"]) %>, <%= emptiable.call(cases["input"]["list2"]) %>).should eq(<%= emptiable.call(cases["expected"]) %>) + ListOps.<%= cases["property"] %>(<%= list_ops_array(cases["input"]["list1"]) %>, <%= list_ops_array(cases["input"]["list2"]) %>).should eq(<%= list_ops_array(cases["expected"]) %>) <%- end -%> <%- if cases["property"].to_s == "concat" -%> - list_ops.<%= cases["property"] %>(<%= nestable_emptiable.call(cases["input"]["lists"]) %>).should eq(<%= + ListOps.<%= cases["property"] %>(<%= list_ops_nestable(cases["input"]["lists"]) %>).should eq(<%= if tests_with_nested.any?(cases["uuid"]) - nestable_emptiable.call(cases["expected"]) + list_ops_nestable(cases["expected"]) else - emptiable.call(cases["expected"]) + list_ops_array(cases["expected"]) end%>) <%- end -%> <%- if cases["property"].to_s == "filter" -%> - filter = <%= function(cases["input"]["function"], ["Int32"]) %> - list_ops.<%= cases["property"] %>(<%= emptiable.call(cases["input"]["list"]) %>, filter).should eq(<%= emptiable.call(cases["expected"]) %>) + filter = <%= list_ops_function(cases["input"]["function"], ["Int32"]) %> + ListOps.<%= cases["property"] %>(<%= list_ops_array(cases["input"]["list"]) %>, filter).should eq(<%= list_ops_array(cases["expected"]) %>) <%- end -%> <%- if cases["property"].to_s == "length" -%> - list_ops.<%= cases["property"] %>(<%= emptiable.call(cases["input"]["list"]) %>).should eq(<%= cases["expected"] %>) + ListOps.<%= cases["property"] %>(<%= list_ops_array(cases["input"]["list"]) %>).should eq(<%= cases["expected"] %>) <%- end -%> <%- if cases["property"].to_s == "map" -%> - mapper = <%= function(cases["input"]["function"], ["Int32"]) %> - list_ops.<%= cases["property"] %>(<%= emptiable.call(cases["input"]["list"]) %>, mapper).should eq(<%= emptiable.call(cases["expected"]) %>) + mapper = <%= list_ops_function(cases["input"]["function"], ["Int32"]) %> + ListOps.<%= cases["property"] %>(<%= list_ops_array(cases["input"]["list"]) %>, mapper).should eq(<%= list_ops_array(cases["expected"]) %>) <%- end -%> <%- if ["foldl", "foldr"].any?(cases["property"]) -%> + <%- # If the function performs a division, use Float64 -%> <%- if cases["input"]["function"].to_s.matches?(/.*->.*\/.*/) -%> - folder = <%= function(cases["input"]["function"], ["Float64", "Int32"]) %> - list_ops.<%= cases["property"] %>(<%= emptiable.call(cases["input"]["list"]) %>, <%= cases["input"]["initial"] %>.to_f64, folder).should eq(<%= cases["expected"] %>.to_f64) + folder = <%= list_ops_function(cases["input"]["function"], ["Float64", "Int32"]) %> + ListOps.<%= cases["property"] %>(<%= list_ops_array(cases["input"]["list"]) %>, <%= cases["input"]["initial"] %>.to_f64, folder).should eq(<%= cases["expected"] %>.to_f64) <%- else -%> - folder = <%= function(cases["input"]["function"], ["Int32", "Int32"]) %> - list_ops.<%= cases["property"] %>(<%= emptiable.call(cases["input"]["list"]) %>, <%= cases["input"]["initial"] %>, folder).should eq(<%= cases["expected"] %>) + folder = <%= list_ops_function(cases["input"]["function"], ["Int32", "Int32"]) %> + ListOps.<%= cases["property"] %>(<%= list_ops_array(cases["input"]["list"]) %>, <%= cases["input"]["initial"] %>, folder).should eq(<%= cases["expected"] %>) <%- end -%> <%- end -%> <%- if cases["property"].to_s == "reverse" -%> <%- if tests_with_nested.any?(cases["uuid"]) -%> - list_ops.<%= cases["property"] %>(<%= nestable_emptiable.call(cases["input"]["list"]) %>).should eq(<%= nestable_emptiable.call(cases["expected"]) %>) + ListOps.<%= cases["property"] %>(<%= list_ops_nestable(cases["input"]["list"]) %>).should eq(<%= list_ops_nestable(cases["expected"]) %>) <%- else -%> - list_ops.<%= cases["property"] %>(<%= emptiable.call(cases["input"]["list"]) %>).should eq(<%= emptiable.call(cases["expected"]) %>) + ListOps.<%= cases["property"] %>(<%= list_ops_array(cases["input"]["list"]) %>).should eq(<%= list_ops_array(cases["expected"]) %>) <%- end -%> <%- end -%> end + <%- end -%> end + <%- end -%> end diff --git a/exercises/practice/list-ops/spec/list_ops_spec.cr b/exercises/practice/list-ops/spec/list_ops_spec.cr index 7ffadb0b..98d08e69 100644 --- a/exercises/practice/list-ops/spec/list_ops_spec.cr +++ b/exercises/practice/list-ops/spec/list_ops_spec.cr @@ -4,116 +4,115 @@ require "../src/*" describe "ListOps" do describe "append entries to a list and return the new list" do it "empty lists" do - list_ops = ListOps(Int32).new - list_ops.append([] of Int32, [] of Int32).should eq([] of Int32) + ListOps.append([] of Int32, [] of Int32).should eq([] of Int32) end + pending "list to empty list" do - list_ops = ListOps(Int32).new - list_ops.append([] of Int32, [1, 2, 3, 4]).should eq([1, 2, 3, 4]) + ListOps.append([] of Int32, [1, 2, 3, 4]).should eq([1, 2, 3, 4]) end + pending "empty list to list" do - list_ops = ListOps(Int32).new - list_ops.append([1, 2, 3, 4], [] of Int32).should eq([1, 2, 3, 4]) + ListOps.append([1, 2, 3, 4], [] of Int32).should eq([1, 2, 3, 4]) end + pending "non-empty lists" do - list_ops = ListOps(Int32).new - list_ops.append([1, 2], [2, 3, 4, 5]).should eq([1, 2, 2, 3, 4, 5]) + ListOps.append([1, 2], [2, 3, 4, 5]).should eq([1, 2, 2, 3, 4, 5]) end end + describe "concatenate a list of lists" do pending "empty list" do - list_ops = ListOps(Int32).new - list_ops.concat([] of Array(Int32)).should eq([] of Int32) + ListOps.concat([] of Array(Int32)).should eq([] of Int32) end + pending "list of lists" do - list_ops = ListOps(Int32).new - list_ops.concat([[1, 2], [3], [] of Int32, [4, 5, 6]]).should eq([1, 2, 3, 4, 5, 6]) + ListOps.concat([[1, 2], [3], [] of Int32, [4, 5, 6]]).should eq([1, 2, 3, 4, 5, 6]) end + pending "list of nested lists" do - list_ops = ListOps(Array(Int32)).new - list_ops.concat([[[1], [2]], [[3]], [[] of Int32], [[4, 5, 6]]]).should eq([[1], [2], [3], [] of Int32, [4, 5, 6]]) + ListOps.concat([[[1], [2]], [[3]], [[] of Int32], [[4, 5, 6]]]).should eq([[1], [2], [3], [] of Int32, [4, 5, 6]]) end end + describe "filter list returning only values that satisfy the filter function" do pending "empty list" do - list_ops = ListOps(Int32).new filter = ->(x : Int32) { x % 2 == 1 } - list_ops.filter([] of Int32, filter).should eq([] of Int32) + ListOps.filter([] of Int32, filter).should eq([] of Int32) end + pending "non-empty list" do - list_ops = ListOps(Int32).new filter = ->(x : Int32) { x % 2 == 1 } - list_ops.filter([1, 2, 3, 5], filter).should eq([1, 3, 5]) + ListOps.filter([1, 2, 3, 5], filter).should eq([1, 3, 5]) end end + describe "returns the length of a list" do pending "empty list" do - list_ops = ListOps(Int32).new - list_ops.length([] of Int32).should eq(0) + ListOps.length([] of Int32).should eq(0) end + pending "non-empty list" do - list_ops = ListOps(Int32).new - list_ops.length([1, 2, 3, 4]).should eq(4) + ListOps.length([1, 2, 3, 4]).should eq(4) end end + describe "return a list of elements whose values equal the list value transformed by the mapping function" do pending "empty list" do - list_ops = ListOps(Int32).new mapper = ->(x : Int32) { x + 1 } - list_ops.map([] of Int32, mapper).should eq([] of Int32) + ListOps.map([] of Int32, mapper).should eq([] of Int32) end + pending "non-empty list" do - list_ops = ListOps(Int32).new mapper = ->(x : Int32) { x + 1 } - list_ops.map([1, 3, 5, 7], mapper).should eq([2, 4, 6, 8]) + ListOps.map([1, 3, 5, 7], mapper).should eq([2, 4, 6, 8]) end end + describe "folds (reduces) the given list from the left with a function" do pending "empty list" do - list_ops = ListOps(Int32).new folder = ->(acc : Int32, el : Int32) { el * acc } - list_ops.foldl([] of Int32, 2, folder).should eq(2) + ListOps.foldl([] of Int32, 2, folder).should eq(2) end + pending "direction independent function applied to non-empty list" do - list_ops = ListOps(Int32).new folder = ->(acc : Int32, el : Int32) { el + acc } - list_ops.foldl([1, 2, 3, 4], 5, folder).should eq(15) + ListOps.foldl([1, 2, 3, 4], 5, folder).should eq(15) end + pending "direction dependent function applied to non-empty list" do - list_ops = ListOps(Int32).new folder = ->(acc : Float64, el : Int32) { el / acc } - list_ops.foldl([1, 2, 3, 4], 24.to_f64, folder).should eq(64.to_f64) + ListOps.foldl([1, 2, 3, 4], 24.to_f64, folder).should eq(64.to_f64) end end + describe "folds (reduces) the given list from the right with a function" do pending "empty list" do - list_ops = ListOps(Int32).new folder = ->(acc : Int32, el : Int32) { el * acc } - list_ops.foldr([] of Int32, 2, folder).should eq(2) + ListOps.foldr([] of Int32, 2, folder).should eq(2) end + pending "direction independent function applied to non-empty list" do - list_ops = ListOps(Int32).new folder = ->(acc : Int32, el : Int32) { el + acc } - list_ops.foldr([1, 2, 3, 4], 5, folder).should eq(15) + ListOps.foldr([1, 2, 3, 4], 5, folder).should eq(15) end + pending "direction dependent function applied to non-empty list" do - list_ops = ListOps(Int32).new folder = ->(acc : Float64, el : Int32) { el / acc } - list_ops.foldr([1, 2, 3, 4], 24.to_f64, folder).should eq(9.to_f64) + ListOps.foldr([1, 2, 3, 4], 24.to_f64, folder).should eq(9.to_f64) end end + describe "reverse the elements of the list" do pending "empty list" do - list_ops = ListOps(Int32).new - list_ops.reverse([] of Int32).should eq([] of Int32) + ListOps.reverse([] of Int32).should eq([] of Int32) end + pending "non-empty list" do - list_ops = ListOps(Int32).new - list_ops.reverse([1, 3, 5, 7]).should eq([7, 5, 3, 1]) + ListOps.reverse([1, 3, 5, 7]).should eq([7, 5, 3, 1]) end + pending "list of lists is not flattened" do - list_ops = ListOps(Array(Int32)).new - list_ops.reverse([[1, 2], [3], [] of Int32, [4, 5, 6]]).should eq([[4, 5, 6], [] of Int32, [3], [1, 2]]) + ListOps.reverse([[1, 2], [3], [] of Int32, [4, 5, 6]]).should eq([[4, 5, 6], [] of Int32, [3], [1, 2]]) end end end diff --git a/exercises/practice/list-ops/src/list_ops.cr b/exercises/practice/list-ops/src/list_ops.cr index 6369703b..82d0f25a 100644 --- a/exercises/practice/list-ops/src/list_ops.cr +++ b/exercises/practice/list-ops/src/list_ops.cr @@ -1,27 +1,27 @@ -class ListOps(T) +module ListOps(T) # Write your code for the 'Simple Cipher' exercise in this file. - def append(list1 : Array(T), list2 : Array(T)) : Array(T) + def self.append(list1 : Array(T), list2 : Array(T)) : Array(T) end - def concat(lists : Array(Array(T))) + def self.concat(lists : Array(Array(T))) end - def filter(list : Array(T), function : T -> Bool) : Array(T) + def self.filter(list : Array(T), function : T -> Bool) : Array(T) end - def length(list : Array(T)) : Int + def self.length(list : Array(T)) : Int end - def map(list : Array(T), function : T -> _) + def self.map(list : Array(T), function : T -> _) end - def foldl(list : Array(T), initial : _, function : _, T -> _) + def self.foldl(list : Array(T), initial : _, function : _, T -> _) end - def foldr(list : Array(T), initial : _, function : _, T -> _) + def self.foldr(list : Array(T), initial : _, function : _, T -> _) end - def reverse(list : Array(T)) : Array(T) + def self.reverse(list : Array(T)) : Array(T) end end diff --git a/test-generator/spec/generator_plugins_tests.cr b/test-generator/spec/generator_plugins_tests.cr index db0f3d1c..5bd8571d 100644 --- a/test-generator/spec/generator_plugins_tests.cr +++ b/test-generator/spec/generator_plugins_tests.cr @@ -7,25 +7,25 @@ include GeneratorPlugins describe GeneratorPlugins do describe "cammelcase" do it "should cammelcase a string" do - to_cammel("hello-world").should eq "HelloWorld" + to_cammel("hello-world").should eq "HelloWorld" end it "should be able to handle spaces" do - to_cammel("hello-world 5").should eq "HelloWorld 5" + to_cammel("hello-world 5").should eq "HelloWorld 5" end end describe "strain" do it "should modify contains function" do - strain("fn(x) -> contains(x, 5)").should eq "x.includes?(5)" + strain("fn(x) -> contains(x, 5)").should eq "x.includes?(5)" end it "should modify starts_with function" do - strain("fn(x) -> starts_with(x, 'z')").should eq "x.starts_with?('z')" + strain("fn(x) -> starts_with(x, 'z')").should eq "x.starts_with?('z')" end it "should not modify other functions" do - strain("fn(x) -> ends_with(x, 'z')").should eq "ends_with(x, 'z')" + strain("fn(x) -> ends_with(x, 'z')").should eq "ends_with(x, 'z')" end end @@ -62,21 +62,62 @@ describe GeneratorPlugins do } " expected = "tree.value.should eq(4)\n" + - "left = tree.left.not_nil!\n" + - " left.value.should eq(2)\n" + - "left_left = left.left.not_nil!\n" + - " left_left.value.should eq(1)\n" + - "left_right = left.right.not_nil!\n" + - " left_right.value.should eq(3)\n" + - "right = tree.right.not_nil!\n" + - " right.value.should eq(6)\n" + - "right_left = right.left.not_nil!\n" + - " right_left.value.should eq(5)\n" + - "right_right = right.right.not_nil!\n" + - " right_right.value.should eq(7)\n" + "left = tree.left.not_nil!\n" + + " left.value.should eq(2)\n" + + "left_left = left.left.not_nil!\n" + + " left_left.value.should eq(1)\n" + + "left_right = left.right.not_nil!\n" + + " left_right.value.should eq(3)\n" + + "right = tree.right.not_nil!\n" + + " right.value.should eq(6)\n" + + "right_left = right.left.not_nil!\n" + + " right_left.value.should eq(5)\n" + + "right_right = right.right.not_nil!\n" + + " right_right.value.should eq(7)\n" json = JSON.parse(tree) result = binary_search_tree(json) result.should eq expected end end + + describe "list_ops_array" do + it "should not modify non-empty array" do + json = JSON.parse("[1, 2, 3]") + list_ops_array(json).should eq("[1, 2, 3]") + end + + it "should append type for empty array" do + json = JSON.parse("[]") + list_ops_array(json).should eq("[] of Int32") + end + end + + describe "list_ops_nestable" do + it "should not modify non-empty array with no empty elements" do + json = JSON.parse("[[1], [2], [3], [4, 5, 6]]") + list_ops_nestable(json).should eq("[[1], [2], [3], [4, 5, 6]]") + end + + it "should append type for empty array" do + json = JSON.parse("[]") + list_ops_nestable(json).should eq("[] of Array(Int32)") + end + + it "should append type for empty elements" do + json = JSON.parse("[[1, 2], [3], [], [4, 5, 6]]") + list_ops_nestable(json).should eq("[[1, 2], [3], [] of Int32, [4, 5, 6]]") + end + end + + describe "list_ops_function" do + it "should convert function with modulo" do + json = JSON.parse("\"(x) -> x modulo 2 == 1\"") + list_ops_function(json, ["Int"]).should eq("->(x: Int) { x % 2 == 1 }") + end + + it "should convert function with multiple args" do + json = JSON.parse("\"(x, y) -> x + y\"") + list_ops_function(json, ["Int", "Int"]).should eq("->(x: Int, y: Int) { x + y }") + end + end end diff --git a/test-generator/src/generator_help.cr b/test-generator/src/generator_help.cr index e5a3772d..1c40778e 100644 --- a/test-generator/src/generator_help.cr +++ b/test-generator/src/generator_help.cr @@ -91,22 +91,6 @@ class GeneratorHelp input.tr("-", "_").camelcase end - def function(input : JSON::Any, arg_types : Array(String)) - func_string = input.to_s - - # Add types to function - args_match = func_string.match(/^\((.*)\)\s*->/) - if args_match && !args_match.captures.empty? - if args_match.captures[0] - typed_args = args_match.captures[0].as(String).split(",").map_with_index{ | elem, i | "#{elem.strip} : #{arg_types[i]}" }.join(",") - else - typed_args = "" - end - func_string = func_string.gsub(args_match[0], "->(#{typed_args}) {") + " }" - end - func_string.gsub("modulo", "%") - end - def status if @first @first = false diff --git a/test-generator/src/generator_plugins.cr b/test-generator/src/generator_plugins.cr index e918769d..ed871afd 100644 --- a/test-generator/src/generator_plugins.cr +++ b/test-generator/src/generator_plugins.cr @@ -46,10 +46,44 @@ module GeneratorPlugins def strain(function : String) : String if function[9..].includes?("contains") - return "x.includes?(5)" + return "x.includes?(5)" elsif function[9..].includes?("starts_with") - return "x.starts_with?('z')" + return "x.starts_with?('z')" end function[9..] end + + def list_ops_array(input : JSON::Any) : String + if input.as_a.empty? + "[] of Int32" + else + input.to_s + end + end + + def list_ops_nestable(input : JSON::Any) : String + if input.as_a.empty? + "[] of Array(Int32)" + else + input.to_s.gsub("[]", "[] of Int32") + end + end + + # Converts the input function to a proc for the list ops exercise + def list_ops_function(input : JSON::Any, arg_types : Array(String)) + func_string = input.to_s + + # Add types to the arguments to the proc + args_match = func_string.match(/^\((.*)\)\s*->/) + if args_match && !args_match.captures.empty? + if args_match.captures[0] + typed_args = args_match.captures[0].as(String).split(",").map_with_index { |elem, i| "#{elem.strip}: #{arg_types[i]}" }.join(", ") + else + typed_args = "" + end + func_string = func_string.gsub(args_match[0], "->(#{typed_args}) {") + " }" + end + + func_string.gsub("modulo", "%") + end end