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