On 1/25/22 11:52, Ali Çehreli wrote: > a program I wrote about spelling-out parts of a number
Here is the program as a single module: module spellout.spellout; // This program was written as a code kata to spell out // certain parts of integers as in "1 million 2 thousand // 42". Note that this way of spelling-out numbers is not // grammatically correct in English. // Returns a string that contains the partly spelled-out version // of the parameter. // // You must copy the returned string when needed as this function // uses the same internal buffer for all invocations of the same // template instance. auto spellOut(T)(in T number_) { import std.array : Appender; import std.string : strip; import std.traits : Unqual; import std.meta : AliasSeq; static Appender!(char[]) result; result.clear; // We treat these specially because the algorithm below does // 'number = -number' and calls the same implementation // function. The trouble is, for example, -int.min is still a // negative number. alias problematics = AliasSeq!( byte, "negative 128", short, "negative 32 thousand 768", int, "negative 2 billion 147 million 483 thousand 648", long, "negative 9 quintillion 223 quadrillion 372 trillion" ~ " 36 billion 854 million 775 thousand 808"); static assert((problematics.length % 2) == 0); static foreach (i, P; problematics) { static if (i % 2) { // This is a string; skip } else { // This is a problematic type static if (is (T == P)) { // Our T happens to be this problematic type if (number_ == T.min) { // and we are dealing with a problematic value result ~= problematics[i + 1]; return result.data; } } } } auto number = cast(Unqual!T)number_; // Thanks 'in'! :p if (number == 0) { result ~= "zero"; } else { if (number < 0) { result ~= "negative"; static if (T.sizeof < int.sizeof) { // Being careful with implicit conversions. (See the dmd // command line switch -preview=intpromote) number = cast(T)(-cast(int)number); } else { number = -number; } } spellOutImpl(number, result); } return result.data.strip; } unittest { assert(1_001_500.spellOut == "1 million 1 thousand 500"); assert((-1_001_500).spellOut == "negative 1 million 1 thousand 500"); assert(1_002_500.spellOut == "1 million 2 thousand 500"); } import std.format : format; import std.range : isOutputRange; void spellOutImpl(T, O)(T number, ref O output) if (isOutputRange!(O, char)) in (number > 0, format!"Invalid number: %s"(number)) { import std.range : retro; import std.format : formattedWrite; foreach (divider; dividers!T.retro) { const quotient = number / divider.value; if (quotient) { output.formattedWrite!" %s %s"(quotient, divider.word); } number %= divider.value; } } struct Divider(T) { T value; // 1_000, 1_000_000, etc. string word; // "thousand", etc } // Returns the words related with the provided size of an // integral type. The parameter is number of bytes // e.g. int.sizeof auto words(size_t typeSize) { // This need not be recursive at all but it was fun using // recursion. final switch (typeSize) { case 1: return [ "" ]; case 2: return words(1) ~ [ "thousand" ]; case 4: return words(2) ~ [ "million", "billion" ]; case 8: return words(4) ~ [ "trillion", "quadrillion", "quintillion" ]; } } unittest { // These are relevant words for 'int' and 'uint' values: assert(words(4) == [ "", "thousand", "million", "billion" ]); } // Returns a Divider!T array associated with T auto dividers(T)() { import std.range : array, enumerate; import std.algorithm : map; static const(Divider!T[]) result = words(T.sizeof) .enumerate!T .map!(t => Divider!T(cast(T)(10^^(t.index * 3)), t.value)) .array; return result; } unittest { // Test a few entries assert(dividers!int[1] == Divider!int(1_000, "thousand")); assert(dividers!ulong[3] == Divider!ulong(1_000_000_000, "billion")); } void main() { version (test) { return; } import std.meta : AliasSeq; import std.stdio : writefln; import std.random : Random, uniform; import std.conv : to; static foreach (T; AliasSeq!(byte, ubyte, short, ushort, int, uint, long, ulong)) {{ // A few numbers for each type report(T.min); report((T.max / 4).to!T); // Overcome int promotion for // shorter types because I want // to test with the exact type // e.g. for byte. report(T.max); }} enum count = 2_000_000; writefln!"Testing with %,s random numbers"(spellOut(count)); // Use the same seed to be fair between compilations enum seed = 0; auto rnd = Random(seed); ulong totalLength; foreach (i; 0 .. count) { const number = uniform(int.min, int.max, rnd); const result = spellOut(number); totalLength += result.length; } writefln!("A meaningless number to prevent the compiler from" ~ " removing the entire loop: %,s")(totalLength); } void report(T)(T number) { import std.stdio : writefln; writefln!" %6s % ,s: %s"(T.stringof, number, spellOut(number)); } Ali