1<?php 2 3namespace Gettext\Languages; 4 5use Exception; 6 7/** 8 * A helper class to convert a CLDR formula to a gettext formula. 9 */ 10class FormulaConverter 11{ 12 /** 13 * Converts a formula from the CLDR representation to the gettext representation. 14 * 15 * @param string $cldrFormula the CLDR formula to convert 16 * 17 * @throws \Exception 18 * 19 * @return bool|string returns true if the gettext will always evaluate to true, false if gettext will always evaluate to false, return the gettext formula otherwise 20 */ 21 public static function convertFormula($cldrFormula) 22 { 23 if (strpbrk($cldrFormula, '()') !== false) { 24 throw new Exception("Unable to convert the formula '${cldrFormula}': parenthesis handling not implemented"); 25 } 26 $orSeparatedChunks = array(); 27 foreach (explode(' or ', $cldrFormula) as $cldrFormulaChunk) { 28 $gettextFormulaChunk = null; 29 $andSeparatedChunks = array(); 30 foreach (explode(' and ', $cldrFormulaChunk) as $cldrAtom) { 31 $gettextAtom = self::convertAtom($cldrAtom); 32 if ($gettextAtom === false) { 33 // One atom joined by 'and' always evaluates to false => the whole 'and' group is always false 34 $gettextFormulaChunk = false; 35 break; 36 } 37 if ($gettextAtom !== true) { 38 $andSeparatedChunks[] = $gettextAtom; 39 } 40 } 41 if (!isset($gettextFormulaChunk)) { 42 if (empty($andSeparatedChunks)) { 43 // All the atoms joined by 'and' always evaluate to true => the whole 'and' group is always true 44 $gettextFormulaChunk = true; 45 } else { 46 $gettextFormulaChunk = implode(' && ', $andSeparatedChunks); 47 // Special cases simplification 48 switch ($gettextFormulaChunk) { 49 case 'n >= 0 && n <= 2 && n != 2': 50 $gettextFormulaChunk = 'n == 0 || n == 1'; 51 break; 52 } 53 } 54 } 55 if ($gettextFormulaChunk === true) { 56 // One part of the formula joined with the others by 'or' always evaluates to true => the whole formula always evaluates to true 57 return true; 58 } 59 if ($gettextFormulaChunk !== false) { 60 $orSeparatedChunks[] = $gettextFormulaChunk; 61 } 62 } 63 if (empty($orSeparatedChunks)) { 64 // All the parts joined by 'or' always evaluate to false => the whole formula always evaluates to false 65 return false; 66 } 67 68 return implode(' || ', $orSeparatedChunks); 69 } 70 71 /** 72 * Converts an atomic part of the CLDR formula to its gettext representation. 73 * 74 * @param string $cldrAtom the CLDR formula atom to convert 75 * 76 * @throws \Exception 77 * 78 * @return bool|string returns true if the gettext will always evaluate to true, false if gettext will always evaluate to false, return the gettext formula otherwise 79 */ 80 private static function convertAtom($cldrAtom) 81 { 82 $m = null; 83 $gettextAtom = $cldrAtom; 84 $gettextAtom = str_replace(' = ', ' == ', $gettextAtom); 85 $gettextAtom = str_replace('i', 'n', $gettextAtom); 86 if (preg_match('/^n( % \d+)? (!=|==) \d+$/', $gettextAtom)) { 87 return $gettextAtom; 88 } 89 if (preg_match('/^n( % \d+)? (!=|==) \d+(,\d+|\.\.\d+)+$/', $gettextAtom)) { 90 return self::expandAtom($gettextAtom); 91 } 92 if (preg_match('/^(?:v|w)(?: % 10+)? == (\d+)(?:\.\.\d+)?$/', $gettextAtom, $m)) { // For gettext: v == 0, w == 0 93 return (int) $m[1] === 0 ? true : false; 94 } 95 if (preg_match('/^(?:v|w)(?: % 10+)? != (\d+)(?:\.\.\d+)?$/', $gettextAtom, $m)) { // For gettext: v == 0, w == 0 96 return (int) $m[1] === 0 ? false : true; 97 } 98 if (preg_match('/^(?:f|t)(?: % 10+)? == (\d+)(?:\.\.\d+)?$/', $gettextAtom, $m)) { // f == empty, t == empty 99 return (int) $m[1] === 0 ? true : false; 100 } 101 if (preg_match('/^(?:f|t)(?: % 10+)? != (\d+)(?:\.\.\d+)?$/', $gettextAtom, $m)) { // f == empty, t == empty 102 return (int) $m[1] === 0 ? false : true; 103 } 104 throw new Exception("Unable to convert the formula chunk '${cldrAtom}' from CLDR to gettext"); 105 } 106 107 /** 108 * Expands an atom containing a range (for instance: 'n == 1,3..5'). 109 * 110 * @param string $atom 111 * 112 * @throws \Exception 113 * 114 * @return string 115 */ 116 private static function expandAtom($atom) 117 { 118 $m = null; 119 if (preg_match('/^(n(?: % \d+)?) (==|!=) (\d+(?:\.\.\d+|,\d+)+)$/', $atom, $m)) { 120 $what = $m[1]; 121 $op = $m[2]; 122 $chunks = array(); 123 foreach (explode(',', $m[3]) as $range) { 124 $chunk = null; 125 if ((!isset($chunk)) && preg_match('/^\d+$/', $range)) { 126 $chunk = "${what} ${op} ${range}"; 127 } 128 if ((!isset($chunk)) && preg_match('/^(\d+)\.\.(\d+)$/', $range, $m)) { 129 $from = (int) $m[1]; 130 $to = (int) $m[2]; 131 if (($to - $from) === 1) { 132 switch ($op) { 133 case '==': 134 $chunk = "(${what} == ${from} || ${what} == ${to})"; 135 break; 136 case '!=': 137 $chunk = "${what} != ${from} && ${what} == ${to}"; 138 break; 139 } 140 } else { 141 switch ($op) { 142 case '==': 143 $chunk = "${what} >= ${from} && ${what} <= ${to}"; 144 break; 145 case '!=': 146 if ($what === 'n' && $from <= 0) { 147 $chunk = "${what} > ${to}"; 148 } else { 149 $chunk = "(${what} < ${from} || ${what} > ${to})"; 150 } 151 break; 152 } 153 } 154 } 155 if (!isset($chunk)) { 156 throw new Exception("Unhandled range '${range}' in '${atom}'"); 157 } 158 $chunks[] = $chunk; 159 } 160 if (count($chunks) === 1) { 161 return $chunks[0]; 162 } 163 switch ($op) { 164 case '==': 165 return '(' . implode(' || ', $chunks) . ')'; break; 166 case '!=': 167 return implode(' && ', $chunks); 168 } 169 } 170 throw new Exception("Unable to expand '${atom}'"); 171 } 172} 173