5
\$\begingroup\$

I am new to programming. I picked up a C# course from freeCodeCamp.org a few days ago and today I created my first code from scratch. I know probably it's not the best optimized but it works. Some things still don't make sense to me completely but I hope in the future it will all click.

However, I am making this post to try to reach to more experienced C# developers to see what I could've done better.

Thank you for your time!

namespace UnitConverter
{
    internal class Program
    {
        static void Main(string[] args)
        {
            string firstMeasure;
            string secondMeasure;
            double MeasureValue;
            bool tryAgain = true;
            string playAgain;
            bool valueIsGood = false;
            bool valueIsGood2 = false;
            bool valueIsGood3 = false;
            string restart;

            while (tryAgain)
            {
                Console.WriteLine("--------------");
                Console.WriteLine("UNIT CONVERTER");
                Console.WriteLine("--------------");
                Console.WriteLine("");
                Console.WriteLine("Choose your first measurement");
                Console.WriteLine("km - kilometers");
                Console.WriteLine("m - meters");
                Console.WriteLine("cm - centimeters");
                Console.Write("Choose: ");
                do
                {
                    firstMeasure = Console.ReadLine();
                    firstMeasure = firstMeasure.ToUpper();
                    if (firstMeasure == "km".ToUpper() || firstMeasure == "cm".ToUpper() || firstMeasure == "m".ToUpper())
                    {
                        Console.Write("Value in " + firstMeasure + ": ");
                        valueIsGood = true;
                    }
                    else
                    {
                        Console.WriteLine("Incorrect measurement");
                        Console.Write("Choose: ");
                    }
                } while (!valueIsGood);


                while (!double.TryParse(Console.ReadLine(), out MeasureValue))
                {
                    Console.WriteLine("This is not a number!");
                    Console.Write("Choose: ");
                }

                Console.WriteLine("Choose your second measurement");
                Console.WriteLine("km - kilometers");
                Console.WriteLine("m - meters");
                Console.WriteLine("cm - centimeters");
                Console.Write("Choose: ");

                do
                {
                    secondMeasure = Console.ReadLine();
                    secondMeasure = secondMeasure.ToUpper();
                    if (secondMeasure == "km".ToUpper() || secondMeasure == "cm".ToUpper() || secondMeasure == "m".ToUpper())
                    {
                        valueIsGood2 = true;
                    }
                    else
                    {
                        Console.WriteLine("Incorrect measurement");
                        Console.Write("Choose: ");
                    }
                } while (!valueIsGood2);

                    if (secondMeasure == "km".ToUpper() || secondMeasure == "cm".ToUpper() || secondMeasure == "m".ToUpper())
                    {
                        if (firstMeasure == "km".ToUpper() && secondMeasure == "km".ToUpper())
                        {
                            double result = MeasureValue * 1;
                            Console.WriteLine("RESULT: " + MeasureValue + firstMeasure + " is " + result + secondMeasure);
                        }
                        else if (firstMeasure == "km".ToUpper() && secondMeasure == "m".ToUpper())
                        {
                            double result = MeasureValue * 1000;
                            Console.WriteLine("RESULT: " + MeasureValue + firstMeasure + " is " + result + secondMeasure);
                        }
                        else if (firstMeasure == "km".ToUpper() && secondMeasure == "cm".ToUpper())
                        {
                            double result = MeasureValue * 100000;
                            Console.WriteLine("RESULT: " + MeasureValue + firstMeasure + " is " + result + secondMeasure);
                        }
                        else if (firstMeasure == "m".ToUpper() && secondMeasure == "km".ToUpper())
                        {
                            double result = MeasureValue / 1000;
                            Console.WriteLine("RESULT: " + MeasureValue + firstMeasure + " is " + result + secondMeasure);
                        }
                        else if (firstMeasure == "m".ToUpper() && secondMeasure == "m".ToUpper())
                        {
                            double result = MeasureValue * 1;
                            Console.WriteLine("RESULT: " + MeasureValue + firstMeasure + " is " + result + secondMeasure);
                        }
                        else if (firstMeasure == "m".ToUpper() && secondMeasure == "cm".ToUpper())
                        {
                            double result = MeasureValue * 100;
                            Console.WriteLine("RESULT: " + MeasureValue + firstMeasure + " is " + result + secondMeasure);
                        }
                        else if (firstMeasure == "cm".ToUpper() && secondMeasure == "km".ToUpper())
                        {
                            double result = MeasureValue / 100000;
                            Console.WriteLine("RESULT: " + MeasureValue + firstMeasure + " is " + result + secondMeasure);
                        }
                        else if (firstMeasure == "cm".ToUpper() && secondMeasure == "m".ToUpper())
                        {
                            double result = MeasureValue / 100;
                            Console.WriteLine("RESULT: " + MeasureValue + firstMeasure + " is " + result + secondMeasure);
                        }
                        else if (firstMeasure == "cm".ToUpper() && secondMeasure == "cm".ToUpper())
                        {
                            double result = MeasureValue * 1;
                            Console.WriteLine("RESULT: " + MeasureValue + firstMeasure + " is " + result + secondMeasure);
                        }
                    }

                do
                {
                    Console.WriteLine("Try again? - Y/N");
                    restart = Console.ReadLine();
                    restart = restart.ToUpper();

                    if (restart == "Y")
                    {
                        valueIsGood3 = true;
                    }
                    else if (restart == "N")
                    {
                        Console.WriteLine("Thank you for using the calculator!");
                        Console.WriteLine("Exiting in...");

                        for (int i = 5; i >= 1; i--)
                        {
                            Console.WriteLine(i);
                            int milliseconds = 1000;
                            Thread.Sleep(milliseconds);
                        }
                        valueIsGood3 = true;
                        tryAgain = false;
                    } else
                    {
                        Console.WriteLine("Incorrect answer.");
                        Console.WriteLine("Options: Y or N");
                        valueIsGood3 = false;
                        tryAgain = true;
                    }
                } while (!valueIsGood3); 


            }
        }
    }
}
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Thank you both for going in-depth in explanations. I really understood how to make it better and my code will definitely improve in the future projects. I really appreciate it. I understood why methods are so important and how to fix that little .ToUpper(); thing I did there, seems funny to look at mine now. \$\endgroup\$
    – South
    Jun 5 at 14:13

