Title: Plural_Forms
Published: November 20, 2017
Last modified: April 28, 2025

---

# class Plural_Forms {}

## In this article

 * [Methods](https://developer.wordpress.org/reference/classes/plural_forms/?output_format=md#methods)
 * [Source](https://developer.wordpress.org/reference/classes/plural_forms/?output_format=md#source)

[ Back to top](https://developer.wordpress.org/reference/classes/plural_forms/?output_format=md#wp--skip-link--target)

## 󠀁[Methods](https://developer.wordpress.org/reference/classes/plural_forms/?output_format=md#methods)󠁿

| Name | Description | 
| [Plural_Forms::__construct](https://developer.wordpress.org/reference/classes/plural_forms/__construct/) | Constructor. | 
| [Plural_Forms::execute](https://developer.wordpress.org/reference/classes/plural_forms/execute/) | Execute the plural form function. | 
| [Plural_Forms::get](https://developer.wordpress.org/reference/classes/plural_forms/get/) | Get the plural form for a number. | 
| [Plural_Forms::parse](https://developer.wordpress.org/reference/classes/plural_forms/parse/) | Parse a Plural-Forms string into tokens. |

## 󠀁[Source](https://developer.wordpress.org/reference/classes/plural_forms/?output_format=md#source)󠁿

    ```php
    class Plural_Forms {
    	/**
    	 * Operator characters.
    	 *
    	 * @since 4.9.0
    	 * @var string OP_CHARS Operator characters.
    	 */
    	const OP_CHARS = '|&><!=%?:';

    	/**
    	 * Valid number characters.
    	 *
    	 * @since 4.9.0
    	 * @var string NUM_CHARS Valid number characters.
    	 */
    	const NUM_CHARS = '0123456789';

    	/**
    	 * Operator precedence.
    	 *
    	 * Operator precedence from highest to lowest. Higher numbers indicate
    	 * higher precedence, and are executed first.
    	 *
    	 * @see https://en.wikipedia.org/wiki/Operators_in_C_and_C%2B%2B#Operator_precedence
    	 *
    	 * @since 4.9.0
    	 * @var array $op_precedence Operator precedence from highest to lowest.
    	 */
    	protected static $op_precedence = array(
    		'%'  => 6,

    		'<'  => 5,
    		'<=' => 5,
    		'>'  => 5,
    		'>=' => 5,

    		'==' => 4,
    		'!=' => 4,

    		'&&' => 3,

    		'||' => 2,

    		'?:' => 1,
    		'?'  => 1,

    		'('  => 0,
    		')'  => 0,
    	);

    	/**
    	 * Tokens generated from the string.
    	 *
    	 * @since 4.9.0
    	 * @var array $tokens List of tokens.
    	 */
    	protected $tokens = array();

    	/**
    	 * Cache for repeated calls to the function.
    	 *
    	 * @since 4.9.0
    	 * @var array $cache Map of $n => $result
    	 */
    	protected $cache = array();

    	/**
    	 * Constructor.
    	 *
    	 * @since 4.9.0
    	 *
    	 * @param string $str Plural function (just the bit after `plural=` from Plural-Forms)
    	 */
    	public function __construct( $str ) {
    		$this->parse( $str );
    	}

    	/**
    	 * Parse a Plural-Forms string into tokens.
    	 *
    	 * Uses the shunting-yard algorithm to convert the string to Reverse Polish
    	 * Notation tokens.
    	 *
    	 * @since 4.9.0
    	 *
    	 * @throws Exception If there is a syntax or parsing error with the string.
    	 *
    	 * @param string $str String to parse.
    	 */
    	protected function parse( $str ) {
    		$pos = 0;
    		$len = strlen( $str );

    		// Convert infix operators to postfix using the shunting-yard algorithm.
    		$output = array();
    		$stack  = array();
    		while ( $pos < $len ) {
    			$next = substr( $str, $pos, 1 );

    			switch ( $next ) {
    				// Ignore whitespace.
    				case ' ':
    				case "\t":
    					++$pos;
    					break;

    				// Variable (n).
    				case 'n':
    					$output[] = array( 'var' );
    					++$pos;
    					break;

    				// Parentheses.
    				case '(':
    					$stack[] = $next;
    					++$pos;
    					break;

    				case ')':
    					$found = false;
    					while ( ! empty( $stack ) ) {
    						$o2 = $stack[ count( $stack ) - 1 ];
    						if ( '(' !== $o2 ) {
    							$output[] = array( 'op', array_pop( $stack ) );
    							continue;
    						}

    						// Discard open paren.
    						array_pop( $stack );
    						$found = true;
    						break;
    					}

    					if ( ! $found ) {
    						throw new Exception( 'Mismatched parentheses' );
    					}

    					++$pos;
    					break;

    				// Operators.
    				case '|':
    				case '&':
    				case '>':
    				case '<':
    				case '!':
    				case '=':
    				case '%':
    				case '?':
    					$end_operator = strspn( $str, self::OP_CHARS, $pos );
    					$operator     = substr( $str, $pos, $end_operator );
    					if ( ! array_key_exists( $operator, self::$op_precedence ) ) {
    						throw new Exception( sprintf( 'Unknown operator "%s"', $operator ) );
    					}

    					while ( ! empty( $stack ) ) {
    						$o2 = $stack[ count( $stack ) - 1 ];

    						// Ternary is right-associative in C.
    						if ( '?:' === $operator || '?' === $operator ) {
    							if ( self::$op_precedence[ $operator ] >= self::$op_precedence[ $o2 ] ) {
    								break;
    							}
    						} elseif ( self::$op_precedence[ $operator ] > self::$op_precedence[ $o2 ] ) {
    							break;
    						}

    						$output[] = array( 'op', array_pop( $stack ) );
    					}
    					$stack[] = $operator;

    					$pos += $end_operator;
    					break;

    				// Ternary "else".
    				case ':':
    					$found = false;
    					$s_pos = count( $stack ) - 1;
    					while ( $s_pos >= 0 ) {
    						$o2 = $stack[ $s_pos ];
    						if ( '?' !== $o2 ) {
    							$output[] = array( 'op', array_pop( $stack ) );
    							--$s_pos;
    							continue;
    						}

    						// Replace.
    						$stack[ $s_pos ] = '?:';
    						$found           = true;
    						break;
    					}

    					if ( ! $found ) {
    						throw new Exception( 'Missing starting "?" ternary operator' );
    					}
    					++$pos;
    					break;

    				// Default - number or invalid.
    				default:
    					if ( $next >= '0' && $next <= '9' ) {
    						$span     = strspn( $str, self::NUM_CHARS, $pos );
    						$output[] = array( 'value', intval( substr( $str, $pos, $span ) ) );
    						$pos     += $span;
    						break;
    					}

    					throw new Exception( sprintf( 'Unknown symbol "%s"', $next ) );
    			}
    		}

    		while ( ! empty( $stack ) ) {
    			$o2 = array_pop( $stack );
    			if ( '(' === $o2 || ')' === $o2 ) {
    				throw new Exception( 'Mismatched parentheses' );
    			}

    			$output[] = array( 'op', $o2 );
    		}

    		$this->tokens = $output;
    	}

    	/**
    	 * Get the plural form for a number.
    	 *
    	 * Caches the value for repeated calls.
    	 *
    	 * @since 4.9.0
    	 *
    	 * @param int $num Number to get plural form for.
    	 * @return int Plural form value.
    	 */
    	public function get( $num ) {
    		if ( isset( $this->cache[ $num ] ) ) {
    			return $this->cache[ $num ];
    		}
    		$this->cache[ $num ] = $this->execute( $num );
    		return $this->cache[ $num ];
    	}

    	/**
    	 * Execute the plural form function.
    	 *
    	 * @since 4.9.0
    	 *
    	 * @throws Exception If the plural form value cannot be calculated.
    	 *
    	 * @param int $n Variable "n" to substitute.
    	 * @return int Plural form value.
    	 */
    	public function execute( $n ) {
    		$stack = array();
    		$i     = 0;
    		$total = count( $this->tokens );
    		while ( $i < $total ) {
    			$next = $this->tokens[ $i ];
    			++$i;
    			if ( 'var' === $next[0] ) {
    				$stack[] = $n;
    				continue;
    			} elseif ( 'value' === $next[0] ) {
    				$stack[] = $next[1];
    				continue;
    			}

    			// Only operators left.
    			switch ( $next[1] ) {
    				case '%':
    					$v2      = array_pop( $stack );
    					$v1      = array_pop( $stack );
    					$stack[] = $v1 % $v2;
    					break;

    				case '||':
    					$v2      = array_pop( $stack );
    					$v1      = array_pop( $stack );
    					$stack[] = $v1 || $v2;
    					break;

    				case '&&':
    					$v2      = array_pop( $stack );
    					$v1      = array_pop( $stack );
    					$stack[] = $v1 && $v2;
    					break;

    				case '<':
    					$v2      = array_pop( $stack );
    					$v1      = array_pop( $stack );
    					$stack[] = $v1 < $v2;
    					break;

    				case '<=':
    					$v2      = array_pop( $stack );
    					$v1      = array_pop( $stack );
    					$stack[] = $v1 <= $v2;
    					break;

    				case '>':
    					$v2      = array_pop( $stack );
    					$v1      = array_pop( $stack );
    					$stack[] = $v1 > $v2;
    					break;

    				case '>=':
    					$v2      = array_pop( $stack );
    					$v1      = array_pop( $stack );
    					$stack[] = $v1 >= $v2;
    					break;

    				case '!=':
    					$v2      = array_pop( $stack );
    					$v1      = array_pop( $stack );
    					$stack[] = $v1 !== $v2;
    					break;

    				case '==':
    					$v2      = array_pop( $stack );
    					$v1      = array_pop( $stack );
    					$stack[] = $v1 === $v2;
    					break;

    				case '?:':
    					$v3      = array_pop( $stack );
    					$v2      = array_pop( $stack );
    					$v1      = array_pop( $stack );
    					$stack[] = $v1 ? $v2 : $v3;
    					break;

    				default:
    					throw new Exception( sprintf( 'Unknown operator "%s"', $next[1] ) );
    			}
    		}

    		if ( count( $stack ) !== 1 ) {
    			throw new Exception( 'Too many values remaining on the stack' );
    		}

    		return (int) $stack[0];
    	}
    }
    ```

[View all references](https://developer.wordpress.org/reference/files/wp-includes/pomo/plural-forms.php/)
[View on Trac](https://core.trac.wordpress.org/browser/tags/6.9.4/src/wp-includes/pomo/plural-forms.php#L10)
[View on GitHub](https://github.com/WordPress/wordpress-develop/blob/6.9.4/src/wp-includes/pomo/plural-forms.php#L10-L350)

## User Contributed Notes

You must [log in](https://login.wordpress.org/?redirect_to=https%3A%2F%2Fdeveloper.wordpress.org%2Freference%2Fclasses%2Fplural_forms%2F)
before being able to contribute a note or feedback.