1<?php
2
3namespace Punic;
4
5/**
6 * Units helper stuff.
7 */
8class Unit
9{
10    /**
11     * Get the list of all the available units.
12     *
13     * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data
14     */
15    public static function getAvailableUnits($locale = '')
16    {
17        $data = Data::get('units', $locale);
18        $categories = array();
19        foreach ($data as $width => $units) {
20            if ($width[0] !== '_') {
21                foreach ($units as $category => $units) {
22                    if ($category[0] !== '_') {
23                        $unitIDs = array_keys($units);
24                        if (isset($categories[$category])) {
25                            $categories[$category] = array_unique(array_merge($categories[$category], $unitIDs));
26                        } else {
27                            $categories[$category] = array_keys($units);
28                        }
29                    }
30                }
31            }
32        }
33        ksort($categories);
34        foreach (array_keys($categories) as $category) {
35            sort($categories[$category]);
36        }
37
38        return $categories;
39    }
40
41    /**
42     * Get the localized name of a unit.
43     *
44     * @param string $unit The unit identifier (eg 'duration/millisecond' or 'millisecond')
45     * @param string $width The format name; it can be 'long' ('milliseconds'), 'short' (eg 'millisecs') or 'narrow' (eg 'msec')
46     * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data
47     *
48     * @throws Exception\ValueNotInList
49     *
50     * @return string
51     */
52    public static function getName($unit, $width = 'short', $locale = '')
53    {
54        $data = self::getDataForWidth($width, $locale);
55        $unitData = self::getDataForUnit($data, $unit);
56
57        return $unitData['_name'];
58    }
59
60    /**
61     * Get the "per" localized format string of a unit.
62     *
63     * @param string $unit The unit identifier (eg 'duration/minute' or 'minute')
64     * @param string $width The format name; it can be 'long' ('%1$s per minute'), 'short' (eg '%1$s/min') or 'narrow' (eg '%1$s/min')
65     * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data
66     *
67     * @throws Exception\ValueNotInList
68     *
69     * @return string
70     */
71    public static function getPerFormat($unit, $width = 'short', $locale = '')
72    {
73        $data = self::getDataForWidth($width, $locale);
74        $unitData = self::getDataForUnit($data, $unit);
75
76        if (isset($unitData['_per'])) {
77            return $unitData['_per'];
78        }
79        $pluralRule = Plural::getRuleOfType(1, Plural::RULETYPE_CARDINAL, $locale);
80        $name = trim(sprintf($unitData[$pluralRule], ''));
81
82        return sprintf($data['_compoundPattern'], '%1$s', $name);
83    }
84
85    /**
86     * Format a unit string.
87     *
88     * @param int|float|string $number The unit amount
89     * @param string $unit The unit identifier (eg 'duration/millisecond' or 'millisecond')
90     * @param string $width The format name; it can be 'long' (eg '3 milliseconds'), 'short' (eg '3 ms') or 'narrow' (eg '3ms'). You can also add a precision specifier ('long,2' or just '2')
91     * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data
92     *
93     * @throws Exception\ValueNotInList
94     *
95     * @return string
96     */
97    public static function format($number, $unit, $width = 'short', $locale = '')
98    {
99        $precision = null;
100        $m = null;
101        if (is_int($width)) {
102            $precision = $width;
103            $width = 'short';
104        } elseif (is_string($width) && preg_match('/^(?:(.*),)?([+\\-]?\\d+)$/', $width, $m)) {
105            $precision = (int) $m[2];
106            $width = (string) $m[1];
107            if ($width === '') {
108                $width = 'short';
109            }
110        }
111        $data = self::getDataForWidth($width, $locale);
112        $rules = self::getDataForUnit($data, $unit);
113        $pluralRule = Plural::getRuleOfType($number, Plural::RULETYPE_CARDINAL, $locale);
114        //@codeCoverageIgnoreStart
115        // These checks aren't necessary since $pluralRule should always be in $rules, but they don't hurt ;)
116        if (!isset($rules[$pluralRule])) {
117            if (isset($rules['other'])) {
118                $pluralRule = 'other';
119            } else {
120                $availableRules = array_keys($rules);
121                $pluralRule = $availableRules[0];
122            }
123        }
124        //@codeCoverageIgnoreEnd
125        return sprintf($rules[$pluralRule], Number::format($number, $precision, $locale));
126    }
127
128    /**
129     * Retrieve the measurement systems and their localized names.
130     *
131     * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data
132     *
133     * @return array The array keys are the measurement system codes (eg 'metric', 'US', 'UK'), the values are the localized measurement system names (eg 'Metric', 'US', 'UK' for English)
134     */
135    public static function getMeasurementSystems($locale = '')
136    {
137        return Data::get('measurementSystemNames', $locale);
138    }
139
140    /**
141     * Retrieve the measurement system for a specific territory.
142     *
143     * @param string $territoryCode The territory code (eg. 'US' for 'United States of America').
144     *
145     * @return string Return the measurement system code (eg: 'metric') for the specified territory. If $territoryCode is not valid we'll return an empty string.
146     */
147    public static function getMeasurementSystemFor($territoryCode)
148    {
149        $result = '';
150        if (is_string($territoryCode) && preg_match('/^[a-z0-9]{2,3}$/i', $territoryCode)) {
151            $territoryCode = strtoupper($territoryCode);
152            $data = Data::getGeneric('measurementData');
153            while ($territoryCode !== '') {
154                if (isset($data['measurementSystem'][$territoryCode])) {
155                    $result = $data['measurementSystem'][$territoryCode];
156                    break;
157                }
158                $territoryCode = Territory::getParentTerritoryCode($territoryCode);
159            }
160        }
161
162        return $result;
163    }
164
165    /**
166     * Returns the list of countries that use a specific measurement system.
167     *
168     * @param string $measurementSystem The measurement system identifier ('metric', 'US' or 'UK')
169     *
170     * @return array The list of country IDs that use the specified measurement system (if $measurementSystem is invalid you'll get an empty array)
171     */
172    public static function getCountriesWithMeasurementSystem($measurementSystem)
173    {
174        $result = array();
175        if (is_string($measurementSystem) && $measurementSystem !== '') {
176            $someGroup = false;
177            $data = Data::getGeneric('measurementData');
178            foreach ($data['measurementSystem'] as $territory => $ms) {
179                if (strcasecmp($measurementSystem, $ms) === 0) {
180                    $children = Territory::getChildTerritoryCodes($territory, true);
181                    if (empty($children)) {
182                        $result[] = $territory;
183                    } else {
184                        $someGroup = true;
185                        $result = array_merge($result, $children);
186                    }
187                }
188            }
189            if ($someGroup) {
190                $otherCountries = array();
191                foreach ($data['measurementSystem'] as $territory => $ms) {
192                    if (($territory !== '001') && (strcasecmp($measurementSystem, $ms) !== 0)) {
193                        $children = Territory::getChildTerritoryCodes($territory, true);
194                        if (empty($children)) {
195                            $otherCountries[] = $territory;
196                        } else {
197                            $otherCountries = array_merge($otherCountries, $children);
198                        }
199                    }
200                }
201                $result = array_values(array_diff($result, $otherCountries));
202            }
203        }
204
205        return $result;
206    }
207
208    /**
209     * Retrieve the standard paper size for a specific territory.
210     *
211     * @param string $territoryCode The territory code (eg. 'US' for 'United States of America').
212     *
213     * @return string Return the standard paper size (eg: 'A4' or 'US-Letter') for the specified territory. If $territoryCode is not valid we'll return an empty string.
214     */
215    public static function getPaperSizeFor($territoryCode)
216    {
217        $result = '';
218        if (is_string($territoryCode) && preg_match('/^[a-z0-9]{2,3}$/i', $territoryCode)) {
219            $territoryCode = strtoupper($territoryCode);
220            $data = Data::getGeneric('measurementData');
221            while ($territoryCode !== '') {
222                if (isset($data['paperSize'][$territoryCode])) {
223                    $result = $data['paperSize'][$territoryCode];
224                    break;
225                }
226                $territoryCode = Territory::getParentTerritoryCode($territoryCode);
227            }
228        }
229
230        return $result;
231    }
232
233    /**
234     * Returns the list of countries that use a specific paper size by default.
235     *
236     * @param string $paperSize The paper size identifier ('A4' or 'US-Letter')
237     *
238     * @return array The list of country IDs that use the specified paper size (if $paperSize is invalid you'll get an empty array)
239     */
240    public static function getCountriesWithPaperSize($paperSize)
241    {
242        $result = array();
243        if (is_string($paperSize) && $paperSize !== '') {
244            $someGroup = false;
245            $data = Data::getGeneric('measurementData');
246            foreach ($data['paperSize'] as $territory => $ms) {
247                if (strcasecmp($paperSize, $ms) === 0) {
248                    $children = Territory::getChildTerritoryCodes($territory, true);
249                    if (empty($children)) {
250                        $result[] = $territory;
251                    } else {
252                        $someGroup = true;
253                        $result = array_merge($result, $children);
254                    }
255                }
256            }
257            if ($someGroup) {
258                $otherCountries = array();
259                foreach ($data['paperSize'] as $territory => $ms) {
260                    if (($territory !== '001') && (strcasecmp($paperSize, $ms) !== 0)) {
261                        $children = Territory::getChildTerritoryCodes($territory, true);
262                        if (empty($children)) {
263                            $otherCountries[] = $territory;
264                        } else {
265                            $otherCountries = array_merge($otherCountries, $children);
266                        }
267                    }
268                }
269                $result = array_values(array_diff($result, $otherCountries));
270            }
271        }
272
273        return $result;
274    }
275
276    /**
277     * Get the width-specific unit data.
278     *
279     * @param string $width the data width
280     * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data
281     *
282     * @throws Exception\ValueNotInList
283     *
284     * @return array
285     */
286    private static function getDataForWidth($width, $locale = '')
287    {
288        $data = Data::get('units', $locale);
289        if ($width[0] === '_' || !isset($data[$width])) {
290            $widths = array();
291            foreach (array_keys($data) as $w) {
292                if (strpos($w, '_') !== 0) {
293                    $widths[] = $w;
294                }
295            }
296            throw new Exception\ValueNotInList($width, $widths);
297        }
298
299        return $data[$width];
300    }
301
302    /**
303     * Get a unit-specific data.
304     *
305     * @param array $data the width-specific data
306     * @param string $unit The unit identifier (eg 'duration/millisecond' or 'millisecond')
307     *
308     * @throws Exception\ValueNotInList
309     *
310     * @return array
311     */
312    private static function getDataForUnit(array $data, $unit)
313    {
314        $chunks = explode('/', $unit, 2);
315        if (isset($chunks[1])) {
316            list($unitCategory, $unitID) = $chunks;
317        } else {
318            $unitCategory = null;
319            $unitID = null;
320            foreach (array_keys($data) as $c) {
321                if ($c[0] !== '_') {
322                    if (isset($data[$c][$unit])) {
323                        if ($unitCategory === null) {
324                            $unitCategory = $c;
325                            $unitID = $unit;
326                        } else {
327                            $unitCategory = null;
328                            break;
329                        }
330                    }
331                }
332            }
333        }
334        if (
335            $unitCategory === null || $unitCategory[0] === '_'
336            || !isset($data[$unitCategory])
337            || $unitID === null || $unitID[0] === '_'
338            || !isset($data[$unitCategory][$unitID])
339            ) {
340            $units = array();
341            foreach ($data as $c => $us) {
342                if (strpos($c, '_') === false) {
343                    foreach (array_keys($us) as $u) {
344                        if (strpos($c, '_') === false) {
345                            $units[] = "{$c}/{$u}";
346                        }
347                    }
348                }
349            }
350            throw new \Punic\Exception\ValueNotInList($unit, $units);
351        }
352
353        return $data[$unitCategory][$unitID];
354    }
355}
356