App-Idea 2: Roman to Decimal

Another number conversion project (Roman-To-Decimal) takes numbers represented as roman numerals from input strings and converts them to their decimal value. Again, we will add the ability to convert back from decimals to roman numeral notation as well. This conversion project will use the same layout design as the Bin2Dec project.

We will add this page to the existing Blazor.AppIdeas.Converters app (rather than creating a separate full project). You can find the source for the Blazor.AppIdeas.Converters on GitHub. And the running sample of the Blazor app online.

We will start by loading Blazor.AppIdeas.Converters solution (created in App-Idea 1: Bin2Dec) into Visual Studio.

Although this project has relatively easy logic, it will allow us to show the full MVVM design: RomanNumeral represents our model, RomanDecimalConverter is the view model, and RomanDecimalConvert Page is our view.

RomanNumeral Class

Since this is our first model class in the project, we will create a Models folder in the Blazor.AppIdeas.Converters project. Then, let’s create the RomanNumeral class. This class is a logical encapsulation of a roman numeral with a couple of operations. If we wanted to support additional operations (like math functions to add, subtract, etc) we would add them to this class.

using System;
using System.Collections.Generic;
    
namespace Blazor.AppIdeas.Converters.Models
{
    public class RomanNumeral
    {
        private static readonly IDictionary<string, int> _romanToDecimals = new Dictionary<string, int>
        {
            { "M", 1000 },
            { "D", 500 },
            { "C", 100 },
            { "L", 50 },
            { "X", 10 },
            { "V", 5 },
            { "I", 1 }
        };

        private static readonly int[] _decimalDivisors =
            new int[] { 1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1 };

        private static readonly string[] _romanEquivalents =
            new string[] { "M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I" };

        public RomanNumeral(string number)
        {
            Value = number ?? throw new ArgumentNullException(nameof(number));
        }

        public string Value { get; private set; }

        public int ToInt()
        {
            int total = 0;
            int previousValue = 0;

            for (int ctr = Value.Length - 1; ctr >= 0; ctr--)
            {
                int currentValue = _romanToDecimals[Value[ctr].ToString().ToUpper()];
                if (currentValue < previousValue)
                {
                    total -= currentValue;
                }
                else
                {
                    total += currentValue;
                }

                previousValue = currentValue;
            }

            return total;
        }

        public static RomanNumeral FromDecimal(int number)
        {
            if (number < 0) throw new FormatException();
            string result = string.Empty;
            int currentPointer = 0;

            while(number > 0)
            {
                var count = number / _decimalDivisors[currentPointer];

                for (int i = 0; i < count; i++)
                {
                    result += _romanEquivalents[currentPointer];
                }

                number -= count * _decimalDivisors[currentPointer];
                currentPointer++;
            }

            return new RomanNumeral(result);
        }
    }
}

