The shocking truth about floating point

Got a bit sidetracked again while writing some code to convert SVG paths to 6809 assembler suitable for a Vectrex (don’t ask), and ended up writing some code to convert numbers to words, with a view for adapting it at some point for TTS/PAF->grammar use.

Entertainingly, as I haven’t introduced any precision or rounding yet, entering – for instance  –  0.03f gives us what is actually stored in the float. You tend to forget that “0.03f” (at least on my PC) is actually more like “zero point zero two nine nine nine nine nine nine eight zero nine two six five one three six seven one eight seven five” …

Here’s the current, unoptimised, code. It requires a fairly recent version of Boost.

Snippet</pre>
<pre>#ifndef NUM_TO_WORDS_HPP_INCLUDED
#define NUM_TO_WORDS_HPP_INCLUDED

static std::string SpeakUnderOneHundred (uint16_t);
static std::string SpeakUnderOneThousand (uint16_t);

enum PrintScale
{
    Short,
    Long,
    LongWithMilliards
};

template <typename T> std::string numeric_to_words (T const & Input, PrintScale DisplayScale = Short, bool UseCommas=true)
{
    #if BOOST_VERSION >= 105500 
        BOOST_STATIC_ASSERT_MSG (boost::is_arithmetic<T>::value, "numeric_to_words can be only instantiated with arithmetic types");
    #else
        BOOST_STATIC_ASSERT (boost::is_arithmetic<T>::value);
    #endif

    const std::string ThousandsShortScale [] = {"", "thousand", "million", "billion", "trillion", "quadrillion", "quintillion"};
    const std::string ThousandsLongScale [] = {"", "thousand", "million", "million", "billion", "billion", "trillion"};
    const std::string ThousandsLongScaleMB [] = {"", "thousand", "million", "milliard", "billion", "billiard", "trillion"};
    const uint8_t IsThousand = 40;  // 8 | 32
    
    const std::string * Thousands;

    const std::string single_digits [] = {"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"};

        const uint8_t MINIMUM_NOT_SET=0xFF;

    uint16_t PowerOf3Boxes [7];

    uint8_t MaxBox = 0, MinBox = MINIMUM_NOT_SET;

    std::string WriteAsString;

    uint8_t MagnitudeBlock = 7;

    uint64_t ValueAsInteger;

    T WorkingCopy = Input;
    
    switch (DisplayScale)
    {
        case Long : Thousands = &ThousandsLongScale [0]; break;
        case LongWithMilliards : Thousands = &ThousandsLongScaleMB [0]; break;
        default : Thousands = &ThousandsShortScale [0]; break;
    };

    #if BOOST_VERSION >= 104800
    if (boost::is_signed<T>::value)
    {
    #endif

        if  (Input < 0)
        {
            WriteAsString += "minus ";

            ValueAsInteger = (uint64_t) (0 - ((int64_t) Input));
        }
        else
            ValueAsInteger = (uint64_t) Input;
    #if BOOST_VERSION >= 104800
    }
    else
        ValueAsInteger = (uint64_t) Input;
    #endif

    if (ValueAsInteger)
    {
        WorkingCopy -= (T) ValueAsInteger;

        // Run forward to populate boxes and determine max/min.

        for (MaxBox = 0; MaxBox < 7; MaxBox++)
        {
        
            if ((PowerOf3Boxes [MaxBox] = (uint16_t) (ValueAsInteger - ((ValueAsInteger / 1000) * 1000))))
                if (MinBox == MINIMUM_NOT_SET) 
                    MinBox = MaxBox;

            ValueAsInteger -= PowerOf3Boxes [MaxBox];

            ValueAsInteger /= 1000;

            if (!ValueAsInteger) break;
        }

        MagnitudeBlock = MaxBox + 1;

        do
        {
            --MagnitudeBlock;

            if ((DisplayScale == Long) && ((1 << MagnitudeBlock) & IsThousand))
            {
                WriteAsString += numeric_to_words ((PowerOf3Boxes [MagnitudeBlock] * 1000) + PowerOf3Boxes [MagnitudeBlock - 1], Long, false);
                
                WriteAsString += " ";
                
                WriteAsString += Thousands [MagnitudeBlock];
                
                MagnitudeBlock --;
            }
            else
            {
                WriteAsString += SpeakUnderOneThousand (PowerOf3Boxes [MagnitudeBlock]);

                if (MagnitudeBlock) WriteAsString += " ";
                
                WriteAsString += Thousands [MagnitudeBlock];

            }

            
            if (MagnitudeBlock > MinBox)
            {
                if ((MagnitudeBlock == 1) && (PowerOf3Boxes [0] <= 99))
                    WriteAsString += " and";
                else if (UseCommas) WriteAsString += ",";
                
                WriteAsString += " ";
            }

        }
        while (MagnitudeBlock > MinBox);
    }
    else
    {
        // Zero

        WriteAsString += single_digits [0];
    }

    if (boost::is_float<T>::value)
    {
        if (WorkingCopy)
        {
            WriteAsString += " point";

            // Deal with decimal point.

            while (WorkingCopy)
            {
                WorkingCopy *= 10;

                WriteAsString += " " + single_digits [(int) WorkingCopy];

                WorkingCopy -= (int) WorkingCopy;
            }
        }
    }
    
    return WriteAsString;
}


static std::string SpeakUnderOneHundred (uint16_t DigitValue)
{
    const std::string Tens [] = {"", "ten", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"};

    const std::string Teens [] = {"ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen"};

    const std::string SingleDigits [] = {"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"};

    std::string WriteAsString;

    if (DigitValue < 10)
        WriteAsString += SingleDigits [DigitValue];
    else if (DigitValue < 20)
        WriteAsString += Teens [DigitValue - 10];
    else
    {
        if (DigitValue / 10) WriteAsString += Tens [DigitValue / 10];
        
        if ((DigitValue / 10) && (DigitValue % 10)) WriteAsString += " ";
        
        if (DigitValue % 10) WriteAsString += SingleDigits [DigitValue % 10];
    }

    return WriteAsString;
}


static std::string SpeakUnderOneThousand (uint16_t DigitValue)
{
    int Hundreds;

    std::string WriteAsString;

    Hundreds = (int) DigitValue / 100;

    if (Hundreds)
    {
        WriteAsString += SpeakUnderOneHundred (Hundreds) + " hundred";

        DigitValue %= (Hundreds * 100);

        if (DigitValue) WriteAsString += " and ";				
    }

    if (DigitValue)	WriteAsString += SpeakUnderOneHundred (DigitValue);

    return WriteAsString;

}

#endif

</pre>
<pre>

And some unit tests, should that sort of thing make your boat floaty. You’ll need UnitTest++.

Snippet</pre>
<pre>#include <cstdlib>
#include <cmath>
#include <limits>
#include <string>
#include <iostream>
#include <boost/type_traits.hpp>
#include <boost/static_assert.hpp>
#include <boost/version.hpp>
 
#include <UnitTest++.h>
 
#if (__cplusplus >= 201103L)
    #include <cstdint>
#else
    #include <stdint.h>
#endif
 
using namespace std;
using namespace UnitTest;
 
#include "../numtowords.hpp"
 
int main (void);
 
TEST (ShortScale)
{
    CHECK_EQUAL ("one quintillion, two hundred and thirty four quadrillion, five hundred and sixty seven trillion, eight hundred and ninety billion, one hundred and twenty three million, four hundred and fifty six thousand, seven hundred and eighty nine", numeric_to_words (1234567890123456789LL));
    CHECK_EQUAL ("zero", numeric_to_words (0));
    CHECK_EQUAL ("one", numeric_to_words (1));
    CHECK_EQUAL ("twenty", numeric_to_words (20));
    CHECK_EQUAL ("one hundred and one", numeric_to_words (101));
    CHECK_EQUAL ("one hundred and twenty", numeric_to_words (120));
    CHECK_EQUAL ("zero", numeric_to_words (0.0));
    CHECK_EQUAL ("zero point one", numeric_to_words (0.1));
    CHECK_EQUAL ("minus ten", numeric_to_words (-10));
    CHECK_EQUAL ("one thousand and one", numeric_to_words (1001));
    CHECK_EQUAL ("one thousand, one hundred and one", numeric_to_words (1101));
    CHECK_EQUAL ("one thousand", numeric_to_words (1000.0f));
    CHECK_EQUAL ("one billion, two hundred and thirty four million, five hundred and sixty seven thousand, eight hundred and ninety", numeric_to_words (1234567890));
}
 
TEST (LongScale)
{
    CHECK_EQUAL ("zero", numeric_to_words (0, Long));
    CHECK_EQUAL ("one", numeric_to_words (1, Long));
    CHECK_EQUAL ("twenty", numeric_to_words (20, Long));
    CHECK_EQUAL ("one hundred and one", numeric_to_words (101, Long));
    CHECK_EQUAL ("one hundred and twenty", numeric_to_words (120, Long));
    CHECK_EQUAL ("zero", numeric_to_words (0.0, Long));
    CHECK_EQUAL ("zero point one", numeric_to_words (0.1, Long));
    CHECK_EQUAL ("minus ten", numeric_to_words (-10, Long));
    CHECK_EQUAL ("one thousand and one", numeric_to_words (1001, Long));
    CHECK_EQUAL ("one thousand", numeric_to_words (1000.0f, Long));
    CHECK_EQUAL ("one thousand two hundred and thirty four million, five hundred and sixty seven thousand, eight hundred and ninety", numeric_to_words (1234567890, Long));
    CHECK_EQUAL ("one trillion, two hundred and thirty four thousand five hundred and sixty seven billion, eight hundred and ninety thousand one hundred and twenty three million, four hundred and fifty six thousand, seven hundred and eighty nine", numeric_to_words (1234567890123456789LL, Long));
}
 
TEST (LongScaleWithMilliards)
{
    CHECK_EQUAL ("zero", numeric_to_words (0, LongWithMilliards));
    CHECK_EQUAL ("one", numeric_to_words (1, LongWithMilliards));
    CHECK_EQUAL ("twenty", numeric_to_words (20, LongWithMilliards));
    CHECK_EQUAL ("one hundred and one", numeric_to_words (101, LongWithMilliards));
    CHECK_EQUAL ("one hundred and twenty", numeric_to_words (120, LongWithMilliards));
    CHECK_EQUAL ("zero", numeric_to_words (0.0, LongWithMilliards));
    CHECK_EQUAL ("zero point one", numeric_to_words (0.1, LongWithMilliards));
    CHECK_EQUAL ("minus ten", numeric_to_words (-10, LongWithMilliards));
    CHECK_EQUAL ("one thousand and one", numeric_to_words (1001, LongWithMilliards));
    CHECK_EQUAL ("one thousand", numeric_to_words (1000.0f, LongWithMilliards));
    CHECK_EQUAL ("one milliard, two hundred and thirty four million, five hundred and sixty seven thousand, eight hundred and ninety", numeric_to_words (1234567890, LongWithMilliards));
    CHECK_EQUAL ("one trillion, two hundred and thirty four billiard, five hundred and sixty seven billion, eight hundred and ninety milliard, one hundred and twenty three million, four hundred and fifty six thousand, seven hundred and eighty nine", numeric_to_words (1234567890123456789LL, LongWithMilliards));
}
 
TEST (ShortScaleNoCommas)
{
    CHECK_EQUAL ("one billion two hundred and thirty four million five hundred and sixty seven thousand eight hundred and ninety", numeric_to_words (1234567890, Short, false));
    CHECK_EQUAL ("one quintillion two hundred and thirty four quadrillion five hundred and sixty seven trillion eight hundred and ninety billion one hundred and twenty three million four hundred and fifty six thousand seven hundred and eighty nine", numeric_to_words (1234567890123456789LL, Short, false));
}
 
TEST (LongScaleNoCommas)
{
    CHECK_EQUAL ("one thousand two hundred and thirty four million five hundred and sixty seven thousand eight hundred and ninety", numeric_to_words (1234567890, Long, false));
    CHECK_EQUAL ("one trillion two hundred and thirty four thousand five hundred and sixty seven billion eight hundred and ninety thousand one hundred and twenty three million four hundred and fifty six thousand seven hundred and eighty nine", numeric_to_words (1234567890123456789LL, Long, false));
}
 
TEST (LongScaleWithMilliardsNoCommas)
{
    CHECK_EQUAL ("one milliard two hundred and thirty four million five hundred and sixty seven thousand eight hundred and ninety", numeric_to_words (1234567890, LongWithMilliards, false));
    CHECK_EQUAL ("one trillion two hundred and thirty four billiard five hundred and sixty seven billion eight hundred and ninety milliard one hundred and twenty three million four hundred and fifty six thousand seven hundred and eighty nine", numeric_to_words (1234567890123456789LL, LongWithMilliards, false));
}
 
 
int main (void)
{	
    int count, ret = UnitTest::RunAllTests ();
 
    for (count = -9999; count < 9999; count++) numeric_to_words (count);
 
    return ret;
}
</pre>
<pre>

EDIT : Fixed code tags. Again.

Leave a comment