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