5 Answers 5

5
\$\begingroup\$

First of all congratulation, you are on a good way to become a software developer.
The solution itself is far from perfect but it is a good start.

In this post I would like to focus on two things: methods and variable scopes.

  • Methods can help you to better organize your code and reuse the same logic in multiple places
  • Variable scope can help you to minimize the chance to access an unrelated variable or forgot to set a related one (for example for the next iteration)

Extracting the measure unit asking logic

static string AskForMeasureUnit()
{
    Console.WriteLine("Choose your first measurement");
    Console.WriteLine("km - kilometers");
    Console.WriteLine("m - meters");
    Console.WriteLine("cm - centimeters");
    Console.Write("Choose: ");

    do
    {
        string measureUnit = Console.ReadLine().ToLower();
        if (measureUnit == "km" || measureUnit == "cm" || measureUnit == "m")
        {
            return measureUnit;
        }

        Console.WriteLine("Incorrect measurement");
        Console.Write("Choose: ");
                
    } while (true);
}
  • In order to get the firstMeasure and secondMeasure you have repeated the exact same code
  • I've moved that logic into an AskForMeasureUnit method to be able to reuse it
  • I've changed your .ToUpper to .ToLower since your string constants ("cm", "m", "km") are already in lower case
  • I've also changed your do-while loop to run indefinitely (while(true)) until an acceptable user input is provided
    • At that point I've used the return measureUnit; to break out from the loop

With the above method in our hand we can rewrite the first half of the while(tryAgain)'s body like this:

Console.WriteLine("--------------");
Console.WriteLine("UNIT CONVERTER");
Console.WriteLine("--------------");
Console.WriteLine("");

string firstMeasure = AskForMeasureUnit();
Console.Write("Value in " + firstMeasure + ": ");

