Neat Tricks for Effortlessly Formatting Currency in C#

.NET
Share
Neat Tricks for Effortlessly Formatting Currency in C#

In the article, I am going to present to you a few ways how to create a handy class that effortlessly formatting currency in C#. Initially, I came up with the idea because I needed to write several System/UI tests that needed to verify price amounts on the site that our team was developing. However, the currency formats on the different web forms were different, so I decided to write a utility for the job. I think it is worth sharing because it is taking advantage of several underutilized features of the C# language like Flags attribute, dynamic, generic and extension methods.

Formatting Currency Needs

All of the expected values of the tests were present in double or decimal, and I needed to format them to particular strings.
For example: $1,220.50, 1220.50$, -1220.5365$. Probably you can notice the few differences in the presented examples- the dollar sign is used as a prefix and as a suffix, the comma, the minus sign and the number precision. All strings had to be formatted using the en-us culture, we don’t have cases where a different currency is used.
Validate Order Summary Amazon
My tests had to verify similar price amounts, discounts, and taxes, as depicted on the image.

Neat Tricks for Effortlessly Formatting Currency in C#

Combine Formatting Options Through Flags Attribute

As pointed previously we have a few possible scenarios for currency formatting. To be able to combine them I have created a special enum decorated with Flags attribute- DigitsFormattingSettings.

[Flags]
public enum DigitsFormattingSettings
{
None = 0,
NoComma = 1,
PrefixDollar = 2,
PrefixMinus = 4,
SufixDollar = 8
}

Flags enumerations are used for masking bit fields and doing bitwise comparisons. They are the correct design to use when multiple enumeration values can be specified at the same time.
It is also important to note that Flags does not automatically make the enum values powers of two. If you omit the numeric values, the enum will not work as one might expect in bitwise operations because the values by default start with 0 and increment.
Depending on the size of your enumeration, the exponential increase in values can get tricky to keep track of or calculate in your head. There is a technique in C# that will make this far more manageable. Through the use of bit shifting, we can move the 1 bit sequentially without having to worry about the actual integer value. It allows to see which bit switch you turned on for a particular value.

[Flags]
public enum DigitsFormattingSettingsBitShifting
{
None = 0,
NoComma = 1 << 0,
PrefixDollar = 1 << 1,
PrefixMinus = 1 << 2,
SufixDollar = 1 << 3
}

Note: You cannot use the None enumerated constant in a bitwise AND operation to test for a flag because the result is always zero. However, you can perform a logical, not a bitwise, comparison between the numeric value and the None enumerated constant to determine whether any bits in the numeric value are set.
Official documentation– https://msdn.microsoft.com/en-us/library/ms229062.aspx

Extension ToString Methods for Currency Formatting

My main idea was to create a class holding multiple double and decimal extensions methods that provide the desired formatting capabilities. Initially, the primary method that handled the effortless formatting required a dynamic parameter (the number that was going to be formatted). Then the method was called internally from all public extension methods.

