diff --git a/config.json b/config.json index abe22a8c3..291d340aa 100644 --- a/config.json +++ b/config.json @@ -590,6 +590,18 @@ "maybe" ] }, + { + "slug": "tree-building", + "uuid": "508868ec-bd5f-4826-a4c0-ab2a545e7038", + "core": false, + "unlocked_by": "collatz-conjecture", + "difficulty": 5, + "topics": [ + "maybe", + "recursion", + "trees" + ] + }, { "slug": "atbash-cipher", "uuid": "4b6d04f4-8cfc-4d8a-ac6e-77f3889f27c3", diff --git a/exercises/tree-building/README.md b/exercises/tree-building/README.md new file mode 100644 index 000000000..e2cb4d8d7 --- /dev/null +++ b/exercises/tree-building/README.md @@ -0,0 +1,83 @@ +# Tree Building + +Refactor a tree building algorithm. + +Some web-forums have a tree layout, so posts are presented as a tree. However +the posts are typically stored in a database as an unsorted set of records. Thus +when presenting the posts to the user the tree structure has to be +reconstructed. + +Your job will be to refactor a working but slow and ugly piece of code that +implements the tree building logic for highly abstracted records. The records +only contain an ID number and a parent ID number. The ID number is always +between 0 (inclusive) and the length of the record list (exclusive). All records +have a parent ID lower than their own ID, except for the root record, which has +a parent ID that's equal to its own ID. + +An example tree: + +```text +root (ID: 0, parent ID: 0) +|-- child1 (ID: 1, parent ID: 0) +| |-- grandchild1 (ID: 2, parent ID: 1) +| +-- grandchild2 (ID: 4, parent ID: 1) ++-- child2 (ID: 3, parent ID: 0) +| +-- grandchild3 (ID: 6, parent ID: 3) ++-- child3 (ID: 5, parent ID: 0) +``` + + +## Getting Started + +For installation and learning resources, refer to the +[exercism help page](http://exercism.io/languages/haskell). + +## Running the tests + +To run the test suite, execute the following command: + +```bash +stack test +``` + +#### If you get an error message like this... + +``` +No .cabal file found in directory +``` + +You are probably running an old stack version and need +to upgrade it. + +#### Otherwise, if you get an error message like this... + +``` +No compiler found, expected minor version match with... +Try running "stack setup" to install the correct GHC... +``` + +Just do as it says and it will download and install +the correct compiler version: + +```bash +stack setup +``` + +## Running *GHCi* + +If you want to play with your solution in GHCi, just run the command: + +```bash +stack ghci +``` + +## Feedback, Issues, Pull Requests + +The [exercism/haskell](https://github.com/exercism/haskell) repository on +GitHub is the home for all of the Haskell exercises. + +If you have feedback about an exercise, or want to help implementing a new +one, head over there and create an issue. We'll do our best to help you! + +## Submitting Incomplete Solutions +It's possible to submit an incomplete solution so you can see how others have completed the exercise. diff --git a/exercises/tree-building/examples/success-standard/package.yaml b/exercises/tree-building/examples/success-standard/package.yaml new file mode 100644 index 000000000..f7b0edddc --- /dev/null +++ b/exercises/tree-building/examples/success-standard/package.yaml @@ -0,0 +1,21 @@ +name: tree-building +version: 1.0.0.0 + +dependencies: + - base + +library: + exposed-modules: TreeBuilding + 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: + - tree-building + - hspec diff --git a/exercises/tree-building/examples/success-standard/src/TreeBuilding.hs b/exercises/tree-building/examples/success-standard/src/TreeBuilding.hs new file mode 100644 index 000000000..1795302e7 --- /dev/null +++ b/exercises/tree-building/examples/success-standard/src/TreeBuilding.hs @@ -0,0 +1,77 @@ +module TreeBuilding (newTree, Record(..), Tree(..)) where + +import Data.List +import Data.Maybe (isNothing) + +type Id = Int +type Children = [Tree] +type ParentGrouping = (Maybe Id, [Id]) + +data Record = Record Id (Maybe Id) deriving (Eq, Show) + +data Tree = Leaf Id | Branch Id Children deriving (Eq, Show) + +newTree :: [Record] -> Maybe Tree +newTree records + | cycles records = Nothing + | succIdCheck records = build . groupByParent $ records + | otherwise = Nothing + +-- Checks for cycling in newTree +cycles :: [Record] -> Bool +cycles = any checkRecord + where checkRecord r = + case r of + (Record i Nothing) -> if' (i /= 0) True False + (Record i (Just p)) -> if' (p >= i) True False + +build :: [ParentGrouping] -> Maybe Tree +build [] = Nothing +build (x:xs) + | not . validRoot $ x = Nothing + | null xs = Just (Leaf 0) + | otherwise = Just (Branch 0 (build' xs)) + +-- Internal build +build' :: [ParentGrouping] -> [Tree] +build' xs + | length xs == 1 = map Leaf (snd (head xs)) + | otherwise = buildWithChildren xs + where + buildWithChildren (y:ys) = + map (\yid -> + let children = filter ((== Just yid) . fst) ys + in if not (null children) + then Branch yid (build' children) + else Leaf yid + ) (snd y) + +-- Validate the Root node +validRoot :: ParentGrouping -> Bool +validRoot (p, ids) = isNothing p && length ids == 1 && sum ids == 0 + +groupByParent :: [Record] -> [ParentGrouping] +groupByParent = sortOn fst + . map (\xs -> (recordParent (head xs), map recordId xs)) + . groupBy parentsEq + . sortOn recordParent + . sortOn recordId + +parentsEq :: Record -> Record -> Bool +parentsEq rx ry = recordParent rx == recordParent ry + +recordParent :: Record -> Maybe Int +recordParent (Record _ p) = p + +recordId :: Record -> Id +recordId (Record i _) = i + +-- verification +succIdCheck :: [Record] -> Bool +succIdCheck = all (\(x,y) -> succ x == y) . pairwise . sort . map recordId + where pairwise = zip <*> tail + +-- General Tools + +if' :: Bool -> a -> a -> a +if' p a b = if p then a else b diff --git a/exercises/tree-building/package.yaml b/exercises/tree-building/package.yaml new file mode 100644 index 000000000..f7b0edddc --- /dev/null +++ b/exercises/tree-building/package.yaml @@ -0,0 +1,21 @@ +name: tree-building +version: 1.0.0.0 + +dependencies: + - base + +library: + exposed-modules: TreeBuilding + 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: + - tree-building + - hspec diff --git a/exercises/tree-building/src/TreeBuilding.hs b/exercises/tree-building/src/TreeBuilding.hs new file mode 100644 index 000000000..1d70363b9 --- /dev/null +++ b/exercises/tree-building/src/TreeBuilding.hs @@ -0,0 +1,49 @@ +module TreeBuilding (newTree, Record(..), Tree(..)) where + +import Data.List +import Data.Ord (comparing) +--import Data.Maybe (fromMaybe) + +type Id = Int +type Children = [Tree] + +data Record = Record Id (Maybe Id) deriving (Show) + +data Tree = Leaf Id | Branch Id Children deriving (Eq, Show) + +newTree :: [Record] -> Maybe Tree +newTree records + | cycleCheck records = Nothing + | succIdCheck records == True = build . groupByParent $ records + | otherwise = Nothing + where + recordParent = \(Record _ p) -> p + recordId = \(Record i _) -> i + succIdCheck = all (\(x,y) -> succ x == y) . pairwise . sort . map recordId + where pairwise = zip <*> tail + cycleCheck = any id . map (\(r) -> case r of + (Record i Nothing) -> if i == 0 + then False + else True + (Record i (Just p)) -> if p >= i + then True + else False) + groupByParent = sortOn fst . map (\xs -> (recordParent (head xs), map recordId xs)) . groupBy parentsEq . sortBy (comparing recordParent) . sortBy (comparing recordId) + parentsEq rx ry = recordParent rx == recordParent ry + build [] = Nothing + build (x:xs) + | not . rootCheck $ x = Nothing + | xs == [] = Just (Leaf 0) + | otherwise = Just (Branch 0 (recursiveBuilding xs)) + where + rootCheck (p,ids) + | p /= Nothing || (length ids /= 1 && (head ids) /= 0) = False + | otherwise = True + recursiveBuilding ys + | length ys == 1 = map (\yid -> Leaf yid) (snd (head ys)) + | otherwise = (\(z:zs) -> map (\xid -> + let children = filter ((== (Just xid)) . fst) zs + in if length children > 0 + then Branch xid (recursiveBuilding children) + else Leaf xid + ) (snd z)) ys diff --git a/exercises/tree-building/stack.yaml b/exercises/tree-building/stack.yaml new file mode 100644 index 000000000..8796ce2b9 --- /dev/null +++ b/exercises/tree-building/stack.yaml @@ -0,0 +1 @@ +resolver: lts-12.4 diff --git a/exercises/tree-building/test/Tests.hs b/exercises/tree-building/test/Tests.hs new file mode 100644 index 000000000..72aedfc2f --- /dev/null +++ b/exercises/tree-building/test/Tests.hs @@ -0,0 +1,146 @@ +{-# LANGUAGE RecordWildCards #-} + +import Data.Foldable (for_) +import Test.Hspec (Spec, describe, it, shouldBe) +import Test.Hspec.Runner (configFastFail, defaultConfig, hspecWith) + +import TreeBuilding (newTree, Tree(..), Record(..)) + +main :: IO () +main = hspecWith defaultConfig {configFastFail = True} specs + +specs :: Spec +specs = describe "tree-building" $ for_ cases test + where + + test Case{..} = it description assertion + where + assertion = newTree records `shouldBe` expected + +data Case = Case { description :: String + , records :: [Record] + , expected :: Maybe Tree + } + +cases :: [Case] +cases = [ Case { description = "Empty input" + , records = [] + , expected = Nothing + } + , Case { description = "One Record" + , records = [ Record 0 Nothing ] + , expected = Just (Leaf 0) + } + , Case { description = "Three nodes in order" + , records = [ Record 0 Nothing + , Record 1 (Just 0) + , Record 2 (Just 0) + ] + , expected = Just (Branch 0 [ Leaf 1, Leaf 2 ]) + } + , Case { description = "Three nodes in reverse order" + , records = [ Record 2 (Just 0) + , Record 1 (Just 0) + , Record 0 Nothing + ] + , expected = Just (Branch 0 [ Leaf 1, Leaf 2 ]) + } + , Case { description = "More than two children" + , records = [ Record 3 (Just 0) + , Record 2 (Just 0) + , Record 1 (Just 0) + , Record 0 Nothing + ] + , expected = Just (Branch 0 [ Leaf 1 + , Leaf 2 + , Leaf 3 ]) + } + , Case { description = "Binary tree" + , records = [ Record 5 (Just 1) + , Record 3 (Just 2) + , Record 2 (Just 0) + , Record 4 (Just 1) + , Record 1 (Just 0) + , Record 0 Nothing + , Record 6 (Just 2) + ] + , expected = Just (Branch 0 [ Branch 1 [ Leaf 4 + , Leaf 5 ] + , Branch 2 [ Leaf 3 + , Leaf 6 ] + ]) + } + , Case { description = "Unbalanced tree" + , records = [ Record 5 (Just 2) + , Record 3 (Just 2) + , Record 2 (Just 0) + , Record 4 (Just 1) + , Record 1 (Just 0) + , Record 0 Nothing + , Record 6 (Just 2) + ] + , expected = Just (Branch 0 [ Branch 1 [ Leaf 4 ] + , Branch 2 [ Leaf 3 + , Leaf 5 + , Leaf 6 ] + ]) + } + , Case { description = "Root has parent" + , records = [ Record 0 (Just 1) + , Record 1 (Just 0) + ] + , expected = Nothing + } + , Case { description = "No root" + , records = [ Record 1 (Just 0) ] + , expected = Nothing + } + , Case { description = "Duplicate" + , records = [ Record 0 Nothing + , Record 1 (Just 0) + , Record 1 (Just 0) + ] + , expected = Nothing + } + , Case { description = "Duplicate root" + , records = [ Record 0 Nothing, Record 0 Nothing ] + , expected = Nothing + } + , Case { description = "Non-continuous" + , records = [ Record 2 (Just 0) + , Record 4 (Just 2) + , Record 1 (Just 0) + , Record 0 Nothing + ] + , expected = Nothing + } + , Case { description = "Cycle Directly" + , records = [ Record 5 (Just 2) + , Record 3 (Just 2) + , Record 2 (Just 2) + , Record 4 (Just 1) + , Record 1 (Just 0) + , Record 0 Nothing + , Record 6 (Just 3) + ] + , expected = Nothing + } + , Case { description = "Cycle indirectly" + , records = [ Record 5 (Just 2) + , Record 3 (Just 2) + , Record 2 (Just 6) + , Record 4 (Just 1) + , Record 1 (Just 0) + , Record 0 Nothing + , Record 6 (Just 3) + ] + , expected = Nothing + } + , Case { description = "Higher id parent of lower id" + , records = [ Record 0 Nothing + , Record 2 (Just 0) + , Record 1 (Just 2) + ] + , expected = Nothing + } + ]