double measuredValue;
while (!double.TryParse(Console.ReadLine(), out measuredValue))
{
    Console.WriteLine("This is not a number!");
    Console.Write("Choose: ");
}

string secondMeasure = AskForMeasureUnit();
  • With this change you don't need to declare the following variables on the top of your Main: firstMeasure, secondMeasure, MeasureValue, valueIsGood and valueIsGood2

Extracting the conversion logic

static double PerformConversion(string firstMeasure, string secondMeasure, double measureValue)
{
    if (firstMeasure == "m" && secondMeasure == "cm")
    {
        return measureValue * 100;
    }
    if (firstMeasure == "cm" && secondMeasure == "m")
    {
        return measureValue / 100;
    }

    if (firstMeasure == "km" && secondMeasure == "m")
    {
        return measureValue * 1_000;
    }
    if (firstMeasure == "m" && secondMeasure == "km")
    {
        return measureValue / 1_000;
    }
            
    if (firstMeasure == "km" && secondMeasure == "cm")
    {
        return measureValue * 100_000;
    }
    if (firstMeasure == "cm" && secondMeasure == "km")
    {
        return measureValue / 100_000;
    }

    return measureValue; //firstMeasure == secondMeasure
}
  • Since your Console.ReadLine statement is the same in each and every case you don't need to repeat it
    • The branches should focus on the differences only
  • I've used digit separator (_) to improve legibility of the numbers
  • I've used multiple return statements to avoid the usage of else if
  • As you can see this method does not have any local variable

With the above method in our hand we can continue the main logic after the string secondMeasure = AskForMeasureUnit(); line like this

var result = PerformConversion(firstMeasure, secondMeasure, measuredValue);
Console.WriteLine("RESULT: " + measuredValue + firstMeasure + " is " + result + secondMeasure);

Extracting the should retry asking logic

static bool AskForRetry()
{
    do
    {
        Console.WriteLine("Try again? - Y/N");
        var restart = Console.ReadLine().ToUpper();
        if(restart != "Y" && restart != "N")
        {
            Console.WriteLine("Incorrect answer.");
            Console.WriteLine("Options: Y or N");
            continue;
        }

        return restart == "Y";
                
    } while (true);
}
  • I've simplified a bit your code by having only a single branch inside the infinite loop
    • The loop runs until it receives a valid input

The countdown logic could remain inside the main code

    tryAgain = AskForRetry();
}

Console.WriteLine("Thank you for using the calculator!");
Console.WriteLine("Exiting in...");

for (int i = 5; i >= 1; i--)
{
    Console.WriteLine(i);
    int milliseconds = 1000;
    Thread.Sleep(milliseconds);
}

For the sake of completeness here is the entire code:

static void Main(string[] args)
{
    bool tryAgain = true;
    while (tryAgain)
    {
        Console.WriteLine("--------------");
        Console.WriteLine("UNIT CONVERTER");
        Console.WriteLine("--------------");
        Console.WriteLine("");

        string firstMeasure = AskForMeasureUnit();
        Console.Write("Value in " + firstMeasure + ": ");

        double measuredValue;
        while (!double.TryParse(Console.ReadLine(), out measuredValue))
        {
            Console.WriteLine("This is not a number!");
            Console.Write("Choose: ");
        }

        string secondMeasure = AskForMeasureUnit();

        var result = PerformConversion(firstMeasure, secondMeasure, measuredValue);
        Console.WriteLine("RESULT: " + measuredValue + firstMeasure + " is " + result + secondMeasure);

        tryAgain = AskForRetry();
    }

    Console.WriteLine("Thank you for using the calculator!");
    Console.WriteLine("Exiting in...");

    for (int i = 5; i >= 1; i--)
    {
        Console.WriteLine(i);
        int milliseconds = 1000;
        Thread.Sleep(milliseconds);
    }
}

static string AskForMeasureUnit()
{
    Console.WriteLine("Choose your first measurement");
    Console.WriteLine("km - kilometers");
    Console.WriteLine("m - meters");
    Console.WriteLine("cm - centimeters");
    Console.Write("Choose: ");

    do
    {
        string measureUnit = Console.ReadLine().ToLower();
        if (measureUnit == "km" || measureUnit == "cm" || measureUnit == "m")
        {
            return measureUnit;
        }

        Console.WriteLine("Incorrect measurement");
        Console.Write("Choose: ");

    } while (true);
}

