'object', 'properties' => array( 'expression' => array( 'type' => 'string', 'description' => 'Mathematical expression such as "25% of 8400" or "(10 + 5) * 2".', ), ), 'required' => array( 'expression' ), ); } public function is_enabled(): bool { return (bool) Aivexa_Settings::get( 'enable_calculator', true ); } /** * Execute calculation. * * @param array $arguments Arguments. */ public function execute( array $arguments ) { $expression = isset( $arguments['expression'] ) ? trim( (string) $arguments['expression'] ) : ''; if ( '' === $expression ) { return new WP_Error( 'aivexa_calculator_empty', __( 'Expression is required.', 'aivexa' ) ); } $result = self::evaluate_expression( $expression ); if ( is_wp_error( $result ) ) { return $result; } return array( 'expression' => $expression, 'result' => $result, ); } /** * Evaluate a safe math expression. * * @param string $expression Expression. * @return float|WP_Error */ public static function evaluate_expression( string $expression ) { $expression = strtolower( $expression ); $expression = str_replace( array( 'percent', 'percentage' ), '%', $expression ); $expression = preg_replace( '/\s*(of)\s*/', '*', $expression ) ?? $expression; $expression = preg_replace( '/(\d+(?:\.\d+)?)\s*%/', '($1/100)', $expression ) ?? $expression; $expression = preg_replace( '/[^0-9+\-*\/().]/', '', $expression ) ?? ''; if ( '' === $expression || ! preg_match( '/^[0-9+\-*\/().]+$/', $expression ) ) { return new WP_Error( 'aivexa_calculator_invalid', __( 'Invalid mathematical expression.', 'aivexa' ) ); } try { $value = self::parse_expression( $expression ); } catch ( Exception $exception ) { unset( $exception ); return new WP_Error( 'aivexa_calculator_error', __( 'Could not evaluate expression.', 'aivexa' ) ); } if ( ! is_finite( $value ) ) { return new WP_Error( 'aivexa_calculator_error', __( 'Could not evaluate expression.', 'aivexa' ) ); } return round( $value, 8 ); } /** * Recursive descent parser. * * @param string $expression Expression. */ private static function parse_expression( string $expression ): float { $pos = 0; $len = strlen( $expression ); $eval = self::parse_add_sub( $expression, $pos, $len ); if ( $pos !== $len ) { throw new InvalidArgumentException( 'Unexpected trailing characters.' ); } return $eval; } /** * @param string $expr Expression. * @param int $pos Position reference. * @param int $len Length. */ private static function parse_add_sub( string $expr, int &$pos, int $len ): float { $value = self::parse_mul_div( $expr, $pos, $len ); while ( $pos < $len ) { $op = $expr[ $pos ]; if ( '+' !== $op && '-' !== $op ) { break; } ++$pos; $right = self::parse_mul_div( $expr, $pos, $len ); $value = ( '+' === $op ) ? $value + $right : $value - $right; } return $value; } /** * @param string $expr Expression. * @param int $pos Position reference. * @param int $len Length. */ private static function parse_mul_div( string $expr, int &$pos, int $len ): float { $value = self::parse_unary( $expr, $pos, $len ); while ( $pos < $len ) { $op = $expr[ $pos ]; if ( '*' !== $op && '/' !== $op ) { break; } ++$pos; $right = self::parse_unary( $expr, $pos, $len ); if ( '/' === $op && 0.0 === $right ) { throw new InvalidArgumentException( 'Division by zero.' ); } $value = ( '*' === $op ) ? $value * $right : $value / $right; } return $value; } /** * @param string $expr Expression. * @param int $pos Position reference. * @param int $len Length. */ private static function parse_unary( string $expr, int &$pos, int $len ): float { if ( $pos < $len && '-' === $expr[ $pos ] ) { ++$pos; return -1 * self::parse_unary( $expr, $pos, $len ); } if ( $pos < $len && '+' === $expr[ $pos ] ) { ++$pos; } return self::parse_primary( $expr, $pos, $len ); } /** * @param string $expr Expression. * @param int $pos Position reference. * @param int $len Length. */ private static function parse_primary( string $expr, int &$pos, int $len ): float { if ( $pos < $len && '(' === $expr[ $pos ] ) { ++$pos; $value = self::parse_add_sub( $expr, $pos, $len ); if ( $pos >= $len || ')' !== $expr[ $pos ] ) { throw new InvalidArgumentException( 'Missing closing parenthesis.' ); } ++$pos; return $value; } $start = $pos; while ( $pos < $len && ( ctype_digit( $expr[ $pos ] ) || '.' === $expr[ $pos ] ) ) { ++$pos; } if ( $start === $pos ) { throw new InvalidArgumentException( 'Expected number.' ); } return (float) substr( $expr, $start, $pos - $start ); } }