Fix currency parsing of non-alphanumeric and no-currency currencies

This commit is contained in:
wltsmrz
2015-03-04 19:10:39 -08:00
parent a9b7d7d793
commit 2166bb2e88
2 changed files with 128 additions and 75 deletions

View File

@@ -1,4 +1,6 @@
var extend = require('extend'); 'use strict';
var extend = require('extend');
var UInt160 = require('./uint160').UInt160; var UInt160 = require('./uint160').UInt160;
var utils = require('./utils'); var utils = require('./utils');
var Float = require('./ieee754').Float; var Float = require('./ieee754').Float;
@@ -16,8 +18,7 @@ var Currency = extend(function() {
// 3-letter code: ... // 3-letter code: ...
// XXX Should support hex, C++ doesn't currently allow it. // XXX Should support hex, C++ doesn't currently allow it.
this._value = NaN; this._value = NaN;
this._update(); this._update();
}, UInt160); }, UInt160);
@@ -32,25 +33,37 @@ Currency.HEX_CURRENCY_BAD = '0000000000000000000000005852500000000000';
* Examples: * Examples:
* *
* USD => currency * USD => currency
* USD - Dollar => currency with optional full currency name * USD - Dollar => currency with optional full currency
* XAU (-0.5%pa) => XAU with 0.5% effective demurrage rate per year * name
* XAU (-0.5%pa) => XAU with 0.5% effective demurrage rate
* per year
* XAU - Gold (-0.5%pa) => Optionally allowed full currency name * XAU - Gold (-0.5%pa) => Optionally allowed full currency name
* USD (1%pa) => US dollars with 1% effective interest per year * USD (1%pa) => US dollars with 1% effective interest
* per year
* INR - Indian Rupees => Optional full currency name with spaces * INR - Indian Rupees => Optional full currency name with spaces
* TYX - 30-Year Treasuries => Optional full currency with numbers and a dash * TYX - 30-Year Treasuries => Optional full currency with numbers
* TYX - 30-Year Treasuries (1.5%pa) => Optional full currency with numbers, dash and interest rate * and a dash
* TYX - 30-Year Treasuries (1.5%pa) => Optional full currency with numbers,
* dash and interest rate
* *
* The regular expression below matches above cases, broken down for better understanding: * The regular expression below matches above cases, broken down for better
* understanding:
* *
* ^\s* // start with any amount of whitespace * ^\s* // start with any amount of whitespace
* ([a-zA-Z]{3}|[0-9]{3}) // either 3 letter alphabetic currency-code or 3 digit numeric currency-code. See ISO 4217 * ([a-zA-Z]{3}|[0-9]{3}) // either 3 letter alphabetic currency-code or 3
* (\s*-\s*[- \w]+) // optional full currency name following the dash after currency code, * digit numeric currency-code. See ISO 4217
* full currency code can contain letters, numbers and dashes * (\s*-\s*[- \w]+) // optional full currency name following the dash
* (\s*\(-?\d+\.?\d*%pa\))? // optional demurrage rate, has optional - and . notation (-0.5%pa) * after currency code, full currency code can
* contain letters, numbers and dashes
* (\s*\(-?\d+\.?\d*%pa\))? // optional demurrage rate, has optional - and
* . notation (-0.5%pa)
* \s*$ // end with any amount of whitespace * \s*$ // end with any amount of whitespace
* *
*/ */
Currency.prototype.human_RE = /^\s*([a-zA-Z0-9]{3})(\s*-\s*[- \w]+)?(\s*\(-?\d+\.?\d*%pa\))?\s*$/;
/*eslint-disable max-len*/
Currency.prototype.human_RE = /^\s*([a-zA-Z0-9\<\>\(\)\{\}\[\]\|\?\!\@\#\$\%\^\&]{3})(\s*-\s*[- \w]+)?(\s*\(-?\d+\.?\d*%pa\))?\s*$/;
/*eslint-enable max-len*/
Currency.from_json = function(j, shouldInterpretXrpAsIou) { Currency.from_json = function(j, shouldInterpretXrpAsIou) {
return (new Currency()).parse_json(j, shouldInterpretXrpAsIou); return (new Currency()).parse_json(j, shouldInterpretXrpAsIou);
@@ -58,39 +71,65 @@ Currency.from_json = function(j, shouldInterpretXrpAsIou) {
Currency.from_human = function(j, opts) { Currency.from_human = function(j, opts) {
return (new Currency().parse_human(j, opts)); return (new Currency().parse_human(j, opts));
} };
// this._value = NaN on error. // this._value = NaN on error.
Currency.prototype.parse_json = function(j, shouldInterpretXrpAsIou) { Currency.prototype.parse_json = function(j, shouldInterpretXrpAsIou) {
this._value = NaN; this._value = NaN;
switch (typeof j) { if (j instanceof Currency) {
case 'string': this._value = j.copyTo({})._value;
this._update();
return this;
}
// if an empty string is given, fall back to XRP switch (typeof j) {
case 'number':
if (!isNaN(j)) {
this.parse_number(j);
}
break;
case 'string':
if (!j || j === '0') { if (!j || j === '0') {
this.parse_hex(shouldInterpretXrpAsIou ? Currency.HEX_CURRENCY_BAD : Currency.HEX_ZERO); // Empty string or XRP
this.parse_hex(shouldInterpretXrpAsIou
? Currency.HEX_CURRENCY_BAD
: Currency.HEX_ZERO);
break;
}
if (j === '1') {
// 'no currency'
this.parse_hex(Currency.HEX_ONE);
break;
}
if (/^[A-F0-9]{40}$/.test(j)) {
// Hex format
this.parse_hex(j);
break; break;
} }
// match the given string to see if it's in an allowed format // match the given string to see if it's in an allowed format
var matches = String(j).match(this.human_RE); var matches = j.match(this.human_RE);
if (matches) { if (matches) {
var currencyCode = matches[1]; var currencyCode = matches[1];
// for the currency 'XRP' case // for the currency 'XRP' case
// we drop everything else that could have been provided // we drop everything else that could have been provided
// e.g. 'XRP - Ripple' // e.g. 'XRP - Ripple'
if (!currencyCode || /^(0|XRP)$/.test(currencyCode)) { if (!currencyCode || /^(0|XRP)$/.test(currencyCode)) {
this.parse_hex(shouldInterpretXrpAsIou ? Currency.HEX_CURRENCY_BAD : Currency.HEX_ZERO); this.parse_hex(shouldInterpretXrpAsIou
? Currency.HEX_CURRENCY_BAD
: Currency.HEX_ZERO);
// early break, we can't have interest on XRP // early break, we can't have interest on XRP
break; break;
} }
// the full currency is matched as it is part of the valid currency format, but not stored // the full currency is matched as it is part of the valid currency
// format, but not stored
// var full_currency = matches[2] || ''; // var full_currency = matches[2] || '';
var interest = matches[3] || ''; var interest = matches[3] || '';
@@ -117,25 +156,28 @@ Currency.prototype.parse_json = function(j, shouldInterpretXrpAsIou) {
currencyData[2] = currencyCode.charCodeAt(1) & 0xff; currencyData[2] = currencyCode.charCodeAt(1) & 0xff;
currencyData[3] = currencyCode.charCodeAt(2) & 0xff; currencyData[3] = currencyCode.charCodeAt(2) & 0xff;
// byte 5-8 are for reference date, but should always be 0 so we won't fill it // byte 5-8 are for reference date, but should always be 0 so we
// won't fill it
// byte 9-16 are for the interest // byte 9-16 are for the interest
percentage = parseFloat(percentage[0]); percentage = parseFloat(percentage[0]);
// the interest or demurrage is expressed as a yearly (per annum) value // the interest or demurrage is expressed as a yearly (per annum)
// value
var secondsPerYear = 31536000; // 60 * 60 * 24 * 365 var secondsPerYear = 31536000; // 60 * 60 * 24 * 365
// Calculating the interest e-fold // Calculating the interest e-fold
// 0.5% demurrage is expressed 0.995, 0.005 less than 1 // 0.5% demurrage is expressed 0.995, 0.005 less than 1
// 0.5% interest is expressed as 1.005, 0.005 more than 1 // 0.5% interest is expressed as 1.005, 0.005 more than 1
var interestEfold = secondsPerYear / Math.log(1 + percentage/100); var interestEfold = secondsPerYear / Math.log(1 + percentage / 100);
var bytes = Float.toIEEE754Double(interestEfold); var bytes = Float.toIEEE754Double(interestEfold);
for (var i=0; i<=bytes.length; i++) { for (var i = 0; i <= bytes.length; i++) {
currencyData[8 + i] = bytes[i] & 0xff; currencyData[8 + i] = bytes[i] & 0xff;
} }
// the last 4 bytes are reserved for future use, so we won't fill those // the last 4 bytes are reserved for future use, so we won't fill
// those
} else { } else {
currencyData[12] = currencyCode.charCodeAt(0) & 0xff; currencyData[12] = currencyCode.charCodeAt(0) & 0xff;
@@ -144,21 +186,6 @@ Currency.prototype.parse_json = function(j, shouldInterpretXrpAsIou) {
} }
this.parse_bytes(currencyData); this.parse_bytes(currencyData);
} else {
this.parse_hex(j);
}
break;
case 'number':
if (!isNaN(j)) {
this.parse_number(j);
}
break;
case 'object':
if (j instanceof Currency) {
this._value = j.copyTo({})._value;
this._update();
} }
break; break;
} }
@@ -166,7 +193,6 @@ Currency.prototype.parse_json = function(j, shouldInterpretXrpAsIou) {
return this; return this;
}; };
Currency.prototype.parse_human = function(j) { Currency.prototype.parse_human = function(j) {
return this.parse_json(j); return this.parse_json(j);
}; };
@@ -176,6 +202,7 @@ Currency.prototype.parse_human = function(j) {
* *
* You should never need to call this. * You should never need to call this.
*/ */
Currency.prototype._update = function() { Currency.prototype._update = function() {
var bytes = this.to_bytes(); var bytes = this.to_bytes();
@@ -183,7 +210,7 @@ Currency.prototype._update = function() {
var isZeroExceptInStandardPositions = true; var isZeroExceptInStandardPositions = true;
if (!bytes) { if (!bytes) {
return 'XRP'; return;
} }
this._native = false; this._native = false;
@@ -192,8 +219,9 @@ Currency.prototype._update = function() {
this._interest_period = NaN; this._interest_period = NaN;
this._iso_code = ''; this._iso_code = '';
for (var i=0; i<20; i++) { for (var i = 0; i < 20; i++) {
isZeroExceptInStandardPositions = isZeroExceptInStandardPositions && (i===12 || i===13 || i===14 || bytes[i]===0); isZeroExceptInStandardPositions = isZeroExceptInStandardPositions
&& (i === 12 || i === 13 || i === 14 || bytes[i] === 0);
} }
if (isZeroExceptInStandardPositions) { if (isZeroExceptInStandardPositions) {
@@ -201,7 +229,7 @@ Currency.prototype._update = function() {
+ String.fromCharCode(bytes[13]) + String.fromCharCode(bytes[13])
+ String.fromCharCode(bytes[14]); + String.fromCharCode(bytes[14]);
if (this._iso_code === '\0\0\0') { if (this._iso_code === '\u0000\u0000\u0000') {
this._native = true; this._native = true;
this._iso_code = 'XRP'; this._iso_code = 'XRP';
} }
@@ -215,8 +243,8 @@ Currency.prototype._update = function() {
this._type = 1; this._type = 1;
this._interest_start = (bytes[4] << 24) + this._interest_start = (bytes[4] << 24) +
(bytes[5] << 16) + (bytes[5] << 16) +
(bytes[6] << 8) + (bytes[6] << 8) +
(bytes[7] ); (bytes[7]);
this._interest_period = Float.fromIEEE754Double(bytes.slice(8, 16)); this._interest_period = Float.fromIEEE754Double(bytes.slice(8, 16));
} }
}; };
@@ -230,7 +258,8 @@ Currency.prototype.parse_bytes = function(byte_array) {
var isZeroExceptInStandardPositions = true; var isZeroExceptInStandardPositions = true;
for (var i=0; i<20; i++) { for (var i=0; i<20; i++) {
isZeroExceptInStandardPositions = isZeroExceptInStandardPositions && (i===12 || i===13 || i===14 || byte_array[0]===0) isZeroExceptInStandardPositions = isZeroExceptInStandardPositions
&& (i===12 || i===13 || i===14 || byte_array[0]===0)
} }
if (isZeroExceptInStandardPositions) { if (isZeroExceptInStandardPositions) {
@@ -260,20 +289,25 @@ Currency.prototype.is_native = function() {
}; };
/** /**
* Whether this currency is an interest-bearing/demurring currency. * @return {Boolean} whether this currency is an interest-bearing currency
*/ */
Currency.prototype.has_interest = function() { Currency.prototype.has_interest = function() {
return this._type === 1 && !isNaN(this._interest_start) && !isNaN(this._interest_period); return this._type === 1
&& !isNaN(this._interest_start)
&& !isNaN(this._interest_period);
}; };
/** /**
* *
* @param referenceDate - number of seconds since the Ripple Epoch (0:00 on January 1, 2000 UTC) * @param {number} referenceDate number of seconds since the Ripple Epoch
* used to calculate the interest over provided interval * (0:00 on January 1, 2000 UTC) used to calculate the
* pass in one years worth of seconds to ge the yearly interest * interest over provided interval pass in one years
* @returns {number} - interest for provided interval, can be negative for demurred currencies * worth of seconds to ge the yearly interest
* @returns {number} interest for provided interval, can be negative for
* demurred currencies
*/ */
Currency.prototype.get_interest_at = function(referenceDate, decimals) { Currency.prototype.get_interest_at = function(referenceDate) {
if (!this.has_interest()) { if (!this.has_interest()) {
return 0; return 0;
} }
@@ -288,18 +322,20 @@ Currency.prototype.get_interest_at = function(referenceDate, decimals) {
} }
// calculate interest by e-fold number // calculate interest by e-fold number
return Math.exp((referenceDate - this._interest_start) / this._interest_period); return Math.exp((referenceDate - this._interest_start)
/ this._interest_period);
}; };
Currency.prototype.get_interest_percentage_at = function(referenceDate, decimals) { Currency.prototype.get_interest_percentage_at
= function(referenceDate, decimals) {
var interest = this.get_interest_at(referenceDate, decimals); var interest = this.get_interest_at(referenceDate, decimals);
// convert to percentage // convert to percentage
var interest = (interest*100)-100; interest = (interest * 100) - 100;
var decimalMultiplier = decimals ? Math.pow(10,decimals) : 100; var decimalMultiplier = decimals ? Math.pow(10, decimals) : 100;
// round to two decimals behind the dot // round to two decimals behind the dot
return Math.round(interest*decimalMultiplier) / decimalMultiplier; return Math.round(interest * decimalMultiplier) / decimalMultiplier;
}; };
// XXX Currently we inherit UInt.prototype.is_valid, which is mostly fine. // XXX Currently we inherit UInt.prototype.is_valid, which is mostly fine.
@@ -307,9 +343,9 @@ Currency.prototype.get_interest_percentage_at = function(referenceDate, decimals
// We could be doing further checks into the internal format of the // We could be doing further checks into the internal format of the
// currency data, since there are some values that are invalid. // currency data, since there are some values that are invalid.
// //
//Currency.prototype.is_valid = function() { // Currency.prototype.is_valid = function() {
// return UInt.prototype.is_valid() && ...; // return UInt.prototype.is_valid() && ...;
//}; // };
Currency.prototype.to_json = function(opts) { Currency.prototype.to_json = function(opts) {
if (!this.is_valid()) { if (!this.is_valid()) {
@@ -317,28 +353,35 @@ Currency.prototype.to_json = function(opts) {
return 'XRP'; return 'XRP';
} }
var opts = opts || {}; if (!opts) {
opts = {};
}
var currency; var currency;
var fullName = opts && opts.full_name ? ' - ' + opts.full_name : ''; var fullName = opts && opts.full_name ? ' - ' + opts.full_name : '';
opts.show_interest = opts.show_interest !== void(0) ? opts.show_interest : this.has_interest(); opts.show_interest = opts.show_interest !== undefined
? opts.show_interest
: this.has_interest();
if (!opts.force_hex && /^[A-Z0-9]{3}$/.test(this._iso_code)) { if (!opts.force_hex && /^[A-Z0-9]{3}$/.test(this._iso_code)) {
currency = this._iso_code + fullName; currency = this._iso_code + fullName;
if (opts.show_interest) { if (opts.show_interest) {
var decimals = !isNaN(opts.decimals) ? opts.decimals : void(0); var decimals = !isNaN(opts.decimals) ? opts.decimals : undefined;
var interestPercentage = this.has_interest() ? this.get_interest_percentage_at(this._interest_start + 3600 * 24 * 365, decimals) : 0; var interestPercentage = this.has_interest()
? this.get_interest_percentage_at(
this._interest_start + 3600 * 24 * 365, decimals
)
: 0;
currency += ' (' + interestPercentage + '%pa)'; currency += ' (' + interestPercentage + '%pa)';
} }
} else { } else {
// Fallback to returning the raw currency hex // Fallback to returning the raw currency hex
currency = this.to_hex(); currency = this.to_hex();
// XXX This is to maintain backwards compatibility, but it is very, very odd // XXX This is to maintain backwards compatibility, but it is very, very
// behavior, so we should deprecate it and get rid of it as soon as // odd behavior, so we should deprecate it and get rid of it as soon as
// possible. // possible.
if (currency === Currency.HEX_ONE) { if (currency === Currency.HEX_ONE) {
currency = 1; currency = 1;
} }
@@ -357,5 +400,3 @@ Currency.prototype.get_iso = function() {
}; };
exports.Currency = Currency; exports.Currency = Currency;
// vim:sw=2:sts=2:ts=8:et

View File

@@ -1,3 +1,5 @@
/*eslint-disable */
var assert = require('assert'); var assert = require('assert');
var currency = require('ripple-lib').Currency; var currency = require('ripple-lib').Currency;
var timeUtil = require('ripple-lib').utils.time; var timeUtil = require('ripple-lib').utils.time;
@@ -54,6 +56,16 @@ describe('Currency', function() {
assert(r.is_valid()); assert(r.is_valid());
assert.strictEqual('1D2', r.to_json()); assert.strictEqual('1D2', r.to_json());
}); });
it('from_json("1").to_human()', function() {
var r = currency.from_json('1');
assert(r.is_valid());
assert.strictEqual(1, r.to_json());
});
it('from_json("#$%").to_human()', function() {
var r = currency.from_json('#$%');
assert(r.is_valid());
assert.strictEqual('0000000000000000000000002324250000000000', r.to_json());
});
it('from_json("XAU").to_json() hex', function() { it('from_json("XAU").to_json() hex', function() {
var r = currency.from_json("XAU"); var r = currency.from_json("XAU");
assert.strictEqual('0000000000000000000000005841550000000000', r.to_json({force_hex: true})); assert.strictEqual('0000000000000000000000005841550000000000', r.to_json({force_hex: true}));