Rounding by Increment

Round your field values (including calculations) up, down, by an increment, or to a specific minimum or maximum value.

Read the Walkthrough

Code

Filename: gw-gravity-forms-rounding.php

<?php
/**
 * Gravity Wiz // Gravity Forms // Rounding by Increment
 *
 * Provides a variety of rounding functions for Gravity Form Number fields powered by the CSS class setting for each field. Functions include:
 *
 *  + rounding to an increment            (i.e. increment of 100 would round 1 to 100, 149 to 100, 150 to 200, etc) | class: 'gw-round-100'
 *  + rounding up by an increment         (i.e. increment of 50 would round 1 to 50, 51 to 100, 149 to 150, etc)    | class: 'gw-round-up-50'
 *  + rounding down by an increment       (i.e. increment of 25 would round 1 to 0, 26 to 25, 51 to 50, etc)        | class: 'gw-round-down-25'
 *  + rounding up to a specific minimum   (i.e. min of 50 would round 1 to 50, 51 and greater would not be rounded) | class: 'gw-round-min-50'
 *  + rounding down to a specific maximum (i.e. max of 25 would round 26 to 25, 25 and below would not be rounded)  | class: 'gw-round-max-25'
 *
 * Plugin Name:  Gravity Forms Rounding
 * Plugin URI:   https://gravitywiz.com/rounding-increments-gravity-forms/
 * Description:  Round your field values (including calculations) up, down, by an increment, or to a specific minimum or maximum value.
 * Author:       Gravity Wiz
 * Version:      1.14
 * Author URI:   https://gravitywiz.com
 */
class GW_Rounding {

	private static $instance = null;

	protected static $is_script_output = false;

	protected $class_regex = 'gw-round-([\w|0-9.]+)-?([\w|0-9.]+)?';

	public static function get_instance() {
		if ( null == self::$instance ) {
			self::$instance = new self;
		}
		return self::$instance;
	}

	private function __construct( $args = array() ) {

		// make sure we're running the required minimum version of Gravity Forms
		if ( ! property_exists( 'GFCommon', 'version' ) || ! version_compare( GFCommon::$version, '1.8', '>=' ) ) {
			return;
		}

		// time for hooks
		add_filter( 'gform_pre_render', array( $this, 'prepare_form_and_load_script' ), 10, 2 );
		add_filter( 'gform_register_init_scripts', array( $this, 'add_init_script' ) );
		add_filter( 'gform_enqueue_scripts', array( $this, 'enqueue_form_scripts' ) );

		add_action( 'gform_pre_submission', array( $this, 'override_submitted_value' ), 10, 5 );
		add_filter( 'gform_calculation_result', array( $this, 'override_submitted_calculation_value' ), 10, 5 );

	}

	public function prepare_form_and_load_script( $form, $is_ajax_enabled ) {

		if ( ! $this->is_applicable_form( $form ) ) {
			return $form;
		}

		if ( $this->is_applicable_form( $form ) && ! has_action( 'wp_footer', array( $this, 'output_script' ) ) ) {
			add_action( 'wp_footer', array( $this, 'output_script' ) );
			add_action( 'gform_preview_footer', array( $this, 'output_script' ) );
			add_action( 'admin_footer', array( $this, 'output_script' ) );
		}

		foreach ( $form['fields'] as &$field ) {
			if ( preg_match( $this->get_class_regex(), $field['cssClass'] ) ) {
				$field['cssClass'] .= ' gw-rounding';
			}
		}

		return $form;
	}

	public function is_ajax_submission( $form_id, $is_ajax_enabled ) {
		return class_exists( 'GFFormDisplay' ) && isset( GFFormDisplay::$submission[ $form_id ] ) && $is_ajax_enabled;
	}

