home *** CD-ROM | disk | FTP | other *** search
/ HTML Examples / WP.iso / wordpress / wp-includes / pomo / plural-forms.php < prev    next >
Encoding:
PHP Script  |  2017-10-03  |  6.8 KB  |  344 lines

  1. <?php
  2.  
  3. /**
  4.  * A gettext Plural-Forms parser.
  5.  *
  6.  * @since 4.9.0
  7.  */
  8. class Plural_Forms {
  9.     /**
  10.      * Operator characters.
  11.      *
  12.      * @since 4.9.0
  13.      * @var string OP_CHARS Operator characters.
  14.      */
  15.     const OP_CHARS = '|&><!=%?:';
  16.  
  17.     /**
  18.      * Valid number characters.
  19.      *
  20.      * @since 4.9.0
  21.      * @var string NUM_CHARS Valid number characters.
  22.      */
  23.     const NUM_CHARS = '0123456789';
  24.  
  25.     /**
  26.      * Operator precedence.
  27.      *
  28.      * Operator precedence from highest to lowest. Higher numbers indicate
  29.      * higher precedence, and are executed first.
  30.      *
  31.      * @see https://en.wikipedia.org/wiki/Operators_in_C_and_C%2B%2B#Operator_precedence
  32.      *
  33.      * @since 4.9.0
  34.      * @var array $op_precedence Operator precedence from highest to lowest.
  35.      */
  36.     protected static $op_precedence = array(
  37.         '%' => 6,
  38.  
  39.         '<' => 5,
  40.         '<=' => 5,
  41.         '>' => 5,
  42.         '>=' => 5,
  43.  
  44.         '==' => 4,
  45.         '!=' => 4,
  46.  
  47.         '&&' => 3,
  48.  
  49.         '||' => 2,
  50.  
  51.         '?:' => 1,
  52.         '?' => 1,
  53.  
  54.         '(' => 0,
  55.         ')' => 0,
  56.     );
  57.  
  58.     /**
  59.      * Tokens generated from the string.
  60.      *
  61.      * @since 4.9.0
  62.      * @var array $tokens List of tokens.
  63.      */
  64.     protected $tokens = array();
  65.  
  66.     /**
  67.      * Cache for repeated calls to the function.
  68.      *
  69.      * @since 4.9.0
  70.      * @var array $cache Map of $n => $result
  71.      */
  72.     protected $cache = array();
  73.  
  74.     /**
  75.      * Constructor.
  76.      *
  77.      * @since 4.9.0
  78.      *
  79.      * @param string $str Plural function (just the bit after `plural=` from Plural-Forms)
  80.      */
  81.     public function __construct( $str ) {
  82.         $this->parse( $str );
  83.     }
  84.  
  85.     /**
  86.      * Parse a Plural-Forms string into tokens.
  87.      *
  88.      * Uses the shunting-yard algorithm to convert the string to Reverse Polish
  89.      * Notation tokens.
  90.      *
  91.      * @since 4.9.0
  92.      *
  93.      * @param string $str String to parse.
  94.      */
  95.     protected function parse( $str ) {
  96.         $pos = 0;
  97.         $len = strlen( $str );
  98.  
  99.         // Convert infix operators to postfix using the shunting-yard algorithm.
  100.         $output = array();
  101.         $stack = array();
  102.         while ( $pos < $len ) {
  103.             $next = substr( $str, $pos, 1 );
  104.  
  105.             switch ( $next ) {
  106.                 // Ignore whitespace
  107.                 case ' ':
  108.                 case "\t":
  109.                     $pos++;
  110.                     break;
  111.  
  112.                 // Variable (n)
  113.                 case 'n':
  114.                     $output[] = array( 'var' );
  115.                     $pos++;
  116.                     break;
  117.  
  118.                 // Parentheses
  119.                 case '(':
  120.                     $stack[] = $next;
  121.                     $pos++;
  122.                     break;
  123.  
  124.                 case ')':
  125.                     $found = false;
  126.                     while ( ! empty( $stack ) ) {
  127.                         $o2 = $stack[ count( $stack ) - 1 ];
  128.                         if ( $o2 !== '(' ) {
  129.                             $output[] = array( 'op', array_pop( $stack ) );
  130.                             continue;
  131.                         }
  132.  
  133.                         // Discard open paren.
  134.                         array_pop( $stack );
  135.                         $found = true;
  136.                         break;
  137.                     }
  138.  
  139.                     if ( ! $found ) {
  140.                         throw new Exception( 'Mismatched parentheses' );
  141.                     }
  142.  
  143.                     $pos++;
  144.                     break;
  145.  
  146.                 // Operators
  147.                 case '|':
  148.                 case '&':
  149.                 case '>':
  150.                 case '<':
  151.                 case '!':
  152.                 case '=':
  153.                 case '%':
  154.                 case '?':
  155.                     $end_operator = strspn( $str, self::OP_CHARS, $pos );
  156.                     $operator = substr( $str, $pos, $end_operator );
  157.                     if ( ! array_key_exists( $operator, self::$op_precedence ) ) {
  158.                         throw new Exception( sprintf( 'Unknown operator "%s"', $operator ) );
  159.                     }
  160.  
  161.                     while ( ! empty( $stack ) ) {
  162.                         $o2 = $stack[ count( $stack ) - 1 ];
  163.  
  164.                         // Ternary is right-associative in C
  165.                         if ( $operator === '?:' || $operator === '?' ) {
  166.                             if ( self::$op_precedence[ $operator ] >= self::$op_precedence[ $o2 ] ) {
  167.                                 break;
  168.                             }
  169.                         } elseif ( self::$op_precedence[ $operator ] > self::$op_precedence[ $o2 ] ) {
  170.                             break;
  171.                         }
  172.  
  173.                         $output[] = array( 'op', array_pop( $stack ) );
  174.                     }
  175.                     $stack[] = $operator;
  176.  
  177.                     $pos += $end_operator;
  178.                     break;
  179.  
  180.                 // Ternary "else"
  181.                 case ':':
  182.                     $found = false;
  183.                     $s_pos = count( $stack ) - 1;
  184.                     while ( $s_pos >= 0 ) {
  185.                         $o2 = $stack[ $s_pos ];
  186.                         if ( $o2 !== '?' ) {
  187.                             $output[] = array( 'op', array_pop( $stack ) );
  188.                             $s_pos--;
  189.                             continue;
  190.                         }
  191.  
  192.                         // Replace.
  193.                         $stack[ $s_pos ] = '?:';
  194.                         $found = true;
  195.                         break;
  196.                     }
  197.  
  198.                     if ( ! $found ) {
  199.                         throw new Exception( 'Missing starting "?" ternary operator' );
  200.                     }
  201.                     $pos++;
  202.                     break;
  203.  
  204.                 // Default - number or invalid
  205.                 default:
  206.                     if ( $next >= '0' && $next <= '9' ) {
  207.                         $span = strspn( $str, self::NUM_CHARS, $pos );
  208.                         $output[] = array( 'value', intval( substr( $str, $pos, $span ) ) );
  209.                         $pos += $span;
  210.                         continue;
  211.                     }
  212.  
  213.                     throw new Exception( sprintf( 'Unknown symbol "%s"', $next ) );
  214.             }
  215.         }
  216.  
  217.         while ( ! empty( $stack ) ) {
  218.             $o2 = array_pop( $stack );
  219.             if ( $o2 === '(' || $o2 === ')' ) {
  220.                 throw new Exception( 'Mismatched parentheses' );
  221.             }
  222.  
  223.             $output[] = array( 'op', $o2 );
  224.         }
  225.  
  226.         $this->tokens = $output;
  227.     }
  228.  
  229.     /**
  230.      * Get the plural form for a number.
  231.      *
  232.      * Caches the value for repeated calls.
  233.      *
  234.      * @since 4.9.0
  235.      *
  236.      * @param int $num Number to get plural form for.
  237.      * @return int Plural form value.
  238.      */
  239.     public function get( $num ) {
  240.         if ( isset( $this->cache[ $num ] ) ) {
  241.             return $this->cache[ $num ];
  242.         }
  243.         return $this->cache[ $num ] = $this->execute( $num );
  244.     }
  245.  
  246.     /**
  247.      * Execute the plural form function.
  248.      *
  249.      * @since 4.9.0
  250.      *
  251.      * @param int $n Variable "n" to substitute.
  252.      * @return int Plural form value.
  253.      */
  254.     public function execute( $n ) {
  255.         $stack = array();
  256.         $i = 0;
  257.         $total = count( $this->tokens );
  258.         while ( $i < $total ) {
  259.             $next = $this->tokens[$i];
  260.             $i++;
  261.             if ( $next[0] === 'var' ) {
  262.                 $stack[] = $n;
  263.                 continue;
  264.             } elseif ( $next[0] === 'value' ) {
  265.                 $stack[] = $next[1];
  266.                 continue;
  267.             }
  268.  
  269.             // Only operators left.
  270.             switch ( $next[1] ) {
  271.                 case '%':
  272.                     $v2 = array_pop( $stack );
  273.                     $v1 = array_pop( $stack );
  274.                     $stack[] = $v1 % $v2;
  275.                     break;
  276.  
  277.                 case '||':
  278.                     $v2 = array_pop( $stack );
  279.                     $v1 = array_pop( $stack );
  280.                     $stack[] = $v1 || $v2;
  281.                     break;
  282.  
  283.                 case '&&':
  284.                     $v2 = array_pop( $stack );
  285.                     $v1 = array_pop( $stack );
  286.                     $stack[] = $v1 && $v2;
  287.                     break;
  288.  
  289.                 case '<':
  290.                     $v2 = array_pop( $stack );
  291.                     $v1 = array_pop( $stack );
  292.                     $stack[] = $v1 < $v2;
  293.                     break;
  294.  
  295.                 case '<=':
  296.                     $v2 = array_pop( $stack );
  297.                     $v1 = array_pop( $stack );
  298.                     $stack[] = $v1 <= $v2;
  299.                     break;
  300.  
  301.                 case '>':
  302.                     $v2 = array_pop( $stack );
  303.                     $v1 = array_pop( $stack );
  304.                     $stack[] = $v1 > $v2;
  305.                     break;
  306.  
  307.                 case '>=':
  308.                     $v2 = array_pop( $stack );
  309.                     $v1 = array_pop( $stack );
  310.                     $stack[] = $v1 >= $v2;
  311.                     break;
  312.  
  313.                 case '!=':
  314.                     $v2 = array_pop( $stack );
  315.                     $v1 = array_pop( $stack );
  316.                     $stack[] = $v1 != $v2;
  317.                     break;
  318.  
  319.                 case '==':
  320.                     $v2 = array_pop( $stack );
  321.                     $v1 = array_pop( $stack );
  322.                     $stack[] = $v1 == $v2;
  323.                     break;
  324.  
  325.                 case '?:':
  326.                     $v3 = array_pop( $stack );
  327.                     $v2 = array_pop( $stack );
  328.                     $v1 = array_pop( $stack );
  329.                     $stack[] = $v1 ? $v2 : $v3;
  330.                     break;
  331.  
  332.                 default:
  333.                     throw new Exception( sprintf( 'Unknown operator "%s"', $next[1] ) );
  334.             }
  335.         }
  336.  
  337.         if ( count( $stack ) !== 1 ) {
  338.             throw new Exception( 'Too many values remaining on the stack' );
  339.         }
  340.  
  341.         return (int) $stack[0];
  342.     }
  343. }
  344.