1
0
Fork 0

Add scale to Number formatter

* src/validate/formatter/Number.js
  (__mixin): Add mixin ctor.
  (parse): Handle scale.
  (styleNumber): Handle scale.
  (scale, split): Add methods.

* test/validate/formatter/NumberTest.js: Modified accordingly.
master
Mike Gerwitz 2016-11-30 11:03:33 -05:00
parent 6db99c8632
commit 5947e7646e
2 changed files with 174 additions and 3 deletions

View File

@ -1,4 +1,5 @@
/**
* @license
* Number formatter
*
* Copyright (C) 2016 LoVullo Associates, Inc.
@ -25,11 +26,83 @@ var Trait = require( 'easejs' ).Trait,
/**
* Formats numbers in en_US locale
*
* Only whole numbers are permitted by default unless the mixin is
* initialized with a scale, where the scale represents the number of
* digits of the significand. If the scale is positive, the
* significand will be padded with zeroes to meet the scale; if
* negative, trailing zeroes will be removed. The significand will be
* truncated (not rounded) if it exceeds the scale:
*
* @example
* EchoFormatter.use( Number ).parse( '00003' ) // => 3
* EchoFormatter.use( Number( -6 ) ).parse( '3.14159' ) // => 3.14159
* EchoFormatter.use( Number( 6 ) ).parse( '3.14159' ) // => 3.141590
* EchoFormatter.use( Number( 2 ) ).parse( '3.14159' ) // => 3.14
*
* Leading zeroes are stripped.
*/
module.exports = Trait( 'Number' )
.implement( ValidatorFormatter )
.extend(
{
/**
* Number of digits after the decimal point
*
* This value should never be negative.
*
* @type {number}
*/
'private _scale': 0,
/**
* Pre-computed zero-padding of scale
*
* This conveniently allows prefixing this padding with a number
* and then truncating to scale.
*
* @type {string}
*/
'private _scalestr': '',
/**
* Initialize optional scale
*
* The scale SCALE is an optional value that specifies the number
* of digits after the decimal point to display. Note that
* trailing zeros are _not_ removed, making this ideal for
* i.e. currency.
*
* The "scale" terminology comes from the Unix bc tool. If
* positive, the significand will be padded with zeros to meet the
* scale. If negative, no padding will take place.
*
* If the significand has more digits than permitted by SCALE, it
* is truncated.
*
* @param {number} scale number of digits after decimal point
*/
__mixin: function( scale )
{
this._scale = Math.abs( scale ) || 0;
this._scalestr = this._padScale( +scale );
},
/**
* Create scale padding for significand
*
* @return {string} string with SCALE zeroes
*/
'private _padScale': function( scale )
{
return ( scale > 0 )
? ( new Array( this._scale + 1 ) ).join( '0' )
: '';
},
/**
* Parse item as a number
*
@ -39,7 +112,10 @@ module.exports = Trait( 'Number' )
*/
'virtual abstract override public parse': function( data )
{
return this.__super( data ).replace( /[ ,]/g, '' );
var cleaned = this.__super( data ).replace( /[ ,]/g, '' ),
parts = this.split( cleaned );
return parts.integer + this.scale( parts.significand, this._scale );
},
@ -65,7 +141,9 @@ module.exports = Trait( 'Number' )
*/
'virtual protected styleNumber': function( number )
{
var i = number.length,
var parts = this.split( number );
var i = parts.integer.length,
ret = [],
chunk = '';
@ -83,6 +161,56 @@ module.exports = Trait( 'Number' )
ret.unshift( chunk );
} while ( i > 0 );
return ret.join( ',' );
return ret.join( ',' ) +
this.scale( parts.significand, this._scale );
},
/**
* Parse significand and return scaled value
*
* If the result is non-empty, then the result will be prefixed
* with a decimal point.
*
* Truncation is determined by whether the initial scale was
* negative. If so, this method will lack a zero padding string
* and return a result without trailing zeroes.
*
* @param {string} significand value after decimal point
* @param {number} scale positive scale
*
* @return {string} scaled significand with decimal point as needed
*/
'virtual protected scale': function( significand, scale )
{
if ( scale <= 0 )
{
return '';
}
// easy cheat: use the pre-filled scale and truncate
var result = ( significand + this._scalestr ).substr( 0, scale );
return ( result )
? '.' + result
: '';
},
/**
* Split integer from significand in NUMBER
*
* @param {string} number number to split
*
* @return {Object.<integer,decimal>} integer and significand
*/
'virtual protected split': function( number )
{
var parts = number.split( '.' );
return {
integer: parts[ 0 ].replace( /^0+/, '' ) || '0',
significand: ( parts[ 1 ] || '' ).replace( /0+$/, '' ),
}
}
} );

View File

@ -27,8 +27,10 @@ var liza = require( '../../../' ),
describe( 'validate.formatter.Number', function()
{
// default case, no decimal places
common.testValidate( EchoFormatter.use( Sut )(), {
"1": [ "1", "1" ],
"001": [ "1", "1" ],
"123": [ "123", "123" ],
"12345": [ "12345", "12,345" ],
"12,345": [ "12345", "12,345" ],
@ -36,6 +38,47 @@ describe( 'validate.formatter.Number', function()
"12,345,": [ "12345", "12,345" ],
" 12,345 ,": [ "12345", "12,345" ],
" 1, ,": [ "1", "1" ],
// strip decimals
"1.234": [ "1", "1" ],
" 1, ,.": [ "1", "1" ],
} );
// decimal places
common.testValidate( EchoFormatter.use( Sut( 3 ) )(), {
"1": [ "1.000", "1.000" ],
"001": [ "1.000", "1.000" ],
"123": [ "123.000", "123.000" ],
"123.1": [ "123.100", "123.100" ],
"0123.1": [ "123.100", "123.100" ],
"123.155": [ "123.155", "123.155" ],
"123.": [ "123.000", "123.000" ],
".123": [ "0.123", "0.123" ],
// truncate, not round (leave that to another formatter)
"123.1554": [ "123.155", "123.155" ],
"123.1556": [ "123.155", "123.155" ],
"12,345": [ "12345.000", "12,345.000" ],
" 1, ,.": [ "1.000", "1.000" ],
} );
// really long decimals should be unstyled
common.testValidate( EchoFormatter.use( Sut( 10 ) )(), {
"0.1234567890": [ true, true ],
} );
// negative scale strips trailing zeroes
common.testValidate( EchoFormatter.use( Sut( -5 ) )(), {
"1": [ "1", "1" ],
"01": [ "1", "1" ],
"1.0": [ "1", "1" ],
"1.0100": [ "1.01", "1.01" ],
"123.155": [ "123.155", "123.155" ],
"123.123456": [ "123.12345", "123.12345" ],
} );