<?php if ( ! defined( 'ABSPATH' ) ) { die( 'You are not allowed to call this page directly.' ); } class FrmProMathController { /** * Process content of frm-math shortcode and evaluate as a math expression. * * @param $atts * @param string $content * * @return mixed|string */ public static function math_shortcode( $atts, $content = '' ) { if ( '0' === $content ) { return '0'; } if ( ! $content ) { return ''; } $atts = shortcode_atts( array( 'decimal' => 0, 'dec_point' => '.', 'thousands_sep' => ',', 'error' => '', 'clean' => 0, ), $atts, 'frm-math' ); $expression = self::get_math_expression_from_shortcode_content( $content, $atts ); if ( $expression === '' ) { return $expression; } if ( self::expression_contains_non_math_characters( $expression ) ) { return self::get_error_content( $atts, $expression ); } $result = self::calculate_math_in_string( $expression ); if ( ! is_numeric( $result ) ) { return self::get_error_content( $atts, $expression ); } return number_format( $result, $atts['decimal'], $atts['dec_point'], $atts['thousands_sep'] ); } /** * Process frm-math shortcode content into string with appropriate content for math expression. * * @param $content * @param $atts * * @return null|string|string[] */ private static function get_math_expression_from_shortcode_content( $content, $atts ) { $expression = do_shortcode( $content ); $expression = preg_replace( '/&#8211;/', '-', $expression ); $expression = self::clear_expression_of_extra_characters( $expression, $atts ); return $expression; } /** * Remove non-math characters or extra spaces, depending on the value of the clean attribute * * @param $expression * @param $atts * * @return null|string|string[] */ private static function clear_expression_of_extra_characters( $expression, $atts ) { if ( isset( $atts['clean'] ) && $atts['clean'] ) { //remove /> and </ so HTML tags can be fully removed $expression = preg_replace( '/\/>|<\/|&lt;\/|\/&gt;/', '', $expression ); return preg_replace( '/[^\+\-\/\*0-9\.\(\)\%]/', '', $expression ); } return preg_replace( '/[\s\,]/', '', $expression ); } /** * Tests if an expression contains characters that don't belong in a math expression, e.g. letters * * @param $expression * * @return bool */ private static function expression_contains_non_math_characters( $expression ) { $result = preg_match( '/[^\+\-\/\*0-9\.\s\(\)\%]/', $expression ); return ( $result === 1 ); } /** * Returns appropriate error content. * * @param $atts * @param $expression * * @return string */ private static function get_error_content( $atts, $expression ) { if ( ! isset( $atts['error'] ) ) { return ''; } if ( $atts['error'] === 'debug' ) { return $expression; } return ( $atts['error'] ); } /** * Calculate value of string math expression. * * @param $math_string * * @return array|array[]|false|mixed|string|string[] */ private static function calculate_math_in_string( $math_string ) { $math_array = self::parse_math_string_into_array( $math_string ); if ( ! is_array( $math_array ) ) { return $math_array; } $post_fix = self::convert_to_postfix( $math_array ); if ( ! is_array( $post_fix ) ) { return $post_fix; } return self::evaluate_postfix_expression( $post_fix ); } /** * Convert math expression string into array. * * @param $math_string * * @return array|array[]|false|string|string[] */ private static function parse_math_string_into_array( $math_string ) { $math_array = preg_split( '/([\+\-\*\/\(\)\%])/', $math_string, - 1, PREG_SPLIT_DELIM_CAPTURE ); $math_array = array_filter( $math_array, 'strlen' ); $math_array = array_values( $math_array ); $math_array = self::set_negative_numbers( $math_array ); return $math_array; } /** * Convert numbers in math array to negative numbers, where appropriate. * * @param $math_array * * @return array|string */ private static function set_negative_numbers( $math_array ) { $negatives = preg_grep( '/\-/', $math_array ); if ( count( $negatives ) === 0 ) { return $math_array; } foreach ( $negatives as $key => $negative ) { if ( $key === 0 || ( preg_match( '/[\-\+\*\/\(\%]/', $math_array[ $key - 1 ] ) > 0 ) ) { $next = $key + 1; if ( isset( $math_array[ $next ] ) && is_numeric( $math_array[ $next ] ) ) { $math_array[ $next ] = - 1 * $math_array[ $next ]; unset( $math_array[ $key ] ); } } } return array_values( $math_array ); } /** * Convert infix (normal) math expression to postfix. * * @param $math_array * * @return array|string */ private static function convert_to_postfix( $math_array ) { $output = array(); $operators = array(); foreach ( $math_array as $index => $element ) { if ( is_numeric( $element ) ) { array_push( $output, $element ); } elseif ( self::is_operator( $element ) ) { self::process_next_operator_in_postfix_conversion( $output, $operators, $element ); } elseif ( '(' === $element ) { array_push( $operators, $element ); } elseif ( ')' === $element ) { $status = self::process_right_paren_in_postfix_conversion( $output, $operators ); if ( false === $status ) { return 'error'; } } } $status = self::move_remaining_operators_to_stack( $output, $operators ); if ( false === $status ) { return 'error'; } return $output; } /** * Determine if a given element is an operator. * * @param $element * * @return bool */ private static function is_operator( $element ) { return self::operator_precedence( $element ) > 0; } /** * Return precedence of operator. Higher is greater precedence. * * @param $operator * * @return int */ private static function operator_precedence( $operator ) { switch ( $operator ) { case '-': return 5; case '+': return 5; case '*': return 7; case '/': return 7; case '%': return 7; default: return 0; } } /** * Process operators in conversion to postfix expression. * * @param $output * @param $operators * @param $operator */ // postfix rules: http://csis.pace.edu/~wolf/CS122/infix-postfix.htm private static function process_next_operator_in_postfix_conversion( &$output, &$operators, $operator ) { if ( count( $operators ) > 0 ) { $current_element_precedence = self::operator_precedence( $operator ); do { $top_operator = array_pop( $operators ); $top_operator_precedence = self::operator_precedence( $top_operator ); if ( $current_element_precedence <= $top_operator_precedence ) { array_push( $output, $top_operator ); } else { // replace last top operator in operator stack; it was only removed for testing array_push( $operators, $top_operator ); } $operators_count = count( $operators ); } while ( $operators_count > 0 && $current_element_precedence <= $top_operator_precedence ); } array_push( $operators, $operator ); } /** * Process right parenthesis in conversion to postfix expression. * * @param $output * @param $operators * * @return bool */ private static function process_right_paren_in_postfix_conversion( &$output, &$operators ) { if ( count( $operators ) === 0 ) { return false; } do { $next_operator = array_pop( $operators ); if ( $next_operator !== '(' ) { array_push( $output, $next_operator ); if ( count( $operators ) === 0 ) { return false; } } } while ( $next_operator !== '(' ); return true; } /** * Move remaining operators to output stack in conversion to postfix expression. * * @param $output * @param $operators * * @return bool */ private static function move_remaining_operators_to_stack( &$output, &$operators ) { $operators_count = count( $operators ); while ( $operators_count > 0 ) { $next_operator = array_pop( $operators ); if ( $next_operator === '(' ) { return false; } array_push( $output, $next_operator ); $operators_count = count( $operators ); } return true; } /** * Evaluate postfix expression mathematically. * * @param $postfix_array * * @return mixed|string */ private static function evaluate_postfix_expression( $postfix_array ) { $stack = array(); foreach ( $postfix_array as $index => $element ) { if ( is_numeric( $element ) ) { array_push( $stack, $element ); } else { if ( count( $stack ) >= 2 ) { $operand2 = array_pop( $stack ); $operand1 = array_pop( $stack ); $result = self::evaluate_simple_math_expression( $operand1, $operand2, $element ); if ( ! is_numeric( $result ) ) { return 'error'; } array_push( $stack, $result ); } else { return 'error'; } } } if ( count( $stack ) === 1 ) { $answer = array_pop( $stack ); if ( is_numeric( $answer ) ) { return $answer; } } return 'error'; } /** * Perform simple arithmetic on two operands. * * @param $operand1 * @param $operand2 * @param $operator * * @return float|int */ private static function evaluate_simple_math_expression( $operand1, $operand2, $operator ) { $result = 'error'; switch ( $operator ) { case '-': $result = $operand1 - $operand2; break; case '+': $result = $operand1 + $operand2; break; case '*': $result = $operand1 * $operand2; break; case '%': if ( $operand2 != 0 ) { $result = $operand1 % $operand2; } break; case '/': if ( $operand2 != 0 ) { $result = $operand1 / $operand2; } break; } return $result; } }