	function output_script() {
		?>

		<script type="text/javascript">

			var GWRounding;

			( function( $ ) {

				GWRounding = function( args ) {

					var self = this;

					// copy all args to current object: (list expected props)
					for( prop in args ) {
						if( args.hasOwnProperty( prop ) )
							self[prop] = args[prop];
					}

					self.init = function() {

						self.fieldElems = $( '#gform_wrapper_' + self.formId + ' .gw-rounding' );

						self.parseElemActions( self.fieldElems );

						self.bindEvents();

					};

					self.parseElemActions = function( elems ) {

						elems.each( function() {

							var cssClasses      = $( this ).attr( 'class' ),
								roundingActions = self.parseActions( cssClasses );

							$( this ).data( 'gw-rounding', roundingActions );

						} );

					};

					self.parseActions = function( str ) {

						var matches         = getMatchGroups( String( str ), new RegExp( self.classRegex.replace( /\\/g, '\\' ), 'i' ) ),
							roundingActions = [];
						for( var i = 0; i < matches.length; i++ ) {
							var action      = matches[i][1],
								actionValue = matches[i][2];

							if( typeof actionValue == 'undefined' && ! isNaN( parseFloat( action ) ) ) {
								actionValue = action;
								action = 'round';
							}

							var roundingAction = {
								'action':      action,
								'actionValue': actionValue
							};

							roundingActions.push( roundingAction );

						}

						return roundingActions;
					};

					self.bindEvents = function() {

						self.fieldElems.each( function() {

							var $targets;

							// Attempt to target only the quantity input of a Single Product field.
							if( $( this ).hasClass( 'gfield_price' ) ) {
								$targets = $( this ).find( '.ginput_quantity' );
								// Currently, we don't have a way to specify if we want to round the quantity or the price
								// of Calculated Product fields. For now, we'll assume if there is no quantity input, we
								// want to round the price.
								if ( ! $targets.length && $( this ).hasClass( 'gfield_calculation' ) ) {
									$targets = $( this ).find( 'input[id^=ginput_base_price]' );
								}
							}

							// Otherwise, target all inputs of the given field.
							if( ! $targets || ! $targets.length ) {
								$targets = $( this ).find( 'input' );
							}

							$targets.each( function() {
								self.applyRoundingActions( $( this ) );
							} ).blur( function() {
								self.applyRoundingActions( $( this ) );
							} );

						} );

						gform.addFilter( 'gform_calculation_result', function( result, formulaField, formId, calcObj ) {

							var $input = $( '#input_' + formId + '_' + formulaField.field_id )
							$field = $input.parents( '.gfield' );

							if( $field.hasClass( 'gw-rounding' ) ) {
								result = self.getRoundedValue( $input, result );
							}

							return result;
						} );

					};

					self.applyRoundingActions = function( $input ) {
						var value = self.getRoundedValue( $input );
						if( $input.val() != value ) {
							$input.val( value ).change();
						}
					};

					self.getRoundedValue = function( $input, value ) {

						var $field  = $input.parents( '.gfield' ),
							actions = $field.data( 'gw-rounding' );

						// allows setting the 'gw-rounding' data for an element to null and it will be reparsed
						if( actions === null ) {
							self.parseElemActions( $field );
							actions = $field.data( 'gw-rounding' );
						}

						if( typeof actions == 'undefined' || actions === false || actions.length <= 0 ) {
							return;
						}

						if( typeof value == 'undefined' ) {

							value = gformToNumber( $input.val() );

							var currency = new Currency(gf_global.gf_currency_config);
							value = gformCleanNumber( value, currency.currency.symbol_right, currency.currency.symbol_left, currency.currency.decimal_separator );

						}

						if( value != '' ) {
							for( var i = 0; i < actions.length; i++ ) {
								value = GWRounding.round( value, actions[i].actionValue, actions[i].action );
							}
						}

						return isNaN( value ) ? '' : value;
					};

					GWRounding.round = function( value, actionValue, action ) {
						var interval, base, min, max;
						value = parseFloat( value );
						actionValue = parseFloat( actionValue );
						switch( action ) {
							case 'min':
								min = actionValue;
								if( value < min ) {
									value = min;
								}
								break;
							case 'max':
								max = actionValue;
								if( value > max ) {
									value = max;
								}
								break;
							case 'up':
								interval = actionValue;
								base     = Math.ceil( value / interval );
								value    = base * interval;
								break;
							case 'down':
								interval = actionValue;
								base     = Math.floor( value / interval );
								value    = base * interval;
								break;
							case 'round':
								interval = actionValue;
								base = Math.round(value / interval);
								value = base * interval;
								break;
							default:
								/**
								 * Custom rounding filter
								 *
								 * Use this filter to implement your own rounding method. The filter's name is based on
								 * the CSS class set on the field.
								 *
								 * Example:
								 * CSS Class: gw-round-mycustomroundroundingfunc-10
								 * Filter: gw_round_mycustomroundingfunc
								 *
								 * @param int value       Current input value to be rounded
								 * @param int actionValue Custom value passed in CSS class name (e.g. gw-round-custom-10, actionValue = 10)
								 */
								value = window.gform.applyFilters( 'gw_round_{0}'.gformFormat(action), value, actionValue );
								break;
						}

						return parseFloat( value );
					};

					self.init();

				}

			} )( jQuery );

		</script>

		<?php
	}

