From 9ba5f1391358e49e19e7795afa197d2be8b09142 Mon Sep 17 00:00:00 2001 From: glaxxie <86179463+glaxxie@users.noreply.github.com> Date: Tue, 20 Feb 2024 11:03:13 -0600 Subject: [PATCH] [New Exercise]: Tree Building --- config.json | 8 + .../tree-building/.docs/instructions.md | 24 +++ .../.meta/TreeBuilding.example.ps1 | 67 ++++++ .../practice/tree-building/.meta/config.json | 20 ++ .../practice/tree-building/TreeBuilding.ps1 | 104 ++++++++++ .../tree-building/TreeBuilding.tests.ps1 | 194 ++++++++++++++++++ 6 files changed, 417 insertions(+) create mode 100644 exercises/practice/tree-building/.docs/instructions.md create mode 100644 exercises/practice/tree-building/.meta/TreeBuilding.example.ps1 create mode 100644 exercises/practice/tree-building/.meta/config.json create mode 100644 exercises/practice/tree-building/TreeBuilding.ps1 create mode 100644 exercises/practice/tree-building/TreeBuilding.tests.ps1 diff --git a/config.json b/config.json index e0ffc35..c02db5e 100644 --- a/config.json +++ b/config.json @@ -1008,6 +1008,14 @@ "prerequisites": [], "difficulty": 5 }, + { + "slug": "tree-building", + "name": "Tree Building", + "uuid": "088fcf19-851f-4fb9-a740-7d9d14362432", + "practices": [], + "prerequisites": [], + "difficulty": 5 + }, { "slug": "rectangles", "name": "Rectangles", diff --git a/exercises/practice/tree-building/.docs/instructions.md b/exercises/practice/tree-building/.docs/instructions.md new file mode 100644 index 0000000..0148e8a --- /dev/null +++ b/exercises/practice/tree-building/.docs/instructions.md @@ -0,0 +1,24 @@ +# Instructions + +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) +``` diff --git a/exercises/practice/tree-building/.meta/TreeBuilding.example.ps1 b/exercises/practice/tree-building/.meta/TreeBuilding.example.ps1 new file mode 100644 index 0000000..cd85914 --- /dev/null +++ b/exercises/practice/tree-building/.meta/TreeBuilding.example.ps1 @@ -0,0 +1,67 @@ +Class Record { + $RecordId + $ParentId + + Record($RecordId, $parentId) { + $this.RecordId = $RecordId + $this.ParentId = $parentId + } + + [void] Validate($upperBound){ + if ($this.RecordId -ge $upperBound) { + Throw "Record id is invalid or out of order." + } + if ($this.RecordId -eq $this.ParentId -and $this.RecordId -ne 0) { + Throw "Only root should have equal record and parent id (0)." + } + if ($this.RecordId -lt $this.ParentId) { + Throw "Node record ID should be greater than parent id." + } + } +} + +Class Node { + [int] $NodeID + [int] $ParentID + [Node[]] $Children + + Node([Record] $record) { + $this.NodeID = $record.RecordId + $this.ParentID = $record.ParentId + $this.Children = @() + } + + [bool] IsLeaf() { + return $this.Children.Count -eq 0 + } +} + +Function Build-Tree() { + <# + .DESCRIPTION + Building tree function + + .PARAMETER Records + Records used to build tree + #> + [CmdletBinding()] + Param( + [Record[]] $Records + ) + $upperBound = $Records.Count + + $Records + | Sort-Object RecordId + | ForEach-Object {$nodes = @{}} { + $_.Validate($upperBound) + $nodes[$_.RecordId] = [Node]::new($_) + } + + foreach ($id in $nodes.Keys) { + if ($id -ne 0) { + $parent = $nodes[$id].ParentId + $nodes[$parent].Children = @($nodes[$id]) + $nodes[$parent].Children + } + } + $nodes[0] +} \ No newline at end of file diff --git a/exercises/practice/tree-building/.meta/config.json b/exercises/practice/tree-building/.meta/config.json new file mode 100644 index 0000000..80b7578 --- /dev/null +++ b/exercises/practice/tree-building/.meta/config.json @@ -0,0 +1,20 @@ +{ + "authors": [ + "glaxxie" + ], + "contributors": [ + "clapmyhands" + ], + "files": { + "solution": [ + "TreeBuilding.ps1" + ], + "test": [ + "TreeBuilding.tests.ps1" + ], + "example": [ + ".meta/TreeBuilding.example.ps1" + ] + }, + "blurb": "Refactor a tree building algorithm." +} diff --git a/exercises/practice/tree-building/TreeBuilding.ps1 b/exercises/practice/tree-building/TreeBuilding.ps1 new file mode 100644 index 0000000..51c9839 --- /dev/null +++ b/exercises/practice/tree-building/TreeBuilding.ps1 @@ -0,0 +1,104 @@ +<# +.SYNOPSIS + Refactor a tree building algorithm. + +.DESCRIPTION + The code below are ugly, confusing and slow. + Not to mention it also failed all the test that need to raise error. + Feel free to rework it however you want to pass the test suite. +#> + +# this stub is adapted from the python track +Class Record { + $RecordId + $ParentId + + Record($RecordId, $parentId) { + $this.RecordId = $RecordId + $this.ParentId = $parentId + } +} + +Class Node { + [int] $NodeID + [Node[]] $Children + + Node($id) { + $this.NodeID = $id + $this.Children = @() + } + + [bool] IsLeaf() { + if ($this.HasChildren()) { + return $false + }else { + return $true + } + } + + [bool] HasChildren() { + if ($this.Children.Count -gt 0) { + return $true + }else { + return $false + } + } +} + +Function Build-Tree() { + <# + .DESCRIPTION + Building tree function + + .PARAMETER Records + Records used to build tree + #> + [CmdletBinding()] + Param( + [Record[]] $Records + ) + $root = $null + + $sortedRecords = $Records | Sort-Object RecordId + + $ordered_id = foreach ($r in $sortedRecords) { + $r.RecordId + } + + $trees = @() + $parent = @{} + + foreach ($i in 0..($ordered_id.Count - 1)) { + foreach ($j in $sortedRecords) { + if ($ordered_id[$i] -eq $j.RecordId) { + $trees += [Node]::new($ordered_id[$i]) + } + } + } + + foreach ($i in 0..($ordered_id.Length - 1)) { + foreach ($j in $trees) { + if ($i -eq $j.NodeID) { + $parent = $j + } + } + foreach ($j in $sortedRecords) { + if ($j.ParentId -eq $i) { + foreach ($k in $trees) { + if ($k.NodeID -eq 0) { + continue + } + if ($j.RecordId -eq $k.NodeID) { + $child = $k + $parent.Children += $child + } + } + } + } + } + + if ($trees.Count -gt 0) { + $root = $trees[0] + } + return $root +} \ No newline at end of file diff --git a/exercises/practice/tree-building/TreeBuilding.tests.ps1 b/exercises/practice/tree-building/TreeBuilding.tests.ps1 new file mode 100644 index 0000000..293a093 --- /dev/null +++ b/exercises/practice/tree-building/TreeBuilding.tests.ps1 @@ -0,0 +1,194 @@ +BeforeAll { + . "./TreeBuilding.ps1" + + # Utility functions + function NodeIsBranch() { + [CmdletBinding()] + Param( + [Node] $Node, + [int] $NodeId, + [int] $ChildrenCount + ) + + $Node.NodeId | Should -Be $NodeId + $Node.IsLeaf() | Should -BeFalse + $Node.Children.Count | Should -BeExactly $ChildrenCount + } + + function NodeIsLeaf($Node, $NodeId) { + $Node.NodeId | Should -Be $NodeId + $Node.IsLeaf() | Should -BeTrue + } +} + +Describe "TreeBuilding test cases" { + Context "Valid records input" { + It "empty list input" { + $records = @() + $tree = Build-Tree -Records $records + + $tree | Should -BeNullOrEmpty + } + + It "one Node" { + $records = @( + [Record]::new(0, 0) + ) + $tree = Build-Tree -Records $records + + NodeIsLeaf -Node $tree -NodeID 0 + } + + It "three Nodes in order" { + $records = @( + [Record]::new(0, 0) + [Record]::new(1, 0) + [Record]::new(2, 0) + ) + $tree = Build-Tree -Records $records + + NodeIsBranch -Node $tree -NodeID 0 -ChildrenCount 2 + NodeIsLeaf -Node $tree.Children[0] -NodeID 1 + NodeIsLeaf -Node $tree.Children[1] -NodeID 2 + } + + It "three Nodes in reverse order" { + $records = @( + [Record]::new(2, 0) + [Record]::new(1, 0) + [Record]::new(0, 0) + ) + $tree = Build-Tree -Records $records + + NodeIsBranch -Node $tree -NodeID 0 -ChildrenCount 2 + NodeIsLeaf -Node $tree.Children[0] -NodeID 1 + NodeIsLeaf -Node $tree.Children[1] -NodeID 2 + } + + It "more than two children" { + $records = @( + [Record]::new(0, 0) + [Record]::new(1, 0) + [Record]::new(2, 0) + [Record]::new(3, 0) + ) + $tree = Build-Tree -Records $records + + NodeIsBranch -Node $tree -NodeID 0 -ChildrenCount 3 + NodeIsLeaf -Node $tree.Children[0] -NodeID 1 + NodeIsLeaf -Node $tree.Children[1] -NodeID 2 + NodeIsLeaf -Node $tree.Children[2] -NodeID 3 + } + + It "binary tree" { + $records = @( + [Record]::new(6, 2) + [Record]::new(0, 0) + [Record]::new(3, 1) + [Record]::new(2, 0) + [Record]::new(4, 1) + [Record]::new(5, 2) + [Record]::new(1, 0) + ) + $tree = Build-Tree -Records $records + + NodeIsBranch -Node $tree -NodeID 0 -ChildrenCount 2 + NodeIsBranch -Node $tree.Children[0] -NodeID 1 -ChildrenCount 2 + NodeIsBranch -Node $tree.Children[1] -NodeID 2 -ChildrenCount 2 + NodeIsLeaf -Node $tree.Children[0].Children[0] -NodeID 3 + NodeIsLeaf -Node $tree.Children[0].Children[1] -NodeID 4 + NodeIsLeaf -Node $tree.Children[1].Children[0] -NodeID 5 + NodeIsLeaf -Node $tree.Children[1].Children[1] -NodeID 6 + } + + It "unbalanced treed" { + $records = @( + [Record]::new(5, 2) + [Record]::new(2, 0) + [Record]::new(3, 2) + [Record]::new(1, 0) + [Record]::new(4, 1) + [Record]::new(0, 0) + [Record]::new(6, 2) + ) + $tree = Build-Tree -Records $records + + NodeIsBranch -Node $tree -NodeID 0 -ChildrenCount 2 + NodeIsBranch -Node $tree.Children[0] -NodeID 1 -ChildrenCount 1 + NodeIsBranch -Node $tree.Children[1] -NodeID 2 -ChildrenCount 3 + NodeIsLeaf -Node $tree.Children[0].Children[0] -NodeID 4 + NodeIsLeaf -Node $tree.Children[1].Children[0] -NodeID 3 + NodeIsLeaf -Node $tree.Children[1].Children[1] -NodeID 5 + NodeIsLeaf -Node $tree.Children[1].Children[2] -NodeID 6 + } + } + + Context "Invalid records input" { + It "root Node has parent" { + $records = @( + [Record]::new(0, 1) + [Record]::new(1, 0) + ) + + {Build-Tree -Records $records} | Should -Throw "*Node record id should be greater than parent id.*" + } + + It "no root Node" { + $records = @( + [Record]::new(1, 0) + [Record]::new(2, 0) + ) + + {Build-Tree -Records $records} | Should -Throw "*Record id is invalid or out of order.*" + } + + It "non continuous" { + $records = @( + [Record]::new(2, 0) + [Record]::new(4, 2) + [Record]::new(1, 0) + [Record]::new(0, 0) + ) + + {Build-Tree -Records $records} | Should -Throw "*Record id is invalid or out of order.*" + } + + It "cycle directly" { + $records = @( + [Record]::new(5, 2) + [Record]::new(3, 2) + [Record]::new(2, 2) + [Record]::new(4, 1) + [Record]::new(1, 0) + [Record]::new(0, 0) + [Record]::new(6, 3) + ) + + {Build-Tree -Records $records} | Should -Throw "*Only root should have equal record and parent id (0).*" + } + + It "cycle indirectly" { + $records = @( + [Record]::new(5, 2) + [Record]::new(3, 2) + [Record]::new(2, 6) + [Record]::new(4, 1) + [Record]::new(1, 0) + [Record]::new(0, 0) + [Record]::new(6, 3) + ) + + {Build-Tree -Records $records} | Should -Throw "*Node record id should be greater than parent id.*" + } + + It "higher id parent of lower id" { + $records = @( + [Record]::new(0, 0) + [Record]::new(2, 0) + [Record]::new(1, 2) + ) + + {Build-Tree -Records $records} | Should -Throw "*Node record id should be greater than parent id.*" + } + } +} \ No newline at end of file