accounting.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. /*!
  2. * accounting.js v0.4.1
  3. * Copyright 2014 Open Exchange Rates
  4. *
  5. * Freely distributable under the MIT license.
  6. * Portions of accounting.js are inspired or borrowed from underscore.js
  7. *
  8. * Full details and documentation:
  9. * http://openexchangerates.github.io/accounting.js/
  10. */
  11. (function(root, undefined) {
  12. /* --- Setup --- */
  13. // Create the local library object, to be exported or referenced globally later
  14. var lib = {};
  15. // Current version
  16. lib.version = '0.4.1';
  17. /* --- Exposed settings --- */
  18. // The library's settings configuration object. Contains default parameters for
  19. // currency and number formatting
  20. lib.settings = {
  21. currency: {
  22. symbol : "$", // default currency symbol is '$'
  23. format : "%s%v", // controls output: %s = symbol, %v = value (can be object, see docs)
  24. decimal : ".", // decimal point separator
  25. thousand : ",", // thousands separator
  26. precision : 2, // decimal places
  27. grouping : 3 // digit grouping (not implemented yet)
  28. },
  29. number: {
  30. precision : 0, // default precision on numbers is 0
  31. grouping : 3, // digit grouping (not implemented yet)
  32. thousand : ",",
  33. decimal : "."
  34. }
  35. };
  36. /* --- Internal Helper Methods --- */
  37. // Store reference to possibly-available ECMAScript 5 methods for later
  38. var nativeMap = Array.prototype.map,
  39. nativeIsArray = Array.isArray,
  40. toString = Object.prototype.toString;
  41. /**
  42. * Tests whether supplied parameter is a string
  43. * from underscore.js
  44. */
  45. function isString(obj) {
  46. return !!(obj === '' || (obj && obj.charCodeAt && obj.substr));
  47. }
  48. /**
  49. * Tests whether supplied parameter is a string
  50. * from underscore.js, delegates to ECMA5's native Array.isArray
  51. */
  52. function isArray(obj) {
  53. return nativeIsArray ? nativeIsArray(obj) : toString.call(obj) === '[object Array]';
  54. }
  55. /**
  56. * Tests whether supplied parameter is a true object
  57. */
  58. function isObject(obj) {
  59. return obj && toString.call(obj) === '[object Object]';
  60. }
  61. /**
  62. * Extends an object with a defaults object, similar to underscore's _.defaults
  63. *
  64. * Used for abstracting parameter handling from API methods
  65. */
  66. function defaults(object, defs) {
  67. var key;
  68. object = object || {};
  69. defs = defs || {};
  70. // Iterate over object non-prototype properties:
  71. for (key in defs) {
  72. if (defs.hasOwnProperty(key)) {
  73. // Replace values with defaults only if undefined (allow empty/zero values):
  74. if (object[key] == null) object[key] = defs[key];
  75. }
  76. }
  77. return object;
  78. }
  79. /**
  80. * Implementation of `Array.map()` for iteration loops
  81. *
  82. * Returns a new Array as a result of calling `iterator` on each array value.
  83. * Defers to native Array.map if available
  84. */
  85. function map(obj, iterator, context) {
  86. var results = [], i, j;
  87. if (!obj) return results;
  88. // Use native .map method if it exists:
  89. if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
  90. // Fallback for native .map:
  91. for (i = 0, j = obj.length; i < j; i++ ) {
  92. results[i] = iterator.call(context, obj[i], i, obj);
  93. }
  94. return results;
  95. }
  96. /**
  97. * Check and normalise the value of precision (must be positive integer)
  98. */
  99. function checkPrecision(val, base) {
  100. val = Math.round(Math.abs(val));
  101. return isNaN(val)? base : val;
  102. }
  103. /**
  104. * Parses a format string or object and returns format obj for use in rendering
  105. *
  106. * `format` is either a string with the default (positive) format, or object
  107. * containing `pos` (required), `neg` and `zero` values (or a function returning
  108. * either a string or object)
  109. *
  110. * Either string or format.pos must contain "%v" (value) to be valid
  111. */
  112. function checkCurrencyFormat(format) {
  113. var defaults = lib.settings.currency.format;
  114. // Allow function as format parameter (should return string or object):
  115. if ( typeof format === "function" ) format = format();
  116. // Format can be a string, in which case `value` ("%v") must be present:
  117. if ( isString( format ) && format.match("%v") ) {
  118. // Create and return positive, negative and zero formats:
  119. return {
  120. pos : format,
  121. neg : format.replace("-", "").replace("%v", "-%v"),
  122. zero : format
  123. };
  124. // If no format, or object is missing valid positive value, use defaults:
  125. } else if ( !format || !format.pos || !format.pos.match("%v") ) {
  126. // If defaults is a string, casts it to an object for faster checking next time:
  127. return ( !isString( defaults ) ) ? defaults : lib.settings.currency.format = {
  128. pos : defaults,
  129. neg : defaults.replace("%v", "-%v"),
  130. zero : defaults
  131. };
  132. }
  133. // Otherwise, assume format was fine:
  134. return format;
  135. }
  136. /* --- API Methods --- */
  137. /**
  138. * Takes a string/array of strings, removes all formatting/cruft and returns the raw float value
  139. * Alias: `accounting.parse(string)`
  140. *
  141. * Decimal must be included in the regular expression to match floats (defaults to
  142. * accounting.settings.number.decimal), so if the number uses a non-standard decimal
  143. * separator, provide it as the second argument.
  144. *
  145. * Also matches bracketed negatives (eg. "$ (1.99)" => -1.99)
  146. *
  147. * Doesn't throw any errors (`NaN`s become 0) but this may change in future
  148. */
  149. var unformat = lib.unformat = lib.parse = function(value, decimal) {
  150. // Recursively unformat arrays:
  151. if (isArray(value)) {
  152. return map(value, function(val) {
  153. return unformat(val, decimal);
  154. });
  155. }
  156. // Fails silently (need decent errors):
  157. value = value || 0;
  158. // Return the value as-is if it's already a number:
  159. if (typeof value === "number") return value;
  160. // Default decimal point comes from settings, but could be set to eg. "," in opts:
  161. decimal = decimal || lib.settings.number.decimal;
  162. // Build regex to strip out everything except digits, decimal point and minus sign:
  163. var regex = new RegExp("[^0-9-" + decimal + "]", ["g"]),
  164. unformatted = parseFloat(
  165. ("" + value)
  166. .replace(/\((.*)\)/, "-$1") // replace bracketed values with negatives
  167. .replace(regex, '') // strip out any cruft
  168. .replace(decimal, '.') // make sure decimal point is standard
  169. );
  170. // This will fail silently which may cause trouble, let's wait and see:
  171. return !isNaN(unformatted) ? unformatted : 0;
  172. };
  173. /**
  174. * Implementation of toFixed() that treats floats more like decimals
  175. *
  176. * Fixes binary rounding issues (eg. (0.615).toFixed(2) === "0.61") that present
  177. * problems for accounting- and finance-related software.
  178. */
  179. var toFixed = lib.toFixed = function(value, precision) {
  180. precision = checkPrecision(precision, lib.settings.number.precision);
  181. var power = Math.pow(10, precision);
  182. // Multiply up by precision, round accurately, then divide and use native toFixed():
  183. return (Math.round(lib.unformat(value) * power) / power).toFixed(precision);
  184. };
  185. /**
  186. * Format a number, with comma-separated thousands and custom precision/decimal places
  187. * Alias: `accounting.format()`
  188. *
  189. * Localise by overriding the precision and thousand / decimal separators
  190. * 2nd parameter `precision` can be an object matching `settings.number`
  191. */
  192. var formatNumber = lib.formatNumber = lib.format = function(number, precision, thousand, decimal) {
  193. // Resursively format arrays:
  194. if (isArray(number)) {
  195. return map(number, function(val) {
  196. return formatNumber(val, precision, thousand, decimal);
  197. });
  198. }
  199. // Clean up number:
  200. number = unformat(number);
  201. // Build options object from second param (if object) or all params, extending defaults:
  202. var opts = defaults(
  203. (isObject(precision) ? precision : {
  204. precision : precision,
  205. thousand : thousand,
  206. decimal : decimal
  207. }),
  208. lib.settings.number
  209. ),
  210. // Clean up precision
  211. usePrecision = checkPrecision(opts.precision),
  212. // Do some calc:
  213. negative = number < 0 ? "-" : "",
  214. base = parseInt(toFixed(Math.abs(number || 0), usePrecision), 10) + "",
  215. mod = base.length > 3 ? base.length % 3 : 0;
  216. // Format the number:
  217. return negative + (mod ? base.substr(0, mod) + opts.thousand : "") + base.substr(mod).replace(/(\d{3})(?=\d)/g, "$1" + opts.thousand) + (usePrecision ? opts.decimal + toFixed(Math.abs(number), usePrecision).split('.')[1] : "");
  218. };
  219. /**
  220. * Format a number into currency
  221. *
  222. * Usage: accounting.formatMoney(number, symbol, precision, thousandsSep, decimalSep, format)
  223. * defaults: (0, "$", 2, ",", ".", "%s%v")
  224. *
  225. * Localise by overriding the symbol, precision, thousand / decimal separators and format
  226. * Second param can be an object matching `settings.currency` which is the easiest way.
  227. *
  228. * To do: tidy up the parameters
  229. */
  230. var formatMoney = lib.formatMoney = function(number, symbol, precision, thousand, decimal, format) {
  231. // Resursively format arrays:
  232. if (isArray(number)) {
  233. return map(number, function(val){
  234. return formatMoney(val, symbol, precision, thousand, decimal, format);
  235. });
  236. }
  237. // Clean up number:
  238. number = unformat(number);
  239. // Build options object from second param (if object) or all params, extending defaults:
  240. var opts = defaults(
  241. (isObject(symbol) ? symbol : {
  242. symbol : symbol,
  243. precision : precision,
  244. thousand : thousand,
  245. decimal : decimal,
  246. format : format
  247. }),
  248. lib.settings.currency
  249. ),
  250. // Check format (returns object with pos, neg and zero):
  251. formats = checkCurrencyFormat(opts.format),
  252. // Choose which format to use for this value:
  253. useFormat = number > 0 ? formats.pos : number < 0 ? formats.neg : formats.zero;
  254. // Return with currency symbol added:
  255. return useFormat.replace('%s', opts.symbol).replace('%v', formatNumber(Math.abs(number), checkPrecision(opts.precision), opts.thousand, opts.decimal));
  256. };
  257. /**
  258. * Format a list of numbers into an accounting column, padding with whitespace
  259. * to line up currency symbols, thousand separators and decimals places
  260. *
  261. * List should be an array of numbers
  262. * Second parameter can be an object containing keys that match the params
  263. *
  264. * Returns array of accouting-formatted number strings of same length
  265. *
  266. * NB: `white-space:pre` CSS rule is required on the list container to prevent
  267. * browsers from collapsing the whitespace in the output strings.
  268. */
  269. lib.formatColumn = function(list, symbol, precision, thousand, decimal, format) {
  270. if (!list) return [];
  271. // Build options object from second param (if object) or all params, extending defaults:
  272. var opts = defaults(
  273. (isObject(symbol) ? symbol : {
  274. symbol : symbol,
  275. precision : precision,
  276. thousand : thousand,
  277. decimal : decimal,
  278. format : format
  279. }),
  280. lib.settings.currency
  281. ),
  282. // Check format (returns object with pos, neg and zero), only need pos for now:
  283. formats = checkCurrencyFormat(opts.format),
  284. // Whether to pad at start of string or after currency symbol:
  285. padAfterSymbol = formats.pos.indexOf("%s") < formats.pos.indexOf("%v") ? true : false,
  286. // Store value for the length of the longest string in the column:
  287. maxLength = 0,
  288. // Format the list according to options, store the length of the longest string:
  289. formatted = map(list, function(val, i) {
  290. if (isArray(val)) {
  291. // Recursively format columns if list is a multi-dimensional array:
  292. return lib.formatColumn(val, opts);
  293. } else {
  294. // Clean up the value
  295. val = unformat(val);
  296. // Choose which format to use for this value (pos, neg or zero):
  297. var useFormat = val > 0 ? formats.pos : val < 0 ? formats.neg : formats.zero,
  298. // Format this value, push into formatted list and save the length:
  299. fVal = useFormat.replace('%s', opts.symbol).replace('%v', formatNumber(Math.abs(val), checkPrecision(opts.precision), opts.thousand, opts.decimal));
  300. if (fVal.length > maxLength) maxLength = fVal.length;
  301. return fVal;
  302. }
  303. });
  304. // Pad each number in the list and send back the column of numbers:
  305. return map(formatted, function(val, i) {
  306. // Only if this is a string (not a nested array, which would have already been padded):
  307. if (isString(val) && val.length < maxLength) {
  308. // Depending on symbol position, pad after symbol or at index 0:
  309. return padAfterSymbol ? val.replace(opts.symbol, opts.symbol+(new Array(maxLength - val.length + 1).join(" "))) : (new Array(maxLength - val.length + 1).join(" ")) + val;
  310. }
  311. return val;
  312. });
  313. };
  314. /* --- Module Definition --- */
  315. // Export accounting for CommonJS. If being loaded as an AMD module, define it as such.
  316. // Otherwise, just add `accounting` to the global object
  317. if (typeof exports !== 'undefined') {
  318. if (typeof module !== 'undefined' && module.exports) {
  319. exports = module.exports = lib;
  320. }
  321. exports.accounting = lib;
  322. } else if (typeof define === 'function' && define.amd) {
  323. // Return the library as an AMD module:
  324. define([], function() {
  325. return lib;
  326. });
  327. } else {
  328. // Use accounting.noConflict to restore `accounting` back to its original value.
  329. // Returns a reference to the library's `accounting` object;
  330. // e.g. `var numbers = accounting.noConflict();`
  331. lib.noConflict = (function(oldAccounting) {
  332. return function() {
  333. // Reset the value of the root's `accounting` variable:
  334. root.accounting = oldAccounting;
  335. // Delete the noConflict method:
  336. lib.noConflict = undefined;
  337. // Return reference to the library to re-assign it:
  338. return lib;
  339. };
  340. })(root.accounting);
  341. // Declare `fx` on the root (global/window) object:
  342. root['accounting'] = lib;
  343. }
  344. // Root will be `window` in browser or `global` on the server:
  345. }(this));