/** * Responsive Mortgage Calculator Pro * http://liddweaver.com/responsive-mortgage-calculator-pro/ * * @package RMCP * @author David Wilder * @copyright Copyright (c) WovenCode Software Inc. * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License * @since 1.0.0 */ (function( window ) { var rmcp = (function() { var forms = {}, processors = {}, rmcp = function() { var r = this, tSep, dSep, dPlaces, pDPlaces, indSys, init = function() { // Set thousands separator, decimal separator, decimal places, Indian system switch ( rmcp_vars.settings.thousands_separator ) { case '{space}': tSep = ' '; break; case '.': tSep = '.'; break; default: tSep = ','; break; } switch ( rmcp_vars.settings.decimal_separator ) { case ',': dSep = ','; break; default: dSep = '.'; break; } dPlaces = r.clean.number( rmcp_vars.settings.decimal_places ); pDPlaces = r.clean.number( rmcp_vars.settings.percent_decimal_places ); indSys = ( rmcp_vars.settings.indian_system === 1 ) ? 1 : 0; }; this.addForm = function( form ) { var init = function() { var i = r.formIndex( form.id ); if ( ! i ) return; forms[i] = form; r.addEvent( 'submit', form, function(e) { e = e || window.event; r.process( getForm(e) ); r.preventDefault(e); }); }; init(); }; this.clean = { // Takes a number and a default number: function( n, d ) { if ( typeof n === 'number' ) { return n; } else if ( typeof n !== 'string' ) { return ( typeof d === 'number' ) ? d : 0; } n = n.replace( /[^\d.,]/g, '' ); // Replace . or , thousands separators if ( tSep === '.' ) { n = n.replace( '.', '' ); } else if ( tSep === ',' ) { n = n.replace( ',', '' ); } // Switch , decimal separator with . if ( dSep === ',' ) { console.log( n ); n = n.replace( ',', '.' ); } // Check for multiple . var dots = n.match( /\./g ); if ( dots && dots.length > 1 ) { n = n.slice( 0, n.indexOf( '.' ) ) + '.' + n.slice( n.indexOf( '.' ) + 1 ).replace( '.', '' ); } return +n; }, period: function( n, options, d ) { if ( options.indexOf( +n ) > 0 ) { return this.number( n ); } return ( typeof d === 'number' ) ? d : 0; }, text: function( n ) { return n.replace( //g, '>' ); } }; this.format = { integer: function( n ) { n = Math.ceil( +n ).toFixed( 0 ); return n.toString().replace( /\B(?=(\d{3})+(?!\d))/g, tSep ); }, number: function( n, dec ) { dec = dec || dPlaces; if ( typeof n !== 'number' ) n = 0; n = n.toFixed( dec ); n = n.toString().replace( /\B(?=(\d{3})+(?!\d))/g, tSep ); if ( dec > 0 && dSep === ',' ) { n = n.substring( 0, n.lastIndexOf(".") ) + ',' + n.substring( n.lastIndexOf(".") + 1 ); } return n; }, currency: function( n ) { if ( indSys === 1 ) { return r.format.indianSystem( n ); } return r.format.number( n, dPlaces ); }, percentage: function( n ) { return r.format.number( n, pDPlaces ); }, indianSystem: function( n ) { n = n.toFixed(0); return n.slice(0, -3).toString().replace(/\B(?=(\d{2})+(?!\d))/g, ',') + ',' + n.slice(-3); }, text: function( t ) { var tagBody = '(?:[^"\'>]|"[^"]*"|\'[^\']*\')*'; // Comment body. // Special "raw text" elements whose content should be elided. // Regular name var tagOrComment = new RegExp( '<(?:' + '!--(?:(?:-*[^->])*--+|-?)' + '|script\\b' + tagBody + '>[\\s\\S]*?[\\s\\S]*?', 'gi' ); var oldHtml; do { oldHtml = html; html = html.replace( tagOrComment, '' ); } while ( html !== oldHtml ); return html.replace(/= def.length ) { return false; } if ( ! canProcess( 'Processor', def[pCount] ) ) { pError = true; return false; } // Run the processor. Manage errors. var pass = processors[ def[pCount].element ].fn( def[pCount], sub, form ); if ( ! pass ) { pError = true; return false; } pCount++; return true; }; var runCalculator = function( def, sub, form ) { cError = false; if ( cCount >= def.length ) { return false; } if ( ! canProcess( 'Calculator', def[cCount] ) ) { cError = true; return false; } // Run the processor. Manage errors. var pass = processors[ def[cCount].element ].fn( def[cCount], sub, form ); if ( ! pass ) { cError = true; return false; } cCount++; return true; }; this.processSubcalculations = function( input, def, form ) { if ( ! canProcess( 'Subcalculation', def ) ) { return false; } return processors[ def.element ].fn( input, def, form ); }; var canProcess = function( dName, def ) { // Make sure the subcalculation definition has a field type if ( ! r.has( def, 'element' ) ) { // Debug message r.d( dName + ' definition is missing element type.', 'error' ); return false; } // Make sure the subcalculation processor is registered else if ( ! r.has( processors, def.element ) ) { // Debug message r.d( 'No processor for element type: ' + def.element + '.', 'warning' ); return false; } return true; }; var processOutput = function( def, form ) { // Debug message r.d( 'Output processing started.' ); for ( var i = 0, c = def.length; i < c; i++ ) { // Make sure the element has a field type if ( ! r.has( def[i], 'element' ) ) { // Debug message r.d( 'Interface element definition is missing element type.', 'error' ); return false; } // Make sure the processor is registered else if ( ! r.has( processors, def[i].element ) ) { // Debug message r.d( 'No processor for element type: ' + def[i].element + '.', 'warning' ); continue; } // Check if processor is skipped else if ( processors[ def[i].element ].isInput ) { // Debug message r.d( 'Output processing skipped for element type: ' + def[i].element + '.' ); continue; } // Process a output. Manage display. processors[ def[i].element ].fn( i, def[i], form ); } }; var tags = {}; this.setTag = function( tag, value, fn ) { fn = ( typeof fn !== 'undefined' ) ? fn : null; tags[tag] = { value: value, fn: fn }; }; this.getTagValue = function( tag ) { return ( typeof tags[tag] !== 'undefined' ) ? tags[tag].value : ''; }; this.getTagFormattedValue = function( tag ) { if ( typeof tags[tag] === 'undefined' || ! tags[tag].fn ) { return ''; } return tags[tag].fn( tags[tag].value ); }; this.getTagCallback = function( tag ) { return ( typeof tags[tag] !== 'undefined' ) ? tags[tag].fn : null; }; var getForm = function(e) { var id = e.target.id, i = r.formIndex( id ); if ( typeof forms[i] !== 'undefined' ) { return forms[i]; } return null; }, inputId = function( form_id, input_id ) { return 'rmcp_input[' + form_id + '][' + input_id + ']'; }; // Check if an object has a property this.has = function( ob, prop ) { return ( ob && ob.hasOwnProperty( prop ) ); }; this.getVal = function( ob, prop ) { return ( r.has( ob, prop ) ) ? ob[prop] : null; }; this.objVals = function( obj ) { var newObj = [], shifted, i = 0, p; for ( p in obj ) { newObj[i] = obj[p]; i++; } return newObj; }; // Get a definition for a calculator this.getDefinition = function( i ) { return ( typeof rmcp_vars.definitions[i] === 'object' ) ? rmcp_vars.definitions[i] : null; }; // Add event listener this.addEvent = function( type, object, callback ) { if ( object && object.addEventListener ) { object.addEventListener( type, callback, false ); } else if ( object && object.attachEvent ) { object.attachEvent( 'on' + type, callback ); } }; // Remove an event this.removeEvent = function( type, object, callback ) { if ( object && object.removeEventListener ) { object.removeEventListener( type, callback, false ); } else if ( object && object.detachEvent ) { object.detachEvent( 'on' + type, callback ); } }; // Prevent submission this.preventDefault = function( e ) { if ( e.preventDefault ) { e.preventDefault(); } else { e.returnValue = false; } return false; }; // Get a form index this.formIndex = function( id ) { return id.replace( 'rmcp-form-', '' ); }; // Register a processor this.registerProcessor = function( fieldType, section, fn, isInput ) { processors[fieldType] = { section: section, fn: fn, isInput: isInput }; }; // Set an error on a form element this.setError = function( form, input_id, message ) { // Debug message r.d( 'Error occurred on input ' + input_id + ' with message: ' + message ); var iId = inputId( r.formIndex( form.id ), input_id ); var input = form[iId]; var par = r.getParentByClassName( input, 'rmcp-input' ); var eSpan = r.getDescendentByClassName( par, 'rmcp-error' ); r.addClass( par, 'rmcp-input-error'); eSpan.innerHTML = message; r.addClass( eSpan, 'rmcp-error-active' ); }; // Remove an error this.removeError = function( form, input_id ) { var iId = inputId( r.formIndex( form.id ), input_id ); var input = form[iId]; var par = r.getParentByClassName( input, 'rmcp-input' ); var eSpan = r.getDescendentByClassName( par, 'rmcp-error' ); r.removeClass( par, 'rmcp-input-error' ); eSpan.innerHTML = ''; r.removeClass( eSpan, 'rmcp-error-active' ); }; // Set a form error this.setFormError = function( form, message ) { // Debug message r.d( 'Settings form error: ' + message ); // Check for an existing error div var eDiv = r.getDescendentByClassName( form, 'rmcp-form-error-report' ), ul, par, li, sButton, n, i, c; if ( typeof eDiv === 'undefined' ) { // Create a new error div // Get the last submit button for ( i = 0, c = form.length; i < c; i++ ) { if ( form[i].type === 'submit' ) { sButton = form[i]; n = i; } } par = r.getParentByClassName( sButton, 'rmcp-input' ); eDiv = document.createElement( 'div'); eDiv.className = 'rmcp-form-error-report'; ul = document.createElement( 'ul' ); eDiv.appendChild( ul ); par.parentNode.insertBefore( eDiv, par ); } else { ul = eDiv.getElementsByTagName( 'ul' )[0]; } // Don't repeat errors for ( i = 0, c = ul.childNodes.length; i < c; i++ ) { if ( ul.childNodes[i].innerHTML === message ) { return; } } li = document.createElement( 'li' ); li.innerHTML = message; ul.appendChild( li ); }; // Remove all form errors this.removeFormErrors = function( form ) { var eDiv = r.getDescendentByClassName( form, 'rmcp-form-error-report' ); if ( eDiv ) { eDiv.parentNode.removeChild( eDiv ); } }; this.getParentByClassName = function( e, classToFind ) { var par; var found = false; var limit = false; while ( ! found || ! limit ) { par = e.parentNode; if ( par.className && par.className.match(new RegExp('(\\s|^)' + classToFind + '(\\s|$)') ) ) { found = true; return par; } else { e = par; } if ( typeof par === 'undefined' ) { limit = true; } } }; this.getDescendentByClassName = function( e, classToFind ) { for ( var i = 0, c = e.childNodes.length; i < c; i++ ) { if ( e.childNodes[i].className && e.childNodes[i].className.indexOf( classToFind ) > -1 ) { return e.childNodes[i]; } } }; this.addClass = function( el, classToAdd ) { var reg = new RegExp('(\\s|^)' + classToAdd + '(\\s|$)'); if ( ! el.className.match( reg ) ) { el.className += ' ' + classToAdd; } }; this.removeClass = function( el, classToRemove ) { var reg = new RegExp('(\\s|^)' + classToRemove + '(\\s|$)'); el.className = el.className.replace( reg, '' ); }; // Debug logger this.d = function( message, level ) { if ( rmcp_vars.settings.debug === 1 ) { var prefix = 'DEBUG LOG: '; switch ( level ) { case 'error': prefix = '!' + prefix; break; case 'warning': prefix = '*' + prefix; break; } console.log( prefix + message ); } }; // Development log var l = function( data ) { console.log( data ); }; init(); }; return new rmcp(); }( rmcp_vars )); window.rmcp = rmcp; })( window ); // Start of Register Processors (function() { // Text Input rmcp.registerProcessor( 'text_input', 'interface', function( i, input, def, form ) { var format = 'number'; // Input value not set if ( typeof input === 'undefined' ) { input = 0; } // Clean the input value if ( rmcp.getVal( def, 'format') === 'text' ) { input = rmcp.clean.text( input ); } else { input = rmcp.clean.number( input ); } // Input is required if ( rmcp.getVal( def, 'required' ) === 1 ) { if ( ! input ) { // Debug message rmcp.d( 'Input value failed for ' + rmcp.getVal( def, 'label' ) + '.' ); rmcp.setError( form, i, rmcp.getVal( def, 'error_message' ) ); return false; } } // Set the format if ( rmcp.getVal( def, 'format' ) ) { format = rmcp.getVal( def, 'format' ); } tag = rmcp.getVal( def, 'tag' ); if ( tag ) { rmcp.setTag( tag, input, format ); } // Debug message rmcp.d( 'Input for ' + rmcp.getVal( def, 'label' ) + ' set to ' + input + '.' ); rmcp.removeError( form, i ); return true; }, true ); // Select Input rmcp.registerProcessor( 'select_input', 'interface', function( i, input, def, form ) { var format = 'number'; // Make sure a value is set if ( typeof input === 'undefined' || typeof def.options === 'undefined' ) { rmcp.setError( form, i, 'Options not set for input ' + i + ': ' + rmcp.getVal( def, 'label' ) + '.' ); return false; } var val = def.options[0].option_value; // Make sure the value is one of the options for ( var j = 0, c = def.options.length; j < c; j++ ) { if ( input === def.options[j].option_value ) { val = def.options[j].option_value; break; } } // Set the format if ( rmcp.getVal( def, 'format' ) ) { format = rmcp.getVal( def, 'format' ); } tag = rmcp.getVal( def, 'tag' ); if ( tag ) { rmcp.setTag( tag, val, format ); } // Debug message rmcp.d( 'Input for ' + rmcp.getVal( def, 'label' ) + ' set to ' + input + '.' ); rmcp.removeError( form, i ); return true; }, true ); // Compounding Period Input rmcp.registerProcessor( 'compounding_period', 'interface', function( i, input, def, form ) { // Make sure a value is set if ( typeof input === 'undefined' ) { rmcp.setError( form, i, 'Options not set for input ' + i + ': ' + rmcp.getVal( def, 'label' ) + '.' ); return false; } // Look for allowed periods var val; switch ( input ) { case 0: val = 0; break; case 1: val = 1; break; case 2: val = 2; break; case 4: val = 4; break; case 26: val = 26; break; case 52: val = 52; break; case 365: val = 365; break; default: val = 12; break; } tag = rmcp.getVal( def, 'tag' ); if ( tag ) { rmcp.setTag( tag, input, 'integer' ); } // Debug message rmcp.d( 'Input for ' + rmcp.getVal( def, 'label' ) + ' set to ' + input + '.' ); rmcp.removeError( form, i ); return true; }, true ); // Payment Period Input rmcp.registerProcessor( 'payment_period', 'interface', function( i, input, def, form ) { // Make sure a value is set if ( typeof input === 'undefined' ) { rmcp.setError( form, i, 'Options not set for input ' + i + ': ' + rmcp.getVal( def, 'label' ) + '.' ); return false; } // Look for allowed periods var val; switch ( input ) { case 1: val = 1; break; case 2: val = 2; break; case 4: val = 4; break; case 26: val = 26; break; case 52: val = 52; break; case 365: val = 365; break; default: val = 12; break; } tag = rmcp.getVal( def, 'tag' ); if ( tag ) { rmcp.setTag( tag, input, 'integer' ); } // Debug message rmcp.d( 'Input for ' + rmcp.getVal( def, 'label' ) + ' set to ' + input + '.' ); rmcp.removeError( form, i ); return true; }, true ); // Results Output rmcp.registerProcessor( 'results', 'interface', function( i, def, form ) { var content = '', process = function() { // Debug message rmcp.d( 'Results Processor: ' + rmcp.getVal( def, 'label') + ' started.' ); // Make sure there is content to operate on if ( ! def.content ) { // Debug message rmpc.d( 'No content to process in Results Processor: ' + rmcp.getVal( def, 'label') + '.', 'warning' ); return; } // Set data for replacing content = def.content; // Strip slashes from quotes stripSlashes(); // Look for conditionals processConditionals(); // Look for remaining tags processTags(); // Turn new lines without tags into breaks processNewLines(); // Add the content to the form var div = rmcp.getDescendentByClassName( form, 'rmcp-results-' + i ); if ( div ) { div.innerHTML = content; } }, stripSlashes = function() { content = content.replace( /\'/g, "'" ); }, processConditionals = function() { // Condition regular expression: {if tag}...{endif} var matches, matched = true, tag, val; // Get all matches while ( matched ) { matches = content.match( /{if(.*?)}(.*?){endif}/i ); if ( matches ) { tag = matches[1].trim(); val = rmcp.getTagValue( tag ); if ( val || tag.indexOf( 'compounding_period' ) > -1 ) { content = content.replace( matches[0], matches[2] ); } else { content = content.replace( matches[0], '' ); } } else { matched = false; } } }, processTags = function() { // Tag regular expression: {tag} var matches, matched = true, tag, val, fn, re, done = []; // Get all matches while ( matched ) { matches = content.match( /{(.*?)}/i ); if ( matches ) { tag = matches[1].trim(); // Don't try to replace the same tag twice if ( done.indexOf( tag ) > -1 ) { continue; } val = rmcp.getTagValue( tag ); fn = rmcp.getTagCallback( tag ); re = new RegExp( matches[0], 'g' ); if ( typeof fn === 'function' ) { content = content.replace( re, fn( tag, val ) ); } else { content = content.replace( re, replaceTag( tag, val, fn ) ); } done.push( tag ); } else { matched = false; } } }, replaceTag = function( tag, val, fn ) { // Choose the correct format for replacing switch ( fn ) { case 'compounding_period_text': return replaceCompoundingPeriodText( tag, val ); case 'payment_period_text': return replacePaymentPeriodText( tag, val ); case 'amortization_period_text': return replaceAmortizationPeriodText( tag, val ); case 'text': return val; case 'percentage': return rmcp.format.number( val, 2 ); case 'currency': return rmcp.format.currency( val ); case 'integer': return rmcp.format.integer( val ); default: return rmcp.format.number( val ); } }, replaceCompoundingPeriodText = function( tag, val ) { var n; // Choose the correct text switch( val ) { case 0: n = 'no_period_text'; break; case 1: n = 'annual_period_text'; break; case 2: n = 'semi_annual_period_text'; break; case 4: n = 'quarterly_period_text'; break; case 12: n = 'monthly_period_text'; break; case 26: n = 'bi_weekly_period_text'; break; case 52: n = 'weekly_period_text'; break; case 365: n = 'daily_period_text'; break; } // Get the replacement from the definition or just use the value return ( def[n] ) ? def[n] : val; }, replacePaymentPeriodText = function( tag, val ) { var n; // Choose the correct text switch( val ) { case 1: n = 'annual_payment_text'; break; case 2: n = 'semi_annual_payment_text'; break; case 4: n = 'quarterly_payment_text'; break; case 12: n = 'monthly_payment_text'; break; case 26: n = 'bi_weekly_payment_text'; break; case 52: n = 'weekly_payment_text'; break; case 365: n = 'daily_payment_text'; break; } // Get the replacement from the definition or just use the value return ( def[n] ) ? def[n] : val; }, replaceAmortizationPeriodText = function( tag, val ) { // Choose the correct format depending on the payment period var payment_period = getPaymentPeriodForAmortizationPeriod( tag ); // Get the years, months and weeks values var times = getYearsMonthsAndWeeksFromAmortizationPeriod( val, payment_period ); // Get the text to use to replace the tag var term_text = getAmortizationPeriodText( times.years, times.months, times.weeks ); // Replace years, months and weeks tags term_text = term_text.replace( '{years}', times.years ); term_text = term_text.replace( '{months}', times.months ); return term_text.replace( '{weeks}', times.weeks ); }, getPaymentPeriodForAmortizationPeriod = function( tag ) { // Find position for 'amortization_period_text' var pos = tag.indexOf( 'amortization_period_text' ); // Text not found. Return default payment period if ( pos < 0 ) { return 12; } // Get the prefix and prepend to 'payment_period' var prefix = tag.substring( 0, pos ).substring( 1 ); var pp_tag = prefix + 'payment_period'; // Get the data based on this payment period tag var pp_val = rmcp.getTagValue( pp_tag ); return ( pp_val ) ? pp_val : 12; }, getYearsMonthsAndWeeksFromAmortizationPeriod = function( amortization_period, payment_period ) { // Set base values var years = Math.floor( amortization_period ); var months = 0, weeks = 0; // Set the remainder if years is a decimal var remainder = amortization_period - years; // Check for months or weeks values if ( remainder > 0 ) { // Calculate weeks if ( +payment_period === 26 || +payment_period === 52 ) { weeks = ( 52 / payment_period ) * Math.ceil( remainder * payment_period ) / payment_period; } // Calculate weeks if daily payment else if ( +payment_period === 365 ) { weeks = Math.ceil( remainder * 52 ) / 52; } // Calculate months else { months = ( 12 / payment_period ) * Math.ceil( remainder * payment_period ); } // Check for rounding up to the next year if ( months === 12 || weeks === 52) { years++; months = weeks = 0; } } return { years: years, months: months, weeks: weeks }; }, getAmortizationPeriodText = function( years, months, weeks ) { // Definition key base var n = 'term_'; // Attach the correct year identifier if ( +years === 1 ) { n += 'sy'; } else if ( years > 1 ) { n += 'py'; } // Attach the correct month or week identifier if ( +months === 1 ) { n += 'sm'; } else if ( months > 1 ) { n += 'pm'; } else if ( +weeks === 1 ) { n += 'sw'; } else if ( weeks > 1 ) { n += 'pw'; } return ( def[n] ) ? def[n] : ''; }, processNewLines = function() { // Define allowable tags to look for var blocks = '([table|thead|tfoot|caption|col|colgroup|tbody|tr|td|th|div|dl|dd|dt|ul|ol|li|pre|p|select|option|form|map|area|blockquote|address|p|h[1-6]|hr|fieldset|legend|section|article|aside|hgroup|header|footer|nav|figure|figcaption|details|menu|summary])'; // Split content on new lines var pattern, re = new RegExp( /\r\n|\r|\n/g ), ps = content.split( re ), p = '', c = ''; // Add paragraphs to all lines for ( var i = 0, count = ps.length; i < count; i++ ) { p = ps.shift().trim(); // Ignore empty lines if ( p !== '' ) { c += '

' + p + '

\n'; } } // Remove paragraphs from block level elements re = new RegExp( '

<(/?' + blocks + ')', 'ig' ); c = c.replace( re, '<' + '$1' ); re = new RegExp( blocks + '>

', 'ig' ); c = c.replace( re, '$1' + '>' ); content = c; }; process(); }, false ); // Processor rmcp.registerProcessor( 'processor', 'processors', function( def, sub, form ) { var error = false, data = {}, output, fieldTypes = { 0: 'logic_value_1', 1: 'logic_value_2', 2: 'default', 3: 'math_value_1', 4: 'math_value_2', }, process = function() { // Debug message rmcp.d( 'Processor: ' + rmcp.getVal( def, 'label' ) + ' started.' ); createDataArray(); if ( error ) { // Debug message rmcp.d( 'Insufficient data for Processor: ' + rmcp.getVal( def, 'label' ) + '.', 'warning' ); return false; } if ( ! processLogic() ) { processFailure(); if ( error ) { return false; } } else { // Do the math processMath(); } setTag(); return true; }, createDataArray = function() { // Loop through field types for ( var i = 0, c = 5; i < c; i++ ) { var type = fieldTypes[i], expected = getValueAndExpectedType( rmcp.getVal( def, type ) ); data[type] = null; switch ( expected[1] ) { case 'tag': var tagVal = rmcp.getTagValue( expected[0] ); if ( type === 'default' ) { if ( !!tagVal || tagVal === 0 ) { data[type] = +tagVal; } else if ( defaultIsRequired() ){ // Debug message rmcp.d( 'Tag for default value not available for Processor: ' + rmcp.getVal( def, 'label' ) + '. Required by ' + type + '.', 'warning' ); error = true; } } else { if ( !!tagVal || tagVal === 0 ) { data[type] = +tagVal; } else { // Debug message rmcp.d( 'Tag for ' + expected[0] + ' not available for Processor: ' + rmcp.getVal( def, 'label' ) + '. Required by ' + type + '.', 'warning' ); error = true; } } break; case 'number': data[type] = +expected[0]; break; case 'text': data[type] = expected[0]; break; default: if ( type === 'default' && defaultIsRequired() ) { // Debug message rmcp.d( 'A value or tag must be provided for Processor: ' + rmcp.getVal( def, 'label' ) + '.' ); error = true; } break; } } }, getValueAndExpectedType = function( value ) { // Look for a null value if ( ! value && value !== 0 ) { return [ null, 'null' ]; } // Test for a number if ( rmcp.clean.number( value ) === +value ) { return [ value, 'number' ]; } // Check for an existing tag if ( value.match( /^{.*?}$/ ) ) { return [ value.replace( /[{}]/g, '' ), 'tag' ]; } // Assume it's text return [ value, 'text' ]; }, defaultIsRequired = function() { // Both logic values must be provided to require a default if ( typeof data.logic_value_1 !== 'number' || typeof data.logic_value_2 !== 'number' ) { return false; } // Throws an error on failure, so no default needed if ( rmcp.getVal( def, 'on_failure' ) !== 1 ) { return false; } return true; }, processLogic = function() { var v1 = data.logic_value_1, v2 = data.logic_value_2, operator = rmcp.getVal( def, 'logic_operator' ); if ( ! operator || ( ! v1 && v1 !== 0 ) || ( ! v2 && v2 !== 0 ) ) { return true; } switch ( operator ) { case '=': return ( v1 === v2 ) ? true : false; case '!=': return ( v1 !== v2 ) ? true : false; case '>': return ( v1 > v2 ) ? true : false; case '>=': return ( v1 >= v2 ) ? true : false; case '<': return ( v1 < v2 ) ? true : false; case '<=': return ( v1 <= v2 ) ? true : false; case '&&': return ( v1 && v2 ) ? true : false; case '||': return ( v1 || v2 ) ? true : false; case '!&&': return ( ! v1 && ! v2 ) ? true : false; case '!||': return ( ! v1 || ! v2 ) ? true : false; default: return false; } }, processFailure = function() { var onFailure = rmcp.getVal( def, 'on_failure' ); if ( +onFailure === 1 ) { if ( ! data.default ) { output = 0; } else { output = data.default; } // Debug message rmcp.d( 'Set Failure Value for Processor: ' + rmcp.getVal( def, 'label' ) + ' to ' + output + '.', 'warning' ); } else { // Debug message rmcp.d( 'Logic failed gracefully for Processor: ' + rmcp.getVal( def, 'label' ) + '.' ); rmcp.setFormError( form, rmcp.getVal( def, 'error_message' ) ); error = true; } }, processMath = function() { var v1 = +data.math_value_1, v2 = +data.math_value_2, operator = rmcp.getVal( def, 'math_operator' ); if ( ! operator || typeof v1 !== 'number' || typeof v2 !== 'number' ) { return false; } switch ( operator ) { case '+': output = ( v1 + v2 ); break; case '-': output = ( v1 - v2 ); break; case '*': output = ( v1 * v2 ); break; case '/': if ( v2 === 0 ) { output = 0; // Debug message rmcp.d( 'Division by zero for Processor: ' + rmcp.getVal( def, 'label' ) + '.', 'warning' ); } else { output = ( v1 / v2 ); } break; default: output = 0; break; } return true; }, // Set the tag for output setTag = function() { var format = 'number'; // Set the format if ( rmcp.getVal( def, 'format' ) ) { format = rmcp.getVal( def, 'format' ); } tag = rmcp.getVal( def, 'tag' ); if ( tag ) { rmcp.setTag( tag, output, format ); } }; return process(); }, false ); // Switch Assigner processor rmcp.registerProcessor( 'switch_assigner', 'processors', function( def, sub, form ) { var error = false, data = { switch_input: null, switch_cases: {}, default_value: null }, result, process = function() { // Debug message rmcp.d( 'Switch Assigner: ' + rmcp.getVal( def, 'label' ) + ' started.' ); createDataArray(); if ( error ) { // Debug message rmcp.d( 'Insufficient data for Switch Assigner: ' + rmcp.getVal( def, 'label' ) + '.', 'warning' ); return false; } if ( ! processLogic() ) { processFailure(); if ( error ) { return false; } } setTag( result ); return true; }, createDataArray = function() { var switch_input, expected, switch_cases, switch_case, switch_assignment, case_key, case_opt, default_value, i, count; // Set the switch input value switch_input = rmcp.getVal( def, 'switch_input' ); expected = getValueAndExpectedType( switch_input ); data.switch_input = getValue( expected[0], expected[1], 'switch_input' ); // If there is no switch input value, ignore it if ( ! data.switch_input && data.switch_input !== 0 ) { return true; } // Set cases switch_cases = rmcp.getVal( def, 'switch_cases' ); // Make sure there are cases if ( ! switch_cases ) { return true; } // Loop through cases to set up options for ( i = 0, count = switch_cases.length; i < count; i++ ) { // Get the case and options switch_case = rmcp.getVal( switch_cases[i], 'switch_case' ); switch_assignment = rmcp.getVal( switch_cases[i], 'switch_assignment' ); // Get the value for the case expected = getValueAndExpectedType( switch_case ); case_key = getValue( expected[0], expected[1], 'switch_case' ); // Get the value for the option/assignment expected = getValueAndExpectedType( switch_assignment ); case_opt = getValue( expected[0], expected[1], 'switch_assignment' ); // Check for an error if ( error ) { return false; } if ( case_key || case_key === 0 ) { data.switch_cases[case_key] = case_opt; } } // Assign the default default_value = rmcp.getVal( def, 'default' ); expected = getValueAndExpectedType( default_value ); data.default_value = getValue( expected[0], expected[1], 'default' ); }, getValueAndExpectedType = function( value ) { // Look for a null value if ( ! value && value !== 0 ) { return [ null, 'null' ]; } // Test for a number if ( rmcp.clean.number( value ) === +value ) { return [ value, 'number' ]; } // Check for an existing tag if ( value.match( /^{.*?}$/ ) ) { return [ value.replace( /[\{\}]/g, '' ), 'tag' ]; } // Assume it's text return [ value, 'text' ]; }, getValue = function( value, expected_type, setting ) { switch ( expected_type ) { case 'tag': var tagVal = rmcp.getTagValue( value ); if ( setting === 'default' ) { if ( !!tagVal || tagVal === 0 ) { return tagVal; } else if ( defaultIsRequired() ){ // Debug message rmcp.d( 'Tag for default value not available for Processor: ' + rmcp.getVal( def, 'label' ) + '. Required by ' + setting + '.', 'warning' ); error = true; } // Return a null value for default if a tag value is not found return null; } else { if ( !!tagVal || tagVal === 0 ) { return tagVal; } else { // Debug message rmcp.d( 'Tag for ' + value + ' not available for Processor: ' + rmcp.getVal( def, 'label' ) + '. Required by ' + setting + '.', 'warning' ); error = true; return null; } } break; case 'number': return value; case 'text': return value; default: if ( setting === 'default' && defaultIsRequired() ) { // Debug message rmcp.d( 'A value or tag must be provided for Processor: ' + rmcp.getVal( def, 'label' ) + '.' ); error = true; } return null; } }, defaultIsRequired = function() { // Throws an error on failure, so no default needed if ( rmcp.getVal( def, 'on_failure' ) !== 1 ) { return false; } return true; }, processLogic = function() { var c; // Make sure the switch input is not null if ( ! data.switch_input && data.switch_input !== 0 ) { return false; } // Loop through cases until a match is found for ( c in data.switch_cases ) { if ( c.toString() === data.switch_input.toString() ) { result = data.switch_cases[c]; return true; } } // If we've made it this far, then no cases matched. Process an error return false; }, processFailure = function() { var onFailure = rmcp.getVal( def, 'on_failure' ); if ( onFailure === 1 ) { if ( ! data.default ) { output = 0; } else { output = data.default; } } else { // Debug message rmcp.d( 'Logic failed gracefully for Processor: ' + rmcp.getVal( def, 'label' ) + '.' ); rmcp.setFormError( form, rmcp.getVal( def, 'error_message' ) ); error = true; } }, setTag = function( value ) { var format = 'number'; // Set the format if ( rmcp.getVal( def, 'format' ) ) { format = rmcp.getVal( def, 'format' ); } tag = rmcp.getVal( def, 'tag' ); if ( tag ) { rmcp.setTag( tag, value, format ); } }; return process(); }, false ); // Cost Ancillary Calculation rmcp.registerProcessor( 'cost', 'subcalculations', function( input, def, form ) { var output = 0, rate_basis, rate_value, range_basis, range_value, marginal, process = function() { // Debug message rmcp.d( 'Cost Processor: ' + rmcp.getVal( def, 'label' ) + ' started.' ); setBaseCost(); setBaseRateCost(); setRangeCost(); // Debug message rmcp.d( rmcp.getVal( def, 'label' ) + ' set to ' + output + '.' ); return output; }, setBaseCost = function() { output += +rmcp.clean.number( rmcp.getVal( def, 'base_cost' ) ); }, setBaseRateCost = function() { var base_rate = +rmcp.clean.number( rmcp.getVal( def, 'base_rate' ) ); if ( ! base_rate ) { return; } var basis = rmcp.getVal( def, 'rate_basis' ); switch ( basis ) { case 'property_value': rate_basis = 'property_value'; break; default: rate_basis = 'loan_amount'; break; } if ( ! input[ basis ] ) { return; } output += ( base_rate * +input[ basis ] / 100 ); }, setRangeCost = function() { if ( ! def.rate_ranges || def.rate_ranges.length < 1 ) { return; } rate_basis = getRateBasis(); if ( ! input[ rate_basis ] ) { return; } rate_value = input[ rate_basis ]; range_basis = getRangeBasis(); range_value = getRangeBasisValue( range_basis ); var m = rmcp.getVal( def, 'marginal' ); marginal = ( m && m === 1 ); var range, b, // bottom t, // top type, increment, cost = 0; for ( var i = 0, c = def.rate_ranges.length; i < c; i++ ) { range = def.rate_ranges[i]; b = ( range.bottom_of_range ) ? range.bottom_of_range : null; t = ( range.top_of_range ) ? range.top_of_range : null; rate = ( range.rate ) ? range.rate : 0; type = ( range.type ) ? range.type : 'rate'; increment = ( range.increment ) ? range.increment : null; // Range costs accumulate for the portion within the range if ( marginal ) { cost += +calculateCostForRange( rate, type, getValueWithinRange( b, t ), increment ); } // Range costs are not cumulative else if ( isInRange( b, t, range_value ) ) { cost = +calculateCostForRange( rate, type, rate_value, increment ); } } // Add the range result to the total result output += cost; }, calculateCostForRange = function( rate, type, basis, increment ) { if ( type === 'fee' ) { if ( increment ) { var number_of_increments = Math.ceil( basis / increment ); return number_of_increments * rate; } else { return rate; } } else { return ( basis * rate / 100 ); } }, getValueWithinRange = function( b, t ) { // If the rate basis value isn't set, set it to zero if ( rate_value === 0 ) return 0; // If the bottom isn't set, set it to zero if ( ! b ) b = 0; // Determine how the comparison is made: dollar amount or percentage var comparator, value; // LTV comparison (percentage) if ( range_basis === 'ltv' ) { comparator = range_value; } // Currency comparison (currency) else { comparator = rate_value; } // Comparisons if ( ! b && ! t ) { value = comparator; } else if ( comparator <= b ) { value = 0; } else if ( ! t && comparator > b ) { value = comparator - b; } else if ( ! b && comparator <= t ) { value = comparator; } else if ( comparator > b && comparator <= t ) { value = ( comparator - b ); } else if ( comparator >= t ) { value = Math.abs( t - b ); // Debug message if ( value !== ( t - b ) ) { // Debug message rmcp.d( 'Bottom of Range is greater than Top of Range in Cost Process: ' + rmcp.getVal( def, 'label' ) + '.', 'error' ); } } else if ( comparator > t ) { value = t; } else { value = 0; } // Convert LTV value to currency for calculations if ( range_basis === 'ltv' ) { value *= data.property_value; } return value; }, isInRange = function( b, t, value ) { // BOTTOM and TOP not set // BOTTOM not set and below TOP // TOP not set and above BOTTOM // Above BOTTOM and below TOP var inRange = ( ( ! b && ! t ) || ( ! b && value <= t ) || ( ! t && value >= b ) || ( value >= b && value <= t ) ); return inRange; }, getRateBasis = function() { return ( def.rate_basis ) ? def.rate_basis : 'loan_amount'; }, getRangeBasis = function() { return ( def.range_basis ) ? def.range_basis : 'loan_amount'; }, getRangeBasisValue = function( range_basis ) { switch ( range_basis ) { case 'ltv': if ( input.property_value === 0 ) { return 0; } return ( 100 * input.loan_amount / input.property_value ); case 'property_value': return input.property_value; default: return input.loan_amount; } }; return process(); }, false ); // Payment Calculator rmcp.registerProcessor( 'payment_calculator', 'calculators', function( def, sub, form ) { var error = false, data = {}, coefs = {}, output, fieldTypes = { 0: 'property_value', 1: 'loan_amount', 2: 'payment_period', 3: 'compounding_period', 4: 'interest_rate', 5: 'amortization_period' }, process = function() { // Debug message rmcp.d( 'Calculator: ' + rmcp.getVal( def, 'label' ) + ' started.' ); result = parseCalculatorData( def, fieldTypes, 6 ); data = result.data; error = result.error; if ( error ) { return false; } coefs = calculateMortgageCoefficients( data.interest_rate, data.payment_period, data.amortization_period, data.compounding_period ); output = calculate( data, coefs ); setCalculatorTags( data, coefs, def ); setResultTags( output, def ); runSubcalculations( data.property_value, data.loan_amount, def, sub, form ); return true; }, calculate = function( d, c ) { // Compound Interest if ( c.r_factor && c.r_factor !== 1 ) { return d.loan_amount * ( c.interest_rate_per_period * c.r_factor / ( c.r_factor - 1 ) ); } // Simple Interest if ( c.number_of_payments > 0 ) { return d.loan_amount * ( ( 1 + c.nominal_interest_rate ) / c.number_of_payments ); } // No Interest return d.loan_amount; }, setResultTags = function( o, def ) { var pre = rmcp.getVal( def, 'tag' ) + '_'; rmcp.setTag( pre + 'payment_result', output ); }; return process(); }, false ); // Loan Calculator rmcp.registerProcessor( 'loan_calculator', 'calculators', function( def, sub, form ) { var error = false, data = {}, coefs = {}, output, fieldTypes = { 0: 'payment_amount', 1: 'payment_period', 2: 'compounding_period', 3: 'interest_rate', 4: 'amortization_period' }, process = function() { // Debug message rmcp.d( 'Calculator: ' + rmcp.getVal( def, 'label' ) + ' started.' ); result = parseCalculatorData( def, fieldTypes, 5 ); data = result.data; error = result.error; if ( error ) { return false; } coefs = calculateMortgageCoefficients( data.interest_rate, data.payment_period, data.amortization_period, data.compounding_period ); output = calculate( data, coefs ); setCalculatorTags( data, coefs, def ); setResultTags( output, def ); runSubcalculations( output, output, def, sub, form ); return true; }, calculate = function( d, c ) { // Compound Interest if ( c.r_factor && c.r_factor !== 1 ) { return d.payment_amount / ( c.interest_rate_per_period * c.r_factor / ( c.r_factor - 1 ) ); } // Simple Interest else if ( c.number_of_payments > 0 ) { return d.payment_amount / ( ( 1 + c.nominal_interest_rate ) / c.number_of_payments ); } // No Interest else { return d.payment_amount; } }, setResultTags = function( o, def ) { var pre = rmcp.getVal( def, 'tag' ) + '_'; rmcp.setTag( pre + 'loan_result', output ); rmcp.setTag( pre + 'property_value', output ); rmcp.setTag( pre + 'loan_amount', output ); }; return process(); }, false ); // Calculator helpers var parseCalculatorData = function( def, fieldTypes, n ) { var data = {}, error = false, process = function() { setData(); return { data: data, error: error }; }, setData = function() { for ( var i = 0, c = n; i < c; i++ ) { var type = fieldTypes[i]; expected = getValueAndExpectedFieldType( type, def ); data[type] = null; switch ( expected[1] ) { case 'tag': var tagVal = rmcp.getTagValue( expected[0] ); if ( tagVal || tagVal === 0 ) { data[type] = clean( type, tagVal ); } else { // Debug message rmcp.d( 'Tag for ' + expected[0] + ' not available for Calculator: ' + rmcp.getVal( def, 'label' ) + '. Required by ' + type + '.', 'warning' ); error = true; } break; case 'number': data[type] = clean( type, expected[0] ); break; default: break; } } if ( rmcp.getVal( def, 'field_type' ) === 'payment_calculator' ) { if ( ! data.property_value ) { data.property_value = data.loan_amount; } else if ( ! data.loan_amount ) { data.loan_amount = data.property_value; } } }, clean = function( type, val ) { switch ( type ) { case 'payment_period': return rmcp.clean.period( val, [ 1, 2, 4, 12, 26, 52, 365 ], 12 ); case 'compounding_period': return rmcp.clean.period( val, [ 0, 1, 2, 4, 12, 26, 52, 365 ] ); default: return rmcp.clean.number( val ); } }; return process(); }, setCalculatorTags = function( data, coefs, def ) { // Get the prefix var pre = rmcp.getVal( def, 'tag' ) + '_', tag; // Tags from data for ( tag in data ) { rmcp.setTag( pre + tag, data[tag] ); } // Tags from coefficients for ( tag in coefs ) { rmcp.setTag( pre + tag, coefs[tag] ); } // Text tags rmcp.setTag( pre + 'payment_period_text', data.payment_period, 'payment_period_text' ); rmcp.setTag( pre + 'amortization_period_text', data.amortization_period, 'amortization_period_text' ); rmcp.setTag( pre + 'compounding_period_text', data.compounding_period, 'compounding_period_text' ); }, runSubcalculations = function( propertyValue, loanAmount, def, sub, form ) { // Determine the cost definitions if ( ! def.subcalculations || def.subcalculations.length < 1 ) { return; } if ( ! sub || sub.length < 1 ) { return; } // Debug message rmcp.d( 'Processing Costs for Calculator: ' + rmcp.getVal( def, 'label' ) + '.' ); var input = { property_value: propertyValue, loan_amount: loanAmount }, pre = rmcp.getVal( def, 'tag' ) + '_', tag; // Cycle through the definitions and get the necessary definitions for ( var i = 0, c = sub.length; i < c; i++ ) { for ( var j = 0, k = def.subcalculations.length; j < k; j++ ) { if ( def.subcalculations[j][0].replace( /[{}]/g, '' ) === sub[i].tag ) { rmcp.setTag( pre + sub[i].tag, rmcp.processSubcalculations( input, sub[i], form ) ); } } } }; // Mortgage and Loan calculations var calculateMortgageCoefficients = function( interestRate, paymentPeriod, amortizationPeriod, compoundingPeriod ) { var numberOfPayments = calculateNumberOfPayments( paymentPeriod, amortizationPeriod ), nominalInterestRate = calculateNominalInterestRate( interestRate ), interestRatePerPeriod = calculateInterestRatePerPeriod( nominalInterestRate, paymentPeriod, compoundingPeriod ), rFactor = calculateRFactor( interestRatePerPeriod, numberOfPayments, compoundingPeriod ); return { number_of_payments: numberOfPayments, nominal_interest_rate: nominalInterestRate, interest_rate_per_period: interestRatePerPeriod, r_factor: rFactor }; }, calculateNumberOfPayments = function( paymentPeriod, amortizationPeriod ) { return Math.ceil( paymentPeriod * amortizationPeriod ); }, calculateNominalInterestRate = function( interestRate ) { return interestRate / 100; }, calculateInterestRatePerPeriod = function( nominalInterestRate, paymentPeriod, compoundingPeriod ) { var rate; if ( +paymentPeriod === 0 ) { paymentPeriod = 12; } if ( +compoundingPeriod === 0 ) { rate = nominalInterestRate / paymentPeriod; } else { rate = ( Math.pow( 1 + ( nominalInterestRate / compoundingPeriod ), compoundingPeriod / paymentPeriod ) - 1 ); } return rate; }, calculateRFactor = function( interestRatePerPeriod, numberOfPayments, compoundingPeriod ) { var rFactor; if ( +compoundingPeriod === 0 ) { rFactor = null; } else { rFactor = Math.pow( interestRatePerPeriod + 1, numberOfPayments ); } return rFactor; }; // Processor and Calculator helpers // Figure out whether a setting is a tag, a number, or nothing var getValueAndExpectedFieldType = function( type, def ) { var value = rmcp.getVal( def, type ); if ( ! value && value !== 0 ) { return [ null, 'null' ]; } if ( rmcp.clean.number( value ) === +value ) { return [ value, 'number' ]; } return [ value.replace( /[{}]/g, '' ), 'tag' ]; }; }()); // End Register Processors (function() { 'use strict'; rmcp.addEvent( 'load', window, function() { // Attach submit event handlers to all RMCP forms var f = document.forms; for ( var i = 0, c = f.length; i < c; i++ ) { if ( f[i].name === 'rmcp_form' ) { rmcp.addForm( f[i] ); } } } ); })();