	function add_init_script( $form ) {

		if ( ! $this->is_applicable_form( $form ) ) {
			return;
		}

		$args   = array(
			'formId'     => $form['id'],
			'classRegex' => $this->class_regex,
		);
		$script = 'new GWRounding( ' . json_encode( $args ) . ' );';

		GFFormDisplay::add_init_script( $form['id'], 'gw_rounding', GFFormDisplay::ON_PAGE_RENDER, $script );

	}

	function enqueue_form_scripts( $form ) {

		if ( $this->is_applicable_form( $form ) ) {
			wp_enqueue_script( 'gform_gravityforms' );
		}

	}

	function override_submitted_value( $form ) {

		foreach ( $form['fields'] as $field ) {
			if ( $this->is_applicable_field( $field ) && ! is_a( $field, 'GF_Field_Calculation' ) ) {

				$value = rgpost( "input_{$field['id']}" );

				$is_currency = $field->get_input_type() == 'number' && $field->numberFormat == 'currency';
				if ( $is_currency ) {
					$value = GFCommon::to_number( $value );
				}

				$value = $this->process_rounding_actions( $value, $this->get_rounding_actions( $field ) );

				$_POST[ "input_{$field['id']}" ] = $value;

			}
		}

	}

	function override_submitted_calculation_value( $result, $formula, $field, $form, $entry ) {

		if ( $this->is_applicable_field( $field ) ) {
			$result = $this->process_rounding_actions( $result, $this->get_rounding_actions( $field ) );
		}

		return $result;
	}

	function process_rounding_actions( $value, $actions ) {

		foreach ( $actions as $action ) {
			$value = $this->round( $value, $action['action'], $action['action_value'] );
		}

		return $value;
	}

	function round( $value, $action, $action_value ) {

		$value        = floatval( $value );
		$action_value = floatval( $action_value );

		switch ( $action ) {
			case 'min':
				$min = $action_value;
				if ( $value < $min ) {
					$value = $min;
				}
				break;
			case 'max':
				$max = $action_value;
				if ( $value > $max ) {
					$value = $max;
				}
				break;
			case 'up':
				$interval = $action_value;
				$base     = ceil( $value / $interval );
				$value    = $base * $interval;
				break;
			case 'down':
				$interval = $action_value;
				$base     = floor( $value / $interval );
				$value    = $base * $interval;
				break;
			case 'round':
				$interval = $action_value;
				$base     = round( $value / $interval );
				$value    = $base * $interval;
				break;
			default:
				/**
				 * Custom rounding filter
				 *
				 * Use this filter to implement your own rounding method. The filter's name is based on
				 * the CSS class set on the field.
				 *
				 * Example:
				 * CSS Class: gw-round-mycustomroundroundingfunc-10
				 * Filter: gw_round_mycustomroundingfunc
				 *
				 * @param int $value       Current input value to be rounded
				 * @param int $actionValue Custom value passed in CSS class name (e.g. gw-round-custom-10, actionValue = 10)
				 */
                // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
				$value = apply_filters( sprintf( 'gw_round_%s', $action ), $value, $action_value );
				break;
		}

		return floatval( $value );
	}

	// # HELPERS

	function is_applicable_form( $form ) {

		foreach ( $form['fields'] as $field ) {
			if ( $this->is_applicable_field( $field ) ) {
				return true;
			}
		}

		return false;
	}

	function is_applicable_field( $field ) {
		return preg_match( $this->get_class_regex(), rgar( $field, 'cssClass' ) ) == true;
	}

	function get_class_regex() {
		return "/{$this->class_regex}/";
	}

	function get_rounding_actions( $field ) {

		$actions = array();

		preg_match_all( $this->get_class_regex(), rgar( $field, 'cssClass' ), $matches, PREG_SET_ORDER );

		foreach ( $matches as $match ) {

			list( $full_match, $action, $action_value ) = array_pad( $match, 3, false );

			if ( $action_value === false && is_numeric( $action ) ) {
				$action_value = $action;
				$action       = 'round';
			}

			$action = array(
				'action'       => $action,
				'action_value' => $action_value,
			);

			$actions[] = $action;

		}

		return $actions;
	}

}

function gw_rounding() {
	return GW_Rounding::get_instance();
}

gw_rounding();

Leave a Reply

Your email address will not be published. Required fields are marked *

  • Trouble installing this snippet? See our troubleshooting tips.
  • Need to include code? Create a gist and link to it in your comment.
  • Reporting a bug? Provide a URL where this issue can be recreated.

By commenting, I understand that I may receive emails related to Gravity Wiz and can unsubscribe at any time.