Avatar of aexolate

aexolate's solution

to Ledger in the C# Track

Published at Apr 23 2019 · 0 comments
Instructions
Test suite
Solution

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.

Running the tests

To run the tests, run the command dotnet test from within the exercise directory.

Initially, only the first test will be enabled. This is to encourage you to solve the exercise one step at a time. Once you get the first test passing, remove the Skip property from the next test and work on getting that test passing. Once none of the tests are skipped and they are all passing, you can submit your solution using exercism submit Ledger.cs

Further information

For more detailed information about the C# track, including how to get help if you're having trouble, please visit the exercism.io C# language page.

LedgerTest.cs

using Xunit;

public class LedgerTest
{
    [Fact]
    public void Empty_ledger()
    {
        var currency = "USD";
        var locale = "en-US";
        var entries = new LedgerEntry[0];
        var expected =
            "Date       | Description               | Change       ";

        Assert.Equal(expected, Ledger.Format(currency, locale, entries));
    }

    [Fact(Skip = "Remove to run test")]
    public void One_entry()
    {
        var currency = "USD";
        var locale = "en-US";
        var entries = new[] 
        {
            Ledger.CreateEntry("2015-01-01", "Buy present", -1000)
        };
        var expected =
            "Date       | Description               | Change       \n" +
            "01/01/2015 | Buy present               |      ($10.00)";

        Assert.Equal(expected, Ledger.Format(currency, locale, entries));
    }

    [Fact(Skip = "Remove to run test")]
    public void Credit_and_debit()
    {
        var currency = "USD";
        var locale = "en-US";
        var entries = new[] 
        {
            Ledger.CreateEntry("2015-01-02", "Get present", 1000),
            Ledger.CreateEntry("2015-01-01", "Buy present", -1000)
        };
        var expected =
            "Date       | Description               | Change       \n" +
            "01/01/2015 | Buy present               |      ($10.00)\n" +
            "01/02/2015 | Get present               |       $10.00 ";

        Assert.Equal(expected, Ledger.Format(currency, locale, entries));
    }

    [Fact(Skip = "Remove to run test")]
    public void Multiple_entries_on_same_date_ordered_by_description()
    {
        var currency = "USD";
        var locale = "en-US";
        var entries = new[] 
        {
            Ledger.CreateEntry("2015-01-01", "Buy present", -1000),
            Ledger.CreateEntry("2015-01-01", "Get present", 1000)
        };
        var expected =
            "Date       | Description               | Change       \n" +
            "01/01/2015 | Buy present               |      ($10.00)\n" +
            "01/01/2015 | Get present               |       $10.00 ";

        Assert.Equal(expected, Ledger.Format(currency, locale, entries));
    }

    [Fact(Skip = "Remove to run test")]
    public void Final_order_tie_breaker_is_change()
    {
        var currency = "USD";
        var locale = "en-US";
        var entries = new[] 
        {
            Ledger.CreateEntry("2015-01-01", "Something", 0),
            Ledger.CreateEntry("2015-01-01", "Something", -1),
            Ledger.CreateEntry("2015-01-01", "Something", 1)
        };
        var expected =
            "Date       | Description               | Change       \n" +
            "01/01/2015 | Something                 |       ($0.01)\n" +
            "01/01/2015 | Something                 |        $0.00 \n" +
            "01/01/2015 | Something                 |        $0.01 ";

        Assert.Equal(expected, Ledger.Format(currency, locale, entries));
    }

    [Fact(Skip = "Remove to run test")]
    public void Overlong_descriptions()
    {
        var currency = "USD";
        var locale = "en-US";
        var entries = new[] 
        {
            Ledger.CreateEntry("2015-01-01", "Freude schoner Gotterfunken", -123456)
        };
        var expected =
            "Date       | Description               | Change       \n" +
            "01/01/2015 | Freude schoner Gotterf... |   ($1,234.56)";

        Assert.Equal(expected, Ledger.Format(currency, locale, entries));
    }

    [Fact(Skip = "Remove to run test")]
    public void Euros()
    {
        var currency = "EUR";
        var locale = "en-US";
        var entries = new[] 
        {
            Ledger.CreateEntry("2015-01-01", "Buy present", -1000)
        };
        var expected =
            "Date       | Description               | Change       \n" +
            "01/01/2015 | Buy present               |      (€10.00)";

        Assert.Equal(expected, Ledger.Format(currency, locale, entries));
    }

    [Fact(Skip = "Remove to run test")]
    public void Dutch_locale()
    {
        var currency = "USD";
        var locale = "nl-NL";
        var entries = new[] 
        {
            Ledger.CreateEntry("2015-03-12", "Buy present", 123456)
        };
        var expected =
            "Datum      | Omschrijving              | Verandering  \n" +
            "12-03-2015 | Buy present               |   $ 1.234,56 ";

        Assert.Equal(expected, Ledger.Format(currency, locale, entries));
    }