public static class CurrencyDelimiterFormatter
{
private static readonly CultureInfo usCultureInfo = CultureInfo.CreateSpecificCulture(en-US);
public static string ToStringUsDigitsFormatting(
this double number,
DigitsFormattingSettings digitsFormattingSettings = DigitsFormattingSettings.None,
int precesion = 2)
{
string result = ToStringUsDigitsFormattingInternal(number, digitsFormattingSettings, precesion);
return result;
}
public static string ToStringUsDigitsFormatting(
this decimal number,
DigitsFormattingSettings digitsFormattingSettings = DigitsFormattingSettings.None,
int precesion = 2)
{
string result = ToStringUsDigitsFormattingInternal(number, digitsFormattingSettings, precesion);
return result;
}
private static string ToStringUsDigitsFormattingInternal(
dynamic number,
DigitsFormattingSettings digitsFormattingSettings = DigitsFormattingSettings.None,
int precesion = 2)
{
string formattedDigits = string.Empty;
string currentNoComaFormatSpecifier = string.Concat(#., new string(0, precesion));
string currentComaFormatSpecifier = string.Concat(##,#., new string(0, precesion));
formattedDigits =
digitsFormattingSettings.HasFlag(DigitsFormattingSettings.NoComma) ? number.ToString(currentNoComaFormatSpecifier, usCultureInfo) :
number.ToString(currentComaFormatSpecifier, usCultureInfo);
if (digitsFormattingSettings.HasFlag(DigitsFormattingSettings.PrefixDollar))
{
formattedDigits = string.Concat($, formattedDigits);
}
if (digitsFormattingSettings.HasFlag(DigitsFormattingSettings.PrefixMinus))
{
formattedDigits = string.Concat(, formattedDigits);
}
if (digitsFormattingSettings.HasFlag(DigitsFormattingSettings.SufixDollar))
{
formattedDigits = string.Concat(formattedDigits, $);
}
return formattedDigits;
}
}

I didn’t want to duplicate the formatting logic twice for double and decimal because of that I came up with this solution. But as you probably know variables of type dynamic are compiled into variables of type object. As a result of the boxing and unboxing between value types and object, the formatting was going to be a little bit slower compared to other alternatives.

Currency Formatting Improved

Improved Generic Extension ToString Methods for Currency Formatting

As pointed above there are better alternatives of my initial implementation. One of them is the usage of generic methods.

private static string ToStringUsDigitsFormattingInternal<T>(
T number,
DigitsFormattingSettings digitsFormattingSettings = DigitsFormattingSettings.None,
int precesion = 2)
where T : struct,
IComparable,
IComparable<T>,
IConvertible,
IEquatable<T>,
IFormattable
{
string formattedDigits = string.Empty;
string currentNoComaFormatSpecifier = string.Concat(#., new string(0, precesion));
string currentComaFormatSpecifier = string.Concat(##,#., new string(0, precesion));
formattedDigits =
digitsFormattingSettings.HasFlag(DigitsFormattingSettings.NoComma) ? number.ToString(currentNoComaFormatSpecifier, usCultureInfo) :
number.ToString(currentComaFormatSpecifier, usCultureInfo);
if (digitsFormattingSettings.HasFlag(DigitsFormattingSettings.PrefixDollar))
{
formattedDigits = string.Concat($, formattedDigits);
}
if (digitsFormattingSettings.HasFlag(DigitsFormattingSettings.PrefixMinus))
{
formattedDigits = string.Concat(, formattedDigits);
}
if (digitsFormattingSettings.HasFlag(DigitsFormattingSettings.SufixDollar))
{
formattedDigits = string.Concat(formattedDigits, $);
}
return formattedDigits;
}

The generic constraints assure that only value types can be passed as a generic parameter.
There are a couple of interesting parts of the code that require further explanations.
The ‘#’ custom specifier serves as a digit-placeholder symbol. If the value that is being formatted has a digit in the position where the “#” symbol appears in the format string, that number is copied to the result string. Otherwise, nothing is stored in that position in the result string.
The “.” custom format specifier inserts a localized decimal separator into the result string. The first period in the format string determines the location of the decimal separator in the formatted value; any additional periods are ignored.
The “,” character serves as both a group separator and a number scaling specifier.
The “0” custom format specifier serves as a zero-placeholder symbol. If the value that is being formatted has a digit in the position where the zero appears in the format string, that digit is copied to the result string; otherwise, a zero appears in the result string. The position of the leftmost zero before the decimal point and the rightmost zero after the decimal point determines the range of digits that are always present in the result string.
Through the precision parameter, we can specify a different count of the digits after the decimal point.
There are two main formats that the method uses, one for comma grouped numbers and one for the rest.

string currentNoComaFormatSpecifier = string.Concat(“#.”, new string(‘0’, precesion));
string currentComaFormatSpecifier = string.Concat(“##,#.”, new string(‘0’, precesion));

The zeroes for the specified precision are generated through the special string constructor that creates a new string from a specified character.
With the release of .NET 4.0, developers found a new method to handle the “dirty work” of checking to see if an enum has a flag. Every enum instance now has this method. You just pass the flag value you want to check for. I am using it to check if an additional formatting is needed as adding a dollar sign as a prefix/suffix, or the minus sign.

Formatting Currency in C# Example Usage

Usually, I set the namespace for my extension methods classes to be equal to the most basic one. In this example – CSharp.Series.Tests. This ease the usage of the extension methods around the project because no additional using statements are required.

public static unsafe void Main(string[] args)
{
Console.WriteLine(1220.5.ToStringUsDigitsFormatting(DigitsFormattingSettings.PrefixDollar));
// Result- $1,220.50
Console.WriteLine(1220.5.ToStringUsDigitsFormatting(DigitsFormattingSettings.SufixDollar | DigitsFormattingSettings.NoComma));
// Result- 1220.50$
Console.WriteLine(1220.53645.ToStringUsDigitsFormatting(DigitsFormattingSettings.SufixDollar | DigitsFormattingSettings.NoComma | DigitsFormattingSettings.PrefixMinus, 4));
// Result- -1220.5365$
}

The usage of the provided solution is straightforward. You can mix as many settings options as you like and specify a custom precision.
You are no more required to check every time the MSDN documentation for the ToString currency format options and hard code them at multiple places.

References