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 }