Avatar of martinvds81

martinvds81's solution

to Ledger in the C# Track

Published at May 08 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.Globalization;
using System.Linq;
using System.Text;

public static class Ledger
{
    private static readonly LedgerService _ledgerService = new LedgerService();
    private static readonly LedgerPrinter _ledgerPrinter = new LedgerPrinter();

    public static LedgerEntry CreateEntry(string date, string description, int change)
    {
        return new LedgerEntry(DateTime.Parse(date, CultureInfo.InvariantCulture), description, change / 100.0m);
    }

    public static string Format(string currency, string locale, LedgerEntry[] entries)
    {
        var formatted = new StringBuilder();

        var ledgerCulture = _ledgerService.GetCulture(locale, currency);

        var translations = _ledgerService.GetTranslations(locale);

        formatted.Append(_ledgerPrinter.PrintHeader(translations));

        if (entries.Any())
        {
            var entriesForOutput = entries.GetOrderedEntries();

            foreach (var entry in entriesForOutput)
            {
                formatted.AppendNewLine(_ledgerPrinter.PrintEntry(ledgerCulture, LedgerConstants.TruncateLength, LedgerConstants.TruncateSuffix, entry));
            }
        }

        return formatted.ToString();
    }
}

public static class LedgerConstants
{
    public const int TruncateLength = 25;
    public const string TruncateSuffix = "...";
}

public static class LedgerExtensions
{
    public static string Truncate(this string input, int maxLength, string suffix)
    {
        return input.Length > maxLength ? $"{input.Substring(0, maxLength - suffix.Length)}{suffix}" : input;
    }

    public static IEnumerable<LedgerEntry> GetOrderedEntries(this LedgerEntry[] entries) => entries
        .OrderBy(x => x.Date)
        .ThenBy(x => x.Description)
        .ThenBy(x => x.Change);

    public static string FormattedChange(this decimal change, IFormatProvider culture) => $"{change.ToString("C", culture)}{(change < 0.0M ? "" : " ")}";

    public static StringBuilder AppendNewLine(this StringBuilder stringBuilder, string value) => stringBuilder.Append($"\n{value}");

    public static string ToLedgerDate(this DateTime date, IFormatProvider culture) => date.ToString("d", culture);
}

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

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

public class LedgerCulture
{
    public string Name { get; set; }
    public int CurrencyNegativePattern { get; set; }
    public string ShortDatePattern { get; set; }
    public LedgerTranslation Translations { get; set; }

    public LedgerCulture(string name, int currencyNegativePattern, string shortDatePattern)
    {
        Name = name;
        CurrencyNegativePattern = currencyNegativePattern;
        ShortDatePattern = shortDatePattern;
    }
}

public class LedgerTranslation
{
    public string Date { get; set; }
    public string Description { get; set; }
    public string Change { get; set; }
}

/// <summary>
/// Responsible for all culture and currency related functionality
/// </summary>
public class LedgerService
{
    /// <summary>
    /// Valid Currencies 
    /// </summary>
    private static readonly Dictionary<string, string> _validCurrencies = new Dictionary<string, string>()
    {
        ["EUR"] = "€",
        ["USD"] = "$"
    };

    /// <summary>
    /// Valid Cultures
    /// </summary>
    private static readonly List<LedgerCulture> _validCulures = new List<LedgerCulture>()
    {
        new LedgerCulture("en-US", 0, "MM/dd/yyyy")
        {
            Translations = new LedgerTranslation()
            {
                Date = "Date",
                Description = "Description",
                Change = "Change"
            }
        },
        new LedgerCulture("nl-NL", 12, "dd/MM/yyyy")
        {
            Translations = new LedgerTranslation()
            {
                Date = "Datum",
                Description = "Omschrijving",
                Change = "Verandering"
            }
        }
    };

    public LedgerTranslation GetTranslations(string name)
    {
        var ledgerCulture = _validCulures.FirstOrDefault(n => n.Name == name);
        if (ledgerCulture == null)
        {
            throw new ArgumentException("Invalid culture");
        }

        return ledgerCulture.Translations;
    }

    public CultureInfo GetCulture(string name, string currency)
    {
        var ledgerCulture = _validCulures.FirstOrDefault(n => n.Name == name);
        if (ledgerCulture == null)
        {
            throw new ArgumentException("Invalid culture");
        }

        if (!_validCurrencies.ContainsKey(currency))
        {
            throw new ArgumentException("Invalid currency");
        }

        return new CultureInfo(ledgerCulture.Name)
        {
            NumberFormat =
            {
                CurrencySymbol = _validCurrencies[currency],
                CurrencyNegativePattern = ledgerCulture.CurrencyNegativePattern
            },
            DateTimeFormat =
            {
                ShortDatePattern = ledgerCulture.ShortDatePattern
            }
        };
    }
}

/// <summary>
/// Responsible for printing
/// </summary>
public class LedgerPrinter
{
    public string PrintHeader(LedgerTranslation translation)
    {
        var cellDate = translation.Date;
        var cellDescription = translation.Description;
        var cellChange = translation.Change;

        return $"{cellDate.PadRight(11)}| {cellDescription.PadRight(26)}| {cellChange.PadRight(13)}";
    }

    public string PrintEntry(IFormatProvider culture, int truncateLenth, string truncateSuffix, LedgerEntry entry)
    {
        var date = entry.Date.ToLedgerDate(culture);
        var description = entry.Description.Truncate(truncateLenth, truncateSuffix);
        var change = entry.Change.FormattedChange(culture);

        return $"{date.PadRight(11)}| {description.PadRight(26)}| {change.PadLeft(13)}";
    }
}

Community comments

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

martinvds81's Reflection

Wanted to go full blown SOLID but mainly focused on single responsibilty and applying Microsoft coding conventions.