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 }