Friday, June 22, 2012

Example: Strip Trailing Zeros After Decimal Point

Question: How do I strip trailing zeros after the decimal point from a string containing a number?

Mind-Boggling Answer: This C# solution got 24 up-votes on StackOverflow, and the author has almost half-a-million reputation points for his 21,000 answers, so it must be right!

using System;
using System.Numerics;

public static class DecimalExtensions
{
    // Avoiding implicit conversions just for clarity
    private static readonly BigInteger Ten = new BigInteger(10);
    private static readonly BigInteger UInt32Mask = new BigInteger(0xffffffffU);

    public static decimal Normalize(this decimal input)
    {
        unchecked
        {
            int[] bits = decimal.GetBits(input);
            BigInteger mantissa = 
                new BigInteger((uint) bits[0]) +
                (new BigInteger((uint) bits[1]) << 32) +
                (new BigInteger((uint) bits[2]) << 64);

            int sign = bits[3] & int.MinValue;            
            int exponent = (bits[3] & 0xff0000) >> 16;

            // The loop condition here is ugly, because we want
            // to do both the DivRem part and the exponent check :(
            while (exponent > 0)
            {
                BigInteger remainder;
                BigInteger divided = BigInteger.DivRem(mantissa, Ten, out remainder);
                if (remainder != BigInteger.Zero)
                {
                    break;
                }
                exponent--;
                mantissa = divided;
            }
            // Okay, now put it all back together again...
            bits[3] = (exponent << 16) | sign;
            // For each 32 bits, convert the bottom 32 bits into a uint (which won't
            // overflow) and then cast to int (which will respect the bits, which
            // is what we want)
            bits[0] = (int) (uint) (mantissa & UInt32Mask);
            mantissa >>= 32;
            bits[1] = (int) (uint) (mantissa & UInt32Mask);
            mantissa >>= 32;
            bits[2] = (int) (uint) (mantissa & UInt32Mask);

            return new decimal(bits);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Check(6.000m);
            Check(6000m);
            Check(6m);
            Check(60.00m);
            Check(12345.00100m);
            Check(-100.00m);
        }

        static void Check(decimal d)
        {
            Console.WriteLine("Before: {0}  -  after: {1}", d, d.Normalize());
        }
    }
}

Gasp! [shudder]

...words cannot express the horror.

How about this instead?

Just... write some code to look for trailing zeros after the decimal point and remove them from the string.

Sound familiar? It's the original Question almost word for word.
CREATE FUNCTION strip_trailing_zeros_after_decimal_point (
   IN @input_number VARCHAR ( 100 ) )
   RETURNS VARCHAR ( 100 ) 
BEGIN
   DECLARE @output_number VARCHAR ( 100 );

   SET @output_number = TRIM ( COALESCE ( @input_number, '' ) );

   IF LOCATE ( @output_number, '.' ) > 0 THEN

      WHILE RIGHT ( @output_number, 1 ) = '0' LOOP
         SET @output_number = LEFT ( @output_number, LENGTH ( @output_number ) - 1 );
      END LOOP;

      IF RIGHT ( @output_number, 1 ) = '.' THEN
         SET @output_number = LEFT ( @output_number, LENGTH ( @output_number ) - 1 );
      END IF;

   END IF;
      
   RETURN @output_number;

END;
  • The SET on line 7 does a bit of input editing: it turns NULL into '' and gets rid of leading and trailing spaces.

  • The IF LOCATE on line 9 makes sure the function doesn't do anything more if there's no decimal point at all.

  • The WHILE loop on lines 11 to 13 zaps each trailing zero, one at a time, from the RIGHT end of the string.

  • The IF RIGHT ... SET on lines 15 to 17 gets rid of the decimal point if it ends up being the last thing on the line.
Not as sexy as "(int) (uint) (mantissa & UInt32Mask)", but it's readable and it does seem to work...
SELECT '000' AS x, strip_trailing_zeros_after_decimal_point ( x ) AS y
UNION
SELECT '123' AS x, strip_trailing_zeros_after_decimal_point ( x ) AS y
UNION
SELECT '123.' AS x, strip_trailing_zeros_after_decimal_point ( x ) AS y
UNION
SELECT '123.0' AS x, strip_trailing_zeros_after_decimal_point ( x ) AS y
UNION
SELECT '123.00' AS x, strip_trailing_zeros_after_decimal_point ( x ) AS y
UNION
SELECT '123.01' AS x, strip_trailing_zeros_after_decimal_point ( x ) AS y
UNION
SELECT '123.10' AS x, strip_trailing_zeros_after_decimal_point ( x ) AS y
UNION
SELECT '123.010' AS x, strip_trailing_zeros_after_decimal_point ( x ) AS y
UNION
SELECT '123.456' AS x, strip_trailing_zeros_after_decimal_point ( x ) AS y
UNION
SELECT '123.4560' AS x, strip_trailing_zeros_after_decimal_point ( x ) AS y
UNION
SELECT '123.45600' AS x, strip_trailing_zeros_after_decimal_point ( x ) AS y
UNION
SELECT '123000' AS x, strip_trailing_zeros_after_decimal_point ( x ) AS y
ORDER BY 1;

x         y
000       000
123       123
123.      123
123.0     123
123.00    123
123.01    123.01
123.010   123.01
123.10    123.1
123.456   123.456
123.4560  123.456
123.45600 123.456
123000    123000
...well, it works for trailing zeros, the "000" is a formatting question for another day.

And I'll bet you could do better! (even simpler, even more straightforward)

Dilbert.com




No comments: