Amount: Full demurrage support.

This commit is contained in:
Stefan Thomas
2014-01-25 11:31:56 -08:00
parent fa07601a2a
commit f678f47155
4 changed files with 202 additions and 34 deletions

View File

@@ -847,9 +847,11 @@ Amount.prototype.to_text = function (allow_nan) {
* default: 3.
* @param opts.signed {Boolean|String} Whether negative numbers will have a
* prefix. If String, that string will be used as the prefix. Default: '-'
* @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.
*/
Amount.prototype.to_human = function (opts) {
var opts = opts || {};
opts = opts || {};
if (!this.is_valid()) return '';
@@ -859,10 +861,29 @@ Amount.prototype.to_human = function (opts) {
opts.group_width = opts.group_width || 3;
var order = this._is_native ? consts.xns_precision : -this._offset;
// 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);
}
}
var order = ref._is_native ? consts.xns_precision : -ref._offset;
var denominator = consts.bi_10.clone().pow(order);
var int_part = this._value.divide(denominator).toString();
var fraction_part = this._value.mod(denominator).toString();
var int_part = ref._value.divide(denominator).toString();
var fraction_part = ref._value.mod(denominator).toString();
// Add leading zeros to fraction
while (fraction_part.length < order) {

View File

@@ -2,6 +2,7 @@
var extend = require('extend');
var UInt160 = require('./uint160').UInt160;
var Float = require('./float').Float;
var utils = require('./utils');
//
@@ -18,6 +19,8 @@ var Currency = extend(function () {
// XXX Should support hex, C++ doesn't currently allow it.
this._value = NaN;
this._update();
}, UInt160);
Currency.prototype = extend({}, UInt160.prototype);
@@ -66,6 +69,7 @@ Currency.prototype.parse_json = function (j, shouldInterpretXrpAsIou) {
case 'object':
if (j instanceof Currency) {
this._value = j.copyTo({})._value;
this._update();
}
break;
}
@@ -73,6 +77,56 @@ Currency.prototype.parse_json = function (j, shouldInterpretXrpAsIou) {
return this;
};
/**
* Recalculate internal representation.
*
* You should never need to call this.
*/
Currency.prototype._update = function () {
var bytes = this.to_bytes();
// is it 0 everywhere except 12, 13, 14?
var isZeroExceptInStandardPositions = true;
if (!bytes) {
return "XRP";
}
this._native = false;
this._type = -1;
this._interest_start = new Date();
this._interest_period = NaN;
this._iso_code = '';
for (var i=0; i<20; i++) {
isZeroExceptInStandardPositions = isZeroExceptInStandardPositions && (i===12 || i===13 || i===14 || bytes[i]===0);
}
if (isZeroExceptInStandardPositions) {
this._iso_code = String.fromCharCode(bytes[12])
+ String.fromCharCode(bytes[13])
+ String.fromCharCode(bytes[14]);
if (this._iso_code === "\0\0\0") {
this._native = true;
this._iso_code = "XRP";
}
this._type = 0;
} else if (bytes[0] === 0x01) { // Demurrage currency
this._iso_code = String.fromCharCode(bytes[1])
+ String.fromCharCode(bytes[2])
+ String.fromCharCode(bytes[3]);
this._type = 1;
this._interest_start = (bytes[4] << 24) +
(bytes[5] << 16) +
(bytes[6] << 8) +
(bytes[7] );
this._interest_period = Float.fromBytes(bytes.slice(8, 16));
}
};
// XXX Probably not needed anymore?
/*
Currency.prototype.parse_bytes = function (byte_array) {
@@ -108,7 +162,24 @@ Currency.prototype.parse_bytes = function (byte_array) {
*/
Currency.prototype.is_native = function () {
return !isNaN(this._value) && this.is_zero();
return this._native;
};
/**
* Whether this currency is an interest-bearing/demurring currency.
*/
Currency.prototype.has_interest = function () {
return this._type === 1 && this._interest_start && !isNaN(this._interest_period);
};
Currency.prototype.get_interest_at = function (referenceDate) {
if (!this.has_interest) return 1;
if (referenceDate instanceof Date) {
referenceDate = utils.fromTimestamp(referenceDate.getTime());
}
return Math.pow(Math.E, (referenceDate - this._interest_start) / this._interest_period);
};
// XXX Currently we inherit UInt.prototype.is_valid, which is mostly fine.
@@ -121,42 +192,26 @@ Currency.prototype.is_native = function () {
//};
Currency.prototype.to_json = function () {
var bytes = this.to_bytes();
// is it 0 everywhere except 12, 13, 14?
var isZeroExceptInStandardPositions = true;
if (!bytes) {
if (!this.is_valid()) {
// XXX This backwards compatible behavior, but probably not very good.
return "XRP";
}
for (var i=0; i<20; i++) {
isZeroExceptInStandardPositions = isZeroExceptInStandardPositions && (i===12 || i===13 || i===14 || bytes[i]===0);
if (/^[A-Z0-9]{3}$/.test(this._iso_code)) {
return this._iso_code;
}
if (isZeroExceptInStandardPositions) {
var currencyCode = String.fromCharCode(bytes[12])
+ String.fromCharCode(bytes[13])
+ String.fromCharCode(bytes[14]);
if (/^[A-Z0-9]{3}$/.test(currencyCode) && currencyCode !== "XRP" ) {
return currencyCode;
} else if (currencyCode === "\0\0\0") {
return "XRP";
} else {
return "XRP";
}
} else {
var currencyHex = this.to_hex();
// Fallback to returning the raw currency hex
var currencyHex = this.to_hex();
// XXX This is to maintain backwards compatibility, but it is very, very odd
// behavior, so we should deprecate it and get rid of it as soon as
// possible.
if (currencyHex === Currency.HEX_ONE) {
return 1;
}
return currencyHex;
// XXX This is to maintain backwards compatibility, but it is very, very odd
// behavior, so we should deprecate it and get rid of it as soon as
// possible.
if (currencyHex === Currency.HEX_ONE) {
return 1;
}
return currencyHex;
};
Currency.prototype.to_human = function () {

56
src/js/ripple/float.js Normal file
View File

@@ -0,0 +1,56 @@
/**
* IEEE 754 floating-point.
*
* Supports single- or double-precision
*/
var Float = exports.Float = {};
var allZeros = /^0+$/;
var allOnes = /^1+$/;
Float.fromBytes = function (bytes) {
// Render in binary. Hackish.
var b = "";
for (var i = 0, n = bytes.length; i < n; i++) {
var bits = (bytes[i] & 0xff).toString(2);
while (bits.length < 8) bits = "0" + bits;
b += bits;
}
// Determine configuration. This could have all been precomputed but it is fast enough.
var exponentBits = bytes.length === 4 ? 4 : 11;
var mantissaBits = (bytes.length * 8) - exponentBits - 1;
var bias = Math.pow(2, exponentBits - 1) - 1;
var minExponent = 1 - bias - mantissaBits;
// Break up the binary representation into its pieces for easier processing.
var s = b[0];
var e = b.substring(1, exponentBits + 1);
var m = b.substring(exponentBits + 1);
var value = 0;
var multiplier = (s === "0" ? 1 : -1);
if (allZeros.test(e)) {
// Zero or denormalized
if (allZeros.test(m)) {
// Value is zero
} else {
value = parseInt(m, 2) * Math.pow(2, minExponent);
}
} else if (allOnes.test(e)) {
// Infinity or NaN
if (allZeros.test(m)) {
value = Infinity;
} else {
value = NaN;
}
} else {
// Normalized
var exponent = parseInt(e, 2) - bias;
var mantissa = parseInt(m, 2);
value = (1 + (mantissa * Math.pow(2, -mantissaBits))) * Math.pow(2, exponent);
}
return value * multiplier;
};

View File

@@ -10,6 +10,9 @@ describe('Currency', function() {
it('json_rewrite("NaN") == "XRP"', function() {
assert.strictEqual('XRP', currency.json_rewrite(NaN));
});
it('json_rewrite("015841551A748AD2C1F76FF6ECB0CCCD00000000") == "XAU"', function() {
assert.strictEqual('XAU', currency.json_rewrite("015841551A748AD2C1F76FF6ECB0CCCD00000000"));
});
});
describe('from_json', function() {
it('from_json(NaN).to_json() == "XRP"', function() {
@@ -52,4 +55,37 @@ describe('Currency', function() {
assert.strictEqual('XRP', currency.from_json('XRP').to_human());
});
});
describe('has_interest', function() {
it('should be true for type 1 currency codes', function() {
assert(currency.from_hex('015841551A748AD2C1F76FF6ECB0CCCD00000000').has_interest());
assert(currency.from_json('015841551A748AD2C1F76FF6ECB0CCCD00000000').has_interest());
});
it('should be false for type 0 currency codes', function() {
assert(!currency.from_hex('0000000000000000000000005553440000000000').has_interest());
assert(!currency.from_json('USD').has_interest());
});
});
function precision(num, precision) {
return +(Math.round(num + "e+"+precision) + "e-"+precision);
}
describe('get_interest_at', function() {
it('returns demurred value for demurrage currency', function() {
var cur = currency.from_json('015841551A748AD2C1F76FF6ECB0CCCD00000000');
// At start, no demurrage should occur
assert.equal(1, cur.get_interest_at(443845330));
// After one year, 0.5% should have occurred
assert.equal(0.995, precision(cur.get_interest_at(443845330 + 31536000), 14));
// After one demurrage period, 1/e should have occurred
assert.equal(1/Math.E, cur.get_interest_at(443845330 + 6291418827.05));
// One year before start, it should be (roughly) 0.5% higher.
assert.equal(1.005, precision(cur.get_interest_at(443845330 - 31536000), 4));
// One demurrage period before start, rate should be e
assert.equal(Math.E, cur.get_interest_at(443845330 - 6291418827.05));
});
});
});