Mike Gerwitz

Activist for User Freedom

aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMike Gerwitz <gerwitzm@lovullo.com>2016-12-06 08:05:37 -0500
committerMike Gerwitz <gerwitzm@lovullo.com>2016-12-06 08:05:37 -0500
commit2523862e1f236e8318e6e6d1bf08f4b11fbbf89d (patch)
treed040bec4e1351f28d40f092f2088f33b8abdae0b
parenta446e454c6a6ababe96a00df6b3ed16144f46854 (diff)
parente883c45e356364c46e4d111a89a2a4473a690d9e (diff)
downloadliza-2523862e1f236e8318e6e6d1bf08f4b11fbbf89d.tar.gz
liza-2523862e1f236e8318e6e6d1bf08f4b11fbbf89d.tar.bz2
liza-2523862e1f236e8318e6e6d1bf08f4b11fbbf89d.zip
Add Currency and StringFormat formatters
-rw-r--r--src/validate/formatter/Currency.js36
-rw-r--r--src/validate/formatter/Number.js144
-rw-r--r--src/validate/formatter/StringFormat.js178
-rw-r--r--test/validate/formatter/NumberTest.js51
-rw-r--r--test/validate/formatter/StringFormatTest.js115
5 files changed, 520 insertions, 4 deletions
diff --git a/src/validate/formatter/Currency.js b/src/validate/formatter/Currency.js
new file mode 100644
index 0000000..757f7a9
--- /dev/null
+++ b/src/validate/formatter/Currency.js
@@ -0,0 +1,36 @@
+/**
+ * @license
+ * Currency formatter
+ *
+ * Copyright (C) 2016 LoVullo Associates, Inc.
+ *
+ * This file is part of liza.
+ *
+ * liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Currency of the form `$%2.f`
+ *
+ * @example
+ * Currency().parse( '$123' ); // => 123.00
+ * Currency().retrieve( '123.4' ); // => $123.40
+ *
+ * @class Currency
+ * @mixes module:validate/formatter.Number
+ * @mixes module:validate/formatter.StringFormat
+ */
+module.exports = require( './EchoFormatter' )
+ .use( require( './Number' )( 2 ) )
+ .use( require( './StringFormat' )( '$%s' ) );
diff --git a/src/validate/formatter/Number.js b/src/validate/formatter/Number.js
index 4adf3c8..19240bc 100644
--- a/src/validate/formatter/Number.js
+++ b/src/validate/formatter/Number.js
@@ -1,4 +1,5 @@
/**
+ * @license
* Number formatter
*
* Copyright (C) 2016 LoVullo Associates, Inc.
@@ -24,22 +25,105 @@ var Trait = require( 'easejs' ).Trait,
/**
- * Formats insurance limit(s)
+ * 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
*
* @param {string} data data to parse
*
* @return {string} data formatted for storage
+ *
+ * @throws Error if number is not of a valid format
*/
'virtual abstract override public parse': function( data )
{
- return this.__super( data ).replace( /[ ,]/g, '' );
+ var cleaned = this.__super( data ).replace( /[ ,]/g, '' );
+
+ if ( !/^[0-9]*(\.[0-9]*)?$/.test( cleaned ) )
+ {
+ throw Error( "Invalid number: " + data );
+ }
+
+ var parts = this.split( cleaned );
+
+ return parts.integer + this.scale( parts.significand, this._scale );
},
@@ -65,7 +149,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 +169,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+$/, '' ),
+ }
}
} ); \ No newline at end of file
diff --git a/src/validate/formatter/StringFormat.js b/src/validate/formatter/StringFormat.js
new file mode 100644
index 0000000..c82510a
--- /dev/null
+++ b/src/validate/formatter/StringFormat.js
@@ -0,0 +1,178 @@
+/**
+ * @license
+ * StringFormat formatter
+ *
+ * Copyright (C) 2016 LoVullo Associates, Inc.
+ *
+ * This file is part of liza.
+ *
+ * liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @module validate/formatter
+ */
+
+var Trait = require( 'easejs' ).Trait,
+ ValidatorFormatter = require( '../ValidatorFormatter' );
+
+
+/**
+ * Basic data formatting
+ *
+ * `StringFormat` effectively allows for the definition of prefixes
+ * and suffixes by using a format string of the form `'pre %s post'`.
+ *
+ * For example, the format string `'$%sUSD'` applied to the
+ * input `'$25.00USD'` would yield `'25.00'` after parsing and
+ * re-yield the original string when retrieved.
+ *
+ * If `StringFormat` is mixed in with other formatters that
+ * modify the input, then the former will be applied after earlier
+ * formatters process the data. For example:
+ *
+ * @example
+ * let fmt = EchoFormatter
+ * .use( Number( 2 ) )
+ * .use( StringFormatter( '$%sUSD' ) )();
+ *
+ * fmt.parse( '$25USD' ); // => 25.00
+ * fmt.retrieve( '25' ); // => $25.00USD
+ *
+ * The simple, restricted format string in place of a regular expression
+ * allows for a declarative stacking of formatters without concern as
+ * to whether the format was written correctly. Regular expressions,
+ * however, might well need associated tests for confidence in the
+ * implementation, which complicates the design. Simply: it's easy to
+ * reason about.
+ */
+module.exports = Trait( 'StringFormat' )
+ .implement( ValidatorFormatter )
+ .extend(
+{
+ /**
+ * Prefix string
+ *
+ * @type {string}
+ */
+ 'private _pre': '',
+
+ /**
+ * Postfix string
+ *
+ * @type {string}
+ */
+ 'private _post': '',
+
+
+ /**
+ * Define format string
+ *
+ * The format string must have a single `'%s'` denoting the
+ * placement of the data.
+ *
+ * @param {string} format format string with single `'%s'`
+ */
+ __mixin: function( format )
+ {
+ var parts = this.parseFormat( ''+format );
+
+ this._pre = parts.pre;
+ this._post = parts.post;
+ },
+
+
+ /**
+ * Extract prefix and suffix from format string FORMAT
+ *
+ * Everything before the `'%s'` is the prefix, and everything
+ * after is the suffix.
+ *
+ * @param {string} format format string with single `'%s'`
+ *
+ * @return {Object.<pre,post>} prefix and suffix of FORMAT
+ *
+ * @throws {Error} if FORMAT does not have exactly one `'%s'`
+ */
+ 'virtual protected parseFormat': function( format )
+ {
+ var parts = format.split( '%s' );
+
+ if ( parts.length !== 2 )
+ {
+ throw Error(
+ "Format string must have a single '%s': " + format
+ );
+ }
+
+ return {
+ pre: parts[ 0 ],
+ post: parts[ 1 ]
+ };
+ },
+
+
+ /**
+ * Remove prefix and suffix from data
+ *
+ * @param {string} data data to parse
+ *
+ * @return {string} data formatted for storage
+ */
+ 'virtual abstract override public parse': function( data )
+ {
+ return this.__super( this._stripPrePost( data ) );
+ },
+
+
+ /**
+ * Recursively strip prefixes and suffixes from STR
+ *
+ * This simply allows us to avoid having to use regexes and,
+ * consequently, worry about escaping format strings.
+ *
+ * @param {string} str string to strip
+ *
+ * @return {string} stripped string
+ */
+ 'private _stripPrePost': function( str )
+ {
+ if ( this._pre && ( str.substr( 0, this._pre.length ) === this._pre ) )
+ {
+ return this._stripPrePost(
+ str.substr( this._pre.length )
+ );
+ }
+
+ if ( this._post && ( str.substr( -this._post.length ) === this._post ) )
+ {
+ return this._stripPrePost(
+ str.substr( 0, str.length - this._post.length )
+ );
+ }
+
+ return str;
+ },
+
+
+ /**
+ * Format data by adding prefix and suffix
+ *
+ * @param {string} data data to format for display
+ *
+ * @return {string} data formatted for display
+ */
+ 'virtual abstract override public retrieve': function( data )
+ {
+ return this._pre + this.__super( data ) + this._post;
+ },
+} );
diff --git a/test/validate/formatter/NumberTest.js b/test/validate/formatter/NumberTest.js
index 74d2595..0e036d8 100644
--- a/test/validate/formatter/NumberTest.js
+++ b/test/validate/formatter/NumberTest.js
@@ -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,55 @@ 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" ],
+
+ // non-numbers
+ "foo": false,
+ "123foo": false,
+ } );
+
+
+ // 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" ],
+
+ // non-numbers
+ "1.foo": false,
+ "123foo.012": false,
+ } );
+
+
+ // 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" ],
} );
diff --git a/test/validate/formatter/StringFormatTest.js b/test/validate/formatter/StringFormatTest.js
new file mode 100644
index 0000000..3580d92
--- /dev/null
+++ b/test/validate/formatter/StringFormatTest.js
@@ -0,0 +1,115 @@
+/**
+ * @license
+ * StringFormat formatter test
+ *
+ * Copyright (C) 2016 LoVullo Associates, Inc.
+ *
+ * This file is part of liza.
+ *
+ * liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+var liza = require( '../../../' ),
+ expect = require( 'chai' ).expect,
+ Sut = liza.validate.formatter.StringFormat,
+ EchoFormatter = liza.validate.formatter.EchoFormatter,
+ common = require( './common' );
+
+
+describe( 'validate.formatter.StringFormat', function()
+{
+ common.testValidate( EchoFormatter.use( Sut( 'PRE%sPOST' ) )(), {
+ // basic prefix/suffix
+ "": [ "", "PREPOST" ],
+ "foo": [ "foo", "PREfooPOST" ],
+ "PREfoo": [ "foo", "PREfooPOST" ],
+ "barPOST": [ "bar", "PREbarPOST" ],
+ "PREbazPOST": [ "baz", "PREbazPOST" ],
+
+ // only prefix/suffix
+ "PRE": [ "", "PREPOST" ],
+ "POST": [ "", "PREPOST" ],
+ "PREPOST": [ "", "PREPOST" ],
+
+ // repeated prefix/suffix normalization
+ "PREPREfoo": [ "foo", "PREfooPOST" ],
+ "barPOSTPOST": [ "bar", "PREbarPOST" ],
+ "PREPREbazPOSTPOSTPOST": [ "baz", "PREbazPOST" ],
+ "PREPREPOSTPOST": [ "", "PREPOST" ],
+
+ // convoluted interpretations
+ "PREfooPOSTPRE": [ "fooPOSTPRE", "PREfooPOSTPREPOST" ],
+ "PREmooPREfooPOST": [ "mooPREfoo", "PREmooPREfooPOST" ],
+ "mooPREfoo": [ "mooPREfoo", "PREmooPREfooPOST" ],
+ } );
+
+
+ // only prefix format
+ common.testValidate( EchoFormatter.use( Sut( 'BEG%s' ) )(), {
+ "foo": [ "foo", "BEGfoo" ],
+ "BEGfoo": [ "foo", "BEGfoo" ],
+ } );
+
+
+ // only suffix format
+ common.testValidate( EchoFormatter.use( Sut( '%sEND' ) )(), {
+ "fooEND": [ "foo", "fooEND" ],
+ "fooEND": [ "foo", "fooEND" ],
+ } );
+
+
+ // no prefix or suffix
+ common.testValidate( EchoFormatter.use( Sut( '%s' ) )(), {
+ "foo": [ "foo", "foo" ],
+ } );
+
+
+ describe( 'given multiple %s', function()
+ {
+ it( 'throws an error', function()
+ {
+ expect( function()
+ {
+ EchoFormatter.use( Sut( 'foo%sbar%sbaz' ) )();
+ } ).to.throw( Error );
+ } );
+ } );
+
+
+ describe( 'given no %s', function()
+ {
+ it( 'throws an error', function()
+ {
+ expect( function()
+ {
+ EchoFormatter.use( Sut( '' ) )();
+ } ).to.throw( Error );
+
+ expect( function()
+ {
+ EchoFormatter.use( Sut( 'Foo' ) )();
+ } ).to.throw( Error );
+ } );
+ } );
+
+
+ common.testMixin(
+ EchoFormatter,
+ Sut( 'PRE%sPOST' ),
+ 'base',
+ 'PREfooPOST',
+ 'basefoo',
+ 'PREbasePREfooPOSTPOST'
+ );
+} );