Wednesday, December 22, 2010

C# Decimal to English Money Converter



I needed a way to convert a money value (decimal) into the kind of English text that appears on a legal document (like a check or a contract), e.g., 1245.36 becomes "One Thousand Two Hundred Forty Five and 36/100 Dollars."
After searching for awhile, I couldn't find anything. Maybe somebody else will make use of this (took a bit longer than I estimated). Has a limitation on millions (I'm not working on a government contract), but this can easily be changed. Requires .Net 4 (uses Tuples).

Sorry for poor code formatting.

First the decimal extension class that contains a method used by the actual converter class and also has a method to call the converter itself:


public static class DecimalExtension
{

/// <summary>
/// Evaluates just the portion of the value to the right of the decimal place and returns it as a 2 character string.
/// Returns 00 if the value is a whole number.
/// </summary>
/// <returns>The value to the right of the decimal place. Returns 0 if the value is a whole number.</returns>
public static string GetDecimalNumbers( this decimal number )
{
int divint = Convert.ToInt32( Decimal.Floor( number ) );
decimal decValue = number - divint;

var result = decValue.ToString();

if ( result.Length > 1 )
{
result = result.Substring( 2 );
if ( result.Length > 2 )
{
result = result.Substring( 0, 2 );
}
else if ( result.Length < 2 )
{
result += "0";
}
}
else
{
result = "00";
}

return result;
}

public static string ToEnglishMoney( this decimal d )
{
return new DecimalToEnglishMoney( d ).ToString();
}

}

Now for the actual converter class:


