paypal-invoice
Version:
PayPal Invoicing for Node.js
1,449 lines (1,200 loc) • 80.6 kB
JavaScript
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.Invoice=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
module.exports = _dereq_('./lib/invoice');
},{"./lib/invoice":4}],2:[function(_dereq_,module,exports){
'use strict';
var InvoiceContactInfo = function (details) {
var t = this;
details = details || {};
'firstName lastName businessName phoneNumber faxNumber website customValue'.split(' ').forEach(function (p) {
if (details[p]) {
t[p] = details[p];
}
});
if (details.address) {
t.address = {};
'country line1 line2 city state postalCode'.split(' ').forEach(function (p) {
t.address[p] = details.address[p];
});
}
};
module.exports = InvoiceContactInfo;
},{}],3:[function(_dereq_,module,exports){
'use strict';
var BigNum = _dereq_('bignumber.js');
var currencyProperties = {
USD: {
symbol: '$',
decimals: 2,
round: BigNum.ROUND_HALF_UP,
iso4217: 840
},
GBP: {
symbol: '£',
decimals: 2,
round: BigNum.ROUND_HALF_UP,
iso4217: 826
},
AUD: {
symbol: '$',
decimals: 2,
round: BigNum.ROUND_HALF_UP,
iso4217: 36
}
};
module.exports = {
round: function (currency, amountAsBigNum) {
return new BigNum(amountAsBigNum.round(currencyProperties[currency].decimals,
currencyProperties[currency].round));
},
isSupported: function (currency) {
return currencyProperties.hasOwnProperty(currency);
},
Number: BigNum,
newNumber: function (v) {
return new BigNum(v);
},
toCents: function (currency, amount) {
var decimals = currencyProperties[currency].decimals;
return new BigNum(10).pow(decimals).times(amount);
},
properties: currencyProperties
};
},{"bignumber.js":6}],4:[function(_dereq_,module,exports){
'use strict';
var currency = _dereq_('./currency');
var $$ = currency.newNumber;
var InvoiceContactInfo = _dereq_('./contactInfo');
var InvoiceTotals = _dereq_('./totals');
var ZERO = $$(0);
var ONE = $$(1);
var Invoice = function (currencyOrDetails) {
if (!currencyOrDetails || typeof(currencyOrDetails) === 'string') {
this.initWithCurrency(currencyOrDetails);
} else {
this.initWithDetails(currencyOrDetails);
}
};
Invoice.STATUS = {
CANCELLED: 'cancelled',
CANCELED: 'cancelled',
DRAFT: 'draft',
PAYMENT_PENDING: 'payment_pending'
};
Invoice.Number = currency.newNumber;
Invoice.Currencies = currency.properties;
Invoice.Item = function (quantity, unitPrice, itemId, detailId) {
if (!quantity) {
throw new Error('You must specify a quantity when adding an item.');
}
this.quantity = $$(quantity);
this.unitPrice = $$(unitPrice||0);
this.itemId = itemId;
this.detailId = detailId || null;
// Other properties: name, description, imageUrl, taxRate, taxName
// Not sure how to tell WebStorm/JS about these w/o making them null and showing up in JSON unnecessarily
};
Invoice.Item.prototype = {
totalForInvoice: function (inv) {
var itemPrice = this.quantity.times(this.unitPrice || ZERO);
if (this.discountPercentage) {
var discount = currency.round(inv.currencyCode, itemPrice.times(this.discountPercentage));
itemPrice = itemPrice.minus(discount);
} else if (this.discountAmount) {
itemPrice = itemPrice.minus(this.discountAmount);
}
return currency.round(inv.currencyCode, itemPrice);
}
};
Invoice.prototype = {
initWithCurrency: function (currencyCode) {
this.currencyCode = currencyCode || 'USD';
if (!currency.isSupported(this.currencyCode)) {
throw new Error('Unsupported currency: ' + this.currencyCode);
}
this.payerEmail = 'noreply@here.paypal.com';
this.paymentTerms = 'DueOnReceipt';
this.taxInclusive = false;
this.taxCalculatedAfterDiscount = true;
this.merchantInfo = new InvoiceContactInfo();
this.billingInfo = new InvoiceContactInfo();
this.shippingInfo = new InvoiceContactInfo();
this.items = [];
this.status = Invoice.STATUS.DRAFT;
},
initWithDetails: function (json) {
this.currencyCode = json.currencyCode;
if(!currency.isSupported(this.currencyCode)) {
throw new Error('Unsupported currency: ' + this.currencyCode);
}
this.taxInclusive = json.taxInclusive ? true : false;
this.taxCalculatedAfterDiscount = json.taxCalculatedAfterDiscount ? true : false;
var t = this;
'status payerEmail number invoiceId paymentTerms referrerCode receiptDetails merchantMemo shippingAmount shippingTaxName customAmountName customAmountValue discountAmount discountPercentage gratuityAmount'
.split(' ').forEach(function (p) {
if (json[p]) {
t[p] = json[p];
}
});
if (json.shippingTaxRate) {
this.shippingTaxRate = $$($$(json.shippingTaxRate).dividedBy(100));
}
this.readMetadata(json);
this.readItems(json.items);
},
readMetadata: function (json) {
this.merchantInfo = new InvoiceContactInfo(json.merchantInfo);
this.billingInfo = new InvoiceContactInfo();
this.shippingInfo = new InvoiceContactInfo();
},
readItems: function (itemsJson) {
this.items = [];
var items = this.items;
if (itemsJson) {
itemsJson.forEach(function (itemJson) {
// Item ids are not stored on the server now so we just make one up
var item = new Invoice.Item(itemJson.quantity, itemJson.unitPrice, itemJson.itemId || ('ItemId'+new Date().getTime()));
item.name = itemJson.name;
item.description = itemJson.description;
if (itemJson.taxRate) {
item.taxName = itemJson.taxName;
item.taxRate = $$(itemJson.taxRate);
}
item.imageUrl = itemJson.imageUrl;
if (itemJson.discountAmount) {
item.discountAmount = itemJson.discountAmount;
}
if (itemJson.discountPercentage) {
item.discountPercentage = itemJson.discountPercentage;
}
items.push(item);
});
}
},
calculate: function () {
return new InvoiceTotals(this);
},
findItem: function (itemOrItemId, detailId) {
if (typeof(itemOrItemId) !== 'string') {
detailId = itemOrItemId.detailId;
itemOrItemId = itemOrItemId.itemId;
}
for (var i = 0, len = this.items.length; i < len; i++) {
var candidate = this.items[i];
if (candidate.itemId === itemOrItemId && candidate.detailId === detailId) {
return candidate;
}
}
return null;
},
addItem: function (item) {
var existing = this.findItem(item);
if (existing) {
existing.quantity = existing.quantity.plus(item.quantity);
return existing;
} else {
if (item.taxRate && !(item.taxRate instanceof currency.Number)) {
item.taxRate = $$(item.taxRate);
}
this.items.push(item);
return item;
}
},
_sortedInvoiceItems: function () {
// Sort the items so that the highest tax rate items are first just so we have some logic to the application
// of pre-sales tax discounts.
var copy = this.items.slice(0); // equivalent to array copy
copy.sort(function (a, b) {
if (!a.taxRate) {
if (!b.taxRate) {
return compare(a.name, b.name);
}
// a should go first
return 1;
} else {
if (!b.taxRate) {
// b should go first
return -1;
}
if (b.taxRate.equals(a.taxRate)) {
return compare(a.name, b.name);
}
return -1 * a.taxRate.comparedTo(b.taxRate);
}
});
return copy;
}
};
function compare(a, b) {
if (a === b) {
return 0;
} else if (a < b) {
return -1;
}
return 1;
}
module.exports = Invoice;
},{"./contactInfo":2,"./currency":3,"./totals":5}],5:[function(_dereq_,module,exports){
'use strict';
var currency = _dereq_('./currency');
var $$ = currency.newNumber;
var ZERO = $$(0);
var ONE = $$(1);
function nonZero(amt) {
return amt && !ZERO.equals(amt);
}
function valOrZero(amt) {
return amt || ZERO;
}
var InvoiceTotals = function (invoice) {
this.invoice = invoice;
this.shippingTax = ZERO;
this.itemSubTotal = ZERO;
this.itemTax = ZERO;
this.discountTotal = ZERO;
this.refundTotal = ZERO;
var sortedItems = invoice._sortedInvoiceItems();
this.itemSubTotal = this.calculateItemSubtotals(sortedItems);
if (invoice.taxCalculatedAfterDiscount || invoice.taxInclusive) {
this.discountTotal = this.calculatePreTaxDiscountForItemSubtotal(this.itemSubTotal);
if (invoice.taxCalculatedAfterDiscount) {
this.taxes = this.calculateTaxesForItemsWithPreTaxDiscountTotal(sortedItems, this.discountTotal, this.itemSubTotal);
} else {
this.taxes = this.calculateTaxesForItemsWithDiscountAfterTax(sortedItems);
}
this.shippingTax = this.calculateShippingTax(this.taxes);
this.itemTax = this.calculateTotalTaxFromTaxes(this.taxes);
this.itemTax = $$(this.itemTax.minus(this.shippingTax));
this.taxes = this.generateRoundedTaxDetailsFromTaxes(this.taxes);
} else {
this.taxes = this.calculateTaxesForItemsWithDiscountAfterTax(sortedItems);
this.shippingTax = this.calculateShippingTax(this.taxes);
this.itemTax = this.calculateTotalTaxFromTaxes(this.taxes);
// MPTODO: This is possibly a rounding issue.
this.itemTax = $$(this.itemTax.minus(this.shippingTax));
this.taxes = this.generateRoundedTaxDetailsFromTaxes(this.taxes);
this.discountTotal = this.calculateDiscountWithItemSubTotal(this.itemSubTotal, this.itemTax);
}
this.setDerivedTotals();
};
InvoiceTotals.prototype = {
_round: function (amt) {
return currency.round(this.invoice.currencyCode, amt);
},
toCents: function (amt) {
return currency.toCents(this.invoice.currencyCode, amt);
},
generateRoundedTaxDetailsFromTaxes: function (taxes) {
var roundedTaxDetails = {};
for (var key in taxes) {
roundedTaxDetails[key] = this._round(taxes[key]);
}
return roundedTaxDetails;
},
calculateTotalTaxFromTaxes: function (taxes) {
var itemTax = ZERO;
for (var key in taxes) {
itemTax = itemTax.plus(taxes[key]);
}
return itemTax;
},
calculateTaxesForItemsWithPreTaxDiscountTotal: function (items, discountTotal, itemSubTotal) {
var taxes = {}, self = this;
items.forEach(function (i) {
var itemDiscount = self.calculateItemDiscount(i, discountTotal, itemSubTotal);
var itemContribution = ZERO;
if (nonZero(i.unitPrice)) {
itemContribution = self.calculateItemPrice(i);
}
if (nonZero(i.taxRate)) {
var key = [i.taxName, ' (', i.taxRate.toString(), '%)'].join('');
// Rounding is complicated. We have to match invoicing, and invoicing rounds per line item.
var taxAmount;
if (self.invoice.taxInclusive) {
var taxSub = itemContribution.minus(itemDiscount);
taxAmount = self._round(taxSub.dividedBy(ONE.plus(i.taxRate.dividedBy(100))));
} else {
taxAmount = self._round(itemContribution.minus(itemDiscount).times(i.taxRate.dividedBy(100)));
}
if (taxAmount && !ZERO.equals(taxAmount)) {
taxes[key] = (taxes[key] || ZERO).plus(taxAmount);
}
}
});
return taxes;
},
calculateItemDiscount: function (i, discountTotal, itemSubTotal) {
if (nonZero(discountTotal)) {
var itemTotal = this.calculateItemPrice(i);
if (nonZero(itemSubTotal)) {
if (this.discountPercentage && !ZERO.equals(this.discountPercentage)) {
return itemTotal.times(this.discountPercentage);
} else {
// This may still be wrong. We're basically reverse engineering the discount percentage for this item
return itemTotal.dividedBy(itemSubTotal).times(discountTotal);
}
}
}
return ZERO;
},
calculateTaxesForItemsWithDiscountAfterTax: function (items) {
var taxes = {}, self = this;
items.forEach(function (i) {
var itemContribution = ZERO;
if (nonZero(i.unitPrice)) {
// In this mode (discounts after tax), even line item discounts don't matter for tax calculations
itemContribution = currency.round(self.invoice.currencyCode, i.quantity.times(i.unitPrice));
}
if (nonZero(i.taxRate)) {
var key = [i.taxName, ' (', i.taxRate.toString(), '%)'].join('');
var taxAmount;
if (self.invoice.taxInclusive) {
var taxSub = itemContribution.dividedBy(ONE.plus(i.taxRate.dividedBy(100)));
taxAmount = self._round(itemContribution.minus(taxSub));
} else {
taxAmount = self._round(itemContribution.times(i.taxRate.dividedBy(100)));
}
if (taxAmount && !ZERO.equals(taxAmount)) {
taxes[key] = (taxes[key] || ZERO).plus(taxAmount);
}
}
});
return taxes;
},
calculateShippingTax: function (taxes) {
if (this.shippingTaxRate && this.shippingAmount && !ZERO.equals(this.shippingTaxRate) && !ZERO.equals(this.shippingAmount)) {
// Rounding is complicated. For best results, we're going to round at the "tax class" level.
// If you have one tax rate, this means "order level." If you have as many tax rates as line items, this means line level.
// The advantage is with this rounding I can display valid tax per tax rate and have consistent totals.
var key = [this.shippingTaxName, ' (', this.shippingTaxRate.toString(), '%)'].join('');
var shippingTax = this.shippingAmount.times(this.shippingTaxRate.dividedBy(100));
if (nonZero(shippingTax)) {
taxes[key] = (taxes[key] || ZERO).plus(shippingTax);
}
return shippingTax;
}
return ZERO;
},
calculateItemSubtotals: function (items) {
var subtotal = ZERO;
for (var i = 0, len = items.length; i < len; i++) {
subtotal = subtotal.plus(this.calculateItemPrice(items[i]));
}
return $$(subtotal);
},
calculateDiscountWithItemSubTotal: function (itemSubTotal, itemTax) {
if (nonZero(this.invoice.discountAmount)) {
return $$(this.discountAmount);
} else if (nonZero(this.invoice.discountPercentage)) {
return currency.round(this.currencyCode, itemSubTotal.plus(itemTax).times(this.discountPercentage));
}
return ZERO;
},
calculatePreTaxDiscountForItemSubtotal: function (subtotal) {
if (nonZero(this.invoice.discountAmount)) {
return $$(this.invoice.discountAmount);
} else if (nonZero(this.invoice.discountPercentage)) {
return currency.round(this.currencyCode, $$(this.invoice.discountPercentage).times(subtotal));
}
return ZERO;
},
calculateItemPrice: function (item) {
var itemPrice = item.quantity.times(item.unitPrice || ZERO);
if (item.discountPercentage) {
var discount = currency.round(this.invoice.currencyCode, itemPrice.times(item.discountPercentage));
itemPrice = itemPrice.minus(discount);
} else if (item.discountAmount) {
itemPrice = itemPrice.minus(item.discountAmount);
}
return this._round(itemPrice);
},
/**
* Once the component values of the total have been calculated, this is used to set
* various derived totals such as the grand total. Rather than using properties, which
* in theory won't work on all browsers, we just spend the cycles to do various additions
* every time the invoice is recalculated.
*/
setDerivedTotals: function () {
this.total = valOrZero(this.itemSubTotal)
.plus(valOrZero(this.invoice.gratuityAmount))
.plus(valOrZero(this.invoice.shippingAmount));
if (!this.invoice.taxInclusive) {
this.total = this.total.plus(valOrZero(this.itemTax))
.plus(valOrZero(this.shippingTax));
}
if (nonZero(this.discountTotal)) {
this.total = this.total.minus(this.discountTotal);
}
this.total = $$(this.total);
},
toJSON: function () {
var stringify = {};
for (var s in this) {
if (this.hasOwnProperty(s) && s !== 'invoice') {
stringify[s] = this[s];
}
}
return stringify;
}
};
module.exports = InvoiceTotals;
},{"./currency":3}],6:[function(_dereq_,module,exports){
/*! bignumber.js v1.5.0 https://github.com/MikeMcl/bignumber.js/LICENCE */
;(function ( global ) {
'use strict';
/*
bignumber.js v1.5.0
A JavaScript library for arbitrary-precision arithmetic.
https://github.com/MikeMcl/bignumber.js
Copyright (c) 2012 Michael Mclaughlin <M8ch88l@gmail.com>
MIT Expat Licence
*/
/*********************************** DEFAULTS ************************************/
/*
* The default values below must be integers within the stated ranges (inclusive).
* Most of these values can be changed during run-time using BigNumber.config().
*/
/*
* The limit on the value of DECIMAL_PLACES, TO_EXP_NEG, TO_EXP_POS, MIN_EXP,
* MAX_EXP, and the argument to toFixed, toPrecision and toExponential, beyond
* which an exception is thrown (if ERRORS is true).
*/
var MAX = 1E9, // 0 to 1e+9
// Limit of magnitude of exponent argument to toPower.
MAX_POWER = 1E6, // 1 to 1e+6
// The maximum number of decimal places for operations involving division.
DECIMAL_PLACES = 20, // 0 to MAX
/*
* The rounding mode used when rounding to the above decimal places, and when
* using toFixed, toPrecision and toExponential, and round (default value).
* UP 0 Away from zero.
* DOWN 1 Towards zero.
* CEIL 2 Towards +Infinity.
* FLOOR 3 Towards -Infinity.
* HALF_UP 4 Towards nearest neighbour. If equidistant, up.
* HALF_DOWN 5 Towards nearest neighbour. If equidistant, down.
* HALF_EVEN 6 Towards nearest neighbour. If equidistant, towards even neighbour.
* HALF_CEIL 7 Towards nearest neighbour. If equidistant, towards +Infinity.
* HALF_FLOOR 8 Towards nearest neighbour. If equidistant, towards -Infinity.
*/
ROUNDING_MODE = 4, // 0 to 8
// EXPONENTIAL_AT : [TO_EXP_NEG , TO_EXP_POS]
// The exponent value at and beneath which toString returns exponential notation.
// Number type: -7
TO_EXP_NEG = -7, // 0 to -MAX
// The exponent value at and above which toString returns exponential notation.
// Number type: 21
TO_EXP_POS = 21, // 0 to MAX
// RANGE : [MIN_EXP, MAX_EXP]
// The minimum exponent value, beneath which underflow to zero occurs.
// Number type: -324 (5e-324)
MIN_EXP = -MAX, // -1 to -MAX
// The maximum exponent value, above which overflow to Infinity occurs.
// Number type: 308 (1.7976931348623157e+308)
MAX_EXP = MAX, // 1 to MAX
// Whether BigNumber Errors are ever thrown.
// CHANGE parseInt to parseFloat if changing ERRORS to false.
ERRORS = true, // true or false
parse = parseInt, // parseInt or parseFloat
/***********************************************************************************/
P = BigNumber.prototype,
DIGITS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_',
outOfRange,
id = 0,
isValid = /^-?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i,
trim = String.prototype.trim || function () {return this.replace(/^\s+|\s+$/g, '')},
ONE = BigNumber(1);
// CONSTRUCTOR
/*
* The exported function.
* Create and return a new instance of a BigNumber object.
*
* n {number|string|BigNumber} A numeric value.
* [b] {number} The base of n. Integer, 2 to 64 inclusive.
*/
function BigNumber( n, b ) {
var e, i, isNum, digits, valid, orig,
x = this;
// Enable constructor usage without new.
if ( !(x instanceof BigNumber) ) {
return new BigNumber( n, b )
}
// Duplicate.
if ( n instanceof BigNumber ) {
id = 0;
// e is undefined.
if ( b !== e ) {
n += ''
} else {
x['s'] = n['s'];
x['e'] = n['e'];
x['c'] = ( n = n['c'] ) ? n.slice() : n;
return;
}
}
// If number, check if minus zero.
if ( typeof n != 'string' ) {
n = ( isNum = typeof n == 'number' ||
Object.prototype.toString.call(n) == '[object Number]' ) &&
n === 0 && 1 / n < 0 ? '-0' : n + '';
}
orig = n;
if ( b === e && isValid.test(n) ) {
// Determine sign.
x['s'] = n.charAt(0) == '-' ? ( n = n.slice(1), -1 ) : 1;
// Either n is not a valid BigNumber or a base has been specified.
} else {
// Enable exponential notation to be used with base 10 argument.
// Ensure return value is rounded to DECIMAL_PLACES as with other bases.
if ( b == 10 ) {
return setMode( n, DECIMAL_PLACES, ROUNDING_MODE );
}
n = trim.call(n).replace( /^\+(?!-)/, '' );
x['s'] = n.charAt(0) == '-' ? ( n = n.replace( /^-(?!-)/, '' ), -1 ) : 1;
if ( b != null ) {
if ( ( b == (b | 0) || !ERRORS ) &&
!( outOfRange = !( b >= 2 && b < 65 ) ) ) {
digits = '[' + DIGITS.slice( 0, b = b | 0 ) + ']+';
// Before non-decimal number validity test and base conversion
// remove the `.` from e.g. '1.', and replace e.g. '.1' with '0.1'.
n = n.replace( /\.$/, '' ).replace( /^\./, '0.' );
// Any number in exponential form will fail due to the e+/-.
if ( valid = new RegExp(
'^' + digits + '(?:\\.' + digits + ')?$', b < 37 ? 'i' : '' ).test(n) ) {
if ( isNum ) {
if ( n.replace( /^0\.0*|\./, '' ).length > 15 ) {
// 'new BigNumber() number type has more than 15 significant digits: {n}'
ifExceptionsThrow( orig, 0 );
}
// Prevent later check for length on converted number.
isNum = !isNum;
}
n = convert( n, 10, b, x['s'] );
} else if ( n != 'Infinity' && n != 'NaN' ) {
// 'new BigNumber() not a base {b} number: {n}'
ifExceptionsThrow( orig, 1, b );
n = 'NaN';
}
} else {
// 'new BigNumber() base not an integer: {b}'
// 'new BigNumber() base out of range: {b}'
ifExceptionsThrow( b, 2 );
// Ignore base.
valid = isValid.test(n);
}
} else {
valid = isValid.test(n);
}
if ( !valid ) {
// Infinity/NaN
x['c'] = x['e'] = null;
// NaN
if ( n != 'Infinity' ) {
// No exception on NaN.
if ( n != 'NaN' ) {
// 'new BigNumber() not a number: {n}'
ifExceptionsThrow( orig, 3 );
}
x['s'] = null;
}
id = 0;
return;
}
}
// Decimal point?
if ( ( e = n.indexOf('.') ) > -1 ) {
n = n.replace( '.', '' );
}
// Exponential form?
if ( ( i = n.search( /e/i ) ) > 0 ) {
// Determine exponent.
if ( e < 0 ) {
e = i;
}
e += +n.slice( i + 1 );
n = n.substring( 0, i );
} else if ( e < 0 ) {
// Integer.
e = n.length;
}
// Determine leading zeros.
for ( i = 0; n.charAt(i) == '0'; i++ ) {
}
b = n.length;
// Disallow numbers with over 15 significant digits if number type.
if ( isNum && b > 15 && n.slice(i).length > 15 ) {
// 'new BigNumber() number type has more than 15 significant digits: {n}'
ifExceptionsThrow( orig, 0 );
}
id = 0;
// Overflow?
if ( ( e -= i + 1 ) > MAX_EXP ) {
// Infinity.
x['c'] = x['e'] = null;
// Zero or underflow?
} else if ( i == b || e < MIN_EXP ) {
// Zero.
x['c'] = [ x['e'] = 0 ];
} else {
// Determine trailing zeros.
for ( ; n.charAt(--b) == '0'; ) {
}
x['e'] = e;
x['c'] = [];
// Convert string to array of digits (without leading and trailing zeros).
for ( e = 0; i <= b; x['c'][e++] = +n.charAt(i++) ) {
}
}
}
// CONSTRUCTOR PROPERTIES/METHODS
BigNumber['ROUND_UP'] = 0;
BigNumber['ROUND_DOWN'] = 1;
BigNumber['ROUND_CEIL'] = 2;
BigNumber['ROUND_FLOOR'] = 3;
BigNumber['ROUND_HALF_UP'] = 4;
BigNumber['ROUND_HALF_DOWN'] = 5;
BigNumber['ROUND_HALF_EVEN'] = 6;
BigNumber['ROUND_HALF_CEIL'] = 7;
BigNumber['ROUND_HALF_FLOOR'] = 8;
/*
* Configure infrequently-changing library-wide settings.
*
* Accept an object or an argument list, with one or many of the following
* properties or parameters respectively:
* [ DECIMAL_PLACES [, ROUNDING_MODE [, EXPONENTIAL_AT [, RANGE [, ERRORS ]]]]]
*
* E.g.
* BigNumber.config(20, 4) is equivalent to
* BigNumber.config({ DECIMAL_PLACES : 20, ROUNDING_MODE : 4 })
* Ignore properties/parameters set to null or undefined.
*
* Return an object with the properties current values.
*/
BigNumber['config'] = function () {
var v, p,
i = 0,
r = {},
a = arguments,
o = a[0],
c = 'config',
inRange = function ( n, lo, hi ) {
return !( ( outOfRange = n < lo || n > hi ) ||
parse(n) != n && n !== 0 );
},
has = o && typeof o == 'object'
? function () {if ( o.hasOwnProperty(p) ) return ( v = o[p] ) != null}
: function () {if ( a.length > i ) return ( v = a[i++] ) != null};
// [DECIMAL_PLACES] {number} Integer, 0 to MAX inclusive.
if ( has( p = 'DECIMAL_PLACES' ) ) {
if ( inRange( v, 0, MAX ) ) {
DECIMAL_PLACES = v | 0;
} else {
// 'config() DECIMAL_PLACES not an integer: {v}'
// 'config() DECIMAL_PLACES out of range: {v}'
ifExceptionsThrow( v, p, c );
}
}
r[p] = DECIMAL_PLACES;
// [ROUNDING_MODE] {number} Integer, 0 to 8 inclusive.
if ( has( p = 'ROUNDING_MODE' ) ) {
if ( inRange( v, 0, 8 ) ) {
ROUNDING_MODE = v | 0;
} else {
// 'config() ROUNDING_MODE not an integer: {v}'
// 'config() ROUNDING_MODE out of range: {v}'
ifExceptionsThrow( v, p, c );
}
}
r[p] = ROUNDING_MODE;
/*
* [EXPONENTIAL_AT] {number|number[]} Integer, -MAX to MAX inclusive or
* [ integer -MAX to 0 inclusive, 0 to MAX inclusive ].
*/
if ( has( p = 'EXPONENTIAL_AT' ) ) {
if ( inRange( v, -MAX, MAX ) ) {
TO_EXP_NEG = -( TO_EXP_POS = ~~( v < 0 ? -v : +v ) );
} else if ( !outOfRange && v && inRange( v[0], -MAX, 0 ) &&
inRange( v[1], 0, MAX ) ) {
TO_EXP_NEG = ~~v[0];
TO_EXP_POS = ~~v[1];
} else {
// 'config() EXPONENTIAL_AT not an integer or not [integer, integer]: {v}'
// 'config() EXPONENTIAL_AT out of range or not [negative, positive: {v}'
ifExceptionsThrow( v, p, c, 1 );
}
}
r[p] = [ TO_EXP_NEG, TO_EXP_POS ];
/*
* [RANGE][ {number|number[]} Non-zero integer, -MAX to MAX inclusive or
* [ integer -MAX to -1 inclusive, integer 1 to MAX inclusive ].
*/
if ( has( p = 'RANGE' ) ) {
if ( inRange( v, -MAX, MAX ) && ~~v ) {
MIN_EXP = -( MAX_EXP = ~~( v < 0 ? -v : +v ) );
} else if ( !outOfRange && v && inRange( v[0], -MAX, -1 ) &&
inRange( v[1], 1, MAX ) ) {
MIN_EXP = ~~v[0], MAX_EXP = ~~v[1];
} else {
// 'config() RANGE not a non-zero integer or not [integer, integer]: {v}'
// 'config() RANGE out of range or not [negative, positive: {v}'
ifExceptionsThrow( v, p, c, 1, 1 );
}
}
r[p] = [ MIN_EXP, MAX_EXP ];
// [ERRORS] {boolean|number} true, false, 1 or 0.
if ( has( p = 'ERRORS' ) ) {
if ( v === !!v || v === 1 || v === 0 ) {
parse = ( outOfRange = id = 0, ERRORS = !!v )
? parseInt
: parseFloat;
} else {
// 'config() ERRORS not a boolean or binary digit: {v}'
ifExceptionsThrow( v, p, c, 0, 0, 1 );
}
}
r[p] = ERRORS;
return r;
};
// PRIVATE FUNCTIONS
// Assemble error messages. Throw BigNumber Errors.
function ifExceptionsThrow( arg, i, j, isArray, isRange, isErrors) {
if ( ERRORS ) {
var error,
method = ['new BigNumber', 'cmp', 'div', 'eq', 'gt', 'gte', 'lt',
'lte', 'minus', 'mod', 'plus', 'times', 'toFr'
][ id ? id < 0 ? -id : id : 1 / id < 0 ? 1 : 0 ] + '()',
message = outOfRange ? ' out of range' : ' not a' +
( isRange ? ' non-zero' : 'n' ) + ' integer';
message = ( [
method + ' number type has more than 15 significant digits',
method + ' not a base ' + j + ' number',
method + ' base' + message,
method + ' not a number' ][i] ||
j + '() ' + i + ( isErrors
? ' not a boolean or binary digit'
: message + ( isArray
? ' or not [' + ( outOfRange
? ' negative, positive'
: ' integer, integer' ) + ' ]'
: '' ) ) ) + ': ' + arg;
outOfRange = id = 0;
error = new Error(message);
error['name'] = 'BigNumber Error';
throw error;
}
}
/*
* Convert a numeric string of baseIn to a numeric string of baseOut.
*/
function convert( nStr, baseOut, baseIn, sign ) {
var e, dvs, dvd, nArr, fracArr, fracBN;
// Convert string of base bIn to an array of numbers of baseOut.
// Eg. strToArr('255', 10) where baseOut is 16, returns [15, 15].
// Eg. strToArr('ff', 16) where baseOut is 10, returns [2, 5, 5].
function strToArr( str, bIn ) {
var j,
i = 0,
strL = str.length,
arrL,
arr = [0];
for ( bIn = bIn || baseIn; i < strL; i++ ) {
for ( arrL = arr.length, j = 0; j < arrL; arr[j] *= bIn, j++ ) {
}
for ( arr[0] += DIGITS.indexOf( str.charAt(i) ), j = 0;
j < arr.length;
j++ ) {
if ( arr[j] > baseOut - 1 ) {
if ( arr[j + 1] == null ) {
arr[j + 1] = 0;
}
arr[j + 1] += arr[j] / baseOut ^ 0;
arr[j] %= baseOut;
}
}
}
return arr.reverse();
}
// Convert array to string.
// E.g. arrToStr( [9, 10, 11] ) becomes '9ab' (in bases above 11).
function arrToStr( arr ) {
var i = 0,
arrL = arr.length,
str = '';
for ( ; i < arrL; str += DIGITS.charAt( arr[i++] ) ) {
}
return str;
}
if ( baseIn < 37 ) {
nStr = nStr.toLowerCase();
}
/*
* If non-integer convert integer part and fraction part separately.
* Convert the fraction part as if it is an integer than use division to
* reduce it down again to a value less than one.
*/
if ( ( e = nStr.indexOf( '.' ) ) > -1 ) {
/*
* Calculate the power to which to raise the base to get the number
* to divide the fraction part by after it has been converted as an
* integer to the required base.
*/
e = nStr.length - e - 1;
// Use toFixed to avoid possible exponential notation.
dvs = strToArr( new BigNumber(baseIn)['pow'](e)['toF'](), 10 );
nArr = nStr.split('.');
// Convert the base of the fraction part (as integer).
dvd = strToArr( nArr[1] );
// Convert the base of the integer part.
nArr = strToArr( nArr[0] );
// Result will be a BigNumber with a value less than 1.
fracBN = divide( dvd, dvs, dvd.length - dvs.length, sign, baseOut,
// Is least significant digit of integer part an odd number?
nArr[nArr.length - 1] & 1 );
fracArr = fracBN['c'];
// e can be <= 0 ( if e == 0, fracArr is [0] or [1] ).
if ( e = fracBN['e'] ) {
// Append zeros according to the exponent of the result.
for ( ; ++e; fracArr.unshift(0) ) {
}
// Append the fraction part to the converted integer part.
nStr = arrToStr(nArr) + '.' + arrToStr(fracArr);
// fracArr is [1].
// Fraction digits rounded up, so increment last digit of integer part.
} else if ( fracArr[0] ) {
if ( nArr[ e = nArr.length - 1 ] < baseOut - 1 ) {
++nArr[e];
nStr = arrToStr(nArr);
} else {
nStr = new BigNumber( arrToStr(nArr),
baseOut )['plus'](ONE)['toS'](baseOut);
}
// fracArr is [0]. No fraction digits.
} else {
nStr = arrToStr(nArr);
}
} else {
// Simple integer. Convert base.
nStr = arrToStr( strToArr(nStr) );
}
return nStr;
}
// Perform division in the specified base. Called by div and convert.
function divide( dvd, dvs, exp, s, base, isOdd ) {
var dvsL, dvsT, next, cmp, remI,
dvsZ = dvs.slice(),
dvdI = dvsL = dvs.length,
dvdL = dvd.length,
rem = dvd.slice( 0, dvsL ),
remL = rem.length,
quo = new BigNumber(ONE),
qc = quo['c'] = [],
qi = 0,
dig = DECIMAL_PLACES + ( quo['e'] = exp ) + 1;
quo['s'] = s;
s = dig < 0 ? 0 : dig;
// Add zeros to make remainder as long as divisor.
for ( ; remL++ < dvsL; rem.push(0) ) {
}
// Create version of divisor with leading zero.
dvsZ.unshift(0);
do {
// 'next' is how many times the divisor goes into the current remainder.
for ( next = 0; next < base; next++ ) {
// Compare divisor and remainder.
if ( dvsL != ( remL = rem.length ) ) {
cmp = dvsL > remL ? 1 : -1;
} else {
for ( remI = -1, cmp = 0; ++remI < dvsL; ) {
if ( dvs[remI] != rem[remI] ) {
cmp = dvs[remI] > rem[remI] ? 1 : -1;
break;
}
}
}
// Subtract divisor from remainder (if divisor < remainder).
if ( cmp < 0 ) {
// Remainder cannot be more than one digit longer than divisor.
// Equalise lengths using divisor with extra leading zero?
for ( dvsT = remL == dvsL ? dvs : dvsZ; remL; ) {
if ( rem[--remL] < dvsT[remL] ) {
for ( remI = remL;
remI && !rem[--remI];
rem[remI] = base - 1 ) {
}
--rem[remI];
rem[remL] += base;
}
rem[remL] -= dvsT[remL];
}
for ( ; !rem[0]; rem.shift() ) {
}
} else {
break;
}
}
// Add the 'next' digit to the result array.
qc[qi++] = cmp ? next : ++next;
// Update the remainder.
rem[0] && cmp
? ( rem[remL] = dvd[dvdI] || 0 )
: ( rem = [ dvd[dvdI] ] );
} while ( ( dvdI++ < dvdL || rem[0] != null ) && s-- );
// Leading zero? Do not remove if result is simply zero (qi == 1).
if ( !qc[0] && qi != 1 ) {
// There can't be more than one zero.
--quo['e'];
qc.shift();
}
// Round?
if ( qi > dig ) {
rnd( quo, DECIMAL_PLACES, base, isOdd, rem[0] != null );
}
// Overflow?
if ( quo['e'] > MAX_EXP ) {
// Infinity.
quo['c'] = quo['e'] = null;
// Underflow?
} else if ( quo['e'] < MIN_EXP ) {
// Zero.
quo['c'] = [quo['e'] = 0];
}
return quo;
}
/*
* Return a string representing the value of BigNumber n in normal or
* exponential notation rounded to the specified decimal places or
* significant digits.
* Called by toString, toExponential (exp 1), toFixed, and toPrecision (exp 2).
* d is the index (with the value in normal notation) of the digit that may be
* rounded up.
*/
function format( n, d, exp ) {
// Initially, i is the number of decimal places required.
var i = d - (n = new BigNumber(n))['e'],
c = n['c'];
// +-Infinity or NaN?
if ( !c ) {
return n['toS']();
}
// Round?
if ( c.length > ++d ) {
rnd( n, i, 10 );
}
// Recalculate d if toFixed as n['e'] may have changed if value rounded up.
i = c[0] == 0 ? i + 1 : exp ? d : n['e'] + i + 1;
// Append zeros?
for ( ; c.length < i; c.push(0) ) {
}
i = n['e'];
/*
* toPrecision returns exponential notation if the number of significant
* digits specified is less than the number of digits necessary to
* represent the integer part of the value in normal notation.
*/
return exp == 1 || exp == 2 && ( --d < i || i <= TO_EXP_NEG )
// Exponential notation.
? ( n['s'] < 0 && c[0] ? '-' : '' ) + ( c.length > 1
? ( c.splice( 1, 0, '.' ), c.join('') )
: c[0] ) + ( i < 0 ? 'e' : 'e+' ) + i
// Normal notation.
: n['toS']();
}
// Round if necessary.
// Called by divide, format, setMode and sqrt.
function rnd( x, dp, base, isOdd, r ) {
var xc = x['c'],
isNeg = x['s'] < 0,
half = base / 2,
i = x['e'] + dp + 1,
// 'next' is the digit after the digit that may be rounded up.
next = xc[i],
/*
* 'more' is whether there are digits after 'next'.
* E.g.
* 0.005 (e = -3) to be rounded to 0 decimal places (dp = 0) gives i = -2
* The 'next' digit is zero, and there ARE 'more' digits after it.
* 0.5 (e = -1) dp = 0 gives i = 0
* The 'next' digit is 5 and there are no 'more' digits after it.
*/
more = r || i < 0 || xc[i + 1] != null;
r = ROUNDING_MODE < 4
? ( next != null || more ) &&
( ROUNDING_MODE == 0 ||
ROUNDING_MODE == 2 && !isNeg ||
ROUNDING_MODE == 3 && isNeg )
: next > half || next == half &&
( ROUNDING_MODE == 4 || more ||
/*
* isOdd is used in base conversion and refers to the least significant
* digit of the integer part of the value to be converted. The fraction
* part is rounded by this method separately from the integer part.
*/
ROUNDING_MODE == 6 && ( xc[i - 1] & 1 || !dp && isOdd ) ||
ROUNDING_MODE == 7 && !isNeg ||
ROUNDING_MODE == 8 && isNeg );
if ( i < 1 || !xc[0] ) {
xc.length = 0;
xc.push(0);
if ( r ) {
// 1, 0.1, 0.01, 0.001, 0.0001 etc.
xc[0] = 1;
x['e'] = -dp;
} else {
// Zero.
x['e'] = 0;
}
return x;
}
// Remove any digits after the required decimal places.
xc.length = i--;
// Round up?
if ( r ) {
// Rounding up may mean the previous digit has to be rounded up and so on.
for ( --base; ++xc[i] > base; ) {
xc[i] = 0;
if ( !i-- ) {
++x['e'];
xc.unshift(1);
}
}
}
// Remove trailing zeros.
for ( i = xc.length; !xc[--i]; xc.pop() ) {
}
return x;
}
// Round after setting the appropriate rounding mode.
// Handles ceil, floor and round.
function setMode( x, dp, rm ) {
var r = ROUNDING_MODE;
ROUNDING_MODE = rm;
x = new BigNumber(x);
x['c'] && rnd( x, dp, 10 );
ROUNDING_MODE = r;
return x;
}
// PROTOTYPE/INSTANCE METHODS
/*
* Return a new BigNumber whose value is the absolute value of this BigNumber.
*/
P['abs'] = P['absoluteValue'] = function () {
var x = new BigNumber(this);
if ( x['s'] < 0 ) {
x['s'] = 1;
}
return x;
};
/*
* Return a new BigNumber whose value is the value of this BigNumber
* rounded to a whole number in the direction of Infinity.
*/
P['ceil'] = function () {
return setMode( this, 0, 2 );
};
/*
* Return
* 1 if the value of this BigNumber is greater than the value of BigNumber(y, b),
* -1 if the value of this BigNumber is less than the value of BigNumber(y, b),
* 0 if they have the same value,
* or null if the value of either is NaN.
*/
P['comparedTo'] = P['cmp'] = function ( y, b ) {
var a,
x = this,
xc = x['c'],
yc = ( id = -id, y = new BigNumber( y, b ) )['c'],
i = x['s'],
j = y['s'],
k = x['e'],
l = y['e'];
// Either NaN?
if ( !i || !j ) {
return null;
}
a = xc && !xc[0], b = yc && !yc[0];
// Either zero?
if ( a || b ) {
return a ? b ? 0 : -j : i;
}
// Signs differ?
if ( i != j ) {
return i;
}
// Either Infinity?
if ( a = i < 0, b = k == l, !xc || !yc ) {
return b ? 0 : !xc ^ a ? 1 : -1;
}
// Compare exponents.
if ( !b ) {
return k > l ^ a ? 1 : -1;
}
// Compare digit by digit.
for ( i = -1,
j = ( k = xc.length ) < ( l = yc.length ) ? k : l;
++i < j; ) {
if ( xc[i] != yc[i] ) {
return xc[i] > yc[i] ^ a ? 1 : -1;
}
}
// Compare lengths.
return k == l ? 0 : k > l ^ a ? 1 : -1;
};
/*
* Return the number of decimal places of the value of this BigNumber,
* or null if the value of this BigNumber is +-Infinity or NaN.
*/
P['decimalPlaces'] = P['dp'] = function () {
var dp;
if ( this['c'] ) {
dp = this['c'].length - this['e'] - 1;
return dp < 0 ? 0 : dp;
}
return null;
};
/*
* n / 0 = I
* n / N = N
* n / I = 0
* 0 / n = 0
* 0 / 0 = N
* 0 / N = N
* 0 / I = 0
* N / n = N
* N / 0 = N
* N / N = N
* N / I = N
* I / n = I
* I / 0 = I
* I / N = N
* I / I = N
*
* Return a new BigNumber whose value is the value of this BigNumber
* divided by the value of BigNumber(y, b), rounded according to
* DECIMAL_PLACES and ROUNDING_MODE.
*/
P['dividedBy'] = P['div'] = function ( y, b ) {
var xc = this['c'],
xe = this['e'],
xs = this['s'],
yc = ( id = 2, y = new BigNumber( y, b ) )['c'],
ye = y['e'],
ys = y['s'],
s = xs == ys ? 1 : -1;
// Either NaN/Infinity/0?
return !xe && ( !xc || !xc[0] ) || !ye && ( !yc || !yc[0] )
// Either NaN?
? new BigNumber( !xs || !ys ||
// Both 0 or both Infinity?
( xc ? yc && xc[0] == yc[0] : !yc )
// Return NaN.
? NaN
// x is 0 or y is Infinity?
: xc && xc[0] == 0 || !yc
// Return +-0.
? s * 0
// y is 0. Return +-Infinity.
: s / 0 )
: divide( xc, yc, xe - ye, s, 10 );
};
/*
* Return true if the value of this BigNumber is equal to the value of
* BigNumber(n, b), otherwise returns