static double PerformConversion(string firstMeasure, string secondMeasure, double measureValue)
{
    if (firstMeasure == "m" && secondMeasure == "cm")
    {
        return measureValue * 100;
    }
    if (firstMeasure == "cm" && secondMeasure == "m")
    {
        return measureValue / 100;
    }

    if (firstMeasure == "km" && secondMeasure == "m")
    {
        return measureValue * 1_000;
    }
    if (firstMeasure == "m" && secondMeasure == "km")
    {
        return measureValue / 1_000;
    }

    if (firstMeasure == "km" && secondMeasure == "cm")
    {
        return measureValue * 100_000;
    }
    if (firstMeasure == "cm" && secondMeasure == "km")
    {
        return measureValue / 100_000;
    }

    return measureValue; //firstMeasure == secondMeasure
}

static bool AskForRetry()
{
    do
    {
        Console.WriteLine("Try again? - Y/N");
        string restart = Console.ReadLine().ToUpper();
        if (restart != "Y" && restart != "N")
        {
            Console.WriteLine("Incorrect answer.");
            Console.WriteLine("Options: Y or N");
            continue;
        }

        return restart == "Y";
    } while (true);
}
\$\endgroup\$
4
\$\begingroup\$

Congratulations on creating your first program from scratch. Since you're new to programming, I'll try and give you a few tips on how to make your program a little more maintainable.

Repetition

In a few places in your code where you're repeating the same code again, which can be abstracted out into its own method and called from. An example of this in your code is:

Console.WriteLine("Choose your first measurement");
Console.WriteLine("km - kilometers");
Console.WriteLine("m - meters");
Console.WriteLine("cm - centimeters");
Console.Write("Choose: ");

and

Console.WriteLine("Choose your second measurement");
Console.WriteLine("km - kilometers");
Console.WriteLine("m - meters");
Console.WriteLine("cm - centimeters");
Console.Write("Choose: ");

The other difference between the two bits of code is in the first line of each. One way of abstracting this out can look like this:

static void WriteMeasurementSelection(bool isFirstMeasurement)
{
    string measurementNumber = "first";

    if (isFirstMeasurement == false) // Can be written as !isFirstMeasurement
    {
        measurementNumber = "second";
    }

    Console.WriteLine($"Choose your {measurementNumber} measurement");
    Console.WriteLine("km - kilometers");
    Console.WriteLine("m - meters");
    Console.WriteLine("cm - centimeters");
    Console.Write("Choose: ");
}

The same type of refactoring can be done when you're getting the user to enter in a valid measurement choice:

static string ReadUserMeasurementChoice()
{
    bool valueIsGood = false;
    string measurement;
    do
    {
        measurement = Console.ReadLine();
        measurement = measurement.ToUpper();
        if (measurement == "km".ToUpper() || measurement == "cm".ToUpper() || measurement == "m".ToUpper())
        {
            Console.Write($"Value in " + measurement + ": ");
            valueIsGood = true;
        }
        else
        {
            Console.WriteLine("Incorrect measurement");
            Console.Write("Choose: ");
        }
    } while (!valueIsGood);

    return measurement;
}

