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;
};