diff --git a/src/validate/VFormat.js b/src/validate/VFormat.js
new file mode 100644
index 0000000..1b8842f
--- /dev/null
+++ b/src/validate/VFormat.js
@@ -0,0 +1,136 @@
+/**
+ * Validator-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 .
+ */
+
+
+var Class = require( 'easejs' ).Class;
+
+
+/**
+ * Data validation and formatting
+ */
+module.exports = Class( 'VFormat',
+{
+ /**
+ * Pattern definition
+ * @type {Array.}
+ */
+ 'private _dfn': [],
+
+ /**
+ * Data retrieval format
+ * @type {Array}
+ */
+ 'private _retdfn': [],
+
+
+ /**
+ * Initialize with a pattern definition and return format definition
+ *
+ * The pattern definition should be an array of arrays with two elements:
+ * the pattern to match against and its replacement. The patterns must be
+ * regular expressions and the replacements may be either strings or
+ * functions.
+ *
+ * The return formatter is an optional single array consisting of a pattern
+ * and a replacement.
+ *
+ * @param {Array.} dfn pattern definition
+ * @param {Array=} retdfn return format definition
+ */
+ __construct: function( dfn, retdfn )
+ {
+ this._dfn = dfn;
+ this._retdfn = retdfn;
+ },
+
+
+ /**
+ * Format the given data or fail if no match is found
+ *
+ * If the given data matches a pattern, it will be formatted with respect to
+ * the first matched pattern. Otherwise, an error will be thrown indicating
+ * a validation failure.
+ *
+ * @param {string} data data to parse
+ *
+ * @return {string} formatted string, if a match is found
+ */
+ 'public parse': function( data )
+ {
+ var dfn = this._dfn;
+
+ // cast all data to a string
+ data = ''+( data );
+
+ var match;
+ for ( var i = 0, len = this._dfn.length; i < len; i += 2 )
+ {
+ if ( match = dfn[ i ].test( data ) )
+ {
+ var replace = dfn[ i + 1 ];
+
+ // minor optimization that may help on very large sets in
+ // poorly performing browsers; do not perform replacement if it
+ // it would result in an identical string
+ try
+ {
+ return ( ( replace === '$&' )
+ ? data
+ : data.replace( dfn[ i ], dfn[ i + 1 ] )
+ );
+ }
+ // throwing an exception within the replacement function is
+ // equivalent to saying "no match" and allows for more
+ // complicated logic that would otherwise overcomplicate a
+ // regex; fall through to continue to the next matcher
+ catch ( e ) {}
+ }
+ }
+
+ throw Error( 'No match for data: ' + data );
+ },
+
+
+ /**
+ * Retrieve data that may require formatting for display
+ *
+ * Return formatting is optional. No formatting will be done if no pattern
+ * was given when the instance was constructed.
+ *
+ * To ensure consistency and correctness, *any data returned by this method
+ * must be reversible* --- that is, parse( retrieve( data ) ) should not
+ * throw an exception.
+ *
+ * @param {string} data data to format for display
+ *
+ * @return {string} data formatted for display
+ */
+ 'public retrieve': function( data )
+ {
+ if ( !( this._retdfn ) )
+ {
+ return data;
+ }
+
+ return ( ''+data ).replace( this._retdfn[ 0 ], this._retdfn[ 1 ] );
+ }
+} );
+
diff --git a/test/validate/VFormatTest.js b/test/validate/VFormatTest.js
new file mode 100644
index 0000000..160822e
--- /dev/null
+++ b/test/validate/VFormatTest.js
@@ -0,0 +1,115 @@
+/**
+ * Test validator-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 .
+ */
+
+var liza = require( '../../' ),
+ Sut = liza.validate.VFormat,
+ expect = require( 'chai' ).expect,
+ assert = require( 'assert' ),
+ dfn = [
+ /^kitten/, '$&s',
+ /^[a-z]+/, 'Foo',
+ /^[0-9]([0-9]+)/, '$1',
+ ];
+
+
+describe( 'VFormat', function()
+{
+ describe( '#parse', function()
+ {
+ it( 'formats string according to given definition', function()
+ {
+ var sut = createSut( dfn );
+
+ // this first test also ensures that the very first match
+ // in dfn takes precedence
+ [
+ [ 'kitten', 'kittens' ],
+ [ 'abcd', 'Foo' ],
+ [ '0123', '123' ],
+ ].forEach( function( test )
+ {
+ assert.equal( sut.parse( test[ 0 ] ), test[ 1 ] );
+ } );
+ } );
+
+
+ // validation error
+ it( 'throws an exception if no match is found', function()
+ {
+ assert.throws( function()
+ {
+ // cannot possibly match anything
+ createSut( [] ).parse( 'foo' );
+ }, Error );
+ } );
+
+
+ /**
+ * To support complex logic that may be difficult to express
+ * (or not worth expressing due to verbosity required with JS's
+ * regex impl.), we permit throwing an exception in a
+ * replacement function to result in the equivalent of "no
+ * match".
+ */
+ it( 'yields no match given exception during replacement', function()
+ {
+ var val = 'bar',
+ sut = createSut( [
+ /^fo/, function() { throw Error( 'ignore me' ); },
+ /^foo/, val,
+ ] );
+
+ assert.equal( val, sut.parse( 'foo' ),
+ "Should ignore matches that throw exceptions"
+ );
+ } );
+ } );
+
+
+ describe( '#retrieve', function()
+ {
+ it( 'retrieval does not format data by default', function()
+ {
+ var str = 'foo';
+ assert.equal( createSut( [] ).retrieve( str ), str );
+ } );
+
+
+ it( 'formats return data according to given definition', function()
+ {
+ // the dfn is technically not required, but for the sake
+ // of a "proper" demonstration, it will be included
+ var dfn = [ [ /-/, '' ] ],
+ retdfn = [ /[a-z]/g, '$&-' ];
+
+ assert.equal(
+ createSut( dfn, retdfn ).retrieve( 'foo' ),
+ 'f-o-o-'
+ );
+ } );
+ } );
+} );
+
+
+function createSut( dfn, retdfn )
+{
+ return Sut( dfn, retdfn );
+}