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