Source wolsey.js

  1. (function (moduleFactory) {
  2. if(typeof exports === "object") {
  3. module.exports = moduleFactory();
  4. } else if (typeof define === "function" && define.amd) {
  5. define([], moduleFactory);
  6. }
  7. }(function () {
  8. /**
  9. * @module wolsey
  10. * @description Generates numerals as words and ordinals
  11. *
  12. * var Wolsey = require("wolsey");
  13. * var cardinal = new Wolsey();
  14. *
  15. * @returns {object} Wolsey {@link module:wolsey.Constructor}
  16. */
  17. /**
  18. * @method lowestEN
  19. * @inner
  20. * @param {number} num Number to be converted
  21. * @param {object} numerals Object representing digits and irregular instances
  22. * @param {object} numeral Wolsey instance’s numeral object
  23. * @return {string} converted Numeral string
  24. */
  25. function lowestEN (num, numerals, numeral) {
  26. var converted;
  27. if (!num) {
  28. converted = "";
  29. } else if (numerals[num]) {
  30. converted = numerals[num];
  31. } else {
  32. converted = numerals[Math.floor(num/10) * 10] + numeral.hyphenatecompound + numerals[num % 10];
  33. }
  34. return converted;
  35. }
  36. /**
  37. * @method EN
  38. * @static
  39. * @param {object} [options]
  40. * @param {object} [powers=powers] Powers to use as units
  41. * @param {boolean} [longscale=false] Whether to use longscale units
  42. * @param {function} [lowest=lowestEN] Method to handle non-power units
  43. * @param {object} [numerals=numerals] Lookup map of numerals
  44. * @param {function} [ordinal=ordinal] Ordinal method
  45. * @param {function} [ordinalAsNumber=ordinalAsNumber] Ordinal as number method
  46. * @description Generic English lang number generator
  47. *
  48. * Calls {@link module:wolsey.LANG}
  49. * @return {object} LANG instance
  50. */
  51. function EN (options) {
  52. options = options || {};
  53. options.lowest = options.lowest || lowestEN;
  54. if (!options.powers) {
  55. options.powers = {
  56. 2: "hundred",
  57. 3: "thousand",
  58. 6: "million",
  59. 9: "billion",
  60. 12: "trillion",
  61. 15: "quadrillion",
  62. 18: "quintillion"
  63. };
  64. if (options.longscale) {
  65. options.powers["9"] = "milliard" ;
  66. options.powers["15"] = "billiard" ;
  67. }
  68. }
  69. if (!options.numerals) {
  70. options.numerals = {
  71. "0": "zero",
  72. "1": "one",
  73. "2": "two",
  74. "3": "three",
  75. "4": "four",
  76. "5": "five",
  77. "6": "six",
  78. "7": "seven",
  79. "8": "eight",
  80. "9": "nine",
  81. "10": "ten",
  82. "11": "eleven",
  83. "12": "twelve",
  84. "13": "thirteen",
  85. "14": "fourteen",
  86. "15": "fifteen",
  87. "16": "sixteen",
  88. "17": "seventeen",
  89. "18": "eighteen",
  90. "19": "nineteen",
  91. "20": "twenty",
  92. "30": "thirty",
  93. "40": "forty",
  94. "50": "fifty",
  95. "60": "sixty",
  96. "70": "seventy",
  97. "80": "eighty",
  98. "90": "ninety"
  99. };
  100. }
  101. if (!options.ordinal) {
  102. options.ordinal = function (num, options) {
  103. var numString = this.numeral(num);
  104. var map = {
  105. one: "first",
  106. two: "second",
  107. three: "third",
  108. five: "fifth",
  109. eight: "eighth",
  110. nine: "ninth",
  111. twelve: "twelfth"
  112. };
  113. var words = numString.split(" ");
  114. var last = words.length - 1,
  115. lastword = words[last];
  116. var lastmap = map[lastword];
  117. if (lastmap) {
  118. words[last] = lastmap;
  119. } else {
  120. var lastchar = lastword.slice(-1);
  121. if (lastchar === "y") {
  122. lastword = lastword.replace("y", "ie");
  123. }
  124. words[last] = lastword + "th";
  125. }
  126. return words.join(" ");
  127. };
  128. }
  129. if (!options.ordinalAsNumber) {
  130. options.ordinalAsNumber = function (num, options) {
  131. var map = {
  132. 1: "st",
  133. 2: "nd",
  134. 3: "rd",
  135. all: "th"
  136. };
  137. var remainder = num % 100;
  138. if (remainder > 20) {
  139. remainder = remainder % 10;
  140. }
  141. return num + (map[remainder] ? map[remainder] : map.all);
  142. };
  143. }
  144. return LANG(options);
  145. }
  146. /**
  147. * @method LANG
  148. * @static
  149. * @param {object} [options]
  150. * @param {object} [options.numerals] Digits and irregualr numbers to use
  151. * @param {array|object} [options.powers] Powers of 10 considered to be increments by language
  152. * @param {function} [options.lowest] Method to handle non-powers
  153. * @param {function} [options.ordinal] Method to handle ordinals
  154. * @param {function} [options.ordinalAsNumber] Method to handle ordinals as numbers
  155. * @param {string} [options.space=" "] Character to use for generic spaces between number words
  156. * @param {string} [options.unitone] Value to use when unit quotient is one
  157. * @param {boolean} [options.pluralizeunitexact=false] Pluralize unit only if no remnant
  158. * @param {boolean} [options.hyphenatecompound=false] Whether to hyphenate unit phrases
  159. * @param {string} [options.oneconjoin] Value to use when final part of remnant is one
  160. * @description Generic lang number generator
  161. * @return {object} methods instance
  162. */
  163. function LANG (options) {
  164. options = options || {};
  165. var numerals = options.numerals;
  166. var powers = options.powers;
  167. var lowestMethod = options.lowest;
  168. var ordinal = options.ordinal;
  169. var ordinalAsNumber = options.ordinalAsNumber;
  170. var space = options.space || " ";
  171. // Should throw an exception if we don't have these
  172. if (!Array.isArray(options.powers)) {
  173. var realpowers = [];
  174. for (var power in powers) {
  175. var realprop = {};
  176. var powerprop = options.powers[power];
  177. if (typeof powerprop === "string") {
  178. realprop.unit = powerprop;
  179. } else {
  180. for (var prop in powerprop) {
  181. realprop[prop] = powerprop[prop];
  182. }
  183. }
  184. realprop.power = power;
  185. realpowers.push(realprop);
  186. }
  187. if (!powers["0"]) {
  188. realpowers.push({ power: 0 });
  189. }
  190. powers = realpowers.sort(function (a, b) {
  191. return a.power*1 > b.power*1;
  192. });
  193. }
  194. // conjoin should not be a default
  195. /**
  196. * @member defaults
  197. * @inner
  198. * @private
  199. * @type {object}
  200. * @property {string} conjoin=and String to conjoin word parts together
  201. * @property {number} conjoinfloor=100 Amount above which conjoining should not occur
  202. * @property {string} separator=, String to use as separator between unit phrases
  203. */
  204. var defaults = {
  205. conjoin: "and",
  206. conjoinfloor: 100,
  207. separator: ","
  208. };
  209. var numeral = {};
  210. numeral.conjoin = options.conjoin !== undefined ? options.conjoin : defaults.conjoin;
  211. numeral.separator = options.separator !== undefined ? options.separator : defaults.separator;
  212. // pass this as a string rather than boolean
  213. numeral.hyphenatecompound = !options.hyphenatecompound ? " " : "-";
  214. var conjoinmatch;
  215. if (numeral.conjoin) {
  216. conjoinmatch = new RegExp("^" + numeral.conjoin + " ") ;
  217. }
  218. /**
  219. * @method convertMethod
  220. * @inner
  221. * @private
  222. * @param {number} num Number to be converted
  223. * @param {object} poptions Describes options for the power level
  224. * @param {function} poptions.nextmethod Method to call for remnant
  225. * @param {function} poptions.highestmethod Method to call for quotient
  226. * @param {number} poptions.floor The power’s value
  227. * @param {string} [poptions.unitspace=" "] Character to use between quotient, unit and remnant
  228. * @param {boolean} [poptions.skiponeunit=false] Whether to suppress number when the unit’s quotient is one
  229. * @param {string} [poptions.plural] Plural form of power
  230. * @param {boolean} [poptions.pluralizeunitexact] Pluralize unit only if no remnant
  231. * @param {boolean} [poptions.invariable] Unit should not be pluralized
  232. * @param {object} poptions.{{moptions.unitkind}} Unit (or alternative kind) conversion is handling
  233. * @param {object} [moptions] Specific options for the conversion
  234. * @param {object} [moptions.lookup=numerals] Object to perform numeric lookups
  235. * @param {string} [moptions.unitkind=unit] Allows a different unit kind to be specified, eg. ordinal, which allows the numeral method to be reused as is
  236. * @description Generates a string by determing the quotient for the current power unit and joining it with its remnant along with spaces, conjoins and separators as required.
  237. *
  238. * The remant is generated by calling the next method, passing it the remainder and the same moptions.
  239. *
  240. * NB. options are the options passed to the lang generating method
  241. * @return {function} Method that converts the number at that power level and then moves to the next power level
  242. */
  243. function convertMethod (num, poptions, moptions) {
  244. moptions = moptions || {};
  245. var numlookup = moptions.lookup || numerals;
  246. var numunitkind = moptions.unitkind || "unit";
  247. var nextmethod = poptions.nextmethod;
  248. var unitmethod = numeral.highestmethod;
  249. var space = options.space;
  250. if (space === undefined) {
  251. space = " ";
  252. }
  253. var unitspace = poptions.unitspace || "";
  254. var unit = poptions[numunitkind];
  255. var skiponeunit = poptions.skiponeunit;
  256. var floor = poptions.floor;
  257. var conjoinfloor = defaults.conjoinfloor;
  258. var converted = "";
  259. if (num >= floor) {
  260. var remainder = num % floor;
  261. var remnant = nextmethod(remainder, moptions);
  262. if (numeral.conjoin && remainder && remainder < conjoinfloor) {
  263. remnant = numeral.conjoin + space + remnant;
  264. }
  265. var separator = numeral.separator;
  266. if (!remnant || (conjoinmatch && remnant.match(conjoinmatch))) {
  267. separator = "";
  268. }
  269. separator += space;
  270. var unitunit = unit;
  271. var unitamount = Math.floor(num/floor);
  272. var unitvalue = unitmethod(unitamount);
  273. if (skiponeunit && unitamount === 1) {
  274. unitvalue = "";
  275. } else {
  276. if (unitamount === 1 && options.unitone) {
  277. unitvalue = options.unitone;
  278. }
  279. unitvalue += space;
  280. if (unitamount > 1 && !poptions.invariable && !moptions.invariable) {
  281. if (options.pluralize || (!remainder && (poptions.pluralizeunitexact || options.pluralizeunitexact))) {
  282. unitunit = poptions.plural || unitunit + "s";
  283. }
  284. }
  285. }
  286. var unitout = unitvalue + unitspace + unitunit;
  287. var numerallookup = unitamount * floor;
  288. if (numlookup[numerallookup+"="] && remainder === 0) {
  289. unitout = numlookup[numerallookup+"="];
  290. } else if (numlookup[numerallookup]) {
  291. unitout = numlookup[numerallookup];
  292. }
  293. converted = unitout + unitspace + separator + remnant;
  294. }
  295. else if (num) {
  296. converted = nextmethod(num, moptions);
  297. }
  298. return converted;
  299. }
  300. /**
  301. * @method convertLowest
  302. * @inner
  303. * @private
  304. * @param {number} num Number to be converted
  305. * @param {object} [options] Specific conversion options
  306. * @description Method for converting any non-powers numbers
  307. * @return {string} Converted number as string
  308. */
  309. var convertLowest = function (num, options) {
  310. options = options || {};
  311. var numlookup = options.lookup || numerals;
  312. return lowestMethod(num, numlookup, numeral);
  313. };
  314. var doubleSpaceCheck = new RegExp(space + "{2,}", "g");
  315. var endSpaceCheck = new RegExp(space + "$");
  316. /**
  317. * @method convert
  318. * @inner
  319. * @private
  320. * @param {number} num Number to be converted
  321. * @param {object} [options] Specific conversion options
  322. * @description Tries to retrieve an explicit lookup value or else invokes the highest power method
  323. * @return {string} Converted number as string
  324. */
  325. function convert (num, options) {
  326. options = options || {};
  327. var numlookup = options.lookup || numerals;
  328. if (isNaN(num)) {
  329. return numeral.notanumber || num;
  330. }
  331. num = num * 1;
  332. var converted = numlookup[num] || numeral.highestmethod(num, options);
  333. // make sure there are no double or trailing spaces
  334. converted = converted.replace(doubleSpaceCheck, space).replace(endSpaceCheck, "");
  335. return converted;
  336. }
  337. var powerlength = powers.length;
  338. var powerpos = {};
  339. /**
  340. * @method createPowerMethod
  341. * @inner
  342. * @private
  343. * @param {object} poptions Describes options for the power level
  344. * @return {function} Method that converts the number at that power level and then moves to the next power level
  345. */
  346. function createPowerMethod (poptions) {
  347. return function (num, options) {
  348. return convertMethod(num, poptions, options);
  349. };
  350. }
  351. powers.forEach(function (pow, index) {
  352. pow.floor = Math.pow(10, pow.power);
  353. if (!pow.conjoinfloor) {
  354. if (pow.conjoinpower) {
  355. pow.conjoinfloor = Math.pow(10, pow.conjoinpower);
  356. } else {
  357. pow.conjoinfloor = pow.floor/10;
  358. }
  359. }
  360. pow.nextmethod = numeral.highestmethod;
  361. if (!pow.power) {
  362. pow.method = pow.method || convertLowest;
  363. }
  364. numeral.highestmethod = pow.method || createPowerMethod(pow);
  365. });
  366. var methods = {
  367. numeral: function (num, options) {
  368. return convert(num, options);
  369. },
  370. ordinal: ordinal,
  371. ordinalAsNumber: ordinalAsNumber
  372. };
  373. return methods;
  374. }
  375. var langs = {
  376. "en-gb": new EN(),
  377. "en-us": new EN({conjoin: false, hyphenatecompound: true})
  378. };
  379. /**
  380. * @method Constructor
  381. * @static
  382. * @param {string} [lang=en-gb] Default lang
  383. * @param {object} [methods=en-gb methods] Lang methods
  384. * @description Create an instance of Wolsey
  385. *
  386. * var foo = {
  387. * numeral: function (num, options) { … },
  388. * ordinal: function (num, options) { … },
  389. * ordinalAsNumber: function (num, options) { … }
  390. * };
  391. * var cardinal = new Wolsey("foo", foo);
  392. *
  393. * Or using one of the additional supplied languages
  394. *
  395. * require("wolsey/lang/fr");
  396. * var cardinal = new Wolsey("fr", Wolsey.FR());
  397. */
  398. function Cardinal (lang, methods) {
  399. var external = {};
  400. var internal = {
  401. cache: {}
  402. };
  403. function CardinalAddLang (lang, methods) {
  404. langs[lang] = methods;
  405. CardinalSetCache(lang);
  406. }
  407. function CardinalSetDefault (lang) {
  408. if (langs[lang]) {
  409. internal.defaultlang = lang;
  410. }
  411. if (!internal.initialdefaultlang) {
  412. internal.initialdefaultlang = lang;
  413. }
  414. }
  415. function CardinalSetCache (lang) {
  416. internal.cache[lang] = {
  417. numeral: {},
  418. ordinal: {},
  419. ordinalAsNumber: {}
  420. };
  421. }
  422. for (var clang in langs) {
  423. CardinalSetCache(clang);
  424. }
  425. if (lang && methods) {
  426. CardinalAddLang(lang, methods);
  427. }
  428. CardinalSetDefault(lang || "en-gb");
  429. /**
  430. * @method genericMethod
  431. * @inner
  432. * @param {string} method Method to be invoked
  433. * @param {number} num Number to be converted
  434. * @param {object} options Options for the method
  435. * @return {string} converted number
  436. */
  437. function genericMethod (method, num, options) {
  438. options = options || {};
  439. var lang = options.lang || internal.defaultlang;
  440. var cachekey = num + JSON.stringify(options);
  441. if (internal.cache[lang][method][cachekey]) {
  442. return internal.cache[lang][method][cachekey];
  443. }
  444. var computed = langs[lang][method](num, options);
  445. internal.cache[lang][method][cachekey] = computed;
  446. return computed;
  447. }
  448. /**
  449. * @method numeral
  450. * @instance
  451. * @param {number} num
  452. * @param {object} options
  453. * @description Calls {@link module:wolsey~genericMethod}
  454. * @return {string} numeral
  455. */
  456. external.numeral = function CardinalNumeral (num, options) {
  457. return genericMethod("numeral", num, options);
  458. };
  459. /**
  460. * @method ordinal
  461. * @instance
  462. * @param {number} num
  463. * @param {object} options
  464. * @description Calls {@link module:wolsey~genericMethod}
  465. * @return {string} ordinal
  466. */
  467. external.ordinal = function CardinalOrdinal (num, options) {
  468. return genericMethod("ordinal", num, options);
  469. };
  470. /**
  471. * @method ordinalAsNumber
  472. * @instance
  473. * @param {number} num
  474. * @param {object} options
  475. * @description Calls {@link module:wolsey~genericMethod}
  476. * @return {string} ordinalAsNUmber
  477. */
  478. external.ordinalAsNumber = function CardinalOrdinalAsNumber (num, options) {
  479. return genericMethod("ordinalAsNumber", num, options);
  480. };
  481. /**
  482. * @method addLang
  483. * @instance
  484. * @param {string} lang
  485. * @param {object} methods
  486. * @description Adds language to Wolsey instance
  487. */
  488. external.addLang = CardinalAddLang;
  489. /**
  490. * @method setDefault
  491. * @instance
  492. * @param {string} lang
  493. * @description Sets language as Wolsey instance’s default
  494. */
  495. external.setDefault = CardinalSetDefault;
  496. /**
  497. * @method resetDefault
  498. * @instance
  499. * @description Resets default language to Wolsey instance’s original default
  500. */
  501. external.resetDefault = function CardinalResetDefault () {
  502. internal.defaultlang = internal.initialdefaultlang;
  503. };
  504. return external;
  505. }
  506. Cardinal.util = {
  507. superscriptOridnal: function CardinalUtilSuperscriptOridnal (str, options) {
  508. options = options || {};
  509. if (options.html) {
  510. str = str.replace(/([^0-9\.]+)$/, "<sup>$1</sup>");
  511. }
  512. return str;
  513. }
  514. };
  515. // Expose EN and LANG to world
  516. Cardinal.EN = EN;
  517. Cardinal.LANG = LANG;
  518. return Cardinal;
  519. }));