This class surfaces a Value property (line #30) to get the text version of the RomanNumeral and two operations (one to convert the current object and its data into an integer, and another that takes an integer, converts it to a roman numeral, and returns a new instance of RomanNumeral.

The ToInt method (lines #32-53) loops in reverse order through all of the characters in the Value property. It converts that character into its decimal value (using the _romanToDecimals lookup table).

  • If that number is less than the previous one we checked, then we subtract the amount from the total… this represents number like 4 which are IV in roman numerals (this would produce a 5 in the first iteration, and subtract 1 from it in the second).
  • If the number is greater than or equal the previous one, then we add it to the total. This is the straight path through roman numerals like XVIII (for 18).
  • Then, we update the previous character value to the current before moving to the next character.
  • Finally, we return the integer total which represents the roman numeral text.

This is a relatively simplistic algorithm, but works for most cases of roman numeral conversion.

The FromDecimal method (lines #55-75) goes through multiple division operations to try to convert the integer into its roman character equivalent, using _decimalDivisors to loop through and map to the corresponding _romanEquivalents.

  • We start with the currentPointer which tracks our position in the _decimalDivisors. We want to try each divisor to see if the current value is divisible by it.
  • We loop through while we still have a value in number.
  • We divide the value of number by each divisor to see the number of iterations we should do for 1000 (or larger numbers).
  • Then, we loop through the counter to add that many roman characters to the result string.
  • Next, we subtract that amount from the number to get the new value.
  • And, we update the currentPointer to the next divisor in the array.
  • When we are through the the loop, we create a new instance of RomanNumeral with the result string and return it from this method.

It is great to encapsulate all of that logic in this class because RomanNumeral is a self-contained concept that should be separate from the view model. Having this class will also simplify our view model implementation, because it can just handle the glue logic between the view and this class.

RomanDecimalConverter View Model

Just like our previous converter project, we will also create a view model to encapsulate the interaction logic and it keep it out of our view/page.

Let’s create the RomanDecimalConverter class in the Blazor.AppIdeas.Converters project and ViewModels folder.

using Blazor.AppIdeas.Converters.Models;
using System;

namespace Blazor.AppIdeas.Converters.ViewModels
{
    public class RomanDecimalConverter
    {
        public string RomanText { get; set; }

        public string Decimal { get; set; }

        public string ErrorMessage { get; private set; }

        public string ErrorDisplay => string.IsNullOrEmpty(ErrorMessage) ? "none" : "normal";

        public void ConvertDecimal()
        {
            try
            {
                ErrorMessage = null;
                if (RomanText is null) throw new FormatException();

                var roman = new RomanNumeral(RomanText);
                Decimal = roman.ToInt().ToString();
            }
            catch
            {
                ErrorMessage = "Roman numerals only support the following characters: I, V, X, L, C, D, M.";
            }
        }

        public void ConvertRoman()
        {
            try
            {
                ErrorMessage = null;
                if (string.IsNullOrEmpty(Decimal)) throw new FormatException();

                var number = Convert.ToInt32(Decimal);
                var roman = RomanNumeral.FromDecimal(number);
                RomanText = roman.Value;
            }
            catch
            {
                ErrorMessage = "Decimal must be a valid number with only digits 0-9.";
            }
        }
    }
}
  • First, we define properties for the user interface to bind with.
    • RomanText holds the roman numeral string representation (line #8).
    • Decimal holds the integer number representation (line #10) also as a string.
    • ErrorMessage is a display message to show whenever an error happens in the conversion (line #12).
    • ErrorDisplay is a derived property (line #14) that returns “none” when the the ErrorMessage is empty, and returns “normal” when the ErrorMessage is present. This allows us to toggle the user interface based on the presence of an error message.
  • The ConvertDecimal method (lines #16-30) takes the RomanText property, converts it to an integer representation, and saves the result as a string in the Decimal property.
  • The ConvertRoman method (lines #32-47) does the reverse. It takes the Decimal property, converts it into an integer, converts that number into a string in roman numeral form, and saves the result into the RomanText property.
  • Both methods have error handling logic to:
    • Start by hiding any previous error messages.
    • Verify that the input strings are not null.
    • And handling any exceptions thrown by the RomanNumeral class.

This logic bridges the functionality between our view and model. We have UI specific code and error handling logic here, so that we don’t pollute our model classes with it.

RomanDecimalConvert Page

Now, we create the view as our RomanDecimalConvert page. Create the new Blazor component in the Blazor.AppIdeas.Converters project and Pages folder.

@page "/rom2dec"
@inject RomanDecimalConverter vm

<div class="col-lg-8 col-md-10 offset-lg-2 offset-md-1 mt-3 pb-3 container">
    <h3 class="mt-3">Roman-Decimal Converter</h3>
    <hr />
    <form class="form-row">
        <div class="form-group col-lg-6 col-md-12">
            <label for="roman">Roman Numeral:</label>
            <input type="text" class="form-control" id="roman" 
                   placeholder="Enter roman numeral" @bind-value="@vm.RomanText">
        </div>
        <div class="form-group col-lg-6 col-md-12">
            <label for="decimal">Decimal:</label>
            <input type="text" class="form-control" id="decimal" 
                   placeholder="Enter decimal number" @bind-value="@vm.Decimal">
        </div>
        <div class="alert alert-danger col-12 ml-1" style="display: @vm.ErrorDisplay">
            <strong>Error:</strong> @vm.ErrorMessage
        </div>
    </form>
    <div class="text-center">
        <input id="btn-convert-decimal" class="btn btn-outline-primary" type="button"
               value="Convert to Decimal" @onclick="vm.ConvertDecimal">
        <input id="btn-convert-roman" class="btn btn-outline-primary" type="button"
               value="Convert to Roman Numeral" @onclick="vm.ConvertRoman">
    </div>
</div>

@code {
}

This follows the exact same layout as the page code in App-Idea 1: Bin2Dec (read that layout section of that article if you need a refresher).

The interesting new pieces to this file are that we:

  • Define a new route for this page @page "/rom2dec" because we need unique routes for each page (line #1).
  • For this project, we use the retrieve our view model from the ASP.NET Core dependency injection system… rather than statically creating it.
  • The @inject directive retrieves the type RomanDecimalConverter and saves it into a local variable named vm. This local variable is then accessible throughout the page.

We will cover how the view model class gets registered in the dependency injection container later in this article.

Other Changes

1. Add a link to the NavMenu (lines #21-25) for the new page: rom2dec.

@attribute [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
<div class="top-row pl-4 navbar navbar-dark">
    <a class="navbar-brand" href="">AppIdeas - Converters</a>
    <button class="navbar-toggler" @onclick="ToggleNavMenu">
        <span class="navbar-toggler-icon"></span>
    </button>
</div>

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="bin2dec">
                <span class="oi oi-arrow-bottom" aria-hidden="true"></span> Bin2Dec
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="rom2dec">
                <span class="oi oi-move" aria-hidden="true"></span> Roman2Dec
            </NavLink>
        </li>
    </ul>
</div>

@code {
    private bool collapseNavMenu = true;

    private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;

    private void ToggleNavMenu()
    {
        collapseNavMenu = !collapseNavMenu;
    }
}

2. Register the RomanDecimalConverter class with the ASP.NET dependency injection container in Program.cs.

using Blazor.AppIdeas.Converters.ViewModels;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

namespace Blazor.AppIdeas.Converters
{
    [ExcludeFromCodeCoverage]
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("#app");

            builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

            // add services and view models to DI container.
            builder.Services.AddTransient<RomanDecimalConverter>();

            await builder.Build().RunAsync();
        }
    }
}

Dependency injection (DI) is an inversion of control design pattern that delegates the creation of objects to another component. There are many dependency injection frameworks available, including one in ASP.NET. The code in line #23 already uses the DI container in builder.Services – this is a service provider implementation (IServiceCollection) – to register the HttpClient to use for this app. So, we can also register our services and view models with the same container. Then, when our pages/components use the @inject directive, the container retrieves an instance of the registered type.

In line #26, we register our view model with the container using the IServiceCollection.AddTransient method with the type of RomanDecimalConverter. Transient means that a new instance of RomanDecimalConverter is created every time it is requested. We can also have a single instance of a registered type using the IServiceCollection.AddSingleton method instead. The Singleton creates that object when it is first called, and then returns that same instance any other time it is requested.

Testing Roman-to-Decimal Conversion

The tests for RomanDecimalConverter and RomanDecimalConvert page are all included in this solution. We won’t go through all of the test code here, because it is similar to our other converter tests. However, we will review one test to see how to setup the view model with dependency injection in the bUnit TestContext..

In the RomanDecimalConvertTests.cs file, let’s review the ConvertToDecimal_Clicked test:

        [Fact]
        public void ConvertToDecimal_Clicked()
        {
            // arrange
            using var ctx = new TestContext();
            var vm = new RomanDecimalConverter { RomanText = "CI" };
            ctx.Services.AddSingleton<RomanDecimalConverter>(vm);

            var cut = ctx.RenderComponent<RomanDecimalConvert>();

            // act
            cut.Find("#btn-convert-decimal").Click();

            // assert
            cut.MarkupMatches(RomanDecimalConvertExpectedResults.ConvertToDecimalResult);
        }
  • In line #6, we create an instance of the RomanDecimalConverter object, with its RomanText property set to a value.
  • The TestContext also has an IServiceCollection as its Services property.
  • In line #7, we call the IServiceCollection.AddSingleton method to register the RomanDecimalConverter type and pass the instance we created as the method parameter.
  • When our component/page tries to inject the RomanDecimalConverter type, it uses the instance we created in our test setup.

With all of the code and test changes complete, we can build and run all of the tests to verify our functionality. When we run the app and navigate to the Roman2Dec page, we can see the new converter and test it out.

Fig 1 – Roman to Decimal Converter

In this article, we learned how to:

  • Use raw HTML/CSS on a Blazor page to produce its layout.
  • Create our layered design to support the MVVM design pattern.
  • Register RomanDecimalConverter with the ASP.NET dependency injection container.
  • Use the @inject directive to find types in the dependency injection container.
  • Setup bUnit tests to use RomanDecimalConverter as a service to inject into components/pages during test execution.

One thought on “App-Idea 2: Roman to Decimal

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s