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 }