We can make further improvements to the above method by:

  1. Prefer string interpolation over string concatenation (i.e. prefer $"Value in {measurement}: " over "Value in " + measurement + ": ") [I've already applied string interpolation in WriteMeasurementSelection]
  2. Prefer doing string comparisons with string.Equals with the ignore case arguments over calling ToUpper. This may not always be possible (which we'll see shortly when refactoring the calculations to switch expressions)

Which looks something like this:

static string ReadUserUnitChoice()
{
    bool valueIsGood = false;
    string measurement;
    do
    {
        measurement = Console.ReadLine().ToUpper();
        
        if (measurement.Equals("km", StringComparison.OrdinalIgnoreCase) || 
            measurement.Equals("cm", StringComparison.OrdinalIgnoreCase) || 
            measurement.Equals("m", StringComparison.OrdinalIgnoreCase))
        {
            Console.Write($"Value in {measurement}: ");
            valueIsGood = true;
        }
        else
        {
            Console.WriteLine("Incorrect measurement");
            Console.Write("Choose: ");
        }
    } while (!valueIsGood);

    return measurement;
}

When displaying the results, we can refactor it to a method like:

static void DisplayResult(
    string firstMeasure,
    string secondMeasure,
    double measureValue,
    double result)
{
    Console.WriteLine($"RESULT: {measureValue}{firstMeasure} is {result}{secondMeasure}");
}

As for the calculations itself, we can certainly refactor these calculations into its own methods, like so (note: the use of KILOMETRES, METRES, and CENTIMETRES will be explained in the next section):

static double CalculateFromKilometres(string secondMeasurement, double measureValue)
{
    return secondMeasurement switch
    {
        KILOMETRES => measureValue,
        METRES => measureValue * 1000,
        CENTIMETRES => measureValue * 100000,
        // We don't expect users to have an incorrect measurement unit, but just in case throw an exception
        var _ => throw new ArgumentException("Incorrect measurement"),
    };
}

static double CalculateFromMetres(string secondMeasurement, double measureValue)
{
    return secondMeasurement switch
    {
        KILOMETRES => measureValue / 1000,
        METRES => measureValue,
        CENTIMETRES => measureValue * 100,
        // We don't expect users to have an incorrect measurement unit, but just in case throw an exception
        var _ => throw new ArgumentException("Incorrect measurement"),
    };
}

static double CalculateFromCentimetres(string secondMeasurement, double measureValue)
{
    return secondMeasurement switch
    {
        KILOMETRES => measureValue / 100000,
        METRES => measureValue / 100,
        CENTIMETRES => measureValue,
        // We don't expect users to have an incorrect measurement unit, but just in case throw an exception
        var _ => throw new ArgumentException("Incorrect measurement"),
    };
}

Magic Strings

Throughout the code, there is repetition of the following string literals: "km", "m" and "cm". This can be replaced by constants at the top of your program, like:

const string KILOMETRE = "km";
const string METRE = "m";
const string CENTIMETRE = "cm";

Everywhere there is any of the above string literals can be replaced by the constants above. The advantage of having constants is that there is now one place to change the value of your constants, and it self documents the intent a little clearer in terms of naming.

Redundancies

The following if statement is redundant because we're already ensuring that secondMeasurement is the correct measurement unit:

if (secondMeasure == "km".ToUpper() || secondMeasure == "cm".ToUpper() || secondMeasure == "m".ToUpper())

Variables

Overall, the style of the variable casing is consistent except for MeasureValue. To keep it inline with other variables, a better name should be measureValue.

Also in the next section below, you'll see that we're combining the declaration of the variables with the initialization. This is preferred as we want variables to be declared as close as possible to where they're being used.

Putting it all together

This is what the code can look like, mainly through the use of refactoring our code into smaller size chunks through methods:

namespace UnitConverter
{
    internal class Program
    {
        const string KILOMETRES = "km";
        const string METRES = "m";
        const string CENTIMETRES = "cm";

        const string YES = "Y";
        const string NO = "N";

        static void Main(string[] args)
        {
            do
            {
                Console.WriteLine("--------------");
                Console.WriteLine("UNIT CONVERTER");
                Console.WriteLine("--------------");
                Console.WriteLine("");

                WriteMeasurementSelection(true);
                string firstMeasure = ReadUserUnitChoice();
                double measureValue = ReadUserMeasurementValue();

                WriteMeasurementSelection(false);
                string secondMeasure = ReadUserUnitChoice();

                double result = CalculateResult(firstMeasure, secondMeasure, measureValue);
                DisplayResult(firstMeasure, secondMeasure, measureValue, result);
            } while (DoYouWishToContinue());
        }

        static void WriteMeasurementSelection(bool isFirstMeasurement)
        {
            string measurementNumber = "first";

            if (isFirstMeasurement == false) // Can be written as !isFirstMeasurement
            {
                measurementNumber = "second";
            }

            Console.WriteLine($"Choose your {measurementNumber} measurement");
            Console.WriteLine($"{KILOMETRES} - kilometers");
            Console.WriteLine($"{METRES} - meters");
            Console.WriteLine($"{CENTIMETRES} - centimeters");
            Console.Write("Choose: ");
        }

        static string ReadUserUnitChoice()
        {
            bool valueIsGood = false;
            string measurement;
            do
            {
                measurement = Console.ReadLine().ToUpper();

                if (measurement.Equals(KILOMETRES, StringComparison.OrdinalIgnoreCase) ||
                    measurement.Equals(CENTIMETRES, StringComparison.OrdinalIgnoreCase) ||
                    measurement.Equals(METRES, StringComparison.OrdinalIgnoreCase))
                {
                    Console.Write($"Value in {measurement}: ");
                    valueIsGood = true;
                }
                else
                {
                    Console.WriteLine("Incorrect measurement");
                    Console.Write("Choose: ");
                }
            } while (!valueIsGood);

            return measurement;
        }

        private static double ReadUserMeasurementValue()
        {
            double measureValue;
            
            while (!double.TryParse(Console.ReadLine(), out measureValue))
            {
                Console.WriteLine("This is not a number!");
                Console.Write("Choose: ");
            }

            return measureValue;
        }

        static double CalculateResult(string firstMeasurement, string secondMeasurement, double measureValue)
        {
            return firstMeasurement.ToLower() switch
            {
                KILOMETRES => CalculateFromKilometres(secondMeasurement.ToLower(), measureValue),
                METRES => CalculateFromMetres(secondMeasurement.ToLower(), measureValue),
                CENTIMETRES => CalculateFromCentimetres(secondMeasurement.ToLower(), measureValue),
                // We don't expect users to have an incorrect measurement unit, but just in case throw an exception
                var _ => throw new ArgumentException("Incorrect measurement"),
            };
        }

        static double CalculateFromKilometres(string secondMeasurement, double measureValue)
        {
            return secondMeasurement switch
            {
                KILOMETRES => measureValue,
                METRES => measureValue * 1000,
                CENTIMETRES => measureValue * 100000,
                // We don't expect users to have an incorrect measurement unit, but just in case throw an exception
                var _ => throw new ArgumentException("Incorrect measurement"),
            };
        }

        static double CalculateFromMetres(string secondMeasurement, double measureValue)
        {
            return secondMeasurement switch
            {
                KILOMETRES => measureValue / 1000,
                METRES => measureValue,
                CENTIMETRES => measureValue * 100,
                // We don't expect users to have an incorrect measurement unit, but just in case throw an exception
                var _ => throw new ArgumentException("Incorrect measurement"),
            };
        }

        static double CalculateFromCentimetres(string secondMeasurement, double measureValue)
        {
            return secondMeasurement switch
            {
                KILOMETRES => measureValue / 100000,
                METRES => measureValue / 100,
                CENTIMETRES => measureValue,
                // We don't expect users to have an incorrect measurement unit, but just in case throw an exception
                var _ => throw new ArgumentException("Incorrect measurement"),
            };
        }

        static void DisplayResult(string firstMeasure, string secondMeasure, double measureValue, double result)
        {
            Console.WriteLine($"RESULT: {measureValue}{firstMeasure} is {result}{secondMeasure}");
        }

        private static bool DoYouWishToContinue()
        {
            bool tryAgain = true;
            bool valueIsGood = false;
            
            while (!valueIsGood)
            {
                Console.WriteLine("Try again? - Y/N");
                string restart = Console.ReadLine();

                bool isNo = restart.Equals(NO, StringComparison.OrdinalIgnoreCase);
                valueIsGood = isNo || restart.Equals(YES, StringComparison.OrdinalIgnoreCase);
                
                if (isNo)
                {
                    Console.WriteLine("Thank you for using the calculator!");
                    Console.WriteLine("Exiting in...");

                    for (int i = 5; i >= 1; i--)
                    {
                        Console.WriteLine(i);
                        int milliseconds = 1000;
                        Thread.Sleep(milliseconds);
                    }

                    tryAgain = false;
                }
                else if (!valueIsGood)
                {
                    Console.WriteLine("Incorrect answer.");
                    Console.WriteLine("Options: Y or N");
                }
            }

            return tryAgain;
        }
    }
}

What's next

Can this be made cleaner? Of course! We can make use of classes to define behaviour in a more extendable fashion, but that maybe overkill for a small program like this. Hope this helps.

\$\endgroup\$
2
  • \$\begingroup\$ It's "meter" not "metre". \$\endgroup\$
    – BCdotWEB
    Jun 5 at 14:29
  • 1
    \$\begingroup\$ @BCdotWEB "metre" is the British spelling \$\endgroup\$
    – Jacob
    Jun 5 at 20:24
4
\$\begingroup\$

Adding to the other answers, and adding some more complex ideas, I'd suggest that you use a type to contain configuration data for the various measurement units you can convert between.

Take this for example:

public class UnitOfLength
{
    // Full name of unit: "kilometer", "centimeter", etc.
    public string Name { get; }

    // Short name of unit: "km", "cm", etc.
    public string ShortName { get; }

    // Number of meters in this unit.
    public double ScaleToMeters { get; }
    
    public UnitOfLength(string name, string shortName, double scale)
    {
        Name = name;
        ShortName = shortName;
        ScaleToMeters = scale;
    }
    
    // Convert 'value' from this unit to the other unit.
    public double ConvertTo(UnitOfLength units, double value)
    {
        var meters = value * ScaleToMeters;
        return value / units.ScaleToMeters;
    }
}

In your program you create a collection of these units like so:

public static readonly UnitOfLength[] MeasurementUnits = new[]
{
    new UnitOfLength("kilometers", "km", 1000d),
    new UnitOfLength("meters", "m", 1d),
    new UnitOfLength("centimeters", "cm", 0.01d),
}

You can then use this list during unit selection and conversion, making the whole process more versatile and configurable. Adding this into the mix of the other suggestions, the next step is to create a unit selection method:

static string AskForMeasureUnit(bool first)
{
    Console.WriteLine($"Choose {(first ? "first" : "second")} measurement:");
    foreach (var unit in MeasurementUnits)
        Console.WriteLine($"\t{unit.ShortName,-2} - {unit.Name}");

    do
    {
        Console.Write("> ");
        string entered = Console.ReadLine().ToLower()

        UnitOfLength result = MeasurementUnits.FirstOrDefault(unit => unit.ShortName == entered);
        if (result is not null)
            return result;

        Console.WriteLine("Unknown measurement.");
    } while (true);
}

Once you have the two units of measure and the value to convert you just need to call the ConvertTo method:

UnitOfLength fromUnit = AskForMeasureUnit(true);
double fromValue = AskForValue(fromUnit.Name);
UnitOfLength toUnit = AskForMeasureUnit(false);

double toValue = fromUnit.ConvertTo(toUnit, fromValue);

Next Steps

And now that you have a configurable unit list, let's add to it:

public static readonly UnitOfLength[] MeasurementUnits = new[]
{
    new UnitOfLength("kilometers", "km", 1000d),
    new UnitOfLength("meters", "m", 1d),
    new UnitOfLength("centimeters", "cm", 0.01d),

    // 'mi' might be a stretch, but we need distinct values
    new UnitOfLength("miles", "mi", 1609.34d),
    new UnitOfLength("yards", "y", 0.9144d),
    new UnitOfLength("feet", "ft", 0.3048d),
}

With that one change to the program's data and zero code changes your program now supports a few US measurement units as well as the metric ones.

Another option is to add different types of units and convert between them. A general Unit base class with subclasses for measurements of different types: volume, temperature, weight, etc. It's more complex than you need right now, but thinking about these sorts of things can help you to plan in the future.

\$\endgroup\$
2
  • \$\begingroup\$ In ConvertTo, did you mean return meters / units.ScaleToMeters;? \$\endgroup\$ Jun 8 at 14:49
  • \$\begingroup\$ @SolomonUcko Yeah, typo. Thanks. \$\endgroup\$
    – Corey
    Jun 9 at 0:33
2
\$\begingroup\$

Modifying PerformConversion from Peter Csala's answer, instead of having a separate case for each pair of units, you can remove the redundancy by converting to meters in the middle: (not tested)

static double MetersInMeasure(string measure)
{
    if (firstMeasure == "cm")
    {
        return 0.01;
    }
    else if (firstMeasure == "m")
    {
        return 1.0;
    }
    else if (firstMeasure == "km")
    {
        return 1000.0;
    }
    else
    {
        throw new ArgumentException($"{measure} is not a valid unit of measure.");
    }
}

static double PerformConversion(string firstMeasure, string secondMeasure, double measureValue)
{
    if (firstMeasure.toUpper() == secondMeasure.toUpper())
    {
        // avoids losing precision from back-and-forth multiplication/division
         return measureValue;
    }
    
    double valueInMeters = measureValue * MetersInMeasure(firstMeasure);
    double valueInSecondMeasure = valueInMeters / MetersInMeasure(secondMeasure);
    return valueInSecondMeasure;
}
```
\$\endgroup\$
1
\$\begingroup\$

While there has been many great suggestion, I would suggest introducing a Length type. This naturally lends itself to managing unit conversions.

public readonly struct Length {
   public double Meters { get; }
   private Length(double meters) => Meters = meters;
}

This always uses the SI unit as the internal representation. This has some potential downsides, like accumulation of errors due to multiple conversions. But unless you are working with very small or large values these errors are likely insignificant. If this is a concern you could consider using decimal instead of double.

To do the actual conversions we can add conversion methods to and from some arbitrary units, using a table for the conversion factors:

public enum Unit
{
    MicroMeter,
    MilliMeter,
    Meter,
    KiloMeter,
    Inch,
}
private double GetConversionFactor(Unit unit)
{
    return unit switch
    {
        Unit.MilliMeter => 0.001,
        Unit.Meter => 1,
        Unit.KiloMeter => 1000,
        Unit.Inch => 0.0254,
        _ => throw new InvalidEnumArgumentException(nameof(unit), (int)unit, typeof(Unit))
    };
}

public static Length From(double v, Unit unit) => new(v * GetConversionFactor(unit));
public double To(Unit unit) => Meters / GetConversionFactor(unit);
public static double Convert(double v, Unit from, Unit to) => From(v, from).To(to);

Since all conversions are from/to meters we do not need conversion factors between every pair of types. Note that the constructor is private, so we always need to call From when creating our length, and are forced to specify the unit.

To handle text input we can add parsing and to string methods, using a table of symbols for each unit:

private static Dictionary<Unit, string[]> Symbols = new()
{
    { Unit.MicroMeter, new[] { "µm", "um" } }, // note that some units may have multiple symbols
    { Unit.MilliMeter, new[] { "mm" } },
    { Unit.KiloMeter, new[] { "km" } },
    { Unit.Inch, new[] { "in", "″" } },
};

public static bool TryParseUnit(string str, out Unit result)
{
    foreach (var (unit, symbols) in Symbols)
    {
        foreach (var symbol in symbols)
        {
            if (symbol.Equals(str, StringComparison.InvariantCultureIgnoreCase))
            {
                result = unit;
                return true;
            }
        }
    }
    value = default;
    return false;
}

public string ToString(Unit unit) => To(unit).ToString();
public string ToString(Unit unit, string format) => To(unit).ToString(format);

The major advantage of this approach is that when you create a larger program you can pass around Length-values, without needing to care about the internal representation. This removes a possible source of bugs.

To make your types more usable you can extend it:

  • Implement IEquatable<Length> and IComparable<Length>
  • Implement IFormattable and handle culture when converting lengths from/to strings
  • Implement math operators, and if using .net 7, implement generic math interfaces
  • Implement a TypeConverter
  • Implement a method that outputs a string in the most suitable unit. '1001mm' would be printed as '1.001m', or '1.0m' if you only need one decimal
  • Binary conversion methods, to write/read the length from a binary stream/span
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.