diff --git a/src/js/ripple/amount.js b/src/js/ripple/amount.js index d5dfc5ba..8d4fd3eb 100644 --- a/src/js/ripple/amount.js +++ b/src/js/ripple/amount.js @@ -349,9 +349,14 @@ Amount.prototype.divide = function (d) { * * @this {Amount} The numerator (top half) of the fraction. * @param {Amount} denominator The denominator (bottom half) of the fraction. + * @param opts Options for the calculation. + * @param opts.reference_date {Date|Number} Date based on which demurrage/interest + * should be applied. Can be given as JavaScript Date or int for Ripple epoch. * @return {Amount} The resulting ratio. Unit will be the same as numerator. */ -Amount.prototype.ratio_human = function (denominator) { +Amount.prototype.ratio_human = function (denominator, opts) { + opts = opts || {}; + if (typeof denominator === 'number' && parseInt(denominator, 10) === denominator) { // Special handling of integer arguments denominator = Amount.from_json('' + denominator + '.0'); @@ -367,6 +372,14 @@ Amount.prototype.ratio_human = function (denominator) { return Amount.NaN(); } + // Apply interest/demurrage + // + // We only need to apply it to the second factor, because the currency unit of + // the first factor will carry over into the result. + if (opts.reference_date) { + denominator = denominator.applyInterest(opts.reference_date); + } + // Special case: The denominator is a native (XRP) amount. // // In that case, it's going to be expressed as base units (1 XRP = @@ -402,9 +415,14 @@ Amount.prototype.ratio_human = function (denominator) { * * @this {Amount} The first factor of the product. * @param {Amount} factor The second factor of the product. + * @param opts Options for the calculation. + * @param opts.reference_date {Date|Number} Date based on which demurrage/interest + * should be applied. Can be given as JavaScript Date or int for Ripple epoch. * @return {Amount} The product. Unit will be the same as the first factor. */ -Amount.prototype.product_human = function (factor) { +Amount.prototype.product_human = function (factor, opts) { + opts = opts || {}; + if (typeof factor === 'number' && parseInt(factor, 10) === factor) { // Special handling of integer arguments factor = Amount.from_json(String(factor) + '.0'); @@ -417,6 +435,14 @@ Amount.prototype.product_human = function (factor) { return Amount.NaN(); } + // Apply interest/demurrage + // + // We only need to apply it to the second factor, because the currency unit of + // the first factor will carry over into the result. + if (opts.reference_date) { + factor = factor.applyInterest(opts.reference_date); + } + var product = this.multiply(factor); // Special case: The second factor is a native (XRP) amount expressed as base @@ -850,6 +876,39 @@ Amount.prototype.to_text = function (allow_nan) { return result; }; +/** + * Calculate present value based on currency and a reference date. + * + * This only affects demurraging and interest-bearing currencies. + * + * User should not store amount objects after the interest is applied. This is + * intended by display functions such as toHuman(). + * + * @param referenceDate {Date|Number} Date based on which demurrage/interest + * should be applied. Can be given as JavaScript Date or int for Ripple epoch. + * @return {Amount} The amount with interest applied. + */ +Amount.prototype.applyInterest = function (referenceDate) { + if (this._currency.has_interest()) { + var interest = this._currency.get_interest_at(referenceDate); + + // XXX Because the Amount parsing routines don't support some of the things + // that JavaScript can output when casting a float to a string, the + // following call sometimes does not produce a valid Amount. + // + // The correct way to solve this is probably to switch to a proper + // BigDecimal for our internal representation and then use that across + // the board instead of instantiating these dummy Amount objects. + var interestTempAmount = Amount.from_json(""+interest+"/1/1"); + + if (interestTempAmount.is_valid()) { + return this.multiply(interestTempAmount); + } + } else { + return this; + } +}; + /** * Format only value in a human-readable format. * @@ -885,21 +944,8 @@ Amount.prototype.to_human = function (opts) { // Apply demurrage/interest var ref = this; - if (opts.reference_date && this._currency.has_interest()) { - var interest = this._currency.get_interest_at(opts.reference_date); - - // XXX Because the Amount parsing routines don't support some of the things - // that JavaScript can output when casting a float to a string, the - // following call sometimes does not produce a valid Amount. - // - // The correct way to solve this is probably to switch to a proper - // BigDecimal for our internal representation and then use that across - // the board instead of instantiating these dummy Amount objects. - var interestTempAmount = Amount.from_json(""+interest+"/1/1"); - - if (interestTempAmount.is_valid()) { - ref = this.multiply(interestTempAmount); - } + if (opts.reference_date) { + ref = this.applyInterest(opts.reference_date); } var order = ref._is_native ? consts.xns_precision : -ref._offset; diff --git a/test/amount-test.js b/test/amount-test.js index 51c40282..7ac14854 100644 --- a/test/amount-test.js +++ b/test/amount-test.js @@ -431,4 +431,85 @@ describe('Amount', function() { assert.strictEqual(a.not_equals_why(b), 'Native mismatch.'); }); }); + + describe('product_human', function() { + it('Multiply 0 XRP with 0 XRP', function () { + assert.strictEqual('0/XRP', Amount.from_json('0').product_human(Amount.from_json('0')).to_text_full()); + }); + it('Multiply 0 USD with 0 XRP', function () { + assert.strictEqual('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('0')).to_text_full()); + }); + it('Multiply 0 XRP with 0 USD', function () { + assert.strictEqual('0/XRP', Amount.from_json('0').product_human(Amount.from_json('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply 1 XRP with 0 XRP', function () { + assert.strictEqual('0/XRP', Amount.from_json('1').product_human(Amount.from_json('0')).to_text_full()); + }); + it('Multiply 1 USD with 0 XRP', function () { + assert.strictEqual('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('1/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('0')).to_text_full()); + }); + it('Multiply 1 XRP with 0 USD', function () { + assert.strictEqual('0/XRP', Amount.from_json('1').product_human(Amount.from_json('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply 0 XRP with 1 XRP', function () { + assert.strictEqual('0/XRP', Amount.from_json('0').product_human(Amount.from_json('1')).to_text_full()); + }); + it('Multiply 0 USD with 1 XRP', function () { + assert.strictEqual('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('1')).to_text_full()); + }); + it('Multiply 0 XRP with 1 USD', function () { + assert.strictEqual('0/XRP', Amount.from_json('0').product_human(Amount.from_json('1/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply XRP with USD', function () { + assert.equal('0.002/XRP', Amount.from_json('200').product_human(Amount.from_json('10/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply XRP with USD', function () { + assert.strictEqual('0.2/XRP', Amount.from_json('20000').product_human(Amount.from_json('10/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply XRP with USD', function () { + assert.strictEqual('20/XRP', Amount.from_json('2000000').product_human(Amount.from_json('10/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply XRP with USD, neg', function () { + assert.strictEqual('-0.002/XRP', Amount.from_json('200').product_human(Amount.from_json('-10/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply XRP with USD, neg, frac', function () { + assert.strictEqual('-0.222/XRP', Amount.from_json('-6000').product_human(Amount.from_json('37/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply USD with USD', function () { + assert.strictEqual('20000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('2000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('10/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply USD with USD', function () { + assert.strictEqual('200000000000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('2000000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('100000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply EUR with USD, result < 1', function () { + assert.strictEqual('100000/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('100/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('1000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply EUR with USD, neg', function () { + assert.strictEqual(Amount.from_json('-24000/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('2000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full(), '-48000000/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + it('Multiply EUR with USD, neg, <1', function () { + assert.strictEqual(Amount.from_json('0.1/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('-1000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full(), '-100/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + it('Multiply EUR with XRP, factor < 1', function () { + assert.strictEqual(Amount.from_json('0.05/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('2000')).to_text_full(), '0.0001/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + it('Multiply EUR with XRP, neg', function () { + assert.strictEqual(Amount.from_json('-100/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('5')).to_text_full(), '-0.0005/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + it('Multiply EUR with XRP, neg, <1', function () { + assert.strictEqual(Amount.from_json('-0.05/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('2000')).to_text_full(), '-0.0001/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + it('Multiply XRP with XRP', function () { + assert.strictEqual(Amount.from_json('10000000').product_human(Amount.from_json('10')).to_text_full(), '0.0001/XRP'); + }); + it('Multiply USD with XAU (dem)', function () { + assert.strictEqual(Amount.from_json('2000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('10/015841551A748AD2C1F76FF6ECB0CCCD00000000/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'), {reference_date: 443845330 + 31535000}).to_text_full(), '19900.00316303882/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + }); + + describe('ratio_human', function() { + it('Divide USD by XAU (dem)', function () { + assert.strictEqual(Amount.from_json('2000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').ratio_human(Amount.from_json('10/015841551A748AD2C1F76FF6ECB0CCCD00000000/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'), {reference_date: 443845330 + 31535000}).to_text_full(), '201.0049931765529/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + }); });