    [Fact(Skip = "Remove to run test")]
    public void Dutch_negative_number_with_3_digits_before_decimal_point()
    {
        var currency = "USD";
        var locale = "nl-NL";
        var entries = new[] 
        {
            Ledger.CreateEntry("2015-03-12", "Buy present", -12345)
        };
        var expected =
            "Datum      | Omschrijving              | Verandering  \n" +
            "12-03-2015 | Buy present               |     $ -123,45";

        Assert.Equal(expected, Ledger.Format(currency, locale, entries));
    }

    [Fact(Skip = "Remove to run test")]
    public void American_negative_number_with_3_digits_before_decimal_point()
    {
        var currency = "USD";
        var locale = "en-US";
        var entries =   new[] 
        {
            Ledger.CreateEntry("2015-03-12", "Buy present", -12345)
        };
        var expected =
            "Date       | Description               | Change       \n" +
            "03/12/2015 | Buy present               |     ($123.45)";

        Assert.Equal(expected, Ledger.Format(currency, locale, entries));
    }
}
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;

/*
 *  List of changes
 * 
 *  - Full property names
 *  - CreateEntry() - =>
 *  - CreateCulture() - Reformat ifelse
 *  - PrintHead() - Use switch statement 
 *  - PrintEntry() - string interpolation
 *  - Format() - foreach
 * 
 */

public class LedgerEntry
{
    public LedgerEntry(DateTime date, string description, decimal change)
    {
        Date = date;
        Description = description;
        Change = change;
    }

    public DateTime Date { get; }
    public string Description { get; }
    public decimal Change { get; }
}

public static class Ledger
{
    private static readonly ReadOnlyDictionary<string, string> CurrencySymbols
        = new ReadOnlyDictionary<string, string>(
           new Dictionary<string, string>
           {
               ["USD"] = "$",
               ["EUR"] = "€"
           });

    private static readonly ReadOnlyDictionary<string, (int currencyNegativePattern, string datePattern)> LocaleInfo
    = new ReadOnlyDictionary<string, (int currencyNegativePattern, string datePattern)>(
       new Dictionary<string, (int currencyNegativePattern, string datePattern)>
       {
           ["en-US"] = (0, "MM/dd/yyyy" ),
           ["nl-NL"] = (12, "dd/MM/yyyy")
       });

    public static LedgerEntry CreateEntry(string date, string desc, int chng)
        => new LedgerEntry(DateTime.Parse(date, CultureInfo.InvariantCulture), desc, chng / 100.0m);

    private static CultureInfo CreateCulture(string cur, string locale)
    {
        if (!CurrencySymbols.ContainsKey(cur) || !LocaleInfo.ContainsKey(locale))
            throw new ArgumentException("Invalid currency");

        var culture = new CultureInfo(locale);
        culture.NumberFormat.CurrencySymbol = CurrencySymbols[cur];
        culture.NumberFormat.CurrencyNegativePattern = LocaleInfo[locale].currencyNegativePattern;
        culture.DateTimeFormat.ShortDatePattern = LocaleInfo[locale].datePattern;
        return culture;
    }

    private static string PrintHead(string loc)
    {
        switch (loc)
        {
            case "en-US":
                return "Date       | Description               | Change       ";

            case "nl-NL":
                return "Datum      | Omschrijving              | Verandering  ";

            default:
                throw new ArgumentException("Invalid locale");
        }
    }

    private static string Date(IFormatProvider culture, DateTime date)
        => date.ToString("d", culture);

    private static string Description(string desc)
        => desc.Length < 25 ? desc : desc.Substring(0, 22) + "...";

    private static string Change(IFormatProvider culture, decimal cgh)
        => cgh < 0.0m ? cgh.ToString("C", culture) : cgh.ToString("C", culture) + " ";

    private static string PrintEntry(IFormatProvider culture, LedgerEntry entry)
    {
        var date = Date(culture, entry.Date);
        var description = Description(entry.Description);
        var change = Change(culture, entry.Change);

        return $"{date} | {string.Format("{0,-25}", description)} | {string.Format("{0,13}", change)}";
    }

    private static IEnumerable<LedgerEntry> Sort(LedgerEntry[] entries)
        => entries.OrderBy(e => e.Change);

    public static string Format(string currency, string locale, LedgerEntry[] entries)
    {
        var formatted = PrintHead(locale);
        var culture = CreateCulture(currency, locale);
        var entriesForOutput = Sort(entries);

        foreach (var entry in entriesForOutput)
        {
            formatted += "\n" + PrintEntry(culture, entry);
        }

        return formatted;
    }
}

Community comments

Find this solution interesting? Ask the author a question to learn more.

What can you learn from this solution?

A huge amount can be learned from reading other people’s code. This is why we wanted to give exercism users the option of making their solutions public.

Here are some questions to help you reflect on this solution and learn the most from it.

  • What compromises have been made?
  • Are there new concepts here that you could read more about to improve your understanding?