Skip to content

Instantly share code, notes, and snippets.

@decon3
Last active February 4, 2017 13:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save decon3/ac4805378a8b511f45b130d7d6aea962 to your computer and use it in GitHub Desktop.
Save decon3/ac4805378a8b511f45b130d7d6aea962 to your computer and use it in GitHub Desktop.
BCD converter
using System;
using System.Collections.Generic;
// Note: Uses the Unpack() method found in
// https://github.com/thevinnie/UnpackDecimal/blob/master/SSISTask/UnpackDecimal/UnpackDecimal.cs
namespace Utils.Conversions
{
/// <summary>
/// Handles Binary Coded Decimal conversions
/// </summary>
/// <remarks>
/// Eg. for handling amount in dollars & cents:
/// var converter = new BcdConverter(2);
/// var bcd = converter.PackAmount(10.23M);
/// var amount = converter.UnpackAmount(bcd);
/// </remarks>
public class BcdConverter
{
int _precision;
public BcdConverter(int precision)
{
_precision = precision;
}
public string PackAmount(decimal amount)
{
return PackAmount(amount, _precision);
}
public string PackAmount(decimal amount, int precision)
{
Stack<byte> bcd = new Stack<byte>(10);
long value = StripDecimalPoint(amount);
byte currentByte;
if (value < 0)
{
currentByte = 0x0d;
value = -value;
}
else
{
currentByte = 0x0c;
}
bool byteComplete = false;
while (value != 0)
{
if (byteComplete)
currentByte = (byte)(value % 10);
else
currentByte |= (byte)((value % 10) << 4);
value /= 10;
byteComplete = !byteComplete;
if (byteComplete)
bcd.Push(currentByte);
}
if (!byteComplete)
bcd.Push(currentByte);
int nibbles = precision + 2 + 1; //precision + 2 decimals + 1 sign
var bcdLength = nibbles % 2 == 0 ? nibbles / 2 : (nibbles + 1) / 2; // even number of bytes
// ensure that the resulting bcd string be of fixed length, independent of the value
// i.e., 10.00 or 10000.00 should be converted into BCD strings of same length
// (length is determined by precision)
if (bcd.Count < bcdLength)
{
var padding = bcdLength - bcd.Count;
for (var i = 0; i < padding; i++)
{
bcd.Push((byte)0);
}
}
if (bcd.Count > bcdLength)
throw new ArgumentOutOfRangeException("amount", amount + " - precision is greater than allowed, namely, " + precision);
return new string(bcd.ToArray().Select(x => (char)x).ToArray());
}
public Decimal UnpackAmount(string bcd)
{
byte[] inp = bcd.Select(c => (byte)c).ToArray();
long lo = 0;
long mid = 0;
long hi = 0;
bool isNegative;
// this nybble stores only the sign, not a digit.
// "C" hex is positive, "D" hex is negative, and "F" hex is unsigned.
switch (nibble(inp, 0))
{
case 0x0D:
isNegative = true;
break;
case 0x0F:
case 0x0C:
isNegative = false;
break;
default:
throw new Exception("Bad sign nibble");
}
long intermediate;
long carry;
long digit;
for (int j = inp.Length * 2 - 1; j > 0; j--)
{
// multiply by 10
intermediate = lo * 10;
lo = intermediate & 0xffffffff;
carry = intermediate >> 32;
intermediate = mid * 10 + carry;
mid = intermediate & 0xffffffff;
carry = intermediate >> 32;
intermediate = hi * 10 + carry;
hi = intermediate & 0xffffffff;
carry = intermediate >> 32;
// By limiting input length to 14, we ensure overflow will never occur
digit = nibble(inp, j);
if (digit > 9)
{
throw new Exception("Bad digit");
}
intermediate = lo + digit;
lo = intermediate & 0xffffffff;
carry = intermediate >> 32;
if (carry > 0)
{
intermediate = mid + carry;
mid = intermediate & 0xffffffff;
carry = intermediate >> 32;
if (carry > 0)
{
intermediate = hi + carry;
hi = intermediate & 0xffffffff;
carry = intermediate >> 32;
// carry should never be non-zero. Back up with validation
}
}
}
return new Decimal((int)lo, (int)mid, (int)hi, isNegative, 2);
}
private long StripDecimalPoint(decimal amount)
{
return Convert.ToInt64(amount * 100);
}
private int nibble(byte[] inp, int nibbleNo)
{
int b = inp[inp.Length - 1 - nibbleNo / 2];
return (nibbleNo % 2 == 0) ? (b & 0x0000000F) : (b >> 4);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment