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.
