From 45c6c836d5c977664ce29fda65e61d7fe3c6a5e2 Mon Sep 17 00:00:00 2001 From: Tomas Fischer Date: Wed, 10 Apr 2024 07:24:41 +0200 Subject: [PATCH] Add Word Search exercise (#1223) --- config.json | 11 + .../word-search/.docs/instructions.md | 24 ++ .../practice/word-search/.meta/config.json | 21 ++ .../examples/success-standard/package.yaml | 16 + .../success-standard/src/WordSearch.hs | 32 ++ .../practice/word-search/.meta/tests.toml | 82 +++++ exercises/practice/word-search/package.yaml | 21 ++ .../practice/word-search/src/WordSearch.hs | 7 + exercises/practice/word-search/stack.yaml | 1 + exercises/practice/word-search/test/Tests.hs | 331 ++++++++++++++++++ 10 files changed, 546 insertions(+) create mode 100644 exercises/practice/word-search/.docs/instructions.md create mode 100644 exercises/practice/word-search/.meta/config.json create mode 100644 exercises/practice/word-search/.meta/examples/success-standard/package.yaml create mode 100644 exercises/practice/word-search/.meta/examples/success-standard/src/WordSearch.hs create mode 100644 exercises/practice/word-search/.meta/tests.toml create mode 100644 exercises/practice/word-search/package.yaml create mode 100644 exercises/practice/word-search/src/WordSearch.hs create mode 100644 exercises/practice/word-search/stack.yaml create mode 100644 exercises/practice/word-search/test/Tests.hs diff --git a/config.json b/config.json index 174416d0f..38eb1277a 100644 --- a/config.json +++ b/config.json @@ -415,6 +415,17 @@ "strings" ] }, + { + "slug": "word-search", + "name": "Word Search", + "uuid": "6d406931-7173-4fc0-b897-798227d09b5e", + "practices": [], + "prerequisites": [], + "difficulty": 5, + "topics": [ + "strings" + ] + }, { "slug": "wordy", "name": "Wordy", diff --git a/exercises/practice/word-search/.docs/instructions.md b/exercises/practice/word-search/.docs/instructions.md new file mode 100644 index 000000000..e2d08aa9e --- /dev/null +++ b/exercises/practice/word-search/.docs/instructions.md @@ -0,0 +1,24 @@ +# Instructions + +In word search puzzles you get a square of letters and have to find specific words in them. + +For example: + +```text +jefblpepre +camdcimgtc +oivokprjsm +pbwasqroua +rixilelhrs +wolcqlirpc +screeaumgr +alxhpburyi +jalaycalmp +clojurermt +``` + +There are several programming languages hidden in the above square. + +Words can be hidden in all kinds of directions: left-to-right, right-to-left, vertical and diagonal. + +Given a puzzle and a list of words return the location of the first and last letter of each word. diff --git a/exercises/practice/word-search/.meta/config.json b/exercises/practice/word-search/.meta/config.json new file mode 100644 index 000000000..718ac47d1 --- /dev/null +++ b/exercises/practice/word-search/.meta/config.json @@ -0,0 +1,21 @@ +{ + "authors": [ + "tofische" + ], + "files": { + "solution": [ + "src/WordSearch.hs", + "package.yaml" + ], + "test": [ + "test/Tests.hs" + ], + "example": [ + ".meta/examples/success-standard/src/WordSearch.hs" + ], + "invalidator": [ + "stack.yaml" + ] + }, + "blurb": "Create a program to solve a word search puzzle." +} diff --git a/exercises/practice/word-search/.meta/examples/success-standard/package.yaml b/exercises/practice/word-search/.meta/examples/success-standard/package.yaml new file mode 100644 index 000000000..35f88d9a3 --- /dev/null +++ b/exercises/practice/word-search/.meta/examples/success-standard/package.yaml @@ -0,0 +1,16 @@ +name: word-search + +dependencies: + - base + +library: + exposed-modules: WordSearch + source-dirs: src + +tests: + test: + main: Tests.hs + source-dirs: test + dependencies: + - word-search + - hspec diff --git a/exercises/practice/word-search/.meta/examples/success-standard/src/WordSearch.hs b/exercises/practice/word-search/.meta/examples/success-standard/src/WordSearch.hs new file mode 100644 index 000000000..159391df7 --- /dev/null +++ b/exercises/practice/word-search/.meta/examples/success-standard/src/WordSearch.hs @@ -0,0 +1,32 @@ +module WordSearch (search, CharPos(..), WordPos(..)) where + +import Data.Maybe (listToMaybe) + +data CharPos = CharPos{col::Int, row::Int} deriving (Eq, Show) +data WordPos = WordPos{start::CharPos, end::CharPos} deriving (Eq, Show) + +search :: [String] -> [String] -> [(String, Maybe WordPos)] +search grid wordList = map (search' grid) wordList + +search' :: [String] -> String -> (String, Maybe WordPos) +search' grid word = (word, listToMaybe hits) + where + lastRow = length grid - 1 + lastCol = length (head grid) - 1 + lastIdx = length word - 1 + hits = [WordPos{start=CharPos{col=startCol+1, row=startRow+1}, end=CharPos{col=endCol+1, row=endRow+1}} | + startCol <- [0 .. lastCol], + dirCol <- [- 1, 0, 1], + let endCol = startCol + lastIdx*dirCol, + endCol >= 0 && endCol <= lastCol, + startRow <- [0 .. lastRow], + dirRow <- [- 1, 0, 1], + let endRow = startRow + lastIdx*dirRow, + endRow >= 0 && endRow <= lastRow, + let gridWord = [ + (grid!!r)!!c | idx <- [0 .. lastIdx], + let c = startCol + idx*dirCol, + let r = startRow + idx*dirRow + ], + and $ zipWith (==) word gridWord + ] diff --git a/exercises/practice/word-search/.meta/tests.toml b/exercises/practice/word-search/.meta/tests.toml new file mode 100644 index 000000000..3f98113d7 --- /dev/null +++ b/exercises/practice/word-search/.meta/tests.toml @@ -0,0 +1,82 @@ +# 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. + +[b4057815-0d01-41f0-9119-6a91f54b2a0a] +description = "Should accept an initial game grid and a target search word" + +[6b22bcc5-6cbf-4674-931b-d2edbff73132] +description = "Should locate one word written left to right" + +[ff462410-434b-442d-9bc3-3360c75f34a8] +description = "Should locate the same word written left to right in a different position" + +[a02febae-6347-443e-b99c-ab0afb0b8fca] +description = "Should locate a different left to right word" + +[e42e9987-6304-4e13-8232-fa07d5280130] +description = "Should locate that different left to right word in a different position" + +[9bff3cee-49b9-4775-bdfb-d55b43a70b2f] +description = "Should locate a left to right word in two line grid" + +[851a35fb-f499-4ec1-9581-395a87903a22] +description = "Should locate a left to right word in three line grid" + +[2f3dcf84-ba7d-4b75-8b8d-a3672b32c035] +description = "Should locate a left to right word in ten line grid" + +[006d4856-f365-4e84-a18c-7d129ce9eefb] +description = "Should locate that left to right word in a different position in a ten line grid" + +[eff7ac9f-ff11-443e-9747-40850c12ab60] +description = "Should locate a different left to right word in a ten line grid" + +[dea39f86-8c67-4164-8884-13bfc48bd13b] +description = "Should locate multiple words" + +[29e6a6a5-f80c-48a6-8e68-05bbbe187a09] +description = "Should locate a single word written right to left" + +[3cf34428-b43f-48b6-b332-ea0b8836011d] +description = "Should locate multiple words written in different horizontal directions" + +[2c8cd344-a02f-464b-93b6-8bf1bd890003] +description = "Should locate words written top to bottom" + +[9ee1e43d-e59d-4c32-9a5f-6a22d4a1550f] +description = "Should locate words written bottom to top" + +[6a21a676-f59e-4238-8e88-9f81015afae9] +description = "Should locate words written top left to bottom right" + +[c9125189-1861-4b0d-a14e-ba5dab29ca7c] +description = "Should locate words written bottom right to top left" + +[b19e2149-7fc5-41ec-a8a9-9bc6c6c38c40] +description = "Should locate words written bottom left to top right" + +[69e1d994-a6d7-4e24-9b5a-db76751c2ef8] +description = "Should locate words written top right to bottom left" + +[695531db-69eb-463f-8bad-8de3bf5ef198] +description = "Should fail to locate a word that is not in the puzzle" + +[fda5b937-6774-4a52-8f89-f64ed833b175] +description = "Should fail to locate words that are not on horizontal, vertical, or diagonal lines" + +[5b6198eb-2847-4e2f-8efe-65045df16bd3] +description = "Should not concatenate different lines to find a horizontal word" + +[eba44139-a34f-4a92-98e1-bd5f259e5769] +description = "Should not wrap around horizontally to find a word" + +[cd1f0fa8-76af-4167-b105-935f78364dac] +description = "Should not wrap around vertically to find a word" diff --git a/exercises/practice/word-search/package.yaml b/exercises/practice/word-search/package.yaml new file mode 100644 index 000000000..e270fda1a --- /dev/null +++ b/exercises/practice/word-search/package.yaml @@ -0,0 +1,21 @@ +name: word-search +version: 1.0.0.0 + +dependencies: + - base + +library: + exposed-modules: WordSearch + source-dirs: src + ghc-options: -Wall + # dependencies: + # - foo # List here the packages you + # - bar # want to use in your solution. + +tests: + test: + main: Tests.hs + source-dirs: test + dependencies: + - word-search + - hspec diff --git a/exercises/practice/word-search/src/WordSearch.hs b/exercises/practice/word-search/src/WordSearch.hs new file mode 100644 index 000000000..0487d142c --- /dev/null +++ b/exercises/practice/word-search/src/WordSearch.hs @@ -0,0 +1,7 @@ +module WordSearch (search, CharPos(..), WordPos(..)) where + +data CharPos = CharPos{col::Int, row::Int} deriving (Eq, Show) +data WordPos = WordPos{start::CharPos, end::CharPos} deriving (Eq, Show) + +search :: [String] -> [String] -> [(String, Maybe WordPos)] +search grid wordList = error "You need to implement this function." diff --git a/exercises/practice/word-search/stack.yaml b/exercises/practice/word-search/stack.yaml new file mode 100644 index 000000000..115878212 --- /dev/null +++ b/exercises/practice/word-search/stack.yaml @@ -0,0 +1 @@ +resolver: lts-20.18 diff --git a/exercises/practice/word-search/test/Tests.hs b/exercises/practice/word-search/test/Tests.hs new file mode 100644 index 000000000..747a9493b --- /dev/null +++ b/exercises/practice/word-search/test/Tests.hs @@ -0,0 +1,331 @@ +{-# OPTIONS_GHC -fno-warn-type-defaults #-} +{-# LANGUAGE RecordWildCards #-} + +import Data.Foldable (for_) +import Test.Hspec (Spec, describe, it, shouldBe) +import Test.Hspec.Runner (configFailFast, defaultConfig, hspecWith) + +import WordSearch (search, CharPos(..), WordPos(..)) + +main :: IO () +main = hspecWith defaultConfig {configFailFast = True} specs + +specs :: Spec +specs = describe "search" $ for_ cases test + where + test Case{..} = it description $ search grid words `shouldBe` expected + +data Case = Case { description :: String + , grid :: [String] + , words :: [String] + , expected :: [(String, Maybe WordPos)] + } + +cases :: [Case] +cases = [ Case { description = "Should accept an initial game grid and a target search word" + , grid = [ "jefblpepre" ] + , words = [ "clojure" ] + , expected = [ ("clojure", Nothing) ] + } + , Case { description = "Should locate one word written left to right" + , grid = [ "clojurermt" ] + , words = [ "clojure" ] + , expected = [ ("clojure", Just WordPos{start=CharPos{col=1, row=1}, end=CharPos{col=7, row=1}}) ] + } + , Case { description = "Should locate the same word written left to right in a different position" + , grid = [ "mtclojurer" ] + , words = [ "clojure" ] + , expected = [ ("clojure", Just WordPos{start=CharPos{col=3, row=1}, end=CharPos{col=9, row=1}}) ] + } + , Case { description = "Should locate a different left to right word" + , grid = [ "coffeelplx" ] + , words = [ "coffee" ] + , expected = [ ("coffee", Just WordPos{start=CharPos{col=1, row=1}, end=CharPos{col=6, row=1}}) ] + } + , Case { description = "Should locate that different left to right word in a different position" + , grid = [ "xcoffeezlp" ] + , words = [ "coffee" ] + , expected = [ ("coffee", Just WordPos{start=CharPos{col=2, row=1}, end=CharPos{col=7, row=1}}) ] + } + , Case { description = "Should locate a left to right word in three line grid" + , grid = [ "camdcimgtc", "jefblpepre","clojurermt" ] + , words = [ "clojure" ] + , expected = [ ("clojure", Just WordPos{start=CharPos{col=1, row=3}, end=CharPos{col=7, row=3}}) ] + } + , Case { description = "Should locate a left to right word in ten line grid" + , grid = [ + "jefblpepre", + "camdcimgtc", + "oivokprjsm", + "pbwasqroua", + "rixilelhrs", + "wolcqlirpc", + "screeaumgr", + "alxhpburyi", + "jalaycalmp", + "clojurermt" + ] + , words = [ "clojure" ] + , expected = [ ("clojure", Just WordPos{start=CharPos{col=1, row=10}, end=CharPos{col=7, row=10}}) ] + } + , Case { description = "Should locate that left to right word in a different position in a ten line grid" + , grid = [ + "jefblpepre", + "camdcimgtc", + "oivokprjsm", + "pbwasqroua", + "rixilelhrs", + "wolcqlirpc", + "screeaumgr", + "alxhpburyi", + "clojurermt", + "jalaycalmp" + ] + , words = [ "clojure" ] + , expected = [ ("clojure", Just WordPos{start=CharPos{col=1, row=9}, end=CharPos{col=7, row=9}}) ] + } + , Case { description = "Should locate a different left to right word in a ten line grid" + , grid = [ + "jefblpepre", + "camdcimgtc", + "oivokprjsm", + "pbwasqroua", + "rixilelhrs", + "wolcqlirpc", + "fortranftw", + "alxhpburyi", + "clojurermt", + "jalaycalmp" + ] + , words = [ "fortran" ] + , expected = [ ("fortran", Just WordPos{start=CharPos{col=1, row=7}, end=CharPos{col=7, row=7}}) ] + } + , Case { description = "Should locate multiple words" + , grid = [ + "jefblpepre", + "camdcimgtc", + "oivokprjsm", + "pbwasqroua", + "rixilelhrs", + "wolcqlirpc", + "fortranftw", + "alxhpburyi", + "jalaycalmp", + "clojurermt" + ] + , words = [ "fortran", "clojure" ] + , expected = [ + ("fortran", Just WordPos{start=CharPos{col=1, row=7}, end=CharPos{col=7, row=7}}), + ("clojure", Just WordPos{start=CharPos{col=1, row=10}, end=CharPos{col=7, row=10}}) + ] + } + , Case { description = "Should locate a single word written right to left" + , grid = [ "rixilelhrs" ] + , words = [ "elixir" ] + , expected = [ ("elixir", Just WordPos{start=CharPos{col=6, row=1}, end=CharPos{col=1, row=1}}) ] + } + , Case { description = "Should locate multiple words written in different horizontal directions" + , grid = [ + "jefblpepre", + "camdcimgtc", + "oivokprjsm", + "pbwasqroua", + "rixilelhrs", + "wolcqlirpc", + "screeaumgr", + "alxhpburyi", + "jalaycalmp", + "clojurermt" + ] + , words = [ "elixir", "clojure" ] + , expected = [ + ("elixir", Just WordPos{start=CharPos{col=6, row=5}, end=CharPos{col=1, row=5}}), + ("clojure", Just WordPos{start=CharPos{col=1, row=10}, end=CharPos{col=7, row=10}}) + ] + } + , Case { description = "Should locate words written top to bottom" + , grid = [ + "jefblpepre", + "camdcimgtc", + "oivokprjsm", + "pbwasqroua", + "rixilelhrs", + "wolcqlirpc", + "screeaumgr", + "alxhpburyi", + "jalaycalmp", + "clojurermt" + ] + , words = [ "clojure", "elixir", "ecmascript" ] + , expected = [ + ("clojure", Just WordPos{start=CharPos{col=1, row=10}, end=CharPos{col=7, row=10}}), + ("elixir", Just WordPos{start=CharPos{col=6, row=5}, end=CharPos{col=1, row=5}}), + ("ecmascript", Just WordPos{start=CharPos{col=10, row=1}, end=CharPos{col=10, row=10}}) + ] + } + , Case { description = "Should locate words written bottom to top" + , grid = [ + "jefblpepre", + "camdcimgtc", + "oivokprjsm", + "pbwasqroua", + "rixilelhrs", + "wolcqlirpc", + "screeaumgr", + "alxhpburyi", + "jalaycalmp", + "clojurermt" + ] + , words = [ "clojure", "elixir", "ecmascript", "rust" ] + , expected = [ + ("clojure", Just WordPos{start=CharPos{col=1, row=10}, end=CharPos{col=7, row=10}}), + ("elixir", Just WordPos{start=CharPos{col=6, row=5}, end=CharPos{col=1, row=5}}), + ("ecmascript", Just WordPos{start=CharPos{col=10, row=1}, end=CharPos{col=10, row=10}}), + ("rust", Just WordPos{start=CharPos{col=9, row=5}, end=CharPos{col=9, row=2}}) + ] + } + , Case { description = "Should locate words written top left to bottom right" + , grid = [ + "jefblpepre", + "camdcimgtc", + "oivokprjsm", + "pbwasqroua", + "rixilelhrs", + "wolcqlirpc", + "screeaumgr", + "alxhpburyi", + "jalaycalmp", + "clojurermt" + ] + , words = [ "clojure", "elixir", "ecmascript", "rust", "java" ] + , expected = [ + ("clojure", Just WordPos{start=CharPos{col=1, row=10}, end=CharPos{col=7, row=10}}), + ("elixir", Just WordPos{start=CharPos{col=6, row=5}, end=CharPos{col=1, row=5}}), + ("ecmascript", Just WordPos{start=CharPos{col=10, row=1}, end=CharPos{col=10, row=10}}), + ("rust", Just WordPos{start=CharPos{col=9, row=5}, end=CharPos{col=9, row=2}}), + ("java", Just WordPos{start=CharPos{col=1, row=1}, end=CharPos{col=4, row=4}}) + ] + } + , Case { description = "Should locate words written bottom right to top left" + , grid = [ + "jefblpepre", + "camdcimgtc", + "oivokprjsm", + "pbwasqroua", + "rixilelhrs", + "wolcqlirpc", + "screeaumgr", + "alxhpburyi", + "jalaycalmp", + "clojurermt" + ] + , words = [ "clojure", "elixir", "ecmascript", "rust", "java", "lua" ] + , expected = [ + ("clojure", Just WordPos{start=CharPos{col=1, row=10}, end=CharPos{col=7, row=10}}), + ("elixir", Just WordPos{start=CharPos{col=6, row=5}, end=CharPos{col=1, row=5}}), + ("ecmascript", Just WordPos{start=CharPos{col=10, row=1}, end=CharPos{col=10, row=10}}), + ("rust", Just WordPos{start=CharPos{col=9, row=5}, end=CharPos{col=9, row=2}}), + ("java", Just WordPos{start=CharPos{col=1, row=1}, end=CharPos{col=4, row=4}}), + ("lua", Just WordPos{start=CharPos{col=8, row=9}, end=CharPos{col=6, row=7}}) + ] + } + , Case { description = "Should locate words written bottom left to top right" + , grid = [ + "jefblpepre", + "camdcimgtc", + "oivokprjsm", + "pbwasqroua", + "rixilelhrs", + "wolcqlirpc", + "screeaumgr", + "alxhpburyi", + "jalaycalmp", + "clojurermt" ] + , words = [ "clojure", "elixir", "ecmascript", "rust", "java", "lua", "lisp" ] + , expected = [ + ("clojure", Just WordPos{start=CharPos{col=1, row=10}, end=CharPos{col=7, row=10}}), + ("elixir", Just WordPos{start=CharPos{col=6, row=5}, end=CharPos{col=1, row=5}}), + ("ecmascript", Just WordPos{start=CharPos{col=10, row=1}, end=CharPos{col=10, row=10}}), + ("rust", Just WordPos{start=CharPos{col=9, row=5}, end=CharPos{col=9, row=2}}), + ("java", Just WordPos{start=CharPos{col=1, row=1}, end=CharPos{col=4, row=4}}), + ("lua", Just WordPos{start=CharPos{col=8, row=9}, end=CharPos{col=6, row=7}}), + ("lisp", Just WordPos{start=CharPos{col=3, row=6}, end=CharPos{col=6, row=3}}) + ] + } + , Case { description = "Should locate words written top right to bottom left" + , grid = [ + "jefblpepre", + "camdcimgtc", + "oivokprjsm", + "pbwasqroua", + "rixilelhrs", + "wolcqlirpc", + "screeaumgr", + "alxhpburyi", + "jalaycalmp", + "clojurermt" + ] + , words = [ "clojure", "elixir", "ecmascript", "rust", "java", "lua", "lisp", "ruby" ] + , expected = [ + ("clojure", Just WordPos{start=CharPos{col=1, row=10}, end=CharPos{col=7, row=10}}), + ("elixir", Just WordPos{start=CharPos{col=6, row=5}, end=CharPos{col=1, row=5}}), + ("ecmascript", Just WordPos{start=CharPos{col=10, row=1}, end=CharPos{col=10, row=10}}), + ("rust", Just WordPos{start=CharPos{col=9, row=5}, end=CharPos{col=9, row=2}}), + ("java", Just WordPos{start=CharPos{col=1, row=1}, end=CharPos{col=4, row=4}}), + ("lua", Just WordPos{start=CharPos{col=8, row=9}, end=CharPos{col=6, row=7}}), + ("lisp", Just WordPos{start=CharPos{col=3, row=6}, end=CharPos{col=6, row=3}}), + ("ruby", Just WordPos{start=CharPos{col=8, row=6}, end=CharPos{col=5, row=9}}) + ] + } + , Case { description = "Should fail to locate a word that is not in the puzzle" + , grid = [ + "jefblpepre", + "camdcimgtc", + "oivokprjsm", + "pbwasqroua", + "rixilelhrs", + "wolcqlirpc", + "screeaumgr", + "alxhpburyi", + "jalaycalmp", + "clojurermt" + ] + , words = [ "clojure", "elixir", "ecmascript", "rust", "java", "lua", "lisp", "ruby", "haskell" ] + , expected = [ + ("clojure", Just WordPos{start=CharPos{col=1, row=10}, end=CharPos{col=7, row=10}}), + ("elixir", Just WordPos{start=CharPos{col=6, row=5}, end=CharPos{col=1, row=5}}), + ("ecmascript", Just WordPos{start=CharPos{col=10, row=1}, end=CharPos{col=10, row=10}}), + ("rust", Just WordPos{start=CharPos{col=9, row=5}, end=CharPos{col=9, row=2}}), + ("java", Just WordPos{start=CharPos{col=1, row=1}, end=CharPos{col=4, row=4}}), + ("lua", Just WordPos{start=CharPos{col=8, row=9}, end=CharPos{col=6, row=7}}), + ("lisp", Just WordPos{start=CharPos{col=3, row=6}, end=CharPos{col=6, row=3}}), + ("ruby", Just WordPos{start=CharPos{col=8, row=6}, end=CharPos{col=5, row=9}}), + ("haskell", Nothing) + ] + } + , Case { description = "Should fail to locate words that are not on horizontal, vertical, or diagonal lines" + , grid = [ "abc", "def" ] + , words = [ "aef", "ced", "abf", "cbd" ] + , expected = [ + ("aef", Nothing), + ("ced", Nothing), + ("abf", Nothing), + ("cbd", Nothing) + ] + } + , Case { description = "Should not concatenate different lines to find a horizontal word" + , grid = [ "abceli", "xirdfg" ] + , words = [ "elixir" ] + , expected = [ ("elixir", Nothing) ] + } + , Case { description = "Should not wrap around horizontally to find a word" + , grid = [ "silabcdefp" ] + , words = [ "lisp" ] + , expected = [ ("lisp", Nothing) ] + } + , Case { description = "Should not wrap around vertically to find a word" + , grid = [ "s", "u", "r", "a", "b", "c", "t" ] + , words = [ "rust" ] + , expected = [ ("rust", Nothing) ] + } + ]