1 /** Conversions between integral values and numerals. */
2 module nxt.numerals;
3 
4 import std.conv: to;
5 import std.traits: isIntegral, isUnsigned, isSomeString;
6 
7 /** Get English ordinal number of unsigned integer $(D n) default to
8  * `defaultOrdinal` if `n` is too large.
9  *
10  * See_Also: https://en.wikipedia.org/wiki/Ordinal_number_(linguistics)
11  */
12 string toEnglishOrdinal(T)(T n, string defaultOrdinal)
13 if (isUnsigned!T)
14 {
15     switch (n)
16     {
17         case 0: return `zeroth`;
18         case 1: return `first`;
19         case 2: return `second`;
20         case 3: return `third`;
21         case 4: return `fourth`;
22         case 5: return `fifth`;
23         case 6: return `sixth`;
24         case 7: return `seventh`;
25         case 8: return `eighth`;
26         case 9: return `ninth`;
27         case 10: return `tenth`;
28         case 11: return `eleventh`;
29         case 12: return `twelveth`;
30         case 13: return `thirteenth`;
31         case 14: return `fourteenth`;
32         case 15: return `fifteenth`;
33         case 16: return `sixteenth`;
34         case 17: return `seventeenth`;
35         case 18: return `eighteenth`;
36         case 19: return `nineteenth`;
37         case 20: return `twentieth`;
38         default: return defaultOrdinal;
39     }
40 }
41 
42 /** Get English ordinal number of unsigned integer $(D n).
43  *
44  * See_Also: https://en.wikipedia.org/wiki/Ordinal_number_(linguistics)
45  */
46 T fromEnglishOrdinalTo(T)(scope const(char)[] ordinal)
47 if (isUnsigned!T)
48 {
49     switch (ordinal)
50     {
51     case `zeroth`: return 0;
52     case `first`: return 1;
53     case `second`: return 2;
54     case `third`: return 3;
55     case `fourth`: return 4;
56     case `fifth`: return 5;
57     case `sixth`: return 6;
58     case `seventh`: return 7;
59     case `eighth`: return 8;
60     case `ninth`: return 9;
61     case `tenth`: return 10;
62     case `eleventh`: return 11;
63     case `twelveth`: return 12;
64     case `thirteenth`: return 13;
65     case `fourteenth`: return 14;
66     case `fifteenth`: return 15;
67     case `sixteenth`: return 16;
68     case `seventeenth`: return 17;
69     case `eighteenth`: return 18;
70     case `nineteenth`: return 19;
71     case `twentieth`: return 20;
72     default:
73         // import nxt.array_algorithm : skipOver;
74         // assert(ordinal.skipOver(`th`));
75         assert(0, `Handle this case`);
76     }
77 }
78 
79 @safe pure nothrow @nogc unittest
80 {
81     assert(`zeroth`.fromEnglishOrdinalTo!uint == 0);
82     assert(`fourteenth`.fromEnglishOrdinalTo!uint == 14);
83 }
84 
85 enum onesNumerals = [ `zero`, `one`, `two`, `three`, `four`,
86                       `five`, `six`, `seven`, `eight`, `nine` ];
87 enum singleWords = onesNumerals ~ [ `ten`, `eleven`, `twelve`, `thirteen`, `fourteen`,
88                                     `fifteen`, `sixteen`, `seventeen`, `eighteen`, `nineteen` ];
89 enum tensNumerals = [ null, `ten`, `twenty`, `thirty`, `forty`,
90                       `fifty`, `sixty`, `seventy`, `eighty`, `ninety`, ];
91 
92 enum englishNumeralsMap = [ `zero`:0, `one`:1, `two`:2, `three`:3, `four`:4,
93                             `five`:5, `six`:6, `seven`:7, `eight`:8, `nine`:9,
94                             `ten`:10, `eleven`:11, `twelve`:12, `thirteen`:13, `fourteen`:14,
95                             `fifteen`:15, `sixteen`:16, `seventeen`:17, `eighteen`:18, `nineteen`:19,
96                             `twenty`:20,
97                             `thirty`:30,
98                             `forty`:40,
99                             `fourty`:40, // common missspelling
100                             `fifty`:50,
101                             `sixty`:60,
102                             `seventy`:70,
103                             `eighty`:80,
104                             `ninety`:90,
105                             `hundred`:100,
106                             `thousand`:1_000,
107                             `million`:1_000_000,
108                             `billion`:1_000_000_000,
109                             `trillion`:1_000_000_000_000 ];
110 
111 /** Check if $(D c) is an English atomic numeral. */
112 bool isEnglishAtomicNumeral(S)(S s)
113 if (isSomeString!S)
114 {
115     return s.among!(`zero`, `one`, `two`, `three`, `four`,
116                     `five`, `six`, `seven`, `eight`, `nine`,
117                     `ten`, `eleven`, `twelve`, `thirteen`, `fourteen`,
118                     `fifteen`, `sixteen`, `seventeen`, `eighteen`, `nineteen`,
119                     `twenty`, `thirty`, `forty`, `fourty`,  // common missspelling
120                     `fifty`, `sixty`, `seventy`, `eighty`, `ninety`,
121                     `hundred`, `thousand`, `million`, `billion`, `trillion`, `quadrillion`);
122 }
123 
124 static immutable ubyte[string] _onesPlaceWordsAA;
125 
126 /* NOTE Be careful with this logic
127    This fails: foreach (ubyte i, e; onesNumerals) { _onesPlaceWordsAA[e] = i; }
128    See_Also: http://forum.dlang.org/thread/vtenbjmktplcxxmbyurt@forum.dlang.org#post-iejbrphbqsszlxcxjpef:40forum.dlang.org
129    */
130 shared static this()
131 {
132     import std.exception: assumeUnique;
133     ubyte[string] tmp;
134     foreach (immutable i, e; onesNumerals)
135     {
136         tmp[e] = cast(ubyte)i;
137     }
138     _onesPlaceWordsAA = assumeUnique(tmp); /* Don't alter tmp from here on. */
139 }
140 
141 import std.traits: isIntegral;
142 
143 /** Convert the number $(D number) to its English textual representation
144     (numeral) also called cardinal number.
145     Opposite: fromNumeral
146     See_Also: https://en.wikipedia.org/wiki/Numeral_(linguistics)
147     See_Also: https://en.wikipedia.org/wiki/Cardinal_number_(linguistics)
148 */
149 string toNumeral(T)(T number, string minusName = `minus`)
150 if (isIntegral!T)
151 {
152     string word;
153 
154     if (number == 0)
155         return `zero`;
156 
157     if (number < 0)
158     {
159         word = minusName ~ ' ';
160         number = -number;
161     }
162 
163     while (number)
164     {
165         if (number < 100)
166         {
167             if (number < singleWords.length)
168             {
169                 word ~= singleWords[cast(int) number];
170                 break;
171             }
172             else
173             {
174                 auto tens = number / 10;
175                 word ~= tensNumerals[cast(int) tens];
176                 number = number % 10;
177                 if (number)
178                     word ~= `-`;
179             }
180         }
181         else if (number < 1_000)
182         {
183             auto hundreds = number / 100;
184             word ~= onesNumerals[cast(int) hundreds] ~ ` hundred`;
185             number = number % 100;
186             if (number)
187                 word ~= ` and `;
188         }
189         else if (number < 1_000_000)
190         {
191             auto thousands = number / 1_000;
192             word ~= toNumeral(thousands) ~ ` thousand`;
193             number = number % 1_000;
194             if (number)
195                 word ~= `, `;
196         }
197         else if (number < 1_000_000_000)
198         {
199             auto millions = number / 1_000_000;
200             word ~= toNumeral(millions) ~ ` million`;
201             number = number % 1_000_000;
202             if (number)
203                 word ~= `, `;
204         }
205         else if (number < 1_000_000_000_000)
206         {
207             auto n = number / 1_000_000_000;
208             word ~= toNumeral(n) ~ ` billion`;
209             number = number % 1_000_000_000;
210             if (number)
211                 word ~= `, `;
212         }
213         else if (number < 1_000_000_000_000_000)
214         {
215             auto n = number / 1_000_000_000_000;
216             word ~= toNumeral(n) ~ ` trillion`;
217             number = number % 1_000_000_000_000;
218             if (number)
219                 word ~= `, `;
220         }
221         else
222         {
223             return to!string(number);
224         }
225     }
226 
227     return word;
228 }
229 alias toTextual = toNumeral;
230 
231 @safe pure nothrow unittest
232 {
233     assert(1.toNumeral == `one`);
234     assert(5.toNumeral == `five`);
235     assert(13.toNumeral == `thirteen`);
236     assert(54.toNumeral == `fifty-four`);
237     assert(178.toNumeral == `one hundred and seventy-eight`);
238     assert(592.toNumeral == `five hundred and ninety-two`);
239     assert(1_234.toNumeral == `one thousand, two hundred and thirty-four`);
240     assert(10_234.toNumeral == `ten thousand, two hundred and thirty-four`);
241     assert(105_234.toNumeral == `one hundred and five thousand, two hundred and thirty-four`);
242     assert(7_105_234.toNumeral == `seven million, one hundred and five thousand, two hundred and thirty-four`);
243     assert(3_007_105_234.toNumeral == `three billion, seven million, one hundred and five thousand, two hundred and thirty-four`);
244     assert(555_555.toNumeral == `five hundred and fifty-five thousand, five hundred and fifty-five`);
245     assert(900_003_007_105_234.toNumeral == `nine hundred trillion, three billion, seven million, one hundred and five thousand, two hundred and thirty-four`);
246     assert((-5).toNumeral == `minus five`);
247 }
248 
249 import std.typecons: Nullable;
250 
251 version = show;
252 
253 /** Convert the number $(D number) to its English textual representation.
254     Opposite: toNumeral.
255     TODO: Throw if number doesn't fit in long.
256     TODO: Add variant to toTextualBigIntegerMaybe.
257     TODO: Could this be merged with to!(T)(string) if (isInteger!T) ?
258 */
259 Nullable!long fromNumeral(T = long, S)(S x) @safe pure
260 if (isSomeString!S)
261 {
262     import std.algorithm: splitter, countUntil, skipOver, findSplit;
263     import nxt.array_algorithm : endsWith;
264     import std.range: empty;
265 
266     typeof(return) total;
267 
268     T sum = 0;
269     bool defined = false;
270     bool negative = false;
271 
272     auto terms = x.splitter(`,`); // comma separate terms
273     foreach (term; terms)
274     {
275         auto factors = term.splitter; // split factors by whitespace
276 
277         // prefixes
278         factors.skipOver(`plus`); // no semantic effect
279         if (factors.skipOver(`minus`) ||
280             factors.skipOver(`negative`))
281         {
282             negative = true;
283         }
284         factors.skipOver(`plus`); // no semantic effect
285 
286         // main
287         T product = 1;
288         bool tempSum = false;
289         foreach (const factor; factors)
290         {
291             if (factor == `and`)
292             {
293                 tempSum = true;
294             }
295             else
296             {
297                 T subSum = 0;
298                 foreach (subTerm; factor.splitter(`-`)) // split for example fifty-five to [`fifty`, `five`]
299                 {
300                     if (const value = subTerm in englishNumeralsMap)
301                     {
302                         subSum += *value;
303                         defined = true;
304                     }
305                     else if (subTerm.endsWith(`s`)) // assume plural s for common misspelling millions instead of million
306                     {
307                         if (const value = subTerm[0 .. $ - 1] in englishNumeralsMap) // without possible plural s
308                         {
309                             subSum += *value;
310                             defined = true;
311                         }
312                     }
313                     else
314                     {
315                         return typeof(return).init; // could not process
316                     }
317                 }
318                 if (tempSum)
319                 {
320                     product += subSum;
321                     tempSum = false;
322                 }
323                 else
324                 {
325                     product *= subSum;
326                 }
327             }
328         }
329 
330         sum += product;
331     }
332 
333     if (defined)
334     {
335         return typeof(return)(negative ? -sum : sum);
336     }
337     else
338     {
339         return typeof(return).init;
340     }
341 }
342 
343 @safe pure unittest
344 {
345     import std.range: chain, iota;
346 
347     // undefined cases
348     assert(``.fromNumeral.isNull);
349     assert(`dum`.fromNumeral.isNull);
350     assert(`plus`.fromNumeral.isNull);
351     assert(`minus`.fromNumeral.isNull);
352 
353     foreach (i; chain(iota(0, 20),
354                       iota(20, 100, 10),
355                       iota(100, 1000, 100),
356                       iota(1000, 10000, 1000),
357                       iota(10000, 100000, 10000),
358                       iota(100000, 1000000, 100000),
359                       [55, 1_200, 105_000, 155_000, 555_555, 150_000, 3_001_200]))
360     {
361         const ti = i.toNumeral;
362         assert(-i == (`minus ` ~ ti).fromNumeral);
363         assert(+i == (`plus ` ~ ti).fromNumeral);
364         assert(+i == ti.fromNumeral);
365     }
366 
367     assert(`nine thousands`.fromNumeral == 9_000);
368     assert(`two millions`.fromNumeral == 2_000_000);
369     assert(`twenty-two hundred`.fromNumeral == 2200);
370     assert(`two fifty`.fromNumeral == 100);
371     assert(`two tens`.fromNumeral == 20);
372     assert(`two ten`.fromNumeral == 20);
373 }