diff --git a/config.json b/config.json index 43f7e736..da83920f 100644 --- a/config.json +++ b/config.json @@ -1000,6 +1000,14 @@ "prerequisites": [], "difficulty": 5 }, + { + "slug": "ledger", + "name": "Ledger", + "uuid": "20bad781-7c4b-4133-8ce3-2160ab12a3a0", + "practices": [], + "prerequisites": [], + "difficulty": 5 + }, { "slug": "rectangles", "name": "Rectangles", diff --git a/exercises/practice/ledger/.docs/instructions.md b/exercises/practice/ledger/.docs/instructions.md new file mode 100644 index 00000000..a53e5c15 --- /dev/null +++ b/exercises/practice/ledger/.docs/instructions.md @@ -0,0 +1,14 @@ +# Instructions + +Refactor a ledger printer. + +The ledger exercise is a refactoring exercise. +There is code that prints a nicely formatted ledger, given a locale (American or Dutch) and a currency (US dollar or euro). +The code however is rather badly written, though (somewhat surprisingly) it consistently passes the test suite. + +Rewrite this code. +Remember that in refactoring the trick is to make small steps that keep the tests passing. +That way you can always quickly go back to a working version. +Version control tools like git can help here as well. + +Please keep a log of what changes you've made and make a comment on the exercise containing that log, this will help reviewers. diff --git a/exercises/practice/ledger/.meta/Ledger.example.ps1 b/exercises/practice/ledger/.meta/Ledger.example.ps1 new file mode 100644 index 00000000..80f3535c --- /dev/null +++ b/exercises/practice/ledger/.meta/Ledger.example.ps1 @@ -0,0 +1,130 @@ +<# +.SYNOPSIS + Refactor a ledger printer. + +.DESCRIPTION + The code below is an attempt of creating a printer for a ledger. + It barely works (only passed some of the tests), and is generally quite messy. + Your job here is to refactor the code. + +.EXAMPLE + $entry1 = CreateEntry -Date '2011-12-13' -Desc 'Birthday present' -Amount 1234 + $entry2 = CreateEntry -Date '2011-11-19' -Desc 'Party prep & catering services' -Amount 98765 + FormatEntries -Currency "EUR" -Locale "en-US" -Entries @($entry1, $entry2) + + Returns: + @" + Date | Description | Change + 11/19/2011 | Party prep & catering ... | €987.65 + 12/13/2011 | Birthday present | €12.34 + "@ +#> + +Class LedgerEntry{ + [datetime] $Date + [string] $Desc + [int] $Change + + LedgerEntry($date, $desc, $change) { + $this.Date = $date + $this.Desc = $desc + $this.Change = $change + } + + [string] Format([string] $Currency, [string] $Locale) { + $format = $this.GetFrame($Locale, $this.Change) + $dateFormat = $this.GetDateFormat($Currency, $Locale) + + $dateStr = $this.Date.ToString($dateFormat) + $moneyStr = ($this.Change / 100).ToString("C2", [cultureinfo]::new($Locale)) + + if ($Currency -eq "EUR" -and $Locale -eq "en-US") { + $moneyStr = $moneyStr -replace '\$', '€' + } + if ($Currency -eq "USD" -and $Locale -eq "nl-NL") { + $moneyStr = $moneyStr -replace '€', '$' + } + if ($this.Desc.Length -gt 25) { + $this.Desc = $this.Desc.Substring(0,22) + "..." + } + return $format -f @($dateStr, $this.Desc, $moneyStr) + } + + [string] hidden GetFrame([string] $Locale, [int] $Change) { + if ($Locale -eq "nl-NL" -or $Change -ge 0) { + return "{0,-10} | {1,-25} | {2,12} " + } + return "{0,-10} | {1,-25} | {2,13}" + } + + [object] hidden GetDateFormat([string] $Currency, [string] $Locale) { + $dateFrm = switch ($Locale) { + "en-US" { "MM\/dd\/yyyy" } + "nl-NL" { "dd-MM-yyyy" } + Default {Throw "Locale not supported"} + } + return $dateFrm + } + + [string] static Header($Locale) { + $format = "{0,-10} | {1,-25} | {2,-13}" + $info = switch ($Locale) { + "en-US" { @("Date", "Description", "Change") } + "nl-NL" { @("Datum", "Omschrijving", "Verandering") } + Default {Throw "Locale not supported"} + } + return $format -f $info + } +} + +Function CreateEntry { + <# + .DESCRIPTION + A function to create an entry for the ledger. + This function is required for the test suite. + + .PARAMETER Date + String represent the date. + + .PARAMETER Desc + String represent the description of the entry. + + .PARAMETER Amount + Integer represent the amount of money in cents. + #> + param ( + [string] $Date, + [string] $Desc, + [int] $Amount + ) + [LedgerEntry]::new($Date, $Desc, $Amount) +} + +Function FormatEntries { + <# + .DESCRIPTION + A function to formats the entries of the ledger based on other info. + This function is required for the test suite. + + .PARAMETER Currency + String represent the currency symbol. + + .PARAMETER Locale + String represent the region and culture to be followed. + + .PARAMETER Entries + Array of entries, each is an instance of the class LedgerEntry, created via CreateEntry function. + #> + param ( + [string] $Currency, + [string] $Locale, + [LedgerEntry[]] $Entries + ) + $table = @() + $table += [LedgerEntry]::Header($Locale) + $Entries = $Entries | Sort-Object Date, Desc, Change + foreach ($entry in $Entries) { + $table += $entry.Format($Currency, $Locale) + } + $table -join "`n" +} diff --git a/exercises/practice/ledger/.meta/config.json b/exercises/practice/ledger/.meta/config.json new file mode 100644 index 00000000..acef10db --- /dev/null +++ b/exercises/practice/ledger/.meta/config.json @@ -0,0 +1,17 @@ +{ + "authors": [ + "glaxxie" + ], + "files": { + "solution": [ + "Ledger.ps1" + ], + "test": [ + "Ledger.tests.ps1" + ], + "example": [ + ".meta/Ledger.example.ps1" + ] + }, + "blurb": "Refactor a ledger printer." +} diff --git a/exercises/practice/ledger/.meta/tests.toml b/exercises/practice/ledger/.meta/tests.toml new file mode 100644 index 00000000..e71dfbfc --- /dev/null +++ b/exercises/practice/ledger/.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. + +[d131ecae-a30e-436c-b8f3-858039a27234] +description = "empty ledger" + +[ce4618d2-9379-4eca-b207-9df1c4ec8aaa] +description = "one entry" + +[8d02e9cb-e6ee-4b77-9ce4-e5aec8eb5ccb] +description = "credit and debit" + +[502c4106-0371-4e7c-a7d8-9ce33f16ccb1] +description = "multiple entries on same date ordered by description" + +[29dd3659-6c2d-4380-94a8-6d96086e28e1] +description = "final order tie breaker is change" + +[9b9712a6-f779-4f5c-a759-af65615fcbb9] +description = "overlong description is truncated" + +[67318aad-af53-4f3d-aa19-1293b4d4c924] +description = "euros" + +[bdc499b6-51f5-4117-95f2-43cb6737208e] +description = "Dutch locale" + +[86591cd4-1379-4208-ae54-0ee2652b4670] +description = "Dutch locale and euros" + +[876bcec8-d7d7-4ba4-82bd-b836ac87c5d2] +description = "Dutch negative number with 3 digits before decimal point" + +[29670d1c-56be-492a-9c5e-427e4b766309] +description = "American negative number with 3 digits before decimal point" diff --git a/exercises/practice/ledger/Ledger.ps1 b/exercises/practice/ledger/Ledger.ps1 new file mode 100644 index 00000000..7c0603eb --- /dev/null +++ b/exercises/practice/ledger/Ledger.ps1 @@ -0,0 +1,234 @@ +<# +.SYNOPSIS + Refactor a ledger printer. + +.DESCRIPTION + The code below is an attempt of creating a printer for a ledger. + It barely works (only passed some of the tests), and is generally quite messy. + Your job here is to refactor the code. + +.EXAMPLE + $entry1 = CreateEntry -Date '2011-12-13' -Desc 'Birthday present' -Amount 1234 + $entry2 = CreateEntry -Date '2011-11-19' -Desc 'Party prep & catering services' -Amount 98765 + FormatEntries -Currency "EUR" -Locale "en-US" -Entries @($entry1, $entry2) + + Returns: + @" + Date | Description | Change + 11/19/2011 | Party prep & catering ... | €987.65 + 12/13/2011 | Birthday present | €12.34 + "@ +#> + +Class LedgerEntry{ + [string] $Date + [string] $Desc + [double] $Change + + LedgerEntry() { + $this.Date = $null + $this.Desc = $null + $this.Change = $null + } + + LedgerEntry($date, $desc, $change) { + $this.Date = $date + $this.Desc = $desc + $this.Change = $change + } +} + +Function CreateEntry { + <# + .DESCRIPTION + A function to create an entry for the ledger. + This function is required for the test suite. + + .PARAMETER Date + String represent the date. + + .PARAMETER Desc + String represent the description of the entry. + + .PARAMETER Amount + Integer represent the amount of money in cents. + #> + param ( + [string] $Date, + [string] $Desc, + [int] $Amount + ) + + $entry = [LedgerEntry]::new() + + if (-not $entry.Date) { + $entry.Date = $Date + } + + if (-not $entry.Desc) { + $entry.Desc = $Desc + } + + if (-not $entry.Change) { + $entry.Change = $Amount + } + return $entry +} + +Function FormatEntries { + <# + .DESCRIPTION + A function to formats the entries of the ledger based on other info. + This function is required for the test suite. + + .PARAMETER Currency + String represent the currency symbol. + + .PARAMETER Locale + String represent the region and culture to be followed. + + .PARAMETER Entries + Array of entries, each is an instance of the class LedgerEntry, created via CreateEntry function. + #> + param ( + [string] $Currency, + [string] $Locale, + [LedgerEntry[]] $Entries + ) + $table = @() + $table += MakeHeader $Locale + $Entries = $Entries | Sort-Object Date, Desc, Change + foreach ($entry in $Entries) { + $table += MakeEntry $Currency $Locale $entry + } + $result = $table -join "`n" + return $result +} + +Function MakeHeader($Locale) { + <# + .DESCRIPTION + An optional helper function that help creating the header. + #> + if ($Locale -eq 'en-Us') { + $header = @("") * 54 + for ($i = 0; $i -lt $header.Count; $i++) { + if ($i -lt 4){ + $header[$i] = "Date"[$i] + }elseif ($i -gt 12 -and $i -lt 24) { + $header[$i] = "Description"[$i-13] + }elseif ($i -gt 40 -and $i -lt 47) { + $header[$i] = "Change"[$i-41] + }elseif ($i -eq 11 -or $i -eq 39) { + $header[$i] = "|" + }else { + $header[$i] = " " + } + } + }elseif ($Locale -eq 'nl-NL') { + $header = @("") * 54 + for ($i = 0; $i -lt $header.Count; $i++) { + if ($i -lt 5){ + $header[$i] = "Datum"[$i] + }elseif ($i -gt 12 -and $i -lt 25) { + $header[$i] = "Omschrijving"[$i-13] + }elseif ($i -gt 40 -and $i -lt 52) { + $header[$i] = "Verandering"[$i-41] + }elseif ($i -eq 11 -or $i -eq 39) { + $header[$i] = "|" + }else { + $header[$i] = " " + } + } + } + return $header -join "" +} + +Function MakeEntry { + <# + .DESCRIPTION + An optional helper function that create and format each entry. + #> + param ( + [string] $Currency, + [string] $Locale, + [LedgerEntry] $Entry + ) + + if ($Currency -eq "USD") { + if ($Locale -eq "en-US") { + $symbol = "$" + $date = [datetime]$Entry.Date + $dateStr = $date.ToString("MM\/dd\/yyyy") + if($Entry.Change -lt 0) { + $money = $entry.Change / 100 + $money = $money.ToString("C2") + $change = $money + }else { + $money = $entry.Change / 100 + $money = $money.ToString("N2", [cultureinfo]"en-US") + $change = "$symbol$money " + } + + }elseif ($Locale -eq "nl-NL") { + $symbol = "$" + $date = [datetime]$Entry.Date + $dateStr = $date.ToString("dd-MM-yyyy") + if($Entry.Change -lt 0) { + $money = $entry.Change / 100 + $money = $money.ToString("N2", [cultureinfo]"nl-NL") + $change = "$symbol $money " + }else { + $money = $entry.Change / 100 + $money = $money.ToString("N2", [cultureinfo]"nl-NL") + $change = "$symbol $money " + } + } + + }elseif ($Currency -eq "EUR") { + if ($Locale -eq "en-US") { + $symbol = "€" + $date = [datetime]$Entry.Date + $dateStr = $date.ToString("MM\/dd\/yyyy") + if($Entry.Change -lt 0) { + $money = $entry.Change / 100 + $money = $money.ToString("C2") + $change = $money -replace "\$", $symbol + }else { + $money = $entry.Change / 100 + $money = $money.ToString("N2", [cultureinfo]"en-US") + $change = "$symbol$money" + } + }elseif ($Locale -eq "nl-NL") { + $symbol = "€" + $date = [datetime]$Entry.Date + $dateStr = $date.ToString("dd-MM-yyyy") + if($Entry.Change -lt 0) { + $money = $entry.Change / 100 + $money = $money.ToString("N2", [cultureinfo]"nl-NL") + $change = "$symbol $money " + }else { + $money = $entry.Change / 100 + $money = $money.ToString("N2", [cultureinfo]"nl-NL") + $change = "$symbol $money " + } + } + } + + #Truncated too long desc + $desc = "" + if ($Entry.Desc.Length -gt 25) { + for ($i = 0; $i -lt 22; $i++) { + $desc += $Entry.Desc[$i] + } + $desc += "..." + }else { + $desc = $Entry.Desc + } + + #Format the text into correct space + $desc = "{0,-25}" -f $desc + $change = "{0,13}" -f $change + + "$dateStr | $desc | $change" +} \ No newline at end of file diff --git a/exercises/practice/ledger/Ledger.tests.ps1 b/exercises/practice/ledger/Ledger.tests.ps1 new file mode 100644 index 00000000..751816e2 --- /dev/null +++ b/exercises/practice/ledger/Ledger.tests.ps1 @@ -0,0 +1,206 @@ +BeforeAll { + . "./Ledger.ps1" +} + +Describe "Ledger test cases" { + It "empty ledger" { + $params = @{ + Currency = "USD" + Locale = "en-US" + Entries = @() + } + + $got = FormatEntries @params + $want = 'Date | Description | Change ' + + $got | Should -BeExactly $want + } + + It "one entry" { + $params = @{ + Currency = "USD" + Locale = "en-US" + Entries = @( + CreateEntry -Date '2015-01-01' -Desc 'Buy present' -Amount -1000 + ) + } + + $got = FormatEntries @params + $want = @( + 'Date | Description | Change ', + '01/01/2015 | Buy present | ($10.00)' + ) -join "`n" + + $got | Should -BeExactly $want + } + + It "credit and debit" { + $params = @{ + Currency = "USD" + Locale = "en-US" + Entries = @( + CreateEntry -Date '2015-01-02' -Desc 'Get present' -Amount 1000 + CreateEntry -Date '2015-01-01' -Desc 'Buy present' -Amount -1000 + ) + } + + $got = FormatEntries @params + $want = @( + 'Date | Description | Change ' + '01/01/2015 | Buy present | ($10.00)' + '01/02/2015 | Get present | $10.00 ' + ) -join "`n" + + $got | Should -BeExactly $want + } + + It "multiple entries on same date ordered by description" { + $params = @{ + Currency = "USD" + Locale = "en-US" + Entries = @( + CreateEntry -Date '2015-01-01' -Desc 'Get present' -Amount 1000 + CreateEntry -Date '2015-01-01' -Desc 'Buy present' -Amount -1000 + ) + } + + $got = FormatEntries @params + $want = @( + 'Date | Description | Change ' + '01/01/2015 | Buy present | ($10.00)' + '01/01/2015 | Get present | $10.00 ' + ) -join "`n" + + $got | Should -BeExactly $want + } + + It "final order tie breaker is change" { + $params = @{ + Currency = "USD" + Locale = "en-US" + Entries = @( + CreateEntry -Date '2015-01-01' -Desc 'Something' -Amount 0 + CreateEntry -Date '2015-01-01' -Desc 'Something' -Amount -1 + CreateEntry -Date '2015-01-01' -Desc 'Something' -Amount 1 + ) + } + + $got = FormatEntries @params + $want = @( + 'Date | Description | Change ' + '01/01/2015 | Something | ($0.01)' + '01/01/2015 | Something | $0.00 ' + '01/01/2015 | Something | $0.01 ' + ) -join "`n" + + $got | Should -BeExactly $want + } + + It "overlong description is truncated" { + $params = @{ + Currency = "USD" + Locale = "en-US" + Entries = @( + CreateEntry -Date '2015-01-01' -Desc 'Freude schoner Gotterfunken' -Amount -123456 + ) + } + + $got = FormatEntries @params + $want = @( + 'Date | Description | Change ', + '01/01/2015 | Freude schoner Gotterf... | ($1,234.56)' + ) -join "`n" + + $got | Should -BeExactly $want + } + + It "euros" { + $params = @{ + Currency = "EUR" + Locale = "en-US" + Entries = @( + CreateEntry -Date '2015-01-01' -Desc 'Buy present' -Amount -1000 + ) + } + + $got = FormatEntries @params + $want = @( + 'Date | Description | Change ', + '01/01/2015 | Buy present | (€10.00)' + ) -join "`n" + + $got | Should -BeExactly $want + } + + It "Dutch locale" { + $params = @{ + Currency = "USD" + Locale = "nl-NL" + Entries = @( + CreateEntry -Date '2015-03-12' -Desc 'Buy present' -Amount 123456 + ) + } + + $got = FormatEntries @params + $want = @( + 'Datum | Omschrijving | Verandering ', + '12-03-2015 | Buy present | $ 1.234,56 ' + ) -join "`n" + + $got | Should -BeExactly $want + } + + It "Dutch locale and euros" { + $params = @{ + Currency = "EUR" + Locale = "nl-NL" + Entries = @( + CreateEntry -Date '2015-03-12' -Desc 'Buy present' -Amount 123456 + ) + } + + $got = FormatEntries @params + $want = @( + 'Datum | Omschrijving | Verandering ', + '12-03-2015 | Buy present | € 1.234,56 ' + ) -join "`n" + + $got | Should -BeExactly $want + } + + It "Dutch negative number with 3 digits before decimal point" { + $params = @{ + Currency = "USD" + Locale = "nl-NL" + Entries = @( + CreateEntry -Date '2015-03-12' -Desc 'Buy present' -Amount -12345 + ) + } + + $got = FormatEntries @params + $want = @( + 'Datum | Omschrijving | Verandering ', + '12-03-2015 | Buy present | $ -123,45 ' + ) -join "`n" + + $got | Should -BeExactly $want + } + + It "American negative number with 3 digits before decimal point" { + $params = @{ + Currency = "USD" + Locale = "en-US" + Entries = @( + CreateEntry -Date '2015-03-12' -Desc 'Buy present' -Amount -12345 + ) + } + + $got = FormatEntries @params + $want = @( + 'Date | Description | Change ', + '03/12/2015 | Buy present | ($123.45)' + ) -join "`n" + + $got | Should -BeExactly $want + } +}