public class DecimalToEnglishMoney
{

private enum DigitPlace
{
Ones = 0,
Tens = 1,
Hundreds = 2
}

private string _result;

public DecimalToEnglishMoney( decimal d )
{
_result = FormatWholePortion( d );
_result += FormatDecimalPortion( d );
}

public override string ToString()
{
return _result;
}

private string FormatDecimalPortion( decimal d )
{
var result = string.Empty;
var decimalPortion = d.GetDecimalNumbers();
result += string.Format( " and {0}/100 Dollars", decimalPortion );
return result;
}

private string FormatWholePortion( decimal d )
{
var result = string.Empty;
var wholePortion = (int)Math.Floor( (double)d );
var groups = FormatToNumberGroups( wholePortion );
groups.ForEach( g =>
{
result += (result == string.Empty) ? string.Empty : " ";
var formattedChunk = FormatChunkToEnglish( g.Item2 );
var amountDescriptor = (g.Item1 == string.Empty) ? string.Empty : " " + g.Item1; // "Thousand"
result += formattedChunk;
result += (amountDescriptor == string.Empty) ? string.Empty : amountDescriptor;
} );
return result;
}

/// <summary>
/// Returns the chunk name (blank, Thousand, Million) and the chunk number
/// </summary>
/// <param name="i"></param>
/// <returns>"Million" "NNN"</returns>
private List<Tuple<string, string>> FormatToNumberGroups( int i )
{
var result = new List<Tuple<string, string>>();
var asString = i.ToString();
var count = 1;
while ( asString.Length > 0 )
{
var chunkStartPos = (asString.Length < 3) ? 0 : asString.Length - 3;
var chunkLength = (asString.Length < 3) ? asString.Length : 3;
var chunk = asString.Substring( chunkStartPos, chunkLength );
switch ( count )
{
case 2:
result.Add( new Tuple<string, string>( "Thousand", chunk ) );
break;
case 3:
result.Add( new Tuple<string, string>( "Million", chunk ) );
break;
default:
/// First chunk is blank and we're not expecting anything > than million
result.Add( new Tuple<string, string>( string.Empty, chunk ) );
break;
}
count++;
asString = (asString.Length > 3) ? asString.Substring( 0, asString.Length - 3 ) : string.Empty;
}
result.Reverse();
return result;
}

public string FormatChunkToEnglish( string chunk )
{
Debug.Assert( (chunk.Length <= 3) && (chunk.Length >= 1), "Expecting 1-3 digit portion of a number to format to english." );

var onesDigit = OnesDigit( chunk );
var tensDigit = TensDigit( chunk );
var hundredsDigit = HundredsDigit( chunk );

var onesDigitFormatted = FormatDigit( onesDigit, DigitPlace.Ones, chunk );
var tensDigitFormatted = (tensDigit == string.Empty) ? string.Empty : FormatDigit( tensDigit, DigitPlace.Tens, chunk );
var hundredsDigitFormatted = (hundredsDigit == string.Empty) ? string.Empty : FormatDigit( hundredsDigit, DigitPlace.Hundreds, chunk );

var result = onesDigitFormatted;
if ( tensDigitFormatted != string.Empty )
{
result = ( result == string.Empty ) ? tensDigitFormatted : string.Format( "{0} {1}", tensDigitFormatted, result );
}
if ( hundredsDigitFormatted != string.Empty )
{
result = string.Format( "{0} {1}", hundredsDigitFormatted, result );
}

return result;
}

#region These all deal with the 1-3 digit chunks

private string HundredsDigit( string chunk )
{
return (chunk.Length > 2) ? chunk.Substring( chunk.Length - 3, 1 ) : string.Empty;
}

private string TensDigit( string chunk )
{
return (chunk.Length > 1) ? chunk.Substring( chunk.Length - 2, 1 ) : string.Empty;
}

private string OnesDigit( string chunk )
{
return chunk.Substring( chunk.Length - 1 );
}

private bool ContainsTeen( string allDigits )
{
return TensDigit( allDigits ) == "1";
}

#endregion

private string FormatDigit( string digit, DigitPlace digitPlace, string allDigits )
{
/// Param must be string due to the way it's constructed
Debug.Assert( digit.Length == 1, "Expecting to format single digit while converting number to english, but received multiple digits" );

switch ( digit )
{
case "0":
return string.Empty;
case "1":
switch ( digitPlace )
{
case DigitPlace.Ones:
return ContainsTeen(allDigits) ? string.Empty : "One";
case DigitPlace.Tens:
return FormatTeen(allDigits);
case DigitPlace.Hundreds:
return "One Hundred";
}
break;
case "2":
switch ( digitPlace )
{
case DigitPlace.Ones:
return ContainsTeen(allDigits) ? string.Empty : "Two";
case DigitPlace.Tens:
return "Twenty";
case DigitPlace.Hundreds:
return "Two Hundred";
}
break;
case "3":
switch ( digitPlace )
{
case DigitPlace.Ones:
return ContainsTeen(allDigits) ? string.Empty : "Three";
case DigitPlace.Tens:
return "Thirty";
case DigitPlace.Hundreds:
return "Three Hundred";
}
break;
case "4":
switch ( digitPlace )
{
case DigitPlace.Ones:
return ContainsTeen(allDigits) ? string.Empty : "Four";
case DigitPlace.Tens:
return "Forty";
case DigitPlace.Hundreds:
return "Four Hundred";
}
break;
case "5":
switch ( digitPlace )
{
case DigitPlace.Ones:
return ContainsTeen(allDigits) ? string.Empty : "Five";
case DigitPlace.Tens:
return "Fifty";
case DigitPlace.Hundreds:
return "Five Hundred";
}
break;
case "6":
switch ( digitPlace )
{
case DigitPlace.Ones:
return ContainsTeen(allDigits) ? string.Empty : "Six";
case DigitPlace.Tens:
return "Sixty";
case DigitPlace.Hundreds:
return "Six Hundred";
}
break;
case "7":
switch ( digitPlace )
{
case DigitPlace.Ones:
return ContainsTeen(allDigits) ? string.Empty : "Seven";
case DigitPlace.Tens:
return "Seventy";
case DigitPlace.Hundreds:
return "Seven Hundred";
}
break;
case "8":
switch ( digitPlace )
{
case DigitPlace.Ones:
return ContainsTeen(allDigits) ? string.Empty : "Eight";
case DigitPlace.Tens:
return "Eighty";
case DigitPlace.Hundreds:
return "Eight Hundred";
}
break;
case "9":
switch ( digitPlace )
{
case DigitPlace.Ones:
return ContainsTeen(allDigits) ? string.Empty : "Nine";
case DigitPlace.Tens:
return "Ninety";
case DigitPlace.Hundreds:
return "Nine Hundred";
}
break;
default:
return string.Empty;
}
return string.Empty; // not a good sign when you have to add code just to make it compile
}

/// <summary>
/// </summary>
/// <param name="allDigits">Either 2 or 3 characters long and the tens digit is a one</param>
/// <returns></returns>
private string FormatTeen( string allDigits )
{
Debug.Assert( (allDigits.Length == 2) || (allDigits.Length == 3) );
switch ( OnesDigit(allDigits) )
{
case "0":
return "Ten";
case "1":
return "Eleven";
case "2":
return "Twelve";
case "3":
return "Thirteen";
case "4":
return "Fourteen";
case "5":
return "Fifteen";
case "6":
return "Sixteen";
case "7":
return "Seventeen";
case "8":
return "Eighteen";
case "9":
return "Nineteen";
default:
return string.Empty;
}